@bastani/atomic 0.8.20-0 → 0.8.21-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +5 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +5 -0
- package/dist/builtin/subagents/agents/code-simplifier.md +78 -22
- package/dist/builtin/subagents/agents/debugger.md +4 -3
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/CHANGELOG.md +5 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +25 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/create-spec/SKILL.md +169 -125
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +89 -80
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +92 -0
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_manual_edit_applier.toml +95 -0
- package/dist/builtin/workflows/skills/impeccable/agents/openai.yaml +4 -0
- package/dist/builtin/workflows/skills/impeccable/reference/adapt.md +122 -1
- package/dist/builtin/workflows/skills/impeccable/reference/animate.md +38 -12
- package/dist/builtin/workflows/skills/impeccable/reference/audit.md +5 -5
- package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +7 -7
- package/dist/builtin/workflows/skills/impeccable/reference/brand.md +4 -14
- package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +115 -1
- package/dist/builtin/workflows/skills/impeccable/reference/codex.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +109 -6
- package/dist/builtin/workflows/skills/impeccable/reference/craft.md +7 -7
- package/dist/builtin/workflows/skills/impeccable/reference/critique.md +623 -94
- package/dist/builtin/workflows/skills/impeccable/reference/delight.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/document.md +16 -14
- package/dist/builtin/workflows/skills/impeccable/reference/extract.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/init.md +172 -0
- package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +0 -6
- package/dist/builtin/workflows/skills/impeccable/reference/layout.md +33 -13
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +96 -19
- package/dist/builtin/workflows/skills/impeccable/reference/onboard.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/polish.md +3 -4
- package/dist/builtin/workflows/skills/impeccable/reference/product.md +1 -3
- package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/shape.md +5 -5
- package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +158 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/cleanup-deprecated.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +225 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +266 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +17 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/design-parser.mjs +16 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detect.mjs +21 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +1725 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +244 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +4543 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +43 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +252 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +535 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +986 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +208 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +12 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +198 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +419 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2316 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +101 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/impeccable-paths.mjs +17 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/is-generated.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +139 -96
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +4491 -526
- package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +1241 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-copy-edit-agent.mjs +683 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-discard-manual-edits.mjs +51 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-event-validation.mjs +136 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +22 -9
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert-ui.mjs +458 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +232 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +363 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edits-buffer.mjs +152 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +288 -110
- package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +47 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +1443 -100
- package/dist/builtin/workflows/skills/impeccable/scripts/live-session-store.mjs +17 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +17 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +216 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +2 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +633 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +1 -1
- package/dist/builtin/workflows/src/extension/index.ts +67 -3
- package/dist/builtin/workflows/src/extension/render-result.ts +26 -1
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +227 -3
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +94 -7
- package/dist/builtin/workflows/src/shared/stage-prompt.ts +326 -0
- package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +62 -7
- package/dist/builtin/workflows/src/shared/store-types.ts +43 -0
- package/dist/builtin/workflows/src/shared/store.ts +37 -0
- package/dist/builtin/workflows/src/tui/chat-surface-message.ts +22 -4
- package/dist/builtin/workflows/src/tui/graph-view.ts +47 -0
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +43 -1
- package/dist/builtin/workflows/src/tui/run-detail.ts +10 -4
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +117 -15
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +9 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +2 -5
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +11 -29
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/docs/quickstart.md +1 -2
- package/package.json +4 -4
- package/dist/builtin/workflows/skills/impeccable/reference/cognitive-load.md +0 -106
- package/dist/builtin/workflows/skills/impeccable/reference/color-and-contrast.md +0 -105
- package/dist/builtin/workflows/skills/impeccable/reference/heuristics-scoring.md +0 -234
- package/dist/builtin/workflows/skills/impeccable/reference/motion-design.md +0 -109
- package/dist/builtin/workflows/skills/impeccable/reference/personas.md +0 -179
- package/dist/builtin/workflows/skills/impeccable/reference/responsive-design.md +0 -114
- package/dist/builtin/workflows/skills/impeccable/reference/spatial-design.md +0 -100
- package/dist/builtin/workflows/skills/impeccable/reference/teach.md +0 -156
- package/dist/builtin/workflows/skills/impeccable/reference/typography.md +0 -159
- package/dist/builtin/workflows/skills/impeccable/reference/ux-writing.md +0 -107
- package/dist/builtin/workflows/skills/impeccable/scripts/load-context.mjs +0 -141
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Collect evidence for pending live copy edits.
|
|
4
|
+
*
|
|
5
|
+
* This module intentionally does not edit source files and does not choose a
|
|
6
|
+
* winner. It gathers staged browser edits, rendered context, framework source
|
|
7
|
+
* hints, and likely source candidates so the AI copy-edit batch runner can make
|
|
8
|
+
* source changes with full repo context.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { isGeneratedFile } from './is-generated.mjs';
|
|
14
|
+
import { readBuffer, getBufferPath } from './live-manual-edits-buffer.mjs';
|
|
15
|
+
|
|
16
|
+
const EVIDENCE_VERSION = 1;
|
|
17
|
+
const TEXT_EXTENSIONS = new Set(['.html', '.jsx', '.tsx', '.vue', '.svelte', '.astro', '.js', '.mjs', '.ts']);
|
|
18
|
+
const SEARCH_DIRS = ['src', 'app', 'pages', 'components', 'public', 'views', 'templates', 'site', 'lib', 'data'];
|
|
19
|
+
const STRONG_LITERAL_MATCH_LIMIT = 8;
|
|
20
|
+
const WEAK_LITERAL_MATCH_LIMIT = 4;
|
|
21
|
+
const OBJECT_KEY_MATCH_LIMIT = 8;
|
|
22
|
+
const LOCATOR_MATCH_LIMIT = 4;
|
|
23
|
+
const CONTEXT_MATCH_LIMIT = 8;
|
|
24
|
+
const CONTEXT_MATCH_PER_HINT = 2;
|
|
25
|
+
const SKIP_DIRS = new Set([
|
|
26
|
+
'node_modules',
|
|
27
|
+
'.git',
|
|
28
|
+
'.impeccable',
|
|
29
|
+
'.astro',
|
|
30
|
+
'.next',
|
|
31
|
+
'.nuxt',
|
|
32
|
+
'.svelte-kit',
|
|
33
|
+
'dist',
|
|
34
|
+
'build',
|
|
35
|
+
'out',
|
|
36
|
+
'coverage',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export function buildManualEditEvidence({ cwd = process.cwd(), pageUrl = null } = {}) {
|
|
40
|
+
const buffer = readBuffer(cwd);
|
|
41
|
+
const entries = pageUrl
|
|
42
|
+
? buffer.entries.filter((entry) => entry.pageUrl === pageUrl)
|
|
43
|
+
: buffer.entries;
|
|
44
|
+
const opCount = countOps(entries);
|
|
45
|
+
|
|
46
|
+
if (opCount === 0) {
|
|
47
|
+
return {
|
|
48
|
+
pageUrl,
|
|
49
|
+
count: 0,
|
|
50
|
+
entries: [],
|
|
51
|
+
ops: [],
|
|
52
|
+
candidates: [],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const searchFiles = collectSearchFiles(cwd);
|
|
57
|
+
const ops = flattenOps(entries);
|
|
58
|
+
const candidates = ops.map((op) => buildCandidatesForOp(op, cwd, searchFiles));
|
|
59
|
+
return {
|
|
60
|
+
version: EVIDENCE_VERSION,
|
|
61
|
+
pageUrl: pageUrl || null,
|
|
62
|
+
count: opCount,
|
|
63
|
+
entries,
|
|
64
|
+
ops,
|
|
65
|
+
context: {
|
|
66
|
+
cwd,
|
|
67
|
+
bufferPath: path.relative(cwd, getBufferPath(cwd)),
|
|
68
|
+
totalEntries: entries.length,
|
|
69
|
+
totalOps: opCount,
|
|
70
|
+
},
|
|
71
|
+
candidates,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function countOps(entries) {
|
|
76
|
+
let count = 0;
|
|
77
|
+
for (const entry of entries) count += Array.isArray(entry.ops) ? entry.ops.length : 0;
|
|
78
|
+
return count;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function flattenOps(entries) {
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
const contextHintsByRef = buildContextHintsByRef(entry);
|
|
85
|
+
for (const op of entry.ops || []) {
|
|
86
|
+
out.push({
|
|
87
|
+
entryId: entry.id,
|
|
88
|
+
pageUrl: entry.pageUrl,
|
|
89
|
+
ref: op.ref,
|
|
90
|
+
contextRef: op.contextRef || null,
|
|
91
|
+
tag: op.tag,
|
|
92
|
+
elementId: op.elementId || null,
|
|
93
|
+
classes: Array.isArray(op.classes) ? op.classes : [],
|
|
94
|
+
originalText: op.originalText,
|
|
95
|
+
newText: op.newText,
|
|
96
|
+
deleted: op.deleted === true,
|
|
97
|
+
sourceHint: op.sourceHint || null,
|
|
98
|
+
leaf: op.leaf || null,
|
|
99
|
+
nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts : [],
|
|
100
|
+
container: op.container || null,
|
|
101
|
+
contextHints: contextHintsByRef.get(op.ref) || [],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildContextHintsByRef(entry) {
|
|
109
|
+
const map = new Map();
|
|
110
|
+
for (const op of entry.ops || []) {
|
|
111
|
+
const hints = new Set();
|
|
112
|
+
const add = (value) => {
|
|
113
|
+
const text = normalizeText(decodeBasicHtml(String(value || '')));
|
|
114
|
+
if (text.length < 3 || text.length > 160) return;
|
|
115
|
+
if (text === normalizeText(op.originalText) || text === normalizeText(op.newText)) return;
|
|
116
|
+
hints.add(text);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
for (const item of op.nearbyEditableTexts || []) {
|
|
120
|
+
add(typeof item === 'string' ? item : item?.text);
|
|
121
|
+
}
|
|
122
|
+
const outer = typeof entry.element?.outerHTML === 'string' ? entry.element.outerHTML : '';
|
|
123
|
+
for (const match of outer.matchAll(/data-impeccable-original-text="([^"]*)"/g)) add(match[1]);
|
|
124
|
+
if (typeof entry.element?.textContent === 'string') {
|
|
125
|
+
for (const chunk of entry.element.textContent.split(/\s{2,}|\n|\t/)) add(chunk);
|
|
126
|
+
}
|
|
127
|
+
map.set(op.ref, [...hints].slice(0, 16));
|
|
128
|
+
}
|
|
129
|
+
return map;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildCandidatesForOp(op, cwd, searchFiles) {
|
|
133
|
+
const originalText = String(op.originalText || '');
|
|
134
|
+
const contextNeedles = op.contextHints || [];
|
|
135
|
+
return {
|
|
136
|
+
entryId: op.entryId,
|
|
137
|
+
ref: op.ref,
|
|
138
|
+
originalText,
|
|
139
|
+
sourceHint: analyzeSourceHint(op, cwd),
|
|
140
|
+
textMatches: originalText ? findLiteralMatches(searchFiles, originalText, { max: literalMatchLimit(originalText) }) : [],
|
|
141
|
+
objectKeyMatches: originalText ? findObjectKeyMatches(searchFiles, originalText, { max: OBJECT_KEY_MATCH_LIMIT }) : [],
|
|
142
|
+
locatorMatches: findLocatorMatches(searchFiles, op, { max: LOCATOR_MATCH_LIMIT }),
|
|
143
|
+
contextTextMatches: findContextMatches(searchFiles, contextNeedles, { maxPerHint: CONTEXT_MATCH_PER_HINT, max: CONTEXT_MATCH_LIMIT }),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function literalMatchLimit(text) {
|
|
148
|
+
return isWeakSourceNeedle(text) ? WEAK_LITERAL_MATCH_LIMIT : STRONG_LITERAL_MATCH_LIMIT;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isWeakSourceNeedle(text) {
|
|
152
|
+
const normalized = normalizeText(text);
|
|
153
|
+
return normalized.length < 4 || /^[\d.,+\-%\s]+$/.test(normalized);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function analyzeSourceHint(op, cwd) {
|
|
157
|
+
const hint = normalizeSourceHint(op.sourceHint);
|
|
158
|
+
if (!hint.file) return null;
|
|
159
|
+
const file = path.resolve(cwd, hint.file);
|
|
160
|
+
const relativeFile = path.relative(cwd, file);
|
|
161
|
+
if (!isPathInsideOrEqual(cwd, file)) {
|
|
162
|
+
return { ...hint, status: 'outside_cwd', relativeFile: hint.file };
|
|
163
|
+
}
|
|
164
|
+
if (!fs.existsSync(file)) {
|
|
165
|
+
return { ...hint, status: 'file_missing', relativeFile };
|
|
166
|
+
}
|
|
167
|
+
if (isGeneratedFile(file, { cwd })) {
|
|
168
|
+
return { ...hint, status: 'generated', relativeFile };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
172
|
+
const lines = content.split('\n');
|
|
173
|
+
const line = hint.line || 1;
|
|
174
|
+
const start = Math.max(0, line - 4);
|
|
175
|
+
const end = Math.min(lines.length, line + 3);
|
|
176
|
+
const windowText = lines.slice(start, end).join('\n');
|
|
177
|
+
const containsOriginalText = typeof op.originalText === 'string' && windowText.includes(op.originalText);
|
|
178
|
+
return {
|
|
179
|
+
...hint,
|
|
180
|
+
status: containsOriginalText ? 'ok' : 'text_not_found_near_hint',
|
|
181
|
+
relativeFile,
|
|
182
|
+
excerpt: lines.slice(start, end).map((text, index) => ({
|
|
183
|
+
line: start + index + 1,
|
|
184
|
+
text: text.slice(0, 240),
|
|
185
|
+
})),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeSourceHint(hint) {
|
|
190
|
+
if (!hint || typeof hint !== 'object') return {};
|
|
191
|
+
let line = Number.isFinite(Number(hint.line)) ? Number(hint.line) : null;
|
|
192
|
+
let column = Number.isFinite(Number(hint.column)) ? Number(hint.column) : null;
|
|
193
|
+
if ((!line || !column) && typeof hint.loc === 'string') {
|
|
194
|
+
const match = hint.loc.match(/^(\d+)(?::(\d+))?/);
|
|
195
|
+
if (match) {
|
|
196
|
+
line = Number(match[1]);
|
|
197
|
+
if (match[2]) column = Number(match[2]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
file: typeof hint.file === 'string' ? hint.file : '',
|
|
202
|
+
loc: typeof hint.loc === 'string' ? hint.loc : '',
|
|
203
|
+
line,
|
|
204
|
+
column,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function collectSearchFiles(cwd) {
|
|
209
|
+
const out = [];
|
|
210
|
+
const seenDirs = new Set();
|
|
211
|
+
const seenFiles = new Set();
|
|
212
|
+
for (const dir of SEARCH_DIRS) {
|
|
213
|
+
scanDir(path.join(cwd, dir), cwd, seenDirs, seenFiles, out, 0);
|
|
214
|
+
}
|
|
215
|
+
scanRootFiles(cwd, seenFiles, out);
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function scanDir(dir, cwd, seenDirs, seenFiles, out, depth) {
|
|
220
|
+
if (depth > 7 || !fs.existsSync(dir)) return;
|
|
221
|
+
let realDir;
|
|
222
|
+
try { realDir = fs.realpathSync(dir); } catch { return; }
|
|
223
|
+
if (seenDirs.has(realDir)) return;
|
|
224
|
+
seenDirs.add(realDir);
|
|
225
|
+
|
|
226
|
+
let entries;
|
|
227
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
const fullPath = path.join(dir, entry.name);
|
|
230
|
+
if (entry.isDirectory()) {
|
|
231
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
232
|
+
scanDir(fullPath, cwd, seenDirs, seenFiles, out, depth + 1);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (!entry.isFile() || !TEXT_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) continue;
|
|
236
|
+
maybeAddSearchFile(fullPath, cwd, seenFiles, out);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function scanRootFiles(cwd, seenFiles, out) {
|
|
241
|
+
let entries;
|
|
242
|
+
try { entries = fs.readdirSync(cwd, { withFileTypes: true }); } catch { return; }
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
if (!entry.isFile() || !TEXT_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) continue;
|
|
245
|
+
maybeAddSearchFile(path.join(cwd, entry.name), cwd, seenFiles, out);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function maybeAddSearchFile(file, cwd, seenFiles, out) {
|
|
250
|
+
let realFile;
|
|
251
|
+
try { realFile = fs.realpathSync(file); } catch { return; }
|
|
252
|
+
if (seenFiles.has(realFile)) return;
|
|
253
|
+
seenFiles.add(realFile);
|
|
254
|
+
if (isGeneratedFile(file, { cwd })) return;
|
|
255
|
+
let content;
|
|
256
|
+
try { content = fs.readFileSync(file, 'utf-8'); } catch { return; }
|
|
257
|
+
out.push({ file, relativeFile: path.relative(cwd, file), content, lines: content.split('\n') });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function findLiteralMatches(searchFiles, needle, { max }) {
|
|
261
|
+
return findMatches(searchFiles, needle, { kind: 'text', max });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function findObjectKeyMatches(searchFiles, text, { max }) {
|
|
265
|
+
const re = new RegExp('(["\\\'`])' + escapeRegExp(text) + '\\1(?=\\s*:)', 'g');
|
|
266
|
+
const out = [];
|
|
267
|
+
for (const file of searchFiles) {
|
|
268
|
+
for (const match of file.content.matchAll(re)) {
|
|
269
|
+
out.push(matchForIndex(file, match.index, 'object_key', text));
|
|
270
|
+
if (out.length >= max) return out;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function findLocatorMatches(searchFiles, op, { max }) {
|
|
277
|
+
const needles = [];
|
|
278
|
+
if (op.elementId) needles.push({ kind: 'id', needle: op.elementId });
|
|
279
|
+
for (const cls of op.classes || []) {
|
|
280
|
+
if (cls) needles.push({ kind: 'class', needle: cls });
|
|
281
|
+
}
|
|
282
|
+
if (op.tag) needles.push({ kind: 'tag', needle: '<' + op.tag });
|
|
283
|
+
|
|
284
|
+
const out = [];
|
|
285
|
+
const seen = new Set();
|
|
286
|
+
for (const { kind, needle } of needles) {
|
|
287
|
+
for (const match of findMatches(searchFiles, needle, { kind, max })) {
|
|
288
|
+
const key = match.file + ':' + match.line + ':' + kind + ':' + needle;
|
|
289
|
+
if (seen.has(key)) continue;
|
|
290
|
+
seen.add(key);
|
|
291
|
+
out.push({ ...match, needle });
|
|
292
|
+
if (out.length >= max) return out;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function findContextMatches(searchFiles, hints, { maxPerHint, max }) {
|
|
299
|
+
const out = [];
|
|
300
|
+
const seen = new Set();
|
|
301
|
+
for (const hint of hints || []) {
|
|
302
|
+
for (const match of findMatches(searchFiles, hint, { kind: 'context', max: maxPerHint })) {
|
|
303
|
+
const key = match.file + ':' + match.line + ':' + hint;
|
|
304
|
+
if (seen.has(key)) continue;
|
|
305
|
+
seen.add(key);
|
|
306
|
+
out.push({ ...match, needle: hint });
|
|
307
|
+
if (out.length >= max) return out;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function findMatches(searchFiles, needle, { kind, max }) {
|
|
314
|
+
const text = String(needle || '');
|
|
315
|
+
if (!text) return [];
|
|
316
|
+
const out = [];
|
|
317
|
+
for (const file of searchFiles) {
|
|
318
|
+
let index = 0;
|
|
319
|
+
while (out.length < max) {
|
|
320
|
+
index = file.content.indexOf(text, index);
|
|
321
|
+
if (index === -1) break;
|
|
322
|
+
out.push(matchForIndex(file, index, kind, text));
|
|
323
|
+
index += Math.max(1, text.length);
|
|
324
|
+
}
|
|
325
|
+
if (out.length >= max) break;
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function matchForIndex(file, index, kind, needle) {
|
|
331
|
+
const line = file.content.slice(0, index).split('\n').length;
|
|
332
|
+
const lineText = file.lines[line - 1] || '';
|
|
333
|
+
return {
|
|
334
|
+
kind,
|
|
335
|
+
file: file.relativeFile,
|
|
336
|
+
line,
|
|
337
|
+
needle,
|
|
338
|
+
excerpt: lineText.trim().slice(0, 240),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function isPathInsideOrEqual(cwd, file) {
|
|
343
|
+
const rel = path.relative(path.resolve(cwd), path.resolve(file));
|
|
344
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function normalizeText(value) {
|
|
348
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function decodeBasicHtml(value) {
|
|
352
|
+
return value
|
|
353
|
+
.replace(/"/g, '"')
|
|
354
|
+
.replace(/'/g, "'")
|
|
355
|
+
.replace(/'/g, "'")
|
|
356
|
+
.replace(/&/g, '&')
|
|
357
|
+
.replace(/</g, '<')
|
|
358
|
+
.replace(/>/g, '>');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function escapeRegExp(value) {
|
|
362
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
363
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the pending-manual-edits buffer on disk.
|
|
3
|
+
*
|
|
4
|
+
* Location: .impeccable/live/pending-manual-edits.json (project-local).
|
|
5
|
+
* Schema: { version: 1, entries: [{ id, pageUrl, element, ops, stagedAt }] }
|
|
6
|
+
*
|
|
7
|
+
* Each entry corresponds to one Save action from the browser. Ops merge by
|
|
8
|
+
* (pageUrl, ref): if the user re-edits the same element before committing, the
|
|
9
|
+
* existing entry's `newText` is replaced and `originalText` is kept (it holds
|
|
10
|
+
* the real source state).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { getLiveDir } from './impeccable-paths.mjs';
|
|
16
|
+
|
|
17
|
+
const BUFFER_VERSION = 1;
|
|
18
|
+
const BUFFER_FILENAME = 'pending-manual-edits.json';
|
|
19
|
+
|
|
20
|
+
export function getBufferPath(cwd = process.cwd()) {
|
|
21
|
+
return path.join(getLiveDir(cwd), BUFFER_FILENAME);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function readBuffer(cwd = process.cwd()) {
|
|
25
|
+
return readBufferInternal(cwd, { strict: false });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function readBufferStrict(cwd = process.cwd()) {
|
|
29
|
+
return readBufferInternal(cwd, { strict: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readBufferInternal(cwd, { strict }) {
|
|
33
|
+
const filePath = getBufferPath(cwd);
|
|
34
|
+
try {
|
|
35
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.entries)) {
|
|
38
|
+
if (strict) throw new Error('manual_edit_buffer_invalid_schema');
|
|
39
|
+
return { version: BUFFER_VERSION, entries: [] };
|
|
40
|
+
}
|
|
41
|
+
return { version: BUFFER_VERSION, entries: parsed.entries };
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (strict && err?.code !== 'ENOENT') {
|
|
44
|
+
throw new Error('manual_edit_buffer_unreadable: ' + (err.message || String(err)));
|
|
45
|
+
}
|
|
46
|
+
return { version: BUFFER_VERSION, entries: [] };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function writeBuffer(cwd, buffer) {
|
|
51
|
+
const filePath = getBufferPath(cwd);
|
|
52
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
53
|
+
fs.writeFileSync(filePath, JSON.stringify({ version: BUFFER_VERSION, entries: buffer.entries }, null, 2));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Merge a new entry into the buffer. For each op in the new entry, if there's
|
|
58
|
+
* already a buffered op for the same (pageUrl, ref), update that op's newText
|
|
59
|
+
* and keep its original originalText (the true source state). Otherwise add
|
|
60
|
+
* the op (creating an entry if needed).
|
|
61
|
+
*
|
|
62
|
+
* Multiple ops in one Save are allowed; each is keyed by (pageUrl, ref).
|
|
63
|
+
*/
|
|
64
|
+
export function stageEntry(cwd, newEntry) {
|
|
65
|
+
const buf = readBufferStrict(cwd);
|
|
66
|
+
const pageUrl = newEntry.pageUrl;
|
|
67
|
+
for (const newOp of newEntry.ops) {
|
|
68
|
+
let mergedIntoExisting = false;
|
|
69
|
+
for (const existing of buf.entries) {
|
|
70
|
+
if (existing.pageUrl !== pageUrl) continue;
|
|
71
|
+
const existingOpIdx = existing.ops.findIndex((op) => op.ref === newOp.ref);
|
|
72
|
+
if (existingOpIdx >= 0) {
|
|
73
|
+
// Keep the original source text but refresh the latest DOM/source evidence.
|
|
74
|
+
existing.ops[existingOpIdx] = {
|
|
75
|
+
...newOp,
|
|
76
|
+
originalText: existing.ops[existingOpIdx].originalText,
|
|
77
|
+
newText: newOp.newText,
|
|
78
|
+
deleted: newOp.deleted || false,
|
|
79
|
+
};
|
|
80
|
+
if (newEntry.element) existing.element = newEntry.element;
|
|
81
|
+
existing.stagedAt = new Date().toISOString();
|
|
82
|
+
mergedIntoExisting = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (mergedIntoExisting) continue;
|
|
87
|
+
// No existing op for this (pageUrl, ref). Find or create an entry to hold it.
|
|
88
|
+
let entry = buf.entries.find((e) => e.pageUrl === pageUrl && e.id === newEntry.id);
|
|
89
|
+
if (!entry) {
|
|
90
|
+
entry = {
|
|
91
|
+
id: newEntry.id,
|
|
92
|
+
pageUrl,
|
|
93
|
+
element: newEntry.element,
|
|
94
|
+
ops: [],
|
|
95
|
+
stagedAt: new Date().toISOString(),
|
|
96
|
+
};
|
|
97
|
+
buf.entries.push(entry);
|
|
98
|
+
}
|
|
99
|
+
entry.ops.push(newOp);
|
|
100
|
+
entry.stagedAt = new Date().toISOString();
|
|
101
|
+
}
|
|
102
|
+
writeBuffer(cwd, buf);
|
|
103
|
+
return buf;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Remove entries matching a predicate. Returns count of removed *ops* (not
|
|
108
|
+
* entries) so callers report a unit consistent with truncateBuffer and the
|
|
109
|
+
* pill's per-page op count. Empty entries (no ops left) are also pruned.
|
|
110
|
+
*/
|
|
111
|
+
export function removeEntries(cwd, predicate) {
|
|
112
|
+
const buf = readBuffer(cwd);
|
|
113
|
+
let removedOps = 0;
|
|
114
|
+
const kept = [];
|
|
115
|
+
for (const entry of buf.entries) {
|
|
116
|
+
if (predicate(entry)) {
|
|
117
|
+
removedOps += entry.ops?.length || 0;
|
|
118
|
+
} else if (entry.ops && entry.ops.length > 0) {
|
|
119
|
+
kept.push(entry);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
buf.entries = kept;
|
|
123
|
+
writeBuffer(cwd, buf);
|
|
124
|
+
return removedOps;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Count by page for the counter UI. Returns { totalCount, perPage: {[pageUrl]: count} }.
|
|
129
|
+
*/
|
|
130
|
+
export function countByPage(cwd = process.cwd()) {
|
|
131
|
+
const buf = readBuffer(cwd);
|
|
132
|
+
const perPage = {};
|
|
133
|
+
let totalCount = 0;
|
|
134
|
+
for (const entry of buf.entries) {
|
|
135
|
+
const n = entry.ops.length;
|
|
136
|
+
perPage[entry.pageUrl] = (perPage[entry.pageUrl] || 0) + n;
|
|
137
|
+
totalCount += n;
|
|
138
|
+
}
|
|
139
|
+
return { totalCount, perPage };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Truncate the buffer to empty (used by discard-all). Returns the count of
|
|
144
|
+
* removed ops.
|
|
145
|
+
*/
|
|
146
|
+
export function truncateBuffer(cwd) {
|
|
147
|
+
const buf = readBuffer(cwd);
|
|
148
|
+
let removed = 0;
|
|
149
|
+
for (const entry of buf.entries) removed += entry.ops.length;
|
|
150
|
+
writeBuffer(cwd, { version: BUFFER_VERSION, entries: [] });
|
|
151
|
+
return removed;
|
|
152
|
+
}
|