feat(skills): add hyperframes-cli skill (#154)

* feat(skills): add hyperframes-cli skill for CLI workflow guidance

Adds a new skill that teaches AI agents how to use the HyperFrames CLI
(init, lint, dev, render, doctor). Previously, agents had no way to
discover the CLI — the compose-video skill only covered HTML authoring.
This led to agents searching for binaries, finding the monorepo, and
running bun run studio manually instead of using npx hyperframes dev.

Also registers the skill in init.ts so new projects get it bundled
alongside hyperframes-compose and hyperframes-captions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(cli): rename dev command to preview

The command starts a preview server — "preview" describes what users
are doing more accurately than "dev". Updates the command name, file
name, all CLI references, docs, skills, and template CLAUDE.md.

22 files updated across CLI source, docs, skills, and templates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(skills): replace stale dev reference with preview in CLI skill

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(docs): catch remaining dev references missed in rename

- testing-local-changes.mdx: two inline command examples
- troubleshooting.mdx: anchor link #dev → #preview, "dev server" → "preview server"
- cli.mdx: "dev server" → "preview server"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-03-31 00:30:55 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent bc956bb6da
commit 9cbfec1eca
22 changed files with 172 additions and 53 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ import { checkForUpdate, printUpdateNotice } from "./utils/updateCheck.js";
const subCommands = {
init: () => import("./commands/init.js").then((m) => m.default),
dev: () => import("./commands/dev.js").then((m) => m.default),
preview: () => import("./commands/preview.js").then((m) => m.default),
render: () => import("./commands/render.js").then((m) => m.default),
lint: () => import("./commands/lint.js").then((m) => m.default),
info: () => import("./commands/info.js").then((m) => m.default),
+6 -6
View File
@@ -398,7 +398,7 @@ function scaffoldProject(
// Copy project-level skills (.claude/skills/) for immediate availability
const skillsSrcDir = getBundledSkillsDir();
if (existsSync(skillsSrcDir)) {
const projectSkills = ["hyperframes-compose", "hyperframes-captions"];
const projectSkills = ["hyperframes-compose", "hyperframes-captions", "hyperframes-cli"];
for (const skill of projectSkills) {
const src = join(skillsSrcDir, skill);
if (existsSync(src)) {
@@ -420,7 +420,7 @@ async function nextStepLoop(destDir: string): Promise<void> {
message: "What do you want to do?",
options: [
{
value: "dev",
value: "preview",
label: "Open in studio",
hint: "full editor with timeline",
},
@@ -437,9 +437,9 @@ async function nextStepLoop(destDir: string): Promise<void> {
// Hand off to the selected command — use explicit imports so the
// bundler can resolve them (dynamic import with a variable fails in bundles)
try {
if (next === "dev") {
const devCmd = await import("./dev.js").then((m) => m.default);
await runCommand(devCmd, { rawArgs: [destDir] });
if (next === "preview") {
const previewCmd = await import("./preview.js").then((m) => m.default);
await runCommand(previewCmd, { rawArgs: [destDir] });
} else if (next === "render") {
const renderCmd = await import("./render.js").then((m) => m.default);
await runCommand(renderCmd, { rawArgs: [destDir] });
@@ -623,7 +623,7 @@ Examples:
);
console.log();
console.log(` ${c.accent("2.")} Preview in the browser:`);
console.log(` ${c.accent(`cd ${name}`)} && ${c.accent("npx hyperframes dev")}`);
console.log(` ${c.accent(`cd ${name}`)} && ${c.accent("npx hyperframes preview")}`);
console.log();
console.log(` ${c.accent("3.")} Render to MP4 when ready:`);
console.log(` ${c.accent(`cd ${name}`)} && ${c.accent("npx hyperframes render")}`);
@@ -59,22 +59,22 @@ async function serveWithPortFallback(
}
export default defineCommand({
meta: { name: "dev", description: "Start the studio for local development" },
meta: { name: "preview", description: "Start the studio for previewing compositions" },
args: {
dir: { type: "positional", description: "Project directory", required: false },
port: { type: "string", description: "Port to run the dev server on", default: "3002" },
port: { type: "string", description: "Port to run the preview server on", default: "3002" },
},
async run({ args }) {
const rawArg = args.dir;
const dir = resolve(rawArg ?? ".");
const startPort = parseInt(args.port ?? "3002", 10);
// Compute display name: preserve symlink/CWD name when user runs "hyperframes dev ."
// Compute display name: preserve symlink/CWD name when user runs "hyperframes preview ."
const isImplicitCwd = !rawArg || rawArg === "." || rawArg === "./";
const projectName = isImplicitCwd ? basename(process.env.PWD ?? dir) : basename(dir);
// Lint before starting — surface issues for the agent to fix.
// dev.ts doesn't use resolveProject() because it needs to proceed even without index.html.
// preview.ts doesn't use resolveProject() because it needs to proceed even without index.html.
const indexPath = join(dir, "index.html");
if (existsSync(indexPath)) {
const project = { dir, name: projectName, indexPath };
@@ -137,7 +137,7 @@ async function runDevMode(dir: string, projectName?: string): Promise<void> {
}
}
clack.intro(c.bold("hyperframes dev"));
clack.intro(c.bold("hyperframes preview"));
const s = clack.spinner();
s.start("Starting studio...");
@@ -240,7 +240,7 @@ async function runLocalStudioMode(dir: string, projectName?: string): Promise<vo
}
}
clack.intro(c.bold("hyperframes dev") + c.dim(" (local studio)"));
clack.intro(c.bold("hyperframes preview") + c.dim(" (local studio)"));
const s = clack.spinner();
s.start("Starting studio...");
@@ -304,7 +304,7 @@ async function runEmbeddedMode(
const pName = projectName ?? basename(dir);
const { app } = createStudioServer({ projectDir: dir, projectName: pName });
clack.intro(c.bold("hyperframes dev"));
clack.intro(c.bold("hyperframes preview"));
const s = clack.spinner();
s.start("Starting studio...");
+1 -1
View File
@@ -53,7 +53,7 @@ ${c.bold("SUBCOMMANDS:")}
${c.accent("disable")} ${c.dim("Disable anonymous telemetry")}
${c.bold("WHAT WE COLLECT:")}
${c.dim("\u2022")} Command names (init, render, dev, etc.)
${c.dim("\u2022")} Command names (init, render, preview, etc.)
${c.dim("\u2022")} Render performance (duration, fps, quality)
${c.dim("\u2022")} Template choices
${c.dim("\u2022")} OS, architecture, Node.js version, CLI version
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Embedded studio server for `hyperframes dev` outside the monorepo.
* Embedded studio server for `hyperframes preview` outside the monorepo.
*
* Uses the shared studio API module from @hyperframes/core/studio-api,
* providing a CLI-specific adapter for single-project, in-process rendering.
+1 -1
View File
@@ -19,7 +19,7 @@
## Commands
```bash
npx hyperframes dev # preview in browser (studio editor)
npx hyperframes preview # preview in browser (studio editor)
npx hyperframes render # render to MP4
npx hyperframes lint # validate compositions (errors + warnings)
npx hyperframes lint --verbose # include info-level findings
+1 -1
View File
@@ -13,7 +13,7 @@ export interface LintFormatOptions {
}
/**
* Format lint findings for console output. Used by lint, render, and dev commands.
* Format lint findings for console output. Used by lint, render, and preview commands.
*/
export function formatLintFindings(
{ results, totalErrors, totalWarnings, totalInfos }: ProjectLintResult,