From c169b84f208b1e84ebec5b538cba24d4ca7e1673 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:32:23 +0000 Subject: [PATCH] Sync generated provider output --- .../skills/impeccable/scripts/live-server.mjs | 53 ++++++++++--------- .../scripts/live/browser-script-parts.mjs | 48 +++++++++++++++++ .../skills/impeccable/scripts/live-server.mjs | 53 ++++++++++--------- .../scripts/live/browser-script-parts.mjs | 48 +++++++++++++++++ .../skills/impeccable/scripts/live-server.mjs | 53 ++++++++++--------- .../scripts/live/browser-script-parts.mjs | 48 +++++++++++++++++ .../skills/impeccable/scripts/live-server.mjs | 53 ++++++++++--------- .../scripts/live/browser-script-parts.mjs | 48 +++++++++++++++++ .../skills/impeccable/scripts/live-server.mjs | 53 ++++++++++--------- .../scripts/live/browser-script-parts.mjs | 48 +++++++++++++++++ .../skills/impeccable/scripts/live-server.mjs | 53 ++++++++++--------- .../scripts/live/browser-script-parts.mjs | 48 +++++++++++++++++ .../skills/impeccable/scripts/live-server.mjs | 53 ++++++++++--------- .../scripts/live/browser-script-parts.mjs | 48 +++++++++++++++++ .pi/skills/impeccable/scripts/live-server.mjs | 53 ++++++++++--------- .../scripts/live/browser-script-parts.mjs | 48 +++++++++++++++++ .../skills/impeccable/scripts/live-server.mjs | 53 ++++++++++--------- .../scripts/live/browser-script-parts.mjs | 48 +++++++++++++++++ .../skills/impeccable/scripts/live-server.mjs | 53 ++++++++++--------- .../scripts/live/browser-script-parts.mjs | 48 +++++++++++++++++ .../skills/impeccable/scripts/live-server.mjs | 53 ++++++++++--------- .../scripts/live/browser-script-parts.mjs | 48 +++++++++++++++++ .../skills/impeccable/scripts/live-server.mjs | 53 ++++++++++--------- .../scripts/live/browser-script-parts.mjs | 48 +++++++++++++++++ .../skills/impeccable/scripts/live-server.mjs | 53 ++++++++++--------- .../scripts/live/browser-script-parts.mjs | 48 +++++++++++++++++ 26 files changed, 975 insertions(+), 338 deletions(-) create mode 100644 .agents/skills/impeccable/scripts/live/browser-script-parts.mjs create mode 100644 .claude/skills/impeccable/scripts/live/browser-script-parts.mjs create mode 100644 .cursor/skills/impeccable/scripts/live/browser-script-parts.mjs create mode 100644 .gemini/skills/impeccable/scripts/live/browser-script-parts.mjs create mode 100644 .github/skills/impeccable/scripts/live/browser-script-parts.mjs create mode 100644 .kiro/skills/impeccable/scripts/live/browser-script-parts.mjs create mode 100644 .opencode/skills/impeccable/scripts/live/browser-script-parts.mjs create mode 100644 .pi/skills/impeccable/scripts/live/browser-script-parts.mjs create mode 100644 .qoder/skills/impeccable/scripts/live/browser-script-parts.mjs create mode 100644 .rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs create mode 100644 .trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs create mode 100644 .trae/skills/impeccable/scripts/live/browser-script-parts.mjs create mode 100644 plugin/skills/impeccable/scripts/live/browser-script-parts.mjs diff --git a/.agents/skills/impeccable/scripts/live-server.mjs b/.agents/skills/impeccable/scripts/live-server.mjs index 68d547026..0fc4d61ba 100644 --- a/.agents/skills/impeccable/scripts/live-server.mjs +++ b/.agents/skills/impeccable/scripts/live-server.mjs @@ -22,6 +22,12 @@ import net from 'node:net'; import { fileURLToPath } from 'node:url'; import { parseDesignMd } from './lib/design-parser.mjs'; import { resolveContextDir } from './context.mjs'; +import { + assembleLiveBrowserScript, + assertLiveBrowserScriptParts, + readLiveBrowserScriptParts, + resolveLiveBrowserScriptParts, +} from './live/browser-script-parts.mjs'; import { createLiveSessionStore } from './live/session-store.mjs'; import { validateEvent } from './live/event-validation.mjs'; import { createManualEditRoutes } from './live/manual-edit-routes.mjs'; @@ -347,19 +353,18 @@ function loadBrowserScripts() { try { detectScript = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ } } - // live-browser.js: DO NOT cache. Return the path so the /live.js handler - // can re-read on every request. Editing the browser script during iteration - // should land on the next tab reload, not require a server restart. - const sessionPath = path.join(__dirname, 'live-browser-session.js'); - const livePath = path.join(__dirname, 'live-browser.js'); - for (const p of [sessionPath, livePath]) { - if (!fs.existsSync(p)) { - process.stderr.write('Error: live browser script not found at ' + p + '\n'); - process.exit(1); - } + // Browser script parts: DO NOT cache. Return paths so the /live.js handler + // can re-read every part on each request. Editing browser code during + // iteration should land on the next tab reload, not require a server restart. + const liveScriptParts = resolveLiveBrowserScriptParts(__dirname); + try { + assertLiveBrowserScriptParts(liveScriptParts); + } catch (err) { + process.stderr.write('Error: ' + err.message + '\n'); + process.exit(1); } - return { detectScript, sessionPath, livePath }; + return { detectScript, liveScriptParts }; } function hasProjectContext() { @@ -379,7 +384,7 @@ function statOrNull(filePath) { // HTTP request handler // --------------------------------------------------------------------------- -function createRequestHandler({ detectScript, sessionPath, livePath }) { +function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); res.setHeader('Access-Control-Allow-Origin', '*'); @@ -395,24 +400,20 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) { // the next tab reload. No-store headers prevent browser caching across // sessions — during iteration, a cached old script silently breaks // every subsequent session. - let sessionScript; - let liveScript; + let parts; try { - sessionScript = fs.readFileSync(sessionPath, 'utf-8'); - liveScript = fs.readFileSync(livePath, 'utf-8'); + parts = readLiveBrowserScriptParts(liveScriptParts); } catch (err) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error reading live browser scripts: ' + err.message); return; } - const body = - `window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` + - `window.__IMPECCABLE_PORT__ = ${state.port};\n` + - // Canonical command vocabulary (values + labels + icons). live-browser.js - // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` + - sessionScript + '\n' + - liveScript; + const body = assembleLiveBrowserScript({ + token: state.token, + port: state.port, + vocabulary: LIVE_COMMANDS, + parts, + }); res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', @@ -1116,8 +1117,8 @@ const annotRoot = getLiveAnnotationsDir(process.cwd()); fs.mkdirSync(annotRoot, { recursive: true }); state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-')); -const { detectScript, sessionPath, livePath } = loadBrowserScripts(); -httpServer = http.createServer(createRequestHandler({ detectScript, sessionPath, livePath })); +const { detectScript, liveScriptParts } = loadBrowserScripts(); +httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts })); httpServer.listen(state.port, '127.0.0.1', () => { writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token }); diff --git a/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs b/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs new file mode 100644 index 000000000..f99704aff --- /dev/null +++ b/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ + Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), +]); + +export function resolveLiveBrowserScriptParts(scriptsDir, parts = LIVE_BROWSER_SCRIPT_PARTS) { + if (!scriptsDir) throw new Error('scriptsDir is required'); + return parts.map((part, index) => ({ + ...part, + index, + path: path.join(scriptsDir, part.file), + })); +} + +export function assertLiveBrowserScriptParts(parts, exists = fs.existsSync) { + for (const part of parts) { + if (!exists(part.path)) { + throw new Error(`Live browser script part missing: ${part.name} (${part.path})`); + } + } + return parts; +} + +export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.readFileSync(filePath, 'utf-8')) { + return parts.map((part) => ({ + ...part, + source: readFile(part.path), + })); +} + +export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { + const prelude = + `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + + `window.__IMPECCABLE_PORT__ = ${port};\n` + + // Canonical command vocabulary (values + labels + icons). live-browser.js + // builds its action picker from this instead of an inline copy. + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + + const body = parts.map((part) => { + const file = part.file || path.basename(part.path || ''); + return `// --- impeccable live script part: ${part.name} (${file}) ---\n${part.source}`; + }).join('\n'); + + return prelude + body; +} diff --git a/.claude/skills/impeccable/scripts/live-server.mjs b/.claude/skills/impeccable/scripts/live-server.mjs index 68d547026..0fc4d61ba 100644 --- a/.claude/skills/impeccable/scripts/live-server.mjs +++ b/.claude/skills/impeccable/scripts/live-server.mjs @@ -22,6 +22,12 @@ import net from 'node:net'; import { fileURLToPath } from 'node:url'; import { parseDesignMd } from './lib/design-parser.mjs'; import { resolveContextDir } from './context.mjs'; +import { + assembleLiveBrowserScript, + assertLiveBrowserScriptParts, + readLiveBrowserScriptParts, + resolveLiveBrowserScriptParts, +} from './live/browser-script-parts.mjs'; import { createLiveSessionStore } from './live/session-store.mjs'; import { validateEvent } from './live/event-validation.mjs'; import { createManualEditRoutes } from './live/manual-edit-routes.mjs'; @@ -347,19 +353,18 @@ function loadBrowserScripts() { try { detectScript = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ } } - // live-browser.js: DO NOT cache. Return the path so the /live.js handler - // can re-read on every request. Editing the browser script during iteration - // should land on the next tab reload, not require a server restart. - const sessionPath = path.join(__dirname, 'live-browser-session.js'); - const livePath = path.join(__dirname, 'live-browser.js'); - for (const p of [sessionPath, livePath]) { - if (!fs.existsSync(p)) { - process.stderr.write('Error: live browser script not found at ' + p + '\n'); - process.exit(1); - } + // Browser script parts: DO NOT cache. Return paths so the /live.js handler + // can re-read every part on each request. Editing browser code during + // iteration should land on the next tab reload, not require a server restart. + const liveScriptParts = resolveLiveBrowserScriptParts(__dirname); + try { + assertLiveBrowserScriptParts(liveScriptParts); + } catch (err) { + process.stderr.write('Error: ' + err.message + '\n'); + process.exit(1); } - return { detectScript, sessionPath, livePath }; + return { detectScript, liveScriptParts }; } function hasProjectContext() { @@ -379,7 +384,7 @@ function statOrNull(filePath) { // HTTP request handler // --------------------------------------------------------------------------- -function createRequestHandler({ detectScript, sessionPath, livePath }) { +function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); res.setHeader('Access-Control-Allow-Origin', '*'); @@ -395,24 +400,20 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) { // the next tab reload. No-store headers prevent browser caching across // sessions — during iteration, a cached old script silently breaks // every subsequent session. - let sessionScript; - let liveScript; + let parts; try { - sessionScript = fs.readFileSync(sessionPath, 'utf-8'); - liveScript = fs.readFileSync(livePath, 'utf-8'); + parts = readLiveBrowserScriptParts(liveScriptParts); } catch (err) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error reading live browser scripts: ' + err.message); return; } - const body = - `window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` + - `window.__IMPECCABLE_PORT__ = ${state.port};\n` + - // Canonical command vocabulary (values + labels + icons). live-browser.js - // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` + - sessionScript + '\n' + - liveScript; + const body = assembleLiveBrowserScript({ + token: state.token, + port: state.port, + vocabulary: LIVE_COMMANDS, + parts, + }); res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', @@ -1116,8 +1117,8 @@ const annotRoot = getLiveAnnotationsDir(process.cwd()); fs.mkdirSync(annotRoot, { recursive: true }); state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-')); -const { detectScript, sessionPath, livePath } = loadBrowserScripts(); -httpServer = http.createServer(createRequestHandler({ detectScript, sessionPath, livePath })); +const { detectScript, liveScriptParts } = loadBrowserScripts(); +httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts })); httpServer.listen(state.port, '127.0.0.1', () => { writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token }); diff --git a/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs b/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs new file mode 100644 index 000000000..f99704aff --- /dev/null +++ b/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ + Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), +]); + +export function resolveLiveBrowserScriptParts(scriptsDir, parts = LIVE_BROWSER_SCRIPT_PARTS) { + if (!scriptsDir) throw new Error('scriptsDir is required'); + return parts.map((part, index) => ({ + ...part, + index, + path: path.join(scriptsDir, part.file), + })); +} + +export function assertLiveBrowserScriptParts(parts, exists = fs.existsSync) { + for (const part of parts) { + if (!exists(part.path)) { + throw new Error(`Live browser script part missing: ${part.name} (${part.path})`); + } + } + return parts; +} + +export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.readFileSync(filePath, 'utf-8')) { + return parts.map((part) => ({ + ...part, + source: readFile(part.path), + })); +} + +export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { + const prelude = + `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + + `window.__IMPECCABLE_PORT__ = ${port};\n` + + // Canonical command vocabulary (values + labels + icons). live-browser.js + // builds its action picker from this instead of an inline copy. + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + + const body = parts.map((part) => { + const file = part.file || path.basename(part.path || ''); + return `// --- impeccable live script part: ${part.name} (${file}) ---\n${part.source}`; + }).join('\n'); + + return prelude + body; +} diff --git a/.cursor/skills/impeccable/scripts/live-server.mjs b/.cursor/skills/impeccable/scripts/live-server.mjs index 68d547026..0fc4d61ba 100644 --- a/.cursor/skills/impeccable/scripts/live-server.mjs +++ b/.cursor/skills/impeccable/scripts/live-server.mjs @@ -22,6 +22,12 @@ import net from 'node:net'; import { fileURLToPath } from 'node:url'; import { parseDesignMd } from './lib/design-parser.mjs'; import { resolveContextDir } from './context.mjs'; +import { + assembleLiveBrowserScript, + assertLiveBrowserScriptParts, + readLiveBrowserScriptParts, + resolveLiveBrowserScriptParts, +} from './live/browser-script-parts.mjs'; import { createLiveSessionStore } from './live/session-store.mjs'; import { validateEvent } from './live/event-validation.mjs'; import { createManualEditRoutes } from './live/manual-edit-routes.mjs'; @@ -347,19 +353,18 @@ function loadBrowserScripts() { try { detectScript = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ } } - // live-browser.js: DO NOT cache. Return the path so the /live.js handler - // can re-read on every request. Editing the browser script during iteration - // should land on the next tab reload, not require a server restart. - const sessionPath = path.join(__dirname, 'live-browser-session.js'); - const livePath = path.join(__dirname, 'live-browser.js'); - for (const p of [sessionPath, livePath]) { - if (!fs.existsSync(p)) { - process.stderr.write('Error: live browser script not found at ' + p + '\n'); - process.exit(1); - } + // Browser script parts: DO NOT cache. Return paths so the /live.js handler + // can re-read every part on each request. Editing browser code during + // iteration should land on the next tab reload, not require a server restart. + const liveScriptParts = resolveLiveBrowserScriptParts(__dirname); + try { + assertLiveBrowserScriptParts(liveScriptParts); + } catch (err) { + process.stderr.write('Error: ' + err.message + '\n'); + process.exit(1); } - return { detectScript, sessionPath, livePath }; + return { detectScript, liveScriptParts }; } function hasProjectContext() { @@ -379,7 +384,7 @@ function statOrNull(filePath) { // HTTP request handler // --------------------------------------------------------------------------- -function createRequestHandler({ detectScript, sessionPath, livePath }) { +function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); res.setHeader('Access-Control-Allow-Origin', '*'); @@ -395,24 +400,20 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) { // the next tab reload. No-store headers prevent browser caching across // sessions — during iteration, a cached old script silently breaks // every subsequent session. - let sessionScript; - let liveScript; + let parts; try { - sessionScript = fs.readFileSync(sessionPath, 'utf-8'); - liveScript = fs.readFileSync(livePath, 'utf-8'); + parts = readLiveBrowserScriptParts(liveScriptParts); } catch (err) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error reading live browser scripts: ' + err.message); return; } - const body = - `window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` + - `window.__IMPECCABLE_PORT__ = ${state.port};\n` + - // Canonical command vocabulary (values + labels + icons). live-browser.js - // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` + - sessionScript + '\n' + - liveScript; + const body = assembleLiveBrowserScript({ + token: state.token, + port: state.port, + vocabulary: LIVE_COMMANDS, + parts, + }); res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', @@ -1116,8 +1117,8 @@ const annotRoot = getLiveAnnotationsDir(process.cwd()); fs.mkdirSync(annotRoot, { recursive: true }); state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-')); -const { detectScript, sessionPath, livePath } = loadBrowserScripts(); -httpServer = http.createServer(createRequestHandler({ detectScript, sessionPath, livePath })); +const { detectScript, liveScriptParts } = loadBrowserScripts(); +httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts })); httpServer.listen(state.port, '127.0.0.1', () => { writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token }); diff --git a/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs b/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs new file mode 100644 index 000000000..f99704aff --- /dev/null +++ b/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ + Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), +]); + +export function resolveLiveBrowserScriptParts(scriptsDir, parts = LIVE_BROWSER_SCRIPT_PARTS) { + if (!scriptsDir) throw new Error('scriptsDir is required'); + return parts.map((part, index) => ({ + ...part, + index, + path: path.join(scriptsDir, part.file), + })); +} + +export function assertLiveBrowserScriptParts(parts, exists = fs.existsSync) { + for (const part of parts) { + if (!exists(part.path)) { + throw new Error(`Live browser script part missing: ${part.name} (${part.path})`); + } + } + return parts; +} + +export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.readFileSync(filePath, 'utf-8')) { + return parts.map((part) => ({ + ...part, + source: readFile(part.path), + })); +} + +export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { + const prelude = + `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + + `window.__IMPECCABLE_PORT__ = ${port};\n` + + // Canonical command vocabulary (values + labels + icons). live-browser.js + // builds its action picker from this instead of an inline copy. + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + + const body = parts.map((part) => { + const file = part.file || path.basename(part.path || ''); + return `// --- impeccable live script part: ${part.name} (${file}) ---\n${part.source}`; + }).join('\n'); + + return prelude + body; +} diff --git a/.gemini/skills/impeccable/scripts/live-server.mjs b/.gemini/skills/impeccable/scripts/live-server.mjs index 68d547026..0fc4d61ba 100644 --- a/.gemini/skills/impeccable/scripts/live-server.mjs +++ b/.gemini/skills/impeccable/scripts/live-server.mjs @@ -22,6 +22,12 @@ import net from 'node:net'; import { fileURLToPath } from 'node:url'; import { parseDesignMd } from './lib/design-parser.mjs'; import { resolveContextDir } from './context.mjs'; +import { + assembleLiveBrowserScript, + assertLiveBrowserScriptParts, + readLiveBrowserScriptParts, + resolveLiveBrowserScriptParts, +} from './live/browser-script-parts.mjs'; import { createLiveSessionStore } from './live/session-store.mjs'; import { validateEvent } from './live/event-validation.mjs'; import { createManualEditRoutes } from './live/manual-edit-routes.mjs'; @@ -347,19 +353,18 @@ function loadBrowserScripts() { try { detectScript = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ } } - // live-browser.js: DO NOT cache. Return the path so the /live.js handler - // can re-read on every request. Editing the browser script during iteration - // should land on the next tab reload, not require a server restart. - const sessionPath = path.join(__dirname, 'live-browser-session.js'); - const livePath = path.join(__dirname, 'live-browser.js'); - for (const p of [sessionPath, livePath]) { - if (!fs.existsSync(p)) { - process.stderr.write('Error: live browser script not found at ' + p + '\n'); - process.exit(1); - } + // Browser script parts: DO NOT cache. Return paths so the /live.js handler + // can re-read every part on each request. Editing browser code during + // iteration should land on the next tab reload, not require a server restart. + const liveScriptParts = resolveLiveBrowserScriptParts(__dirname); + try { + assertLiveBrowserScriptParts(liveScriptParts); + } catch (err) { + process.stderr.write('Error: ' + err.message + '\n'); + process.exit(1); } - return { detectScript, sessionPath, livePath }; + return { detectScript, liveScriptParts }; } function hasProjectContext() { @@ -379,7 +384,7 @@ function statOrNull(filePath) { // HTTP request handler // --------------------------------------------------------------------------- -function createRequestHandler({ detectScript, sessionPath, livePath }) { +function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); res.setHeader('Access-Control-Allow-Origin', '*'); @@ -395,24 +400,20 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) { // the next tab reload. No-store headers prevent browser caching across // sessions — during iteration, a cached old script silently breaks // every subsequent session. - let sessionScript; - let liveScript; + let parts; try { - sessionScript = fs.readFileSync(sessionPath, 'utf-8'); - liveScript = fs.readFileSync(livePath, 'utf-8'); + parts = readLiveBrowserScriptParts(liveScriptParts); } catch (err) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error reading live browser scripts: ' + err.message); return; } - const body = - `window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` + - `window.__IMPECCABLE_PORT__ = ${state.port};\n` + - // Canonical command vocabulary (values + labels + icons). live-browser.js - // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` + - sessionScript + '\n' + - liveScript; + const body = assembleLiveBrowserScript({ + token: state.token, + port: state.port, + vocabulary: LIVE_COMMANDS, + parts, + }); res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', @@ -1116,8 +1117,8 @@ const annotRoot = getLiveAnnotationsDir(process.cwd()); fs.mkdirSync(annotRoot, { recursive: true }); state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-')); -const { detectScript, sessionPath, livePath } = loadBrowserScripts(); -httpServer = http.createServer(createRequestHandler({ detectScript, sessionPath, livePath })); +const { detectScript, liveScriptParts } = loadBrowserScripts(); +httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts })); httpServer.listen(state.port, '127.0.0.1', () => { writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token }); diff --git a/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs b/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs new file mode 100644 index 000000000..f99704aff --- /dev/null +++ b/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ + Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), +]); + +export function resolveLiveBrowserScriptParts(scriptsDir, parts = LIVE_BROWSER_SCRIPT_PARTS) { + if (!scriptsDir) throw new Error('scriptsDir is required'); + return parts.map((part, index) => ({ + ...part, + index, + path: path.join(scriptsDir, part.file), + })); +} + +export function assertLiveBrowserScriptParts(parts, exists = fs.existsSync) { + for (const part of parts) { + if (!exists(part.path)) { + throw new Error(`Live browser script part missing: ${part.name} (${part.path})`); + } + } + return parts; +} + +export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.readFileSync(filePath, 'utf-8')) { + return parts.map((part) => ({ + ...part, + source: readFile(part.path), + })); +} + +export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { + const prelude = + `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + + `window.__IMPECCABLE_PORT__ = ${port};\n` + + // Canonical command vocabulary (values + labels + icons). live-browser.js + // builds its action picker from this instead of an inline copy. + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + + const body = parts.map((part) => { + const file = part.file || path.basename(part.path || ''); + return `// --- impeccable live script part: ${part.name} (${file}) ---\n${part.source}`; + }).join('\n'); + + return prelude + body; +} diff --git a/.github/skills/impeccable/scripts/live-server.mjs b/.github/skills/impeccable/scripts/live-server.mjs index 68d547026..0fc4d61ba 100644 --- a/.github/skills/impeccable/scripts/live-server.mjs +++ b/.github/skills/impeccable/scripts/live-server.mjs @@ -22,6 +22,12 @@ import net from 'node:net'; import { fileURLToPath } from 'node:url'; import { parseDesignMd } from './lib/design-parser.mjs'; import { resolveContextDir } from './context.mjs'; +import { + assembleLiveBrowserScript, + assertLiveBrowserScriptParts, + readLiveBrowserScriptParts, + resolveLiveBrowserScriptParts, +} from './live/browser-script-parts.mjs'; import { createLiveSessionStore } from './live/session-store.mjs'; import { validateEvent } from './live/event-validation.mjs'; import { createManualEditRoutes } from './live/manual-edit-routes.mjs'; @@ -347,19 +353,18 @@ function loadBrowserScripts() { try { detectScript = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ } } - // live-browser.js: DO NOT cache. Return the path so the /live.js handler - // can re-read on every request. Editing the browser script during iteration - // should land on the next tab reload, not require a server restart. - const sessionPath = path.join(__dirname, 'live-browser-session.js'); - const livePath = path.join(__dirname, 'live-browser.js'); - for (const p of [sessionPath, livePath]) { - if (!fs.existsSync(p)) { - process.stderr.write('Error: live browser script not found at ' + p + '\n'); - process.exit(1); - } + // Browser script parts: DO NOT cache. Return paths so the /live.js handler + // can re-read every part on each request. Editing browser code during + // iteration should land on the next tab reload, not require a server restart. + const liveScriptParts = resolveLiveBrowserScriptParts(__dirname); + try { + assertLiveBrowserScriptParts(liveScriptParts); + } catch (err) { + process.stderr.write('Error: ' + err.message + '\n'); + process.exit(1); } - return { detectScript, sessionPath, livePath }; + return { detectScript, liveScriptParts }; } function hasProjectContext() { @@ -379,7 +384,7 @@ function statOrNull(filePath) { // HTTP request handler // --------------------------------------------------------------------------- -function createRequestHandler({ detectScript, sessionPath, livePath }) { +function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); res.setHeader('Access-Control-Allow-Origin', '*'); @@ -395,24 +400,20 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) { // the next tab reload. No-store headers prevent browser caching across // sessions — during iteration, a cached old script silently breaks // every subsequent session. - let sessionScript; - let liveScript; + let parts; try { - sessionScript = fs.readFileSync(sessionPath, 'utf-8'); - liveScript = fs.readFileSync(livePath, 'utf-8'); + parts = readLiveBrowserScriptParts(liveScriptParts); } catch (err) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error reading live browser scripts: ' + err.message); return; } - const body = - `window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` + - `window.__IMPECCABLE_PORT__ = ${state.port};\n` + - // Canonical command vocabulary (values + labels + icons). live-browser.js - // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` + - sessionScript + '\n' + - liveScript; + const body = assembleLiveBrowserScript({ + token: state.token, + port: state.port, + vocabulary: LIVE_COMMANDS, + parts, + }); res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', @@ -1116,8 +1117,8 @@ const annotRoot = getLiveAnnotationsDir(process.cwd()); fs.mkdirSync(annotRoot, { recursive: true }); state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-')); -const { detectScript, sessionPath, livePath } = loadBrowserScripts(); -httpServer = http.createServer(createRequestHandler({ detectScript, sessionPath, livePath })); +const { detectScript, liveScriptParts } = loadBrowserScripts(); +httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts })); httpServer.listen(state.port, '127.0.0.1', () => { writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token }); diff --git a/.github/skills/impeccable/scripts/live/browser-script-parts.mjs b/.github/skills/impeccable/scripts/live/browser-script-parts.mjs new file mode 100644 index 000000000..f99704aff --- /dev/null +++ b/.github/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ + Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), +]); + +export function resolveLiveBrowserScriptParts(scriptsDir, parts = LIVE_BROWSER_SCRIPT_PARTS) { + if (!scriptsDir) throw new Error('scriptsDir is required'); + return parts.map((part, index) => ({ + ...part, + index, + path: path.join(scriptsDir, part.file), + })); +} + +export function assertLiveBrowserScriptParts(parts, exists = fs.existsSync) { + for (const part of parts) { + if (!exists(part.path)) { + throw new Error(`Live browser script part missing: ${part.name} (${part.path})`); + } + } + return parts; +} + +export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.readFileSync(filePath, 'utf-8')) { + return parts.map((part) => ({ + ...part, + source: readFile(part.path), + })); +} + +export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { + const prelude = + `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + + `window.__IMPECCABLE_PORT__ = ${port};\n` + + // Canonical command vocabulary (values + labels + icons). live-browser.js + // builds its action picker from this instead of an inline copy. + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + + const body = parts.map((part) => { + const file = part.file || path.basename(part.path || ''); + return `// --- impeccable live script part: ${part.name} (${file}) ---\n${part.source}`; + }).join('\n'); + + return prelude + body; +} diff --git a/.kiro/skills/impeccable/scripts/live-server.mjs b/.kiro/skills/impeccable/scripts/live-server.mjs index 68d547026..0fc4d61ba 100644 --- a/.kiro/skills/impeccable/scripts/live-server.mjs +++ b/.kiro/skills/impeccable/scripts/live-server.mjs @@ -22,6 +22,12 @@ import net from 'node:net'; import { fileURLToPath } from 'node:url'; import { parseDesignMd } from './lib/design-parser.mjs'; import { resolveContextDir } from './context.mjs'; +import { + assembleLiveBrowserScript, + assertLiveBrowserScriptParts, + readLiveBrowserScriptParts, + resolveLiveBrowserScriptParts, +} from './live/browser-script-parts.mjs'; import { createLiveSessionStore } from './live/session-store.mjs'; import { validateEvent } from './live/event-validation.mjs'; import { createManualEditRoutes } from './live/manual-edit-routes.mjs'; @@ -347,19 +353,18 @@ function loadBrowserScripts() { try { detectScript = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ } } - // live-browser.js: DO NOT cache. Return the path so the /live.js handler - // can re-read on every request. Editing the browser script during iteration - // should land on the next tab reload, not require a server restart. - const sessionPath = path.join(__dirname, 'live-browser-session.js'); - const livePath = path.join(__dirname, 'live-browser.js'); - for (const p of [sessionPath, livePath]) { - if (!fs.existsSync(p)) { - process.stderr.write('Error: live browser script not found at ' + p + '\n'); - process.exit(1); - } + // Browser script parts: DO NOT cache. Return paths so the /live.js handler + // can re-read every part on each request. Editing browser code during + // iteration should land on the next tab reload, not require a server restart. + const liveScriptParts = resolveLiveBrowserScriptParts(__dirname); + try { + assertLiveBrowserScriptParts(liveScriptParts); + } catch (err) { + process.stderr.write('Error: ' + err.message + '\n'); + process.exit(1); } - return { detectScript, sessionPath, livePath }; + return { detectScript, liveScriptParts }; } function hasProjectContext() { @@ -379,7 +384,7 @@ function statOrNull(filePath) { // HTTP request handler // --------------------------------------------------------------------------- -function createRequestHandler({ detectScript, sessionPath, livePath }) { +function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); res.setHeader('Access-Control-Allow-Origin', '*'); @@ -395,24 +400,20 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) { // the next tab reload. No-store headers prevent browser caching across // sessions — during iteration, a cached old script silently breaks // every subsequent session. - let sessionScript; - let liveScript; + let parts; try { - sessionScript = fs.readFileSync(sessionPath, 'utf-8'); - liveScript = fs.readFileSync(livePath, 'utf-8'); + parts = readLiveBrowserScriptParts(liveScriptParts); } catch (err) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error reading live browser scripts: ' + err.message); return; } - const body = - `window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` + - `window.__IMPECCABLE_PORT__ = ${state.port};\n` + - // Canonical command vocabulary (values + labels + icons). live-browser.js - // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` + - sessionScript + '\n' + - liveScript; + const body = assembleLiveBrowserScript({ + token: state.token, + port: state.port, + vocabulary: LIVE_COMMANDS, + parts, + }); res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', @@ -1116,8 +1117,8 @@ const annotRoot = getLiveAnnotationsDir(process.cwd()); fs.mkdirSync(annotRoot, { recursive: true }); state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-')); -const { detectScript, sessionPath, livePath } = loadBrowserScripts(); -httpServer = http.createServer(createRequestHandler({ detectScript, sessionPath, livePath })); +const { detectScript, liveScriptParts } = loadBrowserScripts(); +httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts })); httpServer.listen(state.port, '127.0.0.1', () => { writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token }); diff --git a/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs b/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs new file mode 100644 index 000000000..f99704aff --- /dev/null +++ b/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ + Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), +]); + +export function resolveLiveBrowserScriptParts(scriptsDir, parts = LIVE_BROWSER_SCRIPT_PARTS) { + if (!scriptsDir) throw new Error('scriptsDir is required'); + return parts.map((part, index) => ({ + ...part, + index, + path: path.join(scriptsDir, part.file), + })); +} + +export function assertLiveBrowserScriptParts(parts, exists = fs.existsSync) { + for (const part of parts) { + if (!exists(part.path)) { + throw new Error(`Live browser script part missing: ${part.name} (${part.path})`); + } + } + return parts; +} + +export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.readFileSync(filePath, 'utf-8')) { + return parts.map((part) => ({ + ...part, + source: readFile(part.path), + })); +} + +export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { + const prelude = + `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + + `window.__IMPECCABLE_PORT__ = ${port};\n` + + // Canonical command vocabulary (values + labels + icons). live-browser.js + // builds its action picker from this instead of an inline copy. + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + + const body = parts.map((part) => { + const file = part.file || path.basename(part.path || ''); + return `// --- impeccable live script part: ${part.name} (${file}) ---\n${part.source}`; + }).join('\n'); + + return prelude + body; +} diff --git a/.opencode/skills/impeccable/scripts/live-server.mjs b/.opencode/skills/impeccable/scripts/live-server.mjs index 68d547026..0fc4d61ba 100644 --- a/.opencode/skills/impeccable/scripts/live-server.mjs +++ b/.opencode/skills/impeccable/scripts/live-server.mjs @@ -22,6 +22,12 @@ import net from 'node:net'; import { fileURLToPath } from 'node:url'; import { parseDesignMd } from './lib/design-parser.mjs'; import { resolveContextDir } from './context.mjs'; +import { + assembleLiveBrowserScript, + assertLiveBrowserScriptParts, + readLiveBrowserScriptParts, + resolveLiveBrowserScriptParts, +} from './live/browser-script-parts.mjs'; import { createLiveSessionStore } from './live/session-store.mjs'; import { validateEvent } from './live/event-validation.mjs'; import { createManualEditRoutes } from './live/manual-edit-routes.mjs'; @@ -347,19 +353,18 @@ function loadBrowserScripts() { try { detectScript = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ } } - // live-browser.js: DO NOT cache. Return the path so the /live.js handler - // can re-read on every request. Editing the browser script during iteration - // should land on the next tab reload, not require a server restart. - const sessionPath = path.join(__dirname, 'live-browser-session.js'); - const livePath = path.join(__dirname, 'live-browser.js'); - for (const p of [sessionPath, livePath]) { - if (!fs.existsSync(p)) { - process.stderr.write('Error: live browser script not found at ' + p + '\n'); - process.exit(1); - } + // Browser script parts: DO NOT cache. Return paths so the /live.js handler + // can re-read every part on each request. Editing browser code during + // iteration should land on the next tab reload, not require a server restart. + const liveScriptParts = resolveLiveBrowserScriptParts(__dirname); + try { + assertLiveBrowserScriptParts(liveScriptParts); + } catch (err) { + process.stderr.write('Error: ' + err.message + '\n'); + process.exit(1); } - return { detectScript, sessionPath, livePath }; + return { detectScript, liveScriptParts }; } function hasProjectContext() { @@ -379,7 +384,7 @@ function statOrNull(filePath) { // HTTP request handler // --------------------------------------------------------------------------- -function createRequestHandler({ detectScript, sessionPath, livePath }) { +function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); res.setHeader('Access-Control-Allow-Origin', '*'); @@ -395,24 +400,20 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) { // the next tab reload. No-store headers prevent browser caching across // sessions — during iteration, a cached old script silently breaks // every subsequent session. - let sessionScript; - let liveScript; + let parts; try { - sessionScript = fs.readFileSync(sessionPath, 'utf-8'); - liveScript = fs.readFileSync(livePath, 'utf-8'); + parts = readLiveBrowserScriptParts(liveScriptParts); } catch (err) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error reading live browser scripts: ' + err.message); return; } - const body = - `window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` + - `window.__IMPECCABLE_PORT__ = ${state.port};\n` + - // Canonical command vocabulary (values + labels + icons). live-browser.js - // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` + - sessionScript + '\n' + - liveScript; + const body = assembleLiveBrowserScript({ + token: state.token, + port: state.port, + vocabulary: LIVE_COMMANDS, + parts, + }); res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', @@ -1116,8 +1117,8 @@ const annotRoot = getLiveAnnotationsDir(process.cwd()); fs.mkdirSync(annotRoot, { recursive: true }); state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-')); -const { detectScript, sessionPath, livePath } = loadBrowserScripts(); -httpServer = http.createServer(createRequestHandler({ detectScript, sessionPath, livePath })); +const { detectScript, liveScriptParts } = loadBrowserScripts(); +httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts })); httpServer.listen(state.port, '127.0.0.1', () => { writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token }); diff --git a/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs b/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs new file mode 100644 index 000000000..f99704aff --- /dev/null +++ b/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ + Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), +]); + +export function resolveLiveBrowserScriptParts(scriptsDir, parts = LIVE_BROWSER_SCRIPT_PARTS) { + if (!scriptsDir) throw new Error('scriptsDir is required'); + return parts.map((part, index) => ({ + ...part, + index, + path: path.join(scriptsDir, part.file), + })); +} + +export function assertLiveBrowserScriptParts(parts, exists = fs.existsSync) { + for (const part of parts) { + if (!exists(part.path)) { + throw new Error(`Live browser script part missing: ${part.name} (${part.path})`); + } + } + return parts; +} + +export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.readFileSync(filePath, 'utf-8')) { + return parts.map((part) => ({ + ...part, + source: readFile(part.path), + })); +} + +export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { + const prelude = + `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + + `window.__IMPECCABLE_PORT__ = ${port};\n` + + // Canonical command vocabulary (values + labels + icons). live-browser.js + // builds its action picker from this instead of an inline copy. + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + + const body = parts.map((part) => { + const file = part.file || path.basename(part.path || ''); + return `// --- impeccable live script part: ${part.name} (${file}) ---\n${part.source}`; + }).join('\n'); + + return prelude + body; +} diff --git a/.pi/skills/impeccable/scripts/live-server.mjs b/.pi/skills/impeccable/scripts/live-server.mjs index 68d547026..0fc4d61ba 100644 --- a/.pi/skills/impeccable/scripts/live-server.mjs +++ b/.pi/skills/impeccable/scripts/live-server.mjs @@ -22,6 +22,12 @@ import net from 'node:net'; import { fileURLToPath } from 'node:url'; import { parseDesignMd } from './lib/design-parser.mjs'; import { resolveContextDir } from './context.mjs'; +import { + assembleLiveBrowserScript, + assertLiveBrowserScriptParts, + readLiveBrowserScriptParts, + resolveLiveBrowserScriptParts, +} from './live/browser-script-parts.mjs'; import { createLiveSessionStore } from './live/session-store.mjs'; import { validateEvent } from './live/event-validation.mjs'; import { createManualEditRoutes } from './live/manual-edit-routes.mjs'; @@ -347,19 +353,18 @@ function loadBrowserScripts() { try { detectScript = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ } } - // live-browser.js: DO NOT cache. Return the path so the /live.js handler - // can re-read on every request. Editing the browser script during iteration - // should land on the next tab reload, not require a server restart. - const sessionPath = path.join(__dirname, 'live-browser-session.js'); - const livePath = path.join(__dirname, 'live-browser.js'); - for (const p of [sessionPath, livePath]) { - if (!fs.existsSync(p)) { - process.stderr.write('Error: live browser script not found at ' + p + '\n'); - process.exit(1); - } + // Browser script parts: DO NOT cache. Return paths so the /live.js handler + // can re-read every part on each request. Editing browser code during + // iteration should land on the next tab reload, not require a server restart. + const liveScriptParts = resolveLiveBrowserScriptParts(__dirname); + try { + assertLiveBrowserScriptParts(liveScriptParts); + } catch (err) { + process.stderr.write('Error: ' + err.message + '\n'); + process.exit(1); } - return { detectScript, sessionPath, livePath }; + return { detectScript, liveScriptParts }; } function hasProjectContext() { @@ -379,7 +384,7 @@ function statOrNull(filePath) { // HTTP request handler // --------------------------------------------------------------------------- -function createRequestHandler({ detectScript, sessionPath, livePath }) { +function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); res.setHeader('Access-Control-Allow-Origin', '*'); @@ -395,24 +400,20 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) { // the next tab reload. No-store headers prevent browser caching across // sessions — during iteration, a cached old script silently breaks // every subsequent session. - let sessionScript; - let liveScript; + let parts; try { - sessionScript = fs.readFileSync(sessionPath, 'utf-8'); - liveScript = fs.readFileSync(livePath, 'utf-8'); + parts = readLiveBrowserScriptParts(liveScriptParts); } catch (err) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error reading live browser scripts: ' + err.message); return; } - const body = - `window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` + - `window.__IMPECCABLE_PORT__ = ${state.port};\n` + - // Canonical command vocabulary (values + labels + icons). live-browser.js - // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` + - sessionScript + '\n' + - liveScript; + const body = assembleLiveBrowserScript({ + token: state.token, + port: state.port, + vocabulary: LIVE_COMMANDS, + parts, + }); res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', @@ -1116,8 +1117,8 @@ const annotRoot = getLiveAnnotationsDir(process.cwd()); fs.mkdirSync(annotRoot, { recursive: true }); state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-')); -const { detectScript, sessionPath, livePath } = loadBrowserScripts(); -httpServer = http.createServer(createRequestHandler({ detectScript, sessionPath, livePath })); +const { detectScript, liveScriptParts } = loadBrowserScripts(); +httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts })); httpServer.listen(state.port, '127.0.0.1', () => { writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token }); diff --git a/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs b/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs new file mode 100644 index 000000000..f99704aff --- /dev/null +++ b/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ + Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), +]); + +export function resolveLiveBrowserScriptParts(scriptsDir, parts = LIVE_BROWSER_SCRIPT_PARTS) { + if (!scriptsDir) throw new Error('scriptsDir is required'); + return parts.map((part, index) => ({ + ...part, + index, + path: path.join(scriptsDir, part.file), + })); +} + +export function assertLiveBrowserScriptParts(parts, exists = fs.existsSync) { + for (const part of parts) { + if (!exists(part.path)) { + throw new Error(`Live browser script part missing: ${part.name} (${part.path})`); + } + } + return parts; +} + +export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.readFileSync(filePath, 'utf-8')) { + return parts.map((part) => ({ + ...part, + source: readFile(part.path), + })); +} + +export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { + const prelude = + `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + + `window.__IMPECCABLE_PORT__ = ${port};\n` + + // Canonical command vocabulary (values + labels + icons). live-browser.js + // builds its action picker from this instead of an inline copy. + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + + const body = parts.map((part) => { + const file = part.file || path.basename(part.path || ''); + return `// --- impeccable live script part: ${part.name} (${file}) ---\n${part.source}`; + }).join('\n'); + + return prelude + body; +} diff --git a/.qoder/skills/impeccable/scripts/live-server.mjs b/.qoder/skills/impeccable/scripts/live-server.mjs index 68d547026..0fc4d61ba 100644 --- a/.qoder/skills/impeccable/scripts/live-server.mjs +++ b/.qoder/skills/impeccable/scripts/live-server.mjs @@ -22,6 +22,12 @@ import net from 'node:net'; import { fileURLToPath } from 'node:url'; import { parseDesignMd } from './lib/design-parser.mjs'; import { resolveContextDir } from './context.mjs'; +import { + assembleLiveBrowserScript, + assertLiveBrowserScriptParts, + readLiveBrowserScriptParts, + resolveLiveBrowserScriptParts, +} from './live/browser-script-parts.mjs'; import { createLiveSessionStore } from './live/session-store.mjs'; import { validateEvent } from './live/event-validation.mjs'; import { createManualEditRoutes } from './live/manual-edit-routes.mjs'; @@ -347,19 +353,18 @@ function loadBrowserScripts() { try { detectScript = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ } } - // live-browser.js: DO NOT cache. Return the path so the /live.js handler - // can re-read on every request. Editing the browser script during iteration - // should land on the next tab reload, not require a server restart. - const sessionPath = path.join(__dirname, 'live-browser-session.js'); - const livePath = path.join(__dirname, 'live-browser.js'); - for (const p of [sessionPath, livePath]) { - if (!fs.existsSync(p)) { - process.stderr.write('Error: live browser script not found at ' + p + '\n'); - process.exit(1); - } + // Browser script parts: DO NOT cache. Return paths so the /live.js handler + // can re-read every part on each request. Editing browser code during + // iteration should land on the next tab reload, not require a server restart. + const liveScriptParts = resolveLiveBrowserScriptParts(__dirname); + try { + assertLiveBrowserScriptParts(liveScriptParts); + } catch (err) { + process.stderr.write('Error: ' + err.message + '\n'); + process.exit(1); } - return { detectScript, sessionPath, livePath }; + return { detectScript, liveScriptParts }; } function hasProjectContext() { @@ -379,7 +384,7 @@ function statOrNull(filePath) { // HTTP request handler // --------------------------------------------------------------------------- -function createRequestHandler({ detectScript, sessionPath, livePath }) { +function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); res.setHeader('Access-Control-Allow-Origin', '*'); @@ -395,24 +400,20 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) { // the next tab reload. No-store headers prevent browser caching across // sessions — during iteration, a cached old script silently breaks // every subsequent session. - let sessionScript; - let liveScript; + let parts; try { - sessionScript = fs.readFileSync(sessionPath, 'utf-8'); - liveScript = fs.readFileSync(livePath, 'utf-8'); + parts = readLiveBrowserScriptParts(liveScriptParts); } catch (err) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error reading live browser scripts: ' + err.message); return; } - const body = - `window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` + - `window.__IMPECCABLE_PORT__ = ${state.port};\n` + - // Canonical command vocabulary (values + labels + icons). live-browser.js - // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` + - sessionScript + '\n' + - liveScript; + const body = assembleLiveBrowserScript({ + token: state.token, + port: state.port, + vocabulary: LIVE_COMMANDS, + parts, + }); res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', @@ -1116,8 +1117,8 @@ const annotRoot = getLiveAnnotationsDir(process.cwd()); fs.mkdirSync(annotRoot, { recursive: true }); state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-')); -const { detectScript, sessionPath, livePath } = loadBrowserScripts(); -httpServer = http.createServer(createRequestHandler({ detectScript, sessionPath, livePath })); +const { detectScript, liveScriptParts } = loadBrowserScripts(); +httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts })); httpServer.listen(state.port, '127.0.0.1', () => { writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token }); diff --git a/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs b/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs new file mode 100644 index 000000000..f99704aff --- /dev/null +++ b/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ + Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), +]); + +export function resolveLiveBrowserScriptParts(scriptsDir, parts = LIVE_BROWSER_SCRIPT_PARTS) { + if (!scriptsDir) throw new Error('scriptsDir is required'); + return parts.map((part, index) => ({ + ...part, + index, + path: path.join(scriptsDir, part.file), + })); +} + +export function assertLiveBrowserScriptParts(parts, exists = fs.existsSync) { + for (const part of parts) { + if (!exists(part.path)) { + throw new Error(`Live browser script part missing: ${part.name} (${part.path})`); + } + } + return parts; +} + +export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.readFileSync(filePath, 'utf-8')) { + return parts.map((part) => ({ + ...part, + source: readFile(part.path), + })); +} + +export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { + const prelude = + `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + + `window.__IMPECCABLE_PORT__ = ${port};\n` + + // Canonical command vocabulary (values + labels + icons). live-browser.js + // builds its action picker from this instead of an inline copy. + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + + const body = parts.map((part) => { + const file = part.file || path.basename(part.path || ''); + return `// --- impeccable live script part: ${part.name} (${file}) ---\n${part.source}`; + }).join('\n'); + + return prelude + body; +} diff --git a/.rovodev/skills/impeccable/scripts/live-server.mjs b/.rovodev/skills/impeccable/scripts/live-server.mjs index 68d547026..0fc4d61ba 100644 --- a/.rovodev/skills/impeccable/scripts/live-server.mjs +++ b/.rovodev/skills/impeccable/scripts/live-server.mjs @@ -22,6 +22,12 @@ import net from 'node:net'; import { fileURLToPath } from 'node:url'; import { parseDesignMd } from './lib/design-parser.mjs'; import { resolveContextDir } from './context.mjs'; +import { + assembleLiveBrowserScript, + assertLiveBrowserScriptParts, + readLiveBrowserScriptParts, + resolveLiveBrowserScriptParts, +} from './live/browser-script-parts.mjs'; import { createLiveSessionStore } from './live/session-store.mjs'; import { validateEvent } from './live/event-validation.mjs'; import { createManualEditRoutes } from './live/manual-edit-routes.mjs'; @@ -347,19 +353,18 @@ function loadBrowserScripts() { try { detectScript = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ } } - // live-browser.js: DO NOT cache. Return the path so the /live.js handler - // can re-read on every request. Editing the browser script during iteration - // should land on the next tab reload, not require a server restart. - const sessionPath = path.join(__dirname, 'live-browser-session.js'); - const livePath = path.join(__dirname, 'live-browser.js'); - for (const p of [sessionPath, livePath]) { - if (!fs.existsSync(p)) { - process.stderr.write('Error: live browser script not found at ' + p + '\n'); - process.exit(1); - } + // Browser script parts: DO NOT cache. Return paths so the /live.js handler + // can re-read every part on each request. Editing browser code during + // iteration should land on the next tab reload, not require a server restart. + const liveScriptParts = resolveLiveBrowserScriptParts(__dirname); + try { + assertLiveBrowserScriptParts(liveScriptParts); + } catch (err) { + process.stderr.write('Error: ' + err.message + '\n'); + process.exit(1); } - return { detectScript, sessionPath, livePath }; + return { detectScript, liveScriptParts }; } function hasProjectContext() { @@ -379,7 +384,7 @@ function statOrNull(filePath) { // HTTP request handler // --------------------------------------------------------------------------- -function createRequestHandler({ detectScript, sessionPath, livePath }) { +function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); res.setHeader('Access-Control-Allow-Origin', '*'); @@ -395,24 +400,20 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) { // the next tab reload. No-store headers prevent browser caching across // sessions — during iteration, a cached old script silently breaks // every subsequent session. - let sessionScript; - let liveScript; + let parts; try { - sessionScript = fs.readFileSync(sessionPath, 'utf-8'); - liveScript = fs.readFileSync(livePath, 'utf-8'); + parts = readLiveBrowserScriptParts(liveScriptParts); } catch (err) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error reading live browser scripts: ' + err.message); return; } - const body = - `window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` + - `window.__IMPECCABLE_PORT__ = ${state.port};\n` + - // Canonical command vocabulary (values + labels + icons). live-browser.js - // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` + - sessionScript + '\n' + - liveScript; + const body = assembleLiveBrowserScript({ + token: state.token, + port: state.port, + vocabulary: LIVE_COMMANDS, + parts, + }); res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', @@ -1116,8 +1117,8 @@ const annotRoot = getLiveAnnotationsDir(process.cwd()); fs.mkdirSync(annotRoot, { recursive: true }); state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-')); -const { detectScript, sessionPath, livePath } = loadBrowserScripts(); -httpServer = http.createServer(createRequestHandler({ detectScript, sessionPath, livePath })); +const { detectScript, liveScriptParts } = loadBrowserScripts(); +httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts })); httpServer.listen(state.port, '127.0.0.1', () => { writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token }); diff --git a/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs b/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs new file mode 100644 index 000000000..f99704aff --- /dev/null +++ b/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ + Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), +]); + +export function resolveLiveBrowserScriptParts(scriptsDir, parts = LIVE_BROWSER_SCRIPT_PARTS) { + if (!scriptsDir) throw new Error('scriptsDir is required'); + return parts.map((part, index) => ({ + ...part, + index, + path: path.join(scriptsDir, part.file), + })); +} + +export function assertLiveBrowserScriptParts(parts, exists = fs.existsSync) { + for (const part of parts) { + if (!exists(part.path)) { + throw new Error(`Live browser script part missing: ${part.name} (${part.path})`); + } + } + return parts; +} + +export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.readFileSync(filePath, 'utf-8')) { + return parts.map((part) => ({ + ...part, + source: readFile(part.path), + })); +} + +export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { + const prelude = + `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + + `window.__IMPECCABLE_PORT__ = ${port};\n` + + // Canonical command vocabulary (values + labels + icons). live-browser.js + // builds its action picker from this instead of an inline copy. + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + + const body = parts.map((part) => { + const file = part.file || path.basename(part.path || ''); + return `// --- impeccable live script part: ${part.name} (${file}) ---\n${part.source}`; + }).join('\n'); + + return prelude + body; +} diff --git a/.trae-cn/skills/impeccable/scripts/live-server.mjs b/.trae-cn/skills/impeccable/scripts/live-server.mjs index 68d547026..0fc4d61ba 100644 --- a/.trae-cn/skills/impeccable/scripts/live-server.mjs +++ b/.trae-cn/skills/impeccable/scripts/live-server.mjs @@ -22,6 +22,12 @@ import net from 'node:net'; import { fileURLToPath } from 'node:url'; import { parseDesignMd } from './lib/design-parser.mjs'; import { resolveContextDir } from './context.mjs'; +import { + assembleLiveBrowserScript, + assertLiveBrowserScriptParts, + readLiveBrowserScriptParts, + resolveLiveBrowserScriptParts, +} from './live/browser-script-parts.mjs'; import { createLiveSessionStore } from './live/session-store.mjs'; import { validateEvent } from './live/event-validation.mjs'; import { createManualEditRoutes } from './live/manual-edit-routes.mjs'; @@ -347,19 +353,18 @@ function loadBrowserScripts() { try { detectScript = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ } } - // live-browser.js: DO NOT cache. Return the path so the /live.js handler - // can re-read on every request. Editing the browser script during iteration - // should land on the next tab reload, not require a server restart. - const sessionPath = path.join(__dirname, 'live-browser-session.js'); - const livePath = path.join(__dirname, 'live-browser.js'); - for (const p of [sessionPath, livePath]) { - if (!fs.existsSync(p)) { - process.stderr.write('Error: live browser script not found at ' + p + '\n'); - process.exit(1); - } + // Browser script parts: DO NOT cache. Return paths so the /live.js handler + // can re-read every part on each request. Editing browser code during + // iteration should land on the next tab reload, not require a server restart. + const liveScriptParts = resolveLiveBrowserScriptParts(__dirname); + try { + assertLiveBrowserScriptParts(liveScriptParts); + } catch (err) { + process.stderr.write('Error: ' + err.message + '\n'); + process.exit(1); } - return { detectScript, sessionPath, livePath }; + return { detectScript, liveScriptParts }; } function hasProjectContext() { @@ -379,7 +384,7 @@ function statOrNull(filePath) { // HTTP request handler // --------------------------------------------------------------------------- -function createRequestHandler({ detectScript, sessionPath, livePath }) { +function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); res.setHeader('Access-Control-Allow-Origin', '*'); @@ -395,24 +400,20 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) { // the next tab reload. No-store headers prevent browser caching across // sessions — during iteration, a cached old script silently breaks // every subsequent session. - let sessionScript; - let liveScript; + let parts; try { - sessionScript = fs.readFileSync(sessionPath, 'utf-8'); - liveScript = fs.readFileSync(livePath, 'utf-8'); + parts = readLiveBrowserScriptParts(liveScriptParts); } catch (err) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error reading live browser scripts: ' + err.message); return; } - const body = - `window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` + - `window.__IMPECCABLE_PORT__ = ${state.port};\n` + - // Canonical command vocabulary (values + labels + icons). live-browser.js - // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` + - sessionScript + '\n' + - liveScript; + const body = assembleLiveBrowserScript({ + token: state.token, + port: state.port, + vocabulary: LIVE_COMMANDS, + parts, + }); res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', @@ -1116,8 +1117,8 @@ const annotRoot = getLiveAnnotationsDir(process.cwd()); fs.mkdirSync(annotRoot, { recursive: true }); state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-')); -const { detectScript, sessionPath, livePath } = loadBrowserScripts(); -httpServer = http.createServer(createRequestHandler({ detectScript, sessionPath, livePath })); +const { detectScript, liveScriptParts } = loadBrowserScripts(); +httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts })); httpServer.listen(state.port, '127.0.0.1', () => { writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token }); diff --git a/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs b/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs new file mode 100644 index 000000000..f99704aff --- /dev/null +++ b/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ + Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), +]); + +export function resolveLiveBrowserScriptParts(scriptsDir, parts = LIVE_BROWSER_SCRIPT_PARTS) { + if (!scriptsDir) throw new Error('scriptsDir is required'); + return parts.map((part, index) => ({ + ...part, + index, + path: path.join(scriptsDir, part.file), + })); +} + +export function assertLiveBrowserScriptParts(parts, exists = fs.existsSync) { + for (const part of parts) { + if (!exists(part.path)) { + throw new Error(`Live browser script part missing: ${part.name} (${part.path})`); + } + } + return parts; +} + +export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.readFileSync(filePath, 'utf-8')) { + return parts.map((part) => ({ + ...part, + source: readFile(part.path), + })); +} + +export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { + const prelude = + `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + + `window.__IMPECCABLE_PORT__ = ${port};\n` + + // Canonical command vocabulary (values + labels + icons). live-browser.js + // builds its action picker from this instead of an inline copy. + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + + const body = parts.map((part) => { + const file = part.file || path.basename(part.path || ''); + return `// --- impeccable live script part: ${part.name} (${file}) ---\n${part.source}`; + }).join('\n'); + + return prelude + body; +} diff --git a/.trae/skills/impeccable/scripts/live-server.mjs b/.trae/skills/impeccable/scripts/live-server.mjs index 68d547026..0fc4d61ba 100644 --- a/.trae/skills/impeccable/scripts/live-server.mjs +++ b/.trae/skills/impeccable/scripts/live-server.mjs @@ -22,6 +22,12 @@ import net from 'node:net'; import { fileURLToPath } from 'node:url'; import { parseDesignMd } from './lib/design-parser.mjs'; import { resolveContextDir } from './context.mjs'; +import { + assembleLiveBrowserScript, + assertLiveBrowserScriptParts, + readLiveBrowserScriptParts, + resolveLiveBrowserScriptParts, +} from './live/browser-script-parts.mjs'; import { createLiveSessionStore } from './live/session-store.mjs'; import { validateEvent } from './live/event-validation.mjs'; import { createManualEditRoutes } from './live/manual-edit-routes.mjs'; @@ -347,19 +353,18 @@ function loadBrowserScripts() { try { detectScript = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ } } - // live-browser.js: DO NOT cache. Return the path so the /live.js handler - // can re-read on every request. Editing the browser script during iteration - // should land on the next tab reload, not require a server restart. - const sessionPath = path.join(__dirname, 'live-browser-session.js'); - const livePath = path.join(__dirname, 'live-browser.js'); - for (const p of [sessionPath, livePath]) { - if (!fs.existsSync(p)) { - process.stderr.write('Error: live browser script not found at ' + p + '\n'); - process.exit(1); - } + // Browser script parts: DO NOT cache. Return paths so the /live.js handler + // can re-read every part on each request. Editing browser code during + // iteration should land on the next tab reload, not require a server restart. + const liveScriptParts = resolveLiveBrowserScriptParts(__dirname); + try { + assertLiveBrowserScriptParts(liveScriptParts); + } catch (err) { + process.stderr.write('Error: ' + err.message + '\n'); + process.exit(1); } - return { detectScript, sessionPath, livePath }; + return { detectScript, liveScriptParts }; } function hasProjectContext() { @@ -379,7 +384,7 @@ function statOrNull(filePath) { // HTTP request handler // --------------------------------------------------------------------------- -function createRequestHandler({ detectScript, sessionPath, livePath }) { +function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); res.setHeader('Access-Control-Allow-Origin', '*'); @@ -395,24 +400,20 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) { // the next tab reload. No-store headers prevent browser caching across // sessions — during iteration, a cached old script silently breaks // every subsequent session. - let sessionScript; - let liveScript; + let parts; try { - sessionScript = fs.readFileSync(sessionPath, 'utf-8'); - liveScript = fs.readFileSync(livePath, 'utf-8'); + parts = readLiveBrowserScriptParts(liveScriptParts); } catch (err) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error reading live browser scripts: ' + err.message); return; } - const body = - `window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` + - `window.__IMPECCABLE_PORT__ = ${state.port};\n` + - // Canonical command vocabulary (values + labels + icons). live-browser.js - // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` + - sessionScript + '\n' + - liveScript; + const body = assembleLiveBrowserScript({ + token: state.token, + port: state.port, + vocabulary: LIVE_COMMANDS, + parts, + }); res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', @@ -1116,8 +1117,8 @@ const annotRoot = getLiveAnnotationsDir(process.cwd()); fs.mkdirSync(annotRoot, { recursive: true }); state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-')); -const { detectScript, sessionPath, livePath } = loadBrowserScripts(); -httpServer = http.createServer(createRequestHandler({ detectScript, sessionPath, livePath })); +const { detectScript, liveScriptParts } = loadBrowserScripts(); +httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts })); httpServer.listen(state.port, '127.0.0.1', () => { writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token }); diff --git a/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs b/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs new file mode 100644 index 000000000..f99704aff --- /dev/null +++ b/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ + Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), +]); + +export function resolveLiveBrowserScriptParts(scriptsDir, parts = LIVE_BROWSER_SCRIPT_PARTS) { + if (!scriptsDir) throw new Error('scriptsDir is required'); + return parts.map((part, index) => ({ + ...part, + index, + path: path.join(scriptsDir, part.file), + })); +} + +export function assertLiveBrowserScriptParts(parts, exists = fs.existsSync) { + for (const part of parts) { + if (!exists(part.path)) { + throw new Error(`Live browser script part missing: ${part.name} (${part.path})`); + } + } + return parts; +} + +export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.readFileSync(filePath, 'utf-8')) { + return parts.map((part) => ({ + ...part, + source: readFile(part.path), + })); +} + +export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { + const prelude = + `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + + `window.__IMPECCABLE_PORT__ = ${port};\n` + + // Canonical command vocabulary (values + labels + icons). live-browser.js + // builds its action picker from this instead of an inline copy. + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + + const body = parts.map((part) => { + const file = part.file || path.basename(part.path || ''); + return `// --- impeccable live script part: ${part.name} (${file}) ---\n${part.source}`; + }).join('\n'); + + return prelude + body; +} diff --git a/plugin/skills/impeccable/scripts/live-server.mjs b/plugin/skills/impeccable/scripts/live-server.mjs index 68d547026..0fc4d61ba 100644 --- a/plugin/skills/impeccable/scripts/live-server.mjs +++ b/plugin/skills/impeccable/scripts/live-server.mjs @@ -22,6 +22,12 @@ import net from 'node:net'; import { fileURLToPath } from 'node:url'; import { parseDesignMd } from './lib/design-parser.mjs'; import { resolveContextDir } from './context.mjs'; +import { + assembleLiveBrowserScript, + assertLiveBrowserScriptParts, + readLiveBrowserScriptParts, + resolveLiveBrowserScriptParts, +} from './live/browser-script-parts.mjs'; import { createLiveSessionStore } from './live/session-store.mjs'; import { validateEvent } from './live/event-validation.mjs'; import { createManualEditRoutes } from './live/manual-edit-routes.mjs'; @@ -347,19 +353,18 @@ function loadBrowserScripts() { try { detectScript = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ } } - // live-browser.js: DO NOT cache. Return the path so the /live.js handler - // can re-read on every request. Editing the browser script during iteration - // should land on the next tab reload, not require a server restart. - const sessionPath = path.join(__dirname, 'live-browser-session.js'); - const livePath = path.join(__dirname, 'live-browser.js'); - for (const p of [sessionPath, livePath]) { - if (!fs.existsSync(p)) { - process.stderr.write('Error: live browser script not found at ' + p + '\n'); - process.exit(1); - } + // Browser script parts: DO NOT cache. Return paths so the /live.js handler + // can re-read every part on each request. Editing browser code during + // iteration should land on the next tab reload, not require a server restart. + const liveScriptParts = resolveLiveBrowserScriptParts(__dirname); + try { + assertLiveBrowserScriptParts(liveScriptParts); + } catch (err) { + process.stderr.write('Error: ' + err.message + '\n'); + process.exit(1); } - return { detectScript, sessionPath, livePath }; + return { detectScript, liveScriptParts }; } function hasProjectContext() { @@ -379,7 +384,7 @@ function statOrNull(filePath) { // HTTP request handler // --------------------------------------------------------------------------- -function createRequestHandler({ detectScript, sessionPath, livePath }) { +function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); res.setHeader('Access-Control-Allow-Origin', '*'); @@ -395,24 +400,20 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) { // the next tab reload. No-store headers prevent browser caching across // sessions — during iteration, a cached old script silently breaks // every subsequent session. - let sessionScript; - let liveScript; + let parts; try { - sessionScript = fs.readFileSync(sessionPath, 'utf-8'); - liveScript = fs.readFileSync(livePath, 'utf-8'); + parts = readLiveBrowserScriptParts(liveScriptParts); } catch (err) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error reading live browser scripts: ' + err.message); return; } - const body = - `window.__IMPECCABLE_TOKEN__ = '${state.token}';\n` + - `window.__IMPECCABLE_PORT__ = ${state.port};\n` + - // Canonical command vocabulary (values + labels + icons). live-browser.js - // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(LIVE_COMMANDS)};\n` + - sessionScript + '\n' + - liveScript; + const body = assembleLiveBrowserScript({ + token: state.token, + port: state.port, + vocabulary: LIVE_COMMANDS, + parts, + }); res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', @@ -1116,8 +1117,8 @@ const annotRoot = getLiveAnnotationsDir(process.cwd()); fs.mkdirSync(annotRoot, { recursive: true }); state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-')); -const { detectScript, sessionPath, livePath } = loadBrowserScripts(); -httpServer = http.createServer(createRequestHandler({ detectScript, sessionPath, livePath })); +const { detectScript, liveScriptParts } = loadBrowserScripts(); +httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts })); httpServer.listen(state.port, '127.0.0.1', () => { writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token }); diff --git a/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs b/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs new file mode 100644 index 000000000..f99704aff --- /dev/null +++ b/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ + Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), + Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }), +]); + +export function resolveLiveBrowserScriptParts(scriptsDir, parts = LIVE_BROWSER_SCRIPT_PARTS) { + if (!scriptsDir) throw new Error('scriptsDir is required'); + return parts.map((part, index) => ({ + ...part, + index, + path: path.join(scriptsDir, part.file), + })); +} + +export function assertLiveBrowserScriptParts(parts, exists = fs.existsSync) { + for (const part of parts) { + if (!exists(part.path)) { + throw new Error(`Live browser script part missing: ${part.name} (${part.path})`); + } + } + return parts; +} + +export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.readFileSync(filePath, 'utf-8')) { + return parts.map((part) => ({ + ...part, + source: readFile(part.path), + })); +} + +export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { + const prelude = + `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + + `window.__IMPECCABLE_PORT__ = ${port};\n` + + // Canonical command vocabulary (values + labels + icons). live-browser.js + // builds its action picker from this instead of an inline copy. + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + + const body = parts.map((part) => { + const file = part.file || path.basename(part.path || ''); + return `// --- impeccable live script part: ${part.name} (${file}) ---\n${part.source}`; + }).join('\n'); + + return prelude + body; +}