232 lines
7.7 KiB
JavaScript
232 lines
7.7 KiB
JavaScript
/**
|
|
* CLI helper: find an anchor element in source and splice an insert-variant
|
|
* wrapper before or after it (no original variant — net-new content).
|
|
*
|
|
* Usage:
|
|
* node live-insert.mjs --id SESSION_ID --count N --position after \
|
|
* --classes "hero" --tag section [--file path]
|
|
*/
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { isGeneratedFile } from './is-generated.mjs';
|
|
import {
|
|
buildSearchQueries,
|
|
findElement,
|
|
findAllElements,
|
|
filterByText,
|
|
findFileWithQuery,
|
|
detectCommentSyntax,
|
|
detectStyleMode,
|
|
buildCssAuthoring,
|
|
buildCssSelectorPrefixExamples,
|
|
} from './live-wrap.mjs';
|
|
|
|
const INSERT_POSITIONS = new Set(['before', 'after']);
|
|
|
|
export function isInsertPosition(value) {
|
|
return INSERT_POSITIONS.has(value);
|
|
}
|
|
|
|
export function computeInsertLine(startLine, endLine, position) {
|
|
return position === 'before' ? startLine : endLine + 1;
|
|
}
|
|
|
|
export function buildInsertWrapperLines({ id, count, indent, commentSyntax, isJsx }) {
|
|
const styleContents = isJsx ? 'style={{ display: "contents" }}' : 'style="display: contents"';
|
|
const attrs =
|
|
'data-impeccable-variants="' + id + '" ' +
|
|
'data-impeccable-mode="insert" ' +
|
|
'data-impeccable-variant-count="' + count + '" ' +
|
|
styleContents;
|
|
|
|
if (isJsx) {
|
|
return [
|
|
indent + '<div ' + attrs + '>',
|
|
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
|
|
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
|
|
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
|
|
indent + '</div>',
|
|
];
|
|
}
|
|
|
|
return [
|
|
indent + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
|
|
indent + '<div ' + attrs + '>',
|
|
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
|
|
indent + '</div>',
|
|
indent + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
|
|
];
|
|
}
|
|
|
|
function argVal(args, flag) {
|
|
const idx = args.indexOf(flag);
|
|
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
|
|
}
|
|
|
|
function resolveElementMatch({ lines, queries, tag, text }) {
|
|
if (text) {
|
|
const candidates = [];
|
|
for (const q of queries) {
|
|
const all = findAllElements(lines, q, tag);
|
|
for (const c of all) {
|
|
if (!candidates.some((x) => x.startLine === c.startLine)) candidates.push(c);
|
|
}
|
|
if (candidates.length === 1) break;
|
|
}
|
|
if (candidates.length === 0) return { error: 'element_not_found' };
|
|
if (candidates.length === 1) return { match: candidates[0] };
|
|
const filtered = filterByText(candidates, lines, text);
|
|
if (filtered.length === 1) return { match: filtered[0] };
|
|
if (filtered.length === 0) return { match: candidates[0] };
|
|
return { error: 'element_ambiguous', candidates: filtered };
|
|
}
|
|
|
|
for (const q of queries) {
|
|
const match = findElement(lines, q, tag);
|
|
if (match) return { match };
|
|
}
|
|
return { error: 'element_not_found' };
|
|
}
|
|
|
|
export async function insertCli() {
|
|
const args = process.argv.slice(2);
|
|
|
|
if (args.includes('--help') || args.includes('-h')) {
|
|
console.log(`Usage: node live-insert.mjs [options]
|
|
|
|
Find an anchor element in source and splice an insert-variant wrapper.
|
|
|
|
Required:
|
|
--id ID Session ID for the variant wrapper
|
|
--count N Number of expected variants (1-8)
|
|
--position POS before | after (relative to the anchor element)
|
|
|
|
Element identification (at least one required):
|
|
--element-id ID HTML id attribute of the anchor element
|
|
--classes A,B,C Comma-separated CSS class names
|
|
--tag TAG Tag name (div, section, etc.)
|
|
--query TEXT Fallback: raw text to search for
|
|
|
|
Optional:
|
|
--file PATH Source file to search in (skips auto-detection)
|
|
--text TEXT Anchor textContent for disambiguation (~80 chars)
|
|
|
|
Output (JSON):
|
|
{ mode: "insert", file, position, insertLine, commentSyntax, styleMode, styleTag, cssAuthoring }`);
|
|
process.exit(0);
|
|
}
|
|
|
|
const id = argVal(args, '--id');
|
|
const count = parseInt(argVal(args, '--count') || '3', 10);
|
|
const position = argVal(args, '--position');
|
|
const elementId = argVal(args, '--element-id');
|
|
const classes = argVal(args, '--classes');
|
|
const tag = argVal(args, '--tag');
|
|
const query = argVal(args, '--query');
|
|
const filePath = argVal(args, '--file');
|
|
const text = argVal(args, '--text');
|
|
|
|
if (!id) { console.error('Missing --id'); process.exit(1); }
|
|
if (!position) { console.error('Missing --position (before | after)'); process.exit(1); }
|
|
if (!isInsertPosition(position)) { console.error('Invalid --position: ' + position); process.exit(1); }
|
|
if (!elementId && !classes && !query) {
|
|
console.error('Need at least one of: --element-id, --classes, --query');
|
|
process.exit(1);
|
|
}
|
|
|
|
const queries = buildSearchQueries(elementId, classes, tag, query);
|
|
const genOpts = { cwd: process.cwd() };
|
|
|
|
let targetFile = filePath;
|
|
if (!targetFile) {
|
|
for (const q of queries) {
|
|
targetFile = findFileWithQuery(q, process.cwd(), genOpts);
|
|
if (targetFile) break;
|
|
}
|
|
if (!targetFile) {
|
|
let generatedHit = null;
|
|
for (const q of queries) {
|
|
generatedHit = findFileWithQuery(q, process.cwd(), { ...genOpts, includeGenerated: true });
|
|
if (generatedHit) break;
|
|
}
|
|
console.error(JSON.stringify({
|
|
error: generatedHit ? 'element_not_in_source' : 'element_not_found',
|
|
fallback: 'agent-driven',
|
|
hint: 'See "Handle fallback" in live.md.',
|
|
}));
|
|
process.exit(1);
|
|
}
|
|
} else if (isGeneratedFile(targetFile, genOpts)) {
|
|
console.error(JSON.stringify({
|
|
error: 'file_is_generated',
|
|
fallback: 'agent-driven',
|
|
file: path.relative(process.cwd(), path.resolve(process.cwd(), targetFile)),
|
|
}));
|
|
process.exit(1);
|
|
}
|
|
|
|
const content = fs.readFileSync(targetFile, 'utf-8');
|
|
const lines = content.split('\n');
|
|
const resolved = resolveElementMatch({ lines, queries, tag, text });
|
|
|
|
if (resolved.error === 'element_ambiguous') {
|
|
console.error(JSON.stringify({
|
|
error: 'element_ambiguous',
|
|
fallback: 'agent-driven',
|
|
file: path.relative(process.cwd(), targetFile),
|
|
candidates: resolved.candidates.map((c) => ({
|
|
startLine: c.startLine + 1,
|
|
endLine: c.endLine + 1,
|
|
})),
|
|
}));
|
|
process.exit(1);
|
|
}
|
|
if (!resolved.match) {
|
|
console.error(JSON.stringify({ error: 'element_not_found', fallback: 'agent-driven' }));
|
|
process.exit(1);
|
|
}
|
|
|
|
const { startLine, endLine } = resolved.match;
|
|
const commentSyntax = detectCommentSyntax(targetFile);
|
|
const styleMode = detectStyleMode(targetFile);
|
|
const isJsx = commentSyntax.open === '{/*';
|
|
const spliceIndex = computeInsertLine(startLine, endLine, position);
|
|
const indent = lines[spliceIndex]?.match(/^(\s*)/)?.[1]
|
|
?? lines[startLine]?.match(/^(\s*)/)?.[1]
|
|
?? '';
|
|
|
|
const wrapperLines = buildInsertWrapperLines({
|
|
id,
|
|
count,
|
|
indent,
|
|
commentSyntax,
|
|
isJsx,
|
|
});
|
|
|
|
const newLines = [
|
|
...lines.slice(0, spliceIndex),
|
|
...wrapperLines,
|
|
...lines.slice(spliceIndex),
|
|
];
|
|
fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
|
|
|
|
const insertLine = spliceIndex + 3;
|
|
|
|
console.log(JSON.stringify({
|
|
mode: 'insert',
|
|
position,
|
|
file: path.relative(process.cwd(), targetFile),
|
|
insertLine: insertLine + 1,
|
|
commentSyntax,
|
|
styleMode: styleMode.mode,
|
|
styleTag: styleMode.styleTag,
|
|
cssSelectorPrefixExamples: buildCssSelectorPrefixExamples(styleMode.mode, count),
|
|
cssAuthoring: buildCssAuthoring(styleMode, count),
|
|
}));
|
|
}
|
|
|
|
const _running = process.argv[1];
|
|
if (_running?.endsWith('live-insert.mjs') || _running?.endsWith('live-insert.mjs/')) {
|
|
insertCli();
|
|
}
|