From 0b1d1ee1e8c9512b9ef1a47d37fcef9153967b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 30 Apr 2026 11:16:59 -0400 Subject: [PATCH] feat: scaffold package scripts on init --- packages/cli/src/commands/init.test.ts | 23 +++++++- packages/cli/src/commands/init.ts | 57 ++++++++++++++++++-- packages/cli/src/templates/_shared/AGENTS.md | 12 ++--- packages/cli/src/templates/_shared/CLAUDE.md | 13 ++--- 4 files changed, 88 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/commands/init.test.ts b/packages/cli/src/commands/init.test.ts index 132958fa1..7002b8c79 100644 --- a/packages/cli/src/commands/init.test.ts +++ b/packages/cli/src/commands/init.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { spawnSync } from "node:child_process"; -import { existsSync, mkdtempSync, rmSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -24,13 +24,32 @@ function runInit(args: string[]): { status: number; stdout: string; stderr: stri } describe("hyperframes init flag rename", () => { - it("--example blank scaffolds a bundled project", () => { + it("--example blank scaffolds a bundled project with npm scripts", () => { const dir = mkdtempSync(join(tmpdir(), "hf-init-test-")); const target = join(dir, "proj"); try { const res = runInit([target, "--example", "blank", "--non-interactive", "--skip-skills"]); expect(res.status).toBe(0); expect(existsSync(join(target, "index.html"))).toBe(true); + expect(res.stdout).toContain("npm run dev"); + expect(res.stdout).toContain("npm run check"); + expect(res.stdout).toContain("npm run render"); + + const pkg = JSON.parse(readFileSync(join(target, "package.json"), "utf-8")) as { + private?: boolean; + type?: string; + scripts?: Record; + }; + expect(pkg.private).toBe(true); + expect(pkg.type).toBe("module"); + expect(pkg.scripts).toMatchObject({ + dev: "npx --yes hyperframes preview", + check: + "npx --yes hyperframes lint && npx --yes hyperframes validate && npx --yes hyperframes inspect", + render: "npx --yes hyperframes render", + publish: "npx --yes hyperframes publish", + }); + expect(Object.keys(pkg.scripts ?? {}).sort()).toEqual(["check", "dev", "publish", "render"]); } finally { rmSync(dir, { recursive: true, force: true }); } diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 43a0d5f48..fb7a7d12a 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -32,6 +32,7 @@ import { import { fetchRemoteTemplate } from "../templates/remote.js"; import { trackInitTemplate } from "../telemetry/events.js"; import { hasFFmpeg } from "../whisper/manager.js"; +import { VERSION } from "../version.js"; interface VideoMeta { durationSeconds: number; @@ -168,6 +169,51 @@ function getSharedTemplateDir(): string { return resolveAssetDir(["..", "templates", "_shared"], ["templates", "_shared"]); } +function toPackageName(projectName: string): string { + const normalized = basename(projectName) + .trim() + .toLowerCase() + .replace(/^[._]+/, "") + .replace(/[^a-z0-9._~-]+/g, "-") + .replace(/-+/g, "-") + .replace(/^[-.]+|[-.]+$/g, ""); + + return normalized || "hyperframes-project"; +} + +function getHyperframesPackageSpecifier(): string { + return VERSION === "0.0.0-dev" ? "hyperframes" : `hyperframes@${VERSION}`; +} + +function hyperframesScript(command: string): string { + return `npx --yes ${getHyperframesPackageSpecifier()} ${command}`; +} + +function writeDefaultPackageJson(destDir: string, projectName: string): void { + const packageJsonPath = resolve(destDir, "package.json"); + if (existsSync(packageJsonPath)) return; + + writeFileSync( + packageJsonPath, + `${JSON.stringify( + { + name: toPackageName(projectName), + private: true, + type: "module", + scripts: { + dev: hyperframesScript("preview"), + check: `${hyperframesScript("lint")} && ${hyperframesScript("validate")} && ${hyperframesScript("inspect")}`, + render: hyperframesScript("render"), + publish: hyperframesScript("publish"), + }, + }, + null, + 2, + )}\n`, + "utf-8", + ); +} + function patchVideoSrc( dir: string, videoFilename: string | undefined, @@ -346,6 +392,8 @@ async function scaffoldProject( writeProjectConfig(destDir, DEFAULT_PROJECT_CONFIG); } + writeDefaultPackageJson(destDir, name); + // Copy shared files (CLAUDE.md, AGENTS.md) for AI agent context const sharedDir = getSharedTemplateDir(); if (existsSync(sharedDir)) { @@ -559,10 +607,13 @@ export default defineCommand({ console.log(` ${c.dim("More patterns: hyperframes.heygen.com/guides/prompting")}`); console.log(); console.log(` ${c.accent("4.")} Preview in the browser:`); - console.log(` ${c.accent(`cd ${name}`)} && ${c.accent("npx hyperframes preview")}`); + console.log(` ${c.accent(`cd ${name}`)} && ${c.accent("npm run dev")}`); + console.log(); + console.log(` ${c.accent("5.")} Check the composition:`); + console.log(` ${c.accent(`cd ${name}`)} && ${c.accent("npm run check")}`); console.log(); - console.log(` ${c.accent("5.")} Render to MP4 when ready:`); - console.log(` ${c.accent(`cd ${name}`)} && ${c.accent("npx hyperframes render")}`); + console.log(` ${c.accent("6.")} Render to MP4 when ready:`); + console.log(` ${c.accent(`cd ${name}`)} && ${c.accent("npm run render")}`); console.log(); console.log(` ${c.dim("Full docs: hyperframes.heygen.com")}`); return; diff --git a/packages/cli/src/templates/_shared/AGENTS.md b/packages/cli/src/templates/_shared/AGENTS.md index 506ecf394..9374d67c7 100644 --- a/packages/cli/src/templates/_shared/AGENTS.md +++ b/packages/cli/src/templates/_shared/AGENTS.md @@ -13,10 +13,10 @@ Skills encode patterns like `window.__timelines` registration, `data-*` attribut ## Commands ```bash -npx hyperframes preview # preview in browser (studio editor) -npx hyperframes render # render to MP4 -npx hyperframes lint # validate compositions (errors + warnings) -npx hyperframes lint --json # machine-readable output for CI +npm run dev # preview in browser (studio editor) +npm run check # lint + validate + inspect +npm run render # render to MP4 +npm run publish # publish and get a shareable link npx hyperframes docs # reference docs in terminal ``` @@ -30,10 +30,10 @@ npx hyperframes docs # reference docs in terminal ## Linting — Always Run After Changes -After creating or editing any `.html` composition, run the linter before considering the task complete: +After creating or editing any `.html` composition, run the full check before considering the task complete: ```bash -npx hyperframes lint +npm run check ``` Fix all errors before presenting the result. diff --git a/packages/cli/src/templates/_shared/CLAUDE.md b/packages/cli/src/templates/_shared/CLAUDE.md index d81825cb3..849ba0e39 100644 --- a/packages/cli/src/templates/_shared/CLAUDE.md +++ b/packages/cli/src/templates/_shared/CLAUDE.md @@ -23,9 +23,10 @@ ## Commands ```bash -npx hyperframes preview # preview in browser (studio editor) -npx hyperframes render # render to MP4 -npx hyperframes lint # validate compositions (errors + warnings) +npm run dev # preview in browser (studio editor) +npm run check # lint + validate + inspect +npm run render # render to MP4 +npm run publish # publish and get a shareable link npx hyperframes lint --verbose # include info-level findings npx hyperframes lint --json # machine-readable output for CI npx hyperframes docs # reference docs in terminal @@ -56,13 +57,13 @@ https://hyperframes.heygen.com/llms.txt ## Linting — ALWAYS RUN AFTER CHANGES -After creating or editing any `.html` composition, **always** run the linter before considering the task complete: +After creating or editing any `.html` composition, **always** run the full check before considering the task complete: ```bash -npx hyperframes lint +npm run check ``` -Fix all errors before presenting the result. Warnings are informational and usually safe to ignore. +Fix all errors before presenting the result. Inspect warnings should be reviewed before rendering. ## Key Rules