Skip to content
Open
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
29 changes: 16 additions & 13 deletions packages/cli/src/commands/skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,18 @@ vi.mock("../utils/skillsUpdateCheck.js", () => ({

// The global install command this CLI runs (after `skills add <url>` and the
// per-name `--skill` selection).
const GLOBAL_ARGS_TAIL = [
"--global",
"--agent",
"claude-code",
"universal",
"--copy",
"--full-depth",
"--yes",
] as const;
// --copy only on Windows; elsewhere upstream links the Claude store to the universal one.
function globalArgsTail(platform: NodeJS.Platform): string[] {
return [
"--global",
"--agent",
"claude-code",
"universal",
...(platform === "win32" ? ["--copy"] : []),
"--full-depth",
"--yes",
];
}

function setPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, "platform", {
Expand Down Expand Up @@ -245,7 +248,7 @@ describe("hyperframes skills", () => {
"hyperframes-core",
"--skill",
"pr-to-video",
...GLOBAL_ARGS_TAIL,
...globalArgsTail("linux"),
],
],
[
Expand All @@ -262,7 +265,7 @@ describe("hyperframes skills", () => {
"hyperframes-core",
"--skill",
"pr-to-video",
...GLOBAL_ARGS_TAIL,
...globalArgsTail("darwin"),
],
],
[
Expand All @@ -283,7 +286,7 @@ describe("hyperframes skills", () => {
"hyperframes-core",
"--skill",
"pr-to-video",
...GLOBAL_ARGS_TAIL,
...globalArgsTail("win32"),
],
],
] as const)(
Expand Down Expand Up @@ -319,7 +322,7 @@ describe("hyperframes skills", () => {
// straight from GitHub, globally, as a faithful clone
expect(args).toContain("https://github.com/heygen-com/hyperframes");
expect(args).toContain("--global");
expect(args).toContain("--copy");
expect(args).not.toContain("--copy");
expect(args).toContain("--full-depth");
// targeted per-name selection: the stale core skills only
expect(skillFlagValues(args).sort()).toEqual(["hyperframes", "hyperframes-core"]);
Expand Down
50 changes: 25 additions & 25 deletions packages/cli/src/commands/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ function spawnNpx(args: string[], opts: { cwd?: string } = {}): Promise<void> {
// interactive path too (the user still sees the output).
stdio: ["inherit", 2, 2],
// We install with --full-depth (a full `git clone` of the repo, the only
// path that bypasses the laggy skills.sh blob — see GLOBAL_INSTALL_ARGS_TAIL),
// path that bypasses the laggy skills.sh blob — see globalInstallArgsTail),
// which is heavier than the blob fetch, so allow more headroom.
timeout: 300_000,
cwd: opts.cwd,
Expand Down Expand Up @@ -94,29 +94,29 @@ function spawnNpx(args: string[], opts: { cwd?: string } = {}): Promise<void> {
});
}

// One faithful global install: --copy lands real files in Claude Code's global
// store (~/.claude/skills, which Claude Code reads at global priority) plus the
// shared universal store (~/.agents/skills). mirrorGlobalSkills then fans that
// store out to every OTHER installed agent's global dir. Skills are
// framework-general knowledge, so installing once globally beats copying a full
// set into every project — and avoids the ~70-agent `--all` spray entirely.
// One global install: the universal store (~/.agents/skills) plus Claude Code's
// global store (~/.claude/skills, read at global priority); mirrorGlobalSkills
// fans it out to every OTHER installed agent's global dir. Installing once
// globally beats copying a set into every project and avoids the `--all` spray.
//
// Why --copy: real files (not the upstream symlink default, which re-serialises
// each SKILL.md's frontmatter) so an installed bundle byte-matches the published
// manifest and `skills check` reads it as current. Why --full-depth: it forces a
// full `git clone` of HEAD; without it even a full-URL `skills add` fetches the
// skills.sh registry blob, which lags GitHub main by hours, so a fresh install
// would read as several skills "outdated" (verified: blob → ~9 outdated;
// --full-depth → all current).
const GLOBAL_INSTALL_ARGS_TAIL = [
"--global",
"--agent",
"claude-code",
"universal",
"--copy",
"--full-depth",
"--yes",
];
// On Unix, upstream's symlink mode keeps one copy: real files in the universal
// store, a relative link per skill in the Claude store, still byte-matching the
// manifest (upstream rewrites SKILL.md only for `eve`). Windows symlinks need
// admin or Developer Mode, so --copy writes real files to both stores there.
// --full-depth forces a full `git clone` of HEAD; without it `skills add`
// fetches the skills.sh registry blob, which lags GitHub main by hours, so a
// fresh install would read as several skills "outdated".
function globalInstallArgsTail(platform: NodeJS.Platform = process.platform): string[] {
return [
"--global",
"--agent",
"claude-code",
"universal",
...(platform === "win32" ? ["--copy"] : []),
"--full-depth",
"--yes",
];
}

/** All skills, or an explicit list of skill names to install. */
type SkillSelection = "*" | readonly string[];
Expand All @@ -134,7 +134,7 @@ function runSkillsAdd(
opts: { cwd?: string } = {},
): Promise<void> {
return spawnNpx(
["skills", "add", source, ...skillSelectionArgs(selection), ...GLOBAL_INSTALL_ARGS_TAIL],
["skills", "add", source, ...skillSelectionArgs(selection), ...globalInstallArgsTail()],
opts,
);
}
Expand All @@ -161,7 +161,7 @@ function runSkillsRemove(names: string[], opts: { global: boolean }): Promise<vo
}

// Use the full GitHub URL (not the `owner/repo` slug) as the clone source. The
// freshness comes from --full-depth (see GLOBAL_INSTALL_ARGS_TAIL), which clones the
// freshness comes from --full-depth (see globalInstallArgsTail), which clones the
// repo at latest `main`; the URL just names what to clone. Our freshness check
// resolves "latest" straight from GitHub too, so install and check agree.
const SOURCES = [{ name: "HyperFrames", url: "https://github.com/heygen-com/hyperframes" }];
Expand Down
18 changes: 9 additions & 9 deletions packages/cli/src/utils/skillsMirror.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Fan the canonical global skills store out to every OTHER installed agent.
//
// `skills add --global --agent claude-code universal --copy` writes REAL files
// to two global stores: the Claude store (~/.claude/skills — what Claude Code
// reads, at global priority) and the shared universal store (~/.agents/skills,
// which Cursor/… read in PROJECT scope and Codex, Pi and the .agents-family
// agents read globally). But every other agent reads its OWN global dir (~/.cursor/skills,
// goose → ~/.config/goose/skills, …), which upstream's --global does NOT
// populate.
// `skills add --global --agent claude-code universal` fills two global stores:
// the Claude store (~/.claude/skills — what Claude Code reads, at global
// priority; on Unix each entry links into the universal store) and the shared
// universal store (~/.agents/skills, which Cursor/… read in PROJECT scope and
// Codex, Pi and the .agents-family agents read globally). But every other agent
// reads its OWN global dir (~/.cursor/skills, goose → ~/.config/goose/skills, …),
// which upstream's --global does NOT populate.
//
// So we mirror the canonical Claude store into each of those per-agent dirs, but
// only for agents the machine actually has (their marker dir exists). Agents
Expand Down Expand Up @@ -280,8 +280,8 @@ export function mirrorGlobalSkills(opts: {
const platform = opts.platform ?? process.platform;
const bases = resolveBases(home, opts.env ?? process.env);

// The two stores the global --copy install writes as real files. The mirror
// reads from the Claude store and must never link/copy onto either of them.
// The two stores the global install owns. The mirror reads from the Claude
// store and must never link/copy onto either of them.
const source = join(bases.claudeHome, "skills");
const universalStore = join(home, ".agents", "skills");
if (!existsSync(source)) return { source: null, mirrored: [], skipped: [] };
Expand Down