mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-08 02:36:10 +00:00
* 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.
89 lines
2.9 KiB
TypeScript
89 lines
2.9 KiB
TypeScript
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<typeof import("../utils/updateCheck.js")>()),
|
|
checkForUpdate: vi.fn(async () => ({
|
|
current: "0.7.48",
|
|
latest: "0.7.55",
|
|
updateAvailable: true,
|
|
})),
|
|
}));
|
|
|
|
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[] = [];
|
|
afterEach(() => dirs.forEach((d) => rmSync(d, { recursive: true, force: true })));
|
|
function project(scripts: Record<string, string>): 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);
|
|
});
|
|
});
|