mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
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:
co-authored by
Claude Opus 4.6
parent
f397b9f123
commit
8030bc226a
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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/
|
||||
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -756,6 +756,5 @@
|
||||
|
||||
<script type="module" src="./app.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user