mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:26:17 +00:00
feat(cli): add command + hyperframes.json (#256)
## What PR 5/17 of the catalog system rollout. Adds the `hyperframes add` verb for installing blocks and components from the registry into an existing project, plus the `hyperframes.json` project config that tells `add` which registry to use and where to drop files. Stacks on #255. - **`packages/cli/src/commands/add.ts`** — new `hyperframes add <name>` command. Resolves an item, validates target paths, installs files in parallel, builds an include snippet, copies it to the clipboard. Exposes a testable `runAdd(opts)` function; the citty default wraps it with console output + exit handling - **`packages/cli/src/utils/projectConfig.ts`** — read/write/normalize `hyperframes.json`. Tolerant to missing and partial configs - **`packages/cli/src/utils/clipboard.ts`** — minimal cross-platform clipboard (pbcopy / clip.exe / wl-copy / xclip / xsel). Zero deps. Gracefully no-ops in headless environments - **`packages/cli/src/commands/init.ts`** — write `hyperframes.json` during scaffold if not already present - **`packages/cli/src/cli.ts`** + **`help.ts`** — register `add` under Getting Started (directly below `init`) Design doc: [Hyperframes Catalog System](https://www.notion.so/heygen/Hyperframes-Catalog-System-Design-Plan-341449792c69813f899dcd53b4c0383a). ## UX ```bash # Scaffold a project (now writes hyperframes.json too) npx hyperframes init my-video --example blank cd my-video # Add a block — files land, snippet copied to clipboard npx hyperframes add claude-code-window # ✓ Added claude-code-window (hyperframes:block) # compositions/claude-code-window.html # # Include snippet: # <iframe src="compositions/claude-code-window.html" data-start="0" data-duration="6"></iframe> # # Copied to clipboard — paste into your host composition. # Add a component effect npx hyperframes add shader-wipe # Headless / CI — no clipboard, JSON output for tooling npx hyperframes add shader-wipe --no-clipboard --json ``` Running `hyperframes add warm-grain` (an example) errors clearly pointing to `init --example`. ## Docs (bundled in this PR per the tracker principle) - `docs/packages/cli.mdx` — new `add` subsection under Commands (flags, examples, trigger rules) + new `hyperframes.json` section describing the config file shape ## Tests - **`packages/cli/src/commands/add.test.ts`** — 11 tests: - `remapTarget` / `buildSnippet` pure helpers (5 tests) - `runAdd` integration against a mocked `fetch` registry: block install lands files + returns snippet, component install respects `paths.components` remap, example-typed names throw `AddError` with code `example-type`, unknown names throw `AddError` with code `unknown-item` (4 tests plus 2 covering block default path and non-default path preservation) - **`packages/cli/src/utils/projectConfig.test.ts`** — 9 tests: - Write/read round-trip, partial-config normalization, corrupt-file handling, absent-file fallback to defaults, custom paths preserved - **CLI suite:** 92 passed (was 72 on #255, **+20**). Same 4 pre-existing failures unchanged ## Scope decisions - **`init.ts` full port to new resolver deferred.** The original plan bundled a removal of the `packages/cli/src/templates/` compat shim. That's ~300 more lines and isn't required for `add` to work. The compat shim from #254 still functions; a separate cleanup PR handles it - **No ajv runtime schema validation.** Manifests are trusted as schema-valid. Full validation lands when third-party registries arrive (PR 14/15). Path safety is still enforced by the installer's `assertSafeTarget` guard - **Default project paths stay under `compositions/`.** Blocks → `compositions/<name>.html`; components → `compositions/components/<name>/<file>`. Users override via `hyperframes.json#paths` ## Breaking / migration **None.** Pure additive — new command, new file types, no existing commands or flags change. `init.ts` now writes `hyperframes.json` but that's a new additional file, not a modification of existing output. ## Stacks on #255 — base branch. When #255 merges, this rebases onto `main`. ## Next in stack PR 6 — `feat(registry): seed block — claude-code-window`. First real registry item. Exercises the full `hyperframes add <name>` flow end-to-end against a committed item on `main`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Read and write `hyperframes.json` — the per-project config that tells
|
||||
* `hyperframes add` which registry to pull items from and where to drop them
|
||||
* in the user's project tree.
|
||||
*
|
||||
* The file is created by `hyperframes init` and optionally edited by users to
|
||||
* point at custom registries or reshape their project layout.
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { DEFAULT_REGISTRY_URL } from "../registry/index.js";
|
||||
|
||||
export const PROJECT_CONFIG_FILENAME = "hyperframes.json";
|
||||
export const PROJECT_CONFIG_SCHEMA_URL = "https://hyperframes.heygen.com/schema/hyperframes.json";
|
||||
|
||||
export interface ProjectConfigPaths {
|
||||
/** Where `hyperframes:block` items land, relative to project root. */
|
||||
blocks: string;
|
||||
/** Where `hyperframes:component` items land, relative to project root. */
|
||||
components: string;
|
||||
/** Where asset files (images, fonts, videos) land, relative to project root. */
|
||||
assets: string;
|
||||
}
|
||||
|
||||
export interface ProjectConfig {
|
||||
$schema?: string;
|
||||
/** Base URL of the registry to pull items from. */
|
||||
registry: string;
|
||||
/** Target paths for each item type. */
|
||||
paths: ProjectConfigPaths;
|
||||
}
|
||||
|
||||
export const DEFAULT_PROJECT_CONFIG: ProjectConfig = {
|
||||
$schema: PROJECT_CONFIG_SCHEMA_URL,
|
||||
registry: DEFAULT_REGISTRY_URL,
|
||||
paths: {
|
||||
blocks: "compositions",
|
||||
components: "compositions/components",
|
||||
assets: "assets",
|
||||
},
|
||||
};
|
||||
|
||||
/** Path to the config file for a project rooted at `projectDir`. */
|
||||
export function projectConfigPath(projectDir: string): string {
|
||||
return join(resolve(projectDir), PROJECT_CONFIG_FILENAME);
|
||||
}
|
||||
|
||||
/** Read `hyperframes.json` from a project directory. */
|
||||
export function readProjectConfig(projectDir: string): ProjectConfig | undefined {
|
||||
const path = projectConfigPath(projectDir);
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(path, "utf-8")) as Partial<ProjectConfig>;
|
||||
return normalizeConfig(parsed);
|
||||
} catch {
|
||||
// Missing file or corrupt JSON → no config.
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a valid config — fills in any missing fields with defaults. Used
|
||||
* when a user's config file is present but partial (e.g. they only set
|
||||
* `registry` and rely on default paths).
|
||||
*/
|
||||
export function normalizeConfig(partial: Partial<ProjectConfig>): ProjectConfig {
|
||||
return {
|
||||
$schema: partial.$schema ?? DEFAULT_PROJECT_CONFIG.$schema,
|
||||
registry: partial.registry ?? DEFAULT_PROJECT_CONFIG.registry,
|
||||
paths: {
|
||||
blocks: partial.paths?.blocks ?? DEFAULT_PROJECT_CONFIG.paths.blocks,
|
||||
components: partial.paths?.components ?? DEFAULT_PROJECT_CONFIG.paths.components,
|
||||
assets: partial.paths?.assets ?? DEFAULT_PROJECT_CONFIG.paths.assets,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Write `hyperframes.json` to a project directory. Overwrites if present. */
|
||||
export function writeProjectConfig(
|
||||
projectDir: string,
|
||||
config: ProjectConfig = DEFAULT_PROJECT_CONFIG,
|
||||
): void {
|
||||
const path = projectConfigPath(projectDir);
|
||||
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the project config for the given directory, falling back to defaults
|
||||
* if missing. Mutates nothing on disk. Used by commands that want to operate
|
||||
* with or without an explicit config.
|
||||
*/
|
||||
export function loadProjectConfig(projectDir: string): ProjectConfig {
|
||||
return readProjectConfig(projectDir) ?? DEFAULT_PROJECT_CONFIG;
|
||||
}
|
||||
Reference in New Issue
Block a user