Skip to content
Prev Previous commit
Next Next commit
Fix path resolution for user vs project commands
- Add app context to CommandResolver constructor
- Implement getBaseDirectory() to determine correct base path based on scope
- User commands now resolve files relative to global config directory
- Project commands resolve files relative to project root
- Fallback resolution: first try relative to command file, then base directory
- Addresses reviewer concern about path resolution for different scopes
  • Loading branch information
ezynda3 committed Jul 25, 2025
commit e82dab4aeed615546657433796fd05bc16f58f28
44 changes: 40 additions & 4 deletions packages/opencode/src/command/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as path from "path"
import type { CommandExecutionContext } from "./types"
import { File } from "../file"
import { Global } from "../global"
import { App } from "../app/app"

export class CommandResolver {
constructor() {}
constructor(private app?: App.Info) {}

async resolve(content: string, context: CommandExecutionContext): Promise<string> {
// Validate argument count if specified
Expand Down Expand Up @@ -49,6 +51,20 @@ export class CommandResolver {
}
}

private getBaseDirectory(context: CommandExecutionContext): string {
// For user (global) commands, file references should be relative to the user's config directory
// For project commands, file references should be relative to the project root

if (context.command.scope === "user") {
// Use the global config directory as base for user commands
return Global.Path.config
} else {
// Use the project root for project commands (if available)
// Fall back to working directory if app context is not available
return this.app?.path.root || context.workingDirectory
}
}

private async resolveFileReferences(content: string, context: CommandExecutionContext): Promise<string> {
// Match @{file/path} pattern
const fileRegex = /@\{([^}]+)\}/g
Expand All @@ -58,9 +74,29 @@ export class CommandResolver {
const [fullMatch, filePath] = match

try {
// Resolve relative to command file directory
const commandDir = path.dirname(context.command.path)
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(commandDir, filePath)
let absolutePath: string

if (path.isAbsolute(filePath)) {
// If absolute path, use as-is
absolutePath = filePath
} else {
// For relative paths, we have two resolution strategies:
// 1. First try relative to the command file itself
// 2. If that fails, try relative to the base directory

const commandDir = path.dirname(context.command.path)
const pathRelativeToCommand = path.resolve(commandDir, filePath)

// Check if file exists relative to command
try {
await File.read(pathRelativeToCommand)
absolutePath = pathRelativeToCommand
} catch {
// If not found relative to command, try base directory
const baseDir = this.getBaseDirectory(context)
absolutePath = path.resolve(baseDir, filePath)
}
}

const fileContent = await File.read(absolutePath)

Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -872,13 +872,14 @@ export namespace Server {

// Resolve the command content with arguments
const { CommandResolver } = await import("../command/resolver")
const resolver = new CommandResolver()
const appInfo = App.info()
const resolver = new CommandResolver(appInfo)
const context = {
command,
arguments: args || "",
sessionId: "", // Not needed for resolution
messageId: "", // Not needed for resolution
workingDirectory: App.info().path.cwd,
workingDirectory: appInfo.path.cwd,
}

try {
Expand Down