Add live.mjs combined entry point for fast startup

Previously, starting live mode required ~5-6 sequential bash calls:
read .impeccable.md, start server, check config, read reference, inject
tag, verify. The new live.mjs does all of this in a single command
(~340ms cold, ~90ms when reusing a running server) and returns everything
the agent needs in one JSON blob.

Workflow is now:
  1. node live.mjs        # start + inject + load context (1 bash call)
  2. navigate browser     # optional MCP call
  3. node live-poll.mjs   # enter poll loop

Reference doc collapsed to a single "Start Live Mode" section with the
one-command path plus a first-time config creation fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-13 16:56:20 -07:00
co-authored by Claude Opus 4.6
parent 8030bc226a
commit 996c9af78c
24 changed files with 1992 additions and 336 deletions
+23 -28
View File
@@ -4,28 +4,33 @@ Launch interactive live variant mode: select elements in the browser, pick a des
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
## Start the Server
## Start Live Mode (one command)
1. Read `.impeccable.md` if it exists. Keep the design context in mind for variant generation.
2. Start the live variant server in the background. The `--background` flag spawns a detached server process, waits for it to be ready, prints the connection JSON to stdout, and exits:
```bash
node {{scripts_path}}/live-server.mjs --background
```
The output JSON contains `port` and `token`. Use the port for the script tag below.
## Inject the Browser Script
The `live-inject.mjs` script handles insertion deterministically. It reads `config.json` from its own directory (one-time per-project setup).
### Step 1: Ensure config.json exists
The `live.mjs` entry point does everything in a single call: checks config, starts (or reuses) the server, injects the script tag, loads `.impeccable.md` context.
```bash
node {{scripts_path}}/live-inject.mjs --check
node {{scripts_path}}/live.mjs
```
If the output says `{"ok": true, ...}`, skip to Step 2.
### Happy path
If the output says `{"ok": false, "error": "config_missing", "path": "..."}`, you need to create the config **once**. Look at the project structure and package.json to determine:
Output JSON:
```json
{
"ok": true,
"serverPort": 8400,
"serverToken": "...",
"pageFile": "public/index.html",
"hasContext": true,
"context": "...full .impeccable.md contents..."
}
```
Keep the `context` in mind for variant generation. If browser automation tools are available, navigate to the page so the user can see it. Then proceed directly to the poll loop — no other setup steps needed.
### First-time setup (config missing)
If `live.mjs` outputs `{"ok": false, "error": "config_missing", "configPath": "..."}`, this project has never used live mode before. Create the config at the reported path based on the project's framework:
| Framework | `file` | `insertBefore` | `commentSyntax` |
|-----------|--------|----------------|-----------------|
@@ -38,7 +43,7 @@ If the output says `{"ok": false, "error": "config_missing", "path": "..."}`, yo
| Astro | the root layout `.astro` file | `</body>` | `html` |
| Static site with a non-root HTML file | e.g. `public/index.html` | `</body>` | `html` |
Write the config to the path reported by `--check`. Example for this project:
Use `insertAfter` instead of `insertBefore` if the anchor should be matched **after** a specific line. Example:
```json
{
@@ -48,17 +53,7 @@ Write the config to the path reported by `--check`. Example for this project:
}
```
Use `insertAfter` instead of `insertBefore` if the anchor should be matched **after** a specific line (e.g. just after the main app script).
### Step 2: Insert the live tag
```bash
node {{scripts_path}}/live-inject.mjs --port PORT
```
Use the `port` from the live-server startup output. The script writes the tag idempotently: if a stale tag is present, it's replaced with one pointing at the new port. Save is automatic.
If browser automation tools are available, also navigate to the page so the user can see it.
Then re-run `node {{scripts_path}}/live.mjs` to proceed.
## Enter the Poll Loop
@@ -0,0 +1,143 @@
/**
* CLI entry point: prepare everything needed to enter the live variant poll loop.
*
* Does (all in one command):
* 1. Check config.json (returns config_missing if first-ever run)
* 2. Start the live server in the background (or reuse a running one)
* 3. Inject the browser script tag into the project's entry file
* 4. Read .impeccable.md for design context (if present)
* 5. Print a single JSON blob with everything the agent needs
*
* After this, the agent's only remaining steps are:
* - Navigate the browser to the page (optional, if browser automation is available)
* - Enter the poll loop: `node live-poll.mjs`
*
* Usage:
* node live.mjs # Prepare everything, print JSON, exit
* node live.mjs --help
*/
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PID_FILE = path.join(process.cwd(), '.impeccable-live.json');
const CONTEXT_FILE = path.join(process.cwd(), '.impeccable.md');
async function liveCli() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`Usage: node live.mjs
Prepare everything for live variant mode in a single command:
- Checks scripts/config.json (required, created once per project)
- Starts (or reuses) the live server in the background
- Injects the browser script tag
- Reads .impeccable.md for design context
On success, prints a JSON blob with:
{ ok, serverPort, serverToken, pageFile, hasContext, context }
On config_missing, prints:
{ ok: false, error: "config_missing", configPath, hint }
The agent should then:
1. If config_missing, create the config and re-run this script
2. Optionally navigate the browser to the page
3. Enter the poll loop: node live-poll.mjs`);
process.exit(0);
}
// 1. Check config (fail fast if missing — no point starting anything else)
const checkOut = runScript('live-inject.mjs', ['--check']);
const checkResult = safeParse(checkOut);
if (!checkResult || !checkResult.ok) {
console.log(JSON.stringify(checkResult || { ok: false, error: 'check_failed', raw: checkOut }));
process.exit(0);
}
// 2. Start server (or reuse existing)
const serverInfo = ensureServerRunning();
if (!serverInfo) {
console.log(JSON.stringify({ ok: false, error: 'server_start_failed' }));
process.exit(1);
}
// 3. Inject the script tag at the current port
const injectOut = runScript('live-inject.mjs', ['--port', String(serverInfo.port)]);
const injectResult = safeParse(injectOut);
if (!injectResult || !injectResult.ok) {
console.log(JSON.stringify({
ok: false,
error: 'inject_failed',
detail: injectResult || injectOut,
serverPort: serverInfo.port,
}));
process.exit(1);
}
// 4. Load design context if available
let context = null;
try { context = fs.readFileSync(CONTEXT_FILE, 'utf-8'); } catch { /* optional */ }
// 5. Emit everything the agent needs
console.log(JSON.stringify({
ok: true,
serverPort: serverInfo.port,
serverToken: serverInfo.token,
pageFile: checkResult.config.file,
hasContext: !!context,
context,
}, null, 2));
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function runScript(name, args) {
const scriptPath = path.join(__dirname, name);
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
try {
return execSync(cmd, { encoding: 'utf-8', cwd: process.cwd(), timeout: 15_000 });
} catch (err) {
// execSync throws on non-zero exit; return stdout if any
return err.stdout || err.message || '';
}
}
function safeParse(out) {
try { return JSON.parse(String(out).trim()); } catch { return null; }
}
/**
* Return { pid, port, token } for the running live server, starting one if needed.
*/
function ensureServerRunning() {
// Try to reuse an existing server
try {
const existing = JSON.parse(fs.readFileSync(PID_FILE, 'utf-8'));
if (existing && existing.pid) {
try {
process.kill(existing.pid, 0); // throws if dead
return existing;
} catch { /* stale PID file — the server script will clean it up */ }
}
} catch { /* no PID file */ }
// Start a new server
const out = runScript('live-server.mjs', ['--background']);
return safeParse(out);
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
const _running = process.argv[1];
if (_running?.endsWith('live.mjs') || _running?.endsWith('live.mjs/')) {
liveCli();
}