From e8d494842eaafb80a60be3f47fabeecda6eca166 Mon Sep 17 00:00:00 2001
From: yacosta738 <33158051+yacosta738@users.noreply.github.com>
Date: Sun, 6 Sep 2026 02:57:57 +0000
Subject: [PATCH 1/7] fix(a11y): correct deterministic accessibility
regressions
---
.../reports/frontend-accessibility-auditor.md | 27 ++++++++------
.../state/frontend-accessibility-auditor.yaml | 35 +++++++++++++++++--
.../components/ComposerChannelSelector.vue | 2 +-
.../src/components/WaitlistForm.astro | 4 +--
4 files changed, 51 insertions(+), 17 deletions(-)
diff --git a/.agents/automation/reports/frontend-accessibility-auditor.md b/.agents/automation/reports/frontend-accessibility-auditor.md
index 5903bba14..12b87bbb9 100644
--- a/.agents/automation/reports/frontend-accessibility-auditor.md
+++ b/.agents/automation/reports/frontend-accessibility-auditor.md
@@ -2,31 +2,36 @@
## Purpose
-Audit frontend accessibility for regressions and conformance.
+Audit frontend accessibility for regressions and conformance across web templates and components.
## Execution Result
-No automation execution has been recorded yet. This report is awaiting its first scheduled run.
+`CHANGES_APPLIED`: Identified deterministic accessibility regressions in `WaitlistForm.astro` and `ComposerChannelSelector.vue` and applied minimal evidence-backed fixes.
## Scope Inspected
-Not yet inspected.
+- `apps/web/marketing/src/components/WaitlistForm.astro`
+- `apps/web/app/src/modules/publishing/presentation/components/ComposerChannelSelector.vue`
+- `apps/web/admin/src/views/`
## Changes Applied
-None.
+- Updated `apps/web/marketing/src/components/WaitlistForm.astro` to wrap the waitlist email field label in `
` open near the top of the document.
- const idx = content.indexOf(config.insertAfter)
- if (idx === -1) return content
- const after = idx + config.insertAfter.length
+ const idx = content.indexOf(config.insertAfter);
+ if (idx === -1) return content;
+ const after = idx + config.insertAfter.length;
// Preserve an existing trailing newline if the anchor already has one.
// Slice the remainder from the original anchor offset, not prefix.length:
// in the no-newline case prefix is one char longer than the anchor (the
// appended '\n'), so slicing by prefix.length would drop the first real
// character after the anchor (#227).
- const existingNewline = readLineEndingAt(content, after)
- const prefix = content.slice(0, after) + (existingNewline || lineEnding)
- const rest = content.slice(after + existingNewline.length)
- return prefix + block + rest
+ const existingNewline = readLineEndingAt(content, after);
+ const prefix = content.slice(0, after) + (existingNewline || lineEnding);
+ const rest = content.slice(after + existingNewline.length);
+ return prefix + block + rest;
}
/**
@@ -121,21 +100,21 @@ export function removeTag(content, _syntax) {
const patterns = [
/([ \t]*)[\s\S]*?([ \t]*(?:\r\n|\n|\r|$)?)/,
/([ \t]*)\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}([ \t]*(?:\r\n|\n|\r|$)?)/,
- ]
+ ];
for (const pat of patterns) {
- let changed = false
- let next = content
+ let changed = false;
+ let next = content;
do {
- content = next
+ content = next;
next = content.replace(pat, (_match, leadingIndent, trailing = '') => {
- if (/[\r\n]/.test(trailing)) return leadingIndent
- return leadingIndent || trailing || ''
- })
- if (next !== content) changed = true
- } while (next !== content)
- if (changed) return next
+ if (/[\r\n]/.test(trailing)) return leadingIndent;
+ return leadingIndent || trailing || '';
+ });
+ if (next !== content) changed = true;
+ } while (next !== content);
+ if (changed) return next;
}
- return content
+ return content;
}
// ---------------------------------------------------------------------------
@@ -158,66 +137,66 @@ export function removeTag(content, _syntax) {
// Only the in-source meta-tag form gets the auto-patch.
// ---------------------------------------------------------------------------
-const CSP_MARKER_ATTR = 'data-impeccable-csp-original'
+const CSP_MARKER_ATTR = 'data-impeccable-csp-original';
function findCspMetaTags(content) {
- const out = []
- const tagRe = /]*?)\/?>/gis
- let m
+ const out = [];
+ const tagRe = /]*?)\/?>/gis;
+ let m;
while ((m = tagRe.exec(content)) !== null) {
- const attrs = m[1]
- if (!/(http-equiv|httpEquiv)\s*=\s*(['"])Content-Security-Policy\2/i.test(attrs)) continue
- out.push({ start: m.index, end: m.index + m[0].length, full: m[0], attrs })
+ const attrs = m[1];
+ if (!/(http-equiv|httpEquiv)\s*=\s*(['"])Content-Security-Policy\2/i.test(attrs)) continue;
+ out.push({ start: m.index, end: m.index + m[0].length, full: m[0], attrs });
}
- return out
+ return out;
}
function getAttr(attrs, name) {
- const re = new RegExp(`\\b${name}\\s*=\\s*(['"])([\\s\\S]*?)\\1`, 'i')
- const m = attrs.match(re)
- return m ? { quote: m[1], value: m[2], full: m[0] } : null
+ const re = new RegExp(`\\b${name}\\s*=\\s*(['"])([\\s\\S]*?)\\1`, 'i');
+ const m = attrs.match(re);
+ return m ? { quote: m[1], value: m[2], full: m[0] } : null;
}
function appendOriginToDirective(csp, directive, origin) {
- const re = new RegExp(`(^|;)(\\s*)(${directive})\\s+([^;]*)`, 'i')
- const m = csp.match(re)
+ const re = new RegExp(`(^|;)(\\s*)(${directive})\\s+([^;]*)`, 'i');
+ const m = csp.match(re);
if (m) {
- const tokens = m[4].trim().split(/\s+/)
- if (tokens.includes(origin)) return csp
- return csp.replace(re, `${m[1]}${m[2]}${m[3]} ${[...tokens, origin].join(' ')}`)
+ const tokens = m[4].trim().split(/\s+/);
+ if (tokens.includes(origin)) return csp;
+ return csp.replace(re, `${m[1]}${m[2]}${m[3]} ${[...tokens, origin].join(' ')}`);
}
// Directive missing — add it. Use 'self' + origin so we don't inadvertently
// narrow the policy compared to the default-src fallback (most users with
// an explicit CSP have 'self' there).
- return csp.trim().replace(/;?\s*$/, '') + `; ${directive} 'self' ${origin}`
+ return csp.trim().replace(/;?\s*$/, '') + `; ${directive} 'self' ${origin}`;
}
export function patchCspMeta(content, port) {
- const tags = findCspMetaTags(content)
- if (tags.length === 0) return content
- const origin = `http://localhost:${port}`
+ const tags = findCspMetaTags(content);
+ if (tags.length === 0) return content;
+ const origin = `http://localhost:${port}`;
// Walk last-to-first so prior splices don't invalidate later indices.
- let result = content
+ let result = content;
for (let i = tags.length - 1; i >= 0; i--) {
- const tag = tags[i]
- const attrs = tag.attrs
- if (getAttr(attrs, CSP_MARKER_ATTR)) continue // already patched
- const contentAttr = getAttr(attrs, 'content')
- if (!contentAttr) continue
-
- const original = contentAttr.value
- let patched = original
- patched = appendOriginToDirective(patched, 'script-src', origin)
- patched = appendOriginToDirective(patched, 'connect-src', origin)
+ const tag = tags[i];
+ const attrs = tag.attrs;
+ if (getAttr(attrs, CSP_MARKER_ATTR)) continue; // already patched
+ const contentAttr = getAttr(attrs, 'content');
+ if (!contentAttr) continue;
+
+ const original = contentAttr.value;
+ let patched = original;
+ patched = appendOriginToDirective(patched, 'script-src', origin);
+ patched = appendOriginToDirective(patched, 'connect-src', origin);
// The shader overlay during 'generating' creates a screenshot via
// URL.createObjectURL, producing a `blob:` URL — img-src 'self' rejects
// those. Add `blob:` so the overlay doesn't throw a CSP violation.
- patched = appendOriginToDirective(patched, 'img-src', 'blob:')
- if (patched === original) continue
+ patched = appendOriginToDirective(patched, 'img-src', 'blob:');
+ if (patched === original) continue;
- const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`
- const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`
+ const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
+ const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
// The tagRe captures any whitespace between the last attribute and the
// closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
// a replace would land it BEFORE that trailing space, leaving a double
@@ -225,47 +204,44 @@ export function patchCspMeta(content, port) {
// the trailing whitespace, splice the marker into the attribute body,
// and re-append the original trailing whitespace so a self-closing
// `` round-trips byte-for-byte.
- const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0]
- const attrsBody = attrs.slice(0, attrs.length - trailingWs.length)
- const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs
- const newTag = tag.full.replace(attrs, newAttrs)
+ const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
+ const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
+ const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
+ const newTag = tag.full.replace(attrs, newAttrs);
- result = result.slice(0, tag.start) + newTag + result.slice(tag.end)
+ result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
}
- return result
+ return result;
}
export function revertCspMeta(content) {
- const tags = findCspMetaTags(content)
- if (tags.length === 0) return content
+ const tags = findCspMetaTags(content);
+ if (tags.length === 0) return content;
- let result = content
+ let result = content;
for (let i = tags.length - 1; i >= 0; i--) {
- const tag = tags[i]
- const origAttr = getAttr(tag.attrs, CSP_MARKER_ATTR)
- if (!origAttr) continue
- const contentAttr = getAttr(tag.attrs, 'content')
- if (!contentAttr) continue
-
- let originalValue
- try {
- originalValue = Buffer.from(origAttr.value, 'base64').toString('utf-8')
- } catch {
- continue
- }
-
- const newContentAttr = `content=${contentAttr.quote}${originalValue}${contentAttr.quote}`
- let newAttrs = tag.attrs.replace(contentAttr.full, newContentAttr)
+ const tag = tags[i];
+ const origAttr = getAttr(tag.attrs, CSP_MARKER_ATTR);
+ if (!origAttr) continue;
+ const contentAttr = getAttr(tag.attrs, 'content');
+ if (!contentAttr) continue;
+
+ let originalValue;
+ try { originalValue = Buffer.from(origAttr.value, 'base64').toString('utf-8'); }
+ catch { continue; }
+
+ const newContentAttr = `content=${contentAttr.quote}${originalValue}${contentAttr.quote}`;
+ let newAttrs = tag.attrs.replace(contentAttr.full, newContentAttr);
// Drop the marker attribute and any single space immediately preceding it.
- newAttrs = newAttrs.replace(new RegExp(`\\s*${origAttr.full}`), '')
- const newTag = tag.full.replace(tag.attrs, newAttrs)
+ newAttrs = newAttrs.replace(new RegExp(`\\s*${origAttr.full}`), '');
+ const newTag = tag.full.replace(tag.attrs, newAttrs);
- result = result.slice(0, tag.start) + newTag + result.slice(tag.end)
+ result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
}
- return result
+ return result;
}
/** The journal's undo for a tag-strategy patch: drop the block, restore CSP. */
export function unpatchTagFile(content) {
- return revertCspMeta(removeTag(content))
+ return revertCspMeta(removeTag(content));
}
diff --git a/.agents/skills/impeccable/scripts/live/frameworks/tanstack-start.mjs b/.agents/skills/impeccable/scripts/live/frameworks/tanstack-start.mjs
index d84e594fc..9bfb3db4a 100644
--- a/.agents/skills/impeccable/scripts/live/frameworks/tanstack-start.mjs
+++ b/.agents/skills/impeccable/scripts/live/frameworks/tanstack-start.mjs
@@ -12,34 +12,34 @@ import {
detectTanStackStartProject,
removeTanStackLiveAdapter,
unpatchTanStackRoot,
-} from '../tanstack-adapter.mjs'
+} from '../tanstack-adapter.mjs';
export const tanstackStart = {
name: 'tanstack-start',
detect(cwd) {
- return detectTanStackStartProject(cwd)
+ return detectTanStackStartProject(cwd);
},
inject: {
kind: 'adapter',
apply({ cwd, port, token, project }) {
- return applyTanStackLiveAdapter({ cwd, port, token, project })
+ return applyTanStackLiveAdapter({ cwd, port, token, project });
},
remove({ cwd, project }) {
- return removeTanStackLiveAdapter({ cwd, project })
+ return removeTanStackLiveAdapter({ cwd, project });
},
// The mount component's extension follows the root route's, so the path
// cannot live in the static ignore list.
ignorePatterns(project) {
- return project?.componentFile ? [project.componentFile] : []
+ return project?.componentFile ? [project.componentFile] : [];
},
artifacts({ project }) {
- if (!project) return []
+ if (!project) return [];
return [
{
kind: 'created',
@@ -53,7 +53,7 @@ export const tanstackStart = {
patch: 'tanstack-root',
markers: [TANSTACK_MARKER_OPEN],
},
- ]
+ ];
},
unpatch: {
@@ -67,4 +67,4 @@ export const tanstackStart = {
styleMode: 'scoped',
commentSyntax: 'jsx',
},
-}
+};
diff --git a/.agents/skills/impeccable/scripts/live/frameworks/vite-generic.mjs b/.agents/skills/impeccable/scripts/live/frameworks/vite-generic.mjs
index 2570a570e..4713670f4 100644
--- a/.agents/skills/impeccable/scripts/live/frameworks/vite-generic.mjs
+++ b/.agents/skills/impeccable/scripts/live/frameworks/vite-generic.mjs
@@ -8,27 +8,27 @@
* static-html sits below it.
*/
-import { fileExists, findConfigFile, hasAnyDependency } from './detect-utils.mjs'
+import { fileExists, findConfigFile, hasAnyDependency } from './detect-utils.mjs';
-const VITE_CONFIG_RE = /^vite\.config\.(?:js|mjs|cjs|ts|mts|cts)$/
+const VITE_CONFIG_RE = /^vite\.config\.(?:js|mjs|cjs|ts|mts|cts)$/;
export function detectViteProject(cwd = process.cwd()) {
- const configFile = findConfigFile(cwd, VITE_CONFIG_RE)
- if (configFile) return { configFile, via: 'config' }
- if (hasAnyDependency(cwd, ['vite'])) return { configFile: null, via: 'package' }
+ const configFile = findConfigFile(cwd, VITE_CONFIG_RE);
+ if (configFile) return { configFile, via: 'config' };
+ if (hasAnyDependency(cwd, ['vite'])) return { configFile: null, via: 'package' };
// A zero-config Vite app is index.html + package.json, the same pair
// roots.mjs treats as an app root.
if (fileExists(cwd, 'index.html') && fileExists(cwd, 'package.json')) {
- return { configFile: null, via: 'zero-config' }
+ return { configFile: null, via: 'zero-config' };
}
- return null
+ return null;
}
export const viteGeneric = {
name: 'vite-generic',
detect(cwd) {
- return detectViteProject(cwd)
+ return detectViteProject(cwd);
},
inject: { kind: 'tag' },
@@ -39,4 +39,4 @@ export const viteGeneric = {
styleMode: 'scoped',
commentSyntax: 'jsx',
},
-}
+};
diff --git a/.agents/skills/impeccable/scripts/live/generation-preflight.mjs b/.agents/skills/impeccable/scripts/live/generation-preflight.mjs
index 39477c882..bfe81b32f 100644
--- a/.agents/skills/impeccable/scripts/live/generation-preflight.mjs
+++ b/.agents/skills/impeccable/scripts/live/generation-preflight.mjs
@@ -1,25 +1,25 @@
-import { execFile } from 'node:child_process'
-import path from 'node:path'
-import { promisify } from 'node:util'
+import { execFile } from 'node:child_process';
+import path from 'node:path';
+import { promisify } from 'node:util';
-const execFileAsync = promisify(execFile)
-const PREFLIGHT_TIMEOUT_MS = 15_000
+const execFileAsync = promisify(execFile);
+const PREFLIGHT_TIMEOUT_MS = 15_000;
// Per-target cache of the resolved source file. The wrap search walks the whole
// project tree and was measured at ~7.6s on a large repo; it re-ran on every
// generate for the same picked element (re-rolls, param passes). Keyed by the
// target signature (locator + route), so it invalidates automatically when the
// element or route changes; a failed resolution evicts its entry (see below).
-const sourceResolutionCache = new Map()
+const sourceResolutionCache = new Map();
/** Test/lifecycle hook: drop all cached source resolutions. */
export function clearSourceResolutionCache() {
- sourceResolutionCache.clear()
+ sourceResolutionCache.clear();
}
function targetSignature(event) {
- const isInsert = event.mode === 'insert'
- const target = isInsert ? insertTarget(event) : replaceTarget(event)
+ const isInsert = event.mode === 'insert';
+ const target = isInsert ? insertTarget(event) : replaceTarget(event);
return JSON.stringify({
mode: isInsert ? 'insert' : 'replace',
position: isInsert ? target.position : null,
@@ -27,36 +27,36 @@ function targetSignature(event) {
classes: target.classes || null,
tag: target.tag || null,
pageUrl: event.pageUrl || null,
- })
+ });
}
export function buildGenerationPreflight(event, scriptsDir, { cache = null } = {}) {
- if (!event || event.type !== 'generate' || !event.id) return null
+ if (!event || event.type !== 'generate' || !event.id) return null;
- const isInsert = event.mode === 'insert'
- const target = isInsert ? insertTarget(event) : replaceTarget(event)
- if (!target.elementId && !target.classes) return null
+ const isInsert = event.mode === 'insert';
+ const target = isInsert ? insertTarget(event) : replaceTarget(event);
+ if (!target.elementId && !target.classes) return null;
- const script = path.join(scriptsDir, isInsert ? 'live-insert.mjs' : 'live-wrap.mjs')
- const args = [script, '--id', event.id, '--count', String(event.count || 3)]
+ const script = path.join(scriptsDir, isInsert ? 'live-insert.mjs' : 'live-wrap.mjs');
+ const args = [script, '--id', event.id, '--count', String(event.count || 3)];
// Compute the scaffold but do not write it into source for source-preview
// targets. The agent writes wrapper + variants atomically; a premature
// server-side write reloads the framework and strands the browser at 0/N.
// No-op on the svelte-component path, which never writes the route source.
- args.push('--defer-source-write')
- if (isInsert) args.push('--position', target.position)
- if (target.elementId) args.push('--element-id', target.elementId)
- if (target.classes) args.push('--classes', target.classes)
- if (target.tag) args.push('--tag', target.tag)
- if (target.text) args.push('--text', target.text)
- if (!isInsert && event.pageUrl) args.push('--page-url', event.pageUrl)
- const signature = targetSignature(event)
+ args.push('--defer-source-write');
+ if (isInsert) args.push('--position', target.position);
+ if (target.elementId) args.push('--element-id', target.elementId);
+ if (target.classes) args.push('--classes', target.classes);
+ if (target.tag) args.push('--tag', target.tag);
+ if (target.text) args.push('--text', target.text);
+ if (!isInsert && event.pageUrl) args.push('--page-url', event.pageUrl);
+ const signature = targetSignature(event);
// A cached resolution points the helper straight at the file, skipping the
// tree search. The helper still reads current content, so line ranges stay
// fresh; only discovery is cached.
- const cachedFile = cache ? cache.get(signature) : null
- if (cachedFile) args.push('--file', cachedFile)
- return { script, args, mode: isInsert ? 'insert' : 'replace', signature }
+ const cachedFile = cache ? cache.get(signature) : null;
+ if (cachedFile) args.push('--file', cachedFile);
+ return { script, args, mode: isInsert ? 'insert' : 'replace', signature };
}
/**
@@ -69,82 +69,81 @@ export function buildGenerationPreflight(event, scriptsDir, { cache = null } = {
* server for that entire window: Accept and Discard POSTs, SSE progress
* broadcasts, and every other poll stalled behind it.
*/
-export async function runGenerationPreflight(
- event,
- {
- cwd = process.cwd(),
- scriptsDir,
- execFileImpl = execFileAsync,
- timeoutMs = PREFLIGHT_TIMEOUT_MS,
- cache = sourceResolutionCache,
- } = {},
-) {
- const command = buildGenerationPreflight(event, scriptsDir, { cache })
+export async function runGenerationPreflight(event, {
+ cwd = process.cwd(),
+ scriptsDir,
+ execFileImpl = execFileAsync,
+ timeoutMs = PREFLIGHT_TIMEOUT_MS,
+ cache = sourceResolutionCache,
+} = {}) {
+ const command = buildGenerationPreflight(event, scriptsDir, { cache });
if (!command) {
- return { ok: false, skipped: true, reason: 'insufficient_locator' }
+ return { ok: false, skipped: true, reason: 'insufficient_locator' };
}
- const startedAt = performance.now()
+ const startedAt = performance.now();
try {
const { stdout } = await execFileImpl(process.execPath, command.args, {
cwd,
encoding: 'utf-8',
timeout: timeoutMs,
- })
- const line = String(stdout).trim().split('\n').filter(Boolean).pop()
- if (!line) throw new Error('preflight returned no scaffold metadata')
- const scaffold = JSON.parse(line)
+ });
+ const line = String(stdout).trim().split('\n').filter(Boolean).pop();
+ if (!line) throw new Error('preflight returned no scaffold metadata');
+ const scaffold = JSON.parse(line);
// Cache the resolved SOURCE file (route source, not the svelte manifest) so
// the next generate on this target skips the tree search.
- const resolvedSource = scaffold.sourceFile || scaffold.file
+ const resolvedSource = scaffold.sourceFile || scaffold.file;
if (cache && command.signature && typeof resolvedSource === 'string') {
- cache.set(command.signature, resolvedSource)
+ cache.set(command.signature, resolvedSource);
}
return {
ok: true,
mode: command.mode,
durationMs: performance.now() - startedAt,
scaffold,
- }
+ };
} catch (error) {
// Evict a stale/failed resolution so the next attempt does a full search
// (the element may have moved out of the previously cached file).
- if (cache && command.signature) cache.delete(command.signature)
+ if (cache && command.signature) cache.delete(command.signature);
return {
ok: false,
mode: command.mode,
durationMs: performance.now() - startedAt,
error: compactError(error),
- }
+ };
}
}
function replaceTarget(event) {
- return normalizeTarget(event.element || {})
+ return normalizeTarget(event.element || {});
}
function insertTarget(event) {
return {
...normalizeTarget(event.insert?.anchor || {}),
position: event.insert?.position === 'before' ? 'before' : 'after',
- }
+ };
}
function normalizeTarget(target) {
const classes = Array.isArray(target.classes)
? target.classes.join(' ')
- : String(target.classes || '').trim()
- const text = typeof target.textContent === 'string' ? target.textContent.trim().slice(0, 80) : ''
+ : String(target.classes || '').trim();
+ const text = typeof target.textContent === 'string'
+ ? target.textContent.trim().slice(0, 80)
+ : '';
return {
elementId: target.id || target.elementId || undefined,
classes: classes || undefined,
tag: target.tagName || target.tag || undefined,
text: text || undefined,
- }
+ };
}
function compactError(error) {
- const stderr = error?.stderr ? String(error.stderr).trim() : ''
- const message = stderr.split('\n').filter(Boolean).pop() || error?.message || 'preflight failed'
- return String(message).slice(0, 500)
+ const stderr = error?.stderr ? String(error.stderr).trim() : '';
+ const message = stderr.split('\n').filter(Boolean).pop() || error?.message || 'preflight failed';
+ return String(message).slice(0, 500);
}
diff --git a/.agents/skills/impeccable/scripts/live/insert-ui.mjs b/.agents/skills/impeccable/scripts/live/insert-ui.mjs
index 4b25aa56a..ae54f6f93 100644
--- a/.agents/skills/impeccable/scripts/live/insert-ui.mjs
+++ b/.agents/skills/impeccable/scripts/live/insert-ui.mjs
@@ -3,9 +3,9 @@
* Kept separate from live-browser.js so insert logic is unit-testable.
*/
-export const PLACEHOLDER_DEFAULT_HEIGHT = 80
-export const PLACEHOLDER_MIN_HEIGHT = 48
-export const PLACEHOLDER_MIN_WIDTH = 120
+export const PLACEHOLDER_DEFAULT_HEIGHT = 80;
+export const PLACEHOLDER_MIN_HEIGHT = 48;
+export const PLACEHOLDER_MIN_WIDTH = 120;
/** @typedef {'before' | 'after'} InsertPosition */
/** @typedef {'row' | 'column'} InsertAxis */
@@ -16,22 +16,22 @@ export const PLACEHOLDER_MIN_WIDTH = 120
* @returns {InsertAxis}
*/
export function detectInsertAxisFromStyle(style) {
- const display = style?.display || 'block'
+ const display = style?.display || 'block';
if (display.includes('flex')) {
- const dir = style.flexDirection || 'row'
- return dir.startsWith('row') ? 'row' : 'column'
+ const dir = style.flexDirection || 'row';
+ return dir.startsWith('row') ? 'row' : 'column';
}
if (display === 'grid' || display === 'inline-grid') {
- const flow = style.gridAutoFlow || 'row'
- if (flow.includes('column')) return 'column'
- const cols = (style.gridTemplateColumns || '').trim()
+ const flow = style.gridAutoFlow || 'row';
+ if (flow.includes('column')) return 'column';
+ const cols = (style.gridTemplateColumns || '').trim();
if (cols && cols !== 'none') {
- const colCount = cols.split(/\s+/).filter(Boolean).length
- if (colCount > 1) return 'row'
+ const colCount = cols.split(/\s+/).filter(Boolean).length;
+ if (colCount > 1) return 'row';
}
- return 'row'
+ return 'row';
}
- return 'column'
+ return 'column';
}
/**
@@ -43,17 +43,15 @@ export function detectInsertAxisFromStyle(style) {
* @returns {InsertPosition}
*/
export function computeInsertPosition(clientX, clientY, rect, axis = 'column') {
- if (!rect) return 'after'
+ if (!rect) return 'after';
if (axis === 'row') {
- if (!Number.isFinite(rect.left) || !Number.isFinite(rect.width) || rect.width <= 0)
- return 'after'
- const mid = rect.left + rect.width / 2
- return clientX < mid ? 'before' : 'after'
+ if (!Number.isFinite(rect.left) || !Number.isFinite(rect.width) || rect.width <= 0) return 'after';
+ const mid = rect.left + rect.width / 2;
+ return clientX < mid ? 'before' : 'after';
}
- if (!Number.isFinite(rect.top) || !Number.isFinite(rect.height) || rect.height <= 0)
- return 'after'
- const mid = rect.top + rect.height / 2
- return clientY < mid ? 'before' : 'after'
+ if (!Number.isFinite(rect.top) || !Number.isFinite(rect.height) || rect.height <= 0) return 'after';
+ const mid = rect.top + rect.height / 2;
+ return clientY < mid ? 'before' : 'after';
}
/**
@@ -61,17 +59,18 @@ export function computeInsertPosition(clientX, clientY, rect, axis = 'column') {
* Requires a non-empty prompt OR at least one annotation.
*/
export function canCreateInsert({ prompt, comments, strokes }) {
- const hasPrompt = typeof prompt === 'string' && prompt.trim().length > 0
- const hasComments = Array.isArray(comments) && comments.length > 0
- const hasStrokes =
- Array.isArray(strokes) && strokes.some((s) => Array.isArray(s?.points) && s.points.length >= 2)
- return hasPrompt || hasComments || hasStrokes
+ const hasPrompt = typeof prompt === 'string' && prompt.trim().length > 0;
+ const hasComments = Array.isArray(comments) && comments.length > 0;
+ const hasStrokes = Array.isArray(strokes) && strokes.some(
+ (s) => Array.isArray(s?.points) && s.points.length >= 2,
+ );
+ return hasPrompt || hasComments || hasStrokes;
}
/** Tooltip/title when Create is disabled. */
export function insertCreateDisabledReason({ prompt, comments, strokes }) {
- if (canCreateInsert({ prompt, comments, strokes })) return null
- return 'Add a prompt or annotate the placeholder to create'
+ if (canCreateInsert({ prompt, comments, strokes })) return null;
+ return 'Add a prompt or annotate the placeholder to create';
}
/**
@@ -82,41 +81,41 @@ export function insertCreateDisabledReason({ prompt, comments, strokes }) {
*/
export function insertLineCoords(rect, position, axis = 'column') {
if (axis === 'row') {
- const right = rect.right ?? rect.left + rect.width
- const x = position === 'before' ? rect.left - 2 : right + 2
- return { axis: 'row', top: rect.top, left: x, width: 0, height: rect.height }
+ const right = rect.right ?? rect.left + rect.width;
+ const x = position === 'before' ? rect.left - 2 : right + 2;
+ return { axis: 'row', top: rect.top, left: x, width: 0, height: rect.height };
}
- const bottom = rect.bottom ?? rect.top + rect.height
- const y = position === 'before' ? rect.top - 2 : bottom + 2
- return { axis: 'column', top: y, left: rect.left, width: rect.width, height: 0 }
+ const bottom = rect.bottom ?? rect.top + rect.height;
+ const y = position === 'before' ? rect.top - 2 : bottom + 2;
+ return { axis: 'column', top: y, left: rect.left, width: rect.width, height: 0 };
}
/** Cursor while hovering an insert boundary. */
export function cursorForInsertAxis(axis) {
- return axis === 'row' ? 'ew-resize' : 'ns-resize'
+ return axis === 'row' ? 'ew-resize' : 'ns-resize';
}
function groupSiblingRows(siblings, rowThreshold = 8) {
- const sorted = [...siblings].sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left)
- const rows = []
+ const sorted = [...siblings].sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left);
+ const rows = [];
for (const entry of sorted) {
- let placed = false
+ let placed = false;
for (const row of rows) {
if (Math.abs(entry.rect.top - row[0].rect.top) <= rowThreshold) {
- row.push(entry)
- placed = true
- break
+ row.push(entry);
+ placed = true;
+ break;
}
}
- if (!placed) rows.push([entry])
+ if (!placed) rows.push([entry]);
}
- return rows
+ return rows;
}
function horizontalOverlap(a, b) {
- const left = Math.max(a.left, b.left)
- const right = Math.min(a.right ?? a.left + a.width, b.right ?? b.left + b.width)
- return Math.max(0, right - left)
+ const left = Math.max(a.left, b.left);
+ const right = Math.min(a.right ?? a.left + a.width, b.right ?? b.left + b.width);
+ return Math.max(0, right - left);
}
/**
@@ -127,87 +126,85 @@ function horizontalOverlap(a, b) {
* @param {{ slop?: number, minOverlap?: number }} [opts]
*/
export function hitSiblingInsertGap(clientX, clientY, siblings, opts = {}) {
- if (!Array.isArray(siblings) || siblings.length < 2) return null
- const slop = opts.slop ?? 12
- const minOverlap = opts.minOverlap ?? 0.25
+ if (!Array.isArray(siblings) || siblings.length < 2) return null;
+ const slop = opts.slop ?? 12;
+ const minOverlap = opts.minOverlap ?? 0.25;
for (const row of groupSiblingRows(siblings)) {
- if (row.length < 2) continue
- const sorted = [...row].sort((a, b) => a.rect.left - b.rect.left)
+ if (row.length < 2) continue;
+ const sorted = [...row].sort((a, b) => a.rect.left - b.rect.left);
for (let i = 0; i < sorted.length - 1; i++) {
- const a = sorted[i]
- const b = sorted[i + 1]
- const aRight = a.rect.right ?? a.rect.left + a.rect.width
- const bLeft = b.rect.left
- if (bLeft <= aRight) continue
- const top = Math.max(a.rect.top, b.rect.top)
- const aBottom = a.rect.bottom ?? a.rect.top + a.rect.height
- const bBottom = b.rect.bottom ?? b.rect.top + b.rect.height
- const bottom = Math.min(aBottom, bBottom)
- const span = bottom - top
- const minH = Math.min(a.rect.height, b.rect.height)
- if (span < minH * minOverlap) continue
-
- const inX = clientX >= aRight - slop && clientX <= bLeft + slop
- const inY = clientY >= top - slop && clientY <= bottom + slop
- if (!inX || !inY) continue
-
- const midX = (aRight + bLeft) / 2
+ const a = sorted[i];
+ const b = sorted[i + 1];
+ const aRight = a.rect.right ?? a.rect.left + a.rect.width;
+ const bLeft = b.rect.left;
+ if (bLeft <= aRight) continue;
+ const top = Math.max(a.rect.top, b.rect.top);
+ const aBottom = a.rect.bottom ?? a.rect.top + a.rect.height;
+ const bBottom = b.rect.bottom ?? b.rect.top + b.rect.height;
+ const bottom = Math.min(aBottom, bBottom);
+ const span = bottom - top;
+ const minH = Math.min(a.rect.height, b.rect.height);
+ if (span < minH * minOverlap) continue;
+
+ const inX = clientX >= aRight - slop && clientX <= bLeft + slop;
+ const inY = clientY >= top - slop && clientY <= bottom + slop;
+ if (!inX || !inY) continue;
+
+ const midX = (aRight + bLeft) / 2;
return {
anchor: b.el,
position: 'before',
axis: 'row',
line: { axis: 'row', left: midX, top, width: 0, height: span },
- }
+ };
}
}
- const sortedCol = [...siblings].sort(
- (a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left,
- )
+ const sortedCol = [...siblings].sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left);
for (let i = 0; i < sortedCol.length - 1; i++) {
- const a = sortedCol[i]
- const b = sortedCol[i + 1]
- const overlap = horizontalOverlap(a.rect, b.rect)
- const minW = Math.min(a.rect.width, b.rect.width)
- if (overlap < minW * minOverlap) continue
-
- const aBottom = a.rect.bottom ?? a.rect.top + a.rect.height
- const gapTop = aBottom
- const gapBottom = b.rect.top
- if (gapBottom <= gapTop) continue
-
- const overlapLeft = Math.max(a.rect.left, b.rect.left)
+ const a = sortedCol[i];
+ const b = sortedCol[i + 1];
+ const overlap = horizontalOverlap(a.rect, b.rect);
+ const minW = Math.min(a.rect.width, b.rect.width);
+ if (overlap < minW * minOverlap) continue;
+
+ const aBottom = a.rect.bottom ?? a.rect.top + a.rect.height;
+ const gapTop = aBottom;
+ const gapBottom = b.rect.top;
+ if (gapBottom <= gapTop) continue;
+
+ const overlapLeft = Math.max(a.rect.left, b.rect.left);
const overlapRight = Math.min(
a.rect.right ?? a.rect.left + a.rect.width,
b.rect.right ?? b.rect.left + b.rect.width,
- )
- const inY = clientY >= gapTop - slop && clientY <= gapBottom + slop
- const inX = clientX >= overlapLeft - slop && clientX <= overlapRight + slop
- if (!inY || !inX) continue
+ );
+ const inY = clientY >= gapTop - slop && clientY <= gapBottom + slop;
+ const inX = clientX >= overlapLeft - slop && clientX <= overlapRight + slop;
+ if (!inY || !inX) continue;
- const midY = (gapTop + gapBottom) / 2
+ const midY = (gapTop + gapBottom) / 2;
return {
anchor: b.el,
position: 'before',
axis: 'column',
line: { axis: 'column', top: midY, left: overlapLeft, width: overlap, height: 0 },
- }
+ };
}
- return null
+ return null;
}
/**
* Resolve insert hover target, side, axis, and indicator line for the pointer.
*/
export function resolveInsertHover({ clientX, clientY, target, rect, axis, siblings }) {
- const gap = hitSiblingInsertGap(clientX, clientY, siblings)
- if (gap) return gap
+ const gap = hitSiblingInsertGap(clientX, clientY, siblings);
+ if (gap) return gap;
- const position = computeInsertPosition(clientX, clientY, rect, axis)
- const line = insertLineCoords(rect, position, axis)
- return { anchor: target, position, axis, line }
+ const position = computeInsertPosition(clientX, clientY, rect, axis);
+ const line = insertLineCoords(rect, position, axis);
+ return { anchor: target, position, axis, line };
}
/**
@@ -216,53 +213,54 @@ export function resolveInsertHover({ clientX, clientY, target, rect, axis, sibli
* @returns {{ kind: 'flex', flex: string, minWidth: number } | { kind: 'percent' } | { kind: 'auto' } | { kind: 'explicit', width: number }}
*/
export function placeholderSizing({ axis, parentDisplay, parentWidth, anchorFlex }) {
- const display = parentDisplay || 'block'
- const w = Number.isFinite(parentWidth) ? parentWidth : 0
+ const display = parentDisplay || 'block';
+ const w = Number.isFinite(parentWidth) ? parentWidth : 0;
if (axis === 'row') {
if (display.includes('flex')) {
- const flex =
- anchorFlex && anchorFlex !== 'none' && anchorFlex !== '0 1 auto' ? anchorFlex : '1 1 0'
- return { kind: 'flex', flex, minWidth: 0 }
+ const flex = anchorFlex && anchorFlex !== 'none' && anchorFlex !== '0 1 auto'
+ ? anchorFlex
+ : '1 1 0';
+ return { kind: 'flex', flex, minWidth: 0 };
}
if (display === 'grid' || display === 'inline-grid') {
- return { kind: 'auto' }
+ return { kind: 'auto' };
}
}
if (w >= PLACEHOLDER_MIN_WIDTH) {
- return { kind: 'percent' }
+ return { kind: 'percent' };
}
return {
kind: 'explicit',
width: Math.max(PLACEHOLDER_MIN_WIDTH, w || PLACEHOLDER_MIN_WIDTH),
- }
+ };
}
/** Width kinds that need materializing to px before edge-resize. */
export function placeholderWidthIsImplicit(kind) {
- return kind === 'flex' || kind === 'percent' || kind === 'auto'
+ return kind === 'flex' || kind === 'percent' || kind === 'auto';
}
/**
* Clamp user-resized placeholder dimensions.
*/
export function clampPlaceholderSize(width, height, parentWidth, opts = {}) {
- const minW = opts.minWidth ?? PLACEHOLDER_MIN_WIDTH
- const minH = opts.minHeight ?? PLACEHOLDER_MIN_HEIGHT
- const maxW = opts.maxWidth ?? Math.max(minW, parentWidth || minW)
+ const minW = opts.minWidth ?? PLACEHOLDER_MIN_WIDTH;
+ const minH = opts.minHeight ?? PLACEHOLDER_MIN_HEIGHT;
+ const maxW = opts.maxWidth ?? Math.max(minW, parentWidth || minW);
return {
width: Math.min(maxW, Math.max(minW, Math.round(width))),
height: Math.max(minH, Math.round(height)),
- }
+ };
}
/** CSS cursor for a placeholder edge resize handle. */
export function cursorForPlaceholderEdge(edge) {
- if (edge === 'n' || edge === 's') return 'ns-resize'
- if (edge === 'e' || edge === 'w') return 'ew-resize'
- return 'default'
+ if (edge === 'n' || edge === 's') return 'ns-resize';
+ if (edge === 'e' || edge === 'w') return 'ew-resize';
+ return 'default';
}
/**
@@ -279,22 +277,22 @@ export function resizePlaceholderFromEdge(start, edge, dx, dy, parentWidth, opts
height: start.height,
marginLeft: start.marginLeft ?? 0,
marginTop: start.marginTop ?? 0,
- }
- if (edge === 'e') base.width = start.width + dx
+ };
+ if (edge === 'e') base.width = start.width + dx;
else if (edge === 'w') {
- base.width = start.width - dx
- base.marginLeft = start.marginLeft + dx
- } else if (edge === 's') base.height = start.height + dy
+ base.width = start.width - dx;
+ base.marginLeft = start.marginLeft + dx;
+ } else if (edge === 's') base.height = start.height + dy;
else if (edge === 'n') {
- base.height = start.height - dy
- base.marginTop = start.marginTop + dy
+ base.height = start.height - dy;
+ base.marginTop = start.marginTop + dy;
}
- const clamped = clampPlaceholderSize(base.width, base.height, parentWidth, opts)
+ const clamped = clampPlaceholderSize(base.width, base.height, parentWidth, opts);
if (edge === 'w') {
- base.marginLeft = start.marginLeft + start.width - clamped.width
+ base.marginLeft = start.marginLeft + start.width - clamped.width;
} else if (edge === 'n') {
- base.marginTop = start.marginTop + start.height - clamped.height
+ base.marginTop = start.marginTop + start.height - clamped.height;
}
return {
@@ -302,24 +300,24 @@ export function resizePlaceholderFromEdge(start, edge, dx, dy, parentWidth, opts
height: clamped.height,
marginLeft: Math.round(base.marginLeft),
marginTop: Math.round(base.marginTop),
- }
+ };
}
/** Pick and insert toggles are independent but turning one ON turns the other OFF. */
export function applyPickToggle(pickActive, insertActive) {
- const nextPick = !pickActive
+ const nextPick = !pickActive;
return {
pickActive: nextPick,
insertActive: nextPick ? false : insertActive,
- }
+ };
}
export function applyInsertToggle(pickActive, insertActive) {
- const nextInsert = !insertActive
+ const nextInsert = !insertActive;
return {
pickActive: nextInsert ? false : pickActive,
insertActive: nextInsert,
- }
+ };
}
/**
@@ -349,11 +347,11 @@ export function buildInsertGeneratePayload({
},
placeholder,
freeformPrompt: freeformPrompt?.trim() || undefined,
- }
- if (comments?.length) payload.comments = comments
- if (strokes?.length) payload.strokes = strokes
- if (screenshotPath) payload.screenshotPath = screenshotPath
- return payload
+ };
+ if (comments?.length) payload.comments = comments;
+ if (strokes?.length) payload.strokes = strokes;
+ if (screenshotPath) payload.screenshotPath = screenshotPath;
+ return payload;
}
/**
@@ -361,10 +359,10 @@ export function buildInsertGeneratePayload({
* @param {{ hidden?: boolean, style?: { display?: string } } | null | undefined} el
*/
export function isVariantShown(el) {
- if (!el) return false
- if (el.hidden) return false
- if (el.style?.display === 'none') return false
- return true
+ if (!el) return false;
+ if (el.hidden) return false;
+ if (el.style?.display === 'none') return false;
+ return true;
}
/**
@@ -373,13 +371,13 @@ export function isVariantShown(el) {
* @param {boolean} shown
*/
export function setVariantShown(el, shown) {
- if (!el) return
+ if (!el) return;
if (shown) {
- el.removeAttribute?.('hidden')
- if (el.style) el.style.display = ''
+ el.removeAttribute?.('hidden');
+ if (el.style) el.style.display = '';
} else {
- el.setAttribute?.('hidden', '')
- if (el.style) el.style.display = 'none'
+ el.setAttribute?.('hidden', '');
+ if (el.style) el.style.display = 'none';
}
}
@@ -402,12 +400,12 @@ export function resolveInsertSessionAnchor(opts) {
placeholder,
insertAnchor,
pickVariantContent,
- } = opts || {}
+ } = opts || {};
if (wrapper && variantCount > 0 && visibleVariant > 0 && pickVariantContent) {
- const vis = pickVariantContent(wrapper, visibleVariant)
- if (vis) return vis
+ const vis = pickVariantContent(wrapper, visibleVariant);
+ if (vis) return vis;
}
- return placeholder || insertAnchor || null
+ return placeholder || insertAnchor || null;
}
/**
@@ -435,7 +433,7 @@ export function buildInsertPlaceholderSnapshot(anchor, placeholder, { position,
anchorTag: anchor.tagName || 'DIV',
anchorClasses: anchor.className || '',
anchorText: (anchor.textContent || '').trim().slice(0, 120),
- }
+ };
}
/**
@@ -445,16 +443,16 @@ export function buildInsertPlaceholderSnapshot(anchor, placeholder, { position,
* @param {Element | null | undefined} liveAnchor
*/
export function findInsertAnchorInDom(doc, snapshot, liveAnchor = null) {
- if (liveAnchor && doc.body.contains(liveAnchor)) return liveAnchor
- if (!snapshot) return null
- const tag = (snapshot.anchorTag || 'div').toLowerCase()
- const cls = (snapshot.anchorClasses || '').split(/\s+/).filter(Boolean)[0]
- const needle = snapshot.anchorText || ''
- const sel = cls ? `${tag}.${cls}` : tag
- const candidates = doc.querySelectorAll(sel)
+ if (liveAnchor && doc.body.contains(liveAnchor)) return liveAnchor;
+ if (!snapshot) return null;
+ const tag = (snapshot.anchorTag || 'div').toLowerCase();
+ const cls = (snapshot.anchorClasses || '').split(/\s+/).filter(Boolean)[0];
+ const needle = snapshot.anchorText || '';
+ const sel = cls ? `${tag}.${cls}` : tag;
+ const candidates = doc.querySelectorAll(sel);
for (const candidate of candidates) {
- if (needle && !(candidate.textContent || '').includes(needle.slice(0, 40))) continue
- return candidate
+ if (needle && !(candidate.textContent || '').includes(needle.slice(0, 40))) continue;
+ return candidate;
}
- return null
+ return null;
}
diff --git a/.agents/skills/impeccable/scripts/live/instructions.mjs b/.agents/skills/impeccable/scripts/live/instructions.mjs
index 3a282cfa7..19f6a1ae3 100644
--- a/.agents/skills/impeccable/scripts/live/instructions.mjs
+++ b/.agents/skills/impeccable/scripts/live/instructions.mjs
@@ -15,144 +15,128 @@
* hand-maintained doc can.
*/
-const PLAN_POINTER =
- 'Plan per live.md section 4: extract the identity lock, pick default vs departure mode, commit each variant to a DIFFERENT primary axis, squint-test the trio. Size parameter knobs per section 7 budgets.'
+const PLAN_POINTER = 'Plan per live.md section 4: extract the identity lock, pick default vs departure mode, commit each variant to a DIFFERENT primary axis, squint-test the trio. Size parameter knobs per section 7 budgets.';
function pollCmd(scriptsPath) {
- return `node ${scriptsPath}/live-poll.mjs`
+ return `node ${scriptsPath}/live-poll.mjs`;
}
function replyCmd(scriptsPath, id, rest) {
- return `${pollCmd(scriptsPath)} --reply ${id} ${rest}`
+ return `${pollCmd(scriptsPath)} --reply ${id} ${rest}`;
}
export function instructionsForEvent(event, { scriptsPath = '{{scripts_path}}' } = {}) {
- if (!event || typeof event !== 'object') return undefined
+ if (!event || typeof event !== 'object') return undefined;
switch (event.type) {
case 'generate':
- return generateInstructions(event, scriptsPath)
+ return generateInstructions(event, scriptsPath);
case 'steer':
- return `Do what the message asks (page edits, navigation help, or a short answer). Then reply exactly once: ${replyCmd(scriptsPath, event.id, 'steer_done ["optional short toast"]')} (on failure: --reply ${event.id} error "Short reason"). No pickup ack; poll again immediately after.`
+ return `Do what the message asks (page edits, navigation help, or a short answer). Then reply exactly once: ${replyCmd(scriptsPath, event.id, 'steer_done ["optional short toast"]')} (on failure: --reply ${event.id} error "Short reason"). No pickup ack; poll again immediately after.`;
case 'prefetch':
- return `Speculative pre-read, no reply owed: resolve ${JSON.stringify(event.pageUrl || '/')} to its source file (root "/" is usually the boot's pageFile; multi-page sites map /foo to public/foo/index.html; SPAs map all routes to one entry), read it into context, then poll again. Skip if you cannot resolve it confidently.`
+ return `Speculative pre-read, no reply owed: resolve ${JSON.stringify(event.pageUrl || '/')} to its source file (root "/" is usually the boot's pageFile; multi-page sites map /foo to public/foo/index.html; SPAs map all routes to one entry), read it into context, then poll again. Skip if you cannot resolve it confidently.`;
case 'variant_mount_failed':
- return `The browser could NOT render variant ${event.variant}${event.url ? ` (module: ${event.url})` : ''}${event.error ? `: ${String(event.error).slice(0, 200)}` : ''}. The user sees a persistent error card, not variants. Fix the variant source files, then reply ${replyCmd(scriptsPath, event.id, 'done --file ')}; the browser retries on its own. Poll again after the reply.`
+ return `The browser could NOT render variant ${event.variant}${event.url ? ` (module: ${event.url})` : ''}${event.error ? `: ${String(event.error).slice(0, 200)}` : ''}. The user sees a persistent error card, not variants. Fix the variant source files, then reply ${replyCmd(scriptsPath, event.id, 'done --file ')}; the browser retries on its own. Poll again after the reply.`;
case 'accept':
- return acceptInstructions(event, scriptsPath)
+ return acceptInstructions(event, scriptsPath);
case 'discard':
return event?._completionAck?.ok === true
? 'Original restored and durable completion acknowledged; nothing to do. Poll again.'
- : `Completion was not acknowledged: run node ${scriptsPath}/live-complete.mjs --id ${event.id} --discarded, then poll again.`
+ : `Completion was not acknowledged: run node ${scriptsPath}/live-complete.mjs --id ${event.id} --discarded, then poll again.`;
case 'manual_edit_apply':
- return `The user already clicked Apply; never ask, discard, or redirect. Delegate the source edits to the impeccable_manual_edit_applier subagent when available (pass cwd, scripts path, event id, page URL, chunk/deadline, batch, evidencePath); it must not poll or reply. ${event.repair ? 'A `repair` payload is present: the previous Apply changed source but validation failed; fix the CURRENT source, never roll back yourself. ' : ''}Reply exactly once: ${replyCmd(scriptsPath, event.id, `done --data '{"status":"done","appliedEntryIds":[...],"failed":[],"files":[...],"notes":[]}'`)} (status "partial"/"error" with failed[] when not every entry applied). Then poll again.`
+ return `The user already clicked Apply; never ask, discard, or redirect. Delegate the source edits to the impeccable_manual_edit_applier subagent when available (pass cwd, scripts path, event id, page URL, chunk/deadline, batch, evidencePath); it must not poll or reply. ${event.repair ? 'A `repair` payload is present: the previous Apply changed source but validation failed; fix the CURRENT source, never roll back yourself. ' : ''}Reply exactly once: ${replyCmd(scriptsPath, event.id, `done --data '{"status":"done","appliedEntryIds":[...],"failed":[],"files":[...],"notes":[]}'`)} (status "partial"/"error" with failed[] when not every entry applied). Then poll again.`;
case 'timeout':
- return 'No event arrived; poll again immediately.'
+ return 'No event arrived; poll again immediately.';
case 'exit':
- return `Session over: kill any background poll, then node ${scriptsPath}/live-server.mjs stop (removes the injected script tag). Sweep leftover impeccable-variants-start / impeccable-carbonize-start markers from source.`
+ return `Session over: kill any background poll, then node ${scriptsPath}/live-server.mjs stop (removes the injected script tag). Sweep leftover impeccable-variants-start / impeccable-carbonize-start markers from source.`;
default:
- return undefined
+ return undefined;
}
}
function generateInstructions(event, scriptsPath) {
- const id = event.id
- const scaffold = event.scaffold
- const steps = []
+ const id = event.id;
+ const scaffold = event.scaffold;
+ const steps = [];
if (event.screenshotPath) {
- steps.push(
- `Read the annotated screenshot first: ${event.screenshotPath}. Comment {x,y} positions bind text to the child under that point; strokes read by shape (loop = emphasis on this thing, arrow = direction, cross = delete).`,
- )
+ steps.push(`Read the annotated screenshot first: ${event.screenshotPath}. Comment {x,y} positions bind text to the child under that point; strokes read by shape (loop = emphasis on this thing, arrow = direction, cross = delete).`);
} else {
- steps.push(
- 'No screenshot was sent (the user did not annotate); do not ask for one and do not screenshot the page. Work from element.outerHTML, the computed styles, and the prompt.',
- )
+ steps.push('No screenshot was sent (the user did not annotate); do not ask for one and do not screenshot the page. Work from element.outerHTML, the computed styles, and the prompt.');
}
if (event.mode === 'insert') {
- steps.push(insertScaffoldInstructions(event, scriptsPath))
+ steps.push(insertScaffoldInstructions(event, scriptsPath));
} else if (scaffold?.previewMode === 'svelte-component') {
- steps.push(svelteComponentInstructions(event, scaffold, scriptsPath))
+ steps.push(svelteComponentInstructions(event, scaffold, scriptsPath));
} else if (scaffold && scaffold.sourceWritten === false) {
- steps.push(deferredWrapperInstructions(event, scaffold, scriptsPath))
+ steps.push(deferredWrapperInstructions(event, scaffold, scriptsPath));
} else if (scaffold) {
- steps.push(
- `The wrapper is already written into ${scaffold.file}. Splice preview CSS plus all ${event.count} variants at line ${scaffold.insertLine} in ONE edit, following the returned cssAuthoring contract (styleTag, selector strategy, forbidden patterns). Each variant div holds exactly ONE top-level element (same tag as the original); first visible, others display: none.`,
- )
+ steps.push(`The wrapper is already written into ${scaffold.file}. Splice preview CSS plus all ${event.count} variants at line ${scaffold.insertLine} in ONE edit, following the returned cssAuthoring contract (styleTag, selector strategy, forbidden patterns). Each variant div holds exactly ONE top-level element (same tag as the original); first visible, others display: none.`);
} else {
- steps.push(
- `Preflight could not scaffold${event.scaffoldError ? ` (${event.scaffoldError})` : ''}. Run node ${scriptsPath}/live-wrap.mjs --id ${id} --count ${event.count} --element-id "${event.element?.id || ''}" --classes "${(event.element?.classes || []).join(',')}" --tag "${event.element?.tagName || ''}" --text "". Keep the flags separate; --text disambiguates repeated siblings. On a fallback error, follow live.md's Handle fallback.`,
- )
+ steps.push(`Preflight could not scaffold${event.scaffoldError ? ` (${event.scaffoldError})` : ''}. Run node ${scriptsPath}/live-wrap.mjs --id ${id} --count ${event.count} --element-id "${event.element?.id || ''}" --classes "${(event.element?.classes || []).join(',')}" --tag "${event.element?.tagName || ''}" --text "". Keep the flags separate; --text disambiguates repeated siblings. On a fallback error, follow live.md's Handle fallback.`);
}
- steps.push(
- event.action && event.action !== 'impeccable'
- ? `Action is "${event.action}": read reference/${event.action}.md before planning; its MUST params are non-negotiable. ${PLAN_POINTER}`
- : `Freeform action: work from SKILL.md rules plus craft-floor.md; no sub-command file. ${PLAN_POINTER}`,
- )
+ steps.push(event.action && event.action !== 'impeccable'
+ ? `Action is "${event.action}": read reference/${event.action}.md before planning; its MUST params are non-negotiable. ${PLAN_POINTER}`
+ : `Freeform action: work from SKILL.md rules plus craft-floor.md; no sub-command file. ${PLAN_POINTER}`);
- steps.push(
- `When all ${event.count} variants are delivered: ${replyCmd(scriptsPath, id, 'done --file ')}. Then poll again. If generation fails after the browser flipped to GENERATING, reply --reply ${id} error "Short reason" so the bar resets (never live-accept --discard for this).`,
- )
+ steps.push(`When all ${event.count} variants are delivered: ${replyCmd(scriptsPath, id, 'done --file ')}. Then poll again. If generation fails after the browser flipped to GENERATING, reply --reply ${id} error "Short reason" so the bar resets (never live-accept --discard for this).`);
- return steps.map((s, i) => `${i + 1}. ${s}`).join('\n')
+ return steps.map((s, i) => `${i + 1}. ${s}`).join('\n');
}
function svelteComponentInstructions(event, scaffold, scriptsPath) {
- const dir = scaffold.componentDir
- const count = event.count
- return `Svelte component preview. EDIT the existing stubs ${dir}/v1.svelte ... v${count}.svelte in place; never delete or recreate them; do not read them back (the prop-substituted markup is in scaffold.componentStubMarkup). Keep the stub's control flow ({#each}, {#if}) and propContract prop names exactly; never flatten a loop into literal items. The stub \n`
+ const propsComment = contract.length > 0
+ ? `\n\n`
+ : '';
+ return `${buildPropsScript(contract)}${propsComment}${originalWithProps.trim()}\n\n\n`;
}
function buildInsertVariantStub(variantNum) {
- return `${buildPropsScript([])}Insert variant ${variantNum}
\n\n\n`
+ return `${buildPropsScript([])}Insert variant ${variantNum}
\n\n\n`;
}
/**
@@ -190,37 +191,34 @@ export function scaffoldSvelteComponentSession({
originalLines,
cwd = process.cwd(),
}) {
- const originalMarkup = originalLines.join('\n')
+ const originalMarkup = originalLines.join('\n');
- const compiler = loadSvelteCompiler(cwd)
+ const compiler = loadSvelteCompiler(cwd);
if (!compiler) {
- return {
- fallback: 'source-preview',
- reason: 'svelte 5 compiler not resolvable from the app root',
- }
+ return { fallback: 'source-preview', reason: 'svelte 5 compiler not resolvable from the app root' };
}
- const analysis = analyzeSvelteMarkup(originalMarkup, compiler.parse)
+ const analysis = analyzeSvelteMarkup(originalMarkup, compiler.parse);
if (!analysis.ok) {
- return { fallback: 'source-preview', reason: analysis.reason }
+ return { fallback: 'source-preview', reason: analysis.reason };
}
- ensureRuntimeHelper(cwd)
- const dir = componentSessionDir(id, cwd)
- fs.mkdirSync(dir, { recursive: true })
+ ensureRuntimeHelper(cwd);
+ const dir = componentSessionDir(id, cwd);
+ fs.mkdirSync(dir, { recursive: true });
- const contract = analysis.contract
+ const contract = analysis.contract;
const seeded = extractMatchingSourceCss(
safeReadSource(path.resolve(cwd, sourceFile)),
originalMarkup,
- )
- const seededCss = seeded.css
+ );
+ const seededCss = seeded.css;
// The preview compiles in isolation, so NONE of these source rules applied
// to what the user approved. Accept enforces that preview truth: any of
// them the variant does not re-declare is superseded and removed, instead
// of re-attaching to the accepted markup through kept class names (the
// ".decisions grid grabs the new board" failure). Only the CLASS-matched
// selectors are candidates; tag rules style shared route elements.
- const seededSelectors = [...seeded.supersedable]
+ const seededSelectors = [...seeded.supersedable];
const manifest = {
id,
@@ -242,22 +240,14 @@ export function scaffoldSvelteComponentSession({
runtimeModuleAbs: path.join(cwd, SVELTE_RUNTIME_FILE).split(path.sep).join('/'),
probeModule: `/${SVELTE_PROBE_FILE}`,
probeModuleAbs: path.join(cwd, SVELTE_PROBE_FILE).split(path.sep).join('/'),
- }
+ };
- fs.writeFileSync(
- path.join(dir, 'manifest.json'),
- JSON.stringify(manifest, null, 2) + '\n',
- 'utf-8',
- )
+ fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
for (let n = 1; n <= count; n++) {
- const variantFile = path.join(dir, `v${n}.svelte`)
+ const variantFile = path.join(dir, `v${n}.svelte`);
if (!fs.existsSync(variantFile)) {
- fs.writeFileSync(
- variantFile,
- buildVariantStubV2(n, analysis.markupWithProps, contract, seededCss),
- 'utf-8',
- )
+ fs.writeFileSync(variantFile, buildVariantStubV2(n, analysis.markupWithProps, contract, seededCss), 'utf-8');
}
}
@@ -271,19 +261,15 @@ export function scaffoldSvelteComponentSession({
// the manifest and stub files (or deleting and recreating them).
stubMarkup: analysis.markupWithProps,
seededCss,
- }
+ };
}
function safeReadSource(filePath) {
- try {
- return fs.readFileSync(filePath, 'utf-8')
- } catch {
- return ''
- }
+ try { return fs.readFileSync(filePath, 'utf-8'); } catch { return ''; }
}
function escapeSelectorToken(token) {
- return String(token).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+ return String(token).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
@@ -299,77 +285,69 @@ function escapeSelectorToken(token) {
* candidates for removal.
*/
export function extractMatchingSourceCss(routeSource, originalMarkup) {
- const empty = { css: '', supersedable: new Set() }
- const styleMatch = String(routeSource || '').match(/\n`
- : `\n\n`
- return `${buildPropsScriptV2(contract)}${propsComment}${markupWithProps.trim()}\n${css}`
+ ? `\n\n`
+ : `\n\n`;
+ return `${buildPropsScriptV2(contract)}${propsComment}${markupWithProps.trim()}\n${css}`;
}
export function scaffoldSvelteComponentInsertSession({
@@ -383,11 +361,11 @@ export function scaffoldSvelteComponentInsertSession({
anchorLines,
cwd = process.cwd(),
}) {
- ensureRuntimeHelper(cwd)
- const dir = componentSessionDir(id, cwd)
- fs.mkdirSync(dir, { recursive: true })
+ ensureRuntimeHelper(cwd);
+ const dir = componentSessionDir(id, cwd);
+ fs.mkdirSync(dir, { recursive: true });
- const anchorMarkup = (anchorLines || []).join('\n')
+ const anchorMarkup = (anchorLines || []).join('\n');
const manifest = {
id,
mode: 'insert',
@@ -407,18 +385,14 @@ export function scaffoldSvelteComponentInsertSession({
runtimeModuleAbs: path.join(cwd, SVELTE_RUNTIME_FILE).split(path.sep).join('/'),
probeModule: `/${SVELTE_PROBE_FILE}`,
probeModuleAbs: path.join(cwd, SVELTE_PROBE_FILE).split(path.sep).join('/'),
- }
+ };
- fs.writeFileSync(
- path.join(dir, 'manifest.json'),
- JSON.stringify(manifest, null, 2) + '\n',
- 'utf-8',
- )
+ fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
for (let n = 1; n <= count; n++) {
- const variantFile = path.join(dir, `v${n}.svelte`)
+ const variantFile = path.join(dir, `v${n}.svelte`);
if (!fs.existsSync(variantFile)) {
- fs.writeFileSync(variantFile, buildInsertVariantStub(n), 'utf-8')
+ fs.writeFileSync(variantFile, buildInsertVariantStub(n), 'utf-8');
}
}
@@ -427,307 +401,267 @@ export function scaffoldSvelteComponentInsertSession({
manifestFile: path.relative(cwd, path.join(dir, 'manifest.json')).split(path.sep).join('/'),
componentDir: manifest.componentDir,
propContract: [],
- }
+ };
}
export function findSvelteComponentManifest(id, cwd = process.cwd()) {
- const direct = manifestPathForSession(id, cwd)
+ const direct = manifestPathForSession(id, cwd);
if (fs.existsSync(direct)) {
- return readManifest(direct)
+ return readManifest(direct);
}
// Legacy location: a session scaffolded by an older version can still be
// accepted after an upgrade.
- const legacyDirect = path.join(cwd, LEGACY_SVELTE_COMPONENT_ROOT, id, 'manifest.json')
+ const legacyDirect = path.join(cwd, LEGACY_SVELTE_COMPONENT_ROOT, id, 'manifest.json');
if (fs.existsSync(legacyDirect)) {
- return readManifest(legacyDirect)
+ return readManifest(legacyDirect);
}
for (const rootRel of [SVELTE_COMPONENT_ROOT, LEGACY_SVELTE_COMPONENT_ROOT]) {
- const root = path.join(cwd, rootRel)
- if (!fs.existsSync(root)) continue
+ const root = path.join(cwd, rootRel);
+ if (!fs.existsSync(root)) continue;
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
- if (!entry.isDirectory()) continue
- const candidate = path.join(root, entry.name, 'manifest.json')
- if (!fs.existsSync(candidate)) continue
+ if (!entry.isDirectory()) continue;
+ const candidate = path.join(root, entry.name, 'manifest.json');
+ if (!fs.existsSync(candidate)) continue;
try {
- const manifest = readManifest(candidate)
- if (manifest?.id === id) return { ...manifest, manifestPath: candidate }
- } catch {
- /* skip */
- }
+ const manifest = readManifest(candidate);
+ if (manifest?.id === id) return { ...manifest, manifestPath: candidate };
+ } catch { /* skip */ }
}
}
- return null
+ return null;
}
export function readManifest(manifestPath) {
- const data = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
+ const data = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
return {
...data,
manifestPath,
- }
+ };
}
export function resolveSourceFile(sourceFile, cwd = process.cwd()) {
if (!sourceFile || path.isAbsolute(sourceFile)) {
- throw new Error('Invalid svelte-component source file')
+ throw new Error('Invalid svelte-component source file');
}
- const full = path.resolve(cwd, sourceFile)
- const rel = path.relative(cwd, full)
+ const full = path.resolve(cwd, sourceFile);
+ const rel = path.relative(cwd, full);
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
- throw new Error('Svelte-component source file escapes project root')
+ throw new Error('Svelte-component source file escapes project root');
}
if (!fs.existsSync(full)) {
- throw new Error('Svelte-component source file not found: ' + sourceFile)
+ throw new Error('Svelte-component source file not found: ' + sourceFile);
}
- return full
+ return full;
}
function appendCssToSvelteStyle(lines, cssLines) {
- const closeIdx = findLastStyleCloseLine(lines)
- const prepared = [
- '',
- ...cssLines.map((line) => (line.trim() === '' ? '' : ' ' + line.trimStart())),
- ]
+ const closeIdx = findLastStyleCloseLine(lines);
+ const prepared = ['', ...cssLines.map((line) => (line.trim() === '' ? '' : ' ' + line.trimStart()))];
if (closeIdx === -1) {
- return [...lines, '', '']
+ return [...lines, '', ''];
}
- return [...lines.slice(0, closeIdx), ...prepared, ...lines.slice(closeIdx)]
+ return [
+ ...lines.slice(0, closeIdx),
+ ...prepared,
+ ...lines.slice(closeIdx),
+ ];
}
function findLastStyleCloseLine(lines) {
for (let i = lines.length - 1; i >= 0; i--) {
- if (/<\/style\s*>/.test(lines[i])) return i
+ if (/<\/style\s*>/.test(lines[i])) return i;
}
- return -1
+ return -1;
}
function bakeParamValuesInCss(cssLines, paramValues) {
- if (!paramValues || Object.keys(paramValues).length === 0) return cssLines
+ if (!paramValues || Object.keys(paramValues).length === 0) return cssLines;
return cssLines.map((line) => {
- let out = line
+ let out = line;
for (const [key, value] of Object.entries(paramValues)) {
- const varName = `--p-${key}`
- out = out.replace(
- new RegExp(`var\\(${escapeRegExp(varName)}(?:,\\s*[^)]+)?\\)`, 'g'),
- String(value),
- )
+ const varName = `--p-${key}`;
+ out = out.replace(new RegExp(`var\\(${escapeRegExp(varName)}(?:,\\s*[^)]+)?\\)`, 'g'), String(value));
}
- return out
- })
+ return out;
+ });
}
function sanitizeAcceptedSvelteCss(cssLines, variantNum, paramValues = null, rootTag = 'div') {
- const css = String((cssLines || []).join('\n'))
- if (!/data-impeccable-variant|impeccable-variant-ready/.test(css)) return cssLines
+ const css = String((cssLines || []).join('\n'));
+ if (!/data-impeccable-variant|impeccable-variant-ready/.test(css)) return cssLines;
- const rules = parseCssRules(css)
- const output = []
+ const rules = parseCssRules(css);
+ const output = [];
for (const rule of rules) {
- appendSanitizedCssRule(output, rule, variantNum, paramValues, rootTag)
+ appendSanitizedCssRule(output, rule, variantNum, paramValues, rootTag);
}
- return output
- .join('\n')
+ return output.join('\n')
.split('\n')
.map((line) => line.trimEnd())
- .filter((line) => line.trim() !== '')
+ .filter((line) => line.trim() !== '');
}
function appendSanitizedCssRule(output, rule, variantNum, paramValues, rootTag) {
- const prelude = rule.prelude.trim()
- const body = rule.body.trim()
- if (!prelude || !body || /--impeccable-variant-ready\s*:/.test(body)) return
+ const prelude = rule.prelude.trim();
+ const body = rule.body.trim();
+ if (!prelude || !body || /--impeccable-variant-ready\s*:/.test(body)) return;
if (/^@scope\b/i.test(prelude)) {
- if (/data-impeccable-variant/.test(prelude) && !selectorHasVariant(prelude, variantNum)) return
- const inner = parseCssRules(body)
+ if (/data-impeccable-variant/.test(prelude) && !selectorHasVariant(prelude, variantNum)) return;
+ const inner = parseCssRules(body);
for (const innerRule of inner) {
- const rewrittenPrelude = rewriteAcceptedSvelteSelector(
- innerRule.prelude,
- variantNum,
- paramValues,
- rootTag,
- true,
- )
- if (!rewrittenPrelude || /--impeccable-variant-ready\s*:/.test(innerRule.body)) continue
- output.push(formatCssRule(rewrittenPrelude, innerRule.body.trim()))
+ const rewrittenPrelude = rewriteAcceptedSvelteSelector(innerRule.prelude, variantNum, paramValues, rootTag, true);
+ if (!rewrittenPrelude || /--impeccable-variant-ready\s*:/.test(innerRule.body)) continue;
+ output.push(formatCssRule(rewrittenPrelude, innerRule.body.trim()));
}
- return
+ return;
}
- const rewrittenPrelude = rewriteAcceptedSvelteSelector(
- prelude,
- variantNum,
- paramValues,
- rootTag,
- false,
- )
- if (!rewrittenPrelude) return
- output.push(formatCssRule(rewrittenPrelude, body))
+ const rewrittenPrelude = rewriteAcceptedSvelteSelector(prelude, variantNum, paramValues, rootTag, false);
+ if (!rewrittenPrelude) return;
+ output.push(formatCssRule(rewrittenPrelude, body));
}
function parseCssRules(css) {
- const rules = []
- const text = String(css || '')
- let i = 0
+ const rules = [];
+ const text = String(css || '');
+ let i = 0;
while (i < text.length) {
- while (i < text.length && /\s/.test(text[i])) i++
- const preludeStart = i
- while (i < text.length && text[i] !== '{') i++
- if (i >= text.length) break
- const prelude = text.slice(preludeStart, i).trim()
- i++
- const bodyStart = i
- let depth = 1
- let quote = null
- let comment = false
+ while (i < text.length && /\s/.test(text[i])) i++;
+ const preludeStart = i;
+ while (i < text.length && text[i] !== '{') i++;
+ if (i >= text.length) break;
+ const prelude = text.slice(preludeStart, i).trim();
+ i++;
+ const bodyStart = i;
+ let depth = 1;
+ let quote = null;
+ let comment = false;
while (i < text.length && depth > 0) {
- const ch = text[i]
- const next = text[i + 1]
+ const ch = text[i];
+ const next = text[i + 1];
if (comment) {
if (ch === '*' && next === '/') {
- comment = false
- i += 2
- continue
+ comment = false;
+ i += 2;
+ continue;
}
- i++
- continue
+ i++;
+ continue;
}
if (quote) {
if (ch === '\\') {
- i += 2
- continue
+ i += 2;
+ continue;
}
- if (ch === quote) quote = null
- i++
- continue
+ if (ch === quote) quote = null;
+ i++;
+ continue;
}
if (ch === '/' && next === '*') {
- comment = true
- i += 2
- continue
+ comment = true;
+ i += 2;
+ continue;
}
if (ch === '"' || ch === "'") {
- quote = ch
- i++
- continue
+ quote = ch;
+ i++;
+ continue;
}
- if (ch === '{') depth++
- else if (ch === '}') depth--
- i++
+ if (ch === '{') depth++;
+ else if (ch === '}') depth--;
+ i++;
}
- const body = text.slice(bodyStart, Math.max(bodyStart, i - 1))
- if (prelude) rules.push({ prelude, body })
+ const body = text.slice(bodyStart, Math.max(bodyStart, i - 1));
+ if (prelude) rules.push({ prelude, body });
}
- return rules
+ return rules;
}
function rewriteAcceptedSvelteSelector(prelude, variantNum, paramValues, rootTag, fromScope) {
- const selectors = splitSelectorList(prelude)
- const rewritten = []
+ const selectors = splitSelectorList(prelude);
+ const rewritten = [];
for (const selector of selectors) {
- const next = rewriteAcceptedSvelteSelectorPart(
- selector,
- variantNum,
- paramValues,
- rootTag,
- fromScope,
- )
- if (next) rewritten.push(next)
+ const next = rewriteAcceptedSvelteSelectorPart(selector, variantNum, paramValues, rootTag, fromScope);
+ if (next) rewritten.push(next);
}
- return rewritten.join(', ')
+ return rewritten.join(', ');
}
function rewriteAcceptedSvelteSelectorPart(selector, variantNum, paramValues, rootTag, fromScope) {
- let out = selector.trim()
- const hasVariant = /data-impeccable-variant/.test(out)
- if (hasVariant && !selectorHasVariant(out, variantNum)) return ''
+ let out = selector.trim();
+ const hasVariant = /data-impeccable-variant/.test(out);
+ if (hasVariant && !selectorHasVariant(out, variantNum)) return '';
if (hasVariant) {
- out = out.replace(variantSelectorRegex(variantNum), '')
- out = out.replace(/\[data-impeccable-variant=(["']).*?\1\]/g, '')
+ out = out.replace(variantSelectorRegex(variantNum), '');
+ out = out.replace(/\[data-impeccable-variant=(["']).*?\1\]/g, '');
}
- const paramResult = rewriteParamSelectors(out, paramValues)
- if (!paramResult.keep) return ''
- out = paramResult.selector
+ const paramResult = rewriteParamSelectors(out, paramValues);
+ if (!paramResult.keep) return '';
+ out = paramResult.selector;
out = out
.replace(/:scope(?:\[[^\]]+\])?\s*>\s*/g, '')
.replace(/:scope(?:\[[^\]]+\])?/g, rootTag || '')
.replace(/\s+/g, ' ')
- .trim()
+ .trim();
- out = out.replace(/^[>+~]\s*/, '').trim()
- if (!out && (hasVariant || fromScope)) return rootTag || ':global(*)'
- return out
+ out = out.replace(/^[>+~]\s*/, '').trim();
+ if (!out && (hasVariant || fromScope)) return rootTag || ':global(*)';
+ return out;
}
function rewriteParamSelectors(selector, paramValues) {
- let keep = true
- const next = selector.replace(
- /\[data-p-([A-Za-z0-9_-]+)(?:=(["'])(.*?)\2)?\]/g,
- (_match, key, _quote, expected) => {
- if (!paramValues || !Object.hasOwn(paramValues, key)) return ''
- const actual = paramValues[key]
- if (expected != null && String(actual) !== String(expected)) {
- keep = false
- return ''
- }
- if (
- expected == null &&
- (actual === false ||
- actual == null ||
- actual === 'false' ||
- actual === 'off' ||
- actual === '0')
- ) {
- keep = false
- return ''
- }
- return ''
- },
- )
- return { keep, selector: next }
+ let keep = true;
+ const next = selector.replace(/\[data-p-([A-Za-z0-9_-]+)(?:=(["'])(.*?)\2)?\]/g, (_match, key, _quote, expected) => {
+ if (!paramValues || !Object.prototype.hasOwnProperty.call(paramValues, key)) return '';
+ const actual = paramValues[key];
+ if (expected != null && String(actual) !== String(expected)) {
+ keep = false;
+ return '';
+ }
+ if (expected == null && (actual === false || actual == null || actual === 'false' || actual === 'off' || actual === '0')) {
+ keep = false;
+ return '';
+ }
+ return '';
+ });
+ return { keep, selector: next };
}
+
function selectorHasVariant(selector, variantNum) {
- return variantSelectorRegex(variantNum).test(selector)
+ return variantSelectorRegex(variantNum).test(selector);
}
function variantSelectorRegex(variantNum) {
- return new RegExp(
- `\\[data-impeccable-variant=(["'])${escapeRegExp(String(variantNum))}\\1\\]`,
- 'g',
- )
+ return new RegExp(`\\[data-impeccable-variant=(["'])${escapeRegExp(String(variantNum))}\\1\\]`, 'g');
}
function formatCssRule(selector, body) {
- return `${selector} { ${body.trim()} }`
+ return `${selector} { ${body.trim()} }`;
}
function escapeRegExp(value) {
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
-export function inlineSvelteComponentAccept(
- manifest,
- variantNum,
- paramValues = null,
- cwd = process.cwd(),
-) {
- const sourceFile = resolveSourceFile(manifest.sourceFile, cwd)
- const variantPath = path.join(cwd, manifest.componentDir, `v${variantNum}.svelte`)
+export function inlineSvelteComponentAccept(manifest, variantNum, paramValues = null, cwd = process.cwd()) {
+ const sourceFile = resolveSourceFile(manifest.sourceFile, cwd);
+ const variantPath = path.join(cwd, manifest.componentDir, `v${variantNum}.svelte`);
const resultBase = {
file: manifest.sourceFile,
sourceFile: manifest.sourceFile,
previewMode: 'svelte-component',
componentDir: manifest.componentDir,
carbonize: false,
- }
+ };
if (!fs.existsSync(variantPath)) {
- return { handled: false, error: `Variant ${variantNum} not found`, ...resultBase }
+ return { handled: false, error: `Variant ${variantNum} not found`, ...resultBase };
}
- const { markup, cssLines } = parseSvelteComponentFile(fs.readFileSync(variantPath, 'utf-8'))
+ const { markup, cssLines } = parseSvelteComponentFile(fs.readFileSync(variantPath, 'utf-8'));
if (manifest.mode === 'insert') {
return inlineSvelteComponentInsertAccept({
manifest,
@@ -738,82 +672,72 @@ export function inlineSvelteComponentAccept(
sourceFile,
resultBase,
cwd,
- })
+ });
}
- const rootTag = matchOpeningTag(markup)?.tag || 'div'
- const contract = manifest.propContract || []
- const compiler = loadSvelteCompiler(cwd)
- const mergedMarkup = mergeOriginalTopLevelAttrs(markup, manifest.originalMarkup || '')
+ const rootTag = matchOpeningTag(markup)?.tag || 'div';
+ const contract = manifest.propContract || [];
+ const compiler = loadSvelteCompiler(cwd);
+ const mergedMarkup = mergeOriginalTopLevelAttrs(markup, manifest.originalMarkup || '');
// Restore props back to route expressions. Contract v2 restores through the
// AST so a prop used without braces (each headers, attribute positions)
// still maps back to its original expression; v1 falls back to the textual
// placeholder swap.
- let restoredText
+ let restoredText;
if (Number(manifest.contractVersion) === 2 && compiler) {
- const restored = restoreSvelteMarkup(mergedMarkup, contract, compiler.parse)
+ const restored = restoreSvelteMarkup(mergedMarkup, contract, compiler.parse);
if (!restored.ok) {
- return {
- handled: false,
- error: 'Accepted variant does not parse: ' + restored.reason,
- ...resultBase,
- }
+ return { handled: false, error: 'Accepted variant does not parse: ' + restored.reason, ...resultBase };
}
- restoredText = restored.markup
+ restoredText = restored.markup;
} else {
- restoredText = substitutePropsWithExprs(mergedMarkup, contract)
+ restoredText = substitutePropsWithExprs(mergedMarkup, contract);
}
- const restoredMarkup = restoredText.split('\n').map((line) => line.trimEnd())
-
- const sourceContent = fs.readFileSync(sourceFile, 'utf-8')
- const sourceLines = sourceContent.split('\n')
- const start = Number(manifest.sourceStartLine) - 1
- const end = Number(manifest.sourceEndLine) - 1
- if (
- !Number.isInteger(start) ||
- !Number.isInteger(end) ||
- start < 0 ||
- end < start ||
- end >= sourceLines.length
- ) {
- return {
- handled: false,
- error: 'Invalid source line range for ' + manifest.sourceFile,
- ...resultBase,
- }
+ const restoredMarkup = restoredText.split('\n').map((line) => line.trimEnd());
+
+ const sourceContent = fs.readFileSync(sourceFile, 'utf-8');
+ const sourceLines = sourceContent.split('\n');
+ const start = Number(manifest.sourceStartLine) - 1;
+ const end = Number(manifest.sourceEndLine) - 1;
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end >= sourceLines.length) {
+ return { handled: false, error: 'Invalid source line range for ' + manifest.sourceFile, ...resultBase };
}
- const indent = sourceLines[start].match(/^(\s*)/)?.[1] || ''
- const indentedMarkup = reindentPreservingStructure(restoredMarkup, indent)
+ const indent = sourceLines[start].match(/^(\s*)/)?.[1] || '';
+ const indentedMarkup = reindentPreservingStructure(restoredMarkup, indent);
- let newLines = [...sourceLines.slice(0, start), ...indentedMarkup, ...sourceLines.slice(end + 1)]
+ let newLines = [
+ ...sourceLines.slice(0, start),
+ ...indentedMarkup,
+ ...sourceLines.slice(end + 1),
+ ];
// Selectors that were already unused before this accept are the user's
// pre-existing code; the pruning pass must not touch them.
- const preUnused = compiler ? collectUnusedSelectors(sourceContent, compiler.compile) : new Set()
+ const preUnused = compiler ? collectUnusedSelectors(sourceContent, compiler.compile) : new Set();
// Bake params (declared kinds from params.json drive branch pruning), then
// MERGE into the component's existing style block: matching selectors are
// replaced, new ones appended. Appending alone is how superseded rules used
// to survive their own replacement.
- const declaredParams = readDeclaredParams(manifest, variantNum, cwd)
- let variantCss = cssLines.join('\n')
+ const declaredParams = readDeclaredParams(manifest, variantNum, cwd);
+ let variantCss = cssLines.join('\n');
if (/data-impeccable-variant|impeccable-variant-ready/.test(variantCss)) {
// Defensive: strip preview-wrapper selectors that authoring rules forbid
// on this path but an off-spec agent may still emit.
- variantCss = sanitizeAcceptedSvelteCss(cssLines, variantNum, paramValues, rootTag).join('\n')
+ variantCss = sanitizeAcceptedSvelteCss(cssLines, variantNum, paramValues, rootTag).join('\n');
}
- const bakedCss = bakeParamValues(variantCss, declaredParams, paramValues || {})
- const cssStats = { replaced: 0, appended: 0, pruned: [], superseded: [] }
+ const bakedCss = bakeParamValues(variantCss, declaredParams, paramValues || {});
+ const cssStats = { replaced: 0, appended: 0, pruned: [], superseded: [] };
if (bakedCss.trim()) {
- const merged = mergeCssIntoSvelteSource(newLines.join('\n'), bakedCss)
- newLines = merged.text.split('\n')
- cssStats.replaced = merged.replaced
- cssStats.appended = merged.appended
+ const merged = mergeCssIntoSvelteSource(newLines.join('\n'), bakedCss);
+ newLines = merged.text.split('\n');
+ cssStats.replaced = merged.replaced;
+ cssStats.appended = merged.appended;
}
- let finalText = newLines.join('\n')
+ let finalText = newLines.join('\n');
// Preview truth: the detached preview never applied the source rules that
// styled the replaced selection, so the user approved a design without
@@ -828,42 +752,39 @@ export function inlineSvelteComponentAccept(
// region; deleting it breaks the rest of the route. Keep it.
const outsideMarkup = [...sourceLines.slice(0, start), ...sourceLines.slice(end + 1)]
.join('\n')
- .replace(/`
+ const nodes = transform(parseStylesheet(lastMatch[1]));
+ if (removed.length === 0) return { text, removed };
+ const openTag = lastMatch[0].slice(0, lastMatch[0].indexOf('>') + 1);
+ const rebuilt = `${openTag}\n${serializeNodes(nodes).split('\n').map((l) => (l.trim() ? ' ' + l : '')).join('\n')}\n`;
return {
- text:
- text.slice(0, lastMatch.index) + rebuilt + text.slice(lastMatch.index + lastMatch[0].length),
+ text: text.slice(0, lastMatch.index) + rebuilt + text.slice(lastMatch.index + lastMatch[0].length),
removed,
- }
+ };
}
export function findLostSelectors(beforeSource, afterSource, prunedSelectors = []) {
- const before = collectAllSelectors(styleBlockText(beforeSource))
- const after = collectAllSelectors(styleBlockText(afterSource))
- const pruned = new Set((prunedSelectors || []).map((s) => normalizeSelector(s)))
- const lost = []
+ const before = collectAllSelectors(styleBlockText(beforeSource));
+ const after = collectAllSelectors(styleBlockText(afterSource));
+ const pruned = new Set((prunedSelectors || []).map((s) => normalizeSelector(s)));
+ const lost = [];
for (const selector of before) {
- if (!after.has(selector) && !pruned.has(selector)) lost.push(selector)
+ if (!after.has(selector) && !pruned.has(selector)) lost.push(selector);
}
- return lost
+ return lost;
}
function readDeclaredParams(manifest, variantNum, cwd) {
try {
- const raw = JSON.parse(
- fs.readFileSync(path.join(cwd, manifest.componentDir, 'params.json'), 'utf-8'),
- )
- const list = raw?.[String(variantNum)]
- return Array.isArray(list) ? list : []
+ const raw = JSON.parse(fs.readFileSync(path.join(cwd, manifest.componentDir, 'params.json'), 'utf-8'));
+ const list = raw?.[String(variantNum)];
+ return Array.isArray(list) ? list : [];
} catch {
- return []
+ return [];
}
}
@@ -995,40 +909,37 @@ function readDeclaredParams(manifest, variantNum, cwd) {
* absent), replacing rules whose selectors match and appending the rest.
*/
export function mergeCssIntoSvelteSource(sourceText, incomingCss) {
- const text = String(sourceText || '')
- const styleRe = /\n`,
replaced,
appended,
- }
+ };
}
- const inner = lastMatch[1]
- const { css, replaced, appended } = reconcileCss(inner, incomingCss)
- const openTag = lastMatch[0].slice(0, lastMatch[0].indexOf('>') + 1)
- const replacedBlock = `${openTag}\n${indentCssBlock(css)}\n`
+ const inner = lastMatch[1];
+ const { css, replaced, appended } = reconcileCss(inner, incomingCss);
+ const openTag = lastMatch[0].slice(0, lastMatch[0].indexOf('>') + 1);
+ const replacedBlock = `${openTag}\n${indentCssBlock(css)}\n`;
return {
- text:
- text.slice(0, lastMatch.index) +
- replacedBlock +
- text.slice(lastMatch.index + lastMatch[0].length),
+ text: text.slice(0, lastMatch.index) + replacedBlock + text.slice(lastMatch.index + lastMatch[0].length),
replaced,
appended,
- }
+ };
}
function indentCssBlock(css) {
return String(css || '')
.split('\n')
.map((line) => (line.trim() === '' ? '' : ' ' + line))
- .join('\n')
+ .join('\n');
}
function inlineSvelteComponentInsertAccept({
@@ -1042,65 +953,57 @@ function inlineSvelteComponentInsertAccept({
cwd,
}) {
if (!svelteMarkupHasVisibleContent(markup)) {
- return { handled: false, error: 'Accepted Svelte insert variant is empty', ...resultBase }
+ return { handled: false, error: 'Accepted Svelte insert variant is empty', ...resultBase };
}
if (/\bdata-impeccable-[\w-]*\s*=/.test(markup)) {
- return {
- handled: false,
- error: 'Accepted Svelte insert variant contains preview-only data-impeccable attributes',
- ...resultBase,
- }
+ return { handled: false, error: 'Accepted Svelte insert variant contains preview-only data-impeccable attributes', ...resultBase };
}
- const rootTag = matchOpeningTag(markup)?.tag || 'div'
+ const rootTag = matchOpeningTag(markup)?.tag || 'div';
const restoredMarkup = String(markup || '')
.split('\n')
- .map((line) => line.trimEnd())
- const sourceContent = fs.readFileSync(sourceFile, 'utf-8')
- const sourceLines = sourceContent.split('\n')
- const insertIndex = Number(manifest.insertLine) - 1
+ .map((line) => line.trimEnd());
+ const sourceContent = fs.readFileSync(sourceFile, 'utf-8');
+ const sourceLines = sourceContent.split('\n');
+ const insertIndex = Number(manifest.insertLine) - 1;
if (!Number.isInteger(insertIndex) || insertIndex < 0 || insertIndex > sourceLines.length) {
- return {
- handled: false,
- error: 'Invalid insert line for ' + manifest.sourceFile,
- ...resultBase,
- }
+ return { handled: false, error: 'Invalid insert line for ' + manifest.sourceFile, ...resultBase };
}
- const nearbyLine = sourceLines[insertIndex] ?? sourceLines[insertIndex - 1] ?? ''
- const indent = nearbyLine.match(/^(\s*)/)?.[1] || ''
- const indentedMarkup = reindentPreservingStructure(restoredMarkup, indent)
+ const nearbyLine = sourceLines[insertIndex] ?? sourceLines[insertIndex - 1] ?? '';
+ const indent = nearbyLine.match(/^(\s*)/)?.[1] || '';
+ const indentedMarkup = reindentPreservingStructure(restoredMarkup, indent);
let newLines = [
...sourceLines.slice(0, insertIndex),
...indentedMarkup,
...sourceLines.slice(insertIndex),
- ]
+ ];
- let variantCss = cssLines.join('\n')
+ let variantCss = cssLines.join('\n');
if (/data-impeccable-variant|impeccable-variant-ready/.test(variantCss)) {
- variantCss = sanitizeAcceptedSvelteCss(cssLines, variantNum, paramValues, rootTag).join('\n')
+ variantCss = sanitizeAcceptedSvelteCss(cssLines, variantNum, paramValues, rootTag).join('\n');
}
- const declaredParams = readDeclaredParams(manifest, variantNum, cwd)
- const bakedCss = bakeParamValues(variantCss, declaredParams, paramValues || {})
+ const declaredParams = readDeclaredParams(manifest, variantNum, cwd);
+ const bakedCss = bakeParamValues(variantCss, declaredParams, paramValues || {});
if (bakedCss.trim()) {
- const merged = mergeCssIntoSvelteSource(newLines.join('\n'), bakedCss)
- newLines = merged.text.split('\n')
+ const merged = mergeCssIntoSvelteSource(newLines.join('\n'), bakedCss);
+ newLines = merged.text.split('\n');
}
try {
- fs.writeFileSync(sourceFile, newLines.join('\n'), 'utf-8')
+ fs.writeFileSync(sourceFile, newLines.join('\n'), 'utf-8');
} catch (err) {
- return { handled: false, error: 'Failed to write Svelte source: ' + err.message, ...resultBase }
+ return { handled: false, error: 'Failed to write Svelte source: ' + err.message, ...resultBase };
}
- removeSvelteComponentSession(manifest.id, cwd)
+ removeSvelteComponentSession(manifest.id, cwd);
- const verify = verifyAcceptedSource(newLines.join('\n'))
+ const verify = verifyAcceptedSource(newLines.join('\n'));
return {
handled: true,
verify,
...resultBase,
- }
+ };
}
function svelteMarkupHasVisibleContent(markup) {
@@ -1110,56 +1013,51 @@ function svelteMarkupHasVisibleContent(markup) {
.replace(//g, '')
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
- .trim()
- if (text.length > 0) return true
- return /<(img|svg|canvas|video|audio|picture|input|button|select|textarea)\b/i.test(markup || '')
+ .trim();
+ if (text.length > 0) return true;
+ return /<(img|svg|canvas|video|audio|picture|input|button|select|textarea)\b/i.test(markup || '');
}
function mergeOriginalTopLevelAttrs(markup, originalMarkup) {
- const variantOpen = matchOpeningTag(markup)
- const originalOpen = matchOpeningTag(originalMarkup)
- if (!variantOpen || !originalOpen) return markup
- if (variantOpen.tag.toLowerCase() !== originalOpen.tag.toLowerCase()) return markup
-
- const variantAttrs = parseAttrSegments(variantOpen.attrs)
- const originalAttrs = parseAttrSegments(originalOpen.attrs)
- const additions = []
- let attrs = variantOpen.attrs
-
- const originalClass = originalAttrs.get('class')
- const variantClass = variantAttrs.get('class')
+ const variantOpen = matchOpeningTag(markup);
+ const originalOpen = matchOpeningTag(originalMarkup);
+ if (!variantOpen || !originalOpen) return markup;
+ if (variantOpen.tag.toLowerCase() !== originalOpen.tag.toLowerCase()) return markup;
+
+ const variantAttrs = parseAttrSegments(variantOpen.attrs);
+ const originalAttrs = parseAttrSegments(originalOpen.attrs);
+ const additions = [];
+ let attrs = variantOpen.attrs;
+
+ const originalClass = originalAttrs.get('class');
+ const variantClass = variantAttrs.get('class');
if (originalClass && variantClass) {
- const merged = mergeStaticClassAttr(originalClass, variantClass)
+ const merged = mergeStaticClassAttr(originalClass, variantClass);
if (merged) {
- attrs = attrs.slice(0, variantClass.start) + merged + attrs.slice(variantClass.end)
- variantAttrs.set('class', { ...variantClass, raw: merged })
+ attrs = attrs.slice(0, variantClass.start) + merged + attrs.slice(variantClass.end);
+ variantAttrs.set('class', { ...variantClass, raw: merged });
}
} else if (originalClass && !variantClass) {
- additions.push(originalClass.raw)
+ additions.push(originalClass.raw);
}
for (const [name, attr] of originalAttrs) {
- if (name === 'class') continue
- if (!variantAttrs.has(name)) additions.push(attr.raw)
+ if (name === 'class') continue;
+ if (!variantAttrs.has(name)) additions.push(attr.raw);
}
- if (additions.length === 0 && attrs === variantOpen.attrs) return markup
- const nextOpen =
- variantOpen.prefix +
- variantOpen.tag +
- attrs +
- additions.map((attr) => ' ' + attr.trim()).join('') +
- variantOpen.close
- return (
- markup.slice(0, variantOpen.index) +
- nextOpen +
- markup.slice(variantOpen.index + variantOpen.raw.length)
- )
+ if (additions.length === 0 && attrs === variantOpen.attrs) return markup;
+ const nextOpen = variantOpen.prefix
+ + variantOpen.tag
+ + attrs
+ + additions.map((attr) => ' ' + attr.trim()).join('')
+ + variantOpen.close;
+ return markup.slice(0, variantOpen.index) + nextOpen + markup.slice(variantOpen.index + variantOpen.raw.length);
}
function matchOpeningTag(markup) {
- const match = String(markup || '').match(/^(\s*<)([A-Za-z][\w:-]*)([^>]*?)(\/?>)/)
- if (!match) return null
+ const match = String(markup || '').match(/^(\s*<)([A-Za-z][\w:-]*)([^>]*?)(\/?>)/);
+ if (!match) return null;
return {
raw: match[0],
prefix: match[1],
@@ -1167,44 +1065,43 @@ function matchOpeningTag(markup) {
attrs: match[3] || '',
close: match[4],
index: match.index || 0,
- }
+ };
}
function parseAttrSegments(attrs) {
- const out = new Map()
- const re = /([A-Za-z_:][\w:.-]*)(?:\s*=\s*(?:"[^"]*"|'[^']*'|\{[^}]*\}|[^\s"'>=]+))?/g
- let match
+ const out = new Map();
+ const re = /([A-Za-z_:][\w:.-]*)(?:\s*=\s*(?:"[^"]*"|'[^']*'|\{[^}]*\}|[^\s"'>=]+))?/g;
+ let match;
while ((match = re.exec(attrs))) {
- const raw = match[0]
- const name = match[1]
+ const raw = match[0];
+ const name = match[1];
out.set(name, {
name,
raw,
start: match.index,
end: match.index + raw.length,
- })
+ });
}
- return out
+ return out;
}
function mergeStaticClassAttr(originalClass, variantClass) {
- const originalValue = originalClass.raw.match(/class\s*=\s*(["'])(.*?)\1/)
- const variantValue = variantClass.raw.match(/class\s*=\s*(["'])(.*?)\1/)
- if (!originalValue || !variantValue) return null
- const quote = variantValue[1]
- const classes = [...variantValue[2].split(/\s+/), ...originalValue[2].split(/\s+/)].filter(
- Boolean,
- )
- return `class=${quote}${[...new Set(classes)].join(' ')}${quote}`
+ const originalValue = originalClass.raw.match(/class\s*=\s*(["'])(.*?)\1/);
+ const variantValue = variantClass.raw.match(/class\s*=\s*(["'])(.*?)\1/);
+ if (!originalValue || !variantValue) return null;
+ const quote = variantValue[1];
+ const classes = [
+ ...variantValue[2].split(/\s+/),
+ ...originalValue[2].split(/\s+/),
+ ].filter(Boolean);
+ return `class=${quote}${[...new Set(classes)].join(' ')}${quote}`;
}
export function removeSvelteComponentSession(id, cwd = process.cwd()) {
- const dir = componentSessionDir(id, cwd)
+ const dir = componentSessionDir(id, cwd);
try {
- fs.rmSync(dir, { recursive: true, force: true })
- } catch {
- /* non-fatal */
- }
+ fs.rmSync(dir, { recursive: true, force: true });
+ } catch { /* non-fatal */ }
}
/**
@@ -1216,37 +1113,30 @@ export function removeSvelteComponentSession(id, cwd = process.cwd()) {
* agent-side fix with the exact file and line.
*/
export function compileCheckVariants(id, cwd = process.cwd()) {
- const manifest = findSvelteComponentManifest(id, cwd)
- if (!manifest || !manifest.manifestPath) return { ok: true, failures: [], checked: 0 }
- const compiler = loadSvelteCompiler(cwd)
- if (!compiler || typeof compiler.compile !== 'function')
- return { ok: true, failures: [], checked: 0 }
- const sessionDir = path.dirname(manifest.manifestPath)
- const failures = []
- let checked = 0
- let entries = []
- try {
- entries = fs.readdirSync(sessionDir)
- } catch {
- return { ok: true, failures: [], checked: 0 }
- }
+ const manifest = findSvelteComponentManifest(id, cwd);
+ if (!manifest || !manifest.manifestPath) return { ok: true, failures: [], checked: 0 };
+ const compiler = loadSvelteCompiler(cwd);
+ if (!compiler || typeof compiler.compile !== 'function') return { ok: true, failures: [], checked: 0 };
+ const sessionDir = path.dirname(manifest.manifestPath);
+ const failures = [];
+ let checked = 0;
+ let entries = [];
+ try { entries = fs.readdirSync(sessionDir); } catch { return { ok: true, failures: [], checked: 0 }; }
for (const name of entries) {
- if (!/^v\d+\.svelte$/.test(name)) continue
- checked++
+ if (!/^v\d+\.svelte$/.test(name)) continue;
+ checked++;
try {
- compiler.compile(fs.readFileSync(path.join(sessionDir, name), 'utf-8'), { generate: false })
+ compiler.compile(fs.readFileSync(path.join(sessionDir, name), 'utf-8'), { generate: false });
} catch (err) {
failures.push({
file: `${manifest.componentDir}/${name}`,
line: err?.start?.line ?? null,
column: err?.start?.column ?? null,
- message: String(err?.message || err)
- .split('\n')[0]
- .slice(0, 300),
- })
+ message: String(err?.message || err).split('\n')[0].slice(0, 300),
+ });
}
}
- return { ok: failures.length === 0, failures, checked }
+ return { ok: failures.length === 0, failures, checked };
}
/**
@@ -1256,47 +1146,39 @@ export function compileCheckVariants(id, cwd = process.cwd()) {
* so the dev server can never serve a stale compile of a republished file.
*/
export function bumpSvelteComponentPreviewRevision(id, cwd = process.cwd()) {
- const manifest = findSvelteComponentManifest(id, cwd)
- if (!manifest || !manifest.manifestPath) return null
- const sessionDir = path.dirname(manifest.manifestPath)
- const revision = Number(manifest.revision || 0) + 1
- const revDirName = `r${revision}`
- const revDir = path.join(sessionDir, revDirName)
+ const manifest = findSvelteComponentManifest(id, cwd);
+ if (!manifest || !manifest.manifestPath) return null;
+ const sessionDir = path.dirname(manifest.manifestPath);
+ const revision = Number(manifest.revision || 0) + 1;
+ const revDirName = `r${revision}`;
+ const revDir = path.join(sessionDir, revDirName);
try {
- fs.mkdirSync(revDir, { recursive: true })
- let entries = []
- try {
- entries = fs.readdirSync(sessionDir, { withFileTypes: true })
- } catch {
- /* empty */
- }
+ fs.mkdirSync(revDir, { recursive: true });
+ let entries = [];
+ try { entries = fs.readdirSync(sessionDir, { withFileTypes: true }); } catch { /* empty */ }
for (const entry of entries) {
- if (!entry.isFile()) continue
- if (entry.name === 'manifest.json') continue
- fs.copyFileSync(path.join(sessionDir, entry.name), path.join(revDir, entry.name))
+ if (!entry.isFile()) continue;
+ if (entry.name === 'manifest.json') continue;
+ fs.copyFileSync(path.join(sessionDir, entry.name), path.join(revDir, entry.name));
}
// Previous revision dirs are dead the moment a new one exists.
for (const entry of entries) {
if (entry.isDirectory() && /^r\d+$/.test(entry.name) && entry.name !== revDirName) {
- try {
- fs.rmSync(path.join(sessionDir, entry.name), { recursive: true, force: true })
- } catch {
- /* non-fatal */
- }
+ try { fs.rmSync(path.join(sessionDir, entry.name), { recursive: true, force: true }); } catch { /* non-fatal */ }
}
}
- const relSessionDir = path.relative(cwd, sessionDir).split(path.sep).join('/')
+ const relSessionDir = path.relative(cwd, sessionDir).split(path.sep).join('/');
const updated = {
...manifest,
revision,
revisionDir: `${relSessionDir}/${revDirName}`,
revisionDirAbs: revDir.split(path.sep).join('/'),
- }
- delete updated.manifestPath
- fs.writeFileSync(manifest.manifestPath, JSON.stringify(updated, null, 2) + '\n', 'utf-8')
- return { revision, revisionDir: updated.revisionDir }
+ };
+ delete updated.manifestPath;
+ fs.writeFileSync(manifest.manifestPath, JSON.stringify(updated, null, 2) + '\n', 'utf-8');
+ return { revision, revisionDir: updated.revisionDir };
} catch {
- return null
+ return null;
}
}
@@ -1310,13 +1192,11 @@ export function bumpSvelteComponentPreviewRevision(id, cwd = process.cwd()) {
*/
export function removeAllSvelteComponentSessions(cwd = process.cwd()) {
for (const rootRel of [SVELTE_COMPONENT_ROOT, LEGACY_SVELTE_COMPONENT_ROOT]) {
- const root = path.join(cwd, rootRel)
- if (!fs.existsSync(root)) continue
+ const root = path.join(cwd, rootRel);
+ if (!fs.existsSync(root)) continue;
try {
- fs.rmSync(root, { recursive: true, force: true })
- } catch {
- /* non-fatal */
- }
+ fs.rmSync(root, { recursive: true, force: true });
+ } catch { /* non-fatal */ }
}
}
@@ -1330,121 +1210,113 @@ export function removeAllSvelteComponentSessions(cwd = process.cwd()) {
* @returns {{ removed: string[], removedRoot: boolean, kept: string[] }}
*/
export function sweepInactiveSvelteComponentSessions(activeIds = [], cwd = process.cwd()) {
- const result = { removed: [], removedRoot: false, kept: [] }
- const active = new Set()
+ const result = { removed: [], removedRoot: false, kept: [] };
+ const active = new Set();
for (const id of activeIds || []) {
- if (typeof id === 'string' && id) active.add(id)
+ if (typeof id === 'string' && id) active.add(id);
}
for (const rootRel of [SVELTE_COMPONENT_ROOT, LEGACY_SVELTE_COMPONENT_ROOT]) {
- const root = path.join(cwd, rootRel)
- if (!fs.existsSync(root)) continue
+ const root = path.join(cwd, rootRel);
+ if (!fs.existsSync(root)) continue;
- let entries
+ let entries;
try {
- entries = fs.readdirSync(root, { withFileTypes: true })
+ entries = fs.readdirSync(root, { withFileTypes: true });
} catch {
- continue
+ continue;
}
- let keptHere = 0
+ let keptHere = 0;
for (const entry of entries) {
- if (!entry.isDirectory()) continue
- if (entry.name.startsWith('__')) continue
+ if (!entry.isDirectory()) continue;
+ if (entry.name.startsWith('__')) continue;
if (active.has(entry.name)) {
- result.kept.push(entry.name)
- keptHere++
- continue
+ result.kept.push(entry.name);
+ keptHere++;
+ continue;
}
try {
- fs.rmSync(path.join(root, entry.name), { recursive: true, force: true })
- result.removed.push(entry.name)
+ fs.rmSync(path.join(root, entry.name), { recursive: true, force: true });
+ result.removed.push(entry.name);
} catch {
// Could not remove it, so it still occupies the tree; treat it as kept
// so the parent directory is not torn out from under it.
- result.kept.push(entry.name)
- keptHere++
+ result.kept.push(entry.name);
+ keptHere++;
}
}
if (keptHere === 0) {
try {
- fs.rmSync(root, { recursive: true, force: true })
- result.removedRoot = true
- } catch {
- /* non-fatal */
- }
+ fs.rmSync(root, { recursive: true, force: true });
+ result.removedRoot = true;
+ } catch { /* non-fatal */ }
}
}
- return result
+ return result;
}
export function deferredAcceptsPath(cwd = process.cwd()) {
- const key = createHash('sha256').update(path.resolve(cwd)).digest('hex').slice(0, 16)
- return path.join(os.tmpdir(), 'impeccable-live', key, 'deferred-svelte-component-accepts.json')
+ const key = createHash('sha256').update(path.resolve(cwd)).digest('hex').slice(0, 16);
+ return path.join(os.tmpdir(), 'impeccable-live', key, 'deferred-svelte-component-accepts.json');
}
export function readDeferredAccepts(cwd = process.cwd()) {
- const file = deferredAcceptsPath(cwd)
+ const file = deferredAcceptsPath(cwd);
try {
- return JSON.parse(fs.readFileSync(file, 'utf-8'))
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
} catch {
- return { accepts: [] }
+ return { accepts: [] };
}
}
export function writeDeferredAccept(entry, cwd = process.cwd()) {
- const file = deferredAcceptsPath(cwd)
- fs.mkdirSync(path.dirname(file), { recursive: true })
- const data = readDeferredAccepts(cwd)
- data.accepts = (data.accepts || []).filter((item) => item.id !== entry.id)
- data.accepts.push({ ...entry, createdAt: new Date().toISOString() })
- fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n', 'utf-8')
+ const file = deferredAcceptsPath(cwd);
+ fs.mkdirSync(path.dirname(file), { recursive: true });
+ const data = readDeferredAccepts(cwd);
+ data.accepts = (data.accepts || []).filter((item) => item.id !== entry.id);
+ data.accepts.push({ ...entry, createdAt: new Date().toISOString() });
+ fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n', 'utf-8');
}
export function applyDeferredSvelteComponentAccepts(cwd = process.cwd()) {
- const file = deferredAcceptsPath(cwd)
- const data = readDeferredAccepts(cwd)
- const pending = Array.isArray(data.accepts) ? data.accepts : []
- const results = []
- const remaining = []
+ const file = deferredAcceptsPath(cwd);
+ const data = readDeferredAccepts(cwd);
+ const pending = Array.isArray(data.accepts) ? data.accepts : [];
+ const results = [];
+ const remaining = [];
for (const entry of pending) {
try {
- const manifest = findSvelteComponentManifest(entry.id, cwd)
+ const manifest = findSvelteComponentManifest(entry.id, cwd);
if (!manifest) {
- results.push({ id: entry.id, ok: false, error: 'manifest not found' })
- remaining.push(entry)
- continue
+ results.push({ id: entry.id, ok: false, error: 'manifest not found' });
+ remaining.push(entry);
+ continue;
}
const result = inlineSvelteComponentAccept(
manifest,
entry.variantNum,
entry.paramValues || null,
cwd,
- )
- results.push({ id: entry.id, ok: result.handled !== false, result })
- if (result.handled === false) remaining.push(entry)
+ );
+ results.push({ id: entry.id, ok: result.handled !== false, result });
+ if (result.handled === false) remaining.push(entry);
} catch (err) {
- results.push({ id: entry.id, ok: false, error: err.message })
- remaining.push(entry)
+ results.push({ id: entry.id, ok: false, error: err.message });
+ remaining.push(entry);
}
}
if (remaining.length > 0) {
- fs.writeFileSync(file, JSON.stringify({ accepts: remaining }, null, 2) + '\n', 'utf-8')
+ fs.writeFileSync(file, JSON.stringify({ accepts: remaining }, null, 2) + '\n', 'utf-8');
} else {
- try {
- fs.rmSync(file, { force: true })
- } catch {}
- }
- return {
- applied: results.filter((r) => r.ok).length,
- failed: results.filter((r) => !r.ok).length,
- results,
+ try { fs.rmSync(file, { force: true }); } catch {}
}
+ return { applied: results.filter((r) => r.ok).length, failed: results.filter((r) => !r.ok).length, results };
}
export function buildSvelteComponentCssAuthoring(count) {
- const variantNumbers = Array.from({ length: count }, (_, i) => i + 1)
+ const variantNumbers = Array.from({ length: count }, (_, i) => i + 1);
return {
mode: 'svelte-component',
styleTag: null,
@@ -1466,5 +1338,5 @@ export function buildSvelteComponentCssAuthoring(count) {
'Do not add data-impeccable-* attributes inside component files. Svelte parses { in attribute values as an expression, so data-impeccable-params with JSON breaks the build; use componentDir/params.json instead.',
],
paramsFile: 'params.json',
- }
+ };
}
diff --git a/.agents/skills/impeccable/scripts/live/sveltekit-adapter.mjs b/.agents/skills/impeccable/scripts/live/sveltekit-adapter.mjs
index 70ee54492..e94c54f1e 100644
--- a/.agents/skills/impeccable/scripts/live/sveltekit-adapter.mjs
+++ b/.agents/skills/impeccable/scripts/live/sveltekit-adapter.mjs
@@ -7,20 +7,18 @@
* actual live UI remains the shared plain-DOM browser chrome.
*/
-import crypto from 'node:crypto'
-import fs from 'node:fs'
-import path from 'node:path'
-
-export const SVELTE_LIVE_ROOT_COMPONENT = 'src/lib/impeccable/ImpeccableLiveRoot.svelte'
-export const SVELTE_LAYOUT_MARKER_OPEN = ''
-export const SVELTE_LAYOUT_MARKER_CLOSE = ''
-export const SVELTE_ROOT_IMPORT =
- "import ImpeccableLiveRoot from '$lib/impeccable/ImpeccableLiveRoot.svelte';"
+import crypto from 'node:crypto';
+import fs from 'node:fs';
+import path from 'node:path';
+
+export const SVELTE_LIVE_ROOT_COMPONENT = 'src/lib/impeccable/ImpeccableLiveRoot.svelte';
+export const SVELTE_LAYOUT_MARKER_OPEN = '';
+export const SVELTE_LAYOUT_MARKER_CLOSE = '';
+export const SVELTE_ROOT_IMPORT = "import ImpeccableLiveRoot from '$lib/impeccable/ImpeccableLiveRoot.svelte';";
// Matches the import at ANY revision (or none). [ \t]* bounds only, never
// \s*: a greedy \s* after the statement swallowed the next line's
// indentation on removal, leaving a formatting scar in user layouts.
-const SVELTE_ROOT_IMPORT_LINE_RE =
- /^[ \t]*import ImpeccableLiveRoot from '\$lib\/impeccable\/ImpeccableLiveRoot\.svelte(?:\?[^']*)?';[ \t]*\r?\n?/gm
+const SVELTE_ROOT_IMPORT_LINE_RE = /^[ \t]*import ImpeccableLiveRoot from '\$lib\/impeccable\/ImpeccableLiveRoot\.svelte(?:\?[^']*)?';[ \t]*\r?\n?/gm;
/**
* The import specifier carries a token-derived revision query. The adapter
@@ -31,63 +29,52 @@ const SVELTE_ROOT_IMPORT_LINE_RE =
* which no cache survives.
*/
export function svelteRootImportLine(rev) {
- if (!rev) return SVELTE_ROOT_IMPORT
- return (
- "import ImpeccableLiveRoot from '$lib/impeccable/ImpeccableLiveRoot.svelte?impeccable-live=" +
- rev +
- "';"
- )
+ if (!rev) return SVELTE_ROOT_IMPORT;
+ return "import ImpeccableLiveRoot from '$lib/impeccable/ImpeccableLiveRoot.svelte?impeccable-live=" + rev + "';";
}
export function svelteAdapterRev(token) {
- if (!token) return null
- return crypto.createHash('sha256').update(String(token)).digest('hex').slice(0, 8)
+ if (!token) return null;
+ return crypto.createHash('sha256').update(String(token)).digest('hex').slice(0, 8);
}
export function detectSvelteKitProject(cwd = process.cwd(), config = null) {
- const appHtml = findSvelteKitAppHtml(cwd, config)
- if (!appHtml) return null
- const hasTemplateMarkers =
- fileIncludes(path.join(cwd, appHtml), '%sveltekit.body%') &&
- fileIncludes(path.join(cwd, appHtml), '%sveltekit.head%')
- if (!hasTemplateMarkers) return null
-
- const hasSvelteConfig =
- fs.existsSync(path.join(cwd, 'svelte.config.js')) ||
- fs.existsSync(path.join(cwd, 'svelte.config.mjs')) ||
- fs.existsSync(path.join(cwd, 'svelte.config.cjs')) ||
- fs.existsSync(path.join(cwd, 'svelte.config.ts'))
- const hasKitPackage = packageHasSvelteKit(cwd)
- if (!hasSvelteConfig && !hasKitPackage) return null
+ const appHtml = findSvelteKitAppHtml(cwd, config);
+ if (!appHtml) return null;
+ const hasTemplateMarkers = fileIncludes(path.join(cwd, appHtml), '%sveltekit.body%')
+ && fileIncludes(path.join(cwd, appHtml), '%sveltekit.head%');
+ if (!hasTemplateMarkers) return null;
+
+ const hasSvelteConfig = fs.existsSync(path.join(cwd, 'svelte.config.js'))
+ || fs.existsSync(path.join(cwd, 'svelte.config.mjs'))
+ || fs.existsSync(path.join(cwd, 'svelte.config.cjs'))
+ || fs.existsSync(path.join(cwd, 'svelte.config.ts'));
+ const hasKitPackage = packageHasSvelteKit(cwd);
+ if (!hasSvelteConfig && !hasKitPackage) return null;
return {
appHtml,
layoutFile: findSvelteKitLayout(cwd),
rootComponent: SVELTE_LIVE_ROOT_COMPONENT,
- }
+ };
}
-export function applySvelteKitLiveAdapter({
- cwd = process.cwd(),
- port,
- token,
- config = null,
-} = {}) {
+export function applySvelteKitLiveAdapter({ cwd = process.cwd(), port, token, config = null } = {}) {
if (!Number.isFinite(Number(port))) {
- throw new Error('SvelteKit live adapter requires a numeric port')
+ throw new Error('SvelteKit live adapter requires a numeric port');
}
- const detected = detectSvelteKitProject(cwd, config)
- if (!detected) return null
+ const detected = detectSvelteKitProject(cwd, config);
+ if (!detected) return null;
- ensureSvelteLiveRootComponent(cwd, Number(port), token)
+ ensureSvelteLiveRootComponent(cwd, Number(port), token);
- const layoutRel = detected.layoutFile
- const layoutAbs = path.join(cwd, layoutRel)
- fs.mkdirSync(path.dirname(layoutAbs), { recursive: true })
- const layoutExisted = fs.existsSync(layoutAbs)
- const before = layoutExisted ? fs.readFileSync(layoutAbs, 'utf-8') : defaultSvelteLayout()
- const after = patchSvelteLayout(before, { rev: svelteAdapterRev(token) })
- fs.writeFileSync(layoutAbs, after, 'utf-8')
+ const layoutRel = detected.layoutFile;
+ const layoutAbs = path.join(cwd, layoutRel);
+ fs.mkdirSync(path.dirname(layoutAbs), { recursive: true });
+ const layoutExisted = fs.existsSync(layoutAbs);
+ const before = layoutExisted ? fs.readFileSync(layoutAbs, 'utf-8') : defaultSvelteLayout();
+ const after = patchSvelteLayout(before, { rev: svelteAdapterRev(token) });
+ fs.writeFileSync(layoutAbs, after, 'utf-8');
return {
file: layoutRel,
@@ -95,31 +82,31 @@ export function applySvelteKitLiveAdapter({
inserted: after !== before || !layoutExisted,
appHtmlUntouched: true,
rootComponent: SVELTE_LIVE_ROOT_COMPONENT,
- }
+ };
}
export function removeSvelteKitLiveAdapter({ cwd = process.cwd(), config = null } = {}) {
- const detected = detectSvelteKitProject(cwd, config)
- if (!detected) return null
+ const detected = detectSvelteKitProject(cwd, config);
+ if (!detected) return null;
- const layoutAbs = path.join(cwd, detected.layoutFile)
- let removed = false
+ const layoutAbs = path.join(cwd, detected.layoutFile);
+ let removed = false;
if (fs.existsSync(layoutAbs)) {
- const before = fs.readFileSync(layoutAbs, 'utf-8')
- const after = unpatchSvelteLayout(before)
+ const before = fs.readFileSync(layoutAbs, 'utf-8');
+ const after = unpatchSvelteLayout(before);
if (after !== before) {
- fs.writeFileSync(layoutAbs, after, 'utf-8')
- removed = true
+ fs.writeFileSync(layoutAbs, after, 'utf-8');
+ removed = true;
}
}
- const rootAbs = path.join(cwd, SVELTE_LIVE_ROOT_COMPONENT)
+ const rootAbs = path.join(cwd, SVELTE_LIVE_ROOT_COMPONENT);
if (fs.existsSync(rootAbs)) {
- fs.rmSync(rootAbs, { force: true })
- removed = true
+ fs.rmSync(rootAbs, { force: true });
+ removed = true;
}
- pruneEmptyDir(path.dirname(rootAbs), path.join(cwd, 'src'))
+ pruneEmptyDir(path.dirname(rootAbs), path.join(cwd, 'src'));
return {
file: detected.layoutFile,
@@ -127,77 +114,73 @@ export function removeSvelteKitLiveAdapter({ cwd = process.cwd(), config = null
removed,
appHtmlUntouched: true,
rootComponent: SVELTE_LIVE_ROOT_COMPONENT,
- }
+ };
}
export function patchSvelteLayout(content, { rev = null } = {}) {
- let out = String(content || '')
- const importLine = svelteRootImportLine(rev)
+ let out = String(content || '');
+ const importLine = svelteRootImportLine(rev);
if (!out.includes(importLine)) {
// An import at an older revision is replaced in place, keeping its
// indentation; only a layout with no impeccable import gets an insert.
- let replaced = false
+ let replaced = false;
out = out.replace(SVELTE_ROOT_IMPORT_LINE_RE, (line) => {
- if (replaced) return ''
- replaced = true
- const indent = (line.match(/^[ \t]*/) || [''])[0]
- return indent + importLine + '\n'
- })
+ if (replaced) return '';
+ replaced = true;
+ const indent = (line.match(/^[ \t]*/) || [''])[0];
+ return indent + importLine + '\n';
+ });
if (!replaced) {
- const scriptMatch = out.match(/\n\n` + out
+ out = `\n\n` + out;
}
}
}
if (!out.includes(SVELTE_LAYOUT_MARKER_OPEN)) {
- const block = `${SVELTE_LAYOUT_MARKER_OPEN}\n\n${SVELTE_LAYOUT_MARKER_CLOSE}\n`
- const renderMatch = out.match(/\{@render\s+children(?:\?\.)?\(\)\s*\}/)
- const slotMatch = out.match(//)
- const match = renderMatch || slotMatch
+ const block = `${SVELTE_LAYOUT_MARKER_OPEN}\n\n${SVELTE_LAYOUT_MARKER_CLOSE}\n`;
+ const renderMatch = out.match(/\{@render\s+children(?:\?\.)?\(\)\s*\}/);
+ const slotMatch = out.match(//);
+ const match = renderMatch || slotMatch;
if (match) {
- out = out.slice(0, match.index) + block + out.slice(match.index)
+ out = out.slice(0, match.index) + block + out.slice(match.index);
} else {
- out = out.replace(/\s*$/, '\n\n' + block)
+ out = out.replace(/\s*$/, '\n\n' + block);
}
}
- return out
+ return out;
}
export function unpatchSvelteLayout(content) {
- let out = String(content || '')
+ let out = String(content || '');
const blockRe = new RegExp(
- '([ \\t]*)' +
- escapeRegExp(SVELTE_LAYOUT_MARKER_OPEN) +
- '\\n\\n' +
- escapeRegExp(SVELTE_LAYOUT_MARKER_CLOSE) +
- '\\n?',
+ '([ \\t]*)' + escapeRegExp(SVELTE_LAYOUT_MARKER_OPEN)
+ + '\\n\\n'
+ + escapeRegExp(SVELTE_LAYOUT_MARKER_CLOSE)
+ + '\\n?',
'g',
- )
- out = out.replace(blockRe, '$1')
- out = out.replace(SVELTE_ROOT_IMPORT_LINE_RE, '')
- out = out.replace(/
-`
+`;
}
function findSvelteKitAppHtml(cwd, config) {
- const files = Array.isArray(config?.files) ? config.files : ['src/app.html']
+ const files = Array.isArray(config?.files) ? config.files : ['src/app.html'];
for (const rel of files) {
- if (rel.includes('*')) continue
- const normalized = rel.split(path.sep).join('/')
- if (!normalized.endsWith('app.html')) continue
- const abs = path.join(cwd, normalized)
- if (fs.existsSync(abs)) return normalized
+ if (rel.includes('*')) continue;
+ const normalized = rel.split(path.sep).join('/');
+ if (!normalized.endsWith('app.html')) continue;
+ const abs = path.join(cwd, normalized);
+ if (fs.existsSync(abs)) return normalized;
}
- const fallback = 'src/app.html'
- return fs.existsSync(path.join(cwd, fallback)) ? fallback : null
+ const fallback = 'src/app.html';
+ return fs.existsSync(path.join(cwd, fallback)) ? fallback : null;
}
function findSvelteKitLayout(cwd) {
- const candidates = ['src/routes/+layout.svelte', 'src/routes/(app)/+layout.svelte']
+ const candidates = [
+ 'src/routes/+layout.svelte',
+ 'src/routes/(app)/+layout.svelte',
+ ];
for (const rel of candidates) {
- if (fs.existsSync(path.join(cwd, rel))) return rel
+ if (fs.existsSync(path.join(cwd, rel))) return rel;
}
- return 'src/routes/+layout.svelte'
+ return 'src/routes/+layout.svelte';
}
function defaultSvelteLayout() {
- return `\n\n{@render children?.()}\n`
+ return `\n\n{@render children?.()}\n`;
}
function packageHasSvelteKit(cwd) {
- const file = path.join(cwd, 'package.json')
- if (!fs.existsSync(file)) return false
+ const file = path.join(cwd, 'package.json');
+ if (!fs.existsSync(file)) return false;
try {
- const pkg = JSON.parse(fs.readFileSync(file, 'utf-8'))
+ const pkg = JSON.parse(fs.readFileSync(file, 'utf-8'));
const deps = {
...(pkg.dependencies || {}),
...(pkg.devDependencies || {}),
...(pkg.peerDependencies || {}),
- }
- return Boolean(deps['@sveltejs/kit'] || deps['@sveltejs/vite-plugin-svelte'] || deps.svelte)
+ };
+ return Boolean(deps['@sveltejs/kit'] || deps['@sveltejs/vite-plugin-svelte'] || deps.svelte);
} catch {
- return false
+ return false;
}
}
function fileIncludes(file, text) {
try {
- return fs.readFileSync(file, 'utf-8').includes(text)
+ return fs.readFileSync(file, 'utf-8').includes(text);
} catch {
- return false
+ return false;
}
}
function pruneEmptyDir(dir, stopDir) {
- let current = dir
+ let current = dir;
while (current.startsWith(stopDir) && current !== stopDir) {
try {
- if (fs.readdirSync(current).length > 0) return
- fs.rmdirSync(current)
- current = path.dirname(current)
+ if (fs.readdirSync(current).length > 0) return;
+ fs.rmdirSync(current);
+ current = path.dirname(current);
} catch {
- return
+ return;
}
}
}
function escapeRegExp(value) {
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
diff --git a/.agents/skills/impeccable/scripts/live/tanstack-adapter.mjs b/.agents/skills/impeccable/scripts/live/tanstack-adapter.mjs
index 4b04ffe11..4a1c81a97 100644
--- a/.agents/skills/impeccable/scripts/live/tanstack-adapter.mjs
+++ b/.agents/skills/impeccable/scripts/live/tanstack-adapter.mjs
@@ -17,14 +17,14 @@
* the TanStack Router file-based route generator never treats it as a route.
*/
-import fs from 'node:fs'
-import path from 'node:path'
-import { buildLiveScriptSrc } from './frameworks/script-src.mjs'
+import fs from 'node:fs';
+import path from 'node:path';
+import { buildLiveScriptSrc } from './frameworks/script-src.mjs';
-export const TANSTACK_MARKER_OPEN = '{/* impeccable-live-tanstack-start */}'
-export const TANSTACK_MARKER_CLOSE = '{/* impeccable-live-tanstack-end */}'
-export const TANSTACK_COMPONENT_DIR = 'src/impeccable'
-export const TANSTACK_COMPONENT_BASENAME = 'ImpeccableLiveRoot'
+export const TANSTACK_MARKER_OPEN = '{/* impeccable-live-tanstack-start */}';
+export const TANSTACK_MARKER_CLOSE = '{/* impeccable-live-tanstack-end */}';
+export const TANSTACK_COMPONENT_DIR = 'src/impeccable';
+export const TANSTACK_COMPONENT_BASENAME = 'ImpeccableLiveRoot';
const ROOT_ROUTE_CANDIDATES = [
'src/routes/__root.tsx',
@@ -33,55 +33,54 @@ const ROOT_ROUTE_CANDIDATES = [
'src/routes/__root.js',
'app/routes/__root.tsx',
'app/routes/__root.jsx',
-]
+];
-const START_PACKAGES = ['@tanstack/react-start', '@tanstack/solid-start', '@tanstack/start']
+const START_PACKAGES = [
+ '@tanstack/react-start',
+ '@tanstack/solid-start',
+ '@tanstack/start',
+];
export function detectTanStackStartProject(cwd = process.cwd()) {
- if (!packageHasTanStackStart(cwd)) return null
- const rootRoute = findRootRouteFile(cwd)
- if (!rootRoute) return null
+ if (!packageHasTanStackStart(cwd)) return null;
+ const rootRoute = findRootRouteFile(cwd);
+ if (!rootRoute) return null;
- const ext = path.extname(rootRoute)
- const componentExt = ext === '.jsx' || ext === '.js' ? '.jsx' : '.tsx'
- const componentFile = `${TANSTACK_COMPONENT_DIR}/${TANSTACK_COMPONENT_BASENAME}${componentExt}`
- const componentImport = relativeImportSpecifier(rootRoute, componentFile)
+ const ext = path.extname(rootRoute);
+ const componentExt = ext === '.jsx' || ext === '.js' ? '.jsx' : '.tsx';
+ const componentFile = `${TANSTACK_COMPONENT_DIR}/${TANSTACK_COMPONENT_BASENAME}${componentExt}`;
+ const componentImport = relativeImportSpecifier(rootRoute, componentFile);
- return { rootRoute, componentFile, componentImport, ext }
+ return { rootRoute, componentFile, componentImport, ext };
}
-export function applyTanStackLiveAdapter({
- cwd = process.cwd(),
- port,
- token,
- project = detectTanStackStartProject(cwd),
-} = {}) {
- if (!project) return { error: 'tanstack_not_detected' }
+export function applyTanStackLiveAdapter({ cwd = process.cwd(), port, token, project = detectTanStackStartProject(cwd) } = {}) {
+ if (!project) return { error: 'tanstack_not_detected' };
if (!Number.isFinite(Number(port))) {
- throw new Error('TanStack Start live adapter requires a numeric port')
+ throw new Error('TanStack Start live adapter requires a numeric port');
}
// Write the managed mount component.
- const componentAbs = path.join(cwd, project.componentFile)
- const componentBody = buildTanStackLiveRootComponent(Number(port), token)
- const componentExisted = fs.existsSync(componentAbs)
+ const componentAbs = path.join(cwd, project.componentFile);
+ const componentBody = buildTanStackLiveRootComponent(Number(port), token);
+ const componentExisted = fs.existsSync(componentAbs);
if (componentExisted && !isManagedComponent(fs.readFileSync(componentAbs, 'utf-8'))) {
// A non-Impeccable file already sits at our managed path — refuse to clobber.
return {
file: project.componentFile,
error: 'tanstack_component_conflict',
hint: `${project.componentFile} already exists and is not managed by Impeccable Live`,
- }
+ };
}
- fs.mkdirSync(path.dirname(componentAbs), { recursive: true })
- fs.writeFileSync(componentAbs, componentBody, 'utf-8')
+ fs.mkdirSync(path.dirname(componentAbs), { recursive: true });
+ fs.writeFileSync(componentAbs, componentBody, 'utf-8');
// Patch the root document to import + render the mount component.
- const rootAbs = path.join(cwd, project.rootRoute)
- const before = fs.readFileSync(rootAbs, 'utf-8')
- const after = patchTanStackRoot(before, project.componentImport)
- const changed = after !== before
- if (changed) fs.writeFileSync(rootAbs, after, 'utf-8')
+ const rootAbs = path.join(cwd, project.rootRoute);
+ const before = fs.readFileSync(rootAbs, 'utf-8');
+ const after = patchTanStackRoot(before, project.componentImport);
+ const changed = after !== before;
+ if (changed) fs.writeFileSync(rootAbs, after, 'utf-8');
return {
file: project.rootRoute,
@@ -89,91 +88,91 @@ export function applyTanStackLiveAdapter({
inserted: changed || !componentExisted,
componentFile: project.componentFile,
devOnly: true,
- }
+ };
}
-export function removeTanStackLiveAdapter({
- cwd = process.cwd(),
- project = detectTanStackStartProject(cwd),
-} = {}) {
- if (!project) return { error: 'tanstack_not_detected' }
- let removed = false
+export function removeTanStackLiveAdapter({ cwd = process.cwd(), project = detectTanStackStartProject(cwd) } = {}) {
+ if (!project) return { error: 'tanstack_not_detected' };
+ let removed = false;
- const rootAbs = path.join(cwd, project.rootRoute)
+ const rootAbs = path.join(cwd, project.rootRoute);
if (fs.existsSync(rootAbs)) {
- const before = fs.readFileSync(rootAbs, 'utf-8')
- const after = unpatchTanStackRoot(before)
+ const before = fs.readFileSync(rootAbs, 'utf-8');
+ const after = unpatchTanStackRoot(before);
if (after !== before) {
- fs.writeFileSync(rootAbs, after, 'utf-8')
- removed = true
+ fs.writeFileSync(rootAbs, after, 'utf-8');
+ removed = true;
}
}
- const componentAbs = path.join(cwd, project.componentFile)
+ const componentAbs = path.join(cwd, project.componentFile);
if (fs.existsSync(componentAbs)) {
- fs.rmSync(componentAbs, { force: true })
- removed = true
+ fs.rmSync(componentAbs, { force: true });
+ removed = true;
}
- pruneEmptyDir(path.dirname(componentAbs), path.join(cwd, 'src'))
+ pruneEmptyDir(path.dirname(componentAbs), path.join(cwd, 'src'));
return {
file: project.rootRoute,
adapter: 'tanstack-start',
removed,
componentFile: project.componentFile,
- }
+ };
}
export function patchTanStackRoot(content, componentImport) {
- let out = String(content || '')
- const importStatement = `import ImpeccableLiveRoot from '${componentImport}';`
+ let out = String(content || '');
+ const importStatement = `import ImpeccableLiveRoot from '${componentImport}';`;
if (!out.includes(importStatement)) {
- out = insertAfterLastImport(out, importStatement)
+ out = insertAfterLastImport(out, importStatement);
}
if (!out.includes(TANSTACK_MARKER_OPEN)) {
const block =
- `${TANSTACK_MARKER_OPEN}\n` +
- ` \n` +
- ` ${TANSTACK_MARKER_CLOSE}\n `
+ `${TANSTACK_MARKER_OPEN}\n`
+ + ` \n`
+ + ` ${TANSTACK_MARKER_CLOSE}\n `;
// Anchor before (the stable TanStack Start document marker);
// fall back to before ` naturally
// belong at the end, and the same literal can appear earlier in code blocks
// within rendered documentation pages.
if (config.insertBefore) {
- const idx = content.lastIndexOf(config.insertBefore)
- if (idx === -1) return content
- return content.slice(0, idx) + block + content.slice(idx)
+ const idx = content.lastIndexOf(config.insertBefore);
+ if (idx === -1) return content;
+ return content.slice(0, idx) + block + content.slice(idx);
}
// insertAfter: match the FIRST occurrence — typical anchors like `