From 080b7c8e5d99ab72c70aa1f0eac8bac7080e7786 Mon Sep 17 00:00:00 2001 From: Terence Cho Date: Tue, 3 Nov 2026 19:43:16 -0800 Subject: [PATCH] feat(cli): stage the sixty-day publish transition --- README.md | 10 ++- docs/packages/cli.mdx | 7 +++ packages/cli/src/commands/publish.ts | 4 +- .../cli/src/share/publishTransition.test.ts | 23 +++++++ packages/cli/src/share/publishTransition.ts | 63 +++++++++++++++++++ skills-manifest.json | 2 +- skills/hyperframes-cli/SKILL.md | 6 +- .../references/preview-render.md | 19 ++++++ 8 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 packages/cli/src/share/publishTransition.test.ts create mode 100644 packages/cli/src/share/publishTransition.ts diff --git a/README.md b/README.md index 8d074d503a5..7fef0fced6b 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ This writes `dist/hyperframes-plugin.zip` with a `hyperframes/` root folder and | Skill | Use when | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/hyperframes` | **Read first** for any request to make, edit, animate or render a video, motion graphic, captions or overlays with HyperFrames (HTML rendered to video). Holds the render contract and the work loop, and points to the references for format, motion, look, captions, whole films and storyboards. | -| `/hyperframes-cli` | The HyperFrames CLI by job — scaffold, check, look at frames, edit the timeline, preview, render locally or in the cloud, publish, project history — plus reading its JSON and recovering from build or render failures. | +| `/hyperframes-cli` | The HyperFrames CLI by job — scaffold, check, look at frames, edit the timeline, preview, render locally or in the cloud, share, project history — plus reading its JSON and recovering from build or render failures. | | `/hyperframes-registry` | About 450 ready-made blocks and components installed as editable source — transitions, overlays, titles, kinetic type, captions, charts, maps, 3D, camera moves. Use before hand-building any named visual; also for `hyperframes add` and `catalog`. | | `/media-use` | Get or make any media for a video — music, sound effects, images, icons, logos, voiceover, avatar and generated video, transcripts, cutouts, colour grades and looks — with HeyGen or free local tools, and mix placed audio. Also for footage that looks dark, flat or dull. | @@ -360,3 +360,11 @@ GIT_LFS_SKIP_SMUDGE=1 git clone https://github.com/heygen-com/hyperframes.git ## License [Apache 2.0](LICENSE) + +### Share a video for review + +Sign in with `npx hyperframes auth login`, then run `npx hyperframes share` to +choose who can open the rendered review. Use `share --update` when edits should +become a new version and `share --manage` for web settings. CLI and Desktop use +the same sharing system and five free projects per workspace. Existing legacy +published links remain reachable. diff --git a/docs/packages/cli.mdx b/docs/packages/cli.mdx index b5d43d95121..26228649530 100644 --- a/docs/packages/cli.mdx +++ b/docs/packages/cli.mdx @@ -1063,6 +1063,13 @@ editor, just playback. Same flags, `--port` defaults to 3003, plus `--proxy` / ### `publish` +For new video reviews, use [`share`](#share). Legacy source publishing stays +available with a notice for 60 days from an announced notice release. That +release date has not been scheduled. After the window, new `publish` runs guide +you to `share`, where you choose a new audience; old flags are never silently +translated. Existing published URLs and legacy updates remain supported under +a separate retirement plan. + Upload the project and get back a stable `hyperframes.studio` URL. ```bash diff --git a/packages/cli/src/commands/publish.ts b/packages/cli/src/commands/publish.ts index f81c4d937ec..ff87a01ed9a 100644 --- a/packages/cli/src/commands/publish.ts +++ b/packages/cli/src/commands/publish.ts @@ -1,3 +1,4 @@ +import { checkPublishTransition } from "../share/publishTransition.js"; import { join, posix, relative, resolve } from "node:path"; import { failCommand, setCommandExitCode } from "../utils/commandResult.js"; import { existsSync } from "node:fs"; @@ -60,7 +61,7 @@ export function parseUpdateTarget(value: string): string { export default defineCommand({ meta: { name: "publish", - description: "Upload the project to a stable URL (private by default)", + description: "Legacy source publishing; use share for new video reviews", }, args: { dir: { type: "positional", description: "Project directory", required: false }, @@ -95,6 +96,7 @@ export default defineCommand({ }, }, async run({ args }) { + checkPublishTransition(args); if (args.yes !== true && isUnattended()) { clack.confirmationRequired("hyperframes publish", "Pass --yes to publish without asking."); } diff --git a/packages/cli/src/share/publishTransition.test.ts b/packages/cli/src/share/publishTransition.test.ts new file mode 100644 index 00000000000..ade4dfe2dc0 --- /dev/null +++ b/packages/cli/src/share/publishTransition.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { publishPhase, publishTransitionDecision } from "./publishTransition.js"; +describe("announced 60-day publish transition", () => { + const start = "2027-01-01T00:00:00.000Z"; + const day = 86_400_000; + it("never starts the clock from approval or installation", () => { + expect(publishPhase(null, Date.parse(start) + 365 * day)).toBe("unscheduled"); + expect(publishPhase(start, Date.parse(start) - 1)).toBe("unscheduled"); + }); + it("preserves exactly sixty days and guides only after the boundary", () => { + expect(publishPhase(start, Date.parse(start))).toBe("compatibility"); + expect(publishPhase(start, Date.parse(start) + 60 * day - 1)).toBe("compatibility"); + expect(publishPhase(start, Date.parse(start) + 60 * day)).toBe("share"); + }); + it("retains legacy updates after the new-project transition", () => { + expect(publishTransitionDecision("share", true)).toBe("keep_legacy"); + expect(publishTransitionDecision("share", false)).toBe("guide_to_share"); + expect(publishTransitionDecision("compatibility", false)).toBe("keep_legacy"); + }); + it("fails on an invalid scheduled date rather than silently switching behavior", () => { + expect(() => publishPhase("invalid")).toThrow("timestamp"); + }); +}); diff --git a/packages/cli/src/share/publishTransition.ts b/packages/cli/src/share/publishTransition.ts new file mode 100644 index 00000000000..7c2bea17478 --- /dev/null +++ b/packages/cli/src/share/publishTransition.ts @@ -0,0 +1,63 @@ +import { resolve } from "node:path"; +import { CliUsageError } from "../utils/commandResult.js"; +import { asNotice } from "../utils/jsonError.js"; +import { readProjectLink, readTeamProject } from "../utils/projectLink.js"; + +/** Set to the announced notice release's UTC timestamp in the release PR, after launch gates pass. */ +const PUBLISH_NOTICE_RELEASE: string | null = null; +const PUBLISH_COMPATIBILITY_DAYS = 60; + +export function publishPhase( + start: string | null, + now: number = Date.now(), +): "unscheduled" | "compatibility" | "share" { + if (!start) return "unscheduled"; + const begins = Date.parse(start); + if (!Number.isFinite(begins)) throw new Error("Invalid publish transition release timestamp."); + if (now < begins) return "unscheduled"; + return now < begins + PUBLISH_COMPATIBILITY_DAYS * 86_400_000 ? "compatibility" : "share"; +} +export function publishTransitionDecision( + phase: ReturnType, + legacyUpdate: boolean, +) { + if (phase === "share" && !legacyUpdate) return "guide_to_share"; + return "keep_legacy"; +} +export function checkPublishTransition(args: { dir?: string; update?: string }): void { + const dir = resolve(args.dir ?? "."); + const prior = readProjectLink(dir); + const legacyUpdate = Boolean( + args.update?.trim() || prior?.url || readTeamProject(dir)?.projectId, + ); + const phase = publishPhase(PUBLISH_NOTICE_RELEASE); + asNotice(() => process.stderr.write(publishNotice(phase, legacyUpdate) + "\n")); + if (publishTransitionDecision(phase, legacyUpdate) === "guide_to_share") { + throw new CliUsageError( + "New projects now use hyperframes share. Run it from this project folder and choose an audience; for scripts pass --public, --team, --invite-only or --private explicitly.", + { + code: "publish_replaced", + hint: "Existing publications remain reachable. Use publish --update for a legacy update. --yes and legacy --public do not convert a project or consent to a new review audience.", + }, + ); + } +} +function publishNotice(phase: ReturnType, update: boolean): string { + const kept = update + ? " This run keeps the existing publication." + : " This run keeps legacy publishing behavior."; + if (phase === "unscheduled") + return ( + "For new video reviews, use hyperframes share. The legacy publish transition date has not been scheduled." + + kept + ); + if (phase === "compatibility") + return ( + "Legacy publish is in its 60-day compatibility window. Use hyperframes share for new video reviews." + + kept + ); + return ( + "New projects use hyperframes share. Existing published URLs and explicit legacy updates remain supported." + + (update ? kept : "") + ); +} diff --git a/skills-manifest.json b/skills-manifest.json index 632cd9f0615..860e0c18095 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -10,7 +10,7 @@ "files": 77 }, "hyperframes-cli": { - "hash": "aeb9494dee6777a7", + "hash": "9414b94dd04c54c9", "files": 13 }, "hyperframes-registry": { diff --git a/skills/hyperframes-cli/SKILL.md b/skills/hyperframes-cli/SKILL.md index d180c732bd6..66d0e1cceaa 100644 --- a/skills/hyperframes-cli/SKILL.md +++ b/skills/hyperframes-cli/SKILL.md @@ -1,6 +1,6 @@ --- name: hyperframes-cli -description: The HyperFrames CLI by job - scaffold, check, look at frames, edit the timeline, preview, render locally or in the cloud, publish, project history - plus reading its JSON and recovering from build or render failures. +description: HyperFrames CLI - scaffold, check frames, edit timelines, preview, render, share and project history; read JSON and recover from failures. --- **Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin. @@ -20,7 +20,7 @@ Run `npx hyperframes ` unless the project gives a wrapper; then use the | Edit the timeline | `timeline` with `move`, `trim`, `split`, `set`, `duplicate`, `delete`, `apply`, `undo`, `ids` | | Reuse | `catalog`, `add` (`/hyperframes-registry`) | | Media and sound | `media-treatment`, `grade-compare`, `remove-background`, `tts`, `transcribe`, `beats`, `normalize-audio` (`/media-use`) | -| Show and ship | `preview`, `present` (`/slideshow`), `render`, `publish`, `cloud`, `lambda`, `cloudrun` | +| Show and ship | `preview`, `present` (`/slideshow`), `render`, `share`, `publish` (legacy), `cloud`, `lambda`, `cloudrun` | | Desktop app | `open`, `catch-up` | | Upkeep | `doctor`, `browser`, `upgrade`, `skills`, `usage`, `clean`, `feedback` | @@ -88,7 +88,7 @@ Read the row before running its command. | `lint`, `check`, motion sidecars, `snapshot` | `references/lint-validate-inspect.md` | | `compare`, `grade-compare`, `render --batch` | `references/compare-and-batch.md` | | `beats` | `references/beats.md` | -| `preview`, `play`, `render`, `publish`, Studio context | `references/preview-render.md` | +| `preview`, `play`, `render`, `share`, `publish` (legacy), Studio context | `references/preview-render.md` | | `doctor`, `browser` | `references/doctor-browser.md` | | `auth`, `cloud`, template variables | `references/cloud.md` | | `lambda`, `cloudrun` | `references/lambda.md`, `references/cloudrun.md` | diff --git a/skills/hyperframes-cli/references/preview-render.md b/skills/hyperframes-cli/references/preview-render.md index 31d1255cbff..314d9a12fc3 100644 --- a/skills/hyperframes-cli/references/preview-render.md +++ b/skills/hyperframes-cli/references/preview-render.md @@ -200,3 +200,22 @@ Uploads the project's source (HTML + assets) and returns a stable hosted URL tha Use cloud rendering when the user wants hosted rendering without local Chrome, FFmpeg, or AWS, and for the final render on a paid HyperFrames plan (`cloud.md`). Use Lambda only when AWS ownership is a requirement. Use Cloud Run only when GCP ownership is a requirement. Read the matching reference before running any cloud path. Skill attribution is automatic — the examples above need no `--skill`. A project scaffolded with `hyperframes init --skill=` records its owning skill in `hyperframes.json`, and every later render inherits it on usage telemetry: re-renders, `npm run render`, and `--batch` alike. Pass `--skill=` explicitly only to stamp a project that was not scaffolded with one (its first render then persists it). + +## Share a video for review + +Use `hyperframes share --public` only when the user explicitly wants anyone with +the link to access the review. First sign in with `hyperframes auth login`. +Other first-share choices are `--team`, `--invite-only` and `--private`, subject +to workspace entitlement. `--yes` is not audience consent. + +Read `hyperframes share --status --json`: `ready: true` is required before +calling the video ready to watch. Re-running `share` retrieves the existing +link; `share --update` explicitly renders/uploads changed content. Five retained +free projects are counted per workspace across CLI and Desktop; versions do +not consume slots and turning a link off does not release a slot. + +`publish` uploads source to legacy `/p/` pages. It is not a review share alias. +During the announced 60-day window its old behavior stays available with a +notice. The start date is release-owned and currently unscheduled. After it, new +projects are guided to Share; existing publications and legacy updates stay +reachable. Never convert legacy IDs or carry a legacy flag into review consent.