import { defineCommand } from "citty"; import type { Example } from "./_examples.js"; import { existsSync, readFileSync } from "node:fs"; export const examples: Example[] = [ ["Present the current deck", "hyperframes present"], ["Present a specific project directory", "hyperframes present ./my-deck"], ["Use a custom port", "hyperframes present --port 8080"], ["Start without opening the browser", "hyperframes present --no-open"], ["Open with a specific browser", "hyperframes present --browser-path /usr/bin/chromium"], ]; import { resolve } from "node:path"; import * as clack from "@clack/prompts"; import { c } from "../ui/colors.js"; import { resolveProject } from "../utils/project.js"; import { openBrowser, parseRemoteDebuggingPort, validateRemoteDebuggingPortDeps, } from "../utils/openBrowser.js"; import { resolvePlayerPath, resolveSlideshowPath, listenOnFreePort, assetContentType, } from "../utils/compositionServer.js"; export default defineCommand({ meta: { name: "present", description: "Serve a slideshow deck and open it in presenter mode (with audience sync)", }, args: { dir: { type: "positional", description: "Project directory", required: false }, port: { type: "string", description: "Port to run the present server on", default: "3004" }, open: { type: "boolean", default: true, description: "Open browser automatically" }, "browser-path": { type: "string", description: "Path to the browser executable to open" }, "user-data-dir": { type: "string", description: "Chromium-compatible user data directory (requires --browser-path)", }, "remote-debugging-port": { type: "string", description: "Chromium remote debugging port (requires --browser-path and --user-data-dir)", }, }, async run({ args }) { const project = resolveProject(args.dir); const startPort = parseInt(args.port ?? "3004", 10); if (args["user-data-dir"] && !args["browser-path"]) { clack.log.error("--user-data-dir requires --browser-path"); process.exitCode = 1; return; } const depsError = validateRemoteDebuggingPortDeps({ browserPath: args["browser-path"] as string | undefined, userDataDir: args["user-data-dir"] as string | undefined, remoteDebuggingPort: args["remote-debugging-port"] as string | undefined, }); if (depsError) { clack.log.error(depsError); process.exitCode = 1; return; } let remoteDebuggingPort: number | undefined; try { remoteDebuggingPort = parseRemoteDebuggingPort( args["remote-debugging-port"] as string | undefined, ); } catch (err) { clack.log.error((err as Error).message); process.exitCode = 1; return; } const playerPath = resolvePlayerPath(); const slideshowPath = resolveSlideshowPath(); if (!playerPath || !slideshowPath) { clack.log.error( "@hyperframes/player not found. Run `bun run --cwd packages/player build` first.", ); process.exitCode = 1; return; } // The deck must carry a slideshow island; the presenter view is meaningless // without one. Extract it here so we can inline it into the wrapper page. const indexHtml = readFileSync(project.indexPath, "utf-8"); const { slideshowIslandRegex } = await import("@hyperframes/core/slideshow"); const islandMatch = slideshowIslandRegex("i").exec(indexHtml); if (!islandMatch?.[1]) { clack.log.error( `No slideshow island found in ${project.indexPath}. ` + `Add a