Merge branch 'fix/perf-d08' into fix/perf-omo-in-tree
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
import { promises as fs } from "fs"
|
||||||
|
import { resolve } from "path"
|
||||||
|
|
||||||
|
import type { CommandDefinition } from "./types"
|
||||||
|
|
||||||
|
const commandLoaderCache = new Map<string, Promise<Record<string, CommandDefinition>>>()
|
||||||
|
|
||||||
|
export async function getCommandLoaderCacheKey(directory?: string): Promise<string> {
|
||||||
|
const resolvedDirectory = resolve(directory ?? process.cwd())
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fs.realpath(resolvedDirectory)
|
||||||
|
} catch {
|
||||||
|
return resolvedDirectory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCachedCommands(
|
||||||
|
cacheKey: string,
|
||||||
|
): Promise<Record<string, CommandDefinition>> | undefined {
|
||||||
|
return commandLoaderCache.get(cacheKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCachedCommands(
|
||||||
|
cacheKey: string,
|
||||||
|
commands: Promise<Record<string, CommandDefinition>>,
|
||||||
|
): void {
|
||||||
|
commandLoaderCache.set(cacheKey, commands)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCachedCommands(cacheKey: string): void {
|
||||||
|
commandLoaderCache.delete(cacheKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCommandLoaderCache(): void {
|
||||||
|
commandLoaderCache.clear()
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { execFileSync } from "node:child_process"
|
import { execFileSync } from "node:child_process"
|
||||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test"
|
import { promises as fs } from "node:fs"
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"
|
||||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
|
||||||
import { tmpdir } from "node:os"
|
import { tmpdir } from "node:os"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
import { loadOpencodeGlobalCommands, loadOpencodeProjectCommands } from "./loader"
|
import * as loader from "./loader"
|
||||||
|
|
||||||
const TEST_DIR = join(tmpdir(), `claude-code-command-loader-${Date.now()}`)
|
const TEST_DIR = join(tmpdir(), `claude-code-command-loader-${Date.now()}`)
|
||||||
|
|
||||||
@@ -16,19 +17,41 @@ function writeCommand(directory: string, name: string, description: string): voi
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("claude-code command loader", () => {
|
describe("claude-code command loader", () => {
|
||||||
|
let originalClaudeConfigDir: string | undefined
|
||||||
let originalOpencodeConfigDir: string | undefined
|
let originalOpencodeConfigDir: string | undefined
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mkdirSync(TEST_DIR, { recursive: true })
|
mkdirSync(TEST_DIR, { recursive: true })
|
||||||
|
originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||||
originalOpencodeConfigDir = process.env.OPENCODE_CONFIG_DIR
|
originalOpencodeConfigDir = process.env.OPENCODE_CONFIG_DIR
|
||||||
|
|
||||||
|
const claudeConfigDir = join(TEST_DIR, "claude-config")
|
||||||
|
const opencodeConfigDir = join(TEST_DIR, "opencode-config")
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = claudeConfigDir
|
||||||
|
process.env.OPENCODE_CONFIG_DIR = opencodeConfigDir
|
||||||
|
|
||||||
|
if ("clearCommandLoaderCache" in loader && typeof loader.clearCommandLoaderCache === "function") {
|
||||||
|
loader.clearCommandLoaderCache()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
if (originalClaudeConfigDir === undefined) {
|
||||||
|
delete process.env.CLAUDE_CONFIG_DIR
|
||||||
|
} else {
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir
|
||||||
|
}
|
||||||
|
|
||||||
if (originalOpencodeConfigDir === undefined) {
|
if (originalOpencodeConfigDir === undefined) {
|
||||||
delete process.env.OPENCODE_CONFIG_DIR
|
delete process.env.OPENCODE_CONFIG_DIR
|
||||||
} else {
|
} else {
|
||||||
process.env.OPENCODE_CONFIG_DIR = originalOpencodeConfigDir
|
process.env.OPENCODE_CONFIG_DIR = originalOpencodeConfigDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("clearCommandLoaderCache" in loader && typeof loader.clearCommandLoaderCache === "function") {
|
||||||
|
loader.clearCommandLoaderCache()
|
||||||
|
}
|
||||||
|
|
||||||
rmSync(TEST_DIR, { recursive: true, force: true })
|
rmSync(TEST_DIR, { recursive: true, force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -39,7 +62,7 @@ describe("claude-code command loader", () => {
|
|||||||
writeCommand(join(projectDir, ".opencode", "commands"), "ancestor", "Ancestor command")
|
writeCommand(join(projectDir, ".opencode", "commands"), "ancestor", "Ancestor command")
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const commands = await loadOpencodeProjectCommands(childDir)
|
const commands = await loader.loadOpencodeProjectCommands(childDir)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(commands.ancestor?.description).toBe("(opencode-project) Ancestor command")
|
expect(commands.ancestor?.description).toBe("(opencode-project) Ancestor command")
|
||||||
@@ -50,7 +73,7 @@ describe("claude-code command loader", () => {
|
|||||||
writeCommand(join(TEST_DIR, ".opencode", "command"), "singular", "Singular command")
|
writeCommand(join(TEST_DIR, ".opencode", "command"), "singular", "Singular command")
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const commands = await loadOpencodeProjectCommands(TEST_DIR)
|
const commands = await loader.loadOpencodeProjectCommands(TEST_DIR)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(commands.singular?.description).toBe("(opencode-project) Singular command")
|
expect(commands.singular?.description).toBe("(opencode-project) Singular command")
|
||||||
@@ -66,7 +89,7 @@ describe("claude-code command loader", () => {
|
|||||||
writeCommand(projectDir, "duplicate", "Nearest command")
|
writeCommand(projectDir, "duplicate", "Nearest command")
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const commands = await loadOpencodeProjectCommands(childDir)
|
const commands = await loader.loadOpencodeProjectCommands(childDir)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(commands.duplicate?.description).toBe("(opencode-project) Nearest command")
|
expect(commands.duplicate?.description).toBe("(opencode-project) Nearest command")
|
||||||
@@ -79,7 +102,7 @@ describe("claude-code command loader", () => {
|
|||||||
writeCommand(join(opencodeConfigDir, "commands"), "global-plural", "Global plural command")
|
writeCommand(join(opencodeConfigDir, "commands"), "global-plural", "Global plural command")
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const commands = await loadOpencodeGlobalCommands()
|
const commands = await loader.loadOpencodeGlobalCommands()
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(commands["global-plural"]?.description).toBe("(opencode) Global plural command")
|
expect(commands["global-plural"]?.description).toBe("(opencode) Global plural command")
|
||||||
@@ -94,7 +117,7 @@ describe("claude-code command loader", () => {
|
|||||||
writeCommand(join(profileConfigDir, "commands"), "duplicate-global", "Profile global command")
|
writeCommand(join(profileConfigDir, "commands"), "duplicate-global", "Profile global command")
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const commands = await loadOpencodeGlobalCommands()
|
const commands = await loader.loadOpencodeGlobalCommands()
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(commands["duplicate-global"]?.description).toBe("(opencode) Profile global command")
|
expect(commands["duplicate-global"]?.description).toBe("(opencode) Profile global command")
|
||||||
@@ -114,7 +137,7 @@ describe("claude-code command loader", () => {
|
|||||||
writeCommand(join(TEST_DIR, ".opencode", "commands"), "outside", "Outside command")
|
writeCommand(join(TEST_DIR, ".opencode", "commands"), "outside", "Outside command")
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const commands = await loadOpencodeProjectCommands(nestedDirectory)
|
const commands = await loader.loadOpencodeProjectCommands(nestedDirectory)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(commands["deploy/staging"]?.description).toBe("(opencode-project) Deploy staging")
|
expect(commands["deploy/staging"]?.description).toBe("(opencode-project) Deploy staging")
|
||||||
@@ -122,4 +145,38 @@ describe("claude-code command loader", () => {
|
|||||||
expect(commands.outside).toBeUndefined()
|
expect(commands.outside).toBeUndefined()
|
||||||
expect(commands["deploy:staging"]).toBeUndefined()
|
expect(commands["deploy:staging"]).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("#given commands nested under an excluded basename #when loadProjectCommands is called #then it skips the excluded directory contents", async () => {
|
||||||
|
// given
|
||||||
|
writeCommand(join(TEST_DIR, ".claude", "commands"), "real", "Real command")
|
||||||
|
writeCommand(
|
||||||
|
join(TEST_DIR, ".claude", "commands", "node_modules"),
|
||||||
|
"fake",
|
||||||
|
"Fake command",
|
||||||
|
)
|
||||||
|
|
||||||
|
// when
|
||||||
|
const commands = await loader.loadProjectCommands(TEST_DIR)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(commands.real?.description).toBe("(project) Real command")
|
||||||
|
expect(commands.fake).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("#given a previously loaded directory #when loadAllCommands is called twice #then the second call reuses the cached result without readdir calls", async () => {
|
||||||
|
// given
|
||||||
|
writeCommand(join(TEST_DIR, ".claude", "commands"), "cached", "Cached command")
|
||||||
|
const readdirSpy = spyOn(fs, "readdir")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const firstCommands = await loader.loadAllCommands(TEST_DIR)
|
||||||
|
const firstReaddirCount = readdirSpy.mock.calls.length
|
||||||
|
const secondCommands = await loader.loadAllCommands(TEST_DIR)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(firstCommands.cached?.description).toBe("(project) Cached command")
|
||||||
|
expect(secondCommands).toEqual(firstCommands)
|
||||||
|
expect(firstReaddirCount).toBeGreaterThan(0)
|
||||||
|
expect(readdirSpy.mock.calls.length).toBe(firstReaddirCount)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,13 +4,23 @@ import { parseFrontmatter } from "../../shared/frontmatter"
|
|||||||
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
||||||
import { isMarkdownFile } from "../../shared/file-utils"
|
import { isMarkdownFile } from "../../shared/file-utils"
|
||||||
import {
|
import {
|
||||||
|
EXCLUDED_DIRS,
|
||||||
findProjectOpencodeCommandDirs,
|
findProjectOpencodeCommandDirs,
|
||||||
getClaudeConfigDir,
|
getClaudeConfigDir,
|
||||||
getOpenCodeCommandDirs,
|
getOpenCodeCommandDirs,
|
||||||
} from "../../shared"
|
} from "../../shared"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
|
import {
|
||||||
|
clearCommandLoaderCache,
|
||||||
|
deleteCachedCommands,
|
||||||
|
getCachedCommands,
|
||||||
|
getCommandLoaderCacheKey,
|
||||||
|
setCachedCommands,
|
||||||
|
} from "./loader-cache"
|
||||||
import type { CommandScope, CommandDefinition, CommandFrontmatter, LoadedCommand } from "./types"
|
import type { CommandScope, CommandDefinition, CommandFrontmatter, LoadedCommand } from "./types"
|
||||||
|
|
||||||
|
export { clearCommandLoaderCache }
|
||||||
|
|
||||||
async function loadCommandsFromDir(
|
async function loadCommandsFromDir(
|
||||||
commandsDir: string,
|
commandsDir: string,
|
||||||
scope: CommandScope,
|
scope: CommandScope,
|
||||||
@@ -48,6 +58,7 @@ async function loadCommandsFromDir(
|
|||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
|
if (EXCLUDED_DIRS.has(entry.name)) continue
|
||||||
if (entry.name.startsWith(".")) continue
|
if (entry.name.startsWith(".")) continue
|
||||||
const subDirPath = join(commandsDir, entry.name)
|
const subDirPath = join(commandsDir, entry.name)
|
||||||
const subPrefix = prefix ? `${prefix}/${entry.name}` : entry.name
|
const subPrefix = prefix ? `${prefix}/${entry.name}` : entry.name
|
||||||
@@ -159,11 +170,26 @@ export async function loadOpencodeProjectCommands(directory?: string): Promise<R
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function loadAllCommands(directory?: string): Promise<Record<string, CommandDefinition>> {
|
export async function loadAllCommands(directory?: string): Promise<Record<string, CommandDefinition>> {
|
||||||
const [user, project, global, projectOpencode] = await Promise.all([
|
const cacheKey = await getCommandLoaderCacheKey(directory)
|
||||||
|
const cachedCommands = getCachedCommands(cacheKey)
|
||||||
|
if (cachedCommands) {
|
||||||
|
return cachedCommands
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCommandsPromise = Promise.all([
|
||||||
loadUserCommands(),
|
loadUserCommands(),
|
||||||
loadProjectCommands(directory),
|
loadProjectCommands(directory),
|
||||||
loadOpencodeGlobalCommands(),
|
loadOpencodeGlobalCommands(),
|
||||||
loadOpencodeProjectCommands(directory),
|
loadOpencodeProjectCommands(directory),
|
||||||
])
|
])
|
||||||
return { ...projectOpencode, ...global, ...project, ...user }
|
.then(([user, project, global, projectOpencode]) => {
|
||||||
|
return { ...projectOpencode, ...global, ...project, ...user }
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
deleteCachedCommands(cacheKey)
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
|
||||||
|
setCachedCommands(cacheKey, loadCommandsPromise)
|
||||||
|
return loadCommandsPromise
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user