From 6a98c5bd25f124288ec6ea0303218e0f07f4c9d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 21 Mar 2026 02:29:26 -0400 Subject: [PATCH] fix(cli): update dev command for consolidated studio - hyperframes dev: use pnpm exec vite for the new studio package - Symlink projects into studio/data/projects - Clean embedded mode stub (not yet available for published CLI) - Remove internal-only scripts from root package.json - Strip archive/producer-internal/ngrok references --- package.json | 8 +- packages/cli/package.json | 5 +- packages/cli/src/commands/dev.ts | 208 ++++--------------------------- 3 files changed, 27 insertions(+), 194 deletions(-) diff --git a/package.json b/package.json index 980ae319f..498ce20ee 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,8 @@ "scripts": { "dev": "pnpm studio", "build": "pnpm -r build", - "build:producer": "pnpm --filter @hyperframes/producer build", - "build:hyperframes-runtime": "pnpm --filter @hyperframes/core build:hyperframes-runtime", - "build:hyperframes-runtime:modular": "pnpm --filter @hyperframes/core build:hyperframes-runtime:modular", - "studio": "concurrently \"pnpm --filter @hyperframes/studio-backend dev\" \"pnpm --filter @hyperframes/studio-frontend dev\"", - "studio-backend": "pnpm --filter @hyperframes/studio-backend dev", - "studio-frontend": "pnpm --filter @hyperframes/studio-frontend dev" + "studio": "pnpm --filter @hyperframes/studio dev", + "build:hyperframes-runtime": "pnpm --filter @hyperframes/core build:hyperframes-runtime" }, "devDependencies": { "@types/node": "^25.0.10", diff --git a/packages/cli/package.json b/packages/cli/package.json index c2567c580..373d2e38e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -12,9 +12,9 @@ "scripts": { "dev": "tsx src/cli.ts", "build": "pnpm build:studio && tsup && pnpm build:runtime && pnpm build:copy", - "build:studio": "cd ../studio/frontend && pnpm build", + "build:studio": "cd ../studio && pnpm build", "build:runtime": "tsx scripts/build-runtime.ts", - "build:copy": "mkdir -p dist/studio dist/docs dist/templates && cp -r ../studio/frontend/dist/* dist/studio/ && cp -r src/templates/warm-grain src/templates/play-mode src/templates/swiss-grid src/templates/vignelli dist/templates/ && (cp src/docs/*.md dist/docs/ 2>/dev/null || true)", + "build:copy": "mkdir -p dist/studio dist/docs dist/templates && cp -r ../studio/dist/* dist/studio/ && cp -r src/templates/warm-grain src/templates/play-mode src/templates/swiss-grid src/templates/vignelli dist/templates/ && (cp src/docs/*.md dist/docs/ 2>/dev/null || true)", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -31,7 +31,6 @@ }, "devDependencies": { "@hyperframes/core": "workspace:*", - "@hyperframes/studio-backend": "workspace:*", "@clack/prompts": "^1.1.0", "@hono/node-server": "^1.0.0", "@hyperframes/engine": "workspace:*", diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index cac67d91e..499cc7133 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -7,13 +7,11 @@ import { unlinkSync, readlinkSync, mkdirSync, - readFileSync, } from "node:fs"; import { resolve, dirname, basename, join } from "node:path"; import { fileURLToPath } from "node:url"; import * as clack from "@clack/prompts"; import { c } from "../ui/colors.js"; -import { MIME_TYPES } from "../utils/mime.js"; /** * Check if a port is available by trying to listen on it briefly. @@ -73,12 +71,12 @@ export default defineCommand({ * Dev mode: spawn pnpm studio from the monorepo (existing behavior). */ async function runDevMode(dir: string): Promise { - // Find monorepo root by navigating from cli/ package + // Find monorepo root by navigating from packages/cli/src/commands/ const thisFile = fileURLToPath(import.meta.url); - const repoRoot = resolve(dirname(thisFile), "..", "..", ".."); + const repoRoot = resolve(dirname(thisFile), "..", "..", "..", ".."); - // Symlink project into studio's data directory so it appears in the project list - const projectsDir = join(repoRoot, "studio", "backend", "data", "projects"); + // Symlink project into the studio's data directory + const projectsDir = join(repoRoot, "packages", "studio", "data", "projects"); const projectName = basename(dir); const symlinkPath = join(projectsDir, projectName); @@ -88,10 +86,14 @@ async function runDevMode(dir: string): Promise { if (dir !== symlinkPath) { if (existsSync(symlinkPath)) { try { - const target = readlinkSync(symlinkPath); - if (resolve(target) !== dir) { - unlinkSync(symlinkPath); + const stat = lstatSync(symlinkPath); + if (stat.isSymbolicLink()) { + const target = readlinkSync(symlinkPath); + if (resolve(target) !== resolve(dir)) { + unlinkSync(symlinkPath); + } } + // If it's a real directory, leave it alone } catch { // Not a symlink — don't touch it } @@ -108,47 +110,33 @@ async function runDevMode(dir: string): Promise { const s = clack.spinner(); s.start("Starting studio..."); - // Pipe child output so we can parse it and show clean output - const child = spawn("pnpm", ["studio"], { - cwd: repoRoot, + // Run the new consolidated studio (single Vite dev server with API plugin) + const studioPkgDir = join(repoRoot, "packages", "studio"); + const child = spawn("pnpm", ["exec", "vite"], { + cwd: studioPkgDir, stdio: ["ignore", "pipe", "pipe"], }); - let backendReady = false; let frontendUrl = ""; function handleOutput(data: Buffer): void { const text = data.toString(); - // Detect backend ready - if (!backendReady && text.includes("Studio backend running")) { - backendReady = true; - } - - // Detect frontend URL (Vite may pick a different port) + // Detect Vite URL const localMatch = text.match(/Local:\s+(http:\/\/localhost:\d+)/); - if (localMatch) { + if (localMatch && !frontendUrl) { frontendUrl = localMatch[1] ?? ""; - } - - // Once both are ready, show the clean output - if (backendReady && frontendUrl) { s.stop(c.success("Studio running")); console.log(); console.log(` ${c.dim("Project")} ${c.accent(projectName)}`); - console.log(` ${c.dim("Backend")} ${c.accent("http://localhost:3002")}`); - console.log(` ${c.dim("Frontend")} ${c.accent(frontendUrl)}`); + console.log(` ${c.dim("Studio")} ${c.accent(frontendUrl)}`); console.log(); console.log(` ${c.dim("Press Ctrl+C to stop")}`); console.log(); - // Open browser — capture URL before clearing state const urlToOpen = `${frontendUrl}#/project/${projectName}`; import("open").then((mod) => mod.default(urlToOpen)).catch(() => {}); - // Stop listening — we don't need to parse anymore - backendReady = false; - frontendUrl = ""; child.stdout?.removeListener("data", handleOutput); child.stderr?.removeListener("data", handleOutput); } @@ -189,160 +177,10 @@ async function runDevMode(dir: string): Promise { } /** - * Embedded mode: start an inline Hono server with the studio backend routes - * and serve the pre-built frontend from dist/studio/. + * Embedded mode — not yet available. + * TODO: Migrate to use @hyperframes/studio's built-in Vite server for published CLI. */ -async function runEmbeddedMode(dir: string, port: number): Promise { - const projectName = basename(dir); - - // Resolve the studio frontend dist directory relative to the CLI bundle - const thisFile = fileURLToPath(import.meta.url); - const studioDir = join(dirname(thisFile), "studio"); - - if (!existsSync(studioDir)) { - console.error( - c.error( - `Studio frontend not found at ${studioDir}. Did you run 'pnpm build'?`, - ), - ); - process.exit(1); - } - - // Set up data directories and env vars BEFORE importing the studio backend - // routes, since the route modules read these env vars at initialization time. - const dataDir = join(dirname(thisFile), "data", "projects"); - mkdirSync(dataDir, { recursive: true }); - process.env.STUDIO_DATA_DIR = dataDir; - - const rendersDir = join(dirname(thisFile), "data", "renders"); - mkdirSync(rendersDir, { recursive: true }); - process.env.STUDIO_RENDERS_DIR = rendersDir; - - // Import after env vars are set so the route modules pick up the correct paths - const { serve } = await import("@hono/node-server"); - const { createEmbeddedApp } = await import( - "@hyperframes/studio-backend/embedded" - ); - - // Symlink the project into the data directory - const symlinkPath = join(dataDir, projectName); - let createdSymlink = false; - - if (dir !== symlinkPath) { - // Check if something already exists at the symlink path - let needsCreate = true; - try { - const stat = lstatSync(symlinkPath); - if (stat.isSymbolicLink()) { - const target = readlinkSync(symlinkPath); - if (resolve(target) === resolve(dir)) { - needsCreate = false; // Already points to the right place - } else { - unlinkSync(symlinkPath); // Points elsewhere, replace it - } - } - // If it's a real directory, leave it alone - if (stat.isDirectory() && !stat.isSymbolicLink()) { - needsCreate = false; - } - } catch { - // Nothing at that path — good, we'll create it - } - - if (needsCreate) { - symlinkSync(dir, symlinkPath, "dir"); - createdSymlink = true; - } - } - - // Create the Hono app with all studio backend routes. - // The factory uses the same Hono import as the routes, avoiding class - // mismatches when tsup bundles multiple copies of the Hono module. - const app = createEmbeddedApp(); - - // port is passed as parameter from findAvailablePort() - - // Static file serving: use Hono's notFound handler for SPA fallback - // and register explicit static asset routes. - function serveStaticFile(urlPath: string): Response | null { - const relativePath = urlPath.replace(/^\//, ""); - const filePath = resolve(studioDir, relativePath); - if (!filePath.startsWith(resolve(studioDir) + "/")) return null; - if (!existsSync(filePath)) return null; - - const content = readFileSync(filePath); - const ext = filePath.split(".").pop() ?? ""; - const contentType = MIME_TYPES["." + ext] ?? "application/octet-stream"; - return new Response(content, { - headers: { "Content-Type": contentType }, - }); - } - - // Catch-all: serve static files, then SPA fallback for non-API routes - app.notFound((ctx) => { - // Try to serve a static file from the studio frontend directory - const urlPath = ctx.req.path === "/" ? "/index.html" : ctx.req.path; - const staticResponse = serveStaticFile(urlPath); - if (staticResponse) return staticResponse; - - // SPA fallback for non-API routes - if (!ctx.req.path.startsWith("/api/")) { - const indexPath = join(studioDir, "index.html"); - if (existsSync(indexPath)) { - return ctx.html(readFileSync(indexPath, "utf-8")); - } - } - - return ctx.text("Not found", 404); - }); - - clack.intro(c.bold("hyperframes dev")); - - const s = clack.spinner(); - s.start("Starting embedded studio..."); - - const server = serve({ - fetch: app.fetch, - port, - }); - - const studioUrl = `http://localhost:${port}`; - - s.stop(c.success("Studio running")); - console.log(); - console.log(` ${c.dim("Project")} ${c.accent(projectName)}`); - console.log(` ${c.dim("Studio")} ${c.accent(studioUrl)}`); - console.log(); - console.log(` ${c.dim("Press Ctrl+C to stop")}`); - console.log(); - - // Open browser (skip if HYPERFRAMES_NO_OPEN is set, useful for testing) - if (!process.env.HYPERFRAMES_NO_OPEN) { - const urlToOpen = `${studioUrl}/#/project/${projectName}`; - import("open") - .then((mod) => mod.default(urlToOpen)) - .catch(() => {}); - } - - // Wait for SIGINT to shut down - return new Promise((resolvePromise) => { - function cleanup(): void { - if (createdSymlink && existsSync(symlinkPath)) { - try { - unlinkSync(symlinkPath); - } catch { - /* ignore */ - } - } - } - - process.on("SIGINT", () => { - console.log(); - console.log(c.dim(" Shutting down...")); - server.close(() => { - cleanup(); - resolvePromise(); - }); - }); - }); +async function runEmbeddedMode(_dir: string, _port: number): Promise { + console.error(c.error("Embedded mode not yet available. Run from the monorepo root with: hyperframes dev ")); + process.exit(1); }