chore: update vendored skills and record provenance
humanizer 2.2.0 -> 2.9.1 (blader/humanizer, MIT): adds a Voice Calibration section and a passive-voice pattern, reworks negative parallelisms and em dashes. Version moved to metadata.version upstream. impeccable 3.6.0 -> 4.0.4 (pbakaus/impeccable, Apache-2.0): the repo tags the skill and the npm CLI separately, so 3.6.0 was a real release and npm's 3.5.0 was never the comparison. Adds native-platform reference briefs. Each now carries an UPSTREAM file; bin/check-vendored.sh reports drift.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* CLI helper: find an element in source and wrap it in a variant container.
|
||||
*
|
||||
* Usage:
|
||||
* npx impeccable wrap --id SESSION_ID --count N --query "hero-combined-left" [--file path]
|
||||
* node <scripts_path>/live-wrap.mjs --id SESSION_ID --count N --query "hero-combined-left" [--file path]
|
||||
*
|
||||
* Searches project files for the element matching the query (class name, ID, or
|
||||
* text snippet), wraps it with the variant scaffolding, and prints the file path
|
||||
@@ -14,14 +14,16 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { isGeneratedFile } from './lib/is-generated.mjs';
|
||||
import { resolveLiveTemplateExtensions } from './lib/template-extensions.mjs';
|
||||
import { readBuffer as readManualEditsBuffer } from './live/manual-edits-buffer.mjs';
|
||||
import { findSourceFile } from './live/source-search.mjs';
|
||||
import { resolveSourceTraits } from './live/frameworks/index.mjs';
|
||||
import {
|
||||
buildSvelteComponentCssAuthoring,
|
||||
scaffoldSvelteComponentSession,
|
||||
shouldUseSvelteComponentInjection,
|
||||
} from './live/svelte-component.mjs';
|
||||
|
||||
const EXTENSIONS = ['.html', '.jsx', '.tsx', '.vue', '.svelte', '.astro'];
|
||||
import { enterLiveRoot } from './live/roots.mjs';
|
||||
|
||||
export async function wrapCli() {
|
||||
const args = process.argv.slice(2);
|
||||
@@ -68,6 +70,13 @@ The agent should insert variant HTML at insertLine.`);
|
||||
const filePath = argVal(args, '--file');
|
||||
const text = argVal(args, '--text');
|
||||
const pageUrl = argVal(args, '--page-url');
|
||||
// Preflight passes this for source-preview targets. It computes the scaffold
|
||||
// (element location + wrapper text) but does NOT write it into source. The
|
||||
// agent then writes the wrapper + all variants in one atomic edit. The
|
||||
// premature server-side write full-reloaded the framework mid-generate and
|
||||
// stranded the browser at 0/N (live-server.mjs missed-completion note). It is
|
||||
// a no-op on the svelte-component path, which never writes the route source.
|
||||
const deferSourceWrite = args.includes('--defer-source-write');
|
||||
|
||||
if (!id) { console.error('Missing --id'); process.exit(1); }
|
||||
if (!elementId && !classes && !query) {
|
||||
@@ -160,11 +169,29 @@ The agent should insert variant HTML at insertLine.`);
|
||||
if (filtered.length === 1) {
|
||||
match = filtered[0];
|
||||
} else if (filtered.length === 0) {
|
||||
// Source uses dynamic content (`<h1>{title}</h1>` etc.) so the
|
||||
// browser-side textContent doesn't appear literally in source. Fall
|
||||
// back to first-match rather than refusing — this is the same
|
||||
// behavior unmodified callers see, just preserved.
|
||||
match = candidates[0];
|
||||
const normalizedText = String(text).replace(/\s+/g, ' ').trim();
|
||||
if (normalizedText.length < 8) {
|
||||
// Very short labels cannot disambiguate siblings reliably. Preserve
|
||||
// the legacy behavior for these low-information picker events.
|
||||
match = candidates[0];
|
||||
} else {
|
||||
// Rendered text that is absent from every candidate usually means
|
||||
// the source uses expressions or component props. Picking the first
|
||||
// same-class sibling silently edits the wrong instance (observed on
|
||||
// Astro result cards), so stop and surface every candidate instead.
|
||||
console.error(JSON.stringify({
|
||||
error: 'element_ambiguous',
|
||||
fallback: 'agent-driven',
|
||||
reason: 'rendered_text_not_in_source',
|
||||
file: path.relative(process.cwd(), targetFile),
|
||||
candidates: candidates.map((c) => ({
|
||||
startLine: c.startLine + 1,
|
||||
endLine: c.endLine + 1,
|
||||
})),
|
||||
hint: 'Rendered text does not occur in any matching source branch. The element may use dynamic props or expressions; inspect the candidates and wrap the intended instance manually.',
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
// Multiple candidates ALSO match the text. Truly ambiguous — refuse
|
||||
// rather than pick wrong, and hand the agent the candidate locations
|
||||
@@ -268,7 +295,10 @@ The agent should insert variant HTML at insertLine.`);
|
||||
.join('\n');
|
||||
const originalIndented = reindentOriginal(' ');
|
||||
const relTargetFile = path.relative(process.cwd(), targetFile).split(path.sep).join('/');
|
||||
const useSvelteComponent = shouldUseSvelteComponentInjection(targetFile);
|
||||
// The registry says which files get component preview; the svelte-component
|
||||
// module keeps the env escape hatch that turns it off.
|
||||
const useSvelteComponent = resolveSourceTraits(targetFile).preview === 'component'
|
||||
&& shouldUseSvelteComponentInjection(targetFile);
|
||||
|
||||
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
|
||||
// JSX requires object-literal style and parses string attrs as HTML (which
|
||||
@@ -288,7 +318,7 @@ The agent should insert variant HTML at insertLine.`);
|
||||
// replacement range to include the wrapper's `<div>` open / close lines
|
||||
// so the entire scaffold gets removed cleanly.
|
||||
const wrapperLines = isJsx ? [
|
||||
indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '" ' + styleContents + '>',
|
||||
indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '"' + ' ' + styleContents + '>',
|
||||
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
|
||||
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
|
||||
indent + ' <div data-impeccable-variant="original">',
|
||||
@@ -299,7 +329,7 @@ The agent should insert variant HTML at insertLine.`);
|
||||
indent + '</div>',
|
||||
] : [
|
||||
indent + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
|
||||
indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '" ' + styleContents + '>',
|
||||
indent + '<div data-impeccable-variants="' + id + '" data-impeccable-variant-count="' + count + '"' + ' ' + styleContents + '>',
|
||||
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
|
||||
indent + ' <div data-impeccable-variant="original">',
|
||||
originalIndented,
|
||||
@@ -315,13 +345,20 @@ The agent should insert variant HTML at insertLine.`);
|
||||
let outputEndLine = startLine + wrapperLines.length + (originalLines.length - 1);
|
||||
let insertLine;
|
||||
let svelteSession = null;
|
||||
let deferredWrapper = null;
|
||||
|
||||
let sveltePreviewFallback = null;
|
||||
if (useSvelteComponent) {
|
||||
// Svelte/SvelteKit resets component-local state on markup HMR updates.
|
||||
// Keep generation source-neutral: agents write real variant components
|
||||
// under the generated componentDir, the browser mounts them into the live
|
||||
// DOM, and live-accept.mjs inlines the accepted variant back into the route.
|
||||
svelteSession = scaffoldSvelteComponentSession({
|
||||
//
|
||||
// The scaffold is AST-based and refuses markup a detached preview cannot
|
||||
// support (component tags, bind:/use:, await blocks, bound nested each).
|
||||
// Refusal falls back to the plain source-preview wrapper below: an
|
||||
// HMR-resetting but CORRECT preview beats a detached wrong one.
|
||||
const scaffolded = scaffoldSvelteComponentSession({
|
||||
id,
|
||||
count,
|
||||
sourceFile: relTargetFile,
|
||||
@@ -330,10 +367,32 @@ The agent should insert variant HTML at insertLine.`);
|
||||
originalLines,
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
outputFile = path.resolve(process.cwd(), svelteSession.manifestFile);
|
||||
outputStartLine = 1;
|
||||
outputEndLine = 1;
|
||||
insertLine = 1;
|
||||
if (scaffolded && scaffolded.fallback === 'source-preview') {
|
||||
sveltePreviewFallback = scaffolded.reason || 'unsupported markup';
|
||||
} else {
|
||||
svelteSession = scaffolded;
|
||||
outputFile = path.resolve(process.cwd(), svelteSession.manifestFile);
|
||||
outputStartLine = 1;
|
||||
outputEndLine = 1;
|
||||
insertLine = 1;
|
||||
}
|
||||
}
|
||||
if (svelteSession) {
|
||||
// component preview: outputs already set above
|
||||
} else if (deferSourceWrite) {
|
||||
// Deferred source write: compute the scaffold text but leave source
|
||||
// untouched. The agent replaces the picked element's source range with
|
||||
// `wrapperBlock` (variants spliced at the marker) in one edit. Writing the
|
||||
// scaffold here first would reload the framework before the agent's write
|
||||
// lands, and a browser caught mid-reload misses the `done` and sits at 0/N.
|
||||
deferredWrapper = {
|
||||
block: wrapperLines.join('\n'),
|
||||
replaceStartLine: startLine + 1, // 1-indexed picked-element range the
|
||||
replaceEndLine: endLine + 1, // agent's wrapper block replaces
|
||||
};
|
||||
// insertLine matches the final file position the wrapper occupies once the
|
||||
// agent replaces the picked range, so downstream consumers stay consistent.
|
||||
insertLine = startLine + 6 + (originalLines.length - 1) + 1;
|
||||
} else {
|
||||
// Replace the original element with the wrapper
|
||||
const newLines = [
|
||||
@@ -355,16 +414,31 @@ The agent should insert variant HTML at insertLine.`);
|
||||
|
||||
const outputRelFile = path.relative(process.cwd(), outputFile).split(path.sep).join('/');
|
||||
|
||||
const svelteComponentAuthoring = useSvelteComponent ? buildSvelteComponentCssAuthoring(count) : null;
|
||||
const componentPreviewActive = !!svelteSession;
|
||||
const svelteComponentAuthoring = componentPreviewActive ? buildSvelteComponentCssAuthoring(count) : null;
|
||||
const componentSession = svelteSession;
|
||||
const componentPreviewMode = componentPreviewActive ? 'svelte-component' : undefined;
|
||||
const previewMode = componentPreviewMode;
|
||||
|
||||
console.log(JSON.stringify({
|
||||
file: outputRelFile,
|
||||
sourceFile: useSvelteComponent ? relTargetFile : undefined,
|
||||
previewMode: useSvelteComponent ? 'svelte-component' : undefined,
|
||||
componentDir: svelteSession?.componentDir,
|
||||
propContract: svelteSession?.propContract,
|
||||
sourceStartLine: useSvelteComponent ? startLine + 1 : undefined,
|
||||
sourceEndLine: useSvelteComponent ? endLine + 1 : undefined,
|
||||
sourceFile: componentPreviewActive ? relTargetFile : undefined,
|
||||
previewMode,
|
||||
previewFallback: sveltePreviewFallback
|
||||
? { from: 'svelte-component', reason: sveltePreviewFallback }
|
||||
: undefined,
|
||||
// Deferred source write: the wrapper is NOT yet in source. The agent
|
||||
// replaces [replaceStartLine, replaceEndLine] with `wrapperBlock` (variants
|
||||
// spliced at the "insert below this line" marker) in one atomic edit.
|
||||
sourceWritten: deferredWrapper ? false : undefined,
|
||||
wrapperBlock: deferredWrapper ? deferredWrapper.block : undefined,
|
||||
replaceStartLine: deferredWrapper ? deferredWrapper.replaceStartLine : undefined,
|
||||
replaceEndLine: deferredWrapper ? deferredWrapper.replaceEndLine : undefined,
|
||||
componentDir: componentSession?.componentDir,
|
||||
propContract: componentSession?.propContract,
|
||||
componentStubMarkup: componentSession?.stubMarkup,
|
||||
sourceStartLine: componentPreviewActive ? startLine + 1 : undefined,
|
||||
sourceEndLine: componentPreviewActive ? endLine + 1 : undefined,
|
||||
startLine: outputStartLine, // 1-indexed for the agent
|
||||
// wrapperLines is an array but one element (the original-content slot)
|
||||
// is a `\n`-joined multi-line string, so the actual file-row count is
|
||||
@@ -374,10 +448,10 @@ The agent should insert variant HTML at insertLine.`);
|
||||
endLine: outputEndLine, // 1-indexed
|
||||
insertLine, // 1-indexed: where variants go
|
||||
commentSyntax: commentSyntax,
|
||||
styleMode: useSvelteComponent ? 'svelte-component' : styleMode.mode,
|
||||
styleTag: useSvelteComponent ? null : styleMode.styleTag,
|
||||
cssSelectorPrefixExamples: useSvelteComponent ? [] : buildCssSelectorPrefixExamples(styleMode.mode, count),
|
||||
cssAuthoring: useSvelteComponent ? svelteComponentAuthoring : buildCssAuthoring(styleMode, count),
|
||||
styleMode: componentPreviewMode || styleMode.mode,
|
||||
styleTag: componentPreviewActive ? null : styleMode.styleTag,
|
||||
cssSelectorPrefixExamples: componentPreviewActive ? [] : buildCssSelectorPrefixExamples(styleMode.mode, count),
|
||||
cssAuthoring: svelteComponentAuthoring || buildCssAuthoring(styleMode, count),
|
||||
originalLineCount: originalLines.length,
|
||||
}));
|
||||
}
|
||||
@@ -579,27 +653,22 @@ function attrEscapeDouble(str) {
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Comment syntax, style mode, and preview strategy all come from the framework
|
||||
* registry, keyed on the target file's extension: `.jsx`/`.tsx` author JSX
|
||||
* comments, `.astro` needs global-prefixed preview CSS because Astro scopes
|
||||
* component styles away from the generated wrappers, `.svelte` gets component
|
||||
* preview. See live/frameworks/index.mjs for why extension and not project.
|
||||
*/
|
||||
function detectCommentSyntax(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (ext === '.jsx' || ext === '.tsx') {
|
||||
return { open: '{/*', close: '*/}' };
|
||||
}
|
||||
// HTML, Vue, Svelte, Astro all use HTML comments
|
||||
return { open: '<!--', close: '-->' };
|
||||
return resolveSourceTraits(filePath).commentSyntax === 'jsx'
|
||||
? { open: '{/*', close: '*/}' }
|
||||
: { open: '<!--', close: '-->' };
|
||||
}
|
||||
|
||||
function detectStyleMode(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (ext === '.astro') {
|
||||
return {
|
||||
mode: 'astro-global-prefixed',
|
||||
styleTag: '<style is:inline data-impeccable-css="SESSION_ID">',
|
||||
};
|
||||
}
|
||||
return {
|
||||
mode: 'scoped',
|
||||
styleTag: '<style data-impeccable-css="SESSION_ID">',
|
||||
};
|
||||
const traits = resolveSourceTraits(filePath);
|
||||
return { mode: traits.styleMode, styleTag: traits.styleTag };
|
||||
}
|
||||
|
||||
function buildCssSelectorPrefixExamples(styleMode, count) {
|
||||
@@ -650,56 +719,19 @@ function buildCssAuthoring(styleMode, count) {
|
||||
/**
|
||||
* Search project files for the query string (class name, ID, etc.)
|
||||
* Returns the first matching file path, or null.
|
||||
*
|
||||
* Only `node_modules`, `.git`, and `.impeccable` are skipped outright.
|
||||
* dist/build/out are left to the isGeneratedFile guard so the
|
||||
* `includeGenerated` second pass can still find the element there and report
|
||||
* `generatedMatch`.
|
||||
*/
|
||||
function findFileWithQuery(query, cwd, genOpts = {}) {
|
||||
const searchDirs = ['src', 'app', 'pages', 'components', 'public', 'views', 'templates', '.'];
|
||||
const seen = new Set();
|
||||
|
||||
for (const dir of searchDirs) {
|
||||
const absDir = path.join(cwd, dir);
|
||||
if (!fs.existsSync(absDir)) continue;
|
||||
const result = searchDir(absDir, query, seen, 0, genOpts);
|
||||
if (result) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function searchDir(dir, query, seen, depth, genOpts) {
|
||||
if (depth > 5) return null; // don't go too deep
|
||||
const realDir = fs.realpathSync(dir);
|
||||
if (seen.has(realDir)) return null;
|
||||
seen.add(realDir);
|
||||
|
||||
let entries;
|
||||
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
||||
catch { return null; }
|
||||
|
||||
// Check files first
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue;
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (!EXTENSIONS.includes(ext)) continue;
|
||||
|
||||
const filePath = path.join(dir, entry.name);
|
||||
if (!genOpts.includeGenerated && isGeneratedFile(filePath, genOpts)) continue;
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
if (content.includes(query)) return filePath;
|
||||
} catch { /* skip unreadable files */ }
|
||||
}
|
||||
|
||||
// Then recurse into directories. Always skip node_modules and .git (never
|
||||
// project content). dist/build/out are left to the isGeneratedFile guard so
|
||||
// the includeGenerated second-pass can still find the element there and
|
||||
// report `generatedMatch`.
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
||||
const result = searchDir(path.join(dir, entry.name), query, seen, depth + 1, genOpts);
|
||||
if (result) return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
return findSourceFile({
|
||||
query,
|
||||
cwd,
|
||||
extensions: resolveLiveTemplateExtensions(cwd),
|
||||
fileFilter: (filePath) => genOpts.includeGenerated || !isGeneratedFile(filePath, genOpts),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -876,6 +908,7 @@ function findClosingLine(lines, start) {
|
||||
// Auto-execute when run directly (node live-wrap.mjs ...)
|
||||
const _running = process.argv[1];
|
||||
if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/')) {
|
||||
enterLiveRoot();
|
||||
wrapCli();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user