fix(command-discovery): skip non-directory .claude/commands path
When .claude/commands exists as a file instead of a directory, readdirSync throws ENOTDIR and crashes command discovery, stalling OMO initialization. Add statSync().isDirectory() guard with a warning log. Fixes #3010
This commit is contained in:
@@ -267,3 +267,52 @@ Use nested command.
|
|||||||
expect(startWorkCommand?.metadata.agent).toBe("atlas")
|
expect(startWorkCommand?.metadata.agent).toBe("atlas")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("non-directory commands path", () => {
|
||||||
|
let testDir: string
|
||||||
|
let savedEnv: Record<string, string | undefined>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testDir = mkdtempSync(join(tmpdir(), "omo-cmd-file-"))
|
||||||
|
savedEnv = {
|
||||||
|
CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR,
|
||||||
|
OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR,
|
||||||
|
}
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = join(testDir, "claude-config")
|
||||||
|
process.env.OPENCODE_CONFIG_DIR = join(testDir, "opencode-config")
|
||||||
|
mkdirSync(join(testDir, "claude-config"), { recursive: true })
|
||||||
|
mkdirSync(join(testDir, "opencode-config"), { recursive: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
Object.entries(savedEnv).forEach(([k, v]) => {
|
||||||
|
if (v === undefined) delete process.env[k]
|
||||||
|
else process.env[k] = v
|
||||||
|
})
|
||||||
|
rmSync(testDir, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("#given .claude/commands is a file #when discoverCommandsSync runs #then returns without crashing", () => {
|
||||||
|
const projectDir = join(testDir, "project")
|
||||||
|
mkdirSync(join(projectDir, ".claude"), { recursive: true })
|
||||||
|
writeFileSync(join(projectDir, ".claude", "commands"), "") // file, not directory
|
||||||
|
|
||||||
|
// Should not throw
|
||||||
|
const commands = discoverCommandsSync(projectDir)
|
||||||
|
expect(commands).toBeInstanceOf(Array)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("#given .claude/commands is a directory #when discoverCommandsSync runs #then discovers commands normally", () => {
|
||||||
|
const projectDir = join(testDir, "project")
|
||||||
|
mkdirSync(join(projectDir, ".claude", "commands"), { recursive: true })
|
||||||
|
writeFileSync(
|
||||||
|
join(projectDir, ".claude", "commands", "test-cmd.md"),
|
||||||
|
"---\ndescription: Test\n---\nTest command content.\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
const commands = discoverCommandsSync(projectDir)
|
||||||
|
const testCmd = commands.find((c) => c.name === "test-cmd")
|
||||||
|
expect(testCmd).toBeDefined()
|
||||||
|
expect(testCmd?.content).toContain("Test command content.")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { existsSync, readdirSync, readFileSync } from "fs"
|
import { existsSync, readdirSync, readFileSync, statSync } from "fs"
|
||||||
import { basename, join } from "path"
|
import { basename, join } from "path"
|
||||||
import {
|
import {
|
||||||
parseFrontmatter,
|
parseFrontmatter,
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "../../shared"
|
} from "../../shared"
|
||||||
import type { CommandFrontmatter } from "../../features/claude-code-command-loader/types"
|
import type { CommandFrontmatter } from "../../features/claude-code-command-loader/types"
|
||||||
import { isMarkdownFile } from "../../shared/file-utils"
|
import { isMarkdownFile } from "../../shared/file-utils"
|
||||||
import { getClaudeConfigDir } from "../../shared"
|
import { getClaudeConfigDir, log } from "../../shared"
|
||||||
import { loadBuiltinCommands } from "../../features/builtin-commands"
|
import { loadBuiltinCommands } from "../../features/builtin-commands"
|
||||||
import type { CommandInfo, CommandMetadata, CommandScope } from "./types"
|
import type { CommandInfo, CommandMetadata, CommandScope } from "./types"
|
||||||
|
|
||||||
@@ -26,6 +26,10 @@ function discoverCommandsFromDir(
|
|||||||
prefix = "",
|
prefix = "",
|
||||||
): CommandInfo[] {
|
): CommandInfo[] {
|
||||||
if (!existsSync(commandsDir)) return []
|
if (!existsSync(commandsDir)) return []
|
||||||
|
if (!statSync(commandsDir).isDirectory()) {
|
||||||
|
log(`[command-discovery] Skipping non-directory path: ${commandsDir}`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const entries = readdirSync(commandsDir, { withFileTypes: true })
|
const entries = readdirSync(commandsDir, { withFileTypes: true })
|
||||||
const commands: CommandInfo[] = []
|
const commands: CommandInfo[] = []
|
||||||
|
|||||||
Reference in New Issue
Block a user