diff --git a/.agents/skills/impeccable/reference/live.md b/.agents/skills/impeccable/reference/live.md index f3b785fc7..3f4ad029f 100644 --- a/.agents/skills/impeccable/reference/live.md +++ b/.agents/skills/impeccable/reference/live.md @@ -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 | `` | `html` | | Static site with a non-root HTML file | e.g. `public/index.html` | `` | `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 diff --git a/.agents/skills/impeccable/scripts/live.mjs b/.agents/skills/impeccable/scripts/live.mjs new file mode 100644 index 000000000..93f05456e --- /dev/null +++ b/.agents/skills/impeccable/scripts/live.mjs @@ -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(); +} diff --git a/.claude/skills/impeccable/reference/live.md b/.claude/skills/impeccable/reference/live.md index f3b785fc7..3f4ad029f 100644 --- a/.claude/skills/impeccable/reference/live.md +++ b/.claude/skills/impeccable/reference/live.md @@ -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 | `` | `html` | | Static site with a non-root HTML file | e.g. `public/index.html` | `` | `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 diff --git a/.claude/skills/impeccable/scripts/live.mjs b/.claude/skills/impeccable/scripts/live.mjs new file mode 100644 index 000000000..93f05456e --- /dev/null +++ b/.claude/skills/impeccable/scripts/live.mjs @@ -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(); +} diff --git a/.cursor/skills/impeccable/reference/live.md b/.cursor/skills/impeccable/reference/live.md index f3b785fc7..3f4ad029f 100644 --- a/.cursor/skills/impeccable/reference/live.md +++ b/.cursor/skills/impeccable/reference/live.md @@ -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 | `` | `html` | | Static site with a non-root HTML file | e.g. `public/index.html` | `` | `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 diff --git a/.cursor/skills/impeccable/scripts/live.mjs b/.cursor/skills/impeccable/scripts/live.mjs new file mode 100644 index 000000000..93f05456e --- /dev/null +++ b/.cursor/skills/impeccable/scripts/live.mjs @@ -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(); +} diff --git a/.gemini/skills/impeccable/reference/live.md b/.gemini/skills/impeccable/reference/live.md index f3b785fc7..3f4ad029f 100644 --- a/.gemini/skills/impeccable/reference/live.md +++ b/.gemini/skills/impeccable/reference/live.md @@ -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 | `` | `html` | | Static site with a non-root HTML file | e.g. `public/index.html` | `` | `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 diff --git a/.gemini/skills/impeccable/scripts/live.mjs b/.gemini/skills/impeccable/scripts/live.mjs new file mode 100644 index 000000000..93f05456e --- /dev/null +++ b/.gemini/skills/impeccable/scripts/live.mjs @@ -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(); +} diff --git a/.github/skills/impeccable/reference/live.md b/.github/skills/impeccable/reference/live.md index f3b785fc7..3f4ad029f 100644 --- a/.github/skills/impeccable/reference/live.md +++ b/.github/skills/impeccable/reference/live.md @@ -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 | `` | `html` | | Static site with a non-root HTML file | e.g. `public/index.html` | `` | `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 diff --git a/.github/skills/impeccable/scripts/live.mjs b/.github/skills/impeccable/scripts/live.mjs new file mode 100644 index 000000000..93f05456e --- /dev/null +++ b/.github/skills/impeccable/scripts/live.mjs @@ -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(); +} diff --git a/.kiro/skills/impeccable/reference/live.md b/.kiro/skills/impeccable/reference/live.md index f3b785fc7..3f4ad029f 100644 --- a/.kiro/skills/impeccable/reference/live.md +++ b/.kiro/skills/impeccable/reference/live.md @@ -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 | `` | `html` | | Static site with a non-root HTML file | e.g. `public/index.html` | `` | `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 diff --git a/.kiro/skills/impeccable/scripts/live.mjs b/.kiro/skills/impeccable/scripts/live.mjs new file mode 100644 index 000000000..93f05456e --- /dev/null +++ b/.kiro/skills/impeccable/scripts/live.mjs @@ -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(); +} diff --git a/.opencode/skills/impeccable/reference/live.md b/.opencode/skills/impeccable/reference/live.md index f3b785fc7..3f4ad029f 100644 --- a/.opencode/skills/impeccable/reference/live.md +++ b/.opencode/skills/impeccable/reference/live.md @@ -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 | `` | `html` | | Static site with a non-root HTML file | e.g. `public/index.html` | `` | `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 diff --git a/.opencode/skills/impeccable/scripts/live.mjs b/.opencode/skills/impeccable/scripts/live.mjs new file mode 100644 index 000000000..93f05456e --- /dev/null +++ b/.opencode/skills/impeccable/scripts/live.mjs @@ -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(); +} diff --git a/.pi/skills/impeccable/reference/live.md b/.pi/skills/impeccable/reference/live.md index f3b785fc7..3f4ad029f 100644 --- a/.pi/skills/impeccable/reference/live.md +++ b/.pi/skills/impeccable/reference/live.md @@ -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 | `` | `html` | | Static site with a non-root HTML file | e.g. `public/index.html` | `` | `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 diff --git a/.pi/skills/impeccable/scripts/live.mjs b/.pi/skills/impeccable/scripts/live.mjs new file mode 100644 index 000000000..93f05456e --- /dev/null +++ b/.pi/skills/impeccable/scripts/live.mjs @@ -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(); +} diff --git a/.rovodev/skills/impeccable/reference/live.md b/.rovodev/skills/impeccable/reference/live.md index f3b785fc7..3f4ad029f 100644 --- a/.rovodev/skills/impeccable/reference/live.md +++ b/.rovodev/skills/impeccable/reference/live.md @@ -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 | `` | `html` | | Static site with a non-root HTML file | e.g. `public/index.html` | `` | `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 diff --git a/.rovodev/skills/impeccable/scripts/live.mjs b/.rovodev/skills/impeccable/scripts/live.mjs new file mode 100644 index 000000000..93f05456e --- /dev/null +++ b/.rovodev/skills/impeccable/scripts/live.mjs @@ -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(); +} diff --git a/.trae-cn/skills/impeccable/reference/live.md b/.trae-cn/skills/impeccable/reference/live.md index f3b785fc7..3f4ad029f 100644 --- a/.trae-cn/skills/impeccable/reference/live.md +++ b/.trae-cn/skills/impeccable/reference/live.md @@ -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 | `` | `html` | | Static site with a non-root HTML file | e.g. `public/index.html` | `` | `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 diff --git a/.trae-cn/skills/impeccable/scripts/live.mjs b/.trae-cn/skills/impeccable/scripts/live.mjs new file mode 100644 index 000000000..93f05456e --- /dev/null +++ b/.trae-cn/skills/impeccable/scripts/live.mjs @@ -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(); +} diff --git a/.trae/skills/impeccable/reference/live.md b/.trae/skills/impeccable/reference/live.md index f3b785fc7..3f4ad029f 100644 --- a/.trae/skills/impeccable/reference/live.md +++ b/.trae/skills/impeccable/reference/live.md @@ -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 | `` | `html` | | Static site with a non-root HTML file | e.g. `public/index.html` | `` | `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 diff --git a/.trae/skills/impeccable/scripts/live.mjs b/.trae/skills/impeccable/scripts/live.mjs new file mode 100644 index 000000000..93f05456e --- /dev/null +++ b/.trae/skills/impeccable/scripts/live.mjs @@ -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(); +} diff --git a/source/skills/impeccable/reference/live.md b/source/skills/impeccable/reference/live.md index f3b785fc7..3f4ad029f 100644 --- a/source/skills/impeccable/reference/live.md +++ b/source/skills/impeccable/reference/live.md @@ -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 | `` | `html` | | Static site with a non-root HTML file | e.g. `public/index.html` | `` | `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 diff --git a/source/skills/impeccable/scripts/live.mjs b/source/skills/impeccable/scripts/live.mjs new file mode 100644 index 000000000..93f05456e --- /dev/null +++ b/source/skills/impeccable/scripts/live.mjs @@ -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(); +}