roast-my-design-system 4.2.6 → 4.3.1

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 CHANGED
@@ -12,7 +12,7 @@ Run it on your codebase and get, in about a second:
12
12
  - **Per-package scores for monorepos.** One blended number hides which package is the problem: `packages/ui` scores 80 while `apps/web` scores 40, and now you can see it.
13
13
  - **The receipts behind it.** Every colour and its near-identical twin, every spacing value, typeface, duplicated or never-imported component, inline style and !important, with real file paths, in one self-contained HTML report you can open, Slack or email.
14
14
  - **The first fixes ranked by payoff.** A "Where to start" list derived from your own numbers: keep the report as the audit, or hand it to Claude as the punch list for the fix.
15
- - **Rules that stop the mess coming back.** A generated `design-system-rules.md` for your CLAUDE.md or .cursor/rules: canonical components, your token file, known duplicates to avoid, so your AI agent follows your system instead of guessing at it. `--apply` injects them for you, and every scan checks the rules you already have for stale references: paths that no longer exist, components named canonical that nothing imports anymore.
15
+ - **Rules that stop the mess coming back.** A generated `design-system-rules.md` with canonical components, your token file, and known duplicates to avoid, so your AI agent follows your system instead of guessing at it. `--apply` injects them into every agent file you have: Claude, Cursor, GitHub Copilot, and Windsurf. Every scan also checks the rules you already have for stale references: paths that no longer exist, components named canonical that nothing imports anymore.
16
16
 
17
17
  ## Why this exists
18
18
 
@@ -26,7 +26,7 @@ One scan powers all of it; the flags decide what lands on disk. Combine freely.
26
26
  |---|---|
27
27
  | `npx roast-my-design-system` | The scan and `design-system-roast.html`, opened in your browser |
28
28
  | `npx roast-my-design-system <path>` | Scan a different repo than the current directory |
29
- | `... --apply` | The generated agent rules injected straight into your `CLAUDE.md`, `AGENTS.md`, `.cursorrules` or `.cursor/rules/`, inside a marked block. Re-running replaces only that block, never your own text |
29
+ | `... --apply` | The generated agent rules injected straight into every agent file you have: `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, `.cursor/rules/`, `.windsurfrules` and `.github/copilot-instructions.md`, inside a marked block. Re-running replaces only that block, never your own text. Windsurf and Copilot get a compact variant sized for their limits |
30
30
  | `... --rules` | The same rules written to `design-system-rules.md` instead, for pasting by hand |
31
31
  | `... --card` | `roast-card.svg`: a shareable 1200x630 card with the score and worst findings. Pure SVG, embeds in a README |
32
32
  | `... --sarif` | `design-system-roast.sarif` for GitHub code scanning: upload it in CI and findings appear in the Security tab, annotated on files |
@@ -36,7 +36,7 @@ One scan powers all of it; the flags decide what lands on disk. Combine freely.
36
36
  | `... --theme light` / `--out <file>` / `--no-open` | Light report, custom report path, don't open the browser |
37
37
  | `/roast-my-design-system` (in Claude Code) | The full experience: the roast in chat, the report, the rules offer, and the fix loop with Claude on your own numbers |
38
38
 
39
- Every scan also checks the agent rules you already have and flags stale references, no flag needed.
39
+ **One scan writes rules for every agent: Claude, Cursor, GitHub Copilot, and Windsurf.** Every scan also checks the agent rules you already have and flags stale references, no flag needed.
40
40
 
41
41
  ## Example use cases
42
42
 
package/bin/roast.mjs CHANGED
@@ -55,8 +55,10 @@ Usage: npx roast-my-design-system [path] [options]
55
55
  --rules also write design-system-rules.md: agent rules (for
56
56
  CLAUDE.md / .cursor/rules) generated from the scan
57
57
  --apply inject the rules straight into your agent files (CLAUDE.md,
58
- AGENTS.md, .cursorrules, .cursor/rules/) inside a marked
59
- block; re-running replaces only that block
58
+ AGENTS.md, .cursorrules, .cursor/rules/, .windsurfrules,
59
+ .github/copilot-instructions.md) inside a marked block;
60
+ re-running replaces only that block. Windsurf and Copilot
61
+ get a compact variant sized for their limits
60
62
  --card also write roast-card.svg: a shareable 1200x630 card with
61
63
  the score and worst findings (pure SVG, embeds in READMEs)
62
64
  --sarif also write design-system-roast.sarif for GitHub code
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "4.2.6",
4
- "description": "Your AI can write the UI. This makes sure it writes your UI. A deterministic scanner counts every colour, spacing value and duplicate component, scores you 0-100 against 34 public repos, scopes the scan with .roastignore, injects agent rules with --apply.",
3
+ "version": "4.3.1",
4
+ "description": "Your AI can write the UI. This makes sure it writes your UI. A deterministic scanner scores your design system 0-100 against 34 public repos, scopes the scan with .roastignore, and writes rules for Claude, Cursor, GitHub Copilot and Windsurf with --apply.",
5
5
  "keywords": [
6
6
  "design-system",
7
7
  "design-tokens",
@@ -1,4 +1,4 @@
1
1
  // Single version constant for the engine — imported by diagnose (report
2
2
  // footer) and rules (generated-by line). This is the bump spot that used to
3
3
  // live as a const inside diagnose/index.mjs.
4
- export const VERSION = '4.2.6';
4
+ export const VERSION = '4.3.1';
@@ -9,6 +9,12 @@
9
9
  * block; re-running replaces only our block, never touching their text.
10
10
  * .cursor/rules/ (directory) -> design-system-rules.mdc written
11
11
  * whole, with the frontmatter Cursor expects.
12
+ * .windsurfrules -> COMPACT rules injected (Windsurf
13
+ * caps rules files at a few thousand characters). Injected only when the
14
+ * file already exists; .windsurf/rules/ (directory) gets its own file.
15
+ * .github/copilot-instructions.md -> COMPACT rules injected; created
16
+ * when .github/ exists (Copilot chat and code review read this file, and
17
+ * Copilot's guidance prefers short instructions).
12
18
  * If none exist, falls back to design-system-rules.md at the root and says so.
13
19
  *
14
20
  * node src/rules/apply.mjs <harvest.json> --target <repo-root>
@@ -30,17 +36,20 @@ const END = '<!-- roast-my-design-system:rules:end -->';
30
36
 
31
37
  const h = JSON.parse(readFileSync(inPath, 'utf8'));
32
38
  const { text, ruleCount } = rulesMarkdown(h);
33
- const block = `${BEGIN}\n\n${text.trim()}\n\n${END}`;
39
+ // Size-aware variant for hosts with tight practical limits (Windsurf, Copilot)
40
+ const compactText = rulesMarkdown(h, { compact: true }).text;
41
+ const blockOf = (t) => `${BEGIN}\n\n${t.trim()}\n\n${END}`;
42
+ const block = blockOf(text);
34
43
 
35
44
  /** Inject or replace our marked block in an existing file's content. */
36
- function inject(content) {
45
+ function inject(content, b0 = block) {
37
46
  const b = content.indexOf(BEGIN);
38
47
  const e = content.indexOf(END);
39
48
  if (b !== -1 && e !== -1 && e > b) {
40
- return { next: content.slice(0, b) + block + content.slice(e + END.length), how: 'replaced' };
49
+ return { next: content.slice(0, b) + b0 + content.slice(e + END.length), how: 'replaced' };
41
50
  }
42
51
  const sep = content.length === 0 ? '' : content.endsWith('\n\n') ? '' : content.endsWith('\n') ? '\n' : '\n\n';
43
- return { next: content + sep + block + '\n', how: 'added' };
52
+ return { next: content + sep + b0 + '\n', how: 'added' };
44
53
  }
45
54
 
46
55
  const done = [];
@@ -60,10 +69,39 @@ if (existsSync(cursorDir) && statSync(cursorDir).isDirectory()) {
60
69
  done.push(`.cursor/rules/design-system-rules.mdc (${existed ? 'rewritten' : 'created'})`);
61
70
  }
62
71
 
72
+ // Windsurf: the compact variant, because .windsurfrules has a hard character
73
+ // cap. Injected only into a file the user already has; a .windsurf/rules/
74
+ // directory gets its own file, same as Cursor's.
75
+ const windsurfFile = join(target, '.windsurfrules');
76
+ if (existsSync(windsurfFile) && statSync(windsurfFile).isFile()) {
77
+ const { next, how } = inject(readFileSync(windsurfFile, 'utf8'), blockOf(compactText));
78
+ writeFileSync(windsurfFile, next);
79
+ done.push(`.windsurfrules (${how} the compact rules block)`);
80
+ }
81
+ const windsurfDir = join(target, '.windsurf', 'rules');
82
+ if (existsSync(windsurfDir) && statSync(windsurfDir).isDirectory()) {
83
+ const p = join(windsurfDir, 'design-system-rules.md');
84
+ const existed = existsSync(p);
85
+ writeFileSync(p, `${compactText.trim()}\n`);
86
+ done.push(`.windsurf/rules/design-system-rules.md (${existed ? 'rewritten' : 'created'})`);
87
+ }
88
+
89
+ // Copilot chat and code review read .github/copilot-instructions.md, and
90
+ // Copilot's guidance prefers short instructions: compact variant, created
91
+ // when .github/ exists (a strong signal the repo lives on GitHub).
92
+ const githubDir = join(target, '.github');
93
+ if (existsSync(githubDir) && statSync(githubDir).isDirectory()) {
94
+ const p = join(githubDir, 'copilot-instructions.md');
95
+ const existing = existsSync(p) && statSync(p).isFile() ? readFileSync(p, 'utf8') : '';
96
+ const { next, how } = inject(existing, blockOf(compactText));
97
+ writeFileSync(p, next);
98
+ done.push(`.github/copilot-instructions.md (${existing ? `${how} the compact rules block` : 'created'})`);
99
+ }
100
+
63
101
  if (done.length === 0) {
64
102
  const p = join(target, 'design-system-rules.md');
65
103
  writeFileSync(p, text);
66
- console.log(`No agent file found (CLAUDE.md, AGENTS.md, .cursorrules, .cursor/rules/).`);
104
+ console.log(`No agent file found (CLAUDE.md, AGENTS.md, .cursorrules, .cursor/rules/, .windsurfrules, .github/).`);
67
105
  console.log(`✓ Wrote design-system-rules.md instead; point your agent at it.`);
68
106
  } else {
69
107
  console.log(`✓ ${ruleCount} rules applied, every one with a receipt:`);
@@ -9,7 +9,13 @@ import { nearColorPairs } from './../lib/nearpairs.mjs';
9
9
  import { neverImportedComponents } from '../lib/neverimported.mjs';
10
10
  import { VERSION } from '../lib/version.mjs';
11
11
 
12
- export function rulesMarkdown(h) {
12
+ export function rulesMarkdown(h, opts = {}) {
13
+ // compact: the size-aware variant for agent files with tight practical
14
+ // limits (.windsurfrules caps out at a few thousand characters and Copilot's
15
+ // guidance prefers short instructions). Fewer receipts per rule, no prose
16
+ // preamble, and a hard character budget as a safety net. Same rules, tighter.
17
+ const compact = opts.compact === true;
18
+ const CAP = { components: compact ? 5 : 8, dupes: compact ? 3 : 6 };
13
19
  const repoName = h.profile?.name ?? 'this repo';
14
20
  const t = h.tokens ?? {};
15
21
  const reusable = (h.components ?? []).filter((c) => !c.isPage);
@@ -21,18 +27,24 @@ const repoName = h.profile?.name ?? 'this repo';
21
27
 
22
28
  lines.push('## Design system rules');
23
29
  lines.push('');
24
- lines.push(`<!-- Generated by roast-my-design-system from a scan of ${repoName} on ${scanDate}.`);
25
- lines.push(' Paste into CLAUDE.md, .cursor/rules or AGENTS.md. Regenerate after big refactors:');
26
- lines.push(' npx roast-my-design-system --rules -->');
27
- lines.push('');
30
+ if (!compact) {
31
+ lines.push(`<!-- Generated by roast-my-design-system from a scan of ${repoName} on ${scanDate}.`);
32
+ lines.push(' Paste into CLAUDE.md, .cursor/rules or AGENTS.md. Regenerate after big refactors:');
33
+ lines.push(' npx roast-my-design-system --rules -->');
34
+ lines.push('');
35
+ }
28
36
  // Mirror of the report's "no design system" banner: with almost no colour or
29
37
  // spacing signal there is nothing to derive receipts from, so the preamble
30
38
  // must not claim any. Keep the two universal defaults, labelled as defaults.
31
39
  const spacingSignal = (t.spacing ?? []).length + (t.tailwind?.spacing ?? []).filter((v) => v.value.startsWith('[')).length;
32
40
  const noSystemLikely = (t.colors ?? []).length === 0 || ((t.colors ?? []).length < 3 && spacingSignal === 0);
33
41
  lines.push(noSystemLikely
34
- ? 'The scan found too little styling in this repo to derive repo-specific rules; there is most likely no design system in here yet. The rules below are universal defaults, not findings. Rescan once real UI lands and they will be rebuilt from receipts.'
35
- : 'Follow these rules when writing or editing UI in this repo. Every rule below was derived from a scan of this codebase, with real paths and usage counts.');
42
+ ? (compact
43
+ ? 'Too little styling found to derive repo-specific rules; these are universal defaults. Rescan once real UI lands.'
44
+ : 'The scan found too little styling in this repo to derive repo-specific rules; there is most likely no design system in here yet. The rules below are universal defaults, not findings. Rescan once real UI lands and they will be rebuilt from receipts.')
45
+ : (compact
46
+ ? `Follow these rules when writing or editing UI in this repo; derived from a scan on ${scanDate}.`
47
+ : 'Follow these rules when writing or editing UI in this repo. Every rule below was derived from a scan of this codebase, with real paths and usage counts.'));
36
48
 
37
49
  // ---------- tokens ----------
38
50
  const strays = (t.colors ?? []).filter((c) => !c.isToken);
@@ -63,12 +75,12 @@ const repoName = h.profile?.name ?? 'this repo';
63
75
  for (const c of reusable) nameCounts.set(c.name, (nameCounts.get(c.name) ?? 0) + 1);
64
76
  const top = reusable
65
77
  .filter((c) => c.usageCount > 0 && !FRAMEWORK_NAMES.has(c.name) && nameCounts.get(c.name) === 1)
66
- .sort((a, b) => b.usageCount - a.usageCount).slice(0, 8);
78
+ .sort((a, b) => b.usageCount - a.usageCount).slice(0, CAP.components);
67
79
  if (top.length) {
68
80
  section('Canonical components');
69
81
  rule('Use these existing components instead of writing new ones:');
70
82
  for (const c of top) {
71
- const props = c.propsHint?.named?.length ? ` · props: ${c.propsHint.named.slice(0, 4).join(', ')}` : '';
83
+ const props = !compact && c.propsHint?.named?.length ? ` · props: ${c.propsHint.named.slice(0, 4).join(', ')}` : '';
72
84
  lines.push(` - \`<${c.name}>\` from \`${c.file}\` (used ${c.usageCount}x${props})`);
73
85
  }
74
86
  }
@@ -81,7 +93,7 @@ const repoName = h.profile?.name ?? 'this repo';
81
93
  // No per-file usage data here, so rank by how shared the location looks;
82
94
  // when nothing stands out, do not guess a canonical.
83
95
  const sharedScore = (f) => (f.includes('packages/') ? 4 : 0) + (/\/ui\//.test(f) ? 2 : 0) + (f.includes('components/') ? 1 : 0) - (/\bapp\//.test(f) ? 2 : 0) - (f.includes('/icons/') ? 4 : 0);
84
- for (const d of dupes.slice(0, 6)) {
96
+ for (const d of dupes.slice(0, CAP.dupes)) {
85
97
  const files = d.files.map((f) => (typeof f === 'string' ? f : f.file));
86
98
  const ranked = [...files].sort((a, b) => sharedScore(b) - sharedScore(a));
87
99
  const clear = sharedScore(ranked[0]) > sharedScore(ranked[1]);
@@ -129,10 +141,23 @@ if (neverImported.length >= 3) {
129
141
  rule('Before styling anything new, look at a neighbouring component and match how it does it. Consistency with the repo beats personal preference.');
130
142
 
131
143
  lines.push('');
132
- lines.push('---');
133
- lines.push(`*Generated by [roast-my-design-system](https://github.com/pencilrebel/roast-my-design-system) ver. ${VERSION}. Rescan after refactors to keep these rules honest.*`);
144
+ if (compact) {
145
+ lines.push(`*Compact rules by roast-my-design-system ver. ${VERSION}; the full set with receipts: npx roast-my-design-system --rules*`);
146
+ } else {
147
+ lines.push('---');
148
+ lines.push(`*Generated by [roast-my-design-system](https://github.com/pencilrebel/roast-my-design-system) ver. ${VERSION}. Rescan after refactors to keep these rules honest.*`);
149
+ }
134
150
  lines.push('');
135
- const text = lines.join('\n');
136
- const ruleCount = lines.filter((l) => l.startsWith('- ')).length;
151
+ let text = lines.join('\n');
152
+ // Safety net for tight hosts: if a very messy repo still blows the budget,
153
+ // cut at the last section boundary under the cap and say the set continues.
154
+ const MAX_COMPACT = 5000;
155
+ if (compact && text.length > MAX_COMPACT) {
156
+ const cut = text.lastIndexOf('\n### ', MAX_COMPACT);
157
+ if (cut > 0) {
158
+ text = `${text.slice(0, cut).trimEnd()}\n\n*Trimmed to fit this file's limits; the full rules: npx roast-my-design-system --rules*\n`;
159
+ }
160
+ }
161
+ const ruleCount = text.split('\n').filter((l) => l.startsWith('- ')).length;
137
162
  return { text, ruleCount };
138
163
  }