diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index a640bf218..609633459 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -206,6 +206,7 @@ let _trackCommandResult: }) => void) | undefined; let _printUpdateNotice: (() => void) | undefined; +let _printStalePinNotice: (() => void) | undefined; let _printSkillsUpdateNotice: (() => void) | undefined; // `events` is a telemetry-internal beacon: it self-tracks + self-flushes, so it @@ -245,6 +246,7 @@ if ( import("./utils/updateCheck.js").then(async (mod) => { _printUpdateNotice = mod.printUpdateNotice; + _printStalePinNotice = mod.printStalePinNotice; const result = await mod.checkForUpdate().catch(() => null); if (result?.updateAvailable) { const auto = await import("./utils/autoUpdate.js").catch(() => null); @@ -268,10 +270,12 @@ const runId = getRunId(); // work — so a plain `on` listener would print the update notice (and // re-flush) once per drain (the user-reported double-print). `once` // detaches after first invocation, which is what we want for both. +// fallow-ignore-next-line complexity process.once("beforeExit", () => { _flush?.().catch(() => {}); if (!hasJsonFlag) { _printUpdateNotice?.(); + _printStalePinNotice?.(); _printSkillsUpdateNotice?.(); } }); diff --git a/packages/cli/src/commands/upgrade.project.test.ts b/packages/cli/src/commands/upgrade.project.test.ts new file mode 100644 index 000000000..a08585931 --- /dev/null +++ b/packages/cli/src/commands/upgrade.project.test.ts @@ -0,0 +1,44 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { mkdtempSync, readFileSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +vi.mock("../utils/updateCheck.js", async (orig) => ({ + ...(await orig()), + checkForUpdate: vi.fn(async () => ({ + current: "0.7.48", + latest: "0.7.55", + updateAvailable: true, + })), +})); + +import { upgradeProjectPins } from "./upgrade.js"; + +describe("upgradeProjectPins", () => { + const dirs: string[] = []; + afterEach(() => dirs.forEach((d) => rmSync(d, { recursive: true, force: true }))); + function project(scripts: Record): string { + const d = mkdtempSync(join(tmpdir(), "hf-proj-")); + dirs.push(d); + writeFileSync(join(d, "package.json"), JSON.stringify({ name: "x", scripts }, null, 2)); + return d; + } + + it("rewrites pinned scripts to latest and reports the delta", async () => { + const d = project({ render: "npx --yes hyperframes@0.7.48 render" }); + const r = await upgradeProjectPins(d, { json: false, check: false }); + expect(r.changed).toBe(true); + expect(r.from).toEqual(["0.7.48"]); + expect(r.to).toBe("0.7.55"); + const pkg = JSON.parse(readFileSync(join(d, "package.json"), "utf-8")); + expect(pkg.scripts.render).toBe("npx --yes hyperframes@0.7.55 render"); + }); + + it("--check reports without writing", async () => { + const d = project({ render: "npx --yes hyperframes@0.7.48 render" }); + const before = readFileSync(join(d, "package.json"), "utf-8"); + const r = await upgradeProjectPins(d, { json: false, check: true }); + expect(r.changed).toBe(true); + expect(readFileSync(join(d, "package.json"), "utf-8")).toBe(before); + }); +}); diff --git a/packages/cli/src/commands/upgrade.ts b/packages/cli/src/commands/upgrade.ts index 44f88126c..d370f849b 100644 --- a/packages/cli/src/commands/upgrade.ts +++ b/packages/cli/src/commands/upgrade.ts @@ -2,12 +2,16 @@ import { defineCommand } from "citty"; import type { Example } from "./_examples.js"; import * as clack from "@clack/prompts"; import { execFileSync } from "node:child_process"; +import { existsSync, readFileSync, writeFileSync, renameSync } from "node:fs"; +import { resolve } from "node:path"; import { c } from "../ui/colors.js"; +import { rewriteProjectPinnedScripts } from "../utils/projectPin.js"; export const examples: Example[] = [ ["Check for updates interactively", "hyperframes upgrade"], ["Check for updates without prompting", "hyperframes upgrade --check"], ["Upgrade non-interactively", "hyperframes upgrade --yes"], + ["Bump a project's pinned CLI scripts", "hyperframes upgrade --project"], ]; import { VERSION } from "../version.js"; import { @@ -24,12 +28,28 @@ export default defineCommand({ yes: { type: "boolean", alias: "y", description: "Show upgrade commands without prompting" }, check: { type: "boolean", description: "Check for updates and exit (no prompt)" }, json: { type: "boolean", description: "Output as JSON", default: false }, + project: { + type: "string", + description: + "Bump this project's package.json hyperframes@ script pins to latest (default: current dir)", + }, }, // fallow-ignore-next-line complexity async run({ args }) { const useJson = args.json === true; 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) { + console.log(JSON.stringify(withMeta(res), null, 2)); + return; + } + printProjectPinResult(res, checkOnly); + return; + } + // JSON mode: always force-check and output structured data if (useJson) { const result = await checkForUpdate(true); @@ -137,6 +157,50 @@ function printManualCommands(displayCmd: string, npxFallback: string): void { clack.outro(c.success("Run one of the commands above to upgrade.")); } +export async function upgradeProjectPins( + dir: string, + opts: { json: boolean; check: boolean }, +): Promise<{ changed: boolean; from: string[]; to: string; path: string }> { + const pkgPath = resolve(dir, "package.json"); + if (!existsSync(pkgPath)) { + return { changed: false, from: [], to: "", path: pkgPath }; + } + const raw = JSON.parse(readFileSync(pkgPath, "utf-8")) as { + scripts?: Record; + }; + const scripts = raw.scripts ?? {}; + const { latest } = await checkForUpdate(true); + if (!isSafeVersion(latest)) return { changed: false, from: [], to: latest, path: pkgPath }; + const rewrite = rewriteProjectPinnedScripts(scripts, latest); + if (rewrite.changed && !opts.check) { + raw.scripts = rewrite.scripts; + const tmp = `${pkgPath}.tmp`; + writeFileSync(tmp, `${JSON.stringify(raw, null, 2)}\n`, "utf-8"); + renameSync(tmp, pkgPath); + } + return { changed: rewrite.changed, from: rewrite.fromVersions, to: latest, path: pkgPath }; +} + +function printProjectPinResult( + res: { changed: boolean; from: string[]; to: string; path: string }, + checkOnly: boolean, +): void { + if (!existsSync(res.path)) { + console.log(` ${c.dim("No package.json found — nothing to upgrade.")}`); + return; + } + if (!res.changed) { + console.log(` ${c.success("◇")} Project scripts already on hyperframes@${res.to}`); + return; + } + const verb = checkOnly ? "would bump" : "bumped"; + console.log( + ` ${c.success("◇")} ${verb} project scripts ${res.from.join(", ")} → ${c.accent(res.to)}`, + ); + if (checkOnly) + console.log(` ${c.dim("Run `npx hyperframes@latest upgrade --project` to apply.")}`); +} + export function runDetectedInstall( invocation: { bin: string; args: string[] }, displayCmd: string, diff --git a/packages/cli/src/telemetry/config.ts b/packages/cli/src/telemetry/config.ts index f2e03915f..98054a71b 100644 --- a/packages/cli/src/telemetry/config.ts +++ b/packages/cli/src/telemetry/config.ts @@ -27,6 +27,8 @@ export interface HyperframesConfig { lastUpdateCheck?: string; /** Latest version found on npm */ latestVersion?: string; + /** Throttle for the non-TTY stale-project-pin notice (ms epoch). */ + lastStalePinNoticeAt?: number; /** * Auto-update marker. Set when a background install is spawned so a * subsequent run can skip re-triggering it. Cleared once @@ -122,6 +124,7 @@ export function readConfig(): HyperframesConfig { lastFeedbackPromptAt: parsed.lastFeedbackPromptAt ?? DEFAULT_CONFIG.lastFeedbackPromptAt, lastUpdateCheck: parsed.lastUpdateCheck, latestVersion: parsed.latestVersion, + lastStalePinNoticeAt: parsed.lastStalePinNoticeAt, pendingUpdate: parsed.pendingUpdate, completedUpdate: parsed.completedUpdate, lastSkillsCheck: parsed.lastSkillsCheck, diff --git a/packages/cli/src/templates/_shared/AGENTS.md b/packages/cli/src/templates/_shared/AGENTS.md index 3e8a31aba..7fa340efe 100644 --- a/packages/cli/src/templates/_shared/AGENTS.md +++ b/packages/cli/src/templates/_shared/AGENTS.md @@ -45,6 +45,8 @@ 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. + ## Documentation **For quick reference**, use the local CLI docs command (no network required): diff --git a/packages/cli/src/templates/_shared/CLAUDE.md b/packages/cli/src/templates/_shared/CLAUDE.md index 3e8a31aba..7fa340efe 100644 --- a/packages/cli/src/templates/_shared/CLAUDE.md +++ b/packages/cli/src/templates/_shared/CLAUDE.md @@ -45,6 +45,8 @@ 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. + ## Documentation **For quick reference**, use the local CLI docs command (no network required): diff --git a/packages/cli/src/utils/projectPin.test.ts b/packages/cli/src/utils/projectPin.test.ts new file mode 100644 index 000000000..18a60e725 --- /dev/null +++ b/packages/cli/src/utils/projectPin.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; +import { + rewriteProjectPinnedScripts, + readPinnedHyperframesVersions, + HYPERFRAMES_PIN_RE, +} from "./projectPin.js"; + +describe("HYPERFRAMES_PIN_RE", () => { + it("matches a hyperframes@ token and captures the version", () => { + const match = "npx --yes hyperframes@1.2.3 render".match(HYPERFRAMES_PIN_RE); + expect(match?.[0]).toBe("hyperframes@1.2.3"); + }); +}); + +describe("rewriteProjectPinnedScripts", () => { + const scripts = { + dev: "npx --yes hyperframes@0.7.48 preview", + check: "npx --yes hyperframes@0.7.48 check", + render: "npx --yes hyperframes@0.7.48 render", + unrelated: "echo hi", + unpinned: "npx hyperframes render", + }; + + it("bumps every pinned hyperframes script to the target, leaving others untouched", () => { + const r = rewriteProjectPinnedScripts(scripts, "0.7.55"); + expect(r.changed).toBe(true); + expect(r.fromVersions).toEqual(["0.7.48"]); + expect(r.scripts.render).toBe("npx --yes hyperframes@0.7.55 render"); + expect(r.scripts.dev).toBe("npx --yes hyperframes@0.7.55 preview"); + expect(r.scripts.unrelated).toBe("echo hi"); + expect(r.scripts.unpinned).toBe("npx hyperframes render"); + }); + + it("is a no-op when already at target", () => { + const at = rewriteProjectPinnedScripts( + { render: "npx --yes hyperframes@0.7.55 render" }, + "0.7.55", + ); + expect(at.changed).toBe(false); + expect(at.fromVersions).toEqual([]); + }); + + it("refuses an unsafe target version (no rewrite)", () => { + const r = rewriteProjectPinnedScripts(scripts, "0.7.55; rm -rf /"); + expect(r.changed).toBe(false); + expect(r.scripts.render).toBe(scripts.render); + }); + + it("reads distinct pinned versions across scripts", () => { + expect( + readPinnedHyperframesVersions({ + a: "npx --yes hyperframes@0.7.48 render", + b: "npx hyperframes@0.7.50 check", + c: "npx hyperframes render", + }), + ).toEqual(["0.7.48", "0.7.50"]); + }); +}); diff --git a/packages/cli/src/utils/projectPin.ts b/packages/cli/src/utils/projectPin.ts new file mode 100644 index 000000000..6e7575b4c --- /dev/null +++ b/packages/cli/src/utils/projectPin.ts @@ -0,0 +1,44 @@ +import { isSafeVersion } from "./safeVersion.js"; + +// Matches `hyperframes@` as a whole token inside a script string. The +// version class mirrors isSafeVersion's semver shape; capturing group 1 is the +// old version. `(?=\s|$)` keeps it from matching a longer package name. +export const HYPERFRAMES_PIN_RE = + /\bhyperframes@([0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)(?=\s|$)/g; + +export interface PinRewriteResult { + changed: boolean; + scripts: Record; + fromVersions: string[]; +} + +export function readPinnedHyperframesVersions(scripts: Record): string[] { + const found = new Set(); + for (const cmd of Object.values(scripts ?? {})) { + for (const m of cmd.matchAll(HYPERFRAMES_PIN_RE)) if (m[1]) found.add(m[1]); + } + return [...found].sort(); +} + +export function rewriteProjectPinnedScripts( + scripts: Record, + targetVersion: string, +): PinRewriteResult { + // Never emit an unverified version into a script the user (or npx) will run. + if (!isSafeVersion(targetVersion)) { + return { changed: false, scripts: { ...scripts }, fromVersions: [] }; + } + const fromVersions = new Set(); + const next: Record = {}; + for (const [name, cmd] of Object.entries(scripts ?? {})) { + next[name] = cmd.replace(HYPERFRAMES_PIN_RE, (_full, version: string) => { + if (version !== targetVersion) fromVersions.add(version); + return `hyperframes@${targetVersion}`; + }); + } + return { + changed: [...fromVersions].length > 0, + scripts: next, + fromVersions: [...fromVersions].sort(), + }; +} diff --git a/packages/cli/src/utils/safeVersion.ts b/packages/cli/src/utils/safeVersion.ts new file mode 100644 index 000000000..e5e814602 --- /dev/null +++ b/packages/cli/src/utils/safeVersion.ts @@ -0,0 +1,16 @@ +/** + * True when `v` is a strict semver-shaped string. Registry-supplied versions + * flow into commands that are displayed AND executed (the `upgrade` command and + * the background auto-installer both run them), so a poisoned `latest` carrying + * shell metacharacters must never reach them. This is enforced at the registry + * boundary in `checkForUpdate` — an unsafe `data.version` is never cached — so + * every consumer (notice, upgrade, background auto-install, and any future one) + * is covered by this single gate; the per-consumer checks are defense in depth. + * + * Lives in its own module (rather than updateCheck.ts) so utils/projectPin.ts + * can depend on it without a projectPin.ts <-> updateCheck.ts import cycle — + * updateCheck.ts imports readPinnedHyperframesVersions from projectPin.ts. + */ +export function isSafeVersion(v: string): boolean { + return /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(v); +} diff --git a/packages/cli/src/utils/updateCheck.stalepin.test.ts b/packages/cli/src/utils/updateCheck.stalepin.test.ts new file mode 100644 index 000000000..25e7239c7 --- /dev/null +++ b/packages/cli/src/utils/updateCheck.stalepin.test.ts @@ -0,0 +1,74 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +// Drive `latest` through the REAL getUpdateMeta (defined in the module under +// test) via the mocked config store — self-mocking getUpdateMeta on +// "./updateCheck.js" would only override the export binding, not the internal +// call printStalePinNotice makes to it from within the same module. +let store: Record = {}; +// isDevMode() is true under vitest (module path ends in .ts), which would +// suppress the notice unconditionally — mock ./env.js like updateCheck.test.ts does. +vi.mock("./env.js", () => ({ isDevMode: () => false })); +vi.mock("../telemetry/config.js", () => ({ + readConfig: () => ({ ...store }), + writeConfig: (c: Record) => { + store = { ...c }; + return true; + }, +})); + +import { printStalePinNotice } from "./updateCheck.js"; + +describe("printStalePinNotice", () => { + let dir: string; + let writes: string[]; + const origWrite = process.stderr.write.bind(process.stderr); + beforeEach(() => { + store = { latestVersion: "0.7.55" }; + writes = []; + dir = mkdtempSync(join(tmpdir(), "hf-pin-")); + process.stderr.write = ((s: unknown) => { + writes.push(String(s)); + return true; + }) as typeof process.stderr.write; + delete process.env.CI; + delete process.env.HYPERFRAMES_NO_UPDATE_CHECK; + }); + afterEach(() => { + process.stderr.write = origWrite; + rmSync(dir, { recursive: true, force: true }); + }); + + it("warns once when the project pins an older version", () => { + writeFileSync( + join(dir, "package.json"), + JSON.stringify({ scripts: { render: "npx --yes hyperframes@0.7.48 render" } }), + ); + printStalePinNotice(dir); + printStalePinNotice(dir); // throttled — second call silent + expect(writes.join("")).toContain("0.7.48"); + expect(writes.join("")).toContain("upgrade --project"); + expect(writes.filter((w) => w.includes("upgrade --project")).length).toBe(1); + }); + + it("silent when project pin is current", () => { + writeFileSync( + join(dir, "package.json"), + JSON.stringify({ scripts: { render: "npx --yes hyperframes@0.7.55 render" } }), + ); + printStalePinNotice(dir); + expect(writes.join("")).toBe(""); + }); + + it("silent under CI", () => { + process.env.CI = "true"; + writeFileSync( + join(dir, "package.json"), + JSON.stringify({ scripts: { render: "npx --yes hyperframes@0.7.48 render" } }), + ); + printStalePinNotice(dir); + expect(writes.join("")).toBe(""); + }); +}); diff --git a/packages/cli/src/utils/updateCheck.ts b/packages/cli/src/utils/updateCheck.ts index 6a58fad52..0dddbd3fd 100644 --- a/packages/cli/src/utils/updateCheck.ts +++ b/packages/cli/src/utils/updateCheck.ts @@ -1,21 +1,14 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; import { compareVersions } from "compare-versions"; import { readConfig, writeConfig } from "../telemetry/config.js"; import { VERSION } from "../version.js"; import { isDevMode } from "./env.js"; import { detectInstaller } from "./installerDetection.js"; +import { readPinnedHyperframesVersions } from "./projectPin.js"; +import { isSafeVersion } from "./safeVersion.js"; -/** - * True when `v` is a strict semver-shaped string. Registry-supplied versions - * flow into commands that are displayed AND executed (the `upgrade` command and - * the background auto-installer both run them), so a poisoned `latest` carrying - * shell metacharacters must never reach them. This is enforced at the registry - * boundary in `checkForUpdate` — an unsafe `data.version` is never cached — so - * every consumer (notice, upgrade, background auto-install, and any future one) - * is covered by this single gate; the per-consumer checks are defense in depth. - */ -export function isSafeVersion(v: string): boolean { - return /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(v); -} +export { isSafeVersion } from "./safeVersion.js"; const NPM_REGISTRY_URL = "https://registry.npmjs.org/hyperframes/latest"; const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours @@ -195,3 +188,50 @@ export function printUpdateNotice(): void { ` Run: ${command}\n\n`, ); } + +const STALE_PIN_THROTTLE_MS = 24 * 60 * 60 * 1000; + +/** + * Actionable, throttled notice for a project whose package.json still pins an + * OLD hyperframes version. Unlike printUpdateNotice this DOES fire on non-TTY + * (agents render with piped stderr) \u2014 but only when there's a concrete stale + * pin to act on, at most once/24h per install, and never under --json/CI/dev/ + * opt-out. The whole cli.ts update block is already skipped for --json, so a + * JSON stdout stays clean regardless. + */ +export function printStalePinNotice(cwd: string = process.cwd()): void { + if (isDevMode()) return; + if (process.env["CI"] === "true" || process.env["CI"] === "1") return; + if (process.env["HYPERFRAMES_NO_UPDATE_CHECK"] === "1") return; + + const latest = getUpdateMeta().latestVersion; + if (!latest || !isSafeVersion(latest)) return; + + let scripts: Record = {}; + try { + const pkgPath = join(cwd, "package.json"); + if (!existsSync(pkgPath)) return; + scripts = (JSON.parse(readFileSync(pkgPath, "utf-8")).scripts ?? {}) as Record; + } catch { + return; + } + const stale = readPinnedHyperframesVersions(scripts).filter((v) => { + try { + return compareVersions(latest, v) > 0; + } catch { + return false; + } + }); + if (stale.length === 0) return; + + const config = readConfig(); + const last = config.lastStalePinNoticeAt ?? 0; + if (Date.now() - last < STALE_PIN_THROTTLE_MS) return; + config.lastStalePinNoticeAt = Date.now(); + writeConfig(config); + + process.stderr.write( + `\n This project pins hyperframes@${stale.join(", ")} (latest ${latest}).\n` + + ` Bump it: npx hyperframes@latest upgrade --project\n\n`, + ); +} diff --git a/skills-manifest.json b/skills-manifest.json index c317f496c..982de740f 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -26,7 +26,7 @@ "files": 102 }, "hyperframes-cli": { - "hash": "f78d0c936fd3499f", + "hash": "fbe17780248d5bec", "files": 8 }, "hyperframes-core": { diff --git a/skills/hyperframes-cli/SKILL.md b/skills/hyperframes-cli/SKILL.md index 4442c4511..2b233085b 100644 --- a/skills/hyperframes-cli/SKILL.md +++ b/skills/hyperframes-cli/SKILL.md @@ -40,6 +40,7 @@ Cross-cutting rules that hold for every command: - **Use Studio context for user-directed edits.** When the user says "this selected element", "the thing I clicked", "current selection", or similar, ask them to select it in Studio, then run `npx hyperframes preview --context --json --context-fields selection`. Use the returned `selection.target.hfId` / `selector`, `selection.sourceFile`, `selection.currentTime`, and `selection.thumbnailUrl` to anchor the edit. If `selection` is `null` and `errors.selection.code` is `no-selection`, ask the user to click the element and rerun; do not guess from screenshots. - **Keep Studio context compact.** `preview --context --json` returns compact selection by default. Add `--context-fields selection`, `--context-fields selection,lint`, or `--context-fields lint` to avoid bloating agent context. Use `--context-detail full` only when you need heavy fields like computed styles, inline styles, or text-field metadata. - **Post-render verification.** After `render` returns exit 0, confirm the output file exists and has plausible size before reporting success: `[ -s "$OUTPUT" ] || echo "render produced no output"`. The CLI prints `◇ ` on success; for long renders also sanity-check duration with `ffprobe -i "$OUTPUT" -show_format -v error`. +- **Keep the project's pinned CLI fresh.** `hyperframes init` writes an exact version into `package.json` scripts (`npx --yes hyperframes@X.Y.Z render`) so a project re-renders identically over time — but a project scaffolded weeks ago is frozen on a CLI that predates render, capture-router, and correctness fixes it would silently benefit from. Once per project, check whether the pin is stale and bump it: `npx hyperframes@latest upgrade --project --check` (reports the delta, writes nothing), then `npx hyperframes@latest upgrade --project` to apply. Use the **unpinned** `hyperframes@latest` form — the pinned script would just re-run the old version against itself. Skip if the user deliberately pinned a version. - **Report feedback after a successful render.** Once the output is verified, run `npx hyperframes feedback --rating <0-10> --comment "..."` once per task to report how likely you are to recommend the run — `10` for a clean run, lower when you hit friction. Put any bug, workaround, missing feature, or confusing behaviour you encountered in `--comment`. This is the project's primary signal channel; renders that go silent leave the maintainers blind. Skip only when telemetry is disabled or the user opted out. - **For a reproducible bug, add `--file-issue`** — it publishes a minimal repro of the project to a public URL and opens a pre-filled GitHub issue with your comment + environment. A comment alone almost never lets a maintainer reproduce; the published composition is what makes the report actionable. Point `--dir ` at the failing composition if it isn't the current directory. - **Write bug comments a maintainer can reproduce from — a paraphrase they can't act on wastes the report.** The CLI already attaches os/arch/mem/ffmpeg/CLI-version, so don't repeat those; spend the comment on what only you saw. For a bug, include every one of these that applies: diff --git a/skills/hyperframes-cli/references/upgrade-info-misc.md b/skills/hyperframes-cli/references/upgrade-info-misc.md index 89d72034d..08cbc8bc9 100644 --- a/skills/hyperframes-cli/references/upgrade-info-misc.md +++ b/skills/hyperframes-cli/references/upgrade-info-misc.md @@ -23,6 +23,8 @@ 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 }`. + ## compositions, docs ```bash