Skip to content
Merged
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ HyperFrames ships 7 skills. Make any video by asking for it; the `/hyperframes`

For standalone installation, default to the **core set** (`/hyperframes`, `/hyperframes-cli`, `/hyperframes-registry`, `/media-use`). `npx hyperframes skills update` installs exactly that from anywhere; the interactive picker (`npx skills add heygen-com/hyperframes`) lists it as the "Core Skills" group, nothing pre-selected. The picker is interactive-only — a non-interactive or agent run without `--skill` installs all 7. Use `npx skills add heygen-com/hyperframes --all` to install the complete published set deliberately (skips the picker), or `npx skills add heygen-com/hyperframes --skill <name>` for just one (bare name, no leading `/`).

Standalone installs stay lean after that: `npx hyperframes init` keeps the **core set** fresh (plus whatever is already installed) and never expands a partial install; `/figma`, `/slideshow` and `/remotion-to-hyperframes` install **on demand** with `npx hyperframes skills update <name>`. Nothing re-pulls the full set behind your back.
Standalone installs stay lean after that: `npx hyperframes init` installs the **core set** only when it is missing and never touches a skill you already have (`npx hyperframes skills update` refreshes them, and never removes one); `/figma`, `/slideshow` and `/remotion-to-hyperframes` install **on demand** with `npx hyperframes skills update <name>`. Nothing re-pulls the full set behind your back.

### Plugin packages

Expand Down
11 changes: 6 additions & 5 deletions docs/packages/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ npx hyperframes init my-video --tailwind
| `--keep-original` | Copy the `--video` file as is instead of converting a non-web codec to H.264 MP4 |
| `--model` | Whisper model for that transcription (for example `small.en`, `medium.en`, `large`) |
| `--language` | Language code for transcription (`en`, `es`, `ja`, …). Filters out speech in other languages. |
| `--skip-skills` | Currently ignored. Set `HYPERFRAMES_SKIP_SKILLS=1` to opt out in CI or tests. |
| `--skip-skills` | Leave AI coding skills alone: `init` does not install the core set. `HYPERFRAMES_SKIP_SKILLS=1` does the same.

Pass `--video` or `--audio` and the CLI transcribes the audio with Whisper and
patches the captions into the composition for you. `--skip-transcribe` turns
Expand Down Expand Up @@ -1862,17 +1862,18 @@ npx hyperframes skills update slideshow
Bare `skills` installs everything. `skills update` keeps a deliberate partial
installation partial: it refreshes the core set plus whatever is already
installed, and never expands beyond that unless you name a skill. Naming one
adds it. `check` and `update` both accept `--json`, plus `--dir` and `--source`
to point removed-detection at a different skills directory or source — those two
scope the prune, not the install.
adds it. `check` and `update` both accept `--json`, and `check` also accepts
`--dir` and `--source`. `update` never removes a skill, and it leaves a skill the
install record attributes to another publisher alone, even when the name matches
one of ours.

Set `HYPERFRAMES_SKIP_SKILLS=1` when something else manages the skills (CI, tests,
or an install pinned to one commit). `skills update` and bare `skills` then exit 0
with a one-line notice and leave the installed skills unchanged.

The CLI installs from the current HyperFrames GitHub source and links the global
bundles into whichever compatible agents it finds. After scaffolding, `init`
also refreshes the core set plus any HyperFrames skills you already have.
installs the core set if it is missing. It never refreshes or removes a skill you already have; run `skills update` for that.

#### `fatal: active post-checkout hook found during git clone`

Expand Down
26 changes: 26 additions & 0 deletions packages/cli/src/cli.lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,32 @@ describe("CLI lifecycle", () => {
});
});

describe("stale-skills notice under --json", () => {
it("prints on a --json run while the self-update notices stay off", async () => {
const printSkillsUpdateNotice = vi.fn();
const printUpdateNotice = vi.fn();
mockInitCommand(vi.fn());
mockTelemetry();
vi.doMock("./utils/autoUpdate.js", () => ({
reportCompletedUpdate: vi.fn(),
scheduleBackgroundInstall: vi.fn(),
}));
vi.doMock("./utils/updateCheck.js", () => ({
cachedUpdateCheck: () => ({ current: "1.0.0", latest: "1.0.0", updateAvailable: false }),
printUpdateNotice,
printStalePinNotice: vi.fn(),
}));
vi.doMock("./utils/skillsUpdateCheck.js", () => ({ printSkillsUpdateNotice }));
vi.doMock("./utils/backgroundChecks.js", () => ({ launchBackgroundChecks: vi.fn() }));

process.argv = ["node", "cli.ts", "init", "--json"];
await import("./cli.js");

await vi.waitFor(() => expect(printSkillsUpdateNotice).toHaveBeenCalledTimes(1));
expect(printUpdateNotice).not.toHaveBeenCalled();
});
});

const MOCKED_MODULES = [
"./commands/init.js",
"./telemetry/events.js",
Expand Down
39 changes: 20 additions & 19 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,31 +266,32 @@ if (
// cached check while reporting fresh results of its own).
if (
!isHelp &&
!hasJsonFlag &&
command !== "upgrade" &&
command !== "events" &&
command !== "telemetry" &&
command !== "skills" &&
command !== "usage"
) {
// Report any completed auto-install from the previous run first, before
// kicking off the next check — so the user sees "updated to vX" once and
// we don't over-print.
import("./utils/autoUpdate.js").then((mod) => mod.reportCompletedUpdate()).catch(() => {});

import("./utils/updateCheck.js")
.then(async (mod) => {
_printUpdateNotice = mod.printUpdateNotice;
_printStalePinNotice = mod.printStalePinNotice;
const result = mod.cachedUpdateCheck();
if (result.updateAvailable) {
const auto = await import("./utils/autoUpdate.js").catch(() => null);
auto?.scheduleBackgroundInstall(result.latest, result.current);
}
})
.catch(() => {});
if (!hasJsonFlag) {
// Report any completed auto-install from the previous run first, before
// kicking off the next check — so the user sees "updated to vX" once and
// we don't over-print.
import("./utils/autoUpdate.js").then((mod) => mod.reportCompletedUpdate()).catch(() => {});

import("./utils/updateCheck.js")
.then(async (mod) => {
_printUpdateNotice = mod.printUpdateNotice;
_printStalePinNotice = mod.printStalePinNotice;
const result = mod.cachedUpdateCheck();
if (result.updateAvailable) {
const auto = await import("./utils/autoUpdate.js").catch(() => null);
auto?.scheduleBackgroundInstall(result.latest, result.current);
}
})
.catch(() => {});
}

// Skills freshness nudge — same gating as the CLI self-update notice.
// Skills freshness nudge: stderr only, so it also runs under --json, which agents pass.
import("./utils/skillsUpdateCheck.js")
.then((mod) => {
_printSkillsUpdateNotice = mod.printSkillsUpdateNotice;
Expand Down Expand Up @@ -342,8 +343,8 @@ async function finalizeCli(result: CommandResult): Promise<void> {
if (!hasJsonFlag) {
_printUpdateNotice?.();
_printStalePinNotice?.();
_printSkillsUpdateNotice?.();
}
_printSkillsUpdateNotice?.();
if (_appHistoryNotice) process.stderr.write(`◇ ${_appHistoryNotice}\n`);
process.exitCode = exitCode;
}
Expand Down
30 changes: 26 additions & 4 deletions packages/cli/src/commands/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,20 @@ const tailwindScript =
// TypeScript-aware runtime. vitest runs under node, so `process.execPath`
// would be node and couldn't load the entry. This repo hard-depends on bun
// (package.json scripts), so assuming it's on PATH is safe.
function runInit(args: string[]): { status: number; stdout: string; stderr: string } {
function runInit(
args: string[],
opts: { skillsEnvOptOut?: boolean } = {},
): { status: number; stdout: string; stderr: string } {
const env = { ...process.env, HYPERFRAMES_SKIP_SKILLS: "1" } as Record<
string,
string | undefined
>;
// Default keeps tests offline and fast; a test passes false to rely on the flag alone.
if (opts.skillsEnvOptOut === false) delete env.HYPERFRAMES_SKIP_SKILLS;
const res = spawnSync("bun", ["run", cliEntry, "init", ...args], {
encoding: "utf-8",
timeout: 30_000,
// The `--skip-skills` flag is neutered (see init.ts); the GitHub skills check
// is opted out only via this env var, so tests stay offline and fast.
env: { ...process.env, HYPERFRAMES_SKIP_SKILLS: "1" },
env,
});
return {
status: res.status ?? -1,
Expand Down Expand Up @@ -139,6 +146,21 @@ describe("hyperframes init flag rename", () => {
}
});

it("--skip-skills alone leaves skills alone, without the env opt-out", () => {
const dir = mkdtempSync(join(tmpdir(), "hf-init-test-"));
const target = join(dir, "proj");
try {
const res = runInit([target, "--example", "blank", "--non-interactive", "--skip-skills"], {
skillsEnvOptOut: false,
});
expect(res.status).toBe(0);
expect(res.stdout).not.toContain("Checking AI coding skills");
expect(res.stdout).not.toContain("temporarily ignored");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it("--tailwind enables Tailwind utilities in scaffolded HTML", () => {
const dir = mkdtempSync(join(tmpdir(), "hf-init-test-"));
const target = join(dir, "proj");
Expand Down
46 changes: 7 additions & 39 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,29 +669,18 @@ async function scaffoldProject(
}
}

/**
* Keep the AI coding skills present and current — TARGETED, not the full
* set. Guarantees the core set (the `/hyperframes` entry router + shared
* domain skills) and refreshes any skill already installed; the end-user
* workflow skills are NOT pulled here — they install on demand when their
* workflow is triggered (`hyperframes skills update <name>`, which the router
* runs before entering a workflow). Re-running `init` on an up-to-date machine
* is a no-op, and `init` never expands a deliberate partial install.
* Best-effort: offline, it degrades to a presence check and never breaks init.
* The install itself lands once GLOBALLY (~/.claude/skills + ~/.agents/skills)
* and mirrors into every other installed agent, so it is project-independent —
* the check is global-first to match.
*/
// Installs the core set when missing, globally; an installed skill is never refreshed or removed
// here (that is `hyperframes skills update`). Offline it only checks presence.
async function keepSkillsCurrent(destDir: string): Promise<void> {
const { updateSkills } = await import("./skills.js");

console.log();
console.log(c.bold("Checking AI coding skills against GitHub..."));
// Wrap defensively (non-strict already swallows most failures): a
// skills-install failure can never break `init` itself — it warns and
// proceeds, since --skip-skills no longer escapes this path.
// proceeds.
try {
const result = await updateSkills({ refreshInstalled: true, cwd: destDir });
const result = await updateSkills({ onlyMissing: true, cwd: destDir });
if (result.presenceOnly) {
// Freshness never got checked (GitHub unreachable) — don't claim
// "up to date"; the engine already reported what it could verify or
Expand Down Expand Up @@ -829,17 +818,7 @@ export default defineCommand({
const audioFlag = args.audio;
const skipTranscribe = args["skip-transcribe"] === true;
const keepOriginal = args["keep-original"] === true;
// Temporary measure while the skills.sh registry sync lags GitHub main: the
// `--skip-skills` FLAG is neutered so an agent (or user) that passes it can
// NOT dodge the GitHub skills freshness check. The "don't pass --skip-skills"
// guidance lives in SKILL.md, which ships through the same laggy skills.sh
// channel and can't be relied on to reach the agent — so the guarantee has to
// live in the CLI, the one channel that updates promptly (`npx
// hyperframes@latest`). CI and unit tests still opt out via the
// HYPERFRAMES_SKIP_SKILLS=1 env var, which the agent/user CLI path never sets.
// Revert to `args["skip-skills"] === true` once skills.sh catches up.
const skipSkills = process.env.HYPERFRAMES_SKIP_SKILLS === "1";
const skipSkillsFlagIgnored = args["skip-skills"] === true && !skipSkills;
const skipSkills = args["skip-skills"] === true || process.env.HYPERFRAMES_SKIP_SKILLS === "1";
const tailwind = args.tailwind === true;
const nonInteractive = args["non-interactive"] === true || args.agent === true;
const modelFlag = args.model;
Expand All @@ -850,15 +829,6 @@ export default defineCommand({
);
const interactive = !nonInteractive && process.stdout.isTTY === true;

if (skipSkillsFlagIgnored) {
console.log(
c.dim(
"Note: --skip-skills is temporarily ignored — init always checks AI skills " +
"against GitHub while the skills.sh registry catches up.",
),
);
}

let resolutionPreset: CanvasResolution | undefined;
if (args.resolution !== undefined) {
resolutionPreset = normalizeResolutionFlag(args.resolution);
Expand Down Expand Up @@ -1227,10 +1197,8 @@ export default defineCommand({
const files = readdirSync(destDir);
clack.note(files.map((f) => c.accent(f)).join("\n"), c.success(`Created ${name}/`));

// Check skills against GitHub and refresh only what's stale — the core set
// plus anything already installed; workflow skills install on demand. The
// --skip-skills flag is temporarily neutered (see above); CI/tests opt out
// via HYPERFRAMES_SKIP_SKILLS=1.
// Install the core set if it is missing; installed skills are left alone.
// --skip-skills (or HYPERFRAMES_SKIP_SKILLS=1) leaves skills alone.
if (!skipSkills) {
await keepSkillsCurrent(destDir);
}
Expand Down
Loading
Loading