feat(cli): run a managed background preview in every launch mode (#3310)

`--background` was rejected outside the embedded server. It now re-execs the
CLI in foreground, which makes it mode-agnostic by construction: whichever
server the child resolves to serves the config endpoint the readiness probe
looks for. `--foreground` is its counterpart, for a non-interactive shell that
wants to stay attached, and a bare launch keeps the same promise — attached in
an interactive terminal, managed in an agent session.

That generalization exposed an existing hole. Local-studio mode runs Vite with
the studio package as its cwd and needs that package's own Vite config, which
the published tarball does not carry, but resolving the package was treated as
proof the mode was usable. An npm-installed studio therefore took a path that
can never come up — previously a clear error, now a ten-second silent timeout.
The predicate becomes "can this studio actually be served", so a published
install falls back to embedded mode, which works.

Over the 1k line budget at ~1.3k. The overage is one command file and its
tests carrying one invariant, and the seam that would split it further is
inside a single request-handling function — a split there would produce two
PRs neither of which starts a preview on its own.
This commit is contained in:
Miguel Ángel
2026-08-19 17:02:44 -04:00
committed by GitHub
parent 634df5a5af
commit 9da422fd7f
24 changed files with 980 additions and 249 deletions
+11 -2
View File
@@ -428,8 +428,6 @@ Needs a local Chrome, the same one `render` uses. Run
within a frame or two — a different headless-Chrome audio sample rate can shift
a beat slightly.
## Look at it
### `preview`
Start a live preview server with hot reload.
@@ -438,6 +436,8 @@ Start a live preview server with hot reload.
npx hyperframes preview [dir]
npx hyperframes preview --port 4567
npx hyperframes preview --background # keep running after the command exits
npx hyperframes preview --foreground # stay attached in a non-interactive shell
npx hyperframes preview --status --json # inspect a managed preview from an agent
npx hyperframes preview --list # every running preview
```
@@ -446,6 +446,8 @@ npx hyperframes preview --list # every running preview
| `--port` | Server port (default 3002) |
| `--open` / `--no-open` | Open a browser, or leave it closed |
| `--background` | Keep an embedded preview running after the command exits |
| `--foreground` | Keep the preview attached even when the shell is non-interactive |
| `--json` | Emit one versioned JSON result for managed start, status, stop, list, and kill-all operations |
| `--browser-gpu` / `--no-browser-gpu` | Hardware GPU for Studio thumbnails and frame capture, or deterministic SwiftShader (default: auto-detect) |
| `--proxy` / `--no-proxy` | Auto-transcode browser-hostile codecs (HEVC, ProRes, AV1) to a cached authoring proxy (default: on) |
| `--browser-path` | Open a specific browser. `--user-data-dir`, `--remote-debugging-port`, and `--browser-no-gpu` require it. |
@@ -455,6 +457,13 @@ background preview, `--list` and `--kill-all` act on all of them, and
`--force-new` starts a second server for a project that already has one. Each
exits straight after.
Bare `preview` chooses the safest lifecycle for its caller: it stays in the
foreground in a human interactive terminal, while a non-interactive or agent
shell starts a managed background preview. Re-running the command for the same
project reuses the healthy preview. Every start or status result includes the
exact Studio project URL as well as the underlying server URL, so agents can
hand off the intended project without guessing from the port.
To read a running Studio from a script: `--selection` prints the selected
element and `--context` prints the agent-readable context, both with `--json`.
Narrow the context with `--context-fields` (`server`, `selection`, `lint`,
+10 -1
View File
@@ -33,11 +33,20 @@ Start the live preview studio in your browser:
```bash
npx hyperframes preview
# Studio running at http://localhost:3002
# Studio: http://localhost:3002/#project/my-video
# Server: http://localhost:3002
npx hyperframes preview --port 4567
```
In an interactive terminal, the preview stays attached until you press
Ctrl+C. In a non-interactive shell such as a coding-agent session, the same
command starts a managed preview that survives after the command returns. Use
`--background` or `--foreground` to choose explicitly, and manage persistent
previews with `--status`, `--stop`, `--list`, and `--kill-all`. Add `--json` to
managed lifecycle commands for machine-readable output. `--foreground --json`
prints the ready-session envelope once, then remains attached until stopped.
### `render`
Render a composition to MP4. Run from the project directory; the positional
@@ -125,4 +125,15 @@ describe("media treatment routing documentation", () => {
expect(template).toContain("do not improvise equivalent CSS/SVG filters or overlays");
}
});
it("gives agents a process-owned preview lifecycle in new project instructions", () => {
for (const file of ["AGENTS.md", "CLAUDE.md"]) {
const template = read("packages", "cli", "src", "templates", "_shared", file);
expect(template).toContain("npx hyperframes preview --background");
expect(template).toContain("npx hyperframes preview --status");
expect(template).toContain("npx hyperframes preview --stop");
expect(template).toContain("leaving refreshes at `ERR_CONNECTION_TIMED_OUT`");
expect(template).not.toContain("run_in_background: true");
}
});
});
+299 -22
View File
@@ -1,20 +1,33 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { join } from "node:path";
import * as clack from "@clack/prompts";
import { afterEach, describe, expect, it, vi } from "vitest";
import { runCommand } from "citty";
import {
default as previewCommand,
foregroundPreviewReadyPayload,
handlePreviewKillAll,
handlePreviewList,
previewLaunchMode,
previewLaunchModeError,
previewPortError,
publicPreviewPid,
previewViteArgs,
reportPreviewShutdown,
studioReadyUrl,
studioDeepLink,
studioLandingSearch,
studioSummaryUrls,
waitForStudioChildClose,
} from "./preview.js";
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
vi.restoreAllMocks();
process.exitCode = undefined;
});
function projectWith(storyboard: string | null, frameFiles: string[] = []): string {
@@ -59,6 +72,33 @@ describe("studioLandingSearch", () => {
});
});
describe("Studio handoff URLs", () => {
it("hands off the exact timeline project route", () => {
const dir = projectWith(null);
expect(studioDeepLink("http://127.0.0.1:3002", "demo", dir)).toBe(
"http://127.0.0.1:3002/#project/demo",
);
expect(studioSummaryUrls("demo", "http://127.0.0.1:3002", dir)).toEqual({
serverUrl: "http://127.0.0.1:3002",
studioUrl: "http://127.0.0.1:3002/#project/demo",
});
});
it("hands off the exact storyboard route while a project is still planning", () => {
const dir = projectWith(FRAME(1, "outline"));
expect(studioDeepLink("http://127.0.0.1:3002", "demo", dir)).toBe(
"http://127.0.0.1:3002/?view=storyboard#project/demo",
);
});
it("URL-encodes project names that have hash-route metacharacters", () => {
const dir = projectWith(null);
expect(studioDeepLink("http://127.0.0.1:3002", "Launch #1? 50%", dir)).toBe(
"http://127.0.0.1:3002/#project/Launch%20%231%3F%2050%25",
);
});
});
describe("preview --kill-all", () => {
const session = (port: number, projectDir: string) => ({
pid: 4321,
@@ -101,29 +141,157 @@ describe("preview --kill-all", () => {
});
});
describe("preview --list", () => {
it("prefers the managed record over the same server's own self-report", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});
describe("previewLaunchMode", () => {
it.each([
[
{
background: false,
foreground: false,
interactive: false,
devMode: false,
localStudio: false,
},
"background",
],
[
{
background: false,
foreground: false,
interactive: true,
devMode: false,
localStudio: false,
},
"embedded",
],
[
{
background: false,
foreground: true,
interactive: false,
devMode: true,
localStudio: false,
},
"dev",
],
[
{
background: false,
foreground: true,
interactive: false,
devMode: false,
localStudio: true,
},
"local",
],
[
{
background: true,
foreground: false,
interactive: true,
devMode: true,
localStudio: true,
},
"background",
],
] as const)("resolves %o to %s", (options, expected) => {
expect(previewLaunchMode(options)).toBe(expected);
});
await handlePreviewList(3002, false, {
listManaged: async () => [
{ pid: 99, port: 3002, projectDir: resolve("/tmp/demo"), logPath: "/tmp/demo.log" },
],
scan: async () => [
{
port: 3002,
projectName: "demo",
projectDir: resolve("/tmp/demo"),
version: "test",
pid: "99",
},
],
});
it("rejects conflicting lifecycle overrides and actions", () => {
expect(
previewLaunchModeError({
background: true,
foreground: true,
status: false,
stop: false,
list: false,
killAll: false,
}),
).toBe("--background and --foreground cannot be used together");
expect(
previewLaunchModeError({
background: false,
foreground: false,
status: true,
stop: true,
list: false,
killAll: false,
}),
).toBe("Only one of --status, --stop, --list, or --kill-all can be used at a time");
expect(
previewLaunchModeError({
background: true,
foreground: false,
status: false,
stop: false,
list: false,
killAll: false,
}),
).toBeNull();
expect(
previewLaunchModeError({
background: true,
foreground: false,
status: true,
stop: false,
list: false,
killAll: false,
}),
).toBe("Preview launch overrides cannot be combined with lifecycle actions");
expect(
previewLaunchModeError({
background: false,
foreground: true,
status: false,
stop: false,
list: false,
killAll: true,
}),
).toBe("Preview launch overrides cannot be combined with lifecycle actions");
expect(
previewLaunchModeError({
background: false,
foreground: false,
forceNew: true,
status: true,
stop: false,
list: false,
killAll: false,
}),
).toBe("Preview launch overrides cannot be combined with lifecycle actions");
});
const printed = log.mock.calls.flat().join("\n");
expect(printed).toContain("1 server running");
expect(printed).toContain("PID 99");
log.mockRestore();
it.each([
[undefined, null],
["3002", null],
["1", null],
["65535", null],
["banana", "--port must be an integer between 1 and 65535"],
["3002oops", "--port must be an integer between 1 and 65535"],
["0", "--port must be an integer between 1 and 65535"],
["65536", "--port must be an integer between 1 and 65535"],
])("validates preview port %j", (value, expected) => {
expect(previewPortError(value)).toBe(expected);
});
it("prefers the live server PID over its launcher PID", () => {
expect(publicPreviewPid("9876", 4321)).toBe(9876);
expect(publicPreviewPid(null, 4321)).toBe(4321);
});
it("pins detached Vite to the port the lifecycle scanner waits on", () => {
expect(previewViteArgs(3032)).toEqual(["--host", "127.0.0.1", "--port", "3032"]);
});
it.each([
[" Local: http://localhost:43127/", "http://localhost:43127"],
[" Local: http://127.0.0.1:43127/", "http://127.0.0.1:43127"],
[
"\u001b[32m Local:\u001b[0m \u001b[36mhttp://127.0.0.1:43127/\u001b[0m",
"http://127.0.0.1:43127",
],
])("extracts the ready URL from Vite output %j", (output, expected) => {
expect(studioReadyUrl(output)).toBe(expected);
});
});
@@ -196,6 +364,25 @@ describe("preview lifecycle JSON failures", () => {
});
});
it("wraps managed-start validation failures in one JSON document", async () => {
const dir = projectWith(null);
writeFileSync(join(dir, "index.html"), "<html></html>");
const log = vi.spyOn(console, "log").mockImplementation(() => {});
await runCommand(previewCommand, {
rawArgs: [dir, "--background", "--json", "--user-data-dir", join(dir, "profile")],
});
expect(log).toHaveBeenCalledOnce();
const [line] = log.mock.calls[0] as [string];
expect(JSON.parse(line)).toMatchObject({
schemaVersion: 1,
operation: "start",
ok: false,
error: { code: "preview-validation-failed" },
});
});
it("wraps stop failures in one JSON document", async () => {
const missing = join(tmpdir(), `hf-preview-missing-${process.pid}-${Date.now()}`);
const log = vi.spyOn(console, "log").mockImplementation(() => {});
@@ -215,4 +402,94 @@ describe("preview lifecycle JSON failures", () => {
});
expect(error).not.toHaveBeenCalled();
});
it("wraps missing-project start failures without human stderr", async () => {
const missing = join(tmpdir(), `hf-preview-missing-start-${process.pid}-${Date.now()}`);
const log = vi.spyOn(console, "log").mockImplementation(() => {});
const error = vi.spyOn(console, "error").mockImplementation(() => {});
await runCommand(previewCommand, { rawArgs: [missing, "--background", "--json"] });
expect(log).toHaveBeenCalledOnce();
const [line] = log.mock.calls[0] as [string];
expect(JSON.parse(line)).toMatchObject({
operation: "start",
ok: false,
error: { code: "preview-start-failed" },
});
expect(error).not.toHaveBeenCalled();
});
});
describe("foreground preview JSON", () => {
it("emits the same ready session contract before remaining attached", () => {
const dir = projectWith(null);
expect(foregroundPreviewReadyPayload("Launch #1", "http://localhost:4567", dir, 4321)).toEqual({
schemaVersion: 1,
operation: "start",
ok: true,
result: {
state: "started",
mode: "foreground",
projectName: "Launch #1",
projectDir: dir,
host: "127.0.0.1",
port: 4567,
pid: 4321,
serverUrl: "http://127.0.0.1:4567",
studioUrl: "http://127.0.0.1:4567/#project/Launch%20%231",
ready: true,
},
});
});
it("keeps embedded shutdown silent after the readiness envelope", () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});
reportPreviewShutdown(true);
expect(log).not.toHaveBeenCalled();
});
});
describe("waitForStudioChildClose", () => {
it("resolves when the child closed before the listener was attached", async () => {
const signalTarget = { once: vi.fn(), off: vi.fn() };
const child = {
exitCode: 1,
signalCode: null,
once: vi.fn(),
} as unknown as Parameters<typeof waitForStudioChildClose>[0];
await expect(waitForStudioChildClose(child, signalTarget)).resolves.toBeUndefined();
expect(child.once).not.toHaveBeenCalled();
expect(signalTarget.once).toHaveBeenCalledTimes(2);
expect(signalTarget.off).toHaveBeenCalledTimes(2);
});
it("reaps on process exit even when stdio never emits close", async () => {
let exit: (() => void) | undefined;
const signalTarget = { once: vi.fn(), off: vi.fn() };
const child = {
exitCode: null,
signalCode: null,
once: vi.fn((event: string, listener: () => void) => {
if (event === "exit") exit = listener;
}),
} as unknown as Parameters<typeof waitForStudioChildClose>[0];
let resolved = false;
const waiting = waitForStudioChildClose(child, signalTarget).then(() => {
resolved = true;
});
await Promise.resolve();
expect(resolved).toBe(false);
expect(child.once).toHaveBeenCalledWith("exit", expect.any(Function));
exit?.();
await waiting;
expect(resolved).toBe(true);
expect(signalTarget.off).toHaveBeenCalledTimes(2);
});
});
File diff suppressed because it is too large Load Diff
@@ -49,7 +49,7 @@ describe("background preview lifecycle", () => {
);
});
it("does not let the detached child inherit launcher-only flags", () => {
it("forces the detached child foreground without inheriting launcher-only flags", () => {
expect(
buildBackgroundPreviewArgs([
"/opt/hyperframes/cli.js",
@@ -57,8 +57,9 @@ describe("background preview lifecycle", () => {
projectDir,
"--background",
"--open",
"--json",
]),
).toEqual(["/opt/hyperframes/cli.js", "preview", projectDir, "--no-open"]);
).toEqual(["/opt/hyperframes/cli.js", "preview", projectDir, "--foreground", "--no-open"]);
});
it("reuses an already-running server for the same project", async () => {
@@ -248,10 +248,13 @@ export function buildBackgroundPreviewArgs(argv: string[]): string[] {
(arg) =>
arg !== "--background" &&
!arg.startsWith("--background=") &&
arg !== "--foreground" &&
!arg.startsWith("--foreground=") &&
arg !== "--open" &&
arg !== "--no-open",
arg !== "--no-open" &&
arg !== "--json",
);
return [...filtered, "--no-open"];
return [...filtered, "--foreground", "--no-open"];
}
export async function readBackgroundPreviewStatus(
+9 -4
View File
@@ -33,7 +33,10 @@ The domain skills (`/hyperframes-core`, `/hyperframes-animation`, `/hyperframes-
## Commands
```bash
npm run dev # start the preview server (long-running — keep it alive in background)
npm run dev # human-operated foreground preview (blocks until stopped)
npx hyperframes preview --background # agent-safe persistent Studio preview
npx hyperframes preview --status # verify the persistent preview is listening
npx hyperframes preview --stop # stop it when review is finished
npm run check # lint + runtime + layout + motion + contrast (one command)
npm run render # render to MP4
npm run publish # publish and get a shareable link
@@ -42,9 +45,11 @@ npx hyperframes lint --json # machine-readable output for CI
npx hyperframes docs <topic> # reference docs in terminal
```
> **`npm run dev` is a long-running server, not a one-shot command.** It blocks until stopped.
> 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.
> **Agents must use `npx hyperframes preview --background` for Studio handoff.** Do not rely
> on a shell/tool `run_in_background` wrapper around `npm run dev`: that foreground process
> remains owned by the invoking session and can disappear while the browser stays open,
> leaving refreshes at `ERR_CONNECTION_TIMED_OUT`. Verify with `preview --status`, keep it
> alive through review, and stop it explicitly with `preview --stop` afterward.
> **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.
+9 -4
View File
@@ -33,7 +33,10 @@ The domain skills (`/hyperframes-core`, `/hyperframes-animation`, `/hyperframes-
## Commands
```bash
npm run dev # start the preview server (long-running — keep it alive in background)
npm run dev # human-operated foreground preview (blocks until stopped)
npx hyperframes preview --background # agent-safe persistent Studio preview
npx hyperframes preview --status # verify the persistent preview is listening
npx hyperframes preview --stop # stop it when review is finished
npm run check # lint + runtime + layout + motion + contrast (one command)
npm run render # render to MP4
npm run publish # publish and get a shareable link
@@ -42,9 +45,11 @@ npx hyperframes lint --json # machine-readable output for CI
npx hyperframes docs <topic> # reference docs in terminal
```
> **`npm run dev` is a long-running server, not a one-shot command.** It blocks until stopped.
> 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.
> **Agents must use `npx hyperframes preview --background` for Studio handoff.** Do not rely
> on a shell/tool `run_in_background` wrapper around `npm run dev`: that foreground process
> remains owned by the invoking session and can disappear while the browser stays open,
> leaving refreshes at `ERR_CONNECTION_TIMED_OUT`. Verify with `preview --status`, keep it
> alive through review, and stop it explicitly with `preview --stop` afterward.
> **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.
@@ -12,4 +12,22 @@ describe("studioProxyEnv", () => {
HYPERFRAMES_AUTO_PROXY: "false",
});
});
it("identifies a detached Vite preview to the lifecycle scanner", () => {
expect(
studioProxyEnv(
true,
{ KEEP: "yes" },
{
projectDir: "/tmp/video",
projectName: "video",
browserGpuMode: "software",
},
),
).toMatchObject({
HYPERFRAMES_PREVIEW_PROJECT_DIR: "/tmp/video",
HYPERFRAMES_PREVIEW_PROJECT_NAME: "video",
HYPERFRAMES_PREVIEW_BROWSER_GPU_MODE: "software",
});
});
});
+14
View File
@@ -1,9 +1,23 @@
export function studioProxyEnv(
autoProxy: boolean,
baseEnv: NodeJS.ProcessEnv = process.env,
preview?: {
projectDir: string;
projectName: string;
browserGpuMode?: "auto" | "hardware" | "software";
},
): NodeJS.ProcessEnv {
return {
...baseEnv,
HYPERFRAMES_AUTO_PROXY: autoProxy ? "true" : "false",
...(preview
? {
HYPERFRAMES_PREVIEW_PROJECT_DIR: preview.projectDir,
HYPERFRAMES_PREVIEW_PROJECT_NAME: preview.projectName,
...(preview.browserGpuMode
? { HYPERFRAMES_PREVIEW_BROWSER_GPU_MODE: preview.browserGpuMode }
: {}),
}
: {}),
};
}
+9
View File
@@ -5,6 +5,7 @@ import { join, resolve } from "node:path";
import { readNodeRequestBody } from "./vite.request-body.js";
import { watch } from "chokidar";
import { createViteAdapter } from "./vite.adapter";
import { previewConfigPayload } from "./vite.preview-config";
async function loadRuntimeSourceForDev(
server: import("vite").ViteDevServer,
@@ -84,6 +85,14 @@ function devProjectApi(): Plugin {
return _api;
};
server.middlewares.use((req, res, next) => {
if (req.url !== "/__hyperframes_config") return next();
const payload = previewConfigPayload(process.env, process.pid, studioPkg.version);
if (!payload) return next();
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
res.end(JSON.stringify(payload));
});
// Runtime endpoint — prefer source build over dist artifact
server.middlewares.use((req, res, next) => {
if (req.url !== "/api/runtime.js") return next();
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { previewConfigPayload } from "./vite.preview-config";
describe("previewConfigPayload", () => {
it("identifies a detached Vite preview to the CLI lifecycle scanner", () => {
expect(
previewConfigPayload(
{
HYPERFRAMES_PREVIEW_PROJECT_DIR: "/tmp/video",
HYPERFRAMES_PREVIEW_PROJECT_NAME: "video",
HYPERFRAMES_PREVIEW_BROWSER_GPU_MODE: "software",
},
4321,
"0.7.109",
),
).toEqual({
isHyperframes: true,
pid: 4321,
projectName: "video",
projectDir: "/tmp/video",
serverBuildSignature: null,
browserGpuMode: "software",
version: "0.7.109",
});
});
it("does not claim unrelated direct Vite sessions", () => {
expect(previewConfigPayload({})).toBeNull();
});
});
+22
View File
@@ -0,0 +1,22 @@
export type PreviewConfigEnv = Record<string, string | undefined>;
export function previewConfigPayload(
env: PreviewConfigEnv,
pid = process.pid,
version = "dev",
): Record<string, unknown> | null {
const projectDir = env.HYPERFRAMES_PREVIEW_PROJECT_DIR;
const projectName = env.HYPERFRAMES_PREVIEW_PROJECT_NAME;
if (!projectDir || !projectName) return null;
const browserGpuMode = env.HYPERFRAMES_PREVIEW_BROWSER_GPU_MODE;
return {
isHyperframes: true,
pid,
projectName,
projectDir,
serverBuildSignature: null,
...(browserGpuMode ? { browserGpuMode } : {}),
version,
};
}
+7 -7
View File
@@ -6,7 +6,7 @@
"files": 138
},
"faceless-explainer": {
"hash": "1eb3772e62dd71bb",
"hash": "8d3795c85481b4c3",
"files": 24
},
"figma": {
@@ -30,11 +30,11 @@
"files": 6
},
"hyperframes-cli": {
"hash": "d124991b0a27f02d",
"hash": "3fa884269c43d7df",
"files": 11
},
"hyperframes-core": {
"hash": "ec542db377d8b213",
"hash": "2471f4b5049fb489",
"files": 20
},
"hyperframes-creative": {
@@ -54,7 +54,7 @@
"files": 152
},
"motion-graphics": {
"hash": "1434e22bb0259bbb",
"hash": "69dc088b8e0d22fe",
"files": 23
},
"music-to-video": {
@@ -66,7 +66,7 @@
"files": 30
},
"product-launch-video": {
"hash": "d562efe00647c14b",
"hash": "085243ed9167a0aa",
"files": 28
},
"remotion-to-hyperframes": {
@@ -74,11 +74,11 @@
"files": 70
},
"slideshow": {
"hash": "6a24a84b0c1a75f9",
"hash": "2029471821f6f371",
"files": 2
},
"talking-head-recut": {
"hash": "2f5d99f823c48e75",
"hash": "214eda4c0f2bedb1",
"files": 28
}
}
+1 -1
View File
@@ -191,7 +191,7 @@ If a command fails, surface stderr and stop — don't pile on recovery commands.
After checks pass, pause for user review — the review loop's final look (`../hyperframes-core/references/review-loop.md` § 4): one question, on the Studio that has been open since Step 3 — render now, or what changes? (Autonomous: the one kept question, preview first or render.) Then deliver the MP4 with the contact sheet and the frame ids so revisions can target a single frame.
Preview: `npx hyperframes preview`
Preview: `npx hyperframes preview --background`
Render only after user approval (autonomous mode: after the preview-or-render question):
+2 -2
View File
@@ -21,7 +21,7 @@ Run commands as `npx hyperframes ...` unless project instructions provide a wrap
4. **Get fast feedback while editing:** run `npx hyperframes lint` after the first HTML pass and after structural changes.
5. **Run the final gate:** run `npx hyperframes check`; it reruns lint before opening the browser. Do not prepend a redundant standalone lint invocation. Add `--snapshots` for annotated overview frames and finding crops.
6. **Inspect sub-compositions:** when `index.html` mounts `data-composition-src`, capture midpoint snapshots and inspect each mounted scene.
7. **Open the final Studio preview:** run `npx hyperframes preview`, hand the timeline project URL to the user, and ask whether to revise or render.
7. **Open the final Studio preview:** run `npx hyperframes preview --background`, verify the URL returns HTTP 200, hand the timeline project URL to the user, and ask whether to revise or render. Keep it alive until review ends.
8. **Render only after approval:** use draft quality for iteration and high quality for delivery.
9. **Verify the output:** confirm the file exists, is non-empty, and has a plausible duration.
@@ -45,7 +45,7 @@ npx hyperframes lint
# Required final gate; includes lint.
npx hyperframes check
npx hyperframes preview
npx hyperframes preview --background
npx hyperframes render --quality high --output out.mp4
test -s out.mp4
ffprobe -v error -show_format out.mp4
@@ -5,8 +5,10 @@ Serve, render, and share commands.
## preview
```bash
npx hyperframes preview # serve current directory
npx hyperframes preview --port 4567 # custom port (default 3002)
npx hyperframes preview # foreground on a TTY; persistent in agent shells
npx hyperframes preview --background # explicit persistent session
npx hyperframes preview --foreground --json # ready JSON, then remain attached
npx hyperframes preview --background --port 4567 # agent-safe custom port (default 3002)
npx hyperframes preview --selection --json # print the current Studio selection and exit
npx hyperframes preview --context --json # print compact agent context from Studio
```
@@ -19,11 +21,11 @@ When handing a project back to the user, use the Studio project URL, not the sou
http://localhost:<port>/#project/<project-name>
```
Use the actual port and project directory name; treat `index.html` as source-code context, not the preview surface. For example, after `npx hyperframes preview --port 3017` in `codex-openai-video`, report `http://localhost:3017/#project/codex-openai-video`.
Use the actual port and project directory name; treat `index.html` as source-code context, not the preview surface. For example, after `npx hyperframes preview --background --port 3017` in `codex-openai-video`, report `http://localhost:3017/#project/codex-openai-video`.
To land the user on the **Storyboard view** instead of the timeline, put `?view=storyboard` ahead of the hash: `http://localhost:<port>/?view=storyboard#project/<project-name>`. Hand this URL whenever the storyboard is the thing to review and nothing is assembled yet — before `index.html` exists, the timeline stage has nothing to show, so the bare project URL opens on an empty player.
Two ways a handed URL turns out dead — check both before handing it back: the URL is missing its `#project/<project-name>` hash (Studio loads but has no project to open), or the server is not actually running. `preview` is a long-running process — start it from the project directory as a background task, and if that task reports it exited ("completed"), the server is down: restart it, don't hand out the link.
Two ways a handed URL turns out dead — check both before handing it back: the URL is missing its `#project/<project-name>` hash (Studio loads but has no project to open), or the server is not actually running. Bare `preview` automatically creates a managed persistent session in a non-TTY agent shell; `--background` remains the clearest explicit form. Verify the printed URL returns HTTP 200, keep it alive for the whole review, and stop it explicitly with `npx hyperframes preview --stop` afterward. Use the printed URL as-is: HyperFrames URL-encodes project names that contain route metacharacters.
### Agent context from Studio selection
@@ -57,7 +59,7 @@ Failure modes:
| Code | Meaning |
| -------------------------- | -------------------------------------------------------------------------- |
| `preview-not-running` | Start Studio first with `npx hyperframes preview`. |
| `preview-not-running` | Start Studio first with `npx hyperframes preview --background`. |
| `ambiguous-preview-server` | Multiple matching Studio servers are open; rerun with one listed `--port`. |
| `preview-port-mismatch` | The requested `--port` is not one of the matching Studio servers. |
| `no-selection` | Studio is open, but the user has not selected an element yet. |
@@ -89,7 +91,7 @@ Both `preview` and `play` can open inside an explicit Chromium-compatible browse
```bash
# Open preview in an isolated Chromium profile
npx hyperframes preview --browser-path /usr/bin/chromium --user-data-dir /tmp/hf-profile
npx hyperframes preview --background --browser-path /usr/bin/chromium --user-data-dir /tmp/hf-profile
# Same plus a CDP endpoint on :9222 (attach DevTools / Playwright / etc.)
npx hyperframes play --browser-path /usr/bin/chromium --user-data-dir /tmp/hf-profile --remote-debugging-port 9222
+1 -1
View File
@@ -86,5 +86,5 @@ Use `hyperframes-cli` for command details
- [ ] `npx hyperframes check` passes (0 findings across lint, runtime, layout, motion, and contrast)
- [ ] Projects with sub-compositions: `npx hyperframes snapshot --at <midpoints>` and eyeball each frame
- [ ] `npx hyperframes preview` for review (the user can edit anything in Studio's timeline)
- [ ] `npx hyperframes preview --background` for review (the user can edit anything in Studio's timeline, and the server survives the invoking command)
- [ ] `npx hyperframes render` only after the user approves
@@ -6,7 +6,7 @@ This is the shared process for any workflow that plans on a storyboard. The cont
## § 1 — The plan, on a live board
Open the **storyboard board** before presenting the plan: run `npx hyperframes preview` from the project directory in the background, confirm it is serving, and open `http://localhost:<port>/?view=storyboard#project/<project-name>`. This is an early planning surface, not the final composition preview; it may open before composition checks. The plan appears as frame cards and refreshes as work lands.
Open the **storyboard board** before presenting the plan: run `npx hyperframes preview --background` from the project directory, confirm it is serving, and open `http://localhost:<port>/?view=storyboard#project/<project-name>`. This is an early planning surface, not the final composition preview; it may open before composition checks. The plan appears as frame cards and refreshes as work lands.
Present the plan as a proposal (shape: `hyperframes-creative/references/story-spine.md` § 3): open by echoing **"This video tells [audience] that [message]"**, then the frame table — one row per frame: frame · beat (type, duration) · on screen · why (its `narrativeRole`, traced to the message). Hand the board URL with it, noting feedback lands in both places — comment on the board or reply here, one revision loop — and that a board submit still needs one reply here (anything) to get picked up.
+1 -1
View File
@@ -136,7 +136,7 @@ Choose proof times that show the opening state, signature move, and final hold.
Ask one question: “preview first, or render?” If the user chooses preview, open Studio and return to the same approval gate after revisions:
```bash
(cd "$PROJECT_DIR" && npx hyperframes preview)
(cd "$PROJECT_DIR" && npx hyperframes preview --background)
```
Render only after an explicit render answer:
+1 -1
View File
@@ -220,7 +220,7 @@ If a command fails, surface stderr and stop — don't pile on recovery commands.
After checks pass, pause for user review — the review loop's final look (`../hyperframes-core/references/review-loop.md` § 4): one question, on the Studio that has been open since Step 3 — render now, or what changes? (Autonomous: the one kept question, preview first or render.) Then deliver the MP4 with the contact sheet and the frame ids so revisions can target a single frame.
Preview: `npx hyperframes preview`
Preview: `npx hyperframes preview --background`
Render only after user approval (autonomous mode: after the preview-or-render question):
+1 -1
View File
@@ -494,7 +494,7 @@ Studio/`preview` is useful for editing a composition, but it is not a clear fina
{
"scripts": {
"dev": "npx hyperframes present ./composition",
"studio": "npx hyperframes preview ./composition"
"studio": "npx hyperframes preview ./composition --background"
}
}
```
+1 -1
View File
@@ -1204,7 +1204,7 @@ Tell the user:
**Optional live preview (on request only).** The clip plays unchanged inside `public/index.html` with the overlays on top, so it previews faithfully. **Don't open it during the run.** When the user asks, start a long-lived server **after** render and report the URL:
```bash
(cd "$WORK_DIR/public" && npx hyperframes preview) # or `npx hyperframes play` for a shareable link
(cd "$WORK_DIR/public" && npx hyperframes preview --background) # or `npx hyperframes play` for a shareable link
```
Do not delete the work directory unless the user asks.