Add live-inject.mjs: per-project config for instant script tag management

First live run: agent auto-detects framework and writes a small config.json
(file, insertBefore/insertAfter anchor, comment syntax). Every subsequent
run: live-inject.mjs handles insert/remove deterministically, no LLM needed.

The config lives at {scripts_path}/config.json and is gitignored — it's a
per-project cache that wipes on skill update and regenerates on next use.

- New live-inject.mjs: --port (insert), --remove, --check modes
- Idempotent insert: re-running with a different port replaces cleanly
- Reference doc: one-time detection step, then instant insert/remove

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-13 16:44:26 -07:00
co-authored by Claude Opus 4.6
parent f397b9f123
commit 8030bc226a
26 changed files with 2584 additions and 277 deletions
+41 -23
View File
@@ -15,34 +15,48 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Inject the Browser Script
Find the project's main HTML entry point. This varies by framework:
The `live-inject.mjs` script handles insertion deterministically. It reads `config.json` from its own directory (one-time per-project setup).
| Framework | Typical file |
|-----------|-------------|
| Plain HTML | `index.html` |
| Vite / React | `index.html` (project root) |
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
| Next.js (Pages) | `pages/_document.tsx` |
| Nuxt | `app.vue` or `nuxt.config.ts` |
| Svelte / SvelteKit | `src/app.html` |
### Step 1: Ensure config.json exists
Add the script tag between comment markers (replace PORT with the actual port):
**HTML / Vue / Svelte:**
```html
<!-- impeccable-live-start -->
<script src="http://localhost:PORT/live.js"></script>
<!-- impeccable-live-end -->
```bash
node {{scripts_path}}/live-inject.mjs --check
```
**JSX / TSX (React, Next.js):**
```jsx
{/* impeccable-live-start */}
<script src="http://localhost:PORT/live.js"></script>
{/* impeccable-live-end */}
If the output says `{"ok": true, ...}`, skip to Step 2.
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:
| Framework | `file` | `insertBefore` | `commentSyntax` |
|-----------|--------|----------------|-----------------|
| Plain HTML | `index.html` | `</body>` | `html` |
| Vite / React | `index.html` | `</body>` | `html` |
| Next.js (App Router) | `app/layout.tsx` | `</body>` | `jsx` |
| Next.js (Pages) | `pages/_document.tsx` | `</body>` | `jsx` |
| Nuxt | `app.vue` | `</body>` | `html` |
| Svelte / SvelteKit | `src/app.html` | `</body>` | `html` |
| Astro | the root layout `.astro` file | `</body>` | `html` |
| Static site with a non-root HTML file | e.g. `public/index.html` | `</body>` | `html` |
Write the config to the path reported by `--check`. Example for this project:
```json
{
"file": "public/index.html",
"insertBefore": "</body>",
"commentSyntax": "html"
}
```
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
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.
@@ -177,7 +191,11 @@ If the poll is still running as a background task, kill it and proceed directly
When the loop ends:
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
1. **Remove the injected script tag**:
```bash
node {{scripts_path}}/live-inject.mjs --remove
```
(The config.json stays so future `live-inject.mjs --port PORT` calls are instant.)
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
3. **Remove any leftover carbonize blocks** (search for `impeccable-carbonize-start` markers and clean up).
4. **Stop the server**:
@@ -0,0 +1,174 @@
/**
* CLI helper: insert/remove the live variant mode script tag in the project's
* main HTML entry point.
*
* On first live run, the agent generates `config.json` in this script's
* directory with the project's insertion target (framework-specific). On
* every subsequent run, this script handles insert/remove deterministically
* with zero LLM involvement.
*
* Usage:
* node live-inject.mjs --port PORT # Insert the live script tag
* node live-inject.mjs --remove # Remove the live script tag
* node live-inject.mjs --check # Check whether config.json exists
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = path.join(__dirname, 'config.json');
const MARKER_OPEN_TEXT = 'impeccable-live-start';
const MARKER_CLOSE_TEXT = 'impeccable-live-end';
export async function injectCli() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`Usage: node live-inject.mjs [options]
Insert or remove the live mode script tag in the project's HTML entry point.
Reads configuration from config.json (in this same directory).
Modes:
--port PORT Insert script tag pointing at http://localhost:PORT/live.js
--remove Remove the script tag (if present)
--check Print whether config.json exists and its content
Output (JSON):
{ ok, file, inserted|removed, config? }`);
process.exit(0);
}
if (args.includes('--check')) {
if (!fs.existsSync(CONFIG_PATH)) {
console.log(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(0);
}
try {
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
console.log(JSON.stringify({ ok: true, config: cfg, path: CONFIG_PATH }));
} catch (err) {
console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message }));
}
return;
}
// Load config
if (!fs.existsSync(CONFIG_PATH)) {
console.error(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
validateConfig(config);
const absFile = path.resolve(process.cwd(), config.file);
if (!fs.existsSync(absFile)) {
console.error(JSON.stringify({ ok: false, error: 'file_not_found', file: config.file }));
process.exit(1);
}
const content = fs.readFileSync(absFile, 'utf-8');
if (args.includes('--remove')) {
const updated = removeTag(content, config.commentSyntax);
if (updated === content) {
console.log(JSON.stringify({ ok: true, file: config.file, removed: false, note: 'no tag present' }));
return;
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, removed: true }));
return;
}
// Insert mode — need --port
const portIdx = args.indexOf('--port');
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : NaN;
if (!Number.isFinite(port)) {
console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
process.exit(1);
}
// Already inserted? Replace to refresh the port.
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) {
console.error(JSON.stringify({ ok: false, error: 'insertion_point_not_found', anchor: config.insertBefore }));
process.exit(1);
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, inserted: true, port }));
}
// ---------------------------------------------------------------------------
// Core operations
// ---------------------------------------------------------------------------
function validateConfig(cfg) {
if (!cfg || typeof cfg !== 'object') throw new Error('config.json must be an object');
if (typeof cfg.file !== 'string') throw new Error('config.file (string) required');
if (typeof cfg.insertBefore !== 'string' && typeof cfg.insertAfter !== 'string') {
throw new Error('config.insertBefore or config.insertAfter (string) required');
}
if (cfg.commentSyntax !== 'html' && cfg.commentSyntax !== 'jsx') {
throw new Error("config.commentSyntax must be 'html' or 'jsx'");
}
}
function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : '<!--'; }
function commentClose(syntax) { return syntax === 'jsx' ? '*/}' : '-->'; }
function buildTagBlock(syntax, port) {
const open = commentOpen(syntax);
const close = commentClose(syntax);
return (
open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
'<script src="http://localhost:' + port + '/live.js"></script>\n' +
open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
);
}
function insertTag(content, config, port) {
const block = buildTagBlock(config.commentSyntax, port);
if (config.insertBefore) {
const idx = content.indexOf(config.insertBefore);
if (idx === -1) return content;
return content.slice(0, idx) + block + content.slice(idx);
}
// insertAfter
const idx = content.indexOf(config.insertAfter);
if (idx === -1) return content;
const after = idx + config.insertAfter.length;
// Preserve a single trailing newline if the anchor didn't end with one
const prefix = content[after] === '\n' ? content.slice(0, after + 1) : content.slice(0, after) + '\n';
return prefix + block + content.slice(prefix.length);
}
/**
* Remove the live script block. Matches either HTML or JSX comment markers
* regardless of config (so stale tags from a wrong config can still be cleaned).
*/
function removeTag(content, _syntax) {
// Two patterns: HTML comment markers or JSX comment markers, with any content between.
const patterns = [
/\n?<!--\s*impeccable-live-start\s*-->[\s\S]*?<!--\s*impeccable-live-end\s*-->\n?/,
/\n?\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}\n?/,
];
for (const pat of patterns) {
const next = content.replace(pat, '\n');
if (next !== content) return next;
}
return content;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
const _running = process.argv[1];
if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs/')) {
injectCli();
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
+41 -23
View File
@@ -15,34 +15,48 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Inject the Browser Script
Find the project's main HTML entry point. This varies by framework:
The `live-inject.mjs` script handles insertion deterministically. It reads `config.json` from its own directory (one-time per-project setup).
| Framework | Typical file |
|-----------|-------------|
| Plain HTML | `index.html` |
| Vite / React | `index.html` (project root) |
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
| Next.js (Pages) | `pages/_document.tsx` |
| Nuxt | `app.vue` or `nuxt.config.ts` |
| Svelte / SvelteKit | `src/app.html` |
### Step 1: Ensure config.json exists
Add the script tag between comment markers (replace PORT with the actual port):
**HTML / Vue / Svelte:**
```html
<!-- impeccable-live-start -->
<script src="http://localhost:PORT/live.js"></script>
<!-- impeccable-live-end -->
```bash
node {{scripts_path}}/live-inject.mjs --check
```
**JSX / TSX (React, Next.js):**
```jsx
{/* impeccable-live-start */}
<script src="http://localhost:PORT/live.js"></script>
{/* impeccable-live-end */}
If the output says `{"ok": true, ...}`, skip to Step 2.
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:
| Framework | `file` | `insertBefore` | `commentSyntax` |
|-----------|--------|----------------|-----------------|
| Plain HTML | `index.html` | `</body>` | `html` |
| Vite / React | `index.html` | `</body>` | `html` |
| Next.js (App Router) | `app/layout.tsx` | `</body>` | `jsx` |
| Next.js (Pages) | `pages/_document.tsx` | `</body>` | `jsx` |
| Nuxt | `app.vue` | `</body>` | `html` |
| Svelte / SvelteKit | `src/app.html` | `</body>` | `html` |
| Astro | the root layout `.astro` file | `</body>` | `html` |
| Static site with a non-root HTML file | e.g. `public/index.html` | `</body>` | `html` |
Write the config to the path reported by `--check`. Example for this project:
```json
{
"file": "public/index.html",
"insertBefore": "</body>",
"commentSyntax": "html"
}
```
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
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.
@@ -177,7 +191,11 @@ If the poll is still running as a background task, kill it and proceed directly
When the loop ends:
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
1. **Remove the injected script tag**:
```bash
node {{scripts_path}}/live-inject.mjs --remove
```
(The config.json stays so future `live-inject.mjs --port PORT` calls are instant.)
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
3. **Remove any leftover carbonize blocks** (search for `impeccable-carbonize-start` markers and clean up).
4. **Stop the server**:
@@ -0,0 +1,174 @@
/**
* CLI helper: insert/remove the live variant mode script tag in the project's
* main HTML entry point.
*
* On first live run, the agent generates `config.json` in this script's
* directory with the project's insertion target (framework-specific). On
* every subsequent run, this script handles insert/remove deterministically
* with zero LLM involvement.
*
* Usage:
* node live-inject.mjs --port PORT # Insert the live script tag
* node live-inject.mjs --remove # Remove the live script tag
* node live-inject.mjs --check # Check whether config.json exists
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = path.join(__dirname, 'config.json');
const MARKER_OPEN_TEXT = 'impeccable-live-start';
const MARKER_CLOSE_TEXT = 'impeccable-live-end';
export async function injectCli() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`Usage: node live-inject.mjs [options]
Insert or remove the live mode script tag in the project's HTML entry point.
Reads configuration from config.json (in this same directory).
Modes:
--port PORT Insert script tag pointing at http://localhost:PORT/live.js
--remove Remove the script tag (if present)
--check Print whether config.json exists and its content
Output (JSON):
{ ok, file, inserted|removed, config? }`);
process.exit(0);
}
if (args.includes('--check')) {
if (!fs.existsSync(CONFIG_PATH)) {
console.log(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(0);
}
try {
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
console.log(JSON.stringify({ ok: true, config: cfg, path: CONFIG_PATH }));
} catch (err) {
console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message }));
}
return;
}
// Load config
if (!fs.existsSync(CONFIG_PATH)) {
console.error(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
validateConfig(config);
const absFile = path.resolve(process.cwd(), config.file);
if (!fs.existsSync(absFile)) {
console.error(JSON.stringify({ ok: false, error: 'file_not_found', file: config.file }));
process.exit(1);
}
const content = fs.readFileSync(absFile, 'utf-8');
if (args.includes('--remove')) {
const updated = removeTag(content, config.commentSyntax);
if (updated === content) {
console.log(JSON.stringify({ ok: true, file: config.file, removed: false, note: 'no tag present' }));
return;
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, removed: true }));
return;
}
// Insert mode — need --port
const portIdx = args.indexOf('--port');
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : NaN;
if (!Number.isFinite(port)) {
console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
process.exit(1);
}
// Already inserted? Replace to refresh the port.
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) {
console.error(JSON.stringify({ ok: false, error: 'insertion_point_not_found', anchor: config.insertBefore }));
process.exit(1);
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, inserted: true, port }));
}
// ---------------------------------------------------------------------------
// Core operations
// ---------------------------------------------------------------------------
function validateConfig(cfg) {
if (!cfg || typeof cfg !== 'object') throw new Error('config.json must be an object');
if (typeof cfg.file !== 'string') throw new Error('config.file (string) required');
if (typeof cfg.insertBefore !== 'string' && typeof cfg.insertAfter !== 'string') {
throw new Error('config.insertBefore or config.insertAfter (string) required');
}
if (cfg.commentSyntax !== 'html' && cfg.commentSyntax !== 'jsx') {
throw new Error("config.commentSyntax must be 'html' or 'jsx'");
}
}
function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : '<!--'; }
function commentClose(syntax) { return syntax === 'jsx' ? '*/}' : '-->'; }
function buildTagBlock(syntax, port) {
const open = commentOpen(syntax);
const close = commentClose(syntax);
return (
open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
'<script src="http://localhost:' + port + '/live.js"></script>\n' +
open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
);
}
function insertTag(content, config, port) {
const block = buildTagBlock(config.commentSyntax, port);
if (config.insertBefore) {
const idx = content.indexOf(config.insertBefore);
if (idx === -1) return content;
return content.slice(0, idx) + block + content.slice(idx);
}
// insertAfter
const idx = content.indexOf(config.insertAfter);
if (idx === -1) return content;
const after = idx + config.insertAfter.length;
// Preserve a single trailing newline if the anchor didn't end with one
const prefix = content[after] === '\n' ? content.slice(0, after + 1) : content.slice(0, after) + '\n';
return prefix + block + content.slice(prefix.length);
}
/**
* Remove the live script block. Matches either HTML or JSX comment markers
* regardless of config (so stale tags from a wrong config can still be cleaned).
*/
function removeTag(content, _syntax) {
// Two patterns: HTML comment markers or JSX comment markers, with any content between.
const patterns = [
/\n?<!--\s*impeccable-live-start\s*-->[\s\S]*?<!--\s*impeccable-live-end\s*-->\n?/,
/\n?\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}\n?/,
];
for (const pat of patterns) {
const next = content.replace(pat, '\n');
if (next !== content) return next;
}
return content;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
const _running = process.argv[1];
if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs/')) {
injectCli();
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
+41 -23
View File
@@ -15,34 +15,48 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Inject the Browser Script
Find the project's main HTML entry point. This varies by framework:
The `live-inject.mjs` script handles insertion deterministically. It reads `config.json` from its own directory (one-time per-project setup).
| Framework | Typical file |
|-----------|-------------|
| Plain HTML | `index.html` |
| Vite / React | `index.html` (project root) |
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
| Next.js (Pages) | `pages/_document.tsx` |
| Nuxt | `app.vue` or `nuxt.config.ts` |
| Svelte / SvelteKit | `src/app.html` |
### Step 1: Ensure config.json exists
Add the script tag between comment markers (replace PORT with the actual port):
**HTML / Vue / Svelte:**
```html
<!-- impeccable-live-start -->
<script src="http://localhost:PORT/live.js"></script>
<!-- impeccable-live-end -->
```bash
node {{scripts_path}}/live-inject.mjs --check
```
**JSX / TSX (React, Next.js):**
```jsx
{/* impeccable-live-start */}
<script src="http://localhost:PORT/live.js"></script>
{/* impeccable-live-end */}
If the output says `{"ok": true, ...}`, skip to Step 2.
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:
| Framework | `file` | `insertBefore` | `commentSyntax` |
|-----------|--------|----------------|-----------------|
| Plain HTML | `index.html` | `</body>` | `html` |
| Vite / React | `index.html` | `</body>` | `html` |
| Next.js (App Router) | `app/layout.tsx` | `</body>` | `jsx` |
| Next.js (Pages) | `pages/_document.tsx` | `</body>` | `jsx` |
| Nuxt | `app.vue` | `</body>` | `html` |
| Svelte / SvelteKit | `src/app.html` | `</body>` | `html` |
| Astro | the root layout `.astro` file | `</body>` | `html` |
| Static site with a non-root HTML file | e.g. `public/index.html` | `</body>` | `html` |
Write the config to the path reported by `--check`. Example for this project:
```json
{
"file": "public/index.html",
"insertBefore": "</body>",
"commentSyntax": "html"
}
```
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
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.
@@ -177,7 +191,11 @@ If the poll is still running as a background task, kill it and proceed directly
When the loop ends:
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
1. **Remove the injected script tag**:
```bash
node {{scripts_path}}/live-inject.mjs --remove
```
(The config.json stays so future `live-inject.mjs --port PORT` calls are instant.)
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
3. **Remove any leftover carbonize blocks** (search for `impeccable-carbonize-start` markers and clean up).
4. **Stop the server**:
@@ -0,0 +1,174 @@
/**
* CLI helper: insert/remove the live variant mode script tag in the project's
* main HTML entry point.
*
* On first live run, the agent generates `config.json` in this script's
* directory with the project's insertion target (framework-specific). On
* every subsequent run, this script handles insert/remove deterministically
* with zero LLM involvement.
*
* Usage:
* node live-inject.mjs --port PORT # Insert the live script tag
* node live-inject.mjs --remove # Remove the live script tag
* node live-inject.mjs --check # Check whether config.json exists
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = path.join(__dirname, 'config.json');
const MARKER_OPEN_TEXT = 'impeccable-live-start';
const MARKER_CLOSE_TEXT = 'impeccable-live-end';
export async function injectCli() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`Usage: node live-inject.mjs [options]
Insert or remove the live mode script tag in the project's HTML entry point.
Reads configuration from config.json (in this same directory).
Modes:
--port PORT Insert script tag pointing at http://localhost:PORT/live.js
--remove Remove the script tag (if present)
--check Print whether config.json exists and its content
Output (JSON):
{ ok, file, inserted|removed, config? }`);
process.exit(0);
}
if (args.includes('--check')) {
if (!fs.existsSync(CONFIG_PATH)) {
console.log(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(0);
}
try {
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
console.log(JSON.stringify({ ok: true, config: cfg, path: CONFIG_PATH }));
} catch (err) {
console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message }));
}
return;
}
// Load config
if (!fs.existsSync(CONFIG_PATH)) {
console.error(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
validateConfig(config);
const absFile = path.resolve(process.cwd(), config.file);
if (!fs.existsSync(absFile)) {
console.error(JSON.stringify({ ok: false, error: 'file_not_found', file: config.file }));
process.exit(1);
}
const content = fs.readFileSync(absFile, 'utf-8');
if (args.includes('--remove')) {
const updated = removeTag(content, config.commentSyntax);
if (updated === content) {
console.log(JSON.stringify({ ok: true, file: config.file, removed: false, note: 'no tag present' }));
return;
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, removed: true }));
return;
}
// Insert mode — need --port
const portIdx = args.indexOf('--port');
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : NaN;
if (!Number.isFinite(port)) {
console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
process.exit(1);
}
// Already inserted? Replace to refresh the port.
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) {
console.error(JSON.stringify({ ok: false, error: 'insertion_point_not_found', anchor: config.insertBefore }));
process.exit(1);
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, inserted: true, port }));
}
// ---------------------------------------------------------------------------
// Core operations
// ---------------------------------------------------------------------------
function validateConfig(cfg) {
if (!cfg || typeof cfg !== 'object') throw new Error('config.json must be an object');
if (typeof cfg.file !== 'string') throw new Error('config.file (string) required');
if (typeof cfg.insertBefore !== 'string' && typeof cfg.insertAfter !== 'string') {
throw new Error('config.insertBefore or config.insertAfter (string) required');
}
if (cfg.commentSyntax !== 'html' && cfg.commentSyntax !== 'jsx') {
throw new Error("config.commentSyntax must be 'html' or 'jsx'");
}
}
function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : '<!--'; }
function commentClose(syntax) { return syntax === 'jsx' ? '*/}' : '-->'; }
function buildTagBlock(syntax, port) {
const open = commentOpen(syntax);
const close = commentClose(syntax);
return (
open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
'<script src="http://localhost:' + port + '/live.js"></script>\n' +
open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
);
}
function insertTag(content, config, port) {
const block = buildTagBlock(config.commentSyntax, port);
if (config.insertBefore) {
const idx = content.indexOf(config.insertBefore);
if (idx === -1) return content;
return content.slice(0, idx) + block + content.slice(idx);
}
// insertAfter
const idx = content.indexOf(config.insertAfter);
if (idx === -1) return content;
const after = idx + config.insertAfter.length;
// Preserve a single trailing newline if the anchor didn't end with one
const prefix = content[after] === '\n' ? content.slice(0, after + 1) : content.slice(0, after) + '\n';
return prefix + block + content.slice(prefix.length);
}
/**
* Remove the live script block. Matches either HTML or JSX comment markers
* regardless of config (so stale tags from a wrong config can still be cleaned).
*/
function removeTag(content, _syntax) {
// Two patterns: HTML comment markers or JSX comment markers, with any content between.
const patterns = [
/\n?<!--\s*impeccable-live-start\s*-->[\s\S]*?<!--\s*impeccable-live-end\s*-->\n?/,
/\n?\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}\n?/,
];
for (const pat of patterns) {
const next = content.replace(pat, '\n');
if (next !== content) return next;
}
return content;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
const _running = process.argv[1];
if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs/')) {
injectCli();
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
+41 -23
View File
@@ -15,34 +15,48 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Inject the Browser Script
Find the project's main HTML entry point. This varies by framework:
The `live-inject.mjs` script handles insertion deterministically. It reads `config.json` from its own directory (one-time per-project setup).
| Framework | Typical file |
|-----------|-------------|
| Plain HTML | `index.html` |
| Vite / React | `index.html` (project root) |
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
| Next.js (Pages) | `pages/_document.tsx` |
| Nuxt | `app.vue` or `nuxt.config.ts` |
| Svelte / SvelteKit | `src/app.html` |
### Step 1: Ensure config.json exists
Add the script tag between comment markers (replace PORT with the actual port):
**HTML / Vue / Svelte:**
```html
<!-- impeccable-live-start -->
<script src="http://localhost:PORT/live.js"></script>
<!-- impeccable-live-end -->
```bash
node {{scripts_path}}/live-inject.mjs --check
```
**JSX / TSX (React, Next.js):**
```jsx
{/* impeccable-live-start */}
<script src="http://localhost:PORT/live.js"></script>
{/* impeccable-live-end */}
If the output says `{"ok": true, ...}`, skip to Step 2.
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:
| Framework | `file` | `insertBefore` | `commentSyntax` |
|-----------|--------|----------------|-----------------|
| Plain HTML | `index.html` | `</body>` | `html` |
| Vite / React | `index.html` | `</body>` | `html` |
| Next.js (App Router) | `app/layout.tsx` | `</body>` | `jsx` |
| Next.js (Pages) | `pages/_document.tsx` | `</body>` | `jsx` |
| Nuxt | `app.vue` | `</body>` | `html` |
| Svelte / SvelteKit | `src/app.html` | `</body>` | `html` |
| Astro | the root layout `.astro` file | `</body>` | `html` |
| Static site with a non-root HTML file | e.g. `public/index.html` | `</body>` | `html` |
Write the config to the path reported by `--check`. Example for this project:
```json
{
"file": "public/index.html",
"insertBefore": "</body>",
"commentSyntax": "html"
}
```
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
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.
@@ -177,7 +191,11 @@ If the poll is still running as a background task, kill it and proceed directly
When the loop ends:
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
1. **Remove the injected script tag**:
```bash
node {{scripts_path}}/live-inject.mjs --remove
```
(The config.json stays so future `live-inject.mjs --port PORT` calls are instant.)
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
3. **Remove any leftover carbonize blocks** (search for `impeccable-carbonize-start` markers and clean up).
4. **Stop the server**:
@@ -0,0 +1,174 @@
/**
* CLI helper: insert/remove the live variant mode script tag in the project's
* main HTML entry point.
*
* On first live run, the agent generates `config.json` in this script's
* directory with the project's insertion target (framework-specific). On
* every subsequent run, this script handles insert/remove deterministically
* with zero LLM involvement.
*
* Usage:
* node live-inject.mjs --port PORT # Insert the live script tag
* node live-inject.mjs --remove # Remove the live script tag
* node live-inject.mjs --check # Check whether config.json exists
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = path.join(__dirname, 'config.json');
const MARKER_OPEN_TEXT = 'impeccable-live-start';
const MARKER_CLOSE_TEXT = 'impeccable-live-end';
export async function injectCli() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`Usage: node live-inject.mjs [options]
Insert or remove the live mode script tag in the project's HTML entry point.
Reads configuration from config.json (in this same directory).
Modes:
--port PORT Insert script tag pointing at http://localhost:PORT/live.js
--remove Remove the script tag (if present)
--check Print whether config.json exists and its content
Output (JSON):
{ ok, file, inserted|removed, config? }`);
process.exit(0);
}
if (args.includes('--check')) {
if (!fs.existsSync(CONFIG_PATH)) {
console.log(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(0);
}
try {
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
console.log(JSON.stringify({ ok: true, config: cfg, path: CONFIG_PATH }));
} catch (err) {
console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message }));
}
return;
}
// Load config
if (!fs.existsSync(CONFIG_PATH)) {
console.error(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
validateConfig(config);
const absFile = path.resolve(process.cwd(), config.file);
if (!fs.existsSync(absFile)) {
console.error(JSON.stringify({ ok: false, error: 'file_not_found', file: config.file }));
process.exit(1);
}
const content = fs.readFileSync(absFile, 'utf-8');
if (args.includes('--remove')) {
const updated = removeTag(content, config.commentSyntax);
if (updated === content) {
console.log(JSON.stringify({ ok: true, file: config.file, removed: false, note: 'no tag present' }));
return;
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, removed: true }));
return;
}
// Insert mode — need --port
const portIdx = args.indexOf('--port');
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : NaN;
if (!Number.isFinite(port)) {
console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
process.exit(1);
}
// Already inserted? Replace to refresh the port.
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) {
console.error(JSON.stringify({ ok: false, error: 'insertion_point_not_found', anchor: config.insertBefore }));
process.exit(1);
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, inserted: true, port }));
}
// ---------------------------------------------------------------------------
// Core operations
// ---------------------------------------------------------------------------
function validateConfig(cfg) {
if (!cfg || typeof cfg !== 'object') throw new Error('config.json must be an object');
if (typeof cfg.file !== 'string') throw new Error('config.file (string) required');
if (typeof cfg.insertBefore !== 'string' && typeof cfg.insertAfter !== 'string') {
throw new Error('config.insertBefore or config.insertAfter (string) required');
}
if (cfg.commentSyntax !== 'html' && cfg.commentSyntax !== 'jsx') {
throw new Error("config.commentSyntax must be 'html' or 'jsx'");
}
}
function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : '<!--'; }
function commentClose(syntax) { return syntax === 'jsx' ? '*/}' : '-->'; }
function buildTagBlock(syntax, port) {
const open = commentOpen(syntax);
const close = commentClose(syntax);
return (
open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
'<script src="http://localhost:' + port + '/live.js"></script>\n' +
open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
);
}
function insertTag(content, config, port) {
const block = buildTagBlock(config.commentSyntax, port);
if (config.insertBefore) {
const idx = content.indexOf(config.insertBefore);
if (idx === -1) return content;
return content.slice(0, idx) + block + content.slice(idx);
}
// insertAfter
const idx = content.indexOf(config.insertAfter);
if (idx === -1) return content;
const after = idx + config.insertAfter.length;
// Preserve a single trailing newline if the anchor didn't end with one
const prefix = content[after] === '\n' ? content.slice(0, after + 1) : content.slice(0, after) + '\n';
return prefix + block + content.slice(prefix.length);
}
/**
* Remove the live script block. Matches either HTML or JSX comment markers
* regardless of config (so stale tags from a wrong config can still be cleaned).
*/
function removeTag(content, _syntax) {
// Two patterns: HTML comment markers or JSX comment markers, with any content between.
const patterns = [
/\n?<!--\s*impeccable-live-start\s*-->[\s\S]*?<!--\s*impeccable-live-end\s*-->\n?/,
/\n?\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}\n?/,
];
for (const pat of patterns) {
const next = content.replace(pat, '\n');
if (next !== content) return next;
}
return content;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
const _running = process.argv[1];
if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs/')) {
injectCli();
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
+41 -23
View File
@@ -15,34 +15,48 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Inject the Browser Script
Find the project's main HTML entry point. This varies by framework:
The `live-inject.mjs` script handles insertion deterministically. It reads `config.json` from its own directory (one-time per-project setup).
| Framework | Typical file |
|-----------|-------------|
| Plain HTML | `index.html` |
| Vite / React | `index.html` (project root) |
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
| Next.js (Pages) | `pages/_document.tsx` |
| Nuxt | `app.vue` or `nuxt.config.ts` |
| Svelte / SvelteKit | `src/app.html` |
### Step 1: Ensure config.json exists
Add the script tag between comment markers (replace PORT with the actual port):
**HTML / Vue / Svelte:**
```html
<!-- impeccable-live-start -->
<script src="http://localhost:PORT/live.js"></script>
<!-- impeccable-live-end -->
```bash
node {{scripts_path}}/live-inject.mjs --check
```
**JSX / TSX (React, Next.js):**
```jsx
{/* impeccable-live-start */}
<script src="http://localhost:PORT/live.js"></script>
{/* impeccable-live-end */}
If the output says `{"ok": true, ...}`, skip to Step 2.
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:
| Framework | `file` | `insertBefore` | `commentSyntax` |
|-----------|--------|----------------|-----------------|
| Plain HTML | `index.html` | `</body>` | `html` |
| Vite / React | `index.html` | `</body>` | `html` |
| Next.js (App Router) | `app/layout.tsx` | `</body>` | `jsx` |
| Next.js (Pages) | `pages/_document.tsx` | `</body>` | `jsx` |
| Nuxt | `app.vue` | `</body>` | `html` |
| Svelte / SvelteKit | `src/app.html` | `</body>` | `html` |
| Astro | the root layout `.astro` file | `</body>` | `html` |
| Static site with a non-root HTML file | e.g. `public/index.html` | `</body>` | `html` |
Write the config to the path reported by `--check`. Example for this project:
```json
{
"file": "public/index.html",
"insertBefore": "</body>",
"commentSyntax": "html"
}
```
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
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.
@@ -177,7 +191,11 @@ If the poll is still running as a background task, kill it and proceed directly
When the loop ends:
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
1. **Remove the injected script tag**:
```bash
node {{scripts_path}}/live-inject.mjs --remove
```
(The config.json stays so future `live-inject.mjs --port PORT` calls are instant.)
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
3. **Remove any leftover carbonize blocks** (search for `impeccable-carbonize-start` markers and clean up).
4. **Stop the server**:
@@ -0,0 +1,174 @@
/**
* CLI helper: insert/remove the live variant mode script tag in the project's
* main HTML entry point.
*
* On first live run, the agent generates `config.json` in this script's
* directory with the project's insertion target (framework-specific). On
* every subsequent run, this script handles insert/remove deterministically
* with zero LLM involvement.
*
* Usage:
* node live-inject.mjs --port PORT # Insert the live script tag
* node live-inject.mjs --remove # Remove the live script tag
* node live-inject.mjs --check # Check whether config.json exists
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = path.join(__dirname, 'config.json');
const MARKER_OPEN_TEXT = 'impeccable-live-start';
const MARKER_CLOSE_TEXT = 'impeccable-live-end';
export async function injectCli() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`Usage: node live-inject.mjs [options]
Insert or remove the live mode script tag in the project's HTML entry point.
Reads configuration from config.json (in this same directory).
Modes:
--port PORT Insert script tag pointing at http://localhost:PORT/live.js
--remove Remove the script tag (if present)
--check Print whether config.json exists and its content
Output (JSON):
{ ok, file, inserted|removed, config? }`);
process.exit(0);
}
if (args.includes('--check')) {
if (!fs.existsSync(CONFIG_PATH)) {
console.log(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(0);
}
try {
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
console.log(JSON.stringify({ ok: true, config: cfg, path: CONFIG_PATH }));
} catch (err) {
console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message }));
}
return;
}
// Load config
if (!fs.existsSync(CONFIG_PATH)) {
console.error(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
validateConfig(config);
const absFile = path.resolve(process.cwd(), config.file);
if (!fs.existsSync(absFile)) {
console.error(JSON.stringify({ ok: false, error: 'file_not_found', file: config.file }));
process.exit(1);
}
const content = fs.readFileSync(absFile, 'utf-8');
if (args.includes('--remove')) {
const updated = removeTag(content, config.commentSyntax);
if (updated === content) {
console.log(JSON.stringify({ ok: true, file: config.file, removed: false, note: 'no tag present' }));
return;
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, removed: true }));
return;
}
// Insert mode — need --port
const portIdx = args.indexOf('--port');
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : NaN;
if (!Number.isFinite(port)) {
console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
process.exit(1);
}
// Already inserted? Replace to refresh the port.
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) {
console.error(JSON.stringify({ ok: false, error: 'insertion_point_not_found', anchor: config.insertBefore }));
process.exit(1);
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, inserted: true, port }));
}
// ---------------------------------------------------------------------------
// Core operations
// ---------------------------------------------------------------------------
function validateConfig(cfg) {
if (!cfg || typeof cfg !== 'object') throw new Error('config.json must be an object');
if (typeof cfg.file !== 'string') throw new Error('config.file (string) required');
if (typeof cfg.insertBefore !== 'string' && typeof cfg.insertAfter !== 'string') {
throw new Error('config.insertBefore or config.insertAfter (string) required');
}
if (cfg.commentSyntax !== 'html' && cfg.commentSyntax !== 'jsx') {
throw new Error("config.commentSyntax must be 'html' or 'jsx'");
}
}
function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : '<!--'; }
function commentClose(syntax) { return syntax === 'jsx' ? '*/}' : '-->'; }
function buildTagBlock(syntax, port) {
const open = commentOpen(syntax);
const close = commentClose(syntax);
return (
open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
'<script src="http://localhost:' + port + '/live.js"></script>\n' +
open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
);
}
function insertTag(content, config, port) {
const block = buildTagBlock(config.commentSyntax, port);
if (config.insertBefore) {
const idx = content.indexOf(config.insertBefore);
if (idx === -1) return content;
return content.slice(0, idx) + block + content.slice(idx);
}
// insertAfter
const idx = content.indexOf(config.insertAfter);
if (idx === -1) return content;
const after = idx + config.insertAfter.length;
// Preserve a single trailing newline if the anchor didn't end with one
const prefix = content[after] === '\n' ? content.slice(0, after + 1) : content.slice(0, after) + '\n';
return prefix + block + content.slice(prefix.length);
}
/**
* Remove the live script block. Matches either HTML or JSX comment markers
* regardless of config (so stale tags from a wrong config can still be cleaned).
*/
function removeTag(content, _syntax) {
// Two patterns: HTML comment markers or JSX comment markers, with any content between.
const patterns = [
/\n?<!--\s*impeccable-live-start\s*-->[\s\S]*?<!--\s*impeccable-live-end\s*-->\n?/,
/\n?\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}\n?/,
];
for (const pat of patterns) {
const next = content.replace(pat, '\n');
if (next !== content) return next;
}
return content;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
const _running = process.argv[1];
if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs/')) {
injectCli();
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
+4
View File
@@ -35,6 +35,10 @@ Thumbs.db
# Live mode session file (created by live-server.mjs, cleaned up on stop)
.impeccable-live.json
# Per-project live mode injection config (generated once per project by the
# skill; wiped on skill update, regenerated on next live run)
**/skills/impeccable/scripts/config.json
# Extension build artifacts
extension/detector/
+41 -23
View File
@@ -15,34 +15,48 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Inject the Browser Script
Find the project's main HTML entry point. This varies by framework:
The `live-inject.mjs` script handles insertion deterministically. It reads `config.json` from its own directory (one-time per-project setup).
| Framework | Typical file |
|-----------|-------------|
| Plain HTML | `index.html` |
| Vite / React | `index.html` (project root) |
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
| Next.js (Pages) | `pages/_document.tsx` |
| Nuxt | `app.vue` or `nuxt.config.ts` |
| Svelte / SvelteKit | `src/app.html` |
### Step 1: Ensure config.json exists
Add the script tag between comment markers (replace PORT with the actual port):
**HTML / Vue / Svelte:**
```html
<!-- impeccable-live-start -->
<script src="http://localhost:PORT/live.js"></script>
<!-- impeccable-live-end -->
```bash
node {{scripts_path}}/live-inject.mjs --check
```
**JSX / TSX (React, Next.js):**
```jsx
{/* impeccable-live-start */}
<script src="http://localhost:PORT/live.js"></script>
{/* impeccable-live-end */}
If the output says `{"ok": true, ...}`, skip to Step 2.
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:
| Framework | `file` | `insertBefore` | `commentSyntax` |
|-----------|--------|----------------|-----------------|
| Plain HTML | `index.html` | `</body>` | `html` |
| Vite / React | `index.html` | `</body>` | `html` |
| Next.js (App Router) | `app/layout.tsx` | `</body>` | `jsx` |
| Next.js (Pages) | `pages/_document.tsx` | `</body>` | `jsx` |
| Nuxt | `app.vue` | `</body>` | `html` |
| Svelte / SvelteKit | `src/app.html` | `</body>` | `html` |
| Astro | the root layout `.astro` file | `</body>` | `html` |
| Static site with a non-root HTML file | e.g. `public/index.html` | `</body>` | `html` |
Write the config to the path reported by `--check`. Example for this project:
```json
{
"file": "public/index.html",
"insertBefore": "</body>",
"commentSyntax": "html"
}
```
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
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.
@@ -177,7 +191,11 @@ If the poll is still running as a background task, kill it and proceed directly
When the loop ends:
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
1. **Remove the injected script tag**:
```bash
node {{scripts_path}}/live-inject.mjs --remove
```
(The config.json stays so future `live-inject.mjs --port PORT` calls are instant.)
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
3. **Remove any leftover carbonize blocks** (search for `impeccable-carbonize-start` markers and clean up).
4. **Stop the server**:
@@ -0,0 +1,174 @@
/**
* CLI helper: insert/remove the live variant mode script tag in the project's
* main HTML entry point.
*
* On first live run, the agent generates `config.json` in this script's
* directory with the project's insertion target (framework-specific). On
* every subsequent run, this script handles insert/remove deterministically
* with zero LLM involvement.
*
* Usage:
* node live-inject.mjs --port PORT # Insert the live script tag
* node live-inject.mjs --remove # Remove the live script tag
* node live-inject.mjs --check # Check whether config.json exists
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = path.join(__dirname, 'config.json');
const MARKER_OPEN_TEXT = 'impeccable-live-start';
const MARKER_CLOSE_TEXT = 'impeccable-live-end';
export async function injectCli() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`Usage: node live-inject.mjs [options]
Insert or remove the live mode script tag in the project's HTML entry point.
Reads configuration from config.json (in this same directory).
Modes:
--port PORT Insert script tag pointing at http://localhost:PORT/live.js
--remove Remove the script tag (if present)
--check Print whether config.json exists and its content
Output (JSON):
{ ok, file, inserted|removed, config? }`);
process.exit(0);
}
if (args.includes('--check')) {
if (!fs.existsSync(CONFIG_PATH)) {
console.log(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(0);
}
try {
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
console.log(JSON.stringify({ ok: true, config: cfg, path: CONFIG_PATH }));
} catch (err) {
console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message }));
}
return;
}
// Load config
if (!fs.existsSync(CONFIG_PATH)) {
console.error(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
validateConfig(config);
const absFile = path.resolve(process.cwd(), config.file);
if (!fs.existsSync(absFile)) {
console.error(JSON.stringify({ ok: false, error: 'file_not_found', file: config.file }));
process.exit(1);
}
const content = fs.readFileSync(absFile, 'utf-8');
if (args.includes('--remove')) {
const updated = removeTag(content, config.commentSyntax);
if (updated === content) {
console.log(JSON.stringify({ ok: true, file: config.file, removed: false, note: 'no tag present' }));
return;
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, removed: true }));
return;
}
// Insert mode — need --port
const portIdx = args.indexOf('--port');
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : NaN;
if (!Number.isFinite(port)) {
console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
process.exit(1);
}
// Already inserted? Replace to refresh the port.
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) {
console.error(JSON.stringify({ ok: false, error: 'insertion_point_not_found', anchor: config.insertBefore }));
process.exit(1);
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, inserted: true, port }));
}
// ---------------------------------------------------------------------------
// Core operations
// ---------------------------------------------------------------------------
function validateConfig(cfg) {
if (!cfg || typeof cfg !== 'object') throw new Error('config.json must be an object');
if (typeof cfg.file !== 'string') throw new Error('config.file (string) required');
if (typeof cfg.insertBefore !== 'string' && typeof cfg.insertAfter !== 'string') {
throw new Error('config.insertBefore or config.insertAfter (string) required');
}
if (cfg.commentSyntax !== 'html' && cfg.commentSyntax !== 'jsx') {
throw new Error("config.commentSyntax must be 'html' or 'jsx'");
}
}
function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : '<!--'; }
function commentClose(syntax) { return syntax === 'jsx' ? '*/}' : '-->'; }
function buildTagBlock(syntax, port) {
const open = commentOpen(syntax);
const close = commentClose(syntax);
return (
open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
'<script src="http://localhost:' + port + '/live.js"></script>\n' +
open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
);
}
function insertTag(content, config, port) {
const block = buildTagBlock(config.commentSyntax, port);
if (config.insertBefore) {
const idx = content.indexOf(config.insertBefore);
if (idx === -1) return content;
return content.slice(0, idx) + block + content.slice(idx);
}
// insertAfter
const idx = content.indexOf(config.insertAfter);
if (idx === -1) return content;
const after = idx + config.insertAfter.length;
// Preserve a single trailing newline if the anchor didn't end with one
const prefix = content[after] === '\n' ? content.slice(0, after + 1) : content.slice(0, after) + '\n';
return prefix + block + content.slice(prefix.length);
}
/**
* Remove the live script block. Matches either HTML or JSX comment markers
* regardless of config (so stale tags from a wrong config can still be cleaned).
*/
function removeTag(content, _syntax) {
// Two patterns: HTML comment markers or JSX comment markers, with any content between.
const patterns = [
/\n?<!--\s*impeccable-live-start\s*-->[\s\S]*?<!--\s*impeccable-live-end\s*-->\n?/,
/\n?\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}\n?/,
];
for (const pat of patterns) {
const next = content.replace(pat, '\n');
if (next !== content) return next;
}
return content;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
const _running = process.argv[1];
if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs/')) {
injectCli();
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
+41 -23
View File
@@ -15,34 +15,48 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Inject the Browser Script
Find the project's main HTML entry point. This varies by framework:
The `live-inject.mjs` script handles insertion deterministically. It reads `config.json` from its own directory (one-time per-project setup).
| Framework | Typical file |
|-----------|-------------|
| Plain HTML | `index.html` |
| Vite / React | `index.html` (project root) |
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
| Next.js (Pages) | `pages/_document.tsx` |
| Nuxt | `app.vue` or `nuxt.config.ts` |
| Svelte / SvelteKit | `src/app.html` |
### Step 1: Ensure config.json exists
Add the script tag between comment markers (replace PORT with the actual port):
**HTML / Vue / Svelte:**
```html
<!-- impeccable-live-start -->
<script src="http://localhost:PORT/live.js"></script>
<!-- impeccable-live-end -->
```bash
node {{scripts_path}}/live-inject.mjs --check
```
**JSX / TSX (React, Next.js):**
```jsx
{/* impeccable-live-start */}
<script src="http://localhost:PORT/live.js"></script>
{/* impeccable-live-end */}
If the output says `{"ok": true, ...}`, skip to Step 2.
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:
| Framework | `file` | `insertBefore` | `commentSyntax` |
|-----------|--------|----------------|-----------------|
| Plain HTML | `index.html` | `</body>` | `html` |
| Vite / React | `index.html` | `</body>` | `html` |
| Next.js (App Router) | `app/layout.tsx` | `</body>` | `jsx` |
| Next.js (Pages) | `pages/_document.tsx` | `</body>` | `jsx` |
| Nuxt | `app.vue` | `</body>` | `html` |
| Svelte / SvelteKit | `src/app.html` | `</body>` | `html` |
| Astro | the root layout `.astro` file | `</body>` | `html` |
| Static site with a non-root HTML file | e.g. `public/index.html` | `</body>` | `html` |
Write the config to the path reported by `--check`. Example for this project:
```json
{
"file": "public/index.html",
"insertBefore": "</body>",
"commentSyntax": "html"
}
```
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
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.
@@ -177,7 +191,11 @@ If the poll is still running as a background task, kill it and proceed directly
When the loop ends:
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
1. **Remove the injected script tag**:
```bash
node {{scripts_path}}/live-inject.mjs --remove
```
(The config.json stays so future `live-inject.mjs --port PORT` calls are instant.)
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
3. **Remove any leftover carbonize blocks** (search for `impeccable-carbonize-start` markers and clean up).
4. **Stop the server**:
@@ -0,0 +1,174 @@
/**
* CLI helper: insert/remove the live variant mode script tag in the project's
* main HTML entry point.
*
* On first live run, the agent generates `config.json` in this script's
* directory with the project's insertion target (framework-specific). On
* every subsequent run, this script handles insert/remove deterministically
* with zero LLM involvement.
*
* Usage:
* node live-inject.mjs --port PORT # Insert the live script tag
* node live-inject.mjs --remove # Remove the live script tag
* node live-inject.mjs --check # Check whether config.json exists
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = path.join(__dirname, 'config.json');
const MARKER_OPEN_TEXT = 'impeccable-live-start';
const MARKER_CLOSE_TEXT = 'impeccable-live-end';
export async function injectCli() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`Usage: node live-inject.mjs [options]
Insert or remove the live mode script tag in the project's HTML entry point.
Reads configuration from config.json (in this same directory).
Modes:
--port PORT Insert script tag pointing at http://localhost:PORT/live.js
--remove Remove the script tag (if present)
--check Print whether config.json exists and its content
Output (JSON):
{ ok, file, inserted|removed, config? }`);
process.exit(0);
}
if (args.includes('--check')) {
if (!fs.existsSync(CONFIG_PATH)) {
console.log(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(0);
}
try {
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
console.log(JSON.stringify({ ok: true, config: cfg, path: CONFIG_PATH }));
} catch (err) {
console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message }));
}
return;
}
// Load config
if (!fs.existsSync(CONFIG_PATH)) {
console.error(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
validateConfig(config);
const absFile = path.resolve(process.cwd(), config.file);
if (!fs.existsSync(absFile)) {
console.error(JSON.stringify({ ok: false, error: 'file_not_found', file: config.file }));
process.exit(1);
}
const content = fs.readFileSync(absFile, 'utf-8');
if (args.includes('--remove')) {
const updated = removeTag(content, config.commentSyntax);
if (updated === content) {
console.log(JSON.stringify({ ok: true, file: config.file, removed: false, note: 'no tag present' }));
return;
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, removed: true }));
return;
}
// Insert mode — need --port
const portIdx = args.indexOf('--port');
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : NaN;
if (!Number.isFinite(port)) {
console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
process.exit(1);
}
// Already inserted? Replace to refresh the port.
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) {
console.error(JSON.stringify({ ok: false, error: 'insertion_point_not_found', anchor: config.insertBefore }));
process.exit(1);
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, inserted: true, port }));
}
// ---------------------------------------------------------------------------
// Core operations
// ---------------------------------------------------------------------------
function validateConfig(cfg) {
if (!cfg || typeof cfg !== 'object') throw new Error('config.json must be an object');
if (typeof cfg.file !== 'string') throw new Error('config.file (string) required');
if (typeof cfg.insertBefore !== 'string' && typeof cfg.insertAfter !== 'string') {
throw new Error('config.insertBefore or config.insertAfter (string) required');
}
if (cfg.commentSyntax !== 'html' && cfg.commentSyntax !== 'jsx') {
throw new Error("config.commentSyntax must be 'html' or 'jsx'");
}
}
function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : '<!--'; }
function commentClose(syntax) { return syntax === 'jsx' ? '*/}' : '-->'; }
function buildTagBlock(syntax, port) {
const open = commentOpen(syntax);
const close = commentClose(syntax);
return (
open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
'<script src="http://localhost:' + port + '/live.js"></script>\n' +
open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
);
}
function insertTag(content, config, port) {
const block = buildTagBlock(config.commentSyntax, port);
if (config.insertBefore) {
const idx = content.indexOf(config.insertBefore);
if (idx === -1) return content;
return content.slice(0, idx) + block + content.slice(idx);
}
// insertAfter
const idx = content.indexOf(config.insertAfter);
if (idx === -1) return content;
const after = idx + config.insertAfter.length;
// Preserve a single trailing newline if the anchor didn't end with one
const prefix = content[after] === '\n' ? content.slice(0, after + 1) : content.slice(0, after) + '\n';
return prefix + block + content.slice(prefix.length);
}
/**
* Remove the live script block. Matches either HTML or JSX comment markers
* regardless of config (so stale tags from a wrong config can still be cleaned).
*/
function removeTag(content, _syntax) {
// Two patterns: HTML comment markers or JSX comment markers, with any content between.
const patterns = [
/\n?<!--\s*impeccable-live-start\s*-->[\s\S]*?<!--\s*impeccable-live-end\s*-->\n?/,
/\n?\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}\n?/,
];
for (const pat of patterns) {
const next = content.replace(pat, '\n');
if (next !== content) return next;
}
return content;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
const _running = process.argv[1];
if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs/')) {
injectCli();
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
+41 -23
View File
@@ -15,34 +15,48 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Inject the Browser Script
Find the project's main HTML entry point. This varies by framework:
The `live-inject.mjs` script handles insertion deterministically. It reads `config.json` from its own directory (one-time per-project setup).
| Framework | Typical file |
|-----------|-------------|
| Plain HTML | `index.html` |
| Vite / React | `index.html` (project root) |
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
| Next.js (Pages) | `pages/_document.tsx` |
| Nuxt | `app.vue` or `nuxt.config.ts` |
| Svelte / SvelteKit | `src/app.html` |
### Step 1: Ensure config.json exists
Add the script tag between comment markers (replace PORT with the actual port):
**HTML / Vue / Svelte:**
```html
<!-- impeccable-live-start -->
<script src="http://localhost:PORT/live.js"></script>
<!-- impeccable-live-end -->
```bash
node {{scripts_path}}/live-inject.mjs --check
```
**JSX / TSX (React, Next.js):**
```jsx
{/* impeccable-live-start */}
<script src="http://localhost:PORT/live.js"></script>
{/* impeccable-live-end */}
If the output says `{"ok": true, ...}`, skip to Step 2.
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:
| Framework | `file` | `insertBefore` | `commentSyntax` |
|-----------|--------|----------------|-----------------|
| Plain HTML | `index.html` | `</body>` | `html` |
| Vite / React | `index.html` | `</body>` | `html` |
| Next.js (App Router) | `app/layout.tsx` | `</body>` | `jsx` |
| Next.js (Pages) | `pages/_document.tsx` | `</body>` | `jsx` |
| Nuxt | `app.vue` | `</body>` | `html` |
| Svelte / SvelteKit | `src/app.html` | `</body>` | `html` |
| Astro | the root layout `.astro` file | `</body>` | `html` |
| Static site with a non-root HTML file | e.g. `public/index.html` | `</body>` | `html` |
Write the config to the path reported by `--check`. Example for this project:
```json
{
"file": "public/index.html",
"insertBefore": "</body>",
"commentSyntax": "html"
}
```
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
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.
@@ -177,7 +191,11 @@ If the poll is still running as a background task, kill it and proceed directly
When the loop ends:
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
1. **Remove the injected script tag**:
```bash
node {{scripts_path}}/live-inject.mjs --remove
```
(The config.json stays so future `live-inject.mjs --port PORT` calls are instant.)
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
3. **Remove any leftover carbonize blocks** (search for `impeccable-carbonize-start` markers and clean up).
4. **Stop the server**:
@@ -0,0 +1,174 @@
/**
* CLI helper: insert/remove the live variant mode script tag in the project's
* main HTML entry point.
*
* On first live run, the agent generates `config.json` in this script's
* directory with the project's insertion target (framework-specific). On
* every subsequent run, this script handles insert/remove deterministically
* with zero LLM involvement.
*
* Usage:
* node live-inject.mjs --port PORT # Insert the live script tag
* node live-inject.mjs --remove # Remove the live script tag
* node live-inject.mjs --check # Check whether config.json exists
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = path.join(__dirname, 'config.json');
const MARKER_OPEN_TEXT = 'impeccable-live-start';
const MARKER_CLOSE_TEXT = 'impeccable-live-end';
export async function injectCli() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`Usage: node live-inject.mjs [options]
Insert or remove the live mode script tag in the project's HTML entry point.
Reads configuration from config.json (in this same directory).
Modes:
--port PORT Insert script tag pointing at http://localhost:PORT/live.js
--remove Remove the script tag (if present)
--check Print whether config.json exists and its content
Output (JSON):
{ ok, file, inserted|removed, config? }`);
process.exit(0);
}
if (args.includes('--check')) {
if (!fs.existsSync(CONFIG_PATH)) {
console.log(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(0);
}
try {
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
console.log(JSON.stringify({ ok: true, config: cfg, path: CONFIG_PATH }));
} catch (err) {
console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message }));
}
return;
}
// Load config
if (!fs.existsSync(CONFIG_PATH)) {
console.error(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
validateConfig(config);
const absFile = path.resolve(process.cwd(), config.file);
if (!fs.existsSync(absFile)) {
console.error(JSON.stringify({ ok: false, error: 'file_not_found', file: config.file }));
process.exit(1);
}
const content = fs.readFileSync(absFile, 'utf-8');
if (args.includes('--remove')) {
const updated = removeTag(content, config.commentSyntax);
if (updated === content) {
console.log(JSON.stringify({ ok: true, file: config.file, removed: false, note: 'no tag present' }));
return;
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, removed: true }));
return;
}
// Insert mode — need --port
const portIdx = args.indexOf('--port');
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : NaN;
if (!Number.isFinite(port)) {
console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
process.exit(1);
}
// Already inserted? Replace to refresh the port.
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) {
console.error(JSON.stringify({ ok: false, error: 'insertion_point_not_found', anchor: config.insertBefore }));
process.exit(1);
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, inserted: true, port }));
}
// ---------------------------------------------------------------------------
// Core operations
// ---------------------------------------------------------------------------
function validateConfig(cfg) {
if (!cfg || typeof cfg !== 'object') throw new Error('config.json must be an object');
if (typeof cfg.file !== 'string') throw new Error('config.file (string) required');
if (typeof cfg.insertBefore !== 'string' && typeof cfg.insertAfter !== 'string') {
throw new Error('config.insertBefore or config.insertAfter (string) required');
}
if (cfg.commentSyntax !== 'html' && cfg.commentSyntax !== 'jsx') {
throw new Error("config.commentSyntax must be 'html' or 'jsx'");
}
}
function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : '<!--'; }
function commentClose(syntax) { return syntax === 'jsx' ? '*/}' : '-->'; }
function buildTagBlock(syntax, port) {
const open = commentOpen(syntax);
const close = commentClose(syntax);
return (
open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
'<script src="http://localhost:' + port + '/live.js"></script>\n' +
open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
);
}
function insertTag(content, config, port) {
const block = buildTagBlock(config.commentSyntax, port);
if (config.insertBefore) {
const idx = content.indexOf(config.insertBefore);
if (idx === -1) return content;
return content.slice(0, idx) + block + content.slice(idx);
}
// insertAfter
const idx = content.indexOf(config.insertAfter);
if (idx === -1) return content;
const after = idx + config.insertAfter.length;
// Preserve a single trailing newline if the anchor didn't end with one
const prefix = content[after] === '\n' ? content.slice(0, after + 1) : content.slice(0, after) + '\n';
return prefix + block + content.slice(prefix.length);
}
/**
* Remove the live script block. Matches either HTML or JSX comment markers
* regardless of config (so stale tags from a wrong config can still be cleaned).
*/
function removeTag(content, _syntax) {
// Two patterns: HTML comment markers or JSX comment markers, with any content between.
const patterns = [
/\n?<!--\s*impeccable-live-start\s*-->[\s\S]*?<!--\s*impeccable-live-end\s*-->\n?/,
/\n?\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}\n?/,
];
for (const pat of patterns) {
const next = content.replace(pat, '\n');
if (next !== content) return next;
}
return content;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
const _running = process.argv[1];
if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs/')) {
injectCli();
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
+41 -23
View File
@@ -15,34 +15,48 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Inject the Browser Script
Find the project's main HTML entry point. This varies by framework:
The `live-inject.mjs` script handles insertion deterministically. It reads `config.json` from its own directory (one-time per-project setup).
| Framework | Typical file |
|-----------|-------------|
| Plain HTML | `index.html` |
| Vite / React | `index.html` (project root) |
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
| Next.js (Pages) | `pages/_document.tsx` |
| Nuxt | `app.vue` or `nuxt.config.ts` |
| Svelte / SvelteKit | `src/app.html` |
### Step 1: Ensure config.json exists
Add the script tag between comment markers (replace PORT with the actual port):
**HTML / Vue / Svelte:**
```html
<!-- impeccable-live-start -->
<script src="http://localhost:PORT/live.js"></script>
<!-- impeccable-live-end -->
```bash
node {{scripts_path}}/live-inject.mjs --check
```
**JSX / TSX (React, Next.js):**
```jsx
{/* impeccable-live-start */}
<script src="http://localhost:PORT/live.js"></script>
{/* impeccable-live-end */}
If the output says `{"ok": true, ...}`, skip to Step 2.
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:
| Framework | `file` | `insertBefore` | `commentSyntax` |
|-----------|--------|----------------|-----------------|
| Plain HTML | `index.html` | `</body>` | `html` |
| Vite / React | `index.html` | `</body>` | `html` |
| Next.js (App Router) | `app/layout.tsx` | `</body>` | `jsx` |
| Next.js (Pages) | `pages/_document.tsx` | `</body>` | `jsx` |
| Nuxt | `app.vue` | `</body>` | `html` |
| Svelte / SvelteKit | `src/app.html` | `</body>` | `html` |
| Astro | the root layout `.astro` file | `</body>` | `html` |
| Static site with a non-root HTML file | e.g. `public/index.html` | `</body>` | `html` |
Write the config to the path reported by `--check`. Example for this project:
```json
{
"file": "public/index.html",
"insertBefore": "</body>",
"commentSyntax": "html"
}
```
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
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.
@@ -177,7 +191,11 @@ If the poll is still running as a background task, kill it and proceed directly
When the loop ends:
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
1. **Remove the injected script tag**:
```bash
node {{scripts_path}}/live-inject.mjs --remove
```
(The config.json stays so future `live-inject.mjs --port PORT` calls are instant.)
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
3. **Remove any leftover carbonize blocks** (search for `impeccable-carbonize-start` markers and clean up).
4. **Stop the server**:
@@ -0,0 +1,174 @@
/**
* CLI helper: insert/remove the live variant mode script tag in the project's
* main HTML entry point.
*
* On first live run, the agent generates `config.json` in this script's
* directory with the project's insertion target (framework-specific). On
* every subsequent run, this script handles insert/remove deterministically
* with zero LLM involvement.
*
* Usage:
* node live-inject.mjs --port PORT # Insert the live script tag
* node live-inject.mjs --remove # Remove the live script tag
* node live-inject.mjs --check # Check whether config.json exists
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = path.join(__dirname, 'config.json');
const MARKER_OPEN_TEXT = 'impeccable-live-start';
const MARKER_CLOSE_TEXT = 'impeccable-live-end';
export async function injectCli() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`Usage: node live-inject.mjs [options]
Insert or remove the live mode script tag in the project's HTML entry point.
Reads configuration from config.json (in this same directory).
Modes:
--port PORT Insert script tag pointing at http://localhost:PORT/live.js
--remove Remove the script tag (if present)
--check Print whether config.json exists and its content
Output (JSON):
{ ok, file, inserted|removed, config? }`);
process.exit(0);
}
if (args.includes('--check')) {
if (!fs.existsSync(CONFIG_PATH)) {
console.log(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(0);
}
try {
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
console.log(JSON.stringify({ ok: true, config: cfg, path: CONFIG_PATH }));
} catch (err) {
console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message }));
}
return;
}
// Load config
if (!fs.existsSync(CONFIG_PATH)) {
console.error(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
validateConfig(config);
const absFile = path.resolve(process.cwd(), config.file);
if (!fs.existsSync(absFile)) {
console.error(JSON.stringify({ ok: false, error: 'file_not_found', file: config.file }));
process.exit(1);
}
const content = fs.readFileSync(absFile, 'utf-8');
if (args.includes('--remove')) {
const updated = removeTag(content, config.commentSyntax);
if (updated === content) {
console.log(JSON.stringify({ ok: true, file: config.file, removed: false, note: 'no tag present' }));
return;
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, removed: true }));
return;
}
// Insert mode — need --port
const portIdx = args.indexOf('--port');
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : NaN;
if (!Number.isFinite(port)) {
console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
process.exit(1);
}
// Already inserted? Replace to refresh the port.
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) {
console.error(JSON.stringify({ ok: false, error: 'insertion_point_not_found', anchor: config.insertBefore }));
process.exit(1);
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, inserted: true, port }));
}
// ---------------------------------------------------------------------------
// Core operations
// ---------------------------------------------------------------------------
function validateConfig(cfg) {
if (!cfg || typeof cfg !== 'object') throw new Error('config.json must be an object');
if (typeof cfg.file !== 'string') throw new Error('config.file (string) required');
if (typeof cfg.insertBefore !== 'string' && typeof cfg.insertAfter !== 'string') {
throw new Error('config.insertBefore or config.insertAfter (string) required');
}
if (cfg.commentSyntax !== 'html' && cfg.commentSyntax !== 'jsx') {
throw new Error("config.commentSyntax must be 'html' or 'jsx'");
}
}
function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : '<!--'; }
function commentClose(syntax) { return syntax === 'jsx' ? '*/}' : '-->'; }
function buildTagBlock(syntax, port) {
const open = commentOpen(syntax);
const close = commentClose(syntax);
return (
open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
'<script src="http://localhost:' + port + '/live.js"></script>\n' +
open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
);
}
function insertTag(content, config, port) {
const block = buildTagBlock(config.commentSyntax, port);
if (config.insertBefore) {
const idx = content.indexOf(config.insertBefore);
if (idx === -1) return content;
return content.slice(0, idx) + block + content.slice(idx);
}
// insertAfter
const idx = content.indexOf(config.insertAfter);
if (idx === -1) return content;
const after = idx + config.insertAfter.length;
// Preserve a single trailing newline if the anchor didn't end with one
const prefix = content[after] === '\n' ? content.slice(0, after + 1) : content.slice(0, after) + '\n';
return prefix + block + content.slice(prefix.length);
}
/**
* Remove the live script block. Matches either HTML or JSX comment markers
* regardless of config (so stale tags from a wrong config can still be cleaned).
*/
function removeTag(content, _syntax) {
// Two patterns: HTML comment markers or JSX comment markers, with any content between.
const patterns = [
/\n?<!--\s*impeccable-live-start\s*-->[\s\S]*?<!--\s*impeccable-live-end\s*-->\n?/,
/\n?\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}\n?/,
];
for (const pat of patterns) {
const next = content.replace(pat, '\n');
if (next !== content) return next;
}
return content;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
const _running = process.argv[1];
if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs/')) {
injectCli();
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
+41 -23
View File
@@ -15,34 +15,48 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Inject the Browser Script
Find the project's main HTML entry point. This varies by framework:
The `live-inject.mjs` script handles insertion deterministically. It reads `config.json` from its own directory (one-time per-project setup).
| Framework | Typical file |
|-----------|-------------|
| Plain HTML | `index.html` |
| Vite / React | `index.html` (project root) |
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
| Next.js (Pages) | `pages/_document.tsx` |
| Nuxt | `app.vue` or `nuxt.config.ts` |
| Svelte / SvelteKit | `src/app.html` |
### Step 1: Ensure config.json exists
Add the script tag between comment markers (replace PORT with the actual port):
**HTML / Vue / Svelte:**
```html
<!-- impeccable-live-start -->
<script src="http://localhost:PORT/live.js"></script>
<!-- impeccable-live-end -->
```bash
node {{scripts_path}}/live-inject.mjs --check
```
**JSX / TSX (React, Next.js):**
```jsx
{/* impeccable-live-start */}
<script src="http://localhost:PORT/live.js"></script>
{/* impeccable-live-end */}
If the output says `{"ok": true, ...}`, skip to Step 2.
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:
| Framework | `file` | `insertBefore` | `commentSyntax` |
|-----------|--------|----------------|-----------------|
| Plain HTML | `index.html` | `</body>` | `html` |
| Vite / React | `index.html` | `</body>` | `html` |
| Next.js (App Router) | `app/layout.tsx` | `</body>` | `jsx` |
| Next.js (Pages) | `pages/_document.tsx` | `</body>` | `jsx` |
| Nuxt | `app.vue` | `</body>` | `html` |
| Svelte / SvelteKit | `src/app.html` | `</body>` | `html` |
| Astro | the root layout `.astro` file | `</body>` | `html` |
| Static site with a non-root HTML file | e.g. `public/index.html` | `</body>` | `html` |
Write the config to the path reported by `--check`. Example for this project:
```json
{
"file": "public/index.html",
"insertBefore": "</body>",
"commentSyntax": "html"
}
```
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
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.
@@ -177,7 +191,11 @@ If the poll is still running as a background task, kill it and proceed directly
When the loop ends:
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
1. **Remove the injected script tag**:
```bash
node {{scripts_path}}/live-inject.mjs --remove
```
(The config.json stays so future `live-inject.mjs --port PORT` calls are instant.)
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
3. **Remove any leftover carbonize blocks** (search for `impeccable-carbonize-start` markers and clean up).
4. **Stop the server**:
@@ -0,0 +1,174 @@
/**
* CLI helper: insert/remove the live variant mode script tag in the project's
* main HTML entry point.
*
* On first live run, the agent generates `config.json` in this script's
* directory with the project's insertion target (framework-specific). On
* every subsequent run, this script handles insert/remove deterministically
* with zero LLM involvement.
*
* Usage:
* node live-inject.mjs --port PORT # Insert the live script tag
* node live-inject.mjs --remove # Remove the live script tag
* node live-inject.mjs --check # Check whether config.json exists
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = path.join(__dirname, 'config.json');
const MARKER_OPEN_TEXT = 'impeccable-live-start';
const MARKER_CLOSE_TEXT = 'impeccable-live-end';
export async function injectCli() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`Usage: node live-inject.mjs [options]
Insert or remove the live mode script tag in the project's HTML entry point.
Reads configuration from config.json (in this same directory).
Modes:
--port PORT Insert script tag pointing at http://localhost:PORT/live.js
--remove Remove the script tag (if present)
--check Print whether config.json exists and its content
Output (JSON):
{ ok, file, inserted|removed, config? }`);
process.exit(0);
}
if (args.includes('--check')) {
if (!fs.existsSync(CONFIG_PATH)) {
console.log(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(0);
}
try {
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
console.log(JSON.stringify({ ok: true, config: cfg, path: CONFIG_PATH }));
} catch (err) {
console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message }));
}
return;
}
// Load config
if (!fs.existsSync(CONFIG_PATH)) {
console.error(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
validateConfig(config);
const absFile = path.resolve(process.cwd(), config.file);
if (!fs.existsSync(absFile)) {
console.error(JSON.stringify({ ok: false, error: 'file_not_found', file: config.file }));
process.exit(1);
}
const content = fs.readFileSync(absFile, 'utf-8');
if (args.includes('--remove')) {
const updated = removeTag(content, config.commentSyntax);
if (updated === content) {
console.log(JSON.stringify({ ok: true, file: config.file, removed: false, note: 'no tag present' }));
return;
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, removed: true }));
return;
}
// Insert mode — need --port
const portIdx = args.indexOf('--port');
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : NaN;
if (!Number.isFinite(port)) {
console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
process.exit(1);
}
// Already inserted? Replace to refresh the port.
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) {
console.error(JSON.stringify({ ok: false, error: 'insertion_point_not_found', anchor: config.insertBefore }));
process.exit(1);
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, inserted: true, port }));
}
// ---------------------------------------------------------------------------
// Core operations
// ---------------------------------------------------------------------------
function validateConfig(cfg) {
if (!cfg || typeof cfg !== 'object') throw new Error('config.json must be an object');
if (typeof cfg.file !== 'string') throw new Error('config.file (string) required');
if (typeof cfg.insertBefore !== 'string' && typeof cfg.insertAfter !== 'string') {
throw new Error('config.insertBefore or config.insertAfter (string) required');
}
if (cfg.commentSyntax !== 'html' && cfg.commentSyntax !== 'jsx') {
throw new Error("config.commentSyntax must be 'html' or 'jsx'");
}
}
function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : '<!--'; }
function commentClose(syntax) { return syntax === 'jsx' ? '*/}' : '-->'; }
function buildTagBlock(syntax, port) {
const open = commentOpen(syntax);
const close = commentClose(syntax);
return (
open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
'<script src="http://localhost:' + port + '/live.js"></script>\n' +
open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
);
}
function insertTag(content, config, port) {
const block = buildTagBlock(config.commentSyntax, port);
if (config.insertBefore) {
const idx = content.indexOf(config.insertBefore);
if (idx === -1) return content;
return content.slice(0, idx) + block + content.slice(idx);
}
// insertAfter
const idx = content.indexOf(config.insertAfter);
if (idx === -1) return content;
const after = idx + config.insertAfter.length;
// Preserve a single trailing newline if the anchor didn't end with one
const prefix = content[after] === '\n' ? content.slice(0, after + 1) : content.slice(0, after) + '\n';
return prefix + block + content.slice(prefix.length);
}
/**
* Remove the live script block. Matches either HTML or JSX comment markers
* regardless of config (so stale tags from a wrong config can still be cleaned).
*/
function removeTag(content, _syntax) {
// Two patterns: HTML comment markers or JSX comment markers, with any content between.
const patterns = [
/\n?<!--\s*impeccable-live-start\s*-->[\s\S]*?<!--\s*impeccable-live-end\s*-->\n?/,
/\n?\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}\n?/,
];
for (const pat of patterns) {
const next = content.replace(pat, '\n');
if (next !== content) return next;
}
return content;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
const _running = process.argv[1];
if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs/')) {
injectCli();
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
+41 -23
View File
@@ -15,34 +15,48 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Inject the Browser Script
Find the project's main HTML entry point. This varies by framework:
The `live-inject.mjs` script handles insertion deterministically. It reads `config.json` from its own directory (one-time per-project setup).
| Framework | Typical file |
|-----------|-------------|
| Plain HTML | `index.html` |
| Vite / React | `index.html` (project root) |
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
| Next.js (Pages) | `pages/_document.tsx` |
| Nuxt | `app.vue` or `nuxt.config.ts` |
| Svelte / SvelteKit | `src/app.html` |
### Step 1: Ensure config.json exists
Add the script tag between comment markers (replace PORT with the actual port):
**HTML / Vue / Svelte:**
```html
<!-- impeccable-live-start -->
<script src="http://localhost:PORT/live.js"></script>
<!-- impeccable-live-end -->
```bash
node {{scripts_path}}/live-inject.mjs --check
```
**JSX / TSX (React, Next.js):**
```jsx
{/* impeccable-live-start */}
<script src="http://localhost:PORT/live.js"></script>
{/* impeccable-live-end */}
If the output says `{"ok": true, ...}`, skip to Step 2.
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:
| Framework | `file` | `insertBefore` | `commentSyntax` |
|-----------|--------|----------------|-----------------|
| Plain HTML | `index.html` | `</body>` | `html` |
| Vite / React | `index.html` | `</body>` | `html` |
| Next.js (App Router) | `app/layout.tsx` | `</body>` | `jsx` |
| Next.js (Pages) | `pages/_document.tsx` | `</body>` | `jsx` |
| Nuxt | `app.vue` | `</body>` | `html` |
| Svelte / SvelteKit | `src/app.html` | `</body>` | `html` |
| Astro | the root layout `.astro` file | `</body>` | `html` |
| Static site with a non-root HTML file | e.g. `public/index.html` | `</body>` | `html` |
Write the config to the path reported by `--check`. Example for this project:
```json
{
"file": "public/index.html",
"insertBefore": "</body>",
"commentSyntax": "html"
}
```
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
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.
@@ -177,7 +191,11 @@ If the poll is still running as a background task, kill it and proceed directly
When the loop ends:
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
1. **Remove the injected script tag**:
```bash
node {{scripts_path}}/live-inject.mjs --remove
```
(The config.json stays so future `live-inject.mjs --port PORT` calls are instant.)
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
3. **Remove any leftover carbonize blocks** (search for `impeccable-carbonize-start` markers and clean up).
4. **Stop the server**:
@@ -0,0 +1,174 @@
/**
* CLI helper: insert/remove the live variant mode script tag in the project's
* main HTML entry point.
*
* On first live run, the agent generates `config.json` in this script's
* directory with the project's insertion target (framework-specific). On
* every subsequent run, this script handles insert/remove deterministically
* with zero LLM involvement.
*
* Usage:
* node live-inject.mjs --port PORT # Insert the live script tag
* node live-inject.mjs --remove # Remove the live script tag
* node live-inject.mjs --check # Check whether config.json exists
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = path.join(__dirname, 'config.json');
const MARKER_OPEN_TEXT = 'impeccable-live-start';
const MARKER_CLOSE_TEXT = 'impeccable-live-end';
export async function injectCli() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`Usage: node live-inject.mjs [options]
Insert or remove the live mode script tag in the project's HTML entry point.
Reads configuration from config.json (in this same directory).
Modes:
--port PORT Insert script tag pointing at http://localhost:PORT/live.js
--remove Remove the script tag (if present)
--check Print whether config.json exists and its content
Output (JSON):
{ ok, file, inserted|removed, config? }`);
process.exit(0);
}
if (args.includes('--check')) {
if (!fs.existsSync(CONFIG_PATH)) {
console.log(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(0);
}
try {
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
console.log(JSON.stringify({ ok: true, config: cfg, path: CONFIG_PATH }));
} catch (err) {
console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message }));
}
return;
}
// Load config
if (!fs.existsSync(CONFIG_PATH)) {
console.error(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
validateConfig(config);
const absFile = path.resolve(process.cwd(), config.file);
if (!fs.existsSync(absFile)) {
console.error(JSON.stringify({ ok: false, error: 'file_not_found', file: config.file }));
process.exit(1);
}
const content = fs.readFileSync(absFile, 'utf-8');
if (args.includes('--remove')) {
const updated = removeTag(content, config.commentSyntax);
if (updated === content) {
console.log(JSON.stringify({ ok: true, file: config.file, removed: false, note: 'no tag present' }));
return;
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, removed: true }));
return;
}
// Insert mode — need --port
const portIdx = args.indexOf('--port');
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : NaN;
if (!Number.isFinite(port)) {
console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
process.exit(1);
}
// Already inserted? Replace to refresh the port.
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) {
console.error(JSON.stringify({ ok: false, error: 'insertion_point_not_found', anchor: config.insertBefore }));
process.exit(1);
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, inserted: true, port }));
}
// ---------------------------------------------------------------------------
// Core operations
// ---------------------------------------------------------------------------
function validateConfig(cfg) {
if (!cfg || typeof cfg !== 'object') throw new Error('config.json must be an object');
if (typeof cfg.file !== 'string') throw new Error('config.file (string) required');
if (typeof cfg.insertBefore !== 'string' && typeof cfg.insertAfter !== 'string') {
throw new Error('config.insertBefore or config.insertAfter (string) required');
}
if (cfg.commentSyntax !== 'html' && cfg.commentSyntax !== 'jsx') {
throw new Error("config.commentSyntax must be 'html' or 'jsx'");
}
}
function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : '<!--'; }
function commentClose(syntax) { return syntax === 'jsx' ? '*/}' : '-->'; }
function buildTagBlock(syntax, port) {
const open = commentOpen(syntax);
const close = commentClose(syntax);
return (
open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
'<script src="http://localhost:' + port + '/live.js"></script>\n' +
open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
);
}
function insertTag(content, config, port) {
const block = buildTagBlock(config.commentSyntax, port);
if (config.insertBefore) {
const idx = content.indexOf(config.insertBefore);
if (idx === -1) return content;
return content.slice(0, idx) + block + content.slice(idx);
}
// insertAfter
const idx = content.indexOf(config.insertAfter);
if (idx === -1) return content;
const after = idx + config.insertAfter.length;
// Preserve a single trailing newline if the anchor didn't end with one
const prefix = content[after] === '\n' ? content.slice(0, after + 1) : content.slice(0, after) + '\n';
return prefix + block + content.slice(prefix.length);
}
/**
* Remove the live script block. Matches either HTML or JSX comment markers
* regardless of config (so stale tags from a wrong config can still be cleaned).
*/
function removeTag(content, _syntax) {
// Two patterns: HTML comment markers or JSX comment markers, with any content between.
const patterns = [
/\n?<!--\s*impeccable-live-start\s*-->[\s\S]*?<!--\s*impeccable-live-end\s*-->\n?/,
/\n?\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}\n?/,
];
for (const pat of patterns) {
const next = content.replace(pat, '\n');
if (next !== content) return next;
}
return content;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
const _running = process.argv[1];
if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs/')) {
injectCli();
}
export { insertTag, removeTag, validateConfig, buildTagBlock };
-1
View File
@@ -756,6 +756,5 @@
<script type="module" src="./app.js"></script>
</body>
</html>
+41 -23
View File
@@ -15,34 +15,48 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Inject the Browser Script
Find the project's main HTML entry point. This varies by framework:
The `live-inject.mjs` script handles insertion deterministically. It reads `config.json` from its own directory (one-time per-project setup).
| Framework | Typical file |
|-----------|-------------|
| Plain HTML | `index.html` |
| Vite / React | `index.html` (project root) |
| Next.js (App Router) | `app/layout.tsx` (add a `<Script>` component) |
| Next.js (Pages) | `pages/_document.tsx` |
| Nuxt | `app.vue` or `nuxt.config.ts` |
| Svelte / SvelteKit | `src/app.html` |
### Step 1: Ensure config.json exists
Add the script tag between comment markers (replace PORT with the actual port):
**HTML / Vue / Svelte:**
```html
<!-- impeccable-live-start -->
<script src="http://localhost:PORT/live.js"></script>
<!-- impeccable-live-end -->
```bash
node {{scripts_path}}/live-inject.mjs --check
```
**JSX / TSX (React, Next.js):**
```jsx
{/* impeccable-live-start */}
<script src="http://localhost:PORT/live.js"></script>
{/* impeccable-live-end */}
If the output says `{"ok": true, ...}`, skip to Step 2.
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:
| Framework | `file` | `insertBefore` | `commentSyntax` |
|-----------|--------|----------------|-----------------|
| Plain HTML | `index.html` | `</body>` | `html` |
| Vite / React | `index.html` | `</body>` | `html` |
| Next.js (App Router) | `app/layout.tsx` | `</body>` | `jsx` |
| Next.js (Pages) | `pages/_document.tsx` | `</body>` | `jsx` |
| Nuxt | `app.vue` | `</body>` | `html` |
| Svelte / SvelteKit | `src/app.html` | `</body>` | `html` |
| Astro | the root layout `.astro` file | `</body>` | `html` |
| Static site with a non-root HTML file | e.g. `public/index.html` | `</body>` | `html` |
Write the config to the path reported by `--check`. Example for this project:
```json
{
"file": "public/index.html",
"insertBefore": "</body>",
"commentSyntax": "html"
}
```
Place it before the closing `</body>` or at the end of the layout component. Save the file. The dev server will reload and the element picker will activate.
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.
@@ -177,7 +191,11 @@ If the poll is still running as a background task, kill it and proceed directly
When the loop ends:
1. **Remove the injected script tag** from the source file. Delete everything between `<!-- impeccable-live-start -->` and `<!-- impeccable-live-end -->` (inclusive). Use the appropriate comment syntax for the framework.
1. **Remove the injected script tag**:
```bash
node {{scripts_path}}/live-inject.mjs --remove
```
(The config.json stays so future `live-inject.mjs --port PORT` calls are instant.)
2. **Remove any leftover variant wrappers** (search for `impeccable-variants-start` markers and clean up).
3. **Remove any leftover carbonize blocks** (search for `impeccable-carbonize-start` markers and clean up).
4. **Stop the server**:
@@ -0,0 +1,174 @@
/**
* CLI helper: insert/remove the live variant mode script tag in the project's
* main HTML entry point.
*
* On first live run, the agent generates `config.json` in this script's
* directory with the project's insertion target (framework-specific). On
* every subsequent run, this script handles insert/remove deterministically
* with zero LLM involvement.
*
* Usage:
* node live-inject.mjs --port PORT # Insert the live script tag
* node live-inject.mjs --remove # Remove the live script tag
* node live-inject.mjs --check # Check whether config.json exists
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = path.join(__dirname, 'config.json');
const MARKER_OPEN_TEXT = 'impeccable-live-start';
const MARKER_CLOSE_TEXT = 'impeccable-live-end';
export async function injectCli() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`Usage: node live-inject.mjs [options]
Insert or remove the live mode script tag in the project's HTML entry point.
Reads configuration from config.json (in this same directory).
Modes:
--port PORT Insert script tag pointing at http://localhost:PORT/live.js
--remove Remove the script tag (if present)
--check Print whether config.json exists and its content
Output (JSON):
{ ok, file, inserted|removed, config? }`);
process.exit(0);
}
if (args.includes('--check')) {
if (!fs.existsSync(CONFIG_PATH)) {
console.log(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(0);
}
try {
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
console.log(JSON.stringify({ ok: true, config: cfg, path: CONFIG_PATH }));
} catch (err) {
console.log(JSON.stringify({ ok: false, error: 'config_invalid', message: err.message }));
}
return;
}
// Load config
if (!fs.existsSync(CONFIG_PATH)) {
console.error(JSON.stringify({ ok: false, error: 'config_missing', path: CONFIG_PATH }));
process.exit(1);
}
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
validateConfig(config);
const absFile = path.resolve(process.cwd(), config.file);
if (!fs.existsSync(absFile)) {
console.error(JSON.stringify({ ok: false, error: 'file_not_found', file: config.file }));
process.exit(1);
}
const content = fs.readFileSync(absFile, 'utf-8');
if (args.includes('--remove')) {
const updated = removeTag(content, config.commentSyntax);
if (updated === content) {
console.log(JSON.stringify({ ok: true, file: config.file, removed: false, note: 'no tag present' }));
return;
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, removed: true }));
return;
}
// Insert mode — need --port
const portIdx = args.indexOf('--port');
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : NaN;
if (!Number.isFinite(port)) {
console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
process.exit(1);
}
// Already inserted? Replace to refresh the port.
const withoutOld = removeTag(content, config.commentSyntax);
const updated = insertTag(withoutOld, config, port);
if (updated === withoutOld) {
console.error(JSON.stringify({ ok: false, error: 'insertion_point_not_found', anchor: config.insertBefore }));
process.exit(1);
}
fs.writeFileSync(absFile, updated, 'utf-8');
console.log(JSON.stringify({ ok: true, file: config.file, inserted: true, port }));
}
// ---------------------------------------------------------------------------
// Core operations
// ---------------------------------------------------------------------------
function validateConfig(cfg) {
if (!cfg || typeof cfg !== 'object') throw new Error('config.json must be an object');
if (typeof cfg.file !== 'string') throw new Error('config.file (string) required');
if (typeof cfg.insertBefore !== 'string' && typeof cfg.insertAfter !== 'string') {
throw new Error('config.insertBefore or config.insertAfter (string) required');
}
if (cfg.commentSyntax !== 'html' && cfg.commentSyntax !== 'jsx') {
throw new Error("config.commentSyntax must be 'html' or 'jsx'");
}
}
function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : '<!--'; }
function commentClose(syntax) { return syntax === 'jsx' ? '*/}' : '-->'; }
function buildTagBlock(syntax, port) {
const open = commentOpen(syntax);
const close = commentClose(syntax);
return (
open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
'<script src="http://localhost:' + port + '/live.js"></script>\n' +
open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
);
}
function insertTag(content, config, port) {
const block = buildTagBlock(config.commentSyntax, port);
if (config.insertBefore) {
const idx = content.indexOf(config.insertBefore);
if (idx === -1) return content;
return content.slice(0, idx) + block + content.slice(idx);
}
// insertAfter
const idx = content.indexOf(config.insertAfter);
if (idx === -1) return content;
const after = idx + config.insertAfter.length;
// Preserve a single trailing newline if the anchor didn't end with one
const prefix = content[after] === '\n' ? content.slice(0, after + 1) : content.slice(0, after) + '\n';
return prefix + block + content.slice(prefix.length);
}
/**
* Remove the live script block. Matches either HTML or JSX comment markers
* regardless of config (so stale tags from a wrong config can still be cleaned).
*/
function removeTag(content, _syntax) {
// Two patterns: HTML comment markers or JSX comment markers, with any content between.
const patterns = [
/\n?<!--\s*impeccable-live-start\s*-->[\s\S]*?<!--\s*impeccable-live-end\s*-->\n?/,
/\n?\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}\n?/,
];
for (const pat of patterns) {
const next = content.replace(pat, '\n');
if (next !== content) return next;
}
return content;
}
// ---------------------------------------------------------------------------
// Auto-execute
// ---------------------------------------------------------------------------
const _running = process.argv[1];
if (_running?.endsWith('live-inject.mjs') || _running?.endsWith('live-inject.mjs/')) {
injectCli();
}
export { insertTag, removeTag, validateConfig, buildTagBlock };