mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
* feat(cli): add hyperframes lambda deploy/render/progress/destroy
Wraps the @hyperframes/aws-lambda SDK + the Phase 6a SAM template behind
a single CLI surface so an end-to-end render is three commands instead
of the ~8 manual bun+sam+aws steps the smoke script does today:
hyperframes lambda deploy
hyperframes lambda render ./my-project --width 1920 --height 1080 --wait
hyperframes lambda destroy
Subcommands:
- deploy: build handler.zip + sam-deploy + persist stack outputs
to <cwd>/.hyperframes/lambda-stack-<name>.json
- sites create: pre-upload a project to S3 with a stable content hash
so re-renders skip the tar+PUT pass
- render: start a Step Functions execution; --wait blocks and
streams per-chunk progress + accrued cost
- progress: one-shot snapshot — status, frames, cost breakdown,
errors. Accepts renderId or executionArn
- destroy: sam-delete + drop the local state file (S3 bucket
is Retain'd by the template; documented in --help
and in docs/packages/cli.mdx)
To keep @sparticuz/chromium out of the CLI's transitive deps, this also
adds a dedicated ./sdk subpath export to @hyperframes/aws-lambda; the
CLI imports from @hyperframes/aws-lambda/sdk exclusively. The existing
. barrel still re-exports both handler + SDK for adopters who want one
entry point.
Defaults are deliberately cost-conservative for first-time users:
--concurrency=8 (low enough to never surprise) and --memory=10240 (the
common case; documented for adopters who want to tune down).
Tests: 5 unit tests on the state-file round-trip. CLI integration
against sam local invoke is part of the upcoming PR 6.6 (lambda-local
regression harness).
* refactor(cli): /simplify pass on the lambda command group
Two small cleanups on top of the lambda CLI:
- Replace parseFormat / parseCodec / parseQuality / parseChromeSource
(four near-identical helpers) with a single generic parseEnum() +
typed const-tuple lookups. The four callers now read as one-line
arrow functions that lift the allowed values out of the function
body so they're easy to extend.
- DEFAULT_STACK_NAME was const-declared then re-exported at the
bottom of state.ts; just mark the const export inline.
No behavior changes. All CLI tests still pass.
* fix(cli): keep @hyperframes/aws-lambda external in the tsup bundle
esbuild can't bundle @hyperframes/aws-lambda's transitive AWS SDK
deps (@aws-sdk/* + @smithy/*) cleanly into a node binary — the
SDK's .browser.js conditional re-exports break the resolver:
ESM Build failed
No matching export in "splitStream.browser.js" for import
"splitStream" (and ~10 similar errors)
Mark aws-lambda as `external` so esbuild doesn't follow it, and
move it from devDependencies to dependencies so the published CLI
can resolve it from node_modules at runtime. The lambda subverb
files dynamic-import only on `hyperframes lambda *` invocation, so
the CLI cold-start cost is unchanged.
The install-size hit (AWS SDK + @sparticuz/chromium ≈ 200 MiB) is
documented as a v1 tradeoff; a future split into a lambda-sdk-only
subpackage can pare this back.
* fix(cli): address PR review on lambda CLI
Two blockers + four important items from Vai's review:
- `--memory` was parsed and recorded in the local state file but
never forwarded to `sam deploy` as a parameter override. Worse,
`progress.ts` then read the *recorded* value for cost math, so
`--memory 5120` produced wrong cost numbers downstream. Thread
`LambdaMemoryMb` through samDeploy's --parameter-overrides.
- `--profile` was only consumed by deploy / destroy. render and
progress fell back to the default credentials chain — a user
with `--profile prod` would silently render against their
default account (wrong-account billing footgun). Set
`process.env.AWS_PROFILE` (and `AWS_REGION`) in the dispatcher
before any subverb runs; the AWS SDK reads them natively, so
render / progress / sites all benefit without each subverb
threading the flag through the SDK call.
- `--profile` + destroy now also reads `process.env.AWS_PROFILE`
as a fallback (matching deploy's existing env fallback).
- `--wait --json` printed both the start handle AND the final
progress snapshot, producing two concatenated JSON blobs that
`jq` rejected. Now emits a single document: handle (without
--wait) OR final progress (with --wait).
- Negative integers on `--width` / `--height` / `--chunk-size` /
`--max-parallel-chunks` / `--memory` / `--concurrency` now fail
loudly via a new `parsePositiveInt` wrapper instead of flowing
into the SDK and producing opaque AWS validation errors mid-
render.
- `DEFAULT_STACK_NAME` is now centralized to the literal
`"hyperframes-default"` and consumed from one place. Previously
the value was assembled as `hyperframes-${"default"}` in three
sites and hardcoded as `"hyperframes-default"` in a fourth.
`requireStack`'s hint now matches the dispatcher's default.
The faked `SiteHandle` for `--site-id` keeps the documented
placeholder fields but also surfaces `bucketName` (from PR 909's
extended SiteHandle interface), matching the SDK contract.
All CLI unit tests + the full bundler build still pass.
* fix(cli): keep aws-lambda out of CLI runtime deps
The "Smoke: global install" CI step packs the CLI via `npm pack` and
installs it globally via `npm install -g <tgz>`. npm doesn't understand
the workspace: protocol, so a runtime `dependencies` entry of
`@hyperframes/aws-lambda: workspace:*` blows up with:
npm error code EUNSUPPORTEDPROTOCOL
npm error Unsupported URL Type "workspace:": workspace:*
(pnpm rewrites workspace:* on publish; npm pack doesn't.)
Three changes to unblock the smoke + keep the published CLI install
small for users who don't deploy to Lambda:
- Move `@hyperframes/aws-lambda` from CLI's `dependencies` back to
`devDependencies`. It's already external in tsup.config.ts; the
bundle references it via runtime resolution only.
- Convert the static `import { … } from "@hyperframes/aws-lambda/sdk"`
in sites.ts / render.ts / progress.ts to `await import()` inside
each function. tsup with `splitting: false` was inlining those
static imports at the top of the bundle, which made Node eagerly
resolve them at CLI startup (MODULE_NOT_FOUND before any lambda
subcommand even runs). Dynamic imports stay dynamic in the bundle.
- Add a friendly missing-module check in the lambda dispatcher.
When a user runs `hyperframes lambda deploy / render / sites /
progress / destroy` without aws-lambda installed, they now see:
@hyperframes/aws-lambda is not installed.
The `hyperframes lambda deploy` command needs it at runtime.
Install it alongside the CLI:
npm install -g @hyperframes/aws-lambda
Verified locally: pack + global install + `hyperframes init --example
blank` now succeeds end-to-end (was the same scenario the CI smoke job
runs).
163 lines
7.3 KiB
JavaScript
163 lines
7.3 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
// ── Worker entry path bootstrap (must run before any producer/engine load) ──
|
|
// The hf#677 worker_threads pools (`pngDecodeBlitWorkerPool`,
|
|
// `shaderTransitionWorkerPool`) live in the producer package and try to
|
|
// resolve their worker entry by probing for sibling `.js` files next to
|
|
// `import.meta.url`. When this CLI is bundled by tsup, the producer code is
|
|
// inlined into `cli.js`, but `import.meta.url` resolves to the producer's
|
|
// own dist path (NOT cli.js) on some module-graph layouts — so the sibling
|
|
// probe lands in a directory that does not contain the bundled workers.
|
|
// We emit the worker entries next to cli.js (see tsup.config.ts) and tell
|
|
// the pools where to find them via the published env-var overrides. The
|
|
// pools have an explicit `workerEntryPath` factory option as the canonical
|
|
// API, but setting the env vars here covers every call site without having
|
|
// to thread the path through the renderOrchestrator → captureHdrStage →
|
|
// captureHdrHybridLoop chain.
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { existsSync } from "node:fs";
|
|
|
|
(() => {
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const shader = join(here, "shaderTransitionWorker.js");
|
|
const png = join(here, "pngDecodeBlitWorker.js");
|
|
if (!process.env.HF_SHADER_WORKER_ENTRY && existsSync(shader)) {
|
|
process.env.HF_SHADER_WORKER_ENTRY = shader;
|
|
}
|
|
if (!process.env.HF_PNG_DECODE_BLIT_WORKER_ENTRY && existsSync(png)) {
|
|
process.env.HF_PNG_DECODE_BLIT_WORKER_ENTRY = png;
|
|
}
|
|
})();
|
|
|
|
// ── Fast-path exits ─────────────────────────────────────────────────────────
|
|
// Check --version before importing anything heavy. This makes
|
|
// `hyperframes --version` near-instant (~10ms vs ~80ms).
|
|
import { VERSION } from "./version.js";
|
|
|
|
const argv = process.argv.slice(2);
|
|
const commandArg = argv[0];
|
|
const rootVersionRequested =
|
|
commandArg === "--version" ||
|
|
commandArg === "-V" ||
|
|
(commandArg === undefined && (argv.includes("--version") || argv.includes("-V")));
|
|
|
|
if (rootVersionRequested) {
|
|
console.log(VERSION);
|
|
process.exit(0);
|
|
}
|
|
|
|
// ── Lazy imports ────────────────────────────────────────────────────────────
|
|
// Telemetry, update checks, and heavy modules are imported only when needed.
|
|
// For --help we skip telemetry entirely.
|
|
|
|
import { defineCommand, runMain } from "citty";
|
|
import type { ArgsDef, CommandDef } from "citty";
|
|
|
|
const isHelp = process.argv.includes("--help") || process.argv.includes("-h");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CLI definition — all commands are lazy-loaded via dynamic import()
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const subCommands = {
|
|
init: () => import("./commands/init.js").then((m) => m.default),
|
|
add: () => import("./commands/add.js").then((m) => m.default),
|
|
catalog: () => import("./commands/catalog.js").then((m) => m.default),
|
|
play: () => import("./commands/play.js").then((m) => m.default),
|
|
preview: () => import("./commands/preview.js").then((m) => m.default),
|
|
publish: () => import("./commands/publish.js").then((m) => m.default),
|
|
render: () => import("./commands/render.js").then((m) => m.default),
|
|
lint: () => import("./commands/lint.js").then((m) => m.default),
|
|
inspect: () => import("./commands/inspect.js").then((m) => m.default),
|
|
layout: () => import("./commands/layout.js").then((m) => m.default),
|
|
info: () => import("./commands/info.js").then((m) => m.default),
|
|
compositions: () => import("./commands/compositions.js").then((m) => m.default),
|
|
benchmark: () => import("./commands/benchmark.js").then((m) => m.default),
|
|
browser: () => import("./commands/browser.js").then((m) => m.default),
|
|
"remove-background": () => import("./commands/remove-background.js").then((m) => m.default),
|
|
transcribe: () => import("./commands/transcribe.js").then((m) => m.default),
|
|
tts: () => import("./commands/tts.js").then((m) => m.default),
|
|
docs: () => import("./commands/docs.js").then((m) => m.default),
|
|
doctor: () => import("./commands/doctor.js").then((m) => m.default),
|
|
upgrade: () => import("./commands/upgrade.js").then((m) => m.default),
|
|
skills: () => import("./commands/skills.js").then((m) => m.default),
|
|
telemetry: () => import("./commands/telemetry.js").then((m) => m.default),
|
|
validate: () => import("./commands/validate.js").then((m) => m.default),
|
|
snapshot: () => import("./commands/snapshot.js").then((m) => m.default),
|
|
capture: () => import("./commands/capture.js").then((m) => m.default),
|
|
lambda: () => import("./commands/lambda.js").then((m) => m.default),
|
|
};
|
|
|
|
const main = defineCommand({
|
|
meta: {
|
|
name: "hyperframes",
|
|
version: VERSION,
|
|
description: "Create and render HTML video compositions",
|
|
},
|
|
subCommands,
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Telemetry — lazy-loaded, captured references for exit handlers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const cliCommandArg = process.argv[2];
|
|
const command = cliCommandArg && cliCommandArg in subCommands ? cliCommandArg : "unknown";
|
|
const hasJsonFlag = process.argv.includes("--json");
|
|
|
|
// Captured references — populated when the lazy imports resolve.
|
|
// Used in exit handlers where dynamic import() is unsafe (beforeExit loops,
|
|
// exit handler is synchronous-only).
|
|
let _flush: (() => Promise<void>) | undefined;
|
|
let _flushSync: (() => void) | undefined;
|
|
let _printUpdateNotice: (() => void) | undefined;
|
|
|
|
if (!isHelp && command !== "telemetry" && command !== "unknown") {
|
|
import("./telemetry/index.js").then((mod) => {
|
|
_flush = mod.flush;
|
|
_flushSync = mod.flushSync;
|
|
mod.showTelemetryNotice();
|
|
mod.trackCommand(command);
|
|
if (mod.shouldTrack()) mod.incrementCommandCount();
|
|
});
|
|
}
|
|
|
|
if (!isHelp && !hasJsonFlag && command !== "upgrade") {
|
|
// Report any completed auto-install from the previous run first, before
|
|
// kicking off the next check — so the user sees "updated to vX" once and
|
|
// we don't over-print.
|
|
import("./utils/autoUpdate.js").then((mod) => mod.reportCompletedUpdate()).catch(() => {});
|
|
|
|
import("./utils/updateCheck.js").then(async (mod) => {
|
|
_printUpdateNotice = mod.printUpdateNotice;
|
|
const result = await mod.checkForUpdate().catch(() => null);
|
|
if (result?.updateAvailable) {
|
|
const auto = await import("./utils/autoUpdate.js").catch(() => null);
|
|
auto?.scheduleBackgroundInstall(result.latest, result.current);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Async flush for normal exit (beforeExit fires when the event loop drains)
|
|
process.on("beforeExit", () => {
|
|
_flush?.().catch(() => {});
|
|
if (!hasJsonFlag) _printUpdateNotice?.();
|
|
});
|
|
|
|
// Sync flush for process.exit() calls (exit event only allows synchronous code)
|
|
process.on("exit", () => {
|
|
_flushSync?.();
|
|
});
|
|
|
|
// Lazy-load help renderer — avoids allocating help data on non-help invocations
|
|
async function showUsage<T extends ArgsDef>(
|
|
cmd: CommandDef<T>,
|
|
parent?: CommandDef<T>,
|
|
): Promise<void> {
|
|
const { showUsage: impl } = await import("./help.js");
|
|
return impl(cmd as CommandDef, parent as CommandDef | undefined);
|
|
}
|
|
|
|
runMain(main, { showUsage });
|