Skip to content

Commit 672ea84

Browse files
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
1 parent 1fa31d5 commit 672ea84

8 files changed

Lines changed: 149 additions & 33 deletions

File tree

packages/cli/README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ npx hyperframes render -c ./my-composition.html -o output.mp4
7373

7474
### `publish`
7575

76-
Upload a project directory and get a hosted URL that keeps working after the CLI exits:
76+
Upload a project directory and get a hosted URL that keeps working after the CLI exits.
77+
Published projects are private by default:
7778

7879
```bash
7980
npx hyperframes publish
@@ -82,10 +83,11 @@ npx hyperframes publish --public
8283
npx hyperframes publish --yes
8384
```
8485

85-
Signed-out publishing returns a URL with a claim token; opening it lets someone
86-
sign in and claim the project. Sign in first with `npx hyperframes auth login`
87-
to publish an owned project you can update. Use `--public` to make the claimed
88-
project visible to anyone, and `--yes` to skip the confirmation prompt.
86+
Signed-out publishing returns an authentication-required claim URL; opening it
87+
lets someone sign in and claim the project. Sign in first with
88+
`npx hyperframes auth login` to publish an owned project you can update. Use
89+
`--public` to make the claimed project visible to anyone. `--yes` only skips the
90+
confirmation prompt and does not change visibility.
8991

9092
Signed-in publishers can use `--update <url-or-id>` to target an existing project
9193
or `--space <space-id>` to publish into a shared team space. If the requested

packages/cli/src/commands/publish.test.ts

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ vi.mock("../utils/publishProject.js", async (importOriginal) => ({
1010
publishProjectArchive: publishState.publish,
1111
}));
1212

13-
import publishCommand, { parseUpdateTarget } from "./publish.js";
13+
import publishCommand, { examples, parseUpdateTarget } from "./publish.js";
14+
import { ensureProjectId } from "../utils/projectLink.js";
1415

1516
describe("parseUpdateTarget", () => {
1617
it("extracts the id from a full published URL", () => {
@@ -90,3 +91,87 @@ describe("publish default-entry preflight", () => {
9091
expect(output).toContain("publish accepts project directories, not individual HTML files");
9192
});
9293
});
94+
95+
describe("publish visibility messaging", () => {
96+
async function runPublish(options: {
97+
public: boolean;
98+
claimed?: boolean;
99+
inPlace?: boolean;
100+
}): Promise<string> {
101+
const project = mkdtempSync(join(tmpdir(), "hf-publish-visibility-"));
102+
writeFileSync(
103+
join(project, "index.html"),
104+
`<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>`,
105+
);
106+
// "Updated in place" is decided by the response echoing the id the directory already
107+
// resolves to — no --update flag required, which is how a plain re-publish reaches it.
108+
const projectId = options.inPlace === true ? ensureProjectId(project) : "project-id";
109+
publishState.publish.mockReset();
110+
publishState.publish.mockResolvedValue({
111+
title: "test",
112+
fileCount: 1,
113+
claimed: options.claimed ?? true,
114+
projectId,
115+
url: `https://hyperframes.dev/p/${projectId}`,
116+
claimToken: "claim-secret",
117+
});
118+
const lines: string[] = [];
119+
const log = vi.spyOn(console, "log").mockImplementation((...parts: unknown[]) => {
120+
lines.push(parts.map(String).join(" "));
121+
});
122+
123+
try {
124+
await publishCommand.run?.({
125+
args: { dir: project, yes: true, public: options.public, proxy: false },
126+
} as never);
127+
return lines.join("\n");
128+
} finally {
129+
log.mockRestore();
130+
rmSync(project, { recursive: true, force: true });
131+
}
132+
}
133+
134+
it.each([
135+
{ public: false, label: "Private", hint: "--public" },
136+
{ public: true, label: "Public", hint: undefined },
137+
])(
138+
"keeps --yes orthogonal to requested $label visibility",
139+
async ({ public: isPublic, label, hint }) => {
140+
const output = await runPublish({ public: isPublic });
141+
142+
expect(publishState.publish).toHaveBeenCalledWith(
143+
expect.any(String),
144+
expect.objectContaining({ public: isPublic }),
145+
);
146+
expect(output).toContain("Requested visibility");
147+
expect(output).toContain(label);
148+
if (hint) expect(output).toContain(hint);
149+
},
150+
);
151+
152+
// A re-publish without --public sends no visibility, so the server keeps whatever the
153+
// project already had. Claiming "Private" here would tell someone a public link is locked
154+
// down. This is the plain `hyperframes publish` path in an already-published directory,
155+
// not just --update — the same branch serves all three routes to an in-place update.
156+
it("does not claim private when re-publishing in place without --public", async () => {
157+
const output = await runPublish({ public: false, inPlace: true });
158+
159+
expect(output).toContain("Requested visibility");
160+
expect(output).toContain("Unchanged — keeps this project's current setting");
161+
expect(output).toContain("Updated existing project");
162+
expect(output).not.toContain("Private — authentication and access required");
163+
});
164+
165+
it("labels an authentication-required anonymous URL as a claim URL", async () => {
166+
const output = await runPublish({ public: false, claimed: false });
167+
168+
expect(output).toContain("Claim URL");
169+
expect(output).toContain("claim_token=claim-secret");
170+
expect(output).toContain("sign in");
171+
expect(output).not.toMatch(/^\s*Public\s/m);
172+
});
173+
174+
it("does not describe default publishing as public", () => {
175+
expect(examples[0]?.[0]).not.toContain("public URL");
176+
});
177+
});

packages/cli/src/commands/publish.ts

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
} from "../utils/projectLink.js";
2929

3030
export const examples: Example[] = [
31-
["Publish the current project with a public URL", "hyperframes publish"],
31+
["Publish the current project privately to a stable URL", "hyperframes publish"],
3232
["Publish a specific directory", "hyperframes publish ./my-video"],
3333
["Make the claimed project public to anyone", "hyperframes publish --public"],
3434
["Update an existing published project in place", "hyperframes publish --update <url|id>"],
@@ -56,7 +56,7 @@ export function parseUpdateTarget(value: string): string {
5656
export default defineCommand({
5757
meta: {
5858
name: "publish",
59-
description: "Upload the project and return a stable public URL",
59+
description: "Upload the project to a stable URL (private by default)",
6060
},
6161
args: {
6262
dir: { type: "positional", description: "Project directory", required: false },
@@ -121,12 +121,19 @@ export default defineCommand({
121121

122122
if (args.yes !== true) {
123123
console.log();
124-
console.log(
125-
` ${c.bold("hyperframes publish uploads this project and creates a stable public URL.")}`,
126-
);
127-
console.log(
128-
` ${c.dim("Anyone with the URL can open the published project and claim it after authenticating.")}`,
129-
);
124+
if (args.public === true) {
125+
console.log(
126+
` ${c.bold("hyperframes publish uploads this project and requests public visibility at a stable URL.")}`,
127+
);
128+
console.log(` ${c.dim("Anyone with the URL can open a claimed public project.")}`);
129+
} else {
130+
console.log(
131+
` ${c.bold("hyperframes publish uploads this project privately to a stable URL.")}`,
132+
);
133+
console.log(
134+
` ${c.dim("Viewing requires authentication and access. Pass --public to allow anyone with the URL.")}`,
135+
);
136+
}
130137
console.log();
131138
const approved = await clack.confirm({ message: "Publish this project?" });
132139
if (clack.isCancel(approved) || approved !== true) {
@@ -200,24 +207,42 @@ export default defineCommand({
200207
publishSpinner.stop(c.success("Project published"));
201208

202209
console.log();
203-
console.log(` ${c.dim("Project")} ${c.accent(published.title)}`);
204-
console.log(` ${c.dim("Files")} ${String(published.fileCount)}`);
210+
console.log(` ${c.dim("Project")} ${c.accent(published.title)}`);
211+
console.log(` ${c.dim("Files")} ${String(published.fileCount)}`);
205212
if (proxyBakeManifest) {
206-
console.log(` ${c.dim("Proxies")} ${String(proxyBakeManifest.proxied.length)} baked`);
213+
console.log(
214+
` ${c.dim("Proxies")} ${String(proxyBakeManifest.proxied.length)} baked`,
215+
);
207216
if (proxyBakeManifest.skippedAlpha.length > 0) {
208217
console.log(
209-
` ${c.dim("Proxy note")} ${String(proxyBakeManifest.skippedAlpha.length)} alpha source(s) kept original`,
218+
` ${c.dim("Proxy note")} ${String(proxyBakeManifest.skippedAlpha.length)} alpha source(s) kept original`,
210219
);
211220
}
212221
}
213222

214223
if (published.claimed) {
215224
// The server returns the same id on an in-place update, a fresh id on create.
216225
const updatedInPlace = published.projectId === requestedProjectId;
217-
console.log(` ${c.dim("URL")} ${c.accent(published.url)}`);
226+
console.log(` ${c.dim("URL")} ${c.accent(published.url)}`);
227+
// The CLI only ever states what it ASKED for: no response field carries the project's
228+
// actual visibility. A re-publish without --public sends no visibility at all, so the
229+
// server keeps whatever the project already had — saying "Private" there would claim
230+
// a state we neither requested nor observed.
231+
const requestedVisibility =
232+
args.public === true
233+
? "Public — anyone with the URL"
234+
: updatedInPlace
235+
? "Unchanged — keeps this project's current setting"
236+
: "Private — authentication and access required";
237+
console.log(` ${c.dim("Requested visibility")} ${c.accent(requestedVisibility)}`);
218238
console.log(
219-
` ${c.dim("Status")} ${c.accent(updatedInPlace ? "Updated existing project" : "Created new project")}`,
239+
` ${c.dim("Status")} ${c.accent(updatedInPlace ? "Updated existing project" : "Created new project")}`,
220240
);
241+
if (args.public !== true) {
242+
console.log(
243+
` ${c.dim("Tip")} ${c.dim("Re-publish with --public to allow anyone with the URL.")}`,
244+
);
245+
}
221246
// Warn whenever we aimed at a KNOWN existing project (an explicit --update target or
222247
// a committed team id) but the server created a fresh one instead — so a teammate
223248
// whose space doesn't own the committed project doesn't silently lose the shared link.
@@ -249,7 +274,8 @@ export default defineCommand({
249274
} else {
250275
const claimUrl = new URL(published.url);
251276
claimUrl.searchParams.set("claim_token", published.claimToken);
252-
console.log(` ${c.dim("Public")} ${c.accent(claimUrl.toString())}`);
277+
console.log(` ${c.dim("Claim URL")} ${c.accent(claimUrl.toString())}`);
278+
console.log(` ${c.dim("Access")} ${c.accent("Sign in required to claim")}`);
253279
console.log();
254280
if (updateTarget || spaceOverride) {
255281
// The pre-publish gate saw a credential, but the server didn't accept it (expired
@@ -263,12 +289,14 @@ export default defineCommand({
263289
);
264290
} else {
265291
console.log(
266-
` ${c.dim("Open the URL on hyperframes.dev to claim the project and continue editing.")}`,
292+
` ${c.dim("Open the claim URL on hyperframes.dev, sign in, and claim the project to continue editing.")}`,
267293
);
268294
console.log();
269-
console.log(
270-
` ${c.dim("Tip: run 'hyperframes auth login' first for a stable link you can re-publish to.")}`,
271-
);
295+
const visibilityTip =
296+
args.public === true
297+
? "--public applies to the claimed project; this claim URL still requires sign-in."
298+
: "Run 'hyperframes auth login' first for a stable link you can re-publish to; add --public to allow signed-out viewing.";
299+
console.log(` ${c.dim(`Tip: ${visibilityTip}`)}`);
272300
}
273301
console.log();
274302
}

packages/cli/src/help.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const GROUPS: Group[] = [
2525
["catalog", "Browse and install blocks and components"],
2626
["preview", "Start the studio for previewing compositions"],
2727
["present", "Open a slideshow deck in presenter mode (with audience sync)"],
28-
["publish", "Upload a project and get a stable public URL"],
28+
["publish", "Upload a project to a stable URL (private by default)"],
2929
["render", "Render a composition to MP4 or WebM"],
3030
],
3131
},

skills-manifest.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"files": 4
1919
},
2020
"hyperframes": {
21-
"hash": "5be130da8d7ff59e",
21+
"hash": "f09636f3307817ab",
2222
"files": 17
2323
},
2424
"hyperframes-animation": {
@@ -30,11 +30,11 @@
3030
"files": 7
3131
},
3232
"hyperframes-cli": {
33-
"hash": "70bf0363795a160b",
33+
"hash": "1a691d7069bc158e",
3434
"files": 11
3535
},
3636
"hyperframes-core": {
37-
"hash": "587492c7413387f6",
37+
"hash": "033a6c675e66a5e7",
3838
"files": 20
3939
},
4040
"hyperframes-creative": {

skills/hyperframes-cli/references/preview-render.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,10 @@ Hit a reproducible bug? Add `--file-issue` (optionally `--dir <project>` and `--
182182
## publish
183183

184184
```bash
185-
npx hyperframes publish # upload current project, return public URL
185+
npx hyperframes publish # upload current project privately, return stable URL
186186
npx hyperframes publish ./my-video # specific project
187+
npx hyperframes publish --public # allow anyone with the URL to view the claimed project
187188
npx hyperframes publish --yes # skip the confirmation prompt (scripts/CI)
188189
```
189190

190-
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.
191+
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.

skills/hyperframes-core/references/production-loop.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The shipped narrative workflows implement these stages with their own scripts; a
1414
| **Transitions** | the assembled index | scene handoffs injected | `hyperframes-animation/transitions/overview.md``catalog.md`; borrowable `transitions.mjs` (menu) |
1515
| **Captions** | word timings + the index | the caption track | borrowable `captions.mjs` (menu); no script to time against → `media-use` `scripts/transcribe.mjs` first |
1616
| **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` |
17-
| **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` |
17+
| **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` |
1818

1919
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.
2020

0 commit comments

Comments
 (0)