@monoes/monodesign 1.2.0 → 1.2.2
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/cli/bin/commands/ignores.mjs +6 -6
- package/cli/engine/detect-antipatterns-browser.js +7 -0
- package/cli/engine/fix/index.mjs +1 -1
- package/cli/engine/rules/checks.mjs +7 -0
- package/package.json +3 -3
- package/skill/scripts/context-signals.mjs +1 -1
- package/skill/scripts/context.mjs +11 -7
- package/skill/scripts/critique-storage.mjs +3 -3
- package/skill/scripts/detect-csp.mjs +1 -1
- package/skill/scripts/hook-admin.mjs +10 -10
- package/skill/scripts/hook-before-edit.mjs +2 -2
- package/skill/scripts/hook-lib.mjs +2 -2
- package/skill/scripts/live/manual-apply.mjs +3 -3
- package/skill/scripts/live/svelte-component.mjs +1 -1
- package/skill/scripts/live-accept.mjs +1 -1
- package/skill/scripts/live-commit-manual-edits.mjs +2 -2
- package/skill/scripts/live-copy-edit-agent.mjs +1 -1
- package/skill/scripts/live-insert.mjs +2 -2
- package/skill/scripts/live-manual-edit-evidence.mjs +4 -4
- package/skill/scripts/live-server.mjs +39 -2
- package/skill/scripts/live-wrap.mjs +3 -3
|
@@ -149,8 +149,8 @@ function list(cwd) {
|
|
|
149
149
|
const local = readRawDetectionConfig(cwd, { local: true });
|
|
150
150
|
return [
|
|
151
151
|
'Monodesign detector ignores',
|
|
152
|
-
` shared file: ${path.relative(cwd, getConfigPath(cwd)) || getConfigPath(cwd)}`,
|
|
153
|
-
` local file: ${path.relative(cwd, getLocalConfigPath(cwd)) || getLocalConfigPath(cwd)}`,
|
|
152
|
+
` shared file: ${path.relative(cwd, getConfigPath(cwd)).split(path.sep).join('/') || getConfigPath(cwd)}`,
|
|
153
|
+
` local file: ${path.relative(cwd, getLocalConfigPath(cwd)).split(path.sep).join('/') || getLocalConfigPath(cwd)}`,
|
|
154
154
|
'',
|
|
155
155
|
formatConfig('Merged', merged),
|
|
156
156
|
'',
|
|
@@ -203,7 +203,7 @@ function addRule(cwd, args) {
|
|
|
203
203
|
const config = readScopeConfig(cwd, local);
|
|
204
204
|
if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule);
|
|
205
205
|
const target = writeScopeConfig(cwd, config, local);
|
|
206
|
-
return `Added ${rule} to ${local ? 'local' : 'shared'} detector ignoreRules (${path.relative(cwd, target) || target}).`;
|
|
206
|
+
return `Added ${rule} to ${local ? 'local' : 'shared'} detector ignoreRules (${path.relative(cwd, target).split(path.sep).join('/') || target}).`;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
function addFile(cwd, args) {
|
|
@@ -213,7 +213,7 @@ function addFile(cwd, args) {
|
|
|
213
213
|
const config = readScopeConfig(cwd, local);
|
|
214
214
|
if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob);
|
|
215
215
|
const target = writeScopeConfig(cwd, config, local);
|
|
216
|
-
return `Added ${glob} to ${local ? 'local' : 'shared'} detector ignoreFiles (${path.relative(cwd, target) || target}).`;
|
|
216
|
+
return `Added ${glob} to ${local ? 'local' : 'shared'} detector ignoreFiles (${path.relative(cwd, target).split(path.sep).join('/') || target}).`;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
function addValue(cwd, args) {
|
|
@@ -236,7 +236,7 @@ function addValue(cwd, args) {
|
|
|
236
236
|
config.ignoreValues.push(entry);
|
|
237
237
|
}
|
|
238
238
|
const target = writeScopeConfig(cwd, config, local);
|
|
239
|
-
return `Added ${parsed.rule}=${parsed.value} to ${local ? 'local' : 'shared'} detector ignoreValues (${path.relative(cwd, target) || target}).`;
|
|
239
|
+
return `Added ${parsed.rule}=${parsed.value} to ${local ? 'local' : 'shared'} detector ignoreValues (${path.relative(cwd, target).split(path.sep).join('/') || target}).`;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
function removeFromScopes(cwd, args, remover) {
|
|
@@ -248,7 +248,7 @@ function removeFromScopes(cwd, args, remover) {
|
|
|
248
248
|
const count = remover(config, rest);
|
|
249
249
|
if (count > 0) {
|
|
250
250
|
const target = writeScopeConfig(cwd, config, isLocal);
|
|
251
|
-
removed.push(`${count} from ${isLocal ? 'local' : 'shared'} (${path.relative(cwd, target) || target})`);
|
|
251
|
+
removed.push(`${count} from ${isLocal ? 'local' : 'shared'} (${path.relative(cwd, target).split(path.sep).join('/') || target})`);
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
return removed.length ? `Removed ${removed.join(', ')}.` : 'No matching detector ignore found.';
|
|
@@ -3568,6 +3568,13 @@ function checkElementImageDimensions(el, style) {
|
|
|
3568
3568
|
}
|
|
3569
3569
|
|
|
3570
3570
|
function checkElementImageDimensionsDOM(el) {
|
|
3571
|
+
// Only <img> can have this problem. The static engine gets this for free —
|
|
3572
|
+
// it registers the rule with `selector: 'img'` (engines/static-html/
|
|
3573
|
+
// detect-html.mjs) — but the DOM walker calls every check against every
|
|
3574
|
+
// element, so the tag filter has to live here. Without it this rule fired on
|
|
3575
|
+
// <head>, <meta>, <title> and <style>: a clean 84-line fixture reported 43
|
|
3576
|
+
// findings, all of them "an <img> ships without width and height".
|
|
3577
|
+
if (el.tagName !== 'IMG') return [];
|
|
3571
3578
|
const hasWidthAttr = el.getAttribute('width') != null;
|
|
3572
3579
|
const hasHeightAttr = el.getAttribute('height') != null;
|
|
3573
3580
|
// Browser computed `height` always resolves to a used px value after layout,
|
package/cli/engine/fix/index.mjs
CHANGED
|
@@ -257,7 +257,7 @@ async function runFix(targets, options = {}) {
|
|
|
257
257
|
const diffs = [];
|
|
258
258
|
if (dryRun) {
|
|
259
259
|
for (const [p, b] of changedFiles) {
|
|
260
|
-
diffs.push({ file: p, diff: unifiedDiff(b.original, b.content, path.relative(cwd, p) || p) });
|
|
260
|
+
diffs.push({ file: p, diff: unifiedDiff(b.original, b.content, path.relative(cwd, p).split(path.sep).join('/') || p) });
|
|
261
261
|
}
|
|
262
262
|
} else {
|
|
263
263
|
for (const [p, b] of changedFiles) writeFileAtomic(p, b.content);
|
|
@@ -2845,6 +2845,13 @@ function checkElementImageDimensions(el, style) {
|
|
|
2845
2845
|
}
|
|
2846
2846
|
|
|
2847
2847
|
function checkElementImageDimensionsDOM(el) {
|
|
2848
|
+
// Only <img> can have this problem. The static engine gets this for free —
|
|
2849
|
+
// it registers the rule with `selector: 'img'` (engines/static-html/
|
|
2850
|
+
// detect-html.mjs) — but the DOM walker calls every check against every
|
|
2851
|
+
// element, so the tag filter has to live here. Without it this rule fired on
|
|
2852
|
+
// <head>, <meta>, <title> and <style>: a clean 84-line fixture reported 43
|
|
2853
|
+
// findings, all of them "an <img> ships without width and height".
|
|
2854
|
+
if (el.tagName !== 'IMG') return [];
|
|
2848
2855
|
const hasWidthAttr = el.getAttribute('width') != null;
|
|
2849
2856
|
const hasHeightAttr = el.getAttribute('height') != null;
|
|
2850
2857
|
// Browser computed `height` always resolves to a used px value after layout,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monodesign",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Frontend design intelligence for AI agents \u2014 design tokens, antipattern detection, and the monodesign skill",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "tsc",
|
|
24
|
-
"test": "node
|
|
24
|
+
"test": "node scripts/run-tests.mjs",
|
|
25
25
|
"sync": "node scripts/sync-skill.mjs",
|
|
26
26
|
"prepare": "node scripts/sync-skill.mjs"
|
|
27
27
|
},
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"marked": "^18.0.0"
|
|
46
46
|
},
|
|
47
47
|
"optionalDependencies": {
|
|
48
|
-
"@monoes/monobrowse": "^1.0.
|
|
48
|
+
"@monoes/monobrowse": "^1.0.6",
|
|
49
49
|
"puppeteer": "^25.1.0"
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -93,7 +93,7 @@ function latestCritique(cwd) {
|
|
|
93
93
|
p0: numKey('p0', 'p0_count'),
|
|
94
94
|
p1: numKey('p1', 'p1_count'),
|
|
95
95
|
timestamp: get('timestamp'),
|
|
96
|
-
file: path.relative(cwd, path.join(dir, newest)),
|
|
96
|
+
file: path.relative(cwd, path.join(dir, newest)).split(path.sep).join('/'),
|
|
97
97
|
trend,
|
|
98
98
|
openP0,
|
|
99
99
|
};
|
|
@@ -75,10 +75,10 @@ export function loadContext(cwd = process.cwd(), options = {}) {
|
|
|
75
75
|
return {
|
|
76
76
|
hasProduct: !!product,
|
|
77
77
|
product,
|
|
78
|
-
productPath: productPath ? path.relative(absCwd, productPath) : null,
|
|
78
|
+
productPath: productPath ? path.relative(absCwd, productPath).split(path.sep).join('/') : null,
|
|
79
79
|
hasDesign: !!design,
|
|
80
80
|
design,
|
|
81
|
-
designPath: designPath ? path.relative(absCwd, designPath) : null,
|
|
81
|
+
designPath: designPath ? path.relative(absCwd, designPath).split(path.sep).join('/') : null,
|
|
82
82
|
contextDir: resolved.contextDir,
|
|
83
83
|
productContextDir: productPath ? path.dirname(productPath) : null,
|
|
84
84
|
designContextDir: designPath ? path.dirname(designPath) : null,
|
|
@@ -186,7 +186,7 @@ function resolveProject(cwd = process.cwd(), options = {}) {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
function isPathInside(candidate, root) {
|
|
189
|
-
const rel = path.relative(root, candidate);
|
|
189
|
+
const rel = path.relative(root, candidate).split(path.sep).join('/');
|
|
190
190
|
return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
191
191
|
}
|
|
192
192
|
|
|
@@ -339,9 +339,9 @@ function contextSourceStatus(filePath, repoRoot, projectRoot) {
|
|
|
339
339
|
|
|
340
340
|
function contextSourcePath(filePath, repoRoot) {
|
|
341
341
|
if (!filePath) return null;
|
|
342
|
-
const rel = path.relative(repoRoot, filePath);
|
|
342
|
+
const rel = path.relative(repoRoot, filePath).split(path.sep).join('/');
|
|
343
343
|
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) {
|
|
344
|
-
return rel
|
|
344
|
+
return rel;
|
|
345
345
|
}
|
|
346
346
|
return filePath;
|
|
347
347
|
}
|
|
@@ -447,9 +447,13 @@ function findTargetExample(repoRoot, projectRoot) {
|
|
|
447
447
|
}
|
|
448
448
|
|
|
449
449
|
function resolveWorkspaceProjectRoot(repoRoot, targetDir) {
|
|
450
|
-
const rel = path.relative(repoRoot, targetDir);
|
|
450
|
+
const rel = path.relative(repoRoot, targetDir).split(path.sep).join('/');
|
|
451
451
|
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return repoRoot;
|
|
452
|
-
|
|
452
|
+
// rel is POSIX-normalised above, so split on '/' — splitting on path.sep here
|
|
453
|
+
// returned the whole string as a single segment on Windows, no workspace
|
|
454
|
+
// pattern ever matched, and every child project silently resolved to the
|
|
455
|
+
// repo root instead.
|
|
456
|
+
const relSegments = rel.split('/').filter(Boolean);
|
|
453
457
|
const patterns = readWorkspacePatterns(repoRoot);
|
|
454
458
|
const excluded = isExcludedByWorkspacePattern(relSegments, patterns);
|
|
455
459
|
if (!excluded) {
|
|
@@ -64,7 +64,7 @@ export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
|
|
|
64
64
|
// File path. Make it project-relative so two devs critiquing the same
|
|
65
65
|
// checkout get the same slug regardless of where their repo is cloned.
|
|
66
66
|
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
|
67
|
-
let rel = path.relative(cwd, abs);
|
|
67
|
+
let rel = path.relative(cwd, abs).split(path.sep).join('/');
|
|
68
68
|
// If the target is outside cwd, fall back to the basename so we still
|
|
69
69
|
// produce a stable slug (vs the absolute path, which would include
|
|
70
70
|
// home dirs / usernames).
|
|
@@ -231,7 +231,7 @@ export function mirrorToMemory({ slug, meta = {}, filePath, cwd = process.cwd(),
|
|
|
231
231
|
p1: asFiniteNumber(meta.p1_count ?? meta.p1),
|
|
232
232
|
date: now.toISOString(),
|
|
233
233
|
slug,
|
|
234
|
-
path: filePath ? path.relative(cwd, filePath) : null,
|
|
234
|
+
path: filePath ? path.relative(cwd, filePath).split(path.sep).join('/') : null,
|
|
235
235
|
};
|
|
236
236
|
const project = kebab(path.basename(path.resolve(cwd))) || 'project';
|
|
237
237
|
const key = `${project}-${slug}`;
|
|
@@ -340,7 +340,7 @@ export function formatRecall(recall, { cwd = process.cwd() } = {}) {
|
|
|
340
340
|
if (!issues.p0.length && !issues.p1.length) {
|
|
341
341
|
lines.push('- No open P0/P1 lines found in the latest snapshot.');
|
|
342
342
|
}
|
|
343
|
-
lines.push(`- Snapshot: ${path.relative(cwd, latest.path)}`);
|
|
343
|
+
lines.push(`- Snapshot: ${path.relative(cwd, latest.path).split(path.sep).join('/')}`);
|
|
344
344
|
return lines.join('\n');
|
|
345
345
|
}
|
|
346
346
|
|
|
@@ -186,7 +186,7 @@ function walk(root, dir, depth, visit) {
|
|
|
186
186
|
body = buf.slice(0, n).toString('utf-8');
|
|
187
187
|
} finally { fs.closeSync(fd); }
|
|
188
188
|
} catch { continue; }
|
|
189
|
-
visit(abs, path.relative(root, abs), body);
|
|
189
|
+
visit(abs, path.relative(root, abs).split(path.sep).join('/'), body);
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
|
|
@@ -275,9 +275,9 @@ function statusReport(cwd) {
|
|
|
275
275
|
const cfg = readConfig(cwd);
|
|
276
276
|
const envKill = process.env.MONODESIGN_HOOK_DISABLED;
|
|
277
277
|
const envState = envKill ? `MONODESIGN_HOOK_DISABLED=${envKill}` : 'unset';
|
|
278
|
-
const cfgPath = path.relative(cwd, getConfigPath(cwd)) || '.monodesign/config.json';
|
|
279
|
-
const localPath = path.relative(cwd, getLocalConfigPath(cwd)) || '.monodesign/config.local.json';
|
|
280
|
-
const cachePath = path.relative(cwd, getCachePath(cwd)) || '.monodesign/hook.cache.json';
|
|
278
|
+
const cfgPath = path.relative(cwd, getConfigPath(cwd)).split(path.sep).join('/') || '.monodesign/config.json';
|
|
279
|
+
const localPath = path.relative(cwd, getLocalConfigPath(cwd)).split(path.sep).join('/') || '.monodesign/config.local.json';
|
|
280
|
+
const cachePath = path.relative(cwd, getCachePath(cwd)).split(path.sep).join('/') || '.monodesign/hook.cache.json';
|
|
281
281
|
const fileState = (info, relPath, absent) => {
|
|
282
282
|
if (info.malformed) return `${relPath} (malformed; ignored)`;
|
|
283
283
|
if (info.exists) return relPath;
|
|
@@ -306,14 +306,14 @@ function setEnabled(cwd, value) {
|
|
|
306
306
|
config.enabled = value;
|
|
307
307
|
const target = writeHookConfig(cwd, config);
|
|
308
308
|
if (!value) {
|
|
309
|
-
return `Design hook disabled for this project (wrote ${path.relative(cwd, target) || target}).`;
|
|
309
|
+
return `Design hook disabled for this project (wrote ${path.relative(cwd, target).split(path.sep).join('/') || target}).`;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
const localTarget = writeHookConfig(cwd, { consent: 'accepted' }, { local: true });
|
|
313
313
|
const repaired = repairHookManifests(cwd);
|
|
314
314
|
const parts = [
|
|
315
|
-
`Design hook enabled for this project (wrote ${path.relative(cwd, target) || target}).`,
|
|
316
|
-
`Recorded local hook consent in ${path.relative(cwd, localTarget) || localTarget}.`,
|
|
315
|
+
`Design hook enabled for this project (wrote ${path.relative(cwd, target).split(path.sep).join('/') || target}).`,
|
|
316
|
+
`Recorded local hook consent in ${path.relative(cwd, localTarget).split(path.sep).join('/') || localTarget}.`,
|
|
317
317
|
];
|
|
318
318
|
if (repaired.written.length > 0) {
|
|
319
319
|
parts.push(`Installed or repaired hook manifests for: ${repaired.written.join(', ')}.`);
|
|
@@ -323,7 +323,7 @@ function setEnabled(cwd, value) {
|
|
|
323
323
|
parts.push('No installed provider skill folders found to repair.');
|
|
324
324
|
}
|
|
325
325
|
if (repaired.backups.length > 0) {
|
|
326
|
-
parts.push(`Backed up malformed manifest(s): ${repaired.backups.map((filePath) => path.relative(cwd, filePath) || filePath).join(', ')}.`);
|
|
326
|
+
parts.push(`Backed up malformed manifest(s): ${repaired.backups.map((filePath) => path.relative(cwd, filePath).split(path.sep).join('/') || filePath).join(', ')}.`);
|
|
327
327
|
}
|
|
328
328
|
return parts.join(' ');
|
|
329
329
|
}
|
|
@@ -596,7 +596,7 @@ function addIgnoreValue(cwd, args) {
|
|
|
596
596
|
|
|
597
597
|
const target = writeDetectorConfig(cwd, config, { local });
|
|
598
598
|
const scope = local ? 'local detector.ignoreValues' : 'shared detector.ignoreValues';
|
|
599
|
-
return `Added ${parsed.rule}=${parsed.value} to ${scope} (${path.relative(cwd, target) || target}).`;
|
|
599
|
+
return `Added ${parsed.rule}=${parsed.value} to ${scope} (${path.relative(cwd, target).split(path.sep).join('/') || target}).`;
|
|
600
600
|
}
|
|
601
601
|
|
|
602
602
|
function reset(cwd) {
|
|
@@ -613,7 +613,7 @@ function reset(cwd) {
|
|
|
613
613
|
} else {
|
|
614
614
|
fs.writeFileSync(filePath, JSON.stringify(rest, null, 2) + '\n');
|
|
615
615
|
}
|
|
616
|
-
removed.push(path.relative(cwd, filePath) || filePath);
|
|
616
|
+
removed.push(path.relative(cwd, filePath).split(path.sep).join('/') || filePath);
|
|
617
617
|
} catch { /* ignore */ }
|
|
618
618
|
}
|
|
619
619
|
// State files are wholly ours; delete outright.
|
|
@@ -621,7 +621,7 @@ function reset(cwd) {
|
|
|
621
621
|
try {
|
|
622
622
|
if (fs.existsSync(filePath)) {
|
|
623
623
|
fs.unlinkSync(filePath);
|
|
624
|
-
removed.push(path.relative(cwd, filePath) || filePath);
|
|
624
|
+
removed.push(path.relative(cwd, filePath).split(path.sep).join('/') || filePath);
|
|
625
625
|
}
|
|
626
626
|
} catch { /* ignore */ }
|
|
627
627
|
}
|
|
@@ -320,7 +320,7 @@ function escapeRegExp(value) {
|
|
|
320
320
|
|
|
321
321
|
function relativePath(filePath, cwd) {
|
|
322
322
|
try {
|
|
323
|
-
const rel = path.relative(cwd, filePath);
|
|
323
|
+
const rel = path.relative(cwd, filePath).split(path.sep).join('/');
|
|
324
324
|
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return filePath;
|
|
325
325
|
return rel.split(path.sep).join('/');
|
|
326
326
|
} catch {
|
|
@@ -330,7 +330,7 @@ function relativePath(filePath, cwd) {
|
|
|
330
330
|
|
|
331
331
|
function isInsideProject(filePath, cwd) {
|
|
332
332
|
try {
|
|
333
|
-
const rel = path.relative(cwd, filePath);
|
|
333
|
+
const rel = path.relative(cwd, filePath).split(path.sep).join('/');
|
|
334
334
|
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
335
335
|
} catch {
|
|
336
336
|
return false;
|
|
@@ -1015,7 +1015,7 @@ function quoteCommandArg(value) {
|
|
|
1015
1015
|
|
|
1016
1016
|
function relativize(filePath, cwd) {
|
|
1017
1017
|
try {
|
|
1018
|
-
const rel = path.relative(cwd, filePath);
|
|
1018
|
+
const rel = path.relative(cwd, filePath).split(path.sep).join('/');
|
|
1019
1019
|
if (!rel || rel.startsWith('..')) return filePath;
|
|
1020
1020
|
return rel.split(path.sep).join('/');
|
|
1021
1021
|
} catch {
|
|
@@ -1226,7 +1226,7 @@ function hasPathTraversal(filePath) {
|
|
|
1226
1226
|
function isInsideProject(filePath, projectCwd) {
|
|
1227
1227
|
if (!filePath || !projectCwd || hasPathTraversal(filePath)) return false;
|
|
1228
1228
|
try {
|
|
1229
|
-
const rel = path.relative(projectCwd, filePath);
|
|
1229
|
+
const rel = path.relative(projectCwd, filePath).split(path.sep).join('/');
|
|
1230
1230
|
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
1231
1231
|
} catch {
|
|
1232
1232
|
return false;
|
|
@@ -338,7 +338,7 @@ export function normalizeManualApplyEvidencePath(evidencePath, cwd = process.cwd
|
|
|
338
338
|
if (!evidencePath || typeof evidencePath !== 'string') return null;
|
|
339
339
|
const fullPath = path.isAbsolute(evidencePath) ? evidencePath : path.resolve(cwd, evidencePath);
|
|
340
340
|
const evidenceDir = manualApplyEvidenceDir(cwd);
|
|
341
|
-
const relative = path.relative(evidenceDir, fullPath);
|
|
341
|
+
const relative = path.relative(evidenceDir, fullPath).split(path.sep).join('/');
|
|
342
342
|
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null;
|
|
343
343
|
if (path.extname(relative) !== '.json') return null;
|
|
344
344
|
return fullPath;
|
|
@@ -837,7 +837,7 @@ export function collectManualApplyFiles(batch, extraFiles = [], cwd = process.cw
|
|
|
837
837
|
function normalizeProjectFile(file, cwd = process.cwd()) {
|
|
838
838
|
if (!file || typeof file !== 'string') return null;
|
|
839
839
|
const absolute = path.isAbsolute(file) ? file : path.resolve(cwd, file);
|
|
840
|
-
const relative = path.relative(cwd, absolute);
|
|
840
|
+
const relative = path.relative(cwd, absolute).split(path.sep).join('/');
|
|
841
841
|
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null;
|
|
842
842
|
return relative;
|
|
843
843
|
}
|
|
@@ -927,7 +927,7 @@ export function summarizeManualDiagnostics(items, cwd = process.cwd()) {
|
|
|
927
927
|
export function summarizeManualLogFile(file, cwd = process.cwd()) {
|
|
928
928
|
if (!file || typeof file !== 'string') return undefined;
|
|
929
929
|
if (!path.isAbsolute(file)) return file;
|
|
930
|
-
const relative = path.relative(cwd, file);
|
|
930
|
+
const relative = path.relative(cwd, file).split(path.sep).join('/');
|
|
931
931
|
return relative && !relative.startsWith('..') && !path.isAbsolute(relative) ? relative : file;
|
|
932
932
|
}
|
|
933
933
|
|
|
@@ -265,7 +265,7 @@ export function resolveSourceFile(sourceFile, cwd = process.cwd()) {
|
|
|
265
265
|
throw new Error('Invalid svelte-component source file');
|
|
266
266
|
}
|
|
267
267
|
const full = path.resolve(cwd, sourceFile);
|
|
268
|
-
const rel = path.relative(cwd, full);
|
|
268
|
+
const rel = path.relative(cwd, full).split(path.sep).join('/');
|
|
269
269
|
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
270
270
|
throw new Error('Svelte-component source file escapes project root');
|
|
271
271
|
}
|
|
@@ -119,7 +119,7 @@ Output (JSON):
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
const { file: targetFile, content, lines } = found;
|
|
122
|
-
const relFile = path.relative(process.cwd(), targetFile);
|
|
122
|
+
const relFile = path.relative(process.cwd(), targetFile).split(path.sep).join('/');
|
|
123
123
|
const previewBlock = findMarkerBlock(id, lines);
|
|
124
124
|
const sourceShadowPreview = previewBlock
|
|
125
125
|
? readSourceShadowPreviewMeta(content, id)
|
|
@@ -224,7 +224,7 @@ function buildRepairBatch(batch, repair) {
|
|
|
224
224
|
function normalizeProjectSourcePath(cwd, file, opts = {}) {
|
|
225
225
|
if (!file || typeof file !== 'string') return null;
|
|
226
226
|
const absolute = path.isAbsolute(file) ? file : path.resolve(cwd, file);
|
|
227
|
-
const relative = path.relative(cwd, absolute);
|
|
227
|
+
const relative = path.relative(cwd, absolute).split(path.sep).join('/');
|
|
228
228
|
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null;
|
|
229
229
|
if (opts.requireExists && !fs.existsSync(absolute)) return null;
|
|
230
230
|
if (isGeneratedFile(absolute, { cwd })) return null;
|
|
@@ -622,7 +622,7 @@ function scanRollbackDir(dir, cwd, out, seenDirs, seenFiles, depth) {
|
|
|
622
622
|
try { realFile = fs.realpathSync(absolute); } catch { continue; }
|
|
623
623
|
if (seenFiles.has(realFile)) continue;
|
|
624
624
|
seenFiles.add(realFile);
|
|
625
|
-
const relative = path.relative(cwd, absolute);
|
|
625
|
+
const relative = path.relative(cwd, absolute).split(path.sep).join('/');
|
|
626
626
|
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) continue;
|
|
627
627
|
out.push(relative);
|
|
628
628
|
}
|
|
@@ -534,7 +534,7 @@ function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, m
|
|
|
534
534
|
}
|
|
535
535
|
|
|
536
536
|
function isPathInsideOrEqual(cwd, file) {
|
|
537
|
-
const relative = path.relative(path.resolve(cwd), path.resolve(file));
|
|
537
|
+
const relative = path.relative(path.resolve(cwd), path.resolve(file)).split(path.sep).join('/');
|
|
538
538
|
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
539
539
|
}
|
|
540
540
|
|
|
@@ -166,7 +166,7 @@ Output (JSON):
|
|
|
166
166
|
console.error(JSON.stringify({
|
|
167
167
|
error: 'file_is_generated',
|
|
168
168
|
fallback: 'agent-driven',
|
|
169
|
-
file: path.relative(process.cwd(), path.resolve(process.cwd(), targetFile)),
|
|
169
|
+
file: path.relative(process.cwd(), path.resolve(process.cwd(), targetFile)).split(path.sep).join('/'),
|
|
170
170
|
}));
|
|
171
171
|
process.exit(1);
|
|
172
172
|
}
|
|
@@ -179,7 +179,7 @@ Output (JSON):
|
|
|
179
179
|
console.error(JSON.stringify({
|
|
180
180
|
error: 'element_ambiguous',
|
|
181
181
|
fallback: 'agent-driven',
|
|
182
|
-
file: path.relative(process.cwd(), targetFile),
|
|
182
|
+
file: path.relative(process.cwd(), targetFile).split(path.sep).join('/'),
|
|
183
183
|
candidates: resolved.candidates.map((c) => ({
|
|
184
184
|
startLine: c.startLine + 1,
|
|
185
185
|
endLine: c.endLine + 1,
|
|
@@ -64,7 +64,7 @@ export function buildManualEditEvidence({ cwd = process.cwd(), pageUrl = null }
|
|
|
64
64
|
ops,
|
|
65
65
|
context: {
|
|
66
66
|
cwd,
|
|
67
|
-
bufferPath: path.relative(cwd, getBufferPath(cwd)),
|
|
67
|
+
bufferPath: path.relative(cwd, getBufferPath(cwd)).split(path.sep).join('/'),
|
|
68
68
|
totalEntries: entries.length,
|
|
69
69
|
totalOps: opCount,
|
|
70
70
|
},
|
|
@@ -157,7 +157,7 @@ function analyzeSourceHint(op, cwd) {
|
|
|
157
157
|
const hint = normalizeSourceHint(op.sourceHint);
|
|
158
158
|
if (!hint.file) return null;
|
|
159
159
|
const file = path.resolve(cwd, hint.file);
|
|
160
|
-
const relativeFile = path.relative(cwd, file);
|
|
160
|
+
const relativeFile = path.relative(cwd, file).split(path.sep).join('/');
|
|
161
161
|
if (!isPathInsideOrEqual(cwd, file)) {
|
|
162
162
|
return { ...hint, status: 'outside_cwd', relativeFile: hint.file };
|
|
163
163
|
}
|
|
@@ -254,7 +254,7 @@ function maybeAddSearchFile(file, cwd, seenFiles, out) {
|
|
|
254
254
|
if (isGeneratedFile(file, { cwd })) return;
|
|
255
255
|
let content;
|
|
256
256
|
try { content = fs.readFileSync(file, 'utf-8'); } catch { return; }
|
|
257
|
-
out.push({ file, relativeFile: path.relative(cwd, file), content, lines: content.split('\n') });
|
|
257
|
+
out.push({ file, relativeFile: path.relative(cwd, file).split(path.sep).join('/'), content, lines: content.split('\n') });
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
function findLiteralMatches(searchFiles, needle, { max }) {
|
|
@@ -340,7 +340,7 @@ function matchForIndex(file, index, kind, needle) {
|
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
function isPathInsideOrEqual(cwd, file) {
|
|
343
|
-
const rel = path.relative(path.resolve(cwd), path.resolve(file));
|
|
343
|
+
const rel = path.relative(path.resolve(cwd), path.resolve(file)).split(path.sep).join('/');
|
|
344
344
|
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
345
345
|
}
|
|
346
346
|
|
|
@@ -789,7 +789,7 @@ function sessionFileMetadataFromPollReply(file) {
|
|
|
789
789
|
let full;
|
|
790
790
|
try {
|
|
791
791
|
full = path.resolve(process.cwd(), normalized);
|
|
792
|
-
const rel = path.relative(process.cwd(), full);
|
|
792
|
+
const rel = path.relative(process.cwd(), full).split(path.sep).join('/');
|
|
793
793
|
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return base;
|
|
794
794
|
} catch {
|
|
795
795
|
return base;
|
|
@@ -1112,7 +1112,8 @@ applyLegacyDeferredAcceptsOnStartup();
|
|
|
1112
1112
|
restorePendingEventsFromStore();
|
|
1113
1113
|
manualApply.pruneStaleEvidence();
|
|
1114
1114
|
const portArg = args.find(a => a.startsWith('--port='));
|
|
1115
|
-
|
|
1115
|
+
const explicitPort = portArg ? parseInt(portArg.split('=')[1], 10) : null;
|
|
1116
|
+
state.port = explicitPort ?? await findOpenPort();
|
|
1116
1117
|
// Annotation screenshots live in the project root so the agent's Read tool
|
|
1117
1118
|
// doesn't trip a per-file permission prompt. Sessioned by token so concurrent
|
|
1118
1119
|
// projects (or quick restarts) don't collide.
|
|
@@ -1123,7 +1124,43 @@ state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-'));
|
|
|
1123
1124
|
const { detectScript, liveScriptParts } = loadBrowserScripts();
|
|
1124
1125
|
httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts }));
|
|
1125
1126
|
|
|
1127
|
+
// findOpenPort() probes a port, closes the probe socket, and only then does the
|
|
1128
|
+
// real server bind it. Another process scanning the same range can take the
|
|
1129
|
+
// port inside that gap, which surfaced as an intermittent
|
|
1130
|
+
// "EADDRINUSE 127.0.0.1:8405" — about one run in five of the test suite, where
|
|
1131
|
+
// several server-starting test files run in parallel.
|
|
1132
|
+
//
|
|
1133
|
+
// Retry on the next port when we picked it ourselves. An explicit --port is
|
|
1134
|
+
// never silently moved: the caller asked for that port, so a conflict there
|
|
1135
|
+
// must be an error they can see.
|
|
1136
|
+
const MAX_PORT_RETRIES = 25;
|
|
1137
|
+
let portRetries = 0;
|
|
1138
|
+
|
|
1139
|
+
httpServer.on('error', (err) => {
|
|
1140
|
+
if (err?.code !== 'EADDRINUSE') throw err;
|
|
1141
|
+
|
|
1142
|
+
if (explicitPort !== null) {
|
|
1143
|
+
console.error(`\nPort ${state.port} is already in use.`);
|
|
1144
|
+
console.error('Another live server may be running — stop it, or pass a different --port.');
|
|
1145
|
+
process.exit(1);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (portRetries >= MAX_PORT_RETRIES) {
|
|
1149
|
+
console.error(`\nCould not find a free port after ${MAX_PORT_RETRIES} attempts (last tried ${state.port}).`);
|
|
1150
|
+
process.exit(1);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
portRetries++;
|
|
1154
|
+
state.port++;
|
|
1155
|
+
httpServer.listen(state.port, '127.0.0.1');
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1126
1158
|
httpServer.listen(state.port, '127.0.0.1', () => {
|
|
1159
|
+
// Trust the address the OS actually bound over the one we asked for — after
|
|
1160
|
+
// a retry above, state.port and the bound port must not drift apart.
|
|
1161
|
+
const boundAddr = httpServer.address();
|
|
1162
|
+
if (boundAddr && typeof boundAddr === 'object') state.port = boundAddr.port;
|
|
1163
|
+
|
|
1127
1164
|
writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token });
|
|
1128
1165
|
const url = `http://localhost:${state.port}`;
|
|
1129
1166
|
console.log(`\nMonodesign live server running on ${url}`);
|
|
@@ -102,7 +102,7 @@ The agent should insert variant HTML at insertLine.`);
|
|
|
102
102
|
console.error(JSON.stringify({
|
|
103
103
|
error: 'element_not_in_source',
|
|
104
104
|
fallback: 'agent-driven',
|
|
105
|
-
generatedMatch: path.relative(process.cwd(), generatedHit),
|
|
105
|
+
generatedMatch: path.relative(process.cwd(), generatedHit).split(path.sep).join('/'),
|
|
106
106
|
hint: 'Element found only in a generated file. See "Handle fallback" in live.md.',
|
|
107
107
|
}));
|
|
108
108
|
} else {
|
|
@@ -119,7 +119,7 @@ The agent should insert variant HTML at insertLine.`);
|
|
|
119
119
|
console.error(JSON.stringify({
|
|
120
120
|
error: 'file_is_generated',
|
|
121
121
|
fallback: 'agent-driven',
|
|
122
|
-
file: path.relative(process.cwd(), path.resolve(process.cwd(), targetFile)),
|
|
122
|
+
file: path.relative(process.cwd(), path.resolve(process.cwd(), targetFile)).split(path.sep).join('/'),
|
|
123
123
|
hint: 'Explicit --file points at a generated file. Writing here gets wiped by the next build. See "Handle fallback" in live.md.',
|
|
124
124
|
}));
|
|
125
125
|
process.exit(1);
|
|
@@ -172,7 +172,7 @@ The agent should insert variant HTML at insertLine.`);
|
|
|
172
172
|
console.error(JSON.stringify({
|
|
173
173
|
error: 'element_ambiguous',
|
|
174
174
|
fallback: 'agent-driven',
|
|
175
|
-
file: path.relative(process.cwd(), targetFile),
|
|
175
|
+
file: path.relative(process.cwd(), targetFile).split(path.sep).join('/'),
|
|
176
176
|
candidates: filtered.map((c) => ({
|
|
177
177
|
startLine: c.startLine + 1,
|
|
178
178
|
endLine: c.endLine + 1,
|