Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/skillkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
97 changes: 93 additions & 4 deletions tests/tool-configs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string[]>();
let dynamicBinDirs = new Map<string, string>();

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,
Expand All @@ -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 [
Expand Down Expand Up @@ -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);
});
}
});