mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
`hyperframes skills`, `skills update`, and `init` all shelled out to
`skills add ... --all`, which the upstream CLI expands to
`--skill '*' --agent '*' -y` — every skill into every one of the ~70 agent
conventions it knows about. A user who only runs Claude Code got skill
folders for Cursor, Codex, and 50+ tools they never touch.
Replace `--all` with a resolved target set (new resolveAgentTargets):
1. If the project already has agent skill folders (`.claude/skills`,
`.hermes/skills`, …), install ONLY to those — an existing folder is the
strongest signal of intent, so honour it exactly.
2. Otherwise (blank project):
a. Running under Claude Code (CLAUDECODE) → just claude-code.
b. Else probe PATH for installed agent CLIs (claude, hermes, droid,
cursor, codex, opencode, gemini) — the gstack approach.
c. Else fall back to claude-code + the shared `.agents` universal dir,
which Cursor, Codex, OpenCode, Gemini, Copilot and ~14 others read
from in project scope. Never `--agent '*'`.
Installs stay `--skill '*'` (all skills) + `--copy` (faithful, detectable by
`skills check`); only the agent fan-out is scoped. The dir<->key map is
explicit because dir names differ from upstream keys (`.factory`/droid,
`.hermes`/hermes-agent) and many agents share `.agents` (-> the single
`universal` key). Keys verified against vercel-labs/skills@v1.5.13.
Verified end-to-end in an isolated project + HOME: the new args land exactly
`.claude/skills` (19) + `.agents/skills` (19) and nothing else — 2 folders,
not 50+. Pure resolver covered by skillsTargets.test.ts; command-arg shape and
the "never --all" regression pinned in skills.test.ts.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
131 lines
4.9 KiB
TypeScript
131 lines
4.9 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { buildSkillsAddArgs, resolveAgentTargets } from "./skillsTargets.js";
|
|
|
|
const tmpDirs: string[] = [];
|
|
|
|
function tempDir(prefix: string): string {
|
|
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
tmpDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
/** A project root containing the given `<host>/skills` folders. */
|
|
function projectWith(...hostDirs: string[]): string {
|
|
const root = tempDir("hf-targets-proj-");
|
|
for (const host of hostDirs) mkdirSync(join(root, host, "skills"), { recursive: true });
|
|
return root;
|
|
}
|
|
|
|
/** A PATH-style string pointing at a dir that contains the given fake executables. */
|
|
function pathWith(...bins: string[]): string {
|
|
const dir = tempDir("hf-targets-bin-");
|
|
for (const bin of bins) writeFileSync(join(dir, bin), "");
|
|
return dir;
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const dir of tmpDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("resolveAgentTargets", () => {
|
|
const blank = { env: {}, pathStr: "", platform: "linux" as const };
|
|
|
|
// ── 1. Existing project folders win, mapped dir → upstream key ──────────────
|
|
|
|
it("honours an existing `.hermes/skills` folder, nothing else", () => {
|
|
const result = resolveAgentTargets({ ...blank, cwd: projectWith(".hermes") });
|
|
expect(result.agents).toEqual(["hermes-agent"]);
|
|
});
|
|
|
|
it("maps `.factory` → droid and `.kiro` → kiro-cli (dir names differ from keys)", () => {
|
|
const result = resolveAgentTargets({ ...blank, cwd: projectWith(".factory", ".kiro") });
|
|
expect(result.agents).toEqual(["droid", "kiro-cli"]);
|
|
});
|
|
|
|
it("maps the shared `.agents` dir to the single `universal` key", () => {
|
|
const result = resolveAgentTargets({ ...blank, cwd: projectWith(".agents") });
|
|
expect(result.agents).toEqual(["universal"]);
|
|
});
|
|
|
|
it("returns claude-code first across multiple existing folders", () => {
|
|
const result = resolveAgentTargets({ ...blank, cwd: projectWith(".agents", ".claude") });
|
|
expect(result.agents).toEqual(["claude-code", "universal"]);
|
|
});
|
|
|
|
it("existing folders take precedence over CLAUDECODE and PATH", () => {
|
|
const result = resolveAgentTargets({
|
|
cwd: projectWith(".hermes"),
|
|
env: { CLAUDECODE: "1" },
|
|
pathStr: pathWith("claude", "cursor"),
|
|
platform: "linux",
|
|
});
|
|
expect(result.agents).toEqual(["hermes-agent"]);
|
|
});
|
|
|
|
// ── 2a. Claude Code env on a blank project ──────────────────────────────────
|
|
|
|
it("targets just claude-code when running under Claude Code", () => {
|
|
const result = resolveAgentTargets({
|
|
...blank,
|
|
cwd: projectWith(),
|
|
env: { CLAUDECODE: "1" },
|
|
});
|
|
expect(result.agents).toEqual(["claude-code"]);
|
|
});
|
|
|
|
// ── 2b. gstack route: installed agent CLIs on PATH ──────────────────────────
|
|
|
|
it("detects installed agent CLIs on PATH (blank project, no CLAUDECODE)", () => {
|
|
const result = resolveAgentTargets({
|
|
cwd: projectWith(),
|
|
env: {},
|
|
pathStr: pathWith("claude", "hermes"),
|
|
platform: "linux",
|
|
});
|
|
expect(result.agents).toEqual(["claude-code", "hermes-agent"]);
|
|
});
|
|
|
|
it("collapses universal-bucket CLIs (cursor/codex/…) to a single `universal`", () => {
|
|
const result = resolveAgentTargets({
|
|
cwd: projectWith(),
|
|
env: {},
|
|
pathStr: pathWith("cursor", "codex", "gemini"),
|
|
platform: "linux",
|
|
});
|
|
expect(result.agents).toEqual(["universal"]);
|
|
});
|
|
|
|
// ── 2c. Floor ───────────────────────────────────────────────────────────────
|
|
|
|
it("falls back to claude-code + universal (.claude + .agents) when nothing is found", () => {
|
|
const result = resolveAgentTargets({ ...blank, cwd: projectWith() });
|
|
expect(result.agents).toEqual(["claude-code", "universal"]);
|
|
});
|
|
|
|
// ── Invariant: never the `--all` spray ──────────────────────────────────────
|
|
|
|
it("never returns the `'*'` wildcard agent", () => {
|
|
for (const cwd of [projectWith(), projectWith(".hermes"), projectWith(".claude")]) {
|
|
const result = resolveAgentTargets({ ...blank, cwd });
|
|
expect(result.agents).not.toContain("*");
|
|
expect(result.agents.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("buildSkillsAddArgs", () => {
|
|
it("installs every skill to the given agents, non-interactive — not `--all`", () => {
|
|
expect(buildSkillsAddArgs(["claude-code", "universal"])).toEqual([
|
|
"--skill",
|
|
"*",
|
|
"--agent",
|
|
"claude-code",
|
|
"universal",
|
|
"--yes",
|
|
]);
|
|
});
|
|
});
|