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 } from './load-context.mjs';25import { resolveFiles } from './live-inject.mjs';26import { readLiveServerInfo } from './impeccable-paths.mjs';2728const __dirname = path.dirname(fileURLToPath(import.meta.url));2930async function liveCli() {31const args = process.argv.slice(2);3233if (args.includes('--help') || args.includes('-h')) {34console.log(`Usage: node live.mjs3536Prepare everything for live variant mode in a single command:37- Checks .impeccable/live/config.json (required, created once per project)38- Starts (or reuses) the live server in the background39- Injects the browser script tag40- Reads PRODUCT.md / DESIGN.md for project context4142On success, prints a JSON blob with:43{ ok, serverPort, serverToken, pageFile, hasContext, context }4445On config_missing, prints:46{ ok: false, error: "config_missing", configPath, hint }4748The agent should then:491. If config_missing, create the config and re-run this script502. Optionally open the project's dev/preview URL in the browser (see reference/live.md—not serverPort)513. Enter the poll loop: node live-poll.mjs`);52process.exit(0);53}5455// 1. Check config (fail fast if missing — no point starting anything else)56const checkOut = runScript('live-inject.mjs', ['--check']);57const checkResult = safeParse(checkOut);58if (!checkResult || !checkResult.ok) {59console.log(JSON.stringify(checkResult || { ok: false, error: 'check_failed', raw: checkOut }));60process.exit(0);61}6263// 2. Start server (or reuse existing)64const serverInfo = ensureServerRunning();65if (!serverInfo) {66console.log(JSON.stringify({ ok: false, error: 'server_start_failed' }));67process.exit(1);68}6970// 3. Inject the script tag at the current port71const injectOut = runScript('live-inject.mjs', ['--port', String(serverInfo.port)]);72const injectResult = safeParse(injectOut);73if (!injectResult || !injectResult.ok) {74console.log(JSON.stringify({75ok: false,76error: 'inject_failed',77detail: injectResult || injectOut,78serverPort: serverInfo.port,79}));80process.exit(1);81}8283// 4. Load PRODUCT.md + DESIGN.md context (auto-migrates legacy .impeccable.md)84const ctx = loadContext(process.cwd());8586// 5. Compute drift-heal: compare resolved inject targets against the87// project's HTML files. Orphans are HTML files not covered by config.88// Warning only — the agent decides whether to act.89const resolvedFiles = resolveFiles(process.cwd(), checkResult.config);90const drift = scanForDrift(process.cwd(), resolvedFiles, checkResult.config);9192// 6. Emit everything the agent needs93console.log(JSON.stringify({94ok: true,95serverPort: serverInfo.port,96serverToken: serverInfo.token,97pageFiles: resolvedFiles,98configDrift: drift,99hasProduct: ctx.hasProduct,100product: ctx.product,101productPath: ctx.productPath,102hasDesign: ctx.hasDesign,103design: ctx.design,104designPath: ctx.designPath,105migrated: ctx.migrated,106}, null, 2));107}108109/**110* Drift-heal scan. Walks the project for HTML files under common111* page-source directories (public/, src/, app/, pages/) and reports any112* that aren't covered by the resolved inject targets. This is purely113* advisory — the agent can ignore it, or suggest the user add the114* orphans to config.files.115*116* Skipped if config.files already contains at least one glob pattern117* covering everything in practice (signaled by the orphan count being 0).118*/119function scanForDrift(rootDir, resolvedFiles, config) {120const SCAN_ROOTS = ['public', 'src', 'app', 'pages'];121const IGNORE_DIRS = new Set([122'node_modules', '.git', '.next', '.nuxt', '.svelte-kit', '.astro',123'.turbo', '.vercel', '.cache', 'coverage', 'dist', 'build',124]);125126const resolvedSet = new Set(resolvedFiles.map((f) => f.split(path.sep).join('/')));127128// Files matching the user's `exclude` globs are intentional omissions,129// not drift. Compile them to regexes so the orphan list stays signal.130const userExcludeRegexes = (Array.isArray(config.exclude) ? config.exclude : [])131.map((p) => globToRegex(p));132const isUserExcluded = (rel) => userExcludeRegexes.some((re) => re.test(rel));133134const orphans = [];135136const walk = (dir, relBase) => {137let entries;138try { entries = fs.readdirSync(dir, { withFileTypes: true }); }139catch { return; }140for (const e of entries) {141const rel = relBase ? `${relBase}/${e.name}` : e.name;142if (e.isDirectory()) {143if (IGNORE_DIRS.has(e.name) || e.name.startsWith('.')) continue;144walk(path.join(dir, e.name), rel);145} else if (e.isFile() && e.name.endsWith('.html')) {146if (resolvedSet.has(rel)) continue;147if (isUserExcluded(rel)) continue;148orphans.push(rel);149}150}151};152153for (const root of SCAN_ROOTS) {154const abs = path.join(rootDir, root);155if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) {156walk(abs, root);157}158}159160if (orphans.length === 0) return null;161const capped = orphans.slice(0, 20);162return {163orphans: capped,164orphanCount: orphans.length,165hint: `${orphans.length} HTML file(s) exist but aren't in config.files. Consider adding them, or use a glob pattern like "public/**/*.html".`,166};167}168169/**170* Same glob-to-regex mapping used by live-inject.mjs. Kept inline here171* to avoid a circular import (live-inject.mjs already imports nothing172* from live.mjs). The two must stay in sync.173*/174function globToRegex(pattern) {175let re = '';176let i = 0;177while (i < pattern.length) {178const c = pattern[i];179if (c === '*') {180if (pattern[i + 1] === '*') {181if (pattern[i + 2] === '/') { re += '(?:.*/)?'; i += 3; }182else { re += '.*'; i += 2; }183} else {184re += '[^/]*';185i += 1;186}187} else if (c === '?') {188re += '[^/]';189i += 1;190} else if (/[.+^${}()|[\]\\]/.test(c)) {191re += '\\' + c;192i += 1;193} else {194re += c;195i += 1;196}197}198return new RegExp('^' + re + '$');199}200201// ---------------------------------------------------------------------------202// Helpers203// ---------------------------------------------------------------------------204205function runScript(name, args) {206const scriptPath = path.join(__dirname, name);207const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;208try {209return execSync(cmd, { encoding: 'utf-8', cwd: process.cwd(), timeout: 15_000 });210} catch (err) {211// execSync throws on non-zero exit; return stdout if any212return err.stdout || err.message || '';213}214}215216function safeParse(out) {217try { return JSON.parse(String(out).trim()); } catch { return null; }218}219220/**221* Return { pid, port, token } for the running live server, starting one if needed.222*/223function ensureServerRunning() {224// Try to reuse an existing server225try {226const existing = readLiveServerInfo(process.cwd())?.info;227if (existing && existing.pid) {228try {229process.kill(existing.pid, 0); // throws if dead230return existing;231} catch { /* stale PID file — the server script will clean it up */ }232}233} catch { /* no PID file */ }234235// Start a new server236const out = runScript('live-server.mjs', ['--background']);237return safeParse(out);238}239240// ---------------------------------------------------------------------------241// Auto-execute242// ---------------------------------------------------------------------------243244const _running = process.argv[1];245if (_running?.endsWith('live.mjs') || _running?.endsWith('live.mjs/')) {246liveCli();247}248