From f8c33cab72de3e6e7f9f2defbb208b1d91366cac Mon Sep 17 00:00:00 2001 From: WaterrrForever Date: Thu, 16 Jul 2026 22:28:11 +0800 Subject: [PATCH] feat(skills): act on stale CLI pin during project resume (#2540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(skills): probe and bump stale CLI pins during project resume The entry skill now keeps a resumed project's pinned CLI current instead of leaving that to a notice nobody acts on. On resuming a project with pinned scripts, run the read-only probe 'npx hyperframes@latest upgrade --project . --check'; when it (or the stale-pin stderr notice, or _meta.updateAvailable from a pinned run) reports the project behind, apply the bump and verify with 'hyperframes check'. A failed check reverts the bump and keeps the project on its pinned version, preserving the reproducibility contract the pin exists for. The probe matters because the stale-pin notice only exists in >= 0.7.59: a pinned run of an older CLI prints no warning at all, so a notice-only trigger never fires for exactly the projects most behind. The probe runs unpinned, so its behavior never depends on the project's CLI age. Telemetry: the fleet converges to new releases within about a week via the background auto-updater and ephemeral npx, but pinned projects form a persistent stale tail (~10% of weekly actives, e.g. 6.3k users still on 0.6.x three weeks after 0.7.0). Both skill surfaces now pass an explicit dir ('--project .') because a bare '--project' followed by another flag consumes that flag as its directory value and no-ops; the parsing fix is a separate CLI change. * fix(cli): stop bare --project from eating the next flag as its directory citty parses --project as a string option, so 'upgrade --project --check' arrived with project="--check": the dir resolved to a nonexistent path and the command no-opd with 'No package.json found' while --check was lost. The documented default-cwd behavior only worked when --project was the final token — and the trap-prone form is exactly what the scaffolded template CLAUDE.md instructs. A leading dash can never be a real directory argument, so resolveProjectArgs now reclaims the eaten token as the flag the user wrote (--check / --json), falls back to the current directory, and drops unrelated eaten flags rather than treating them as paths. Templates and skill references switch to the explicit-dir form ('--project .'), which behaves correctly on every release including ones that predate this fix. * feat(skills): report a successful pin bump in the run summary Review follow-up on the stale-pin rule: 'hyperframes check' validates composition structure, not render-output equivalence, so a check-passing bump can still shift a project's rendered output. The bump stays the right default for stale projects, but it must not be silent — the summary now names the old and new version so the user knows the reproducibility trade was made. --- .../cli/src/commands/upgrade.project.test.ts | 46 ++++++++++++++++++- packages/cli/src/commands/upgrade.ts | 32 +++++++++++-- packages/cli/src/templates/_shared/AGENTS.md | 2 +- packages/cli/src/templates/_shared/CLAUDE.md | 2 +- skills-manifest.json | 4 +- .../references/upgrade-info-misc.md | 2 +- skills/hyperframes/SKILL.md | 10 ++++ 7 files changed, 88 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/commands/upgrade.project.test.ts b/packages/cli/src/commands/upgrade.project.test.ts index a08585931..2fbb94e39 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 d370f849b..6d22d82bf 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 40f24af4d..a63562ce1 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 40f24af4d..a63562ce1 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 dc480e377..6be6a702b 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 08cbc8bc9..2fdc89537 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 205d5ae47..34bec9539 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.