diff --git a/packages/cli/src/commands/upgrade.project.test.ts b/packages/cli/src/commands/upgrade.project.test.ts index a08585931f..2fbb94e39f 100644 --- a/packages/cli/src/commands/upgrade.project.test.ts +++ b/packages/cli/src/commands/upgrade.project.test.ts @@ -12,7 +12,51 @@ vi.mock("../utils/updateCheck.js", async (orig) => ({ })), })); -import { upgradeProjectPins } from "./upgrade.js"; +import { resolveProjectArgs, upgradeProjectPins } from "./upgrade.js"; + +describe("resolveProjectArgs", () => { + it("reclaims a flag eaten as the --project value", () => { + expect(resolveProjectArgs("--check", { check: false, json: false })).toEqual({ + dir: ".", + check: true, + json: false, + }); + expect(resolveProjectArgs("--json", { check: false, json: false })).toEqual({ + dir: ".", + check: false, + json: true, + }); + }); + + it("defaults to the current directory for a bare or boolean --project", () => { + expect(resolveProjectArgs(true, { check: true, json: false })).toEqual({ + dir: ".", + check: true, + json: false, + }); + expect(resolveProjectArgs("", { check: false, json: false })).toEqual({ + dir: ".", + check: false, + json: false, + }); + }); + + it("passes a real directory through untouched", () => { + expect(resolveProjectArgs("apps/site", { check: false, json: true })).toEqual({ + dir: "apps/site", + check: false, + json: true, + }); + }); + + it("drops an unrelated eaten flag instead of treating it as a directory", () => { + expect(resolveProjectArgs("--yes", { check: false, json: false })).toEqual({ + dir: ".", + check: false, + json: false, + }); + }); +}); describe("upgradeProjectPins", () => { const dirs: string[] = []; diff --git a/packages/cli/src/commands/upgrade.ts b/packages/cli/src/commands/upgrade.ts index d370f849b0..6d22d82bfb 100644 --- a/packages/cli/src/commands/upgrade.ts +++ b/packages/cli/src/commands/upgrade.ts @@ -40,13 +40,13 @@ export default defineCommand({ const checkOnly = args.check === true; if (args.project !== undefined) { - const dir = typeof args.project === "string" && args.project.length ? args.project : "."; - const res = await upgradeProjectPins(resolve(dir), { json: useJson, check: checkOnly }); - if (useJson) { + const p = resolveProjectArgs(args.project, { check: checkOnly, json: useJson }); + const res = await upgradeProjectPins(resolve(p.dir), { json: p.json, check: p.check }); + if (p.json) { console.log(JSON.stringify(withMeta(res), null, 2)); return; } - printProjectPinResult(res, checkOnly); + printProjectPinResult(res, p.check); return; } @@ -157,6 +157,30 @@ function printManualCommands(displayCmd: string, npxFallback: string): void { clack.outro(c.success("Run one of the commands above to upgrade.")); } +/** + * citty parses `--project` as a string option, so a bare `--project` followed + * by another flag consumes that flag as its value (`upgrade --project --check` + * arrives here as project="--check"). A leading dash can never be a real + * directory argument, so reclaim the eaten token as the flag the user wrote + * and fall back to the current directory. + */ +export function resolveProjectArgs( + project: string | boolean, + opts: { check: boolean; json: boolean }, +): { dir: string; check: boolean; json: boolean } { + if (typeof project !== "string" || project.length === 0) { + return { dir: ".", check: opts.check, json: opts.json }; + } + if (project.startsWith("-")) { + return { + dir: ".", + check: opts.check || project === "--check", + json: opts.json || project === "--json", + }; + } + return { dir: project, check: opts.check, json: opts.json }; +} + export async function upgradeProjectPins( dir: string, opts: { json: boolean; check: boolean }, diff --git a/packages/cli/src/templates/_shared/AGENTS.md b/packages/cli/src/templates/_shared/AGENTS.md index 40f24af4d5..a63562ce11 100644 --- a/packages/cli/src/templates/_shared/AGENTS.md +++ b/packages/cli/src/templates/_shared/AGENTS.md @@ -44,7 +44,7 @@ npx hyperframes docs # reference docs in terminal > In Claude Code, always run it with `run_in_background: true`. Never run it as a foreground > command — it will time out and the server will die, breaking the browser preview. -> **Pinned CLI version.** These scripts pin an exact `hyperframes@X.Y.Z` so this project re-renders identically over time. Weeks later that pin lags fixes shipped since. To move up: `npx hyperframes@latest upgrade --project --check` (shows the delta), then `npx hyperframes@latest upgrade --project` to rewrite the pins. Always unpinned — the pinned script re-runs the old version against itself. +> **Pinned CLI version.** These scripts pin an exact `hyperframes@X.Y.Z` so this project re-renders identically over time. Weeks later that pin lags fixes shipped since. To move up: `npx hyperframes@latest upgrade --project . --check` (shows the delta), then `npx hyperframes@latest upgrade --project .` to rewrite the pins. Always unpinned — the pinned script re-runs the old version against itself. ## Documentation diff --git a/packages/cli/src/templates/_shared/CLAUDE.md b/packages/cli/src/templates/_shared/CLAUDE.md index 40f24af4d5..a63562ce11 100644 --- a/packages/cli/src/templates/_shared/CLAUDE.md +++ b/packages/cli/src/templates/_shared/CLAUDE.md @@ -44,7 +44,7 @@ npx hyperframes docs # reference docs in terminal > In Claude Code, always run it with `run_in_background: true`. Never run it as a foreground > command — it will time out and the server will die, breaking the browser preview. -> **Pinned CLI version.** These scripts pin an exact `hyperframes@X.Y.Z` so this project re-renders identically over time. Weeks later that pin lags fixes shipped since. To move up: `npx hyperframes@latest upgrade --project --check` (shows the delta), then `npx hyperframes@latest upgrade --project` to rewrite the pins. Always unpinned — the pinned script re-runs the old version against itself. +> **Pinned CLI version.** These scripts pin an exact `hyperframes@X.Y.Z` so this project re-renders identically over time. Weeks later that pin lags fixes shipped since. To move up: `npx hyperframes@latest upgrade --project . --check` (shows the delta), then `npx hyperframes@latest upgrade --project .` to rewrite the pins. Always unpinned — the pinned script re-runs the old version against itself. ## Documentation diff --git a/skills-manifest.json b/skills-manifest.json index dc480e3778..6be6a702ba 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -18,7 +18,7 @@ "files": 1 }, "hyperframes": { - "hash": "07dfc1f0b31ff8ed", + "hash": "de6908d7047b8702", "files": 6 }, "hyperframes-animation": { @@ -26,7 +26,7 @@ "files": 102 }, "hyperframes-cli": { - "hash": "05f7c65e6f9db100", + "hash": "79ab5a7856a42d68", "files": 11 }, "hyperframes-core": { diff --git a/skills/hyperframes-cli/references/upgrade-info-misc.md b/skills/hyperframes-cli/references/upgrade-info-misc.md index 08cbc8bc93..2fdc89537e 100644 --- a/skills/hyperframes-cli/references/upgrade-info-misc.md +++ b/skills/hyperframes-cli/references/upgrade-info-misc.md @@ -23,7 +23,7 @@ npx hyperframes upgrade --yes # print upgrade commands without promptin Compares the installed CLI version against npm latest. -`--project [dir]` bumps a **project's** pinned scripts instead of the global install: it rewrites every `npx …hyperframes@…` in `/package.json` (default cwd) to npm-latest. Always invoke it unpinned (`npx hyperframes@latest upgrade --project`) — a project scaffolded on an old CLI stays frozen otherwise. `--project --check` reports the delta without writing; add `--json` for `{ changed, from, to, path }`. +`--project [dir]` bumps a **project's** pinned scripts instead of the global install: it rewrites every `npx …hyperframes@…` in `/package.json` (default cwd) to npm-latest. Always invoke it unpinned (`npx hyperframes@latest upgrade --project`) — a project scaffolded on an old CLI stays frozen otherwise. `--project . --check` reports the delta without writing; add `--json` for `{ changed, from, to, path }`. Pass the dir explicitly whenever another flag follows `--project` — on older releases a bare `--project` consumes the next flag as its directory value. ## compositions, docs diff --git a/skills/hyperframes/SKILL.md b/skills/hyperframes/SKILL.md index 205d5ae47d..34bec95394 100644 --- a/skills/hyperframes/SKILL.md +++ b/skills/hyperframes/SKILL.md @@ -33,6 +33,16 @@ Continue with source adapters in § 2. A direct or resumed workflow route skips If a fresh request does not identify the subject or input, ask what the video is about before routing. Check preferences and recipes before asking anything (§ 4, step 1). +### Keep the project's CLI current + +A scaffolded project pins `hyperframes@` in its `package.json` scripts so renders stay reproducible; the pin never advances on its own, and a pinned run of an older CLI prints no warning about it. When resuming a project whose scripts carry a pin, probe once before the first render-affecting command: + +```bash +npx hyperframes@latest upgrade --project . --check +``` + +The probe is read-only and reports the pin against the latest release; keep the explicit `.` — on older CLI releases a bare `--project` followed by another flag consumes that flag as its directory value. When it reports the project behind — or any CLI output already shows it (the stderr notice `This project pins hyperframes@… (latest …)`, or `_meta.updateAvailable: true` in a `--json` result from a pinned script) — apply with `npx hyperframes@latest upgrade --project .`, then verify with `npx hyperframes check`. A passing check confirms the project's compositions still validate on the new version — not that rendered output is frame-identical to the old pin — so a successful bump is never silent: name the old and new version in the run's summary. A project with no composition yet needs no verification. If the check fails, revert the `package.json` change, continue on the pinned version, and report which version the project stays on and why. Act on the signal rather than relaying it to the user; never leave a bumped pin unverified. + ## 2. Adapt orthogonal inputs before routing A Figma source changes **how assets and design enter the project**, not which workflow owns the deliverable.