Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
One-time setup that gathers your project's design context and saves it to CLAUDE.md for future sessions.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/live.mjs
1/**2* CLI entry point: prepare everything needed to enter the live variant poll loop.3*4* Does (all in one command):5* 1. Check .impeccable/live/config.json (returns config_missing if first-ever run)6* 2. Start the live server in the background (or reuse a running one)7* 3. Inject the browser script tag into the project's entry file8* 4. Read PRODUCT.md / DESIGN.md for project context9* 5. Print a single JSON blob with everything the agent needs10*11* After this, the agent's only remaining steps are:12* - Open the project's live dev/preview URL in the browser (optional, if browser automation exists)—not `serverPort`; that port is the Impeccable helper for /live.js and /poll13* - Enter the poll loop: `node live-poll.mjs`14*15* Usage:16* node live.mjs # Prepare everything, print JSON, exit17* node live.mjs --help18*/1920import { execSync } from 'node:child_process';21import fs from 'node:fs';22import path from 'node:path';23import { fileURLToPath } from 'node:url';24import { loadContext, resolveTargetSelection } from './context.mjs';25import { resolveFiles } from './live-inject.mjs';26import { readLiveServerInfo } from './lib/impeccable-paths.mjs';27import { resolveLiveTarget } from './live-target.mjs';2829const __dirname = path.dirname(fileURLToPath(import.meta.url));3031async function liveCli() {32const args = process.argv.slice(2);33const liveTarget = resolveLiveTarget(process.cwd(), args);3435if (args.includes('--help') || args.includes('-h')) {36console.log(`Usage: node live.mjs3738Prepare everything for live variant mode in a single command:39- Checks .impeccable/live/config.json (required, created once per project)40- Starts (or reuses) the live server in the background41- Injects the browser script tag42- Reads PRODUCT.md / DESIGN.md for project context43- In monorepos, choose a child app first; --target <path> is the fallback/manual path4445On success, prints a JSON blob with:46{ ok, serverPort, serverToken, pageFiles, projectRoot, repoRoot, targetPath, productPath, designPath }4748On target_selection_required, prints:49{ ok: false, error: "target_selection_required", targetCandidates }5051On config_missing, prints:52{ ok: false, error: "config_missing", configPath, hint }5354The agent should then:551. If target_selection_required, ask which app to use and rerun from that child cwd562. If config_missing, create the config and re-run this script573. Optionally open the project's dev/preview URL in the browser (see reference/live.md—not serverPort)584. Enter the poll loop: node live-poll.mjs`);59process.exit(0);60}6162const targetSelection = resolveTargetSelection(liveTarget.originalCwd, liveTarget.targetOptions);63if (targetSelection) {64console.log(JSON.stringify({65ok: false,66error: 'target_selection_required',67...targetSelection,68hint: 'Ask the user which app Impeccable should use, then rerun live from that child app cwd. Use --target <path> only as a fallback or explicit path diagnostic.',69}, null, 2));70process.exit(0);71}7273const ctx = loadContext(liveTarget.originalCwd, liveTarget.targetOptions);74const activeCwd = ctx.projectRoot;75const outputTargetPath = liveTarget.targetPath || null;7677const missingContext = missingLiveContext(ctx);78if (missingContext.length > 0) {79console.log(JSON.stringify({80ok: false,81error: 'context_missing',82missing: missingContext,83nextCommand: missingContext.includes('PRODUCT.md') ? 'init' : 'document',84targetPath: outputTargetPath,85projectRoot: ctx.projectRoot,86repoRoot: ctx.repoRoot,87productPath: ctx.productPath,88designPath: ctx.designPath,89}, null, 2));90process.exit(0);91}9293// 1. Check config (fail fast if missing — no point starting anything else)94const checkOut = runScript('live-inject.mjs', ['--check'], { cwd: activeCwd });95const checkResult = safeParse(checkOut);96if (!checkResult || !checkResult.ok) {97console.log(JSON.stringify({98...(checkResult || { ok: false, error: 'check_failed', raw: checkOut }),99targetPath: outputTargetPath,100projectRoot: ctx.projectRoot,101repoRoot: ctx.repoRoot,102}));103process.exit(0);104}105106// 2. Start server (or reuse existing)107const serverInfo = ensureServerRunning(activeCwd);108if (!serverInfo) {109console.log(JSON.stringify({ ok: false, error: 'server_start_failed' }));110process.exit(1);111}112113// 3. Inject the script tag at the current port114const injectOut = runScript('live-inject.mjs', ['--port', String(serverInfo.port)], { cwd: activeCwd });115const injectResult = safeParse(injectOut);116if (!injectResult || !injectResult.ok) {117console.log(JSON.stringify({118ok: false,119error: 'inject_failed',120detail: injectResult || injectOut,121serverPort: serverInfo.port,122}));123process.exit(1);124}125126// 4. Compute drift-heal: compare resolved inject targets against the127// project's HTML files. Orphans are HTML files not covered by config.128// Warning only — the agent decides whether to act.129const resolvedFiles = resolveFiles(activeCwd, checkResult.config);130const drift = scanForDrift(activeCwd, resolvedFiles, checkResult.config);131132// 5. Emit everything the agent needs133console.log(JSON.stringify({134ok: true,135serverPort: serverInfo.port,136serverToken: serverInfo.token,137pageFiles: resolvedFiles,138liveConfigPath: checkResult.path,139configDrift: drift,140targetPath: outputTargetPath,141projectRoot: ctx.projectRoot,142repoRoot: ctx.repoRoot,143hasProduct: ctx.hasProduct,144product: ctx.product,145productPath: ctx.productPath,146hasDesign: ctx.hasDesign,147design: ctx.design,148designPath: ctx.designPath,149}, null, 2));150}151152function missingLiveContext(ctx) {153const missing = [];154if (!ctx.hasProduct) missing.push('PRODUCT.md');155if (!ctx.hasDesign) missing.push('DESIGN.md');156return missing;157}158159/**160* Drift-heal scan. Walks the project for HTML files under common161* page-source directories (public/, src/, app/, pages/) and reports any162* that aren't covered by the resolved inject targets. This is purely163* advisory — the agent can ignore it, or suggest the user add the164* orphans to config.files.165*166* Skipped if config.files already contains at least one glob pattern167* covering everything in practice (signaled by the orphan count being 0).168*/169function scanForDrift(rootDir, resolvedFiles, config) {170const SCAN_ROOTS = ['public', 'src', 'app', 'pages'];171const IGNORE_DIRS = new Set([172'node_modules', '.git', '.next', '.nuxt', '.svelte-kit', '.astro',173'.turbo', '.vercel', '.cache', 'coverage', 'dist', 'build',174]);175176const resolvedSet = new Set(resolvedFiles.map((f) => f.split(path.sep).join('/')));177178// Files matching the user's `exclude` globs are intentional omissions,179// not drift. Compile them to regexes so the orphan list stays signal.180const userExcludeRegexes = (Array.isArray(config.exclude) ? config.exclude : [])181.map((p) => globToRegex(p));182const isUserExcluded = (rel) => userExcludeRegexes.some((re) => re.test(rel));183184const orphans = [];185186const walk = (dir, relBase) => {187let entries;188try { entries = fs.readdirSync(dir, { withFileTypes: true }); }189catch { return; }190for (const e of entries) {191const rel = relBase ? `${relBase}/${e.name}` : e.name;192if (e.isDirectory()) {193if (IGNORE_DIRS.has(e.name) || e.name.startsWith('.')) continue;194walk(path.join(dir, e.name), rel);195} else if (e.isFile() && e.name.endsWith('.html')) {196if (resolvedSet.has(rel)) continue;197if (isUserExcluded(rel)) continue;198orphans.push(rel);199}200}201};202203for (const root of SCAN_ROOTS) {204const abs = path.join(rootDir, root);205if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) {206walk(abs, root);207}208}209210if (orphans.length === 0) return null;211const capped = orphans.slice(0, 20);212return {213orphans: capped,214orphanCount: orphans.length,215hint: `${orphans.length} HTML file(s) exist but aren't in config.files. Consider adding them, or use a glob pattern like "public/**/*.html".`,216};217}218219/**220* Same glob-to-regex mapping used by live-inject.mjs. Kept inline here221* to avoid a circular import (live-inject.mjs already imports nothing222* from live.mjs). The two must stay in sync.223*/224function globToRegex(pattern) {225let re = '';226let i = 0;227while (i < pattern.length) {228const c = pattern[i];229if (c === '*') {230if (pattern[i + 1] === '*') {231if (pattern[i + 2] === '/') { re += '(?:.*/)?'; i += 3; }232else { re += '.*'; i += 2; }233} else {234re += '[^/]*';235i += 1;236}237} else if (c === '?') {238re += '[^/]';239i += 1;240} else if (/[.+^${}()|[\]\\]/.test(c)) {241re += '\\' + c;242i += 1;243} else {244re += c;245i += 1;246}247}248return new RegExp('^' + re + '$');249}250251// ---------------------------------------------------------------------------252// Helpers253// ---------------------------------------------------------------------------254255function runScript(name, args, options = {}) {256const scriptPath = path.join(__dirname, name);257const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;258try {259return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });260} catch (err) {261// execSync throws on non-zero exit; return stdout if any262return err.stdout || err.message || '';263}264}265266function safeParse(out) {267try { return JSON.parse(String(out).trim()); } catch { return null; }268}269270/**271* Return { pid, port, token } for the running live server, starting one if needed.272*/273function ensureServerRunning(cwd = process.cwd()) {274// Try to reuse an existing server275try {276const existing = readLiveServerInfo(cwd)?.info;277if (existing && existing.pid) {278try {279process.kill(existing.pid, 0); // throws if dead280return existing;281} catch { /* stale PID file — the server script will clean it up */ }282}283} catch { /* no PID file */ }284285// Start a new server286const out = runScript('live-server.mjs', ['--background'], { cwd });287return safeParse(out);288}289290// ---------------------------------------------------------------------------291// Auto-execute292// ---------------------------------------------------------------------------293294const _running = process.argv[1];295if (_running?.endsWith('live.mjs') || _running?.endsWith('live.mjs/')) {296liveCli();297}298