fix(auto-slash-command): resolve project commands from session dir
Use the plugin session directory instead of process.cwd() when resolving project slash commands. This restores project and opencode-project slashcommand behavior when the runtime cwd differs from the actual session workspace. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -42,11 +42,12 @@ export interface ExecutorOptions {
|
|||||||
pluginsEnabled?: boolean
|
pluginsEnabled?: boolean
|
||||||
enabledPluginsOverride?: Record<string, boolean>
|
enabledPluginsOverride?: Record<string, boolean>
|
||||||
agent?: string
|
agent?: string
|
||||||
|
directory?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function discoverAllCommands(options?: ExecutorOptions): Promise<CommandInfo[]> {
|
async function discoverAllCommands(options?: ExecutorOptions): Promise<CommandInfo[]> {
|
||||||
const discoveredCommands = discoverCommandsSync(process.cwd(), {
|
const discoveredCommands = discoverCommandsSync(options?.directory ?? process.cwd(), {
|
||||||
pluginsEnabled: options?.pluginsEnabled,
|
pluginsEnabled: options?.pluginsEnabled,
|
||||||
enabledPluginsOverride: options?.enabledPluginsOverride,
|
enabledPluginsOverride: options?.enabledPluginsOverride,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export interface AutoSlashCommandHookOptions {
|
|||||||
skills?: LoadedSkill[]
|
skills?: LoadedSkill[]
|
||||||
pluginsEnabled?: boolean
|
pluginsEnabled?: boolean
|
||||||
enabledPluginsOverride?: Record<string, boolean>
|
enabledPluginsOverride?: Record<string, boolean>
|
||||||
|
directory?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAutoSlashCommandHook(options?: AutoSlashCommandHookOptions) {
|
export function createAutoSlashCommandHook(options?: AutoSlashCommandHookOptions) {
|
||||||
@@ -75,6 +76,7 @@ export function createAutoSlashCommandHook(options?: AutoSlashCommandHookOptions
|
|||||||
skills: options?.skills,
|
skills: options?.skills,
|
||||||
pluginsEnabled: options?.pluginsEnabled,
|
pluginsEnabled: options?.pluginsEnabled,
|
||||||
enabledPluginsOverride: options?.enabledPluginsOverride,
|
enabledPluginsOverride: options?.enabledPluginsOverride,
|
||||||
|
directory: options?.directory,
|
||||||
}
|
}
|
||||||
const sessionProcessedCommands = createProcessedCommandStore()
|
const sessionProcessedCommands = createProcessedCommandStore()
|
||||||
const sessionProcessedCommandExecutions = createProcessedCommandStore()
|
const sessionProcessedCommandExecutions = createProcessedCommandStore()
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { describe, expect, it, beforeEach, mock, spyOn } from "bun:test"
|
import { describe, expect, it, beforeEach, afterEach, spyOn } from "bun:test"
|
||||||
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
|
||||||
|
import { tmpdir } from "node:os"
|
||||||
|
import { join } from "node:path"
|
||||||
import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
|
import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
|
||||||
import type {
|
import type {
|
||||||
AutoSlashCommandHookInput,
|
AutoSlashCommandHookInput,
|
||||||
@@ -39,11 +42,45 @@ function createMockOutput(text: string): AutoSlashCommandHookOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("createAutoSlashCommandHook", () => {
|
describe("createAutoSlashCommandHook", () => {
|
||||||
|
let tempDir = ""
|
||||||
|
let originalWorkingDirectory = ""
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
logMock.mockClear()
|
logMock.mockClear()
|
||||||
|
tempDir = mkdtempSync(join(tmpdir(), "omo-auto-slash-hook-test-"))
|
||||||
|
originalWorkingDirectory = process.cwd()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.chdir(originalWorkingDirectory)
|
||||||
|
rmSync(tempDir, { recursive: true, force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("slash command replacement", () => {
|
describe("slash command replacement", () => {
|
||||||
|
it("should resolve project commands from provided directory even when cwd differs", async () => {
|
||||||
|
// given
|
||||||
|
const projectDir = join(tempDir, "project")
|
||||||
|
const commandDir = join(projectDir, ".claude", "commands")
|
||||||
|
mkdirSync(commandDir, { recursive: true })
|
||||||
|
writeFileSync(
|
||||||
|
join(commandDir, "project-only-command.md"),
|
||||||
|
`---\ndescription: Project command\n---\nExecute from project directory.\n`,
|
||||||
|
)
|
||||||
|
process.chdir("/tmp")
|
||||||
|
|
||||||
|
const hook = createAutoSlashCommandHook({ directory: projectDir })
|
||||||
|
const input = createMockInput(`test-session-project-${Date.now()}`)
|
||||||
|
const output = createMockOutput("/project-only-command")
|
||||||
|
|
||||||
|
// when
|
||||||
|
await hook["chat.message"](input, output)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(output.parts[0].text).toContain("<auto-slash-command>")
|
||||||
|
expect(output.parts[0].text).toContain("Execute from project directory.")
|
||||||
|
expect(output.parts[0].text).toContain("**Scope**: project")
|
||||||
|
})
|
||||||
|
|
||||||
it("should not modify message when command not found", async () => {
|
it("should not modify message when command not found", async () => {
|
||||||
// given a slash command that doesn't exist
|
// given a slash command that doesn't exist
|
||||||
const hook = createAutoSlashCommandHook()
|
const hook = createAutoSlashCommandHook()
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export function createSkillHooks(args: {
|
|||||||
skills: mergedSkills,
|
skills: mergedSkills,
|
||||||
pluginsEnabled: pluginConfig.claude_code?.plugins ?? true,
|
pluginsEnabled: pluginConfig.claude_code?.plugins ?? true,
|
||||||
enabledPluginsOverride: pluginConfig.claude_code?.plugins_override,
|
enabledPluginsOverride: pluginConfig.claude_code?.plugins_override,
|
||||||
|
directory: ctx.directory,
|
||||||
}))
|
}))
|
||||||
: null
|
: null
|
||||||
|
|
||||||
|
|||||||
@@ -60,4 +60,32 @@ describe("slashcommand discovery and execution compatibility", () => {
|
|||||||
expect(result.replacementText).toContain("Execute from parent config.")
|
expect(result.replacementText).toContain("Execute from parent config.")
|
||||||
expect(result.replacementText).toContain("**Scope**: opencode")
|
expect(result.replacementText).toContain("**Scope**: opencode")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("executes project commands using the provided directory even when cwd differs", async () => {
|
||||||
|
// given
|
||||||
|
const projectDir = join(tempDir, "project")
|
||||||
|
const commandDir = join(projectDir, ".claude", "commands")
|
||||||
|
const commandName = "project-only-command"
|
||||||
|
|
||||||
|
mkdirSync(commandDir, { recursive: true })
|
||||||
|
writeFileSync(
|
||||||
|
join(commandDir, `${commandName}.md`),
|
||||||
|
`---\ndescription: Project command\n---\nExecute from project directory.\n`,
|
||||||
|
)
|
||||||
|
process.chdir("/tmp")
|
||||||
|
|
||||||
|
expect(discoverCommandsSync(projectDir).some(command => command.name === commandName)).toBe(true)
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = await executeSlashCommand({
|
||||||
|
command: commandName,
|
||||||
|
args: "",
|
||||||
|
raw: `/${commandName}`,
|
||||||
|
}, { skills: [], directory: projectDir })
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(result.replacementText).toContain("Execute from project directory.")
|
||||||
|
expect(result.replacementText).toContain("**Scope**: project")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user