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
103 changes: 96 additions & 7 deletions docs/packages/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ npm install -g hyperframes
| Edit from the terminal | [`timeline`](#timeline), [`history`](#history) |
| Make a file | [`render`](#render), [`benchmark`](#benchmark) |
| Render somewhere else | [`cloud`](#hyperframes-cloud), [`lambda`](#hyperframes-lambda), [`cloudrun`](#hyperframes-cloudrun) |
| Fix your setup | [`doctor`](#doctor), [`browser`](#browser), [`upgrade`](#upgrade), [`docs`](#docs), [`info`](#info-and-compositions), [`compositions`](#info-and-compositions), [`clean`](#clean) |
| Fix your setup | [`doctor`](#doctor), [`browser`](#browser), [`update`](#update), [`upgrade`](#upgrade), [`docs`](#docs), [`info`](#info-and-compositions), [`compositions`](#info-and-compositions), [`clean`](#clean) |
| Connect other tools | [`auth`](#hyperframes-auth), [`billing`](#billing), [`templates`](#templates), [`skills`](#skills), [`usage`](#usage), [`figma`](#figma-and-events), [`events`](#figma-and-events), [`telemetry`](#telemetry), [`feedback`](#feedback) |

`validate`, `inspect`, and `layout` still work but are
Expand All @@ -59,15 +59,42 @@ second call:
"_meta": {
"version": "0.1.4",
"latestVersion": "0.1.5",
"updateAvailable": true
"updateAvailable": true,
"checkedAt": "2026-11-03T08:00:00.000Z",
"skillsOutdated": 2,
"skillsMissing": 0,
"updateNotice": "For the user: HyperFrames CLI 0.1.5 is available (this run is 0.1.4), and 2 installed HyperFrames skills are out of date. To update both, run: hyperframes update"
}
}
```

The version numbers come from a cache refreshed at most once a day, in a
The version numbers come from a cache refreshed at most every two hours, in a
background process, and never under CI or `HYPERFRAMES_NO_UPDATE_CHECK=1`.
`--json` never makes a network request of its own. Deprecated commands add
`_meta.deprecated: true`.
`checkedAt` says when npm answered. A cached latest older than the running CLI
is left out and rechecked on the next run. `skillsOutdated` counts the installed
HyperFrames skills that are behind this CLI's release, and `skillsMissing` the
core skills that are not installed, from the same background check; both are
absent until that check has run for this version, once it is more than a day old
(as when `HYPERFRAMES_NO_UPDATE_CHECK=1` stops the checks), and under
`HYPERFRAMES_SKIP_SKILLS=1`. `--json` never makes a network request of its own.
Deprecated commands add `_meta.deprecated: true`.

`updateNotice` is one sentence for the agent to pass on to the person when the
CLI or the skills are behind: what is newer and the command that updates it.
The person decides whether to run it. The command moves the install that is
running:

| Install | CLI behind | Skills behind too, or only them |
| --- | --- | --- |
| npm, pnpm, bun or Homebrew global | the manager's command (`npm install -g hyperframes@0.9.8`, `brew upgrade hyperframes`, …) | `hyperframes update` (the global CLI's own; `npx hyperframes` would run a project's copy or a fresh one) |
| project `node_modules` | the project's manager, by lockfile: `npm install`, `pnpm add`, `bun add` or `yarn add hyperframes@0.9.8` | that, then `npx hyperframes update`; skills only: `npx hyperframes update` |
| `npx` | `npx hyperframes@latest update` (npx's cache may hold the old CLI) | the same; skills only: `npx hyperframes update` |

The skills half is left out when no skill is behind or under
`HYPERFRAMES_SKIP_SKILLS=1`. The notice appears at most once a day per project,
while the numeric fields stay on every result. It is never added under CI,
`HYPERFRAMES_NO_UPDATE_CHECK=1`, or an agent plugin (`HYPERFRAMES_PLUGIN_VERSION`),
and the output of `update`, `upgrade` and `skills` leaves it out.

A failure under `--json` always prints one JSON object on stdout, with a
machine-readable `code` and, when there is a next step, a `hint`:
Expand Down Expand Up @@ -127,7 +154,8 @@ notice; `HYPERFRAMES_NO_AUTO_INSTALL=1` turns off only the install.
**Skills notice.** When installed HyperFrames skills are out of date or missing,
a command ends with one line on stderr naming `npx hyperframes skills update`.
It shows at most once a day per project. In an unattended run it shows only when
a core skill is missing. `HYPERFRAMES_SKIP_SKILLS=1` turns it off. No notice
a core skill is missing; outdated skills reach agents as `_meta.skillsOutdated`
and `_meta.updateNotice` instead. `HYPERFRAMES_SKIP_SKILLS=1` turns it off. No notice
follows a usage error.

## Create a project
Expand Down Expand Up @@ -1972,6 +2000,7 @@ npx hyperframes doctor --json
hyperframes doctor

✓ Version 0.1.4 (latest)
✓ CLI binary npm global install · /usr/local/lib/node_modules/hyperframes/dist/cli.js
✓ Node.js v22.x (linux x64)
✓ FFmpeg 7.x
✓ FFprobe 7.x
Expand All @@ -1982,7 +2011,9 @@ hyperframes doctor
◇ All checks passed
```

It reports CLI version, Node.js, CPU, memory, disk, the extracted-frame cache,
It reports CLI version, which binary is running and from where (a global npm,
pnpm, bun or Homebrew install, a project's `node_modules`, `npx`, or an agent
plugin) with its path, Node.js, CPU, memory, disk, the extracted-frame cache,
the archive extractor, `/dev/shm` on Linux, environment, whisper-cpp, local TTS
and BGM models, FFmpeg, FFprobe, Chrome, and Docker.

Expand Down Expand Up @@ -2033,6 +2064,64 @@ duration, element counts by type, track count, and total size.
`npx hyperframes compositions` lists every composition in the project with its
ID, duration, resolution, and element count. Both take `--json`.

### `update`

```bash
npx hyperframes update --check # report the CLI and the skills, install nothing
npx hyperframes update --check --json # the same, for agents
npx hyperframes update --yes # update both without prompting
```

`update` asks npm for the latest CLI (past the two-hour cache) and checks the
installed skills against the manifest at this CLI's release tag, in one call.
`--check` only reports. Without it, `update` upgrades the CLI the way
`upgrade --yes` does (an `npx` run gets `npx hyperframes@latest` to run instead,
and a project-local run the project manager's command, such as
`npm install hyperframes@0.9.8`) and then runs `skills update` at the release
tag of the CLI that is now installed. Homebrew installs whatever its formula
has, which can trail npm, so `update` reads the installed version back and
reports `unchanged` when the formula has not moved. Without `--yes` or
`--check`, an unattended run and any `--json` run exit 2, with the hint
"Updating is the person's call; `update --check` only reports." Under an agent
plugin, `update --check` reports, and `update` itself exits 1 with
`plugin_managed`: the plugin manager updates it. `HYPERFRAMES_SKIP_SKILLS=1`
leaves the skills alone. When npm does not answer, the human report says
"npm registry not reached" instead of calling the CLI the latest.
`--check --json` returns:

```json
{
"cli": {
"current": "0.9.5",
"latest": "0.9.8",
"updateAvailable": true,
"checkedAt": "2026-11-03T08:00:00.000Z",
"install": { "channel": "npm", "path": "/usr/local/lib/node_modules/hyperframes/dist/cli.js" },
"command": "npm install -g hyperframes@0.9.8"
},
"skills": {
"status": "checked",
"ref": "v0.9.5",
"location": "/Users/me/.claude/skills",
"updateAvailable": true,
"outdated": 2,
"missing": 0,
"command": "hyperframes update"
},
"_meta": {
"version": "0.9.5",
"latestVersion": "0.9.8",
"updateAvailable": true,
"checkedAt": "2026-11-03T08:00:00.000Z",
"skillsOutdated": 2,
"skillsMissing": 0
}
}
```

`cli.checkedAt` is `null` when npm did not answer. `install.channel` is one of
`npm`, `pnpm`, `bun`, `brew`, `npx`, `project`, `workspace`, `plugin` or `unknown`.

### `upgrade`

```bash
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,15 @@ npx hyperframes upgrade
npx hyperframes upgrade --check --json # machine-readable for agents
```

### `update`

Check the CLI and the installed skills for updates in one call, or install both:

```bash
npx hyperframes update --check --json # report only, machine-readable for agents
npx hyperframes update --yes # upgrade the CLI, then refresh the skills
```

## Documentation

Full documentation: [hyperframes.heygen.com/packages/cli](https://hyperframes.heygen.com/packages/cli)
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/cli.agentMode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,16 @@ describe("agent-mode contract", () => {
},
);

// Updating is the person's decision: the hint states that and names no step for the agent.
it("update without --yes refuses to prompt, exits 2, and leaves updating to the person", async () => {
const run = await runAsAgent(["update"]);
expectQuiet(run);
expect(run.code, run.stderr).toBe(2);
expect(run.stdout).toBe("");
expect(run.stderr).toContain("Updating is the person's call; `update --check` only reports.");
expect(run.stderr).not.toContain("--yes");
});

it("only the prompt wrapper loads @clack/prompts, statically or dynamically", () => {
const offenders = (readdirSync(srcDir, { recursive: true }) as string[]).filter(
(file) =>
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ const commandLoaders = {
doctor: () => import("./commands/doctor.js").then((m) => m.default),
clean: () => import("./commands/clean.js").then((m) => m.default),
upgrade: () => import("./commands/upgrade.js").then((m) => m.default),
update: () => import("./commands/update.js").then((m) => m.default),
skills: () => import("./commands/skills.js").then((m) => m.default),
feedback: () => import("./commands/feedback.js").then((m) => m.default),
telemetry: () => import("./commands/telemetry.js").then((m) => m.default),
Expand Down Expand Up @@ -282,6 +283,7 @@ if (
if (
!isHelp &&
command !== "upgrade" &&
command !== "update" &&
command !== "events" &&
command !== "telemetry" &&
command !== "skills" &&
Expand Down
84 changes: 84 additions & 0 deletions packages/cli/src/commands/doctor.install.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// `doctor` names the binary that is running and where it lives, and its update hint is the
// command for that install: the manager's for a global one, `npx hyperframes@latest` for npx.
// The install detection is real; only the resolved entry path and the cached latest are stubbed.

import { afterEach, describe, expect, it, vi } from "vitest";

const ENTRIES = {
npm: "/usr/local/lib/node_modules/hyperframes/dist/cli.js",
brew: "/opt/homebrew/Cellar/hyperframes/0.9.5/libexec/lib/node_modules/hyperframes/dist/cli.js",
npx: "/home/me/.npm/_npx/4f1c2a/node_modules/hyperframes/dist/cli.js",
project: "/home/me/video/node_modules/hyperframes/dist/cli.js",
} as const;

const originalArgv1 = process.argv[1];

async function doctorAt(entry: string, latestVersion: string | undefined) {
vi.resetModules();
vi.stubEnv("HYPERFRAMES_PLUGIN_VERSION", "");
vi.doMock("node:fs", async () => ({
...(await vi.importActual<typeof import("node:fs")>("node:fs")),
realpathSync: () => entry,
}));
vi.doMock("../utils/updateCheck.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../utils/updateCheck.js")>()),
getUpdateMeta: () => ({
version: "0.9.5",
latestVersion,
updateAvailable: latestVersion !== undefined && latestVersion !== "0.9.5",
}),
}));
vi.doMock("../version.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../version.js")>()),
VERSION: "0.9.5",
}));
process.argv[1] = "/usr/local/bin/hyperframes";
return import("./doctor.js");
}

afterEach(() => {
process.argv[1] = originalArgv1 ?? "";
vi.doUnmock("node:fs");
vi.doUnmock("../utils/updateCheck.js");
vi.doUnmock("../version.js");
vi.unstubAllEnvs();
vi.resetModules();
});

describe("doctor: the running binary", () => {
it.each([
["npm", ENTRIES.npm, "npm global install"],
["brew", ENTRIES.brew, "Homebrew install"],
["npx", ENTRIES.npx, "npx"],
["project", ENTRIES.project, "project-local install"],
])("names a %s binary and its path", async (_channel, entry, label) => {
const { checkInstall } = await doctorAt(entry, "0.9.5");
const result = checkInstall();
expect(result.ok).toBe(true);
expect(result.detail).toContain(label);
expect(result.detail).toContain(entry);
});

it("names a plugin run as plugin-pinned", async () => {
const { checkInstall } = await doctorAt(ENTRIES.npx, "0.9.5");
vi.stubEnv("HYPERFRAMES_PLUGIN_VERSION", "0.9.5");
expect(checkInstall().detail).toContain("agent plugin");
});
});

describe("doctor: the update hint fits the install", () => {
it.each([
["npm", ENTRIES.npm, "Run: npm install -g hyperframes@0.9.8"],
["brew", ENTRIES.brew, "Run: brew upgrade hyperframes"],
["npx", ENTRIES.npx, "Run: npx hyperframes@latest"],
["project", ENTRIES.project, "Run: npm install hyperframes@0.9.8"],
])("%s", async (_channel, entry, hint) => {
const { checkVersion } = await doctorAt(entry, "0.9.8");
expect(checkVersion()).toEqual({ ok: false, detail: "0.9.5 → 0.9.8 available", hint });
});

it("does not claim 'latest' before any check has answered", async () => {
const { checkVersion } = await doctorAt(ENTRIES.npm, undefined);
expect(checkVersion()).toEqual({ ok: true, detail: "0.9.5 (not checked yet)" });
});
});
18 changes: 16 additions & 2 deletions packages/cli/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import { KOKORO_MODULES, KOKORO_PIP, MUSICGEN_MODULES, MUSICGEN_PIP } from "../a
import { hasPythonModules, describeRejectedPythonOverride } from "../tts/python.js";
import { VERSION } from "../version.js";
import { getUpdateMeta, withMeta } from "../utils/updateCheck.js";
import {
installChannelLabel,
runningInstall,
updateCommandFor,
} from "../utils/installerDetection.js";
import {
OPTIONAL_PACKAGES,
installedOptionalPackageVersion,
Expand Down Expand Up @@ -82,18 +87,26 @@ function checkDockerRunning(): CheckResult {
}
}

function checkVersion(): CheckResult {
export function checkVersion(): CheckResult {
const meta = getUpdateMeta();
if (meta.updateAvailable && meta.latestVersion) {
return {
ok: false,
detail: `${VERSION} \u2192 ${meta.latestVersion} available`,
hint: "Run: hyperframes upgrade",
hint: `Run: ${updateCommandFor(runningInstall(), meta.latestVersion)}`,
};
}
if (!meta.latestVersion) return { ok: true, detail: `${VERSION} (not checked yet)` };
return { ok: true, detail: `${VERSION} (latest)` };
}

/** Which binary is running and from where: a global install per manager, project-local, npx or a plugin. */
export function checkInstall(): CheckResult {
const install = runningInstall();
const where = install.path ?? "entry path unknown";
return { ok: true, detail: `${installChannelLabel(install.channel)} \u00B7 ${where}` };
}

function checkNode(): CheckResult {
return { ok: true, detail: `${process.version} (${process.platform} ${process.arch})` };
}
Expand Down Expand Up @@ -380,6 +393,7 @@ export default defineCommand({
const environment = await runEnvironmentChecks({ includeBrowser: true });
const checks: Check[] = [
{ name: "Version", run: checkVersion },
{ name: "CLI binary", run: checkInstall },
{ name: "Node.js", run: checkNode },
{ name: "CPU", run: checkCPU },
{ name: "Memory", run: checkMemory },
Expand Down
Loading
Loading