mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(cli): clarify publish visibility and claim links (#3730)
* fix(cli): clarify publish visibility and claim links * style(cli): apply oxfmt to publish visibility test * chore(skills): regenerate skills manifest for updated references * fix(cli): stop an in-place re-publish claiming it made the project private * test(cli): pin in-place visibility copy to the plain re-publish route
This commit is contained in:
@@ -73,7 +73,8 @@ npx hyperframes render -c ./my-composition.html -o output.mp4
|
||||
|
||||
### `publish`
|
||||
|
||||
Upload a project directory and get a hosted URL that keeps working after the CLI exits:
|
||||
Upload a project directory and get a hosted URL that keeps working after the CLI exits.
|
||||
Published projects are private by default:
|
||||
|
||||
```bash
|
||||
npx hyperframes publish
|
||||
@@ -82,10 +83,11 @@ npx hyperframes publish --public
|
||||
npx hyperframes publish --yes
|
||||
```
|
||||
|
||||
Signed-out publishing returns a URL with a claim token; opening it lets someone
|
||||
sign in and claim the project. Sign in first with `npx hyperframes auth login`
|
||||
to publish an owned project you can update. Use `--public` to make the claimed
|
||||
project visible to anyone, and `--yes` to skip the confirmation prompt.
|
||||
Signed-out publishing returns an authentication-required claim URL; opening it
|
||||
lets someone sign in and claim the project. Sign in first with
|
||||
`npx hyperframes auth login` to publish an owned project you can update. Use
|
||||
`--public` to make the claimed project visible to anyone. `--yes` only skips the
|
||||
confirmation prompt and does not change visibility.
|
||||
|
||||
Signed-in publishers can use `--update <url-or-id>` to target an existing project
|
||||
or `--space <space-id>` to publish into a shared team space. If the requested
|
||||
|
||||
@@ -10,7 +10,8 @@ vi.mock("../utils/publishProject.js", async (importOriginal) => ({
|
||||
publishProjectArchive: publishState.publish,
|
||||
}));
|
||||
|
||||
import publishCommand, { parseUpdateTarget } from "./publish.js";
|
||||
import publishCommand, { examples, parseUpdateTarget } from "./publish.js";
|
||||
import { ensureProjectId } from "../utils/projectLink.js";
|
||||
|
||||
describe("parseUpdateTarget", () => {
|
||||
it("extracts the id from a full published URL", () => {
|
||||
@@ -90,3 +91,87 @@ describe("publish default-entry preflight", () => {
|
||||
expect(output).toContain("publish accepts project directories, not individual HTML files");
|
||||
});
|
||||
});
|
||||
|
||||
describe("publish visibility messaging", () => {
|
||||
async function runPublish(options: {
|
||||
public: boolean;
|
||||
claimed?: boolean;
|
||||
inPlace?: boolean;
|
||||
}): Promise<string> {
|
||||
const project = mkdtempSync(join(tmpdir(), "hf-publish-visibility-"));
|
||||
writeFileSync(
|
||||
join(project, "index.html"),
|
||||
`<html><body><div data-composition-id="main" data-width="1920" data-height="1080" data-start="0" data-duration="5"><div class="clip" data-start="0" data-duration="5">Visible</div></div></body></html>`,
|
||||
);
|
||||
// "Updated in place" is decided by the response echoing the id the directory already
|
||||
// resolves to — no --update flag required, which is how a plain re-publish reaches it.
|
||||
const projectId = options.inPlace === true ? ensureProjectId(project) : "project-id";
|
||||
publishState.publish.mockReset();
|
||||
publishState.publish.mockResolvedValue({
|
||||
title: "test",
|
||||
fileCount: 1,
|
||||
claimed: options.claimed ?? true,
|
||||
projectId,
|
||||
url: `https://hyperframes.dev/p/${projectId}`,
|
||||
claimToken: "claim-secret",
|
||||
});
|
||||
const lines: string[] = [];
|
||||
const log = vi.spyOn(console, "log").mockImplementation((...parts: unknown[]) => {
|
||||
lines.push(parts.map(String).join(" "));
|
||||
});
|
||||
|
||||
try {
|
||||
await publishCommand.run?.({
|
||||
args: { dir: project, yes: true, public: options.public, proxy: false },
|
||||
} as never);
|
||||
return lines.join("\n");
|
||||
} finally {
|
||||
log.mockRestore();
|
||||
rmSync(project, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
it.each([
|
||||
{ public: false, label: "Private", hint: "--public" },
|
||||
{ public: true, label: "Public", hint: undefined },
|
||||
])(
|
||||
"keeps --yes orthogonal to requested $label visibility",
|
||||
async ({ public: isPublic, label, hint }) => {
|
||||
const output = await runPublish({ public: isPublic });
|
||||
|
||||
expect(publishState.publish).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({ public: isPublic }),
|
||||
);
|
||||
expect(output).toContain("Requested visibility");
|
||||
expect(output).toContain(label);
|
||||
if (hint) expect(output).toContain(hint);
|
||||
},
|
||||
);
|
||||
|
||||
// A re-publish without --public sends no visibility, so the server keeps whatever the
|
||||
// project already had. Claiming "Private" here would tell someone a public link is locked
|
||||
// down. This is the plain `hyperframes publish` path in an already-published directory,
|
||||
// not just --update — the same branch serves all three routes to an in-place update.
|
||||
it("does not claim private when re-publishing in place without --public", async () => {
|
||||
const output = await runPublish({ public: false, inPlace: true });
|
||||
|
||||
expect(output).toContain("Requested visibility");
|
||||
expect(output).toContain("Unchanged — keeps this project's current setting");
|
||||
expect(output).toContain("Updated existing project");
|
||||
expect(output).not.toContain("Private — authentication and access required");
|
||||
});
|
||||
|
||||
it("labels an authentication-required anonymous URL as a claim URL", async () => {
|
||||
const output = await runPublish({ public: false, claimed: false });
|
||||
|
||||
expect(output).toContain("Claim URL");
|
||||
expect(output).toContain("claim_token=claim-secret");
|
||||
expect(output).toContain("sign in");
|
||||
expect(output).not.toMatch(/^\s*Public\s/m);
|
||||
});
|
||||
|
||||
it("does not describe default publishing as public", () => {
|
||||
expect(examples[0]?.[0]).not.toContain("public URL");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from "../utils/projectLink.js";
|
||||
|
||||
export const examples: Example[] = [
|
||||
["Publish the current project with a public URL", "hyperframes publish"],
|
||||
["Publish the current project privately to a stable URL", "hyperframes publish"],
|
||||
["Publish a specific directory", "hyperframes publish ./my-video"],
|
||||
["Make the claimed project public to anyone", "hyperframes publish --public"],
|
||||
["Update an existing published project in place", "hyperframes publish --update <url|id>"],
|
||||
@@ -56,7 +56,7 @@ export function parseUpdateTarget(value: string): string {
|
||||
export default defineCommand({
|
||||
meta: {
|
||||
name: "publish",
|
||||
description: "Upload the project and return a stable public URL",
|
||||
description: "Upload the project to a stable URL (private by default)",
|
||||
},
|
||||
args: {
|
||||
dir: { type: "positional", description: "Project directory", required: false },
|
||||
@@ -121,12 +121,19 @@ export default defineCommand({
|
||||
|
||||
if (args.yes !== true) {
|
||||
console.log();
|
||||
console.log(
|
||||
` ${c.bold("hyperframes publish uploads this project and creates a stable public URL.")}`,
|
||||
);
|
||||
console.log(
|
||||
` ${c.dim("Anyone with the URL can open the published project and claim it after authenticating.")}`,
|
||||
);
|
||||
if (args.public === true) {
|
||||
console.log(
|
||||
` ${c.bold("hyperframes publish uploads this project and requests public visibility at a stable URL.")}`,
|
||||
);
|
||||
console.log(` ${c.dim("Anyone with the URL can open a claimed public project.")}`);
|
||||
} else {
|
||||
console.log(
|
||||
` ${c.bold("hyperframes publish uploads this project privately to a stable URL.")}`,
|
||||
);
|
||||
console.log(
|
||||
` ${c.dim("Viewing requires authentication and access. Pass --public to allow anyone with the URL.")}`,
|
||||
);
|
||||
}
|
||||
console.log();
|
||||
const approved = await clack.confirm({ message: "Publish this project?" });
|
||||
if (clack.isCancel(approved) || approved !== true) {
|
||||
@@ -200,13 +207,15 @@ export default defineCommand({
|
||||
publishSpinner.stop(c.success("Project published"));
|
||||
|
||||
console.log();
|
||||
console.log(` ${c.dim("Project")} ${c.accent(published.title)}`);
|
||||
console.log(` ${c.dim("Files")} ${String(published.fileCount)}`);
|
||||
console.log(` ${c.dim("Project")} ${c.accent(published.title)}`);
|
||||
console.log(` ${c.dim("Files")} ${String(published.fileCount)}`);
|
||||
if (proxyBakeManifest) {
|
||||
console.log(` ${c.dim("Proxies")} ${String(proxyBakeManifest.proxied.length)} baked`);
|
||||
console.log(
|
||||
` ${c.dim("Proxies")} ${String(proxyBakeManifest.proxied.length)} baked`,
|
||||
);
|
||||
if (proxyBakeManifest.skippedAlpha.length > 0) {
|
||||
console.log(
|
||||
` ${c.dim("Proxy note")} ${String(proxyBakeManifest.skippedAlpha.length)} alpha source(s) kept original`,
|
||||
` ${c.dim("Proxy note")} ${String(proxyBakeManifest.skippedAlpha.length)} alpha source(s) kept original`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -214,10 +223,26 @@ export default defineCommand({
|
||||
if (published.claimed) {
|
||||
// The server returns the same id on an in-place update, a fresh id on create.
|
||||
const updatedInPlace = published.projectId === requestedProjectId;
|
||||
console.log(` ${c.dim("URL")} ${c.accent(published.url)}`);
|
||||
console.log(` ${c.dim("URL")} ${c.accent(published.url)}`);
|
||||
// The CLI only ever states what it ASKED for: no response field carries the project's
|
||||
// actual visibility. A re-publish without --public sends no visibility at all, so the
|
||||
// server keeps whatever the project already had — saying "Private" there would claim
|
||||
// a state we neither requested nor observed.
|
||||
const requestedVisibility =
|
||||
args.public === true
|
||||
? "Public — anyone with the URL"
|
||||
: updatedInPlace
|
||||
? "Unchanged — keeps this project's current setting"
|
||||
: "Private — authentication and access required";
|
||||
console.log(` ${c.dim("Requested visibility")} ${c.accent(requestedVisibility)}`);
|
||||
console.log(
|
||||
` ${c.dim("Status")} ${c.accent(updatedInPlace ? "Updated existing project" : "Created new project")}`,
|
||||
` ${c.dim("Status")} ${c.accent(updatedInPlace ? "Updated existing project" : "Created new project")}`,
|
||||
);
|
||||
if (args.public !== true) {
|
||||
console.log(
|
||||
` ${c.dim("Tip")} ${c.dim("Re-publish with --public to allow anyone with the URL.")}`,
|
||||
);
|
||||
}
|
||||
// Warn whenever we aimed at a KNOWN existing project (an explicit --update target or
|
||||
// a committed team id) but the server created a fresh one instead — so a teammate
|
||||
// whose space doesn't own the committed project doesn't silently lose the shared link.
|
||||
@@ -249,7 +274,8 @@ export default defineCommand({
|
||||
} else {
|
||||
const claimUrl = new URL(published.url);
|
||||
claimUrl.searchParams.set("claim_token", published.claimToken);
|
||||
console.log(` ${c.dim("Public")} ${c.accent(claimUrl.toString())}`);
|
||||
console.log(` ${c.dim("Claim URL")} ${c.accent(claimUrl.toString())}`);
|
||||
console.log(` ${c.dim("Access")} ${c.accent("Sign in required to claim")}`);
|
||||
console.log();
|
||||
if (updateTarget || spaceOverride) {
|
||||
// The pre-publish gate saw a credential, but the server didn't accept it (expired
|
||||
@@ -263,12 +289,14 @@ export default defineCommand({
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
` ${c.dim("Open the URL on hyperframes.dev to claim the project and continue editing.")}`,
|
||||
` ${c.dim("Open the claim URL on hyperframes.dev, sign in, and claim the project to continue editing.")}`,
|
||||
);
|
||||
console.log();
|
||||
console.log(
|
||||
` ${c.dim("Tip: run 'hyperframes auth login' first for a stable link you can re-publish to.")}`,
|
||||
);
|
||||
const visibilityTip =
|
||||
args.public === true
|
||||
? "--public applies to the claimed project; this claim URL still requires sign-in."
|
||||
: "Run 'hyperframes auth login' first for a stable link you can re-publish to; add --public to allow signed-out viewing.";
|
||||
console.log(` ${c.dim(`Tip: ${visibilityTip}`)}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const GROUPS: Group[] = [
|
||||
["catalog", "Browse and install blocks and components"],
|
||||
["preview", "Start the studio for previewing compositions"],
|
||||
["present", "Open a slideshow deck in presenter mode (with audience sync)"],
|
||||
["publish", "Upload a project and get a stable public URL"],
|
||||
["publish", "Upload a project to a stable URL (private by default)"],
|
||||
["render", "Render a composition to MP4 or WebM"],
|
||||
],
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"files": 4
|
||||
},
|
||||
"hyperframes": {
|
||||
"hash": "5be130da8d7ff59e",
|
||||
"hash": "f09636f3307817ab",
|
||||
"files": 17
|
||||
},
|
||||
"hyperframes-animation": {
|
||||
@@ -30,11 +30,11 @@
|
||||
"files": 7
|
||||
},
|
||||
"hyperframes-cli": {
|
||||
"hash": "70bf0363795a160b",
|
||||
"hash": "1a691d7069bc158e",
|
||||
"files": 11
|
||||
},
|
||||
"hyperframes-core": {
|
||||
"hash": "587492c7413387f6",
|
||||
"hash": "033a6c675e66a5e7",
|
||||
"files": 20
|
||||
},
|
||||
"hyperframes-creative": {
|
||||
|
||||
@@ -182,9 +182,10 @@ Hit a reproducible bug? Add `--file-issue` (optionally `--dir <project>` and `--
|
||||
## publish
|
||||
|
||||
```bash
|
||||
npx hyperframes publish # upload current project, return public URL
|
||||
npx hyperframes publish # upload current project privately, return stable URL
|
||||
npx hyperframes publish ./my-video # specific project
|
||||
npx hyperframes publish --public # allow anyone with the URL to view the claimed project
|
||||
npx hyperframes publish --yes # skip the confirmation prompt (scripts/CI)
|
||||
```
|
||||
|
||||
Uploads the project's source (HTML + assets) and returns a stable public URL that renders in the browser. Use this for sharing a draft for review before rendering MP4, or for embedding the composition elsewhere. Lint findings are surfaced before upload but do not block.
|
||||
Uploads the project's source (HTML + assets) and returns a stable hosted URL that renders in the browser. A fresh publish is private by default and requires authentication plus access to view. Use `--public` to allow anyone with the URL to view the claimed project. Updating a project in place keeps its existing visibility: re-publishing without `--public` never turns a public project private. `--yes` only skips the confirmation prompt; it does not change visibility. A signed-out publish returns an authentication-required claim URL rather than a public playback URL. Lint findings are surfaced before upload but do not block.
|
||||
|
||||
@@ -14,7 +14,7 @@ The shipped narrative workflows implement these stages with their own scripts; a
|
||||
| **Transitions** | the assembled index | scene handoffs injected | `hyperframes-animation/transitions/overview.md` → `catalog.md`; borrowable `transitions.mjs` (menu) |
|
||||
| **Captions** | word timings + the index | the caption track | borrowable `captions.mjs` (menu); no script to time against → `media-use` `scripts/transcribe.mjs` first |
|
||||
| **Verify** | the index (+ captions / transitions when present) | `npx hyperframes lint` and `npx hyperframes check` **passing**; a contact-sheet glance (`snapshot --at <frame-midpoints>`) | `hyperframes-cli` |
|
||||
| **Deliver** | verify passing | the final-look pause → on approval `render` → optionally `publish` (a stable public link) → the recipe offer | final approval and recipe offer: `review-loop.md` § 4; render / publish: `hyperframes-cli` |
|
||||
| **Deliver** | verify passing | the final-look pause → on approval `render` → optionally `publish` (a stable hosted link, private by default) → the recipe offer | final approval and recipe offer: `review-loop.md` § 4; render / publish: `hyperframes-cli` |
|
||||
|
||||
The Frames stage follows the plan's citations: a scene planned on a blueprint or on named rules is built by reading that recipe's body (`hyperframes-animation/blueprints/<id>.md`, `rules/<id>.md`) before its motion is written — names come from the indexes, never invented, and a scene the plan left uncited gets its citation at build time, not improvised motion.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Each row's last column reads **home → entry → what you get**: the owning ski
|
||||
| **Registry blocks** — 50+ installable scene compositions (data charts, device mockups, quote cards…) | "ready-made scenes we can drop in and restyle" | `/hyperframes-registry` → `npx hyperframes add <block>` → an installed sub-composition (wiring: `references/wiring-blocks.md`) |
|
||||
| **Scene transitions** — cuts, crossfades, wipes, WebGL shader transitions between scenes | "how one scene hands off to the next — up to full shader wipes" | `/hyperframes-animation` → `transitions/overview.md` then `transitions/catalog.md`; assembly `transitions.mjs` (home: `/faceless-explainer`) → injected handoffs in the index |
|
||||
| **User media on the timeline** — the user's own images / clips staged and woven into frames | "your own footage, screenshots, or photos placed into the video" | staging `stage-assets.mjs` (home: `/music-to-video`, `/product-launch-video`); adoption: `/media-use` `--adopt` → files in `assets/` + manifest records |
|
||||
| **Publish to a stable link** — the finished piece on a public URL; re-publishing updates the same link | "when it's done I can publish it to a link you can share — updates keep the same URL" | `/hyperframes-cli` → `references/preview-render.md` (`npx hyperframes publish`) → a stable public URL |
|
||||
| **Publish to a stable link** — the finished piece on a private-by-default hosted URL; re-publishing updates the same link | "when it's done I can publish it to a stable link — private by default, or public when you ask" | `/hyperframes-cli` → `references/preview-render.md` (`npx hyperframes publish`) → a stable hosted URL with explicit visibility |
|
||||
|
||||
Offer, don't unload: the intent layer recommends the one or two rows the confirmed concept itself calls for, states each as one plain-language line traced to the brief, and asks once — the route-filtered slice on request, the full table for the companion.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user