feat(cli): name-or-tag resolution for hyperframes add

hyperframes add now auto-detects whether the argument is a single
block name or a tag. No --tag flag needed:

  hyperframes add html-in-canvas  # installs all 7 html-in-canvas blocks
  hyperframes add captions        # installs all 5 caption blocks
  hyperframes add vfx-shatter     # installs one block

When the name doesn't match a registry item, the CLI falls back to
tag-based resolution and bulk-installs all matching blocks.

Also fixes tag assignments:
- VFX blocks tagged "html-in-canvas" (not "vfx")
- Caption blocks tagged "captions" only (no html-in-canvas)
This commit is contained in:
Miguel Ángel
2026-05-06 00:58:46 -07:00
parent 3ad51235c8
commit 20c1341cc8
13 changed files with 62 additions and 87 deletions
+62 -75
View File
@@ -4,7 +4,8 @@ import type { Example } from "./_examples.js";
export const examples: Example[] = [
["Add a block to the current project", "hyperframes add claude-code-window"],
["Add a component effect", "hyperframes add shader-wipe"],
["Add all blocks with a tag", "hyperframes add --tag vfx"],
["Add all HTML-in-Canvas blocks", "hyperframes add html-in-canvas"],
["Add all caption blocks", "hyperframes add captions"],
["Target a specific project directory", "hyperframes add shader-wipe --dir ./my-video"],
["Skip the clipboard copy (CI/headless)", "hyperframes add shader-wipe --no-clipboard"],
];
@@ -174,13 +175,10 @@ export default defineCommand({
args: {
name: {
type: "positional",
description: "Registry item name (e.g. claude-code-window, shader-wipe)",
required: false,
},
tag: {
type: "string",
alias: "t",
description: "Install all blocks matching a tag (e.g. --tag vfx, --tag captions)",
description:
"Registry item name or tag. Single items install directly (e.g. shader-wipe). " +
"If the name matches a tag instead, all blocks with that tag are installed (e.g. html-in-canvas, captions).",
required: true,
},
dir: {
type: "string",
@@ -199,70 +197,9 @@ export default defineCommand({
const projectDir = resolve(args.dir ?? process.cwd());
const json = args.json === true;
const skipClipboard = args["no-clipboard"] === true;
const tag = args.tag?.trim();
const hasConfigBefore = existsSync(projectConfigPath(projectDir));
// ── Tag-based bulk install ──────────────────────────────────────────
if (tag) {
let config = loadProjectConfig(projectDir);
if (
!existsSync(projectConfigPath(projectDir)) &&
existsSync(resolve(projectDir, "index.html"))
) {
writeProjectConfig(projectDir, DEFAULT_PROJECT_CONFIG);
config = DEFAULT_PROJECT_CONFIG;
}
let items: Awaited<ReturnType<typeof resolveItemsByTag>>;
try {
items = await resolveItemsByTag(tag, { baseUrl: config.registry });
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (json) console.log(JSON.stringify({ ok: false, error: msg }));
else console.error(c.error(msg));
process.exit(1);
}
if (items.length === 0) {
const msg = `No blocks found with tag "${tag}".`;
if (json) console.log(JSON.stringify({ ok: false, error: msg }));
else console.error(c.error(msg));
process.exit(1);
}
if (!json) {
console.log("");
console.log(
`${c.accent("◆")} Installing ${c.accent(String(items.length))} blocks tagged ${c.accent(tag)}`,
);
}
const results: RunAddResult[] = [];
for (const item of items) {
try {
const result = await runAdd({ name: item.name, projectDir, skipClipboard: true });
results.push(result);
if (!json) console.log(` ${c.success("✓")} ${result.name}`);
} catch {
if (!json) console.log(` ${c.error("✗")} ${item.name} (skipped)`);
}
}
if (json) {
console.log(JSON.stringify({ ok: true, tag, installed: results.map((r) => r.name) }));
} else {
console.log("");
console.log(`${c.success("✓")} Installed ${results.length}/${items.length} blocks`);
}
return;
}
// ── Single item install ────────────────────────────────────────────
if (!args.name) {
console.error(c.error("Provide a block name or use --tag <tag> to install by tag."));
process.exit(1);
}
// Try single item first. If it fails, check if the name matches a tag.
try {
const result = await runAdd({ name: args.name, projectDir, skipClipboard });
const wroteConfig = !hasConfigBefore && existsSync(projectConfigPath(projectDir));
@@ -291,14 +228,64 @@ export default defineCommand({
: c.dim("Paste the snippet above into your host composition."),
);
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
} catch (singleErr) {
// Not a single item — try as a tag for bulk install
if (!(singleErr instanceof AddError) || singleErr.code !== "unknown-item") {
const msg = singleErr instanceof Error ? singleErr.message : String(singleErr);
if (json) console.log(JSON.stringify({ ok: false, error: msg }));
else console.error(c.error(msg));
process.exit(1);
}
let config = loadProjectConfig(projectDir);
if (
!existsSync(projectConfigPath(projectDir)) &&
existsSync(resolve(projectDir, "index.html"))
) {
writeProjectConfig(projectDir, DEFAULT_PROJECT_CONFIG);
config = DEFAULT_PROJECT_CONFIG;
}
let items: Awaited<ReturnType<typeof resolveItemsByTag>>;
try {
items = await resolveItemsByTag(args.name, { baseUrl: config.registry });
} catch {
items = [];
}
if (items.length === 0) {
const msg = singleErr instanceof Error ? singleErr.message : String(singleErr);
if (json) console.log(JSON.stringify({ ok: false, error: msg }));
else console.error(c.error(msg));
process.exit(1);
}
if (!json) {
console.log("");
console.log(
`${c.accent("◆")} Installing ${c.accent(String(items.length))} blocks tagged ${c.accent(args.name)}`,
);
}
const results: RunAddResult[] = [];
for (const item of items) {
try {
const result = await runAdd({ name: item.name, projectDir, skipClipboard: true });
results.push(result);
if (!json) console.log(` ${c.success("✓")} ${result.name}`);
} catch {
if (!json) console.log(` ${c.error("✗")} ${item.name} (skipped)`);
}
}
if (json) {
console.log(JSON.stringify({ ok: false, error: msg }));
console.log(
JSON.stringify({ ok: true, tag: args.name, installed: results.map((r) => r.name) }),
);
} else {
console.error(c.error(msg));
console.log("");
console.log(`${c.success("✓")} Installed ${results.length}/${items.length} blocks`);
}
process.exit(1);
}
},
});