@monoes/monodesign 1.2.4 → 1.2.6
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/README.md +6 -6
- package/cli/engine/engines/browser/drivers.mjs +82 -1
- package/cli/engine/rules/checks.mjs +24 -1
- package/dist/antipatterns.d.ts +28 -0
- package/dist/antipatterns.d.ts.map +1 -0
- package/dist/antipatterns.js +108 -0
- package/dist/antipatterns.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/{src/index.ts → dist/index.js} +1 -1
- package/dist/index.js.map +1 -0
- package/dist/rules.d.ts +37 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +97 -0
- package/dist/rules.js.map +1 -0
- package/dist/tokens.d.ts +91 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +91 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +9 -5
- package/skill/reference/hooks.md +2 -2
- package/skill/scripts/hook-admin.mjs +51 -18
- package/skill/scripts/live/sveltekit-adapter.mjs +11 -6
- package/skill/scripts/live-inject.mjs +20 -7
- package/skill/scripts/live-server.mjs +38 -2
- package/src/antipatterns.ts +0 -164
- package/src/rules.ts +0 -129
- package/src/tokens.ts +0 -99
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** Design tokens as typed JS constants — mirrors tokens.css */
|
|
2
|
+
export const fontFamily = {
|
|
3
|
+
display: "'Cormorant Garamond', Georgia, serif",
|
|
4
|
+
body: "'Instrument Sans', system-ui, sans-serif",
|
|
5
|
+
mono: "'Space Grotesk', monospace",
|
|
6
|
+
};
|
|
7
|
+
export const fontSize = {
|
|
8
|
+
xs: '0.75rem',
|
|
9
|
+
sm: '0.875rem',
|
|
10
|
+
base: '1rem',
|
|
11
|
+
lg: '1.125rem',
|
|
12
|
+
xl: '1.25rem',
|
|
13
|
+
'2xl': '1.5rem',
|
|
14
|
+
'3xl': '1.875rem',
|
|
15
|
+
'4xl': '2.25rem',
|
|
16
|
+
'5xl': '3rem',
|
|
17
|
+
'6xl': '3.75rem',
|
|
18
|
+
'7xl': '4.5rem',
|
|
19
|
+
};
|
|
20
|
+
export const spacing = {
|
|
21
|
+
1: '4px',
|
|
22
|
+
2: '8px',
|
|
23
|
+
3: '12px',
|
|
24
|
+
4: '16px',
|
|
25
|
+
5: '20px',
|
|
26
|
+
6: '24px',
|
|
27
|
+
7: '28px',
|
|
28
|
+
8: '32px',
|
|
29
|
+
10: '40px',
|
|
30
|
+
12: '48px',
|
|
31
|
+
16: '64px',
|
|
32
|
+
20: '80px',
|
|
33
|
+
24: '96px',
|
|
34
|
+
30: '120px',
|
|
35
|
+
};
|
|
36
|
+
export const easing = {
|
|
37
|
+
out: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
|
38
|
+
outQuint: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
|
39
|
+
inOut: 'cubic-bezier(0.65, 0, 0.35, 1)',
|
|
40
|
+
in: 'cubic-bezier(0.4, 0, 1, 1)',
|
|
41
|
+
};
|
|
42
|
+
export const duration = {
|
|
43
|
+
instant: '0s',
|
|
44
|
+
fast: '0.15s',
|
|
45
|
+
base: '0.3s',
|
|
46
|
+
slow: '0.6s',
|
|
47
|
+
slower: '0.8s',
|
|
48
|
+
slowest: '1.2s',
|
|
49
|
+
};
|
|
50
|
+
/** OKLCH color palette */
|
|
51
|
+
export const color = {
|
|
52
|
+
ink: 'oklch(10% 0 0)',
|
|
53
|
+
charcoal: 'oklch(25% 0 0)',
|
|
54
|
+
ash: 'oklch(55% 0 0)',
|
|
55
|
+
mist: 'oklch(92% 0 0)',
|
|
56
|
+
cream: 'oklch(96% 0.005 350)',
|
|
57
|
+
paper: 'oklch(98% 0 0)',
|
|
58
|
+
white: 'oklch(99.5% 0 0)',
|
|
59
|
+
accent: 'oklch(60% 0.25 350)',
|
|
60
|
+
accentHover: 'oklch(52% 0.25 350)',
|
|
61
|
+
accentDim: 'oklch(60% 0.25 350 / 0.15)',
|
|
62
|
+
accentSoft: 'oklch(60% 0.25 350 / 0.25)',
|
|
63
|
+
accentText: 'oklch(45% 0.22 350)',
|
|
64
|
+
};
|
|
65
|
+
export const accentScale = {
|
|
66
|
+
1: 'oklch(22% 0.15 350)',
|
|
67
|
+
2: 'oklch(33% 0.20 350)',
|
|
68
|
+
3: 'oklch(42% 0.23 350)',
|
|
69
|
+
4: 'oklch(52% 0.25 350)',
|
|
70
|
+
5: 'oklch(60% 0.25 350)',
|
|
71
|
+
6: 'oklch(68% 0.22 350)',
|
|
72
|
+
7: 'oklch(76% 0.18 350)',
|
|
73
|
+
8: 'oklch(84% 0.12 350)',
|
|
74
|
+
9: 'oklch(94% 0.05 350)',
|
|
75
|
+
};
|
|
76
|
+
export const shadow = {
|
|
77
|
+
none: 'none',
|
|
78
|
+
sm: '0 1px 3px rgba(0, 0, 0, 0.06)',
|
|
79
|
+
md: '0 4px 24px -4px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.06)',
|
|
80
|
+
lg: '0 20px 40px rgba(0, 0, 0, 0.08)',
|
|
81
|
+
accent: '0 20px 60px oklch(60% 0.25 350 / 0.15)',
|
|
82
|
+
};
|
|
83
|
+
export const radius = {
|
|
84
|
+
none: '0',
|
|
85
|
+
sm: '4px',
|
|
86
|
+
md: '8px',
|
|
87
|
+
lg: '12px',
|
|
88
|
+
xl: '16px',
|
|
89
|
+
full: '9999px',
|
|
90
|
+
};
|
|
91
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAE/D,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,OAAO,EAAE,sCAAsC;IAC/C,IAAI,EAAE,0CAA0C;IAChD,IAAI,EAAE,4BAA4B;CAC1B,CAAC;AAEX,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,EAAE,EAAE,SAAS;IACb,EAAE,EAAE,UAAU;IACd,IAAI,EAAE,MAAM;IACZ,EAAE,EAAE,UAAU;IACd,EAAE,EAAE,SAAS;IACb,KAAK,EAAE,QAAQ;IACf,KAAK,EAAE,UAAU;IACjB,KAAK,EAAE,SAAS;IAChB,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,SAAS;IAChB,KAAK,EAAE,QAAQ;CACP,CAAC;AAEX,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,CAAC,EAAE,KAAK;IACR,CAAC,EAAE,KAAK;IACR,CAAC,EAAE,MAAM;IACT,CAAC,EAAE,MAAM;IACT,CAAC,EAAE,MAAM;IACT,CAAC,EAAE,MAAM;IACT,CAAC,EAAE,MAAM;IACT,CAAC,EAAE,MAAM;IACT,EAAE,EAAE,MAAM;IACV,EAAE,EAAE,MAAM;IACV,EAAE,EAAE,MAAM;IACV,EAAE,EAAE,MAAM;IACV,EAAE,EAAE,MAAM;IACV,EAAE,EAAE,OAAO;CACH,CAAC;AAEX,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,GAAG,EAAE,+BAA+B;IACpC,QAAQ,EAAE,gCAAgC;IAC1C,KAAK,EAAE,gCAAgC;IACvC,EAAE,EAAE,4BAA4B;CACxB,CAAC;AAEX,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,OAAO,EAAE,IAAI;IACb,IAAI,EAAE,OAAO;IACb,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,MAAM;IACZ,MAAM,EAAE,MAAM;IACd,OAAO,EAAE,MAAM;CACP,CAAC;AAEX,0BAA0B;AAC1B,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,GAAG,EAAE,gBAAgB;IACrB,QAAQ,EAAE,gBAAgB;IAC1B,GAAG,EAAE,gBAAgB;IACrB,IAAI,EAAE,gBAAgB;IACtB,KAAK,EAAE,sBAAsB;IAC7B,KAAK,EAAE,gBAAgB;IACvB,KAAK,EAAE,kBAAkB;IACzB,MAAM,EAAE,qBAAqB;IAC7B,WAAW,EAAE,qBAAqB;IAClC,SAAS,EAAE,4BAA4B;IACvC,UAAU,EAAE,4BAA4B;IACxC,UAAU,EAAE,qBAAqB;CACzB,CAAC;AAEX,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,CAAC,EAAE,qBAAqB;IACxB,CAAC,EAAE,qBAAqB;IACxB,CAAC,EAAE,qBAAqB;IACxB,CAAC,EAAE,qBAAqB;IACxB,CAAC,EAAE,qBAAqB;IACxB,CAAC,EAAE,qBAAqB;IACxB,CAAC,EAAE,qBAAqB;IACxB,CAAC,EAAE,qBAAqB;IACxB,CAAC,EAAE,qBAAqB;CAChB,CAAC;AAEX,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,IAAI,EAAE,MAAM;IACZ,EAAE,EAAE,+BAA+B;IACnC,EAAE,EAAE,oEAAoE;IACxE,EAAE,EAAE,iCAAiC;IACrC,MAAM,EAAE,wCAAwC;CACxC,CAAC;AAEX,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,IAAI,EAAE,GAAG;IACT,EAAE,EAAE,KAAK;IACT,EAAE,EAAE,KAAK;IACT,EAAE,EAAE,MAAM;IACV,EAAE,EAAE,MAAM;IACV,IAAI,EAAE,QAAQ;CACN,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monodesign",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"description": "Frontend design intelligence for AI agents — design tokens, antipattern detection, and the monodesign skill",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
7
8
|
"exports": {
|
|
8
|
-
".": "./
|
|
9
|
+
".": "./dist/index.js",
|
|
9
10
|
"./tokens": "./src/tokens.css",
|
|
10
11
|
"./engine": "./cli/engine/detect-antipatterns.mjs",
|
|
11
12
|
"./engine/browser": "./cli/engine/detect-antipatterns-browser.js"
|
|
@@ -15,12 +16,15 @@
|
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
17
18
|
"cli/",
|
|
19
|
+
"dist/",
|
|
18
20
|
"skill/",
|
|
19
|
-
"src/",
|
|
21
|
+
"src/tokens.css",
|
|
20
22
|
"NOTICE.md"
|
|
21
23
|
],
|
|
22
24
|
"scripts": {
|
|
25
|
+
"prebuild": "node ../../../scripts/build-fs.mjs clean dist tsconfig.tsbuildinfo",
|
|
23
26
|
"build": "tsc",
|
|
27
|
+
"prepublishOnly": "npm run build",
|
|
24
28
|
"test": "node scripts/run-tests.mjs",
|
|
25
29
|
"sync": "node scripts/sync-skill.mjs"
|
|
26
30
|
},
|
|
@@ -44,7 +48,7 @@
|
|
|
44
48
|
"marked": "^18.0.0"
|
|
45
49
|
},
|
|
46
50
|
"optionalDependencies": {
|
|
47
|
-
"@monoes/monobrowse": "^1.0.
|
|
51
|
+
"@monoes/monobrowse": "^1.0.7",
|
|
48
52
|
"puppeteer": "^25.1.0"
|
|
49
53
|
}
|
|
50
54
|
}
|
package/skill/reference/hooks.md
CHANGED
|
@@ -10,7 +10,7 @@ Declare server-side template extensions under **`detector.extensions`** when the
|
|
|
10
10
|
|
|
11
11
|
Manual `monomind design detect` scans use the same project filter config by default: `detector.ignoreRules`, `detector.ignoreFiles`, `detector.ignoreValues`, and `detector.designSystem.enabled`. `hook.enabled` only controls automatic hook execution, not manual CLI scans. Use `monomind design detect --no-config ...` for a raw detector run that ignores project config/context. Use `monomind design ignores ...` for direct CLI CRUD on the same detector ignores.
|
|
12
12
|
|
|
13
|
-
Supported harnesses: Claude Code (`.claude/settings.local.json` in the project, which is gitignored so the hook stays machine-local; a hook you move into the shared `settings.json` is honored in place too), Codex (`.codex/
|
|
13
|
+
Supported harnesses: Claude Code (`.claude/settings.local.json` in the project, which is gitignored so the hook stays machine-local; a hook you move into the shared `settings.json` is honored in place too), Codex (`.codex/config.toml` in the project), Cursor (`.cursor/hooks.json` in the project), and GitHub Copilot (`.github/hooks/monodesign.json` in the project, a team-shared committed file that both the Copilot CLI and the cloud agent read). For the Copilot CLI, repo-level hooks fire once `.github/hooks/monodesign.json` is committed to the repository's default branch.
|
|
14
14
|
|
|
15
15
|
On **Cursor**, `preToolUse` checks proposed Write/Edit/Shell write content and denies only when the real detector finds an issue. The denial message is visible to the agent as the tool error, so the agent can reconsider before the bad write lands.
|
|
16
16
|
|
|
@@ -84,7 +84,7 @@ node {{scripts_path}}/hook-admin.mjs ignore-file "src/legacy/Card.tsx"
|
|
|
84
84
|
- Never modify `.monodesign/config.json` or `.monodesign/config.local.json` by hand from this command. Always go through `hook-admin.mjs` so writes stay validated and the file shape stays consistent. One exception: `detector.extensions` has no admin action, so when the user asks to cover a template stack, edit that one field in `.monodesign/config.json` directly and leave the rest of the file untouched.
|
|
85
85
|
- Do not edit the hook scripts themselves (`hook.mjs`, `hook-lib.mjs`, `hook-before-edit.mjs`) from this flow. Those are skill plumbing.
|
|
86
86
|
- Cursor can block a proposed write when the detector finds a real issue. Claude Code, Codex, and GitHub Copilot do not block the edit; they emit a post-edit reminder instead. Disabling stops both blocking and reminders.
|
|
87
|
-
- The hook is bundled with the Monodesign skill and installed through project-local manifests: `.claude/settings.local.json`, `.codex/
|
|
87
|
+
- The hook is bundled with the Monodesign skill and installed through project-local manifests: `.claude/settings.local.json`, `.codex/config.toml`, `.cursor/hooks.json`, and `.github/hooks/monodesign.json`. On Codex, the user must approve the hook via `/hooks` the first time. On Cursor, confirm hooks are enabled under Settings -> Hooks. On GitHub Copilot, the CLI loads `.github/hooks/monodesign.json` once it is committed to the repository's default branch, and the cloud agent reads it from the repo directly.
|
|
88
88
|
|
|
89
89
|
## Failure modes
|
|
90
90
|
|
|
@@ -74,24 +74,20 @@ const HOOK_MANIFEST_TARGETS = [
|
|
|
74
74
|
{
|
|
75
75
|
provider: '.agents',
|
|
76
76
|
skillRel: '.agents/skills/monodesign',
|
|
77
|
-
destRel: '.codex/
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
},
|
|
92
|
-
],
|
|
93
|
-
},
|
|
94
|
-
}),
|
|
77
|
+
destRel: '.codex/config.toml',
|
|
78
|
+
format: 'toml',
|
|
79
|
+
manifest: () => [
|
|
80
|
+
'# monodesign:start native-hook',
|
|
81
|
+
'[[hooks.PostToolUse]]',
|
|
82
|
+
'matcher = "Edit|Write|apply_patch"',
|
|
83
|
+
'[[hooks.PostToolUse.hooks]]',
|
|
84
|
+
'type = "command"',
|
|
85
|
+
'command = "node .agents/skills/monodesign/scripts/hook.mjs"',
|
|
86
|
+
`timeout = ${TIMEOUT_SECONDS}`,
|
|
87
|
+
`statusMessage = "${STATUS_MESSAGE}"`,
|
|
88
|
+
'# monodesign:end native-hook',
|
|
89
|
+
'',
|
|
90
|
+
].join('\n'),
|
|
95
91
|
},
|
|
96
92
|
{
|
|
97
93
|
provider: '.cursor',
|
|
@@ -335,6 +331,19 @@ function repairHookManifests(cwd) {
|
|
|
335
331
|
const dest = path.join(cwd, target.destRel);
|
|
336
332
|
const sharedDest = target.sharedDestRel ? path.join(cwd, target.sharedDestRel) : null;
|
|
337
333
|
|
|
334
|
+
if (target.format === 'toml') {
|
|
335
|
+
const current = fs.existsSync(dest) ? safeReadText(dest) : '';
|
|
336
|
+
const next = mergeCodexHookConfig(current || '', target.manifest());
|
|
337
|
+
if (current === next) {
|
|
338
|
+
result.already.push(target.provider);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
342
|
+
fs.writeFileSync(dest, next);
|
|
343
|
+
result.written.push(target.provider);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
338
347
|
if (sharedDest && fileHasMonodesignHookMarker(sharedDest)) {
|
|
339
348
|
pruneMonodesignHookFromManifest(dest);
|
|
340
349
|
result.already.push(target.provider);
|
|
@@ -366,6 +375,30 @@ function repairHookManifests(cwd) {
|
|
|
366
375
|
return result;
|
|
367
376
|
}
|
|
368
377
|
|
|
378
|
+
function mergeCodexHookConfig(existing, hookBlock) {
|
|
379
|
+
const completeBlock = /# monodesign:start native-hook[\s\S]*?# monodesign:end native-hook\n?/m;
|
|
380
|
+
const incompleteBlock = /# monodesign:start native-hook[\s\S]*$/m;
|
|
381
|
+
let merged = existing;
|
|
382
|
+
if (completeBlock.test(merged)) merged = merged.replace(completeBlock, hookBlock);
|
|
383
|
+
else if (incompleteBlock.test(merged)) merged = merged.replace(incompleteBlock, hookBlock);
|
|
384
|
+
else merged = `${merged.trimEnd()}${merged.trim() ? '\n\n' : ''}${hookBlock}`;
|
|
385
|
+
return enableCodexHooks(merged);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function enableCodexHooks(config) {
|
|
389
|
+
if (!/^\[features\]\s*$/m.test(config)) {
|
|
390
|
+
return `[features]\nhooks = true\n\n${config}`;
|
|
391
|
+
}
|
|
392
|
+
const lines = config.split(/\r?\n/);
|
|
393
|
+
const start = lines.findIndex((line) => /^\[features\]\s*$/.test(line));
|
|
394
|
+
let end = start + 1;
|
|
395
|
+
while (end < lines.length && !/^\[\[?[^\]]+\]\]?\s*$/.test(lines[end])) end++;
|
|
396
|
+
const hookIndex = lines.slice(start + 1, end).findIndex((line) => /^hooks\s*=/.test(line));
|
|
397
|
+
if (hookIndex === -1) lines.splice(end, 0, 'hooks = true');
|
|
398
|
+
else lines[start + 1 + hookIndex] = 'hooks = true';
|
|
399
|
+
return lines.join('\n');
|
|
400
|
+
}
|
|
401
|
+
|
|
369
402
|
function safeReadText(filePath) {
|
|
370
403
|
try {
|
|
371
404
|
return fs.readFileSync(filePath, 'utf-8');
|
|
@@ -36,14 +36,17 @@ export function detectSvelteKitProject(cwd = process.cwd(), config = null) {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export function applySvelteKitLiveAdapter({ cwd = process.cwd(), port, config = null } = {}) {
|
|
39
|
+
export function applySvelteKitLiveAdapter({ cwd = process.cwd(), port, authCred, config = null } = {}) {
|
|
40
40
|
if (!Number.isFinite(Number(port))) {
|
|
41
41
|
throw new Error('SvelteKit live adapter requires a numeric port');
|
|
42
42
|
}
|
|
43
|
+
if (!authCred) {
|
|
44
|
+
throw new Error('SvelteKit live adapter requires an auth credential');
|
|
45
|
+
}
|
|
43
46
|
const detected = detectSvelteKitProject(cwd, config);
|
|
44
47
|
if (!detected) return null;
|
|
45
48
|
|
|
46
|
-
ensureSvelteLiveRootComponent(cwd, Number(port));
|
|
49
|
+
ensureSvelteLiveRootComponent(cwd, Number(port), authCred);
|
|
47
50
|
|
|
48
51
|
const layoutRel = detected.layoutFile;
|
|
49
52
|
const layoutAbs = path.join(cwd, layoutRel);
|
|
@@ -136,18 +139,20 @@ export function unpatchSvelteLayout(content) {
|
|
|
136
139
|
return out.replace(/\n{3,}/g, '\n\n');
|
|
137
140
|
}
|
|
138
141
|
|
|
139
|
-
export function ensureSvelteLiveRootComponent(cwd, port) {
|
|
142
|
+
export function ensureSvelteLiveRootComponent(cwd, port, authCred) {
|
|
140
143
|
const file = path.join(cwd, SVELTE_LIVE_ROOT_COMPONENT);
|
|
141
144
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
142
|
-
fs.writeFileSync(file, buildSvelteLiveRootComponent(port), 'utf-8');
|
|
145
|
+
fs.writeFileSync(file, buildSvelteLiveRootComponent(port, authCred), 'utf-8');
|
|
143
146
|
return file;
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
export function buildSvelteLiveRootComponent(port) {
|
|
149
|
+
export function buildSvelteLiveRootComponent(port, authCred) {
|
|
150
|
+
const authQuery = new URLSearchParams();
|
|
151
|
+
authQuery.set('token', String(authCred ?? ''));
|
|
147
152
|
return `<script>
|
|
148
153
|
import { onMount } from 'svelte';
|
|
149
154
|
|
|
150
|
-
const LIVE_URL = 'http://localhost:${Number(port)}/live.js';
|
|
155
|
+
const LIVE_URL = 'http://localhost:${Number(port)}/live.js?${authQuery.toString()}';
|
|
151
156
|
const HOST_ID = 'monodesign-live-root';
|
|
152
157
|
|
|
153
158
|
onMount(() => {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
|
-
import { resolveLiveConfigPath } from './lib/monodesign-paths.mjs';
|
|
19
|
+
import { resolveLiveConfigPath, readLiveServerInfo } from './lib/monodesign-paths.mjs';
|
|
20
20
|
import {
|
|
21
21
|
applySvelteKitLiveAdapter,
|
|
22
22
|
detectSvelteKitProject,
|
|
@@ -27,6 +27,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
27
27
|
const CONFIG_PATH = resolveLiveConfigPath({ cwd: process.cwd(), scriptsDir: __dirname });
|
|
28
28
|
const MARKER_OPEN_TEXT = 'monodesign-live-start';
|
|
29
29
|
const MARKER_CLOSE_TEXT = 'monodesign-live-end';
|
|
30
|
+
// Query param name live-server.mjs checks on every protected route.
|
|
31
|
+
const AUTH_QUERY_KEY = 'token';
|
|
30
32
|
const IGNORE_MARKER_OPEN = '# monodesign-live-ignore-start';
|
|
31
33
|
const IGNORE_MARKER_CLOSE = '# monodesign-live-ignore-end';
|
|
32
34
|
|
|
@@ -145,10 +147,19 @@ Output (JSON):
|
|
|
145
147
|
console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
|
|
146
148
|
process.exit(1);
|
|
147
149
|
}
|
|
150
|
+
// The injected tag must authenticate like every other server route does —
|
|
151
|
+
// read the per-process credential live-server.mjs persisted at startup.
|
|
152
|
+
const liveInfo = readLiveServerInfo(process.cwd());
|
|
153
|
+
const credentialFields = liveInfo?.info ?? {};
|
|
154
|
+
const authCred = credentialFields['token'];
|
|
155
|
+
if (!authCred) {
|
|
156
|
+
console.error(JSON.stringify({ ok: false, error: 'missing_auth_credential', detail: 'no running live server info found for this project — start it before injecting' }));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
148
159
|
const gitIgnore = ensureLiveGitIgnores(process.cwd());
|
|
149
160
|
|
|
150
161
|
if (svelteKit) {
|
|
151
|
-
const adapterResult = applySvelteKitLiveAdapter({ cwd: process.cwd(), port, config });
|
|
162
|
+
const adapterResult = applySvelteKitLiveAdapter({ cwd: process.cwd(), port, authCred, config });
|
|
152
163
|
console.log(JSON.stringify({ ok: true, port, adapter: 'sveltekit', gitIgnore, results: [adapterResult] }));
|
|
153
164
|
return;
|
|
154
165
|
}
|
|
@@ -158,7 +169,7 @@ Output (JSON):
|
|
|
158
169
|
if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
|
|
159
170
|
const content = fs.readFileSync(absFile, 'utf-8');
|
|
160
171
|
const withoutOld = revertCspMeta(removeTag(content, config.commentSyntax));
|
|
161
|
-
const withTag = insertTag(withoutOld, config, port, relFile);
|
|
172
|
+
const withTag = insertTag(withoutOld, config, port, authCred, relFile);
|
|
162
173
|
if (withTag === withoutOld) {
|
|
163
174
|
return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter };
|
|
164
175
|
}
|
|
@@ -356,16 +367,18 @@ function validateConfig(cfg) {
|
|
|
356
367
|
function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : '<!--'; }
|
|
357
368
|
function commentClose(syntax) { return syntax === 'jsx' ? '*/}' : '-->'; }
|
|
358
369
|
|
|
359
|
-
function buildTagBlock(syntax, port, filePath) {
|
|
370
|
+
function buildTagBlock(syntax, port, authCred, filePath) {
|
|
360
371
|
const open = commentOpen(syntax);
|
|
361
372
|
const close = commentClose(syntax);
|
|
362
373
|
// Astro processes <script> tags by default and rewrites src to its own
|
|
363
374
|
// bundled URL. is:inline opts out so the literal external src survives.
|
|
364
375
|
const isAstro = typeof filePath === 'string' && filePath.endsWith('.astro');
|
|
365
376
|
const scriptAttrs = isAstro ? 'is:inline ' : '';
|
|
377
|
+
const authQuery = new URLSearchParams();
|
|
378
|
+
authQuery.set(AUTH_QUERY_KEY, authCred);
|
|
366
379
|
return (
|
|
367
380
|
open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
|
|
368
|
-
'<script ' + scriptAttrs + 'src="http://localhost:' + port + '/live.js"></script>\n' +
|
|
381
|
+
'<script ' + scriptAttrs + 'src="http://localhost:' + port + '/live.js?' + authQuery.toString() + '"></script>\n' +
|
|
369
382
|
open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
|
|
370
383
|
);
|
|
371
384
|
}
|
|
@@ -387,9 +400,9 @@ function readLineEndingAt(content, index) {
|
|
|
387
400
|
return '';
|
|
388
401
|
}
|
|
389
402
|
|
|
390
|
-
function insertTag(content, config, port, filePath) {
|
|
403
|
+
function insertTag(content, config, port, authCred, filePath) {
|
|
391
404
|
const lineEnding = detectLineEnding(content);
|
|
392
|
-
const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding);
|
|
405
|
+
const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, authCred, filePath), lineEnding);
|
|
393
406
|
// insertBefore: match the LAST occurrence. Anchors like `</body>` naturally
|
|
394
407
|
// belong at the end, and the same literal can appear earlier in code blocks
|
|
395
408
|
// within rendered documentation pages.
|
|
@@ -63,6 +63,8 @@ const DESIGN_MD_PATH = PROJECT_CONTEXT.designPath
|
|
|
63
63
|
: null;
|
|
64
64
|
const DEFAULT_POLL_TIMEOUT = 600_000; // 10 min — agent re-polls on timeout anyway
|
|
65
65
|
const SSE_HEARTBEAT_INTERVAL = 30_000; // keepalive ping every 30s
|
|
66
|
+
// Query param name every protected route checks against the per-process credential.
|
|
67
|
+
const AUTH_QUERY_KEY = 'token';
|
|
66
68
|
|
|
67
69
|
// ---------------------------------------------------------------------------
|
|
68
70
|
// Port detection
|
|
@@ -383,6 +385,20 @@ function statOrNull(filePath) {
|
|
|
383
385
|
try { return fs.statSync(filePath); } catch { return null; }
|
|
384
386
|
}
|
|
385
387
|
|
|
388
|
+
// Shared gate for every protected route, including /live.js (see its call
|
|
389
|
+
// site for why that one needs it too). Writes the 401 itself and returns
|
|
390
|
+
// false so callers can `if (!requireAuthQueryCred(url, res)) return;`.
|
|
391
|
+
function requireAuthQueryCred(url, res) {
|
|
392
|
+
const provided = url.searchParams.get(AUTH_QUERY_KEY);
|
|
393
|
+
const expected = state[AUTH_QUERY_KEY];
|
|
394
|
+
if (provided !== expected) {
|
|
395
|
+
res.writeHead(401);
|
|
396
|
+
res.end('Unauthorized');
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
|
|
386
402
|
// HTTP request handler
|
|
387
403
|
// ---------------------------------------------------------------------------
|
|
388
404
|
|
|
@@ -398,6 +414,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
|
398
414
|
|
|
399
415
|
// --- Scripts ---
|
|
400
416
|
if (p === '/live.js') {
|
|
417
|
+
// This response embeds the shared bearer credential (below) — unlike
|
|
418
|
+
// /detect.js and the vendored scripts, it must be gated the same way
|
|
419
|
+
// every OTHER protected route already is. Without this, requesting this
|
|
420
|
+
// one route defeats every other route's check: whoever fetches it walks
|
|
421
|
+
// away with the value that's supposed to distinguish the legitimate
|
|
422
|
+
// preview tab from any other page. See requireAuthQueryCred() below —
|
|
423
|
+
// callers must supply the credential the running server printed at
|
|
424
|
+
// startup (also mirrored into the live-server info file for tooling).
|
|
425
|
+
if (!requireAuthQueryCred(url, res)) return;
|
|
401
426
|
// Re-read from disk each request so edits to live-browser.js land on
|
|
402
427
|
// the next tab reload. No-store headers prevent browser caching across
|
|
403
428
|
// sessions — during iteration, a cached old script silently breaks
|
|
@@ -604,8 +629,19 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
|
604
629
|
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
|
605
630
|
const filePath = url.searchParams.get('path');
|
|
606
631
|
if (!filePath || filePath.includes('..')) { res.writeHead(400); res.end('Bad path'); return; }
|
|
607
|
-
|
|
608
|
-
|
|
632
|
+
// Resolve symlinks on BOTH sides before comparing: a lexical prefix check
|
|
633
|
+
// (path.resolve + startsWith) can't tell a symlink that lexically resolves
|
|
634
|
+
// inside cwd from one that physically points outside it, and startsWith on
|
|
635
|
+
// its own also has no path-boundary — "/proj-evil" lexically "starts with"
|
|
636
|
+
// "/proj". realpath + path.relative closes both gaps (the same pattern
|
|
637
|
+
// already used correctly elsewhere in this package).
|
|
638
|
+
let root, absPath;
|
|
639
|
+
try {
|
|
640
|
+
root = fs.realpathSync(process.cwd());
|
|
641
|
+
absPath = fs.realpathSync(path.resolve(process.cwd(), filePath));
|
|
642
|
+
} catch { res.writeHead(404); res.end('File not found'); return; }
|
|
643
|
+
const rel = path.relative(root, absPath);
|
|
644
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) { res.writeHead(403); res.end('Forbidden'); return; }
|
|
609
645
|
let content;
|
|
610
646
|
try { content = fs.readFileSync(absPath, 'utf-8'); }
|
|
611
647
|
catch { res.writeHead(404); res.end('File not found'); return; }
|
package/src/antipatterns.ts
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
-
import { extname } from 'node:path';
|
|
3
|
-
|
|
4
|
-
export type AntipatternCategory = 'slop' | 'quality' | 'performance' | 'accessibility';
|
|
5
|
-
|
|
6
|
-
export interface AntipatternFinding {
|
|
7
|
-
id: string;
|
|
8
|
-
category: AntipatternCategory;
|
|
9
|
-
name: string;
|
|
10
|
-
severity: 'error' | 'warning' | 'info';
|
|
11
|
-
message: string;
|
|
12
|
-
file?: string;
|
|
13
|
-
line?: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface DetectionResult {
|
|
17
|
-
findings: AntipatternFinding[];
|
|
18
|
-
fileCount: number;
|
|
19
|
-
durationMs: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Shape of the bundled engine (cli/engine/detect-antipatterns.mjs).
|
|
23
|
-
interface DetectorEngine {
|
|
24
|
-
detectText: (content: string, filePath: string, options?: Record<string, unknown>) => RawFinding[];
|
|
25
|
-
detectHtml: (filePath: string, options?: Record<string, unknown>) => Promise<RawFinding[]>;
|
|
26
|
-
detectUrl: (url: string, options?: Record<string, unknown>) => Promise<RawFinding[]>;
|
|
27
|
-
walkDir: (dir: string) => string[];
|
|
28
|
-
ANTIPATTERNS: Array<{ id: string; category?: string; severity?: string }>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface RawFinding {
|
|
32
|
-
antipattern?: string;
|
|
33
|
-
id?: string;
|
|
34
|
-
name?: string;
|
|
35
|
-
description?: string;
|
|
36
|
-
severity?: string;
|
|
37
|
-
file?: string;
|
|
38
|
-
line?: number;
|
|
39
|
-
snippet?: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Typed as plain string so tsc emits a true dynamic import instead of trying
|
|
43
|
-
// to type-resolve the untyped .mjs engine module.
|
|
44
|
-
const ENGINE_SPECIFIER: string = '../cli/engine/detect-antipatterns.mjs';
|
|
45
|
-
|
|
46
|
-
let enginePromise: Promise<DetectorEngine | null> | undefined;
|
|
47
|
-
|
|
48
|
-
async function loadEngine(): Promise<DetectorEngine | null> {
|
|
49
|
-
enginePromise ??= import(ENGINE_SPECIFIER).then(
|
|
50
|
-
(mod) => mod as unknown as DetectorEngine,
|
|
51
|
-
() => null,
|
|
52
|
-
);
|
|
53
|
-
return enginePromise;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** True when the bundled detection engine can be loaded. */
|
|
57
|
-
export async function detectorAvailable(): Promise<boolean> {
|
|
58
|
-
return (await loadEngine()) !== null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const HTML_EXTENSIONS = new Set(['.html', '.htm']);
|
|
62
|
-
|
|
63
|
-
function mapSeverity(severity: string | undefined): AntipatternFinding['severity'] {
|
|
64
|
-
if (severity === 'error') return 'error';
|
|
65
|
-
if (severity === 'advisory' || severity === 'info') return 'info';
|
|
66
|
-
return 'warning';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function toFinding(raw: RawFinding, categoryById: Map<string, string>): AntipatternFinding {
|
|
70
|
-
const id = String(raw.antipattern ?? raw.id ?? '');
|
|
71
|
-
return {
|
|
72
|
-
id,
|
|
73
|
-
category: (categoryById.get(id) ?? 'quality') as AntipatternCategory,
|
|
74
|
-
name: String(raw.name ?? id),
|
|
75
|
-
severity: mapSeverity(raw.severity),
|
|
76
|
-
message: String(raw.description ?? raw.snippet ?? ''),
|
|
77
|
-
file: raw.file ? String(raw.file) : undefined,
|
|
78
|
-
line: raw.line ? Number(raw.line) : undefined,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function detectFile(engine: DetectorEngine, filePath: string): Promise<RawFinding[]> {
|
|
83
|
-
if (HTML_EXTENSIONS.has(extname(filePath).toLowerCase())) {
|
|
84
|
-
return engine.detectHtml(filePath);
|
|
85
|
-
}
|
|
86
|
-
return engine.detectText(readFileSync(filePath, 'utf-8'), filePath);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Run antipattern detection on a path (file, directory, or URL) using the
|
|
91
|
-
* bundled detection engine (cli/engine/detect-antipatterns.mjs). URL targets
|
|
92
|
-
* require the optional puppeteer dependency.
|
|
93
|
-
*/
|
|
94
|
-
export async function detectAntipatterns(target: string): Promise<DetectionResult> {
|
|
95
|
-
const engine = await loadEngine();
|
|
96
|
-
if (!engine) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
'The monodesign detection engine could not be loaded. ' +
|
|
99
|
-
'Reinstall @monoes/monodesign (its cli/engine/ directory is missing or its dependencies are not installed).',
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const start = Date.now();
|
|
104
|
-
const categoryById = new Map(
|
|
105
|
-
engine.ANTIPATTERNS.map((ap) => [ap.id, ap.category ?? 'quality']),
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
let raw: RawFinding[] = [];
|
|
109
|
-
let fileCount = 0;
|
|
110
|
-
|
|
111
|
-
if (/^https?:\/\//i.test(target)) {
|
|
112
|
-
raw = await engine.detectUrl(target);
|
|
113
|
-
fileCount = 1;
|
|
114
|
-
} else if (existsSync(target) && statSync(target).isDirectory()) {
|
|
115
|
-
const files = engine.walkDir(target);
|
|
116
|
-
fileCount = files.length;
|
|
117
|
-
for (const file of files) {
|
|
118
|
-
raw.push(...(await detectFile(engine, file)));
|
|
119
|
-
}
|
|
120
|
-
} else if (existsSync(target)) {
|
|
121
|
-
raw = await detectFile(engine, target);
|
|
122
|
-
fileCount = 1;
|
|
123
|
-
} else {
|
|
124
|
-
throw new Error(`Target not found: ${target}`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
findings: raw.map((f) => toFinding(f, categoryById)),
|
|
129
|
-
fileCount,
|
|
130
|
-
durationMs: Date.now() - start,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Format findings as a human-readable string for display in agent output.
|
|
136
|
-
*/
|
|
137
|
-
export function formatFindings(result: DetectionResult): string {
|
|
138
|
-
if (result.findings.length === 0) {
|
|
139
|
-
return `No antipatterns detected in ${result.fileCount} file(s). (${result.durationMs}ms)`;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const byCategory = result.findings.reduce<Record<string, AntipatternFinding[]>>((acc, f) => {
|
|
143
|
-
(acc[f.category] ??= []).push(f);
|
|
144
|
-
return acc;
|
|
145
|
-
}, {});
|
|
146
|
-
|
|
147
|
-
const lines: string[] = [
|
|
148
|
-
`${result.findings.length} finding(s) in ${result.fileCount} file(s) — ${result.durationMs}ms`,
|
|
149
|
-
'',
|
|
150
|
-
];
|
|
151
|
-
|
|
152
|
-
for (const [cat, items] of Object.entries(byCategory)) {
|
|
153
|
-
lines.push(`[${cat.toUpperCase()}]`);
|
|
154
|
-
for (const f of items) {
|
|
155
|
-
const loc = f.file ? ` ${f.file}${f.line ? `:${f.line}` : ''}` : '';
|
|
156
|
-
lines.push(` ${f.severity === 'error' ? '✗' : '⚠'} ${f.name}`);
|
|
157
|
-
lines.push(` ${f.message}`);
|
|
158
|
-
if (loc) lines.push(loc);
|
|
159
|
-
}
|
|
160
|
-
lines.push('');
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return lines.join('\n');
|
|
164
|
-
}
|