/** * CLI helper: deterministic accept/discard of variant sessions. * * Usage: * node live-accept.mjs --id SESSION_ID --discard * node live-accept.mjs --id SESSION_ID --variant N * * For discard: removes the entire variant wrapper and restores the original. * For accept: replaces the wrapper with the chosen variant's content. If the * session had a colocated ' : '')); if (paramValues && Object.keys(paramValues).length > 0) { // Preserve the user's knob positions for the carbonize-cleanup agent // to bake into the final CSS when it collapses scoped rules. replacement.push(indent + commentSyntax.open + ' impeccable-param-values ' + id + ': ' + JSON.stringify(paramValues) + ' ' + commentSyntax.close); } replacement.push(indent + commentSyntax.open + ' impeccable-carbonize-end ' + id + ' ' + commentSyntax.close); } // Keep the `@scope ([data-impeccable-variant="N"])` selectors in the // carbonize CSS block working visually by re-wrapping the accepted content // in a data-impeccable-variant="N" div with `display: contents` (so layout // isn't affected). The carbonize agent strips this attribute + wrapper when // it moves the CSS to a proper stylesheet. // // Style attribute syntax has to follow the host file's flavor — JSX files // need the object form, otherwise React 19 throws "Failed to set indexed // property [0] on CSSStyleDeclaration" while parsing the string char-by-char. if (cssContent) { const styleAttr = isJsx ? "style={{ display: 'contents' }}" : 'style="display: contents"'; replacement.push(indent + '
'); replacement.push(...restored); replacement.push(indent + '
'); } else { replacement.push(...restored); } const newLines = [ ...lines.slice(0, replaceRange.start), ...replacement, ...lines.slice(replaceRange.end + 1), ]; fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8'); return { carbonize: needsCarbonize }; } // --------------------------------------------------------------------------- // Parsing helpers // --------------------------------------------------------------------------- /** * Find the start/end marker lines for a session. * Returns { start, end } (0-indexed line numbers) or null. */ function findMarkerBlock(id, lines) { let start = -1; let end = -1; const startPattern = 'impeccable-variants-start ' + id; const endPattern = 'impeccable-variants-end ' + id; for (let i = 0; i < lines.length; i++) { if (start === -1 && lines[i].includes(startPattern)) start = i; if (lines[i].includes(endPattern)) { end = i; break; } } return (start !== -1 && end !== -1) ? { start, end } : null; } /** * Compute the line range to REPLACE (vs. just the marker range to extract * from). For JSX/TSX wrappers, live-wrap places the marker comments INSIDE * the `
` outer wrapper so the picked * element's JSX slot keeps a single child — a Fragment `<>` would have * solved the multi-sibling case but failed inside `asChild` / cloneElement * parents with "Invalid prop supplied to React.Fragment". * * That means the marker block is enclosed by the wrapper `
` opener * (with `data-impeccable-variants="ID"`) and its matching `
`. We * walk back to the opener and forward to the closer so accept/discard * remove the entire scaffold, not just the inner markers. * * Marker lines themselves stay where they were so extractOriginal / * extractVariant / extractCss continue to walk the same range. */ function expandReplaceRange(block, lines, isJsx) { if (!isJsx) return { start: block.start, end: block.end }; let { start, end } = block; // Walk back for the wrapper `
= Math.max(0, start - 12); i--) { if (/data-impeccable-variants=/.test(lines[i])) { let opener = i; while (opener > 0 && !/` by div-depth tracking from the // wrapper opener. Operate on JOINED text instead of per-line: a // multi-line self-closing JSX `` would // fool per-line regex tracking (the `` line never matches selfCloseRe since it needs `` orphaned after accept/discard. Single regex with // `[^>]*?` (which spans newlines in JS) handles either form correctly. const joined = lines.slice(start).join('\n'); // Match either `
` (self-close, group 1 is `/`), `
` // (open, group 1 is empty), or `
`. const tagRe = /]*?(\/?)>|<\/div\s*>/g; let depth = 0; let m; while ((m = tagRe.exec(joined)) !== null) { const isClose = m[0].startsWith('= end) { end = candidateEnd; break; } } } return { start, end }; } /** * Join wrapper lines into a single string with `` to close on) * - Same-line `` blocks * - Multi-line `` blocks */ function stripStyleAndJoin(lines, block) { const out = []; let inStyle = false; for (let i = block.start; i <= block.end; i++) { let line = lines[i]; if (!inStyle) { // Strip any complete . const closeIdx = line.search(/<\/style\s*>/); if (closeIdx !== -1) { inStyle = false; out.push(line.slice(closeIdx).replace(/<\/style\s*>/, '')); } // else: skip line entirely } } return out.join('\n'); } /** * Find the inner content of `` inside `text`, * handling nested same-tag elements via depth counting. `attrMatch` is a * regex source fragment that must appear inside the opener tag. * Returns the inner string (may be empty), or null if not found. */ function extractInnerByAttr(text, attrMatch) { const openerRe = new RegExp('<([A-Za-z][A-Za-z0-9]*)\\b[^>]*' + attrMatch + '[^>]*>'); const openMatch = text.match(openerRe); if (!openMatch) return null; const tagName = openMatch[1]; const innerStart = openMatch.index + openMatch[0].length; // Match any opener or closer of this tag name after innerStart. // (Does not match self-closing , which doesn't contribute to depth.) const tagRe = new RegExp('<(?:/)?' + tagName + '\\b[^>]*>', 'g'); tagRe.lastIndex = innerStart; let depth = 1; let m; while ((m = tagRe.exec(text))) { const isClose = m[0].startsWith('$/.test(m[0]); if (isClose) { depth--; if (depth === 0) return text.slice(innerStart, m.index); } else if (!isSelfClose) { depth++; } } return null; } /** * Extract the original element content from within the variant wrapper. * Returns an array of lines. */ function extractOriginal(lines, block) { const text = stripStyleAndJoin(lines, block); const inner = extractInnerByAttr(text, 'data-impeccable-variant="original"'); if (inner === null) return []; return inner.split('\n'); } /** * Extract a specific variant's inner content (stripping the wrapper div). * Returns an array of lines, or null if not found. */ function extractVariant(lines, block, variantNum) { const text = stripStyleAndJoin(lines, block); const inner = extractInnerByAttr(text, 'data-impeccable-variant="' + variantNum + '"'); if (inner === null) return null; const result = inner.split('\n'); // Collapse a lone empty leading/trailing line (common after string splice). while (result.length > 1 && result[0].trim() === '') result.shift(); while (result.length > 1 && result[result.length - 1].trim() === '') result.pop(); return result.length > 0 ? result : null; } /** * Extract the colocated ` — return the inner content. * 3. Multi-line: `` on a later line — return * the lines between them. */ function extractCss(lines, block, id) { const styleAttr = 'data-impeccable-css="' + id + '"'; let inStyle = false; const content = []; for (let i = block.start; i <= block.end; i++) { const line = lines[i]; if (!inStyle && line.includes(styleAttr)) { // Self-closing: nothing to carbonize. if (/]*\/\s*>/.test(line)) return null; // Same-line open + close: extract inner text. const sameLine = line.match(/]*>([\s\S]*?)<\/style\s*>/); if (sameLine) { const inner = stripJsxTemplateWrap(sameLine[1]); return inner.length > 0 ? inner.split('\n') : null; } inStyle = true; continue; // skip the anywhere on the line — JSX template-literal closes // (`}`) put the close mid-line, and we don't want to absorb the // template-literal punctuation as CSS content. const closeIdx = line.indexOf(''); if (closeIdx !== -1) break; content.push(line); } } if (content.length === 0) return null; return stripJsxTemplateLines(content); } /** * Strip a JSX template-literal wrap (`{` … `}`) from CSS extracted out of a * `