diff --git a/src/skillkit.ts b/src/skillkit.ts index a36d2fb..5871a29 100644 --- a/src/skillkit.ts +++ b/src/skillkit.ts @@ -96,7 +96,7 @@ function isCrafterSkillkit(binPath: string): boolean { } catch { return false; } } -function findSkillkitBin(): string | null { +export function findSkillkitBin(): string | null { const candidates: string[] = []; const searchDirs: string[] = []; if (IS_WIN) { diff --git a/tests/tool-configs.test.ts b/tests/tool-configs.test.ts index 62323f3..1e7f763 100644 --- a/tests/tool-configs.test.ts +++ b/tests/tool-configs.test.ts @@ -2,10 +2,23 @@ import { describe, expect, mock, test } from "bun:test"; import { join } from "path"; const HOME = "/home/agentfiles-test"; +const normalize = (path: string) => path.replaceAll("\\", "/"); let existsPredicate = (_path: string) => false; -const existsSync = mock((path: unknown) => - existsPredicate(String(path).replaceAll("\\", "/")), -); +let directoryEntries = new Map(); +let dynamicBinDirs = new Map(); + +const existsSync = mock((path: unknown) => existsPredicate(normalize(String(path)))); +const readdirSync = mock((path: unknown) => directoryEntries.get(normalize(String(path))) ?? []); +const execFileSync = mock((command: unknown, args: unknown[] = []) => { + const normalizedCommand = normalize(String(command)); + if (existsPredicate(normalizedCommand)) return "Analytics for AI agent skills\n"; + + const key = [String(command), ...args.map(String)].join(" "); + const binDir = dynamicBinDirs.get(key); + if (binDir) return `${binDir}\n`; + + throw new Error(`Unexpected command: ${key}`); +}); mock.module("os", () => ({ homedir: () => HOME, @@ -14,10 +27,22 @@ mock.module("os", () => ({ mock.module("fs", () => ({ existsSync, - readdirSync: () => [], + readdirSync, +})); + +mock.module("child_process", () => ({ + execFileSync, + execFile: mock(() => undefined), })); const toolConfigs = await import("../src/tool-configs"); +const skillkit = await import("../src/skillkit"); + +function resetDiscovery(): void { + existsPredicate = () => false; + directoryEntries = new Map(); + dynamicBinDirs = new Map(); +} test("finds CLIs installed by supported package managers", () => { for (const parts of [ @@ -66,3 +91,67 @@ describe("VS Code fork extension storage detection", () => { }); } }); + +const STATIC_PACKAGE_MANAGER_PATHS = [ + { manager: "Bun", parts: [".bun", "bin"] }, + { manager: "mise shims", parts: [".local", "share", "mise", "shims"] }, + { manager: "pnpm", parts: [".local", "share", "pnpm"] }, + { manager: "Volta", parts: [".volta", "bin"] }, + { manager: "Yarn classic", parts: [".yarn", "bin"] }, + { manager: "Yarn global", parts: [".config", "yarn", "global", "node_modules", ".bin"] }, + { manager: "fnm", parts: [".fnm", "aliases", "default", "bin"] }, + { manager: "asdf", parts: [".asdf", "shims"] }, + { manager: "proto", parts: [".proto", "bin"] }, +] as const; + +describe("findSkillkitBin package-manager discovery", () => { + for (const { manager, parts } of STATIC_PACKAGE_MANAGER_PATHS) { + test(`finds skillkit installed via ${manager}`, () => { + resetDiscovery(); + const expected = normalize(join(HOME, ...parts, "skillkit")); + existsPredicate = (path) => path === expected; + + expect(skillkit.findSkillkitBin()).toBe(expected); + }); + } + + test("keeps a case for every static package-manager directory", () => { + const expectedDirs = STATIC_PACKAGE_MANAGER_PATHS.map(({ parts }) => + normalize(join(HOME, ...parts)), + ); + + expect(skillkit.getPackageManagerBinDirs(HOME).map(normalize)).toEqual(expectedDirs); + }); + + for (const { manager, baseParts, version } of [ + { manager: "NVM", baseParts: [".nvm", "versions", "node"], version: "v22.0.0" }, + { manager: "mise Node", baseParts: [".local", "share", "mise", "installs", "node"], version: "22.0.0" }, + { manager: "mise Bun", baseParts: [".local", "share", "mise", "installs", "bun"], version: "1.2.0" }, + ] as const) { + test(`finds skillkit installed via ${manager} version directories`, () => { + resetDiscovery(); + const baseDir = normalize(join(HOME, ...baseParts)); + const expected = normalize(join(baseDir, version, "bin", "skillkit")); + directoryEntries.set(baseDir, [version]); + existsPredicate = (path) => path === expected; + + expect(skillkit.findSkillkitBin()).toBe(expected); + }); + } + + for (const { manager, command } of [ + { manager: "pnpm", command: "pnpm bin -g" }, + { manager: "Yarn", command: "yarn global bin" }, + { manager: "npm", command: "npm bin -g" }, + ] as const) { + test(`finds skillkit via the ${manager} dynamic fallback`, () => { + resetDiscovery(); + const binDir = normalize(join(HOME, "dynamic", manager.toLowerCase())); + const expected = normalize(join(binDir, "skillkit")); + dynamicBinDirs.set(command, binDir); + existsPredicate = (path) => path === expected; + + expect(skillkit.findSkillkitBin()).toBe(expected); + }); + } +});