feat(cli): catalog browser command (#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.
This commit is contained in:
James Russo
2026-04-14 16:46:42 -07:00
committed by GitHub
parent 9943091247
commit 13ab1932ad
4 changed files with 180 additions and 0 deletions
+28
View File
@@ -205,6 +205,34 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
Trying `add` with an example's name (e.g. `hyperframes add warm-grain`) emits a clear error pointing you at `init --example`.
### `catalog`
Browse the registry — list available blocks and components with optional filters:
```bash
# List everything (default: table output)
npx hyperframes catalog
# Filter by type or tag
npx hyperframes catalog --type block
npx hyperframes catalog --type block --tag social
# Machine-readable JSON
npx hyperframes catalog --json
# Interactive picker — select to install
npx hyperframes catalog --human-friendly
```
| Flag | Description |
|------|-------------|
| `--type` | Filter by `block` or `component` |
| `--tag` | Filter by tag (e.g. `social`, `transition`, `text`) |
| `--json` | Print matching items as JSON (non-interactive) |
| `--human-friendly` | Interactive picker — select an item to install it |
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.
### `compositions`
List all compositions in the current project:
+1
View File
@@ -26,6 +26,7 @@ const isHelp = process.argv.includes("--help") || process.argv.includes("-h");
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),
render: () => import("./commands/render.js").then((m) => m.default),
+150
View File
@@ -0,0 +1,150 @@
import { defineCommand } from "citty";
import type { Example } from "./_examples.js";
export const examples: Example[] = [
["List all blocks and components", "hyperframes catalog"],
["List blocks only", "hyperframes catalog --type block"],
["Filter by tag", "hyperframes catalog --type block --tag social"],
["Machine-readable JSON", "hyperframes catalog --json"],
["Interactive picker (install on select)", "hyperframes catalog --human-friendly"],
];
import * as clack from "@clack/prompts";
import { type ItemType } from "@hyperframes/core";
import { c } from "../ui/colors.js";
import { listRegistryItems, loadAllItems } from "../registry/resolver.js";
import { loadProjectConfig, DEFAULT_PROJECT_CONFIG } from "../utils/projectConfig.js";
import { resolve } from "node:path";
import { runAdd } from "./add.js";
export default defineCommand({
meta: {
name: "catalog",
description: "Browse and install blocks and components from the registry",
},
args: {
type: {
type: "string",
description: 'Filter by type: "block" or "component"',
},
tag: {
type: "string",
description: "Filter by tag (e.g. social, transition, text)",
},
json: {
type: "boolean",
description: "Print matching items as JSON to stdout",
},
"human-friendly": {
type: "boolean",
description: "Interactive picker — select an item to install",
},
},
async run({ args }) {
const json = args.json === true;
const interactive = args["human-friendly"] === true;
const dir = resolve(process.cwd());
const config = loadProjectConfig(dir) ?? DEFAULT_PROJECT_CONFIG;
let typeFilter: ItemType | undefined;
if (args.type === "block") typeFilter = "hyperframes:block";
else if (args.type === "component") typeFilter = "hyperframes:component";
else if (args.type) {
console.error(`Invalid --type: "${args.type}". Use "block" or "component".`);
process.exit(1);
}
const entries = await listRegistryItems(typeFilter ? { type: typeFilter } : undefined, {
baseUrl: config.registry,
});
const filtered = entries.filter((e) => e.type !== "hyperframes:example");
if (filtered.length === 0) {
if (json) console.log("[]");
else console.log("No items found in registry.");
return;
}
const items = await loadAllItems(filtered, { baseUrl: config.registry });
const tagFilter = args.tag?.toLowerCase();
const matching = tagFilter
? items.filter((item) => item.tags?.some((t) => t.toLowerCase() === tagFilter))
: items;
if (matching.length === 0) {
if (json) console.log("[]");
else console.log(`No items match tag "${args.tag}".`);
return;
}
if (json) {
const output = matching.map((item) => ({
name: item.name,
type: item.type.replace("hyperframes:", ""),
title: item.title,
description: item.description,
tags: item.tags ?? [],
...("dimensions" in item && item.dimensions ? { dimensions: item.dimensions } : {}),
...("duration" in item && item.duration ? { duration: item.duration } : {}),
}));
console.log(JSON.stringify(output, null, 2));
return;
}
if (interactive) {
const options = matching.map((item) => ({
value: item.name,
label: item.name,
hint: item.description,
}));
const selected = await clack.select({
message: `${matching.length} items available — pick one to install`,
options,
});
if (clack.isCancel(selected)) {
clack.cancel("Cancelled.");
process.exit(0);
}
const result = await runAdd({
name: selected as string,
projectDir: dir,
skipClipboard: false,
});
console.log("");
console.log(`${c.success("✓")} Installed ${c.accent(result.name)} (${result.type})`);
for (const file of result.written) {
const rel = file.replace(dir + "/", "");
console.log(` ${c.dim(rel)}`);
}
if (result.snippet) {
console.log("");
console.log(c.dim("Include snippet:"));
console.log(` ${result.snippet}`);
}
return;
}
const NAME_COL = 28;
const TYPE_COL = 12;
console.log(
`${c.bold("Name".padEnd(NAME_COL))}${c.bold("Type".padEnd(TYPE_COL))}${c.bold("Description")}`,
);
console.log("-".repeat(80));
for (const item of matching) {
const type = item.type.replace("hyperframes:", "");
const tags = item.tags?.length ? c.dim(` [${item.tags.join(", ")}]`) : "";
console.log(
`${c.cyan(item.name.padEnd(NAME_COL))}${type.padEnd(TYPE_COL)}${item.description}${tags}`,
);
}
console.log("");
console.log(c.dim(`${matching.length} items. Run "hyperframes add <name>" to install.`));
},
});
+1
View File
@@ -21,6 +21,7 @@ const GROUPS: Group[] = [
commands: [
["init", "Scaffold a new composition project"],
["add", "Install a block or component from the registry"],
["catalog", "Browse and install blocks and components"],
["preview", "Start the studio for previewing compositions"],
["render", "Render a composition to MP4 or WebM"],
],