Skip to content

Commit 13ab193

Browse files
authored
feat(cli): catalog browser command (heygen-com#271)
Adds `hyperframes catalog` for browsing the registry: - Default: non-interactive table output (agent-friendly) - --type block/component and --tag filters - --json for machine-readable output - --human-friendly for interactive picker that installs on select Registered in cli.ts, help.ts, documented in docs/packages/cli.mdx.
1 parent 9943091 commit 13ab193

4 files changed

Lines changed: 180 additions & 0 deletions

File tree

docs/packages/cli.mdx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,34 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
205205

206206
Trying `add` with an example's name (e.g. `hyperframes add warm-grain`) emits a clear error pointing you at `init --example`.
207207

208+
### `catalog`
209+
210+
Browse the registry — list available blocks and components with optional filters:
211+
212+
```bash
213+
# List everything (default: table output)
214+
npx hyperframes catalog
215+
216+
# Filter by type or tag
217+
npx hyperframes catalog --type block
218+
npx hyperframes catalog --type block --tag social
219+
220+
# Machine-readable JSON
221+
npx hyperframes catalog --json
222+
223+
# Interactive picker — select to install
224+
npx hyperframes catalog --human-friendly
225+
```
226+
227+
| Flag | Description |
228+
|------|-------------|
229+
| `--type` | Filter by `block` or `component` |
230+
| `--tag` | Filter by tag (e.g. `social`, `transition`, `text`) |
231+
| `--json` | Print matching items as JSON (non-interactive) |
232+
| `--human-friendly` | Interactive picker — select an item to install it |
233+
234+
Default output is a table listing name, type, description, and tags — designed for agents to parse. `--json` produces structured output. `--human-friendly` opens an interactive picker that runs `add` on selection.
235+
208236
### `compositions`
209237

210238
List all compositions in the current project:

packages/cli/src/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const isHelp = process.argv.includes("--help") || process.argv.includes("-h");
2626
const subCommands = {
2727
init: () => import("./commands/init.js").then((m) => m.default),
2828
add: () => import("./commands/add.js").then((m) => m.default),
29+
catalog: () => import("./commands/catalog.js").then((m) => m.default),
2930
play: () => import("./commands/play.js").then((m) => m.default),
3031
preview: () => import("./commands/preview.js").then((m) => m.default),
3132
render: () => import("./commands/render.js").then((m) => m.default),
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { defineCommand } from "citty";
2+
import type { Example } from "./_examples.js";
3+
4+
export const examples: Example[] = [
5+
["List all blocks and components", "hyperframes catalog"],
6+
["List blocks only", "hyperframes catalog --type block"],
7+
["Filter by tag", "hyperframes catalog --type block --tag social"],
8+
["Machine-readable JSON", "hyperframes catalog --json"],
9+
["Interactive picker (install on select)", "hyperframes catalog --human-friendly"],
10+
];
11+
12+
import * as clack from "@clack/prompts";
13+
import { type ItemType } from "@hyperframes/core";
14+
import { c } from "../ui/colors.js";
15+
import { listRegistryItems, loadAllItems } from "../registry/resolver.js";
16+
import { loadProjectConfig, DEFAULT_PROJECT_CONFIG } from "../utils/projectConfig.js";
17+
import { resolve } from "node:path";
18+
import { runAdd } from "./add.js";
19+
20+
export default defineCommand({
21+
meta: {
22+
name: "catalog",
23+
description: "Browse and install blocks and components from the registry",
24+
},
25+
args: {
26+
type: {
27+
type: "string",
28+
description: 'Filter by type: "block" or "component"',
29+
},
30+
tag: {
31+
type: "string",
32+
description: "Filter by tag (e.g. social, transition, text)",
33+
},
34+
json: {
35+
type: "boolean",
36+
description: "Print matching items as JSON to stdout",
37+
},
38+
"human-friendly": {
39+
type: "boolean",
40+
description: "Interactive picker — select an item to install",
41+
},
42+
},
43+
async run({ args }) {
44+
const json = args.json === true;
45+
const interactive = args["human-friendly"] === true;
46+
const dir = resolve(process.cwd());
47+
const config = loadProjectConfig(dir) ?? DEFAULT_PROJECT_CONFIG;
48+
49+
let typeFilter: ItemType | undefined;
50+
if (args.type === "block") typeFilter = "hyperframes:block";
51+
else if (args.type === "component") typeFilter = "hyperframes:component";
52+
else if (args.type) {
53+
console.error(`Invalid --type: "${args.type}". Use "block" or "component".`);
54+
process.exit(1);
55+
}
56+
57+
const entries = await listRegistryItems(typeFilter ? { type: typeFilter } : undefined, {
58+
baseUrl: config.registry,
59+
});
60+
const filtered = entries.filter((e) => e.type !== "hyperframes:example");
61+
62+
if (filtered.length === 0) {
63+
if (json) console.log("[]");
64+
else console.log("No items found in registry.");
65+
return;
66+
}
67+
68+
const items = await loadAllItems(filtered, { baseUrl: config.registry });
69+
70+
const tagFilter = args.tag?.toLowerCase();
71+
const matching = tagFilter
72+
? items.filter((item) => item.tags?.some((t) => t.toLowerCase() === tagFilter))
73+
: items;
74+
75+
if (matching.length === 0) {
76+
if (json) console.log("[]");
77+
else console.log(`No items match tag "${args.tag}".`);
78+
return;
79+
}
80+
81+
if (json) {
82+
const output = matching.map((item) => ({
83+
name: item.name,
84+
type: item.type.replace("hyperframes:", ""),
85+
title: item.title,
86+
description: item.description,
87+
tags: item.tags ?? [],
88+
...("dimensions" in item && item.dimensions ? { dimensions: item.dimensions } : {}),
89+
...("duration" in item && item.duration ? { duration: item.duration } : {}),
90+
}));
91+
console.log(JSON.stringify(output, null, 2));
92+
return;
93+
}
94+
95+
if (interactive) {
96+
const options = matching.map((item) => ({
97+
value: item.name,
98+
label: item.name,
99+
hint: item.description,
100+
}));
101+
102+
const selected = await clack.select({
103+
message: `${matching.length} items available — pick one to install`,
104+
options,
105+
});
106+
107+
if (clack.isCancel(selected)) {
108+
clack.cancel("Cancelled.");
109+
process.exit(0);
110+
}
111+
112+
const result = await runAdd({
113+
name: selected as string,
114+
projectDir: dir,
115+
skipClipboard: false,
116+
});
117+
118+
console.log("");
119+
console.log(`${c.success("✓")} Installed ${c.accent(result.name)} (${result.type})`);
120+
for (const file of result.written) {
121+
const rel = file.replace(dir + "/", "");
122+
console.log(` ${c.dim(rel)}`);
123+
}
124+
if (result.snippet) {
125+
console.log("");
126+
console.log(c.dim("Include snippet:"));
127+
console.log(` ${result.snippet}`);
128+
}
129+
return;
130+
}
131+
132+
const NAME_COL = 28;
133+
const TYPE_COL = 12;
134+
console.log(
135+
`${c.bold("Name".padEnd(NAME_COL))}${c.bold("Type".padEnd(TYPE_COL))}${c.bold("Description")}`,
136+
);
137+
console.log("-".repeat(80));
138+
139+
for (const item of matching) {
140+
const type = item.type.replace("hyperframes:", "");
141+
const tags = item.tags?.length ? c.dim(` [${item.tags.join(", ")}]`) : "";
142+
console.log(
143+
`${c.cyan(item.name.padEnd(NAME_COL))}${type.padEnd(TYPE_COL)}${item.description}${tags}`,
144+
);
145+
}
146+
147+
console.log("");
148+
console.log(c.dim(`${matching.length} items. Run "hyperframes add <name>" to install.`));
149+
},
150+
});

packages/cli/src/help.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const GROUPS: Group[] = [
2121
commands: [
2222
["init", "Scaffold a new composition project"],
2323
["add", "Install a block or component from the registry"],
24+
["catalog", "Browse and install blocks and components"],
2425
["preview", "Start the studio for previewing compositions"],
2526
["render", "Render a composition to MP4 or WebM"],
2627
],

0 commit comments

Comments
 (0)