@buoy-design/cli 0.3.33 → 0.3.35

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.
Files changed (93) hide show
  1. package/dist/commands/audit.d.ts +3 -0
  2. package/dist/commands/audit.d.ts.map +1 -0
  3. package/dist/commands/audit.js +235 -0
  4. package/dist/commands/audit.js.map +1 -0
  5. package/dist/commands/baseline.d.ts +47 -0
  6. package/dist/commands/baseline.d.ts.map +1 -0
  7. package/dist/commands/baseline.js +327 -0
  8. package/dist/commands/baseline.js.map +1 -0
  9. package/dist/commands/begin.d.ts +9 -0
  10. package/dist/commands/begin.d.ts.map +1 -0
  11. package/dist/commands/begin.js +827 -0
  12. package/dist/commands/begin.js.map +1 -0
  13. package/dist/commands/build.d.ts +8 -0
  14. package/dist/commands/build.d.ts.map +1 -0
  15. package/dist/commands/build.js +26 -0
  16. package/dist/commands/build.js.map +1 -0
  17. package/dist/commands/check.d.ts.sync-conflict-20260305-170128-6PCZ3ZU.map +1 -0
  18. package/dist/commands/check.sync-conflict-20260305-155016-6PCZ3ZU.d.ts +26 -0
  19. package/dist/commands/check.sync-conflict-20260305-155016-6PCZ3ZU.d.ts.map +1 -0
  20. package/dist/commands/check.sync-conflict-20260305-155016-6PCZ3ZU.js +438 -0
  21. package/dist/commands/check.sync-conflict-20260305-155016-6PCZ3ZU.js.map +1 -0
  22. package/dist/commands/commands.d.ts +8 -0
  23. package/dist/commands/commands.d.ts.map +1 -0
  24. package/dist/commands/commands.js +148 -0
  25. package/dist/commands/commands.js.map +1 -0
  26. package/dist/commands/components-query.d.ts +13 -0
  27. package/dist/commands/components-query.d.ts.map +1 -0
  28. package/dist/commands/components-query.js +263 -0
  29. package/dist/commands/components-query.js.map +1 -0
  30. package/dist/commands/components.d.ts +8 -0
  31. package/dist/commands/components.d.ts.map +1 -0
  32. package/dist/commands/components.js +14 -0
  33. package/dist/commands/components.js.map +1 -0
  34. package/dist/commands/dock.sync-conflict-20260309-191923-6PCZ3ZU.js +1006 -0
  35. package/dist/commands/history.d.ts +3 -0
  36. package/dist/commands/history.d.ts.map +1 -0
  37. package/dist/commands/history.js +352 -0
  38. package/dist/commands/history.js.map +1 -0
  39. package/dist/commands/plugins.d.ts +3 -0
  40. package/dist/commands/plugins.d.ts.map +1 -0
  41. package/dist/commands/plugins.js +63 -0
  42. package/dist/commands/plugins.js.map +1 -0
  43. package/dist/commands/show.d.ts.sync-conflict-20260306-165917-6PCZ3ZU.map +1 -0
  44. package/dist/commands/show.sync-conflict-20260305-140755-6PCZ3ZU.js +1735 -0
  45. package/dist/commands/show.sync-conflict-20260309-130326-6PCZ3ZU.d.ts +11 -0
  46. package/dist/commands/show.sync-conflict-20260309-130326-6PCZ3ZU.d.ts.map +1 -0
  47. package/dist/commands/show.sync-conflict-20260309-130326-6PCZ3ZU.js +1735 -0
  48. package/dist/commands/show.sync-conflict-20260309-130326-6PCZ3ZU.js.map +1 -0
  49. package/dist/config/loader.js +1 -1
  50. package/dist/config/loader.js.map +1 -1
  51. package/dist/config/loader.js.sync-conflict-20260309-033512-6PCZ3ZU.map +1 -0
  52. package/dist/config/loader.sync-conflict-20260307-175208-6PCZ3ZU.d.ts +8 -0
  53. package/dist/config/loader.sync-conflict-20260307-175208-6PCZ3ZU.d.ts.map +1 -0
  54. package/dist/config/loader.sync-conflict-20260307-175208-6PCZ3ZU.js +162 -0
  55. package/dist/config/loader.sync-conflict-20260307-175208-6PCZ3ZU.js.map +1 -0
  56. package/dist/config/schema.d.ts.sync-conflict-20260309-154654-6PCZ3ZU.map +1 -0
  57. package/dist/config/schema.sync-conflict-20260309-135703-6PCZ3ZU.js +214 -0
  58. package/dist/detect/frameworks.js.sync-conflict-20260306-123756-6PCZ3ZU.map +1 -0
  59. package/dist/detect/monorepo-patterns.js.sync-conflict-20260309-155400-6PCZ3ZU.map +1 -0
  60. package/dist/hooks/index.d.ts.sync-conflict-20260306-220901-6PCZ3ZU.map +1 -0
  61. package/dist/output/formatters.js.sync-conflict-20260306-134702-6PCZ3ZU.map +1 -0
  62. package/dist/output/formatters.sync-conflict-20260306-180804-6PCZ3ZU.js +867 -0
  63. package/dist/output/formatters.sync-conflict-20260307-131418-5SN2GZG.d.ts +29 -0
  64. package/dist/output/formatters.sync-conflict-20260307-131418-5SN2GZG.d.ts.map +1 -0
  65. package/dist/output/formatters.sync-conflict-20260307-131418-5SN2GZG.js +867 -0
  66. package/dist/output/formatters.sync-conflict-20260307-131418-5SN2GZG.js.map +1 -0
  67. package/dist/output/formatters.sync-conflict-20260307-143122-5SN2GZG.d.ts +29 -0
  68. package/dist/output/formatters.sync-conflict-20260307-143122-5SN2GZG.d.ts.map +1 -0
  69. package/dist/output/formatters.sync-conflict-20260307-143122-5SN2GZG.js +867 -0
  70. package/dist/output/formatters.sync-conflict-20260307-143122-5SN2GZG.js.map +1 -0
  71. package/dist/output/index.sync-conflict-20260309-222859-6PCZ3ZU.js +5 -0
  72. package/dist/output/reporters.d.sync-conflict-20260309-193820-6PCZ3ZU.ts +38 -0
  73. package/dist/output/reporters.d.ts.sync-conflict-20260306-193811-6PCZ3ZU.map +1 -0
  74. package/dist/output/reporters.sync-conflict-20260309-030558-6PCZ3ZU.js +182 -0
  75. package/dist/output/reports.d.ts.sync-conflict-20260307-172149-6PCZ3ZU.map +1 -0
  76. package/dist/output/reports.js.sync-conflict-20260305-161643-6PCZ3ZU.map +1 -0
  77. package/dist/output/reports.sync-conflict-20260305-211951-6PCZ3ZU.js +393 -0
  78. package/dist/output/visuals.d.ts +53 -0
  79. package/dist/output/visuals.d.ts.map +1 -0
  80. package/dist/output/visuals.js +194 -0
  81. package/dist/output/visuals.js.map +1 -0
  82. package/dist/services/drift-analysis.d.sync-conflict-20260306-151016-6PCZ3ZU.ts +194 -0
  83. package/dist/services/drift-analysis.d.ts.sync-conflict-20260307-175904-6PCZ3ZU.map +1 -0
  84. package/dist/services/drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.d.ts +194 -0
  85. package/dist/services/drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.d.ts.map +1 -0
  86. package/dist/services/drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.js +1022 -0
  87. package/dist/services/drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.js.map +1 -0
  88. package/dist/services/skill-export.d.ts.sync-conflict-20260309-171021-6PCZ3ZU.map +1 -0
  89. package/dist/services/skill-export.sync-conflict-20260309-144621-6PCZ3ZU.d.ts +109 -0
  90. package/dist/services/skill-export.sync-conflict-20260309-144621-6PCZ3ZU.d.ts.map +1 -0
  91. package/dist/services/skill-export.sync-conflict-20260309-144621-6PCZ3ZU.js +737 -0
  92. package/dist/services/skill-export.sync-conflict-20260309-144621-6PCZ3ZU.js.map +1 -0
  93. package/package.json +3 -3
@@ -0,0 +1,1735 @@
1
+ import { Command, Option } from "commander";
2
+ import chalk from "chalk";
3
+ import { existsSync, readFileSync, readdirSync } from "fs";
4
+ import { writeFileSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ import { loadConfig, getConfigPath } from "../config/loader.js";
8
+ import { buildAutoConfig, detectMonorepo } from "../config/auto-detect.js";
9
+ import { spinner, error, info, success, header, keyValue, newline, setJsonMode, } from "../output/reporters.js";
10
+ import { formatDriftTable, formatDriftList, formatDriftTree, formatMarkdown, formatHtml, formatAgent, } from "../output/formatters.js";
11
+ import { ScanOrchestrator } from "../scan/orchestrator.js";
12
+ import { DriftAnalysisService } from "../services/drift-analysis.js";
13
+ import { saveHistory, getLastScore } from "../services/scan-history.js";
14
+ import { withOptionalCache } from "@buoy-design/scanners";
15
+ import { formatUpgradeHint } from "../utils/upgrade-hints.js";
16
+ import { calculateHealthScorePillar, DriftAggregator } from "@buoy-design/core";
17
+ import { detectHookSystem } from "../hooks/index.js";
18
+ import { detectFrameworks, BUILTIN_SCANNERS, PLUGIN_INFO, } from "../detect/frameworks.js";
19
+ // Design system library names detected by detectFrameworks()
20
+ const DS_LIBRARY_NAMES = [
21
+ "mui", "chakra", "mantine", "ant-design", "radix",
22
+ "headlessui", "fluentui", "nextui", "primereact",
23
+ "ariakit", "vuetify", "element-plus", "naive-ui", "bootstrap",
24
+ "shadcn",
25
+ ];
26
+ // Utility/styling framework names
27
+ const UTILITY_FRAMEWORK_NAMES = [
28
+ "tailwind", "styled-components", "emotion", "stitches",
29
+ ];
30
+ // DS libraries that include their own styling systems
31
+ const DS_WITH_STYLING = ["chakra", "mantine", "mui"];
32
+ // Known vendored shadcn/ui component filenames
33
+ const VENDORED_SHADCN_FILES = new Set([
34
+ 'dropdown-menu', 'sidebar', 'menubar', 'select', 'sheet',
35
+ 'dialog', 'popover', 'tooltip', 'accordion', 'alert-dialog',
36
+ 'command', 'context-menu', 'navigation-menu', 'hover-card',
37
+ 'radio-group', 'toggle-group', 'tabs', 'scroll-area',
38
+ 'separator', 'slider', 'switch', 'textarea', 'toast',
39
+ 'toaster', 'sonner', 'carousel', 'chart', 'drawer',
40
+ 'input-otp', 'resizable', 'pagination', 'breadcrumb',
41
+ 'collapsible', 'aspect-ratio', 'avatar', 'badge',
42
+ 'calendar', 'card', 'checkbox', 'form', 'input', 'label',
43
+ 'progress', 'skeleton', 'table', 'toggle', 'button',
44
+ ]);
45
+ function isVendoredShadcnFile(filePath) {
46
+ // Extract basename without extension
47
+ const basename = (filePath.split('/').pop() || '').replace(/\.(tsx|jsx|ts|js)$/, '');
48
+ if (!VENDORED_SHADCN_FILES.has(basename))
49
+ return false;
50
+ // Match on any UI-related directory path (not just components/ui/)
51
+ // Catches: src/ui/, src/primitives/, src/ds/components/, src/libs/ui/,
52
+ // registry/new-york/ui/, packages/ui/src/components/, etc.
53
+ return /\/(ui|primitives|registry|ds)\b/.test(filePath)
54
+ || /\/components\//.test(filePath);
55
+ }
56
+ function isComponentFile(filePath) {
57
+ const componentExts = ['.tsx', '.jsx', '.vue', '.svelte'];
58
+ return componentExts.some(ext => filePath.endsWith(ext));
59
+ }
60
+ function isLikelyGeneratedFile(filePath, issueCount) {
61
+ const filename = filePath.split('/').pop() || '';
62
+ if (issueCount > 50 && (filename.startsWith('icon') || filename.includes('icons')))
63
+ return true;
64
+ return false;
65
+ }
66
+ export function createShowCommand() {
67
+ const cmd = new Command("show")
68
+ .description("Show design system information")
69
+ .option("--json", "Output as JSON (default)")
70
+ .option("--no-cache", "Disable incremental scanning cache");
71
+ // show components [query]
72
+ cmd
73
+ .command("components")
74
+ .description("Show components found in the codebase (with optional search)")
75
+ .argument("[query]", "Search query (component name or partial match)")
76
+ .option("--json", "Output as JSON")
77
+ .option("--prop <propName>", "Search by prop name (e.g., \"onClick\")")
78
+ .option("--pattern <pattern>", "Search by pattern (checks name, props, variants)")
79
+ .option("-n, --limit <number>", "Maximum results to show", "10")
80
+ .action(async (query, options, command) => {
81
+ const parentOpts = command.parent?.opts() || {};
82
+ const json = options.json || parentOpts.json !== false;
83
+ if (json)
84
+ setJsonMode(true);
85
+ const spin = spinner("Scanning components...");
86
+ try {
87
+ const config = await getOrBuildConfig();
88
+ const { result: scanResult } = await withOptionalCache(process.cwd(), parentOpts.cache !== false, async (cache) => {
89
+ const orchestrator = new ScanOrchestrator(config, process.cwd(), { cache });
90
+ return orchestrator.scanComponents({
91
+ onProgress: (msg) => { spin.text = msg; },
92
+ });
93
+ });
94
+ spin.stop();
95
+ const components = scanResult.components;
96
+ // If no search query/options, list all components
97
+ if (!query && !options.prop && !options.pattern) {
98
+ console.log(JSON.stringify({ components }, null, 2));
99
+ return;
100
+ }
101
+ // Search mode
102
+ let results = [];
103
+ if (options.prop) {
104
+ const lowerProp = options.prop.toLowerCase();
105
+ for (const component of components) {
106
+ const props = component.props || [];
107
+ const matchingProp = props.find((p) => p.name.toLowerCase().includes(lowerProp));
108
+ if (matchingProp) {
109
+ results.push({
110
+ component,
111
+ score: matchingProp.name.toLowerCase() === lowerProp ? 100 : 80,
112
+ matchType: "prop",
113
+ });
114
+ }
115
+ }
116
+ }
117
+ else if (options.pattern) {
118
+ const lowerPattern = options.pattern.toLowerCase();
119
+ for (const component of components) {
120
+ let score = 0;
121
+ const nameScore = fuzzyScore(options.pattern, component.name);
122
+ if (nameScore > 0)
123
+ score += nameScore * 0.5;
124
+ const props = component.props || [];
125
+ if (props.some((p) => p.name.toLowerCase().includes(lowerPattern) ||
126
+ (p.type && p.type.toLowerCase().includes(lowerPattern))))
127
+ score += 30;
128
+ const variants = component.variants || [];
129
+ if (variants.some((v) => v.name.toLowerCase().includes(lowerPattern)))
130
+ score += 20;
131
+ if (score >= 30) {
132
+ results.push({ component, score: Math.min(100, score), matchType: "pattern" });
133
+ }
134
+ }
135
+ }
136
+ else if (query) {
137
+ for (const component of components) {
138
+ const score = fuzzyScore(query, component.name);
139
+ if (score >= 30) {
140
+ results.push({
141
+ component,
142
+ score,
143
+ matchType: score === 100 ? "exact" : "fuzzy",
144
+ });
145
+ }
146
+ }
147
+ }
148
+ results.sort((a, b) => b.score - a.score);
149
+ const limit = parseInt(options.limit, 10);
150
+ results = results.slice(0, limit);
151
+ if (json) {
152
+ console.log(JSON.stringify({
153
+ query: query || null,
154
+ prop: options.prop || null,
155
+ pattern: options.pattern || null,
156
+ results: results.map(r => ({
157
+ name: r.component.name,
158
+ path: "path" in r.component.source ? r.component.source.path : "unknown",
159
+ props: r.component.props?.map((p) => ({
160
+ name: p.name, type: p.type, required: p.required,
161
+ })),
162
+ variants: r.component.variants?.map((v) => v.name),
163
+ score: Math.round(r.score),
164
+ matchType: r.matchType,
165
+ })),
166
+ totalComponents: components.length,
167
+ }, null, 2));
168
+ return;
169
+ }
170
+ if (results.length === 0) {
171
+ newline();
172
+ info("No matching components found");
173
+ if (query)
174
+ info("Try a different search term or use --pattern for broader search");
175
+ return;
176
+ }
177
+ newline();
178
+ header(`Found ${results.length} component${results.length === 1 ? "" : "s"}`);
179
+ newline();
180
+ for (const { component, score, matchType } of results) {
181
+ const scoreLabel = matchType === "exact" ? chalk.green("exact")
182
+ : matchType === "prop" ? chalk.cyan("prop match")
183
+ : matchType === "pattern" ? chalk.yellow("pattern")
184
+ : chalk.dim(`${Math.round(score)}%`);
185
+ console.log(` ${chalk.bold(component.name)} ${scoreLabel}`);
186
+ keyValue(" Path", "path" in component.source ? component.source.path : "unknown");
187
+ const props = component.props || [];
188
+ const propStr = props.length === 0 ? chalk.dim("(no props)") : props.slice(0, 5).map((p) => {
189
+ const req = p.required ? "*" : "";
190
+ const t = p.type ? chalk.dim(`: ${p.type}`) : "";
191
+ return `${p.name}${req}${t}`;
192
+ }).join(", ") + (props.length > 5 ? chalk.dim(` +${props.length - 5} more`) : "");
193
+ keyValue(" Props", propStr);
194
+ const variants = component.variants || [];
195
+ if (variants.length > 0) {
196
+ const variantNames = variants.slice(0, 4).map((v) => v.name).join(", ");
197
+ const more = variants.length > 4 ? chalk.dim(` +${variants.length - 4} more`) : "";
198
+ keyValue(" Variants", variantNames + more);
199
+ }
200
+ newline();
201
+ }
202
+ console.log(chalk.dim("─".repeat(40)));
203
+ info(`${components.length} total components in project`);
204
+ if (results.length === limit)
205
+ info(`Showing first ${limit} results. Use --limit to see more.`);
206
+ }
207
+ catch (err) {
208
+ spin.stop();
209
+ error(err instanceof Error ? err.message : String(err));
210
+ process.exit(1);
211
+ }
212
+ });
213
+ // show tokens
214
+ cmd
215
+ .command("tokens")
216
+ .description("Show design tokens found in the codebase")
217
+ .option("--json", "Output as JSON")
218
+ .action(async (options, command) => {
219
+ const parentOpts = command.parent?.opts() || {};
220
+ const json = options.json || parentOpts.json !== false;
221
+ if (json)
222
+ setJsonMode(true);
223
+ const spin = spinner("Scanning tokens...");
224
+ try {
225
+ const config = await getOrBuildConfig();
226
+ const { result: scanResult } = await withOptionalCache(process.cwd(), parentOpts.cache !== false, async (cache) => {
227
+ const orchestrator = new ScanOrchestrator(config, process.cwd(), { cache });
228
+ return orchestrator.scanTokens({
229
+ onProgress: (msg) => { spin.text = msg; },
230
+ });
231
+ });
232
+ spin.stop();
233
+ console.log(JSON.stringify({ tokens: scanResult.tokens }, null, 2));
234
+ }
235
+ catch (err) {
236
+ spin.stop();
237
+ error(err instanceof Error ? err.message : String(err));
238
+ process.exit(1);
239
+ }
240
+ });
241
+ // show drift
242
+ cmd
243
+ .command("drift")
244
+ .description("Show drift signals (design system violations)")
245
+ .option("--json", "Output as JSON")
246
+ .option("--raw", "Output raw signals without grouping")
247
+ .option("-f, --format <type>", "Output format: json, markdown, html, table, tree, agent")
248
+ .addOption(new Option("-S, --severity <level>", "Filter by minimum severity").choices(["info", "warning", "critical"]))
249
+ .option("-t, --type <type>", "Filter by drift type")
250
+ .option("-v, --verbose", "Verbose output with full details")
251
+ .option("--include-ignored", "Include ignored drifts (show all)")
252
+ .option("--clear-cache", "Clear cache before scanning")
253
+ .action(async (options, command) => {
254
+ const parentOpts = command.parent?.opts() || {};
255
+ const json = options.json || parentOpts.json !== false;
256
+ const useJson = json && !options.format && !options.verbose;
257
+ if (useJson || options.format === "json" || options.format === "agent") {
258
+ setJsonMode(true);
259
+ }
260
+ const spin = spinner("Analyzing drift...");
261
+ try {
262
+ const config = await getOrBuildConfig();
263
+ const { result } = await withOptionalCache(process.cwd(), parentOpts.cache !== false, async (cache) => {
264
+ const service = new DriftAnalysisService(config);
265
+ return service.analyze({
266
+ onProgress: (msg) => { spin.text = msg; },
267
+ includeIgnored: options.includeIgnored ?? false,
268
+ minSeverity: options.severity,
269
+ filterType: options.type,
270
+ cache,
271
+ });
272
+ }, {
273
+ clearCache: options.clearCache,
274
+ onVerbose: options.verbose ? info : undefined,
275
+ });
276
+ const drifts = result.drifts;
277
+ const sourceComponents = result.components;
278
+ const ignoredCount = result.ignoredCount;
279
+ spin.stop();
280
+ // Determine output format
281
+ const format = options.format;
282
+ if (format === "agent") {
283
+ console.log(formatAgent(drifts));
284
+ return;
285
+ }
286
+ if (format === "markdown") {
287
+ console.log(formatMarkdown(drifts));
288
+ return;
289
+ }
290
+ if (format === "html") {
291
+ const htmlContent = formatHtml(drifts, { designerFriendly: true });
292
+ const filename = "drift-report.html";
293
+ writeFileSync(filename, htmlContent);
294
+ success(`HTML report saved to ${filename}`);
295
+ return;
296
+ }
297
+ // Raw mode: output signals without grouping
298
+ if (options.raw) {
299
+ const output = {
300
+ drifts: result.drifts,
301
+ summary: {
302
+ total: result.drifts.length,
303
+ critical: result.drifts.filter((d) => d.severity === "critical").length,
304
+ warning: result.drifts.filter((d) => d.severity === "warning").length,
305
+ info: result.drifts.filter((d) => d.severity === "info").length,
306
+ },
307
+ };
308
+ console.log(JSON.stringify(output, null, 2));
309
+ return;
310
+ }
311
+ // JSON output (explicit --json or --format json)
312
+ if (format === "json" || useJson) {
313
+ // Grouped mode: aggregate signals for actionability
314
+ const aggregationConfig = config.drift?.aggregation ?? {};
315
+ const aggregator = new DriftAggregator({
316
+ strategies: aggregationConfig.strategies,
317
+ minGroupSize: aggregationConfig.minGroupSize,
318
+ pathPatterns: aggregationConfig.pathPatterns,
319
+ });
320
+ const aggregated = aggregator.aggregate(result.drifts);
321
+ const output = {
322
+ groups: aggregated.groups.map(g => ({
323
+ id: g.id,
324
+ strategy: g.groupingKey.strategy,
325
+ key: g.groupingKey.value,
326
+ summary: g.summary,
327
+ count: g.totalCount,
328
+ severity: g.bySeverity,
329
+ representative: {
330
+ type: g.representative.type,
331
+ message: g.representative.message,
332
+ location: g.representative.source.location,
333
+ tokenSuggestions: g.representative.details?.tokenSuggestions || undefined,
334
+ },
335
+ })),
336
+ ungrouped: aggregated.ungrouped.map(d => ({
337
+ id: d.id,
338
+ type: d.type,
339
+ severity: d.severity,
340
+ message: d.message,
341
+ location: d.source.location,
342
+ tokenSuggestions: d.details?.tokenSuggestions || undefined,
343
+ })),
344
+ summary: {
345
+ totalSignals: aggregated.totalSignals,
346
+ totalGroups: aggregated.totalGroups,
347
+ ungroupedCount: aggregated.ungrouped.length,
348
+ reductionRatio: Math.round(aggregated.reductionRatio * 10) / 10,
349
+ bySeverity: {
350
+ critical: result.drifts.filter((d) => d.severity === "critical").length,
351
+ warning: result.drifts.filter((d) => d.severity === "warning").length,
352
+ info: result.drifts.filter((d) => d.severity === "info").length,
353
+ },
354
+ },
355
+ ignoredCount,
356
+ };
357
+ console.log(JSON.stringify(output, null, 2));
358
+ return;
359
+ }
360
+ // Human-readable output formats
361
+ const uniqueFiles = new Set(drifts.map(d => d.source.location?.split(':')[0] || d.source.entityName));
362
+ if (options.verbose) {
363
+ header("Drift Analysis");
364
+ newline();
365
+ const summary = getSummary(drifts);
366
+ keyValue("Components scanned", String(sourceComponents.length));
367
+ keyValue("Critical", String(summary.critical));
368
+ keyValue("Warning", String(summary.warning));
369
+ keyValue("Info", String(summary.info));
370
+ if (ignoredCount > 0) {
371
+ keyValue("Ignored (hidden)", String(ignoredCount));
372
+ }
373
+ newline();
374
+ console.log(formatDriftList(drifts));
375
+ }
376
+ else if (format === "table") {
377
+ header("Drift Analysis");
378
+ newline();
379
+ console.log(formatDriftTable(drifts));
380
+ }
381
+ else {
382
+ // Default: tree view
383
+ newline();
384
+ console.log(formatDriftTree(drifts, uniqueFiles.size));
385
+ }
386
+ // Handle empty results
387
+ if (drifts.length === 0) {
388
+ if (sourceComponents.length === 0) {
389
+ newline();
390
+ info("No components found to analyze.");
391
+ info("To find hardcoded inline styles:");
392
+ info(" " + chalk.cyan("buoy show health") + " # See all hardcoded values");
393
+ }
394
+ else {
395
+ const hasTokens = config.sources.tokens?.enabled &&
396
+ (config.sources.tokens.files?.length ?? 0) > 0;
397
+ const hasFigma = config.sources.figma?.enabled;
398
+ const hasStorybook = config.sources.storybook?.enabled;
399
+ const hasDesignTokensFile = existsSync('design-tokens.css') ||
400
+ existsSync('design-tokens.json');
401
+ if (!hasTokens && !hasFigma && !hasStorybook && !hasDesignTokensFile) {
402
+ newline();
403
+ info("No reference source configured.");
404
+ info("Run " + chalk.cyan("buoy dock tokens") + " to extract design tokens.");
405
+ }
406
+ }
407
+ }
408
+ // Show upgrade hint when drifts found
409
+ if (drifts.length > 0) {
410
+ const hint = formatUpgradeHint('after-drift-found');
411
+ if (hint) {
412
+ newline();
413
+ console.log(hint);
414
+ }
415
+ }
416
+ }
417
+ catch (err) {
418
+ spin.stop();
419
+ error(err instanceof Error ? err.message : String(err));
420
+ process.exit(1);
421
+ }
422
+ });
423
+ // show health
424
+ cmd
425
+ .command("health")
426
+ .description("Show design system health score")
427
+ .option("--json", "Output as JSON")
428
+ .action(async (options, command) => {
429
+ const parentOpts = command.parent?.opts() || {};
430
+ const json = options.json ?? parentOpts.json;
431
+ if (json)
432
+ setJsonMode(true);
433
+ const spin = spinner("Analyzing design system health...");
434
+ try {
435
+ const config = await getOrBuildConfig();
436
+ // Gather all health metrics from drift analysis
437
+ const healthMetrics = await gatherHealthMetrics(config, spin, parentOpts.cache !== false);
438
+ spin.stop();
439
+ const result = calculateHealthScorePillar(healthMetrics);
440
+ // Save to local history for trend tracking
441
+ saveHistory(process.cwd(), {
442
+ timestamp: new Date().toISOString(),
443
+ score: result.score,
444
+ tier: result.tier,
445
+ driftCount: healthMetrics.totalDriftCount ?? 0,
446
+ componentCount: healthMetrics.componentCount,
447
+ });
448
+ if (json) {
449
+ console.log(JSON.stringify({
450
+ score: result.score,
451
+ tier: result.tier,
452
+ pillars: {
453
+ valueDiscipline: { score: result.pillars.valueDiscipline.score, max: 60 },
454
+ tokenHealth: { score: result.pillars.tokenHealth.score, max: 20 },
455
+ consistency: { score: result.pillars.consistency.score, max: 10 },
456
+ criticalIssues: { score: result.pillars.criticalIssues.score, max: 10 },
457
+ },
458
+ suggestions: result.suggestions,
459
+ metrics: result.metrics,
460
+ }, null, 2));
461
+ }
462
+ else {
463
+ printPillarHealthReport(result);
464
+ }
465
+ }
466
+ catch (err) {
467
+ spin.stop();
468
+ error(err instanceof Error ? err.message : String(err));
469
+ process.exit(1);
470
+ }
471
+ });
472
+ // show history
473
+ const historyCmd = cmd
474
+ .command("history")
475
+ .description("Show scan history and trends")
476
+ .argument("[scan-id]", "Show details of a specific scan")
477
+ .option("--json", "Output as JSON")
478
+ .option("-n, --limit <number>", "Number of entries to show", "10")
479
+ .option("-v, --verbose", "Show detailed information")
480
+ .action(async (scanId, options, command) => {
481
+ const parentOpts = command.parent?.opts() || {};
482
+ const json = options.json || parentOpts.json !== false;
483
+ if (json)
484
+ setJsonMode(true);
485
+ try {
486
+ // Import store dynamically to avoid circular deps
487
+ const { createStore, getProjectName } = await import("../store/index.js");
488
+ // If a scan ID was provided, show details for that scan
489
+ if (scanId) {
490
+ const spin = spinner("Loading scan details...");
491
+ const store = createStore({ forceLocal: true });
492
+ const scan = await store.getScan(scanId);
493
+ if (!scan) {
494
+ spin.stop();
495
+ error(`Scan not found: ${scanId}`);
496
+ store.close();
497
+ process.exit(1);
498
+ }
499
+ const components = await store.getComponents(scanId);
500
+ const tokens = await store.getTokens(scanId);
501
+ const drifts = await store.getDriftSignals(scanId);
502
+ spin.stop();
503
+ if (json) {
504
+ console.log(JSON.stringify({
505
+ scan: {
506
+ id: scan.id,
507
+ status: scan.status,
508
+ sources: scan.sources,
509
+ stats: scan.stats,
510
+ startedAt: scan.startedAt,
511
+ completedAt: scan.completedAt,
512
+ },
513
+ components,
514
+ tokens,
515
+ drifts,
516
+ }, null, 2));
517
+ store.close();
518
+ return;
519
+ }
520
+ header(`Scan: ${scan.id}`);
521
+ newline();
522
+ keyValue("Status", scan.status);
523
+ keyValue("Sources", scan.sources.join(", "));
524
+ if (scan.startedAt) {
525
+ keyValue("Started", scan.startedAt.toLocaleString());
526
+ }
527
+ if (scan.completedAt) {
528
+ keyValue("Completed", scan.completedAt.toLocaleString());
529
+ }
530
+ if (scan.stats?.duration) {
531
+ keyValue("Duration", `${(scan.stats.duration / 1000).toFixed(1)}s`);
532
+ }
533
+ newline();
534
+ keyValue("Components", String(components.length));
535
+ keyValue("Tokens", String(tokens.length));
536
+ keyValue("Drift signals", String(drifts.length));
537
+ if (components.length > 0) {
538
+ const componentsWithDrift = new Set(drifts.map(d => d.source?.location).filter(Boolean)).size;
539
+ const coveragePercent = Math.round(((components.length - componentsWithDrift) / components.length) * 100);
540
+ const coverageColor = coveragePercent >= 80 ? chalk.green : coveragePercent >= 50 ? chalk.yellow : chalk.red;
541
+ keyValue("Coverage", coverageColor(`${coveragePercent}%`));
542
+ }
543
+ newline();
544
+ if (drifts.length > 0) {
545
+ const critical = drifts.filter((d) => d.severity === "critical").length;
546
+ const warning = drifts.filter((d) => d.severity === "warning").length;
547
+ const infoCount = drifts.filter((d) => d.severity === "info").length;
548
+ console.log(chalk.bold("Drift Breakdown"));
549
+ console.log(` ${chalk.red("Critical:")} ${critical} ` +
550
+ `${chalk.yellow("Warning:")} ${warning} ` +
551
+ `${chalk.blue("Info:")} ${infoCount}`);
552
+ }
553
+ store.close();
554
+ return;
555
+ }
556
+ // Default: list all scans
557
+ const spin = spinner("Loading scan history...");
558
+ const store = createStore({ forceLocal: true });
559
+ const projectName = getProjectName();
560
+ try {
561
+ const project = await store.getOrCreateProject(projectName);
562
+ const limit = parseInt(options.limit, 10) || 10;
563
+ const scans = await store.getScans(project.id, limit);
564
+ const snapshots = await store.getSnapshots(project.id, limit);
565
+ spin.stop();
566
+ if (json) {
567
+ console.log(JSON.stringify({
568
+ project: {
569
+ id: project.id,
570
+ name: project.name,
571
+ },
572
+ scans: scans.map((s) => ({
573
+ id: s.id,
574
+ status: s.status,
575
+ sources: s.sources,
576
+ stats: s.stats,
577
+ startedAt: s.startedAt,
578
+ completedAt: s.completedAt,
579
+ })),
580
+ snapshots: snapshots.map((s) => ({
581
+ scanId: s.scanId,
582
+ componentCount: s.componentCount,
583
+ tokenCount: s.tokenCount,
584
+ driftCount: s.driftCount,
585
+ summary: s.summary,
586
+ createdAt: s.createdAt,
587
+ })),
588
+ }, null, 2));
589
+ store.close();
590
+ return;
591
+ }
592
+ header("Scan History");
593
+ newline();
594
+ keyValue("Project", project.name);
595
+ keyValue("Total scans", String(scans.length));
596
+ newline();
597
+ if (scans.length === 0) {
598
+ info("No scans recorded yet. Run " + chalk.cyan("buoy show all") + " to start tracking.");
599
+ store.close();
600
+ return;
601
+ }
602
+ // Display scans in a table format
603
+ console.log(chalk.dim("ID".padEnd(15)) +
604
+ chalk.dim("Status".padEnd(12)) +
605
+ chalk.dim("Components".padEnd(12)) +
606
+ chalk.dim("Drift".padEnd(8)) +
607
+ chalk.dim("Coverage".padEnd(10)) +
608
+ chalk.dim("Date"));
609
+ console.log(chalk.dim("─".repeat(70)));
610
+ for (const scan of scans) {
611
+ const snapshot = snapshots.find((s) => s.scanId === scan.id);
612
+ const statusColor = scan.status === "completed"
613
+ ? chalk.green
614
+ : scan.status === "failed"
615
+ ? chalk.red
616
+ : chalk.yellow;
617
+ const date = scan.completedAt || scan.startedAt || scan.createdAt;
618
+ const dateStr = date ? formatRelativeDate(date) : "—";
619
+ const compCount = snapshot?.componentCount ?? scan.stats?.componentCount ?? "—";
620
+ const driftCount = snapshot?.driftCount ?? scan.stats?.driftCount ?? "—";
621
+ const coverage = snapshot?.coverageScore != null
622
+ ? `${snapshot.coverageScore}%`
623
+ : "—";
624
+ console.log(chalk.cyan(scan.id.padEnd(15)) +
625
+ statusColor(scan.status.padEnd(12)) +
626
+ String(compCount).padEnd(12) +
627
+ String(driftCount).padEnd(8) +
628
+ coverage.padEnd(10) +
629
+ chalk.dim(dateStr));
630
+ if (options.verbose && snapshot) {
631
+ console.log(chalk.dim(" ") +
632
+ `Critical: ${snapshot.summary.critical}, ` +
633
+ `Warning: ${snapshot.summary.warning}, ` +
634
+ `Info: ${snapshot.summary.info}`);
635
+ if (snapshot.summary.frameworks?.length > 0) {
636
+ console.log(chalk.dim(" Frameworks: ") +
637
+ snapshot.summary.frameworks.join(", "));
638
+ }
639
+ }
640
+ }
641
+ newline();
642
+ // Show trend summary
643
+ if (snapshots.length >= 2) {
644
+ const latest = snapshots[0];
645
+ const previous = snapshots[1];
646
+ const driftDelta = latest.driftCount - previous.driftCount;
647
+ const compDelta = latest.componentCount - previous.componentCount;
648
+ console.log(chalk.bold("Trend Summary"));
649
+ console.log(chalk.dim("─".repeat(30)));
650
+ if (driftDelta > 0) {
651
+ console.log(`Drift: ${chalk.red("+" + driftDelta)} since last scan`);
652
+ }
653
+ else if (driftDelta < 0) {
654
+ console.log(`Drift: ${chalk.green(driftDelta)} since last scan`);
655
+ }
656
+ else {
657
+ console.log(`Drift: ${chalk.dim("no change")}`);
658
+ }
659
+ if (compDelta > 0) {
660
+ console.log(`Components: ${chalk.green("+" + compDelta)} since last scan`);
661
+ }
662
+ else if (compDelta < 0) {
663
+ console.log(`Components: ${chalk.red(compDelta)} since last scan`);
664
+ }
665
+ else {
666
+ console.log(`Components: ${chalk.dim("no change")}`);
667
+ }
668
+ if (latest.coverageScore != null && previous.coverageScore != null) {
669
+ const covDelta = latest.coverageScore - previous.coverageScore;
670
+ if (covDelta > 0) {
671
+ console.log(`Coverage: ${chalk.green("+" + covDelta + "%")} since last scan`);
672
+ }
673
+ else if (covDelta < 0) {
674
+ console.log(`Coverage: ${chalk.red(covDelta + "%")} since last scan`);
675
+ }
676
+ else {
677
+ console.log(`Coverage: ${chalk.dim("no change")}`);
678
+ }
679
+ }
680
+ }
681
+ store.close();
682
+ }
683
+ catch (storeErr) {
684
+ spin.stop();
685
+ store.close();
686
+ const msg = storeErr instanceof Error ? storeErr.message : String(storeErr);
687
+ error(`Failed to load history: ${msg}`);
688
+ info("Run " + chalk.cyan("buoy show all") + " first to start tracking history.");
689
+ process.exit(1);
690
+ }
691
+ }
692
+ catch (err) {
693
+ error(err instanceof Error ? err.message : String(err));
694
+ process.exit(1);
695
+ }
696
+ });
697
+ // show history compare <scan1> <scan2>
698
+ historyCmd
699
+ .command("compare <scan1> <scan2>")
700
+ .description("Compare two scans")
701
+ .option("--json", "Output as JSON")
702
+ .action(async (scan1, scan2, options) => {
703
+ if (options.json) {
704
+ setJsonMode(true);
705
+ }
706
+ const spin = spinner("Comparing scans...");
707
+ try {
708
+ const { createStore } = await import("../store/index.js");
709
+ const store = createStore({ forceLocal: true });
710
+ const diff = await store.compareScan(scan1, scan2);
711
+ spin.stop();
712
+ if (options.json) {
713
+ console.log(JSON.stringify(diff, null, 2));
714
+ store.close();
715
+ return;
716
+ }
717
+ header(`Comparing ${scan1} → ${scan2}`);
718
+ newline();
719
+ console.log(chalk.bold("Components"));
720
+ console.log(` ${chalk.green("Added:")} ${diff.added.components.length} ` +
721
+ `${chalk.red("Removed:")} ${diff.removed.components.length} ` +
722
+ `${chalk.yellow("Modified:")} ${diff.modified.components.length}`);
723
+ if (diff.added.components.length > 0) {
724
+ console.log(chalk.green(" + ") + diff.added.components.map((c) => c.name).join(", "));
725
+ }
726
+ if (diff.removed.components.length > 0) {
727
+ console.log(chalk.red(" - ") + diff.removed.components.map((c) => c.name).join(", "));
728
+ }
729
+ newline();
730
+ console.log(chalk.bold("Tokens"));
731
+ console.log(` ${chalk.green("Added:")} ${diff.added.tokens.length} ` +
732
+ `${chalk.red("Removed:")} ${diff.removed.tokens.length} ` +
733
+ `${chalk.yellow("Modified:")} ${diff.modified.tokens.length}`);
734
+ newline();
735
+ console.log(chalk.bold("Drift Signals"));
736
+ console.log(` ${chalk.green("New:")} ${diff.added.drifts.length} ` +
737
+ `${chalk.red("Resolved:")} ${diff.removed.drifts.length}`);
738
+ if (diff.added.drifts.length > 0) {
739
+ newline();
740
+ console.log(chalk.yellow("New drift signals:"));
741
+ for (const d of diff.added.drifts.slice(0, 5)) {
742
+ console.log(` ${d.severity === "critical" ? chalk.red("!") : chalk.yellow("~")} ${d.message}`);
743
+ }
744
+ if (diff.added.drifts.length > 5) {
745
+ console.log(chalk.dim(` ... and ${diff.added.drifts.length - 5} more`));
746
+ }
747
+ }
748
+ newline();
749
+ console.log(chalk.bold("Summary"));
750
+ if (diff.removed.drifts.length > 0) {
751
+ console.log(` ${chalk.green("\u2193")} ${diff.removed.drifts.length} issues resolved`);
752
+ }
753
+ if (diff.added.drifts.length > 0) {
754
+ console.log(` ${chalk.red("\u2191")} ${diff.added.drifts.length} new issues introduced`);
755
+ }
756
+ store.close();
757
+ }
758
+ catch (err) {
759
+ spin.stop();
760
+ const message = err instanceof Error ? err.message : String(err);
761
+ error(`Compare failed: ${message}`);
762
+ process.exit(1);
763
+ }
764
+ });
765
+ // show config
766
+ cmd
767
+ .command("config")
768
+ .description("Show current .buoy.yaml configuration")
769
+ .option("--json", "Output as JSON")
770
+ .action(async (options, command) => {
771
+ const parentOpts = command.parent?.opts() || {};
772
+ const json = options.json || parentOpts.json !== false;
773
+ if (json)
774
+ setJsonMode(true);
775
+ const configPath = getConfigPath();
776
+ if (!configPath) {
777
+ if (json) {
778
+ console.log(JSON.stringify({ exists: false, path: null }, null, 2));
779
+ }
780
+ else {
781
+ info("No config found. Run " + chalk.cyan("buoy dock config") + " to create .buoy.yaml.");
782
+ }
783
+ return;
784
+ }
785
+ try {
786
+ const { config } = await loadConfig();
787
+ if (json) {
788
+ console.log(JSON.stringify({ exists: true, path: configPath, config }, null, 2));
789
+ }
790
+ else {
791
+ header("Configuration");
792
+ newline();
793
+ keyValue("Path", configPath);
794
+ if (config.project?.name)
795
+ keyValue("Project", config.project.name);
796
+ newline();
797
+ // Show enabled sources
798
+ if (config.sources) {
799
+ header("Sources");
800
+ for (const [key, source] of Object.entries(config.sources)) {
801
+ if (source && typeof source === "object" && "enabled" in source) {
802
+ const enabled = source.enabled;
803
+ const status = enabled ? chalk.green("enabled") : chalk.dim("disabled");
804
+ keyValue(` ${key}`, status);
805
+ }
806
+ }
807
+ }
808
+ newline();
809
+ }
810
+ }
811
+ catch (err) {
812
+ error(err instanceof Error ? err.message : String(err));
813
+ process.exit(1);
814
+ }
815
+ });
816
+ // show skills
817
+ cmd
818
+ .command("skills")
819
+ .description("Show AI agent skill files")
820
+ .option("--json", "Output as JSON")
821
+ .action(async (options, command) => {
822
+ const parentOpts = command.parent?.opts() || {};
823
+ const json = options.json || parentOpts.json !== false;
824
+ if (json)
825
+ setJsonMode(true);
826
+ const cwd = process.cwd();
827
+ const skillsDir = join(cwd, ".claude", "skills", "design-system");
828
+ if (!existsSync(skillsDir)) {
829
+ if (json) {
830
+ console.log(JSON.stringify({ exists: false, path: skillsDir, files: [] }, null, 2));
831
+ }
832
+ else {
833
+ info("No skills found. Run " + chalk.cyan("buoy dock skills") + " to create AI agent skills.");
834
+ }
835
+ return;
836
+ }
837
+ try {
838
+ const files = walkDir(skillsDir);
839
+ if (json) {
840
+ console.log(JSON.stringify({
841
+ exists: true,
842
+ path: skillsDir,
843
+ fileCount: files.length,
844
+ files: files.map(f => f.replace(cwd + "/", "")),
845
+ }, null, 2));
846
+ }
847
+ else {
848
+ header("Design System Skills");
849
+ newline();
850
+ keyValue("Path", skillsDir.replace(cwd + "/", ""));
851
+ keyValue("Files", String(files.length));
852
+ newline();
853
+ for (const file of files) {
854
+ console.log(` ${chalk.green("•")} ${file.replace(cwd + "/", "")}`);
855
+ }
856
+ newline();
857
+ }
858
+ }
859
+ catch (err) {
860
+ error(err instanceof Error ? err.message : String(err));
861
+ process.exit(1);
862
+ }
863
+ });
864
+ // show agents
865
+ cmd
866
+ .command("agents")
867
+ .description("Show configured AI agents and commands")
868
+ .option("--json", "Output as JSON")
869
+ .action(async (options, command) => {
870
+ const parentOpts = command.parent?.opts() || {};
871
+ const json = options.json || parentOpts.json !== false;
872
+ if (json)
873
+ setJsonMode(true);
874
+ const cwd = process.cwd();
875
+ const agentsDir = join(cwd, ".claude", "agents");
876
+ const commandsDir = join(cwd, ".claude", "commands");
877
+ const hasAgents = existsSync(agentsDir) && readdirSync(agentsDir).some(f => f.endsWith(".md"));
878
+ const hasCommands = existsSync(commandsDir) && readdirSync(commandsDir).some(f => f.endsWith(".md"));
879
+ if (!hasAgents && !hasCommands) {
880
+ if (json) {
881
+ console.log(JSON.stringify({ exists: false, agents: [], commands: [] }, null, 2));
882
+ }
883
+ else {
884
+ info("No agents configured. Run " + chalk.cyan("buoy dock agents") + " to set up AI agents.");
885
+ }
886
+ return;
887
+ }
888
+ const agents = hasAgents
889
+ ? readdirSync(agentsDir).filter(f => f.endsWith(".md")).map(f => ({
890
+ name: f.replace(".md", ""),
891
+ path: join(".claude", "agents", f),
892
+ }))
893
+ : [];
894
+ const commands = hasCommands
895
+ ? readdirSync(commandsDir).filter(f => f.endsWith(".md")).map(f => ({
896
+ name: f.replace(".md", ""),
897
+ path: join(".claude", "commands", f),
898
+ }))
899
+ : [];
900
+ if (json) {
901
+ console.log(JSON.stringify({ exists: true, agents, commands }, null, 2));
902
+ }
903
+ else {
904
+ if (agents.length > 0) {
905
+ header("Agents");
906
+ newline();
907
+ for (const agent of agents) {
908
+ console.log(` ${chalk.green("•")} ${agent.name} ${chalk.dim(agent.path)}`);
909
+ }
910
+ newline();
911
+ }
912
+ if (commands.length > 0) {
913
+ header("Commands");
914
+ newline();
915
+ for (const cmd of commands) {
916
+ console.log(` ${chalk.green("•")} /${cmd.name} ${chalk.dim(cmd.path)}`);
917
+ }
918
+ newline();
919
+ }
920
+ }
921
+ });
922
+ // show context
923
+ cmd
924
+ .command("context")
925
+ .description("Show design system context in CLAUDE.md")
926
+ .option("--json", "Output as JSON")
927
+ .action(async (options, command) => {
928
+ const parentOpts = command.parent?.opts() || {};
929
+ const json = options.json || parentOpts.json !== false;
930
+ if (json)
931
+ setJsonMode(true);
932
+ const cwd = process.cwd();
933
+ const claudeMdPath = join(cwd, "CLAUDE.md");
934
+ if (!existsSync(claudeMdPath)) {
935
+ if (json) {
936
+ console.log(JSON.stringify({ exists: false, path: claudeMdPath }, null, 2));
937
+ }
938
+ else {
939
+ info("No design system context in CLAUDE.md. Run " + chalk.cyan("buoy dock context") + " to generate it.");
940
+ }
941
+ return;
942
+ }
943
+ const content = readFileSync(claudeMdPath, "utf-8");
944
+ const sectionMatch = content.match(/^##?\s*Design\s*System\b.*$/m);
945
+ if (!sectionMatch) {
946
+ if (json) {
947
+ console.log(JSON.stringify({ exists: false, path: claudeMdPath, hasSection: false }, null, 2));
948
+ }
949
+ else {
950
+ info("No design system context in CLAUDE.md. Run " + chalk.cyan("buoy dock context") + " to generate it.");
951
+ }
952
+ return;
953
+ }
954
+ // Extract section content between ## Design System and next ## header
955
+ const sectionStart = content.indexOf(sectionMatch[0]);
956
+ const afterHeader = content.slice(sectionStart + sectionMatch[0].length);
957
+ const nextHeaderMatch = afterHeader.match(/\n##?\s+[^\n]/);
958
+ const sectionContent = nextHeaderMatch
959
+ ? afterHeader.slice(0, nextHeaderMatch.index)
960
+ : afterHeader;
961
+ const sectionLines = sectionContent.trim().split("\n").length;
962
+ const sectionWords = sectionContent.trim().split(/\s+/).length;
963
+ if (json) {
964
+ console.log(JSON.stringify({
965
+ exists: true,
966
+ path: claudeMdPath,
967
+ hasSection: true,
968
+ sectionLines,
969
+ sectionWords,
970
+ }, null, 2));
971
+ }
972
+ else {
973
+ header("Design System Context");
974
+ newline();
975
+ keyValue("Path", "CLAUDE.md");
976
+ keyValue("Lines", String(sectionLines));
977
+ keyValue("Words", String(sectionWords));
978
+ newline();
979
+ // Show preview (first 5 lines)
980
+ const preview = sectionContent.trim().split("\n").slice(0, 5);
981
+ for (const line of preview) {
982
+ console.log(chalk.dim(` ${line}`));
983
+ }
984
+ if (sectionLines > 5) {
985
+ console.log(chalk.dim(` ... ${sectionLines - 5} more lines`));
986
+ }
987
+ newline();
988
+ }
989
+ });
990
+ // show hooks
991
+ cmd
992
+ .command("hooks")
993
+ .description("Show configured hooks for drift checking")
994
+ .option("--json", "Output as JSON")
995
+ .action(async (options, command) => {
996
+ const parentOpts = command.parent?.opts() || {};
997
+ const json = options.json || parentOpts.json !== false;
998
+ if (json)
999
+ setJsonMode(true);
1000
+ const cwd = process.cwd();
1001
+ // Detect git hook system
1002
+ const hookSystem = detectHookSystem(cwd);
1003
+ // Check for buoy in the hook
1004
+ let gitHookInstalled = false;
1005
+ let gitHookPath = null;
1006
+ if (hookSystem === "husky") {
1007
+ gitHookPath = join(cwd, ".husky", "pre-commit");
1008
+ }
1009
+ else if (hookSystem === "pre-commit") {
1010
+ gitHookPath = join(cwd, ".pre-commit-config.yaml");
1011
+ }
1012
+ else if (hookSystem === "git") {
1013
+ gitHookPath = join(cwd, ".git", "hooks", "pre-commit");
1014
+ }
1015
+ if (gitHookPath && existsSync(gitHookPath)) {
1016
+ try {
1017
+ const hookContent = readFileSync(gitHookPath, "utf-8");
1018
+ gitHookInstalled = hookContent.includes("buoy");
1019
+ }
1020
+ catch {
1021
+ // Ignore read errors
1022
+ }
1023
+ }
1024
+ // Check for Claude hooks
1025
+ const claudeSettingsPath = join(cwd, ".claude", "settings.local.json");
1026
+ let claudeHooksEnabled = false;
1027
+ let claudeEvents = [];
1028
+ if (existsSync(claudeSettingsPath)) {
1029
+ try {
1030
+ const settings = JSON.parse(readFileSync(claudeSettingsPath, "utf-8"));
1031
+ const hooks = settings.hooks;
1032
+ if (hooks) {
1033
+ if (hooks.SessionStart?.some((h) => h.hooks?.some(hk => hk.command?.includes("buoy") || hk.command?.includes("Design system")))) {
1034
+ claudeHooksEnabled = true;
1035
+ claudeEvents.push("SessionStart");
1036
+ }
1037
+ if (hooks.PostToolUse?.some((h) => h.hooks?.some(hk => hk.command?.includes("buoy")))) {
1038
+ claudeHooksEnabled = true;
1039
+ claudeEvents.push("PostToolUse");
1040
+ }
1041
+ }
1042
+ }
1043
+ catch {
1044
+ // Ignore parse errors
1045
+ }
1046
+ }
1047
+ if (!gitHookInstalled && !claudeHooksEnabled) {
1048
+ if (json) {
1049
+ console.log(JSON.stringify({
1050
+ gitHooks: { type: hookSystem, installed: false },
1051
+ claudeHooks: { enabled: false, events: [] },
1052
+ }, null, 2));
1053
+ }
1054
+ else {
1055
+ info("No hooks configured. Run " + chalk.cyan("buoy dock hooks") + " to set up drift checking.");
1056
+ }
1057
+ return;
1058
+ }
1059
+ if (json) {
1060
+ console.log(JSON.stringify({
1061
+ gitHooks: { type: hookSystem, path: gitHookPath, installed: gitHookInstalled },
1062
+ claudeHooks: { enabled: claudeHooksEnabled, events: claudeEvents },
1063
+ }, null, 2));
1064
+ }
1065
+ else {
1066
+ header("Hooks");
1067
+ newline();
1068
+ if (gitHookInstalled) {
1069
+ console.log(` ${chalk.green("✓")} Git pre-commit hook (${hookSystem})`);
1070
+ if (gitHookPath)
1071
+ keyValue(" Path", gitHookPath.replace(cwd + "/", ""));
1072
+ }
1073
+ else {
1074
+ console.log(` ${chalk.dim("○")} Git pre-commit hook not installed`);
1075
+ }
1076
+ if (claudeHooksEnabled) {
1077
+ console.log(` ${chalk.green("✓")} Claude Code hooks`);
1078
+ keyValue(" Events", claudeEvents.join(", "));
1079
+ }
1080
+ else {
1081
+ console.log(` ${chalk.dim("○")} Claude Code hooks not configured`);
1082
+ }
1083
+ newline();
1084
+ }
1085
+ });
1086
+ // show commands
1087
+ cmd
1088
+ .command("commands")
1089
+ .description("Show installed slash commands")
1090
+ .option("--json", "Output as JSON")
1091
+ .action(async (options, command) => {
1092
+ const parentOpts = command.parent?.opts() || {};
1093
+ const json = options.json || parentOpts.json !== false;
1094
+ if (json)
1095
+ setJsonMode(true);
1096
+ const commandsDir = join(homedir(), ".claude", "commands");
1097
+ if (!existsSync(commandsDir)) {
1098
+ if (json) {
1099
+ console.log(JSON.stringify({ commands: [] }, null, 2));
1100
+ }
1101
+ else {
1102
+ info("No slash commands installed. Run " + chalk.cyan("buoy dock commands install") + " to set them up.");
1103
+ }
1104
+ return;
1105
+ }
1106
+ const buoyCommands = readdirSync(commandsDir)
1107
+ .filter(f => f.endsWith(".md"))
1108
+ .map(f => ({
1109
+ name: f.replace(".md", ""),
1110
+ installed: true,
1111
+ }));
1112
+ if (buoyCommands.length === 0) {
1113
+ if (json) {
1114
+ console.log(JSON.stringify({ commands: [] }, null, 2));
1115
+ }
1116
+ else {
1117
+ info("No slash commands installed. Run " + chalk.cyan("buoy dock commands install") + " to set them up.");
1118
+ }
1119
+ return;
1120
+ }
1121
+ if (json) {
1122
+ console.log(JSON.stringify({ commands: buoyCommands }, null, 2));
1123
+ }
1124
+ else {
1125
+ header("Slash Commands");
1126
+ newline();
1127
+ for (const cmd of buoyCommands) {
1128
+ console.log(` ${chalk.green("✓")} /${cmd.name}`);
1129
+ }
1130
+ newline();
1131
+ }
1132
+ });
1133
+ // show graph
1134
+ cmd
1135
+ .command("graph")
1136
+ .description("Show knowledge graph statistics")
1137
+ .option("--json", "Output as JSON")
1138
+ .action(async (options, command) => {
1139
+ const parentOpts = command.parent?.opts() || {};
1140
+ const json = options.json || parentOpts.json !== false;
1141
+ if (json)
1142
+ setJsonMode(true);
1143
+ const cwd = process.cwd();
1144
+ const graphPath = join(cwd, ".buoy", "graph.json");
1145
+ if (!existsSync(graphPath)) {
1146
+ if (json) {
1147
+ console.log(JSON.stringify({ exists: false }, null, 2));
1148
+ }
1149
+ else {
1150
+ info("No knowledge graph built. Run " + chalk.cyan("buoy dock graph") + " to build it.");
1151
+ }
1152
+ return;
1153
+ }
1154
+ try {
1155
+ const { importFromJSON, getGraphStats } = await import("@buoy-design/core");
1156
+ const graphData = JSON.parse(readFileSync(graphPath, "utf-8"));
1157
+ const graph = importFromJSON(graphData);
1158
+ const stats = getGraphStats(graph);
1159
+ if (json) {
1160
+ console.log(JSON.stringify({
1161
+ exists: true,
1162
+ path: graphPath,
1163
+ nodes: stats.nodeCount,
1164
+ edges: stats.edgeCount,
1165
+ nodesByType: stats.nodesByType,
1166
+ edgesByType: stats.edgesByType,
1167
+ }, null, 2));
1168
+ }
1169
+ else {
1170
+ header("Knowledge Graph");
1171
+ newline();
1172
+ keyValue("Path", ".buoy/graph.json");
1173
+ keyValue("Nodes", String(stats.nodeCount));
1174
+ keyValue("Edges", String(stats.edgeCount));
1175
+ newline();
1176
+ if (Object.keys(stats.nodesByType).length > 0) {
1177
+ header("Nodes by Type");
1178
+ for (const [type, count] of Object.entries(stats.nodesByType)) {
1179
+ keyValue(` ${type}`, String(count));
1180
+ }
1181
+ newline();
1182
+ }
1183
+ }
1184
+ }
1185
+ catch (err) {
1186
+ error(err instanceof Error ? err.message : String(err));
1187
+ process.exit(1);
1188
+ }
1189
+ });
1190
+ // show plugins
1191
+ cmd
1192
+ .command("plugins")
1193
+ .description("Show available scanners and plugins")
1194
+ .option("--json", "Output as JSON")
1195
+ .action(async (options, command) => {
1196
+ const parentOpts = command.parent?.opts() || {};
1197
+ const json = options.json || parentOpts.json !== false;
1198
+ if (json)
1199
+ setJsonMode(true);
1200
+ const cwd = process.cwd();
1201
+ const detected = await detectFrameworks(cwd);
1202
+ const builtIn = Object.entries(BUILTIN_SCANNERS).map(([key, info]) => ({
1203
+ key,
1204
+ description: info.description,
1205
+ detects: info.detects,
1206
+ }));
1207
+ const optional = Object.entries(PLUGIN_INFO).map(([key, info]) => ({
1208
+ key,
1209
+ name: info.name,
1210
+ description: info.description,
1211
+ }));
1212
+ if (json) {
1213
+ console.log(JSON.stringify({
1214
+ builtIn,
1215
+ detected: detected.map(fw => ({
1216
+ name: fw.name,
1217
+ scanner: fw.scanner,
1218
+ plugin: fw.plugin,
1219
+ confidence: fw.confidence,
1220
+ })),
1221
+ optional,
1222
+ }, null, 2));
1223
+ }
1224
+ else {
1225
+ header("Built-in Scanners");
1226
+ newline();
1227
+ for (const scanner of builtIn) {
1228
+ console.log(` ${chalk.green("✓")} ${chalk.cyan(scanner.description)}`);
1229
+ console.log(` ${chalk.dim(`Detects: ${scanner.detects}`)}`);
1230
+ }
1231
+ newline();
1232
+ if (detected.length > 0) {
1233
+ header("Detected Frameworks");
1234
+ newline();
1235
+ for (const fw of detected) {
1236
+ console.log(` ${chalk.green("•")} ${fw.name} ${chalk.dim(`(${fw.confidence})`)}`);
1237
+ }
1238
+ newline();
1239
+ }
1240
+ if (optional.length > 0) {
1241
+ header("Optional Plugins");
1242
+ newline();
1243
+ for (const plugin of optional) {
1244
+ console.log(` ${chalk.dim("○")} ${chalk.cyan(plugin.name)}`);
1245
+ console.log(` ${chalk.dim(plugin.description)}`);
1246
+ }
1247
+ newline();
1248
+ }
1249
+ }
1250
+ });
1251
+ // show all
1252
+ cmd
1253
+ .command("all")
1254
+ .description("Show everything: components, tokens, drift, and health")
1255
+ .option("--json", "Output as JSON")
1256
+ .action(async (options, command) => {
1257
+ const parentOpts = command.parent?.opts() || {};
1258
+ const json = options.json || parentOpts.json !== false;
1259
+ if (json)
1260
+ setJsonMode(true);
1261
+ const spin = spinner("Gathering design system data...");
1262
+ try {
1263
+ const config = await getOrBuildConfig();
1264
+ const { result: allResults } = await withOptionalCache(process.cwd(), parentOpts.cache !== false, async (cache) => {
1265
+ // Scan components and tokens
1266
+ spin.text = "Scanning components and tokens...";
1267
+ const orchestrator = new ScanOrchestrator(config, process.cwd(), { cache });
1268
+ const scanResult = await orchestrator.scan({
1269
+ onProgress: (msg) => { spin.text = msg; },
1270
+ });
1271
+ // Analyze drift
1272
+ spin.text = "Analyzing drift...";
1273
+ const service = new DriftAnalysisService(config);
1274
+ const driftResult = await service.analyze({
1275
+ onProgress: (msg) => { spin.text = msg; },
1276
+ includeIgnored: false,
1277
+ cache,
1278
+ });
1279
+ return { scanResult, driftResult };
1280
+ });
1281
+ const { scanResult, driftResult } = allResults;
1282
+ // Calculate health using 4-pillar system
1283
+ spin.text = "Calculating health score...";
1284
+ const drifts = driftResult.drifts;
1285
+ const monorepoInfo = await detectMonorepo(process.cwd());
1286
+ const detected = await detectFrameworks(process.cwd(), monorepoInfo ?? undefined);
1287
+ const richContext = computeRichSuggestionContext(drifts);
1288
+ // Count files with high hardcoded value density
1289
+ const fileDriftCounts = new Map();
1290
+ for (const d of drifts) {
1291
+ if (d.type !== "hardcoded-value")
1292
+ continue;
1293
+ const loc = d.source?.location;
1294
+ if (!loc)
1295
+ continue;
1296
+ const file = loc.split(":")[0];
1297
+ if (file)
1298
+ fileDriftCounts.set(file, (fileDriftCounts.get(file) || 0) + 1);
1299
+ }
1300
+ const highDensityFileCount = [...fileDriftCounts.values()].filter(count => count > 2).length;
1301
+ const healthMetrics = {
1302
+ componentCount: scanResult.components.length,
1303
+ tokenCount: scanResult.tokens.length,
1304
+ hardcodedValueCount: drifts.filter(d => d.type === "hardcoded-value").length,
1305
+ unusedTokenCount: drifts.filter(d => d.type === "unused-token").length,
1306
+ namingInconsistencyCount: drifts.filter(d => d.type === "naming-inconsistency").length,
1307
+ criticalCount: drifts.filter(d => d.severity === "critical").length,
1308
+ hasUtilityFramework: detected.some(f => UTILITY_FRAMEWORK_NAMES.includes(f.name))
1309
+ || detected.some(f => DS_WITH_STYLING.includes(f.name)),
1310
+ hasDesignSystemLibrary: detected.some(f => DS_LIBRARY_NAMES.includes(f.name)),
1311
+ totalDriftCount: drifts.length,
1312
+ unusedComponentCount: drifts.filter(d => d.type === "unused-component").length,
1313
+ repeatedPatternCount: drifts.filter(d => d.type === "repeated-pattern").length,
1314
+ orphanedComponentCount: drifts.filter(d => d.type === "orphaned-component").length,
1315
+ semanticMismatchCount: drifts.filter(d => d.type === "semantic-mismatch").length,
1316
+ deprecatedPatternCount: drifts.filter(d => d.type === "deprecated-pattern").length,
1317
+ highDensityFileCount,
1318
+ vendoredDriftCount: richContext.vendoredDriftCount,
1319
+ topHardcodedColor: richContext.topHardcodedColor,
1320
+ worstFile: richContext.worstFile,
1321
+ uniqueSpacingValues: richContext.uniqueSpacingValues,
1322
+ detectedFrameworkNames: detected.map(f => f.name),
1323
+ };
1324
+ const healthResult = calculateHealthScorePillar(healthMetrics);
1325
+ spin.stop();
1326
+ // Aggregate drift signals
1327
+ const aggregationConfig = config.drift?.aggregation ?? {};
1328
+ const aggregator = new DriftAggregator({
1329
+ strategies: aggregationConfig.strategies,
1330
+ minGroupSize: aggregationConfig.minGroupSize,
1331
+ pathPatterns: aggregationConfig.pathPatterns,
1332
+ });
1333
+ const aggregated = aggregator.aggregate(driftResult.drifts);
1334
+ // Gather setup status
1335
+ const cwd = process.cwd();
1336
+ const setup = getSetupStatus(cwd);
1337
+ const output = {
1338
+ components: scanResult.components,
1339
+ tokens: scanResult.tokens,
1340
+ drift: {
1341
+ groups: aggregated.groups.map(g => ({
1342
+ id: g.id,
1343
+ strategy: g.groupingKey.strategy,
1344
+ key: g.groupingKey.value,
1345
+ summary: g.summary,
1346
+ count: g.totalCount,
1347
+ severity: g.bySeverity,
1348
+ })),
1349
+ ungrouped: aggregated.ungrouped.length,
1350
+ summary: {
1351
+ totalSignals: aggregated.totalSignals,
1352
+ totalGroups: aggregated.totalGroups,
1353
+ reductionRatio: Math.round(aggregated.reductionRatio * 10) / 10,
1354
+ bySeverity: {
1355
+ critical: driftResult.drifts.filter((d) => d.severity === "critical").length,
1356
+ warning: driftResult.drifts.filter((d) => d.severity === "warning").length,
1357
+ info: driftResult.drifts.filter((d) => d.severity === "info").length,
1358
+ },
1359
+ },
1360
+ },
1361
+ health: {
1362
+ score: healthResult.score,
1363
+ tier: healthResult.tier,
1364
+ pillars: {
1365
+ valueDiscipline: { score: healthResult.pillars.valueDiscipline.score, max: 60 },
1366
+ tokenHealth: { score: healthResult.pillars.tokenHealth.score, max: 20 },
1367
+ consistency: { score: healthResult.pillars.consistency.score, max: 10 },
1368
+ criticalIssues: { score: healthResult.pillars.criticalIssues.score, max: 10 },
1369
+ },
1370
+ suggestions: healthResult.suggestions,
1371
+ metrics: healthResult.metrics,
1372
+ },
1373
+ setup,
1374
+ };
1375
+ console.log(JSON.stringify(output, null, 2));
1376
+ }
1377
+ catch (err) {
1378
+ spin.stop();
1379
+ error(err instanceof Error ? err.message : String(err));
1380
+ process.exit(1);
1381
+ }
1382
+ });
1383
+ return cmd;
1384
+ }
1385
+ // Helper: Load or auto-build config
1386
+ async function getOrBuildConfig() {
1387
+ const existingConfigPath = getConfigPath();
1388
+ if (existingConfigPath) {
1389
+ const { config } = await loadConfig();
1390
+ return config;
1391
+ }
1392
+ const autoResult = await buildAutoConfig(process.cwd());
1393
+ return autoResult.config;
1394
+ }
1395
+ // Helper: Get setup status for all dock tools
1396
+ function getSetupStatus(cwd) {
1397
+ const hasConfig = !!getConfigPath();
1398
+ const hasSkills = existsSync(join(cwd, ".claude", "skills", "design-system"));
1399
+ const agentsDir = join(cwd, ".claude", "agents");
1400
+ const hasAgents = existsSync(agentsDir) && readdirSync(agentsDir).some(f => f.endsWith(".md"));
1401
+ // Check CLAUDE.md for Design System section
1402
+ const claudeMdPath = join(cwd, "CLAUDE.md");
1403
+ let hasContext = false;
1404
+ if (existsSync(claudeMdPath)) {
1405
+ try {
1406
+ const content = readFileSync(claudeMdPath, "utf-8");
1407
+ hasContext = /^##?\s*Design\s*System/m.test(content);
1408
+ }
1409
+ catch {
1410
+ // Ignore read errors
1411
+ }
1412
+ }
1413
+ // Check hooks
1414
+ const hookSystem = detectHookSystem(cwd);
1415
+ let gitHookInstalled = false;
1416
+ if (hookSystem === "husky") {
1417
+ const p = join(cwd, ".husky", "pre-commit");
1418
+ if (existsSync(p)) {
1419
+ try {
1420
+ gitHookInstalled = readFileSync(p, "utf-8").includes("buoy");
1421
+ }
1422
+ catch { /* skip */ }
1423
+ }
1424
+ }
1425
+ else if (hookSystem === "git") {
1426
+ const p = join(cwd, ".git", "hooks", "pre-commit");
1427
+ if (existsSync(p)) {
1428
+ try {
1429
+ gitHookInstalled = readFileSync(p, "utf-8").includes("buoy");
1430
+ }
1431
+ catch { /* skip */ }
1432
+ }
1433
+ }
1434
+ let claudeHooksEnabled = false;
1435
+ const claudeSettingsPath = join(cwd, ".claude", "settings.local.json");
1436
+ if (existsSync(claudeSettingsPath)) {
1437
+ try {
1438
+ const settings = JSON.parse(readFileSync(claudeSettingsPath, "utf-8"));
1439
+ claudeHooksEnabled = !!settings.hooks?.SessionStart?.some((h) => h.hooks?.some(hk => hk.command?.includes("buoy") || hk.command?.includes("Design system")));
1440
+ }
1441
+ catch { /* skip */ }
1442
+ }
1443
+ // Check commands
1444
+ const commandsDir = join(homedir(), ".claude", "commands");
1445
+ const hasCommands = existsSync(commandsDir) && readdirSync(commandsDir).some(f => f.endsWith(".md"));
1446
+ // Check graph
1447
+ const hasGraph = existsSync(join(cwd, ".buoy", "graph.json"));
1448
+ return {
1449
+ config: hasConfig,
1450
+ skills: hasSkills,
1451
+ agents: hasAgents,
1452
+ context: hasContext,
1453
+ hooks: { git: gitHookInstalled, claude: claudeHooksEnabled },
1454
+ commands: hasCommands,
1455
+ graph: hasGraph,
1456
+ };
1457
+ }
1458
+ /**
1459
+ * Extract rich suggestion context from drift signals for better health suggestions.
1460
+ */
1461
+ function computeRichSuggestionContext(drifts) {
1462
+ // Extract hardcoded colors from drift messages
1463
+ // Messages follow the pattern: 'Component "X" has N hardcoded colors: #fff, #000, #333'
1464
+ const colorCounts = new Map();
1465
+ for (const d of drifts) {
1466
+ if (d.type !== "hardcoded-value")
1467
+ continue;
1468
+ if (!d.message.includes("color"))
1469
+ continue;
1470
+ // Extract color values from the message after the colon
1471
+ const colonIdx = d.message.lastIndexOf(":");
1472
+ if (colonIdx === -1)
1473
+ continue;
1474
+ const valuesStr = d.message.slice(colonIdx + 1).trim();
1475
+ const colors = valuesStr.split(",").map(v => v.trim()).filter(v => v.startsWith("#") || v.startsWith("rgb"));
1476
+ for (const c of colors) {
1477
+ colorCounts.set(c, (colorCounts.get(c) || 0) + 1);
1478
+ }
1479
+ }
1480
+ const topColorEntry = [...colorCounts.entries()].sort((a, b) => b[1] - a[1])[0];
1481
+ const topHardcodedColor = topColorEntry
1482
+ ? { value: topColorEntry[0], count: topColorEntry[1] }
1483
+ : undefined;
1484
+ // Find file with most drift issues, separating vendored shadcn files
1485
+ let vendoredDriftCount = 0;
1486
+ const userFileCounts = new Map();
1487
+ for (const d of drifts) {
1488
+ const loc = d.source?.location;
1489
+ if (!loc)
1490
+ continue;
1491
+ const file = loc.split(":")[0];
1492
+ if (!file)
1493
+ continue;
1494
+ // Filter vendored files for ALL drift types (not just hardcoded-value)
1495
+ if (isVendoredShadcnFile(file)) {
1496
+ if (d.type === "hardcoded-value")
1497
+ vendoredDriftCount++;
1498
+ continue;
1499
+ }
1500
+ if (isComponentFile(file)) {
1501
+ userFileCounts.set(file, (userFileCounts.get(file) || 0) + 1);
1502
+ }
1503
+ }
1504
+ const worstFileEntry = [...userFileCounts.entries()]
1505
+ .filter(([file, count]) => !isLikelyGeneratedFile(file, count))
1506
+ .sort((a, b) => b[1] - a[1])[0];
1507
+ const worstFile = worstFileEntry
1508
+ ? { path: worstFileEntry[0], issueCount: worstFileEntry[1] }
1509
+ : undefined;
1510
+ // Count unique spacing values from hardcoded-value messages about size/spacing
1511
+ const spacingValues = new Set();
1512
+ for (const d of drifts) {
1513
+ if (d.type !== "hardcoded-value")
1514
+ continue;
1515
+ if (!d.message.includes("size value"))
1516
+ continue;
1517
+ const colonIdx = d.message.lastIndexOf(":");
1518
+ if (colonIdx === -1)
1519
+ continue;
1520
+ const valuesStr = d.message.slice(colonIdx + 1).trim();
1521
+ const values = valuesStr.split(",").map(v => v.trim()).filter(v => v.length > 0);
1522
+ for (const v of values) {
1523
+ spacingValues.add(v);
1524
+ }
1525
+ }
1526
+ const uniqueSpacingValues = spacingValues.size > 0 ? spacingValues.size : undefined;
1527
+ return { topHardcodedColor, worstFile, uniqueSpacingValues, vendoredDriftCount };
1528
+ }
1529
+ function getSummary(drifts) {
1530
+ return {
1531
+ critical: drifts.filter((d) => d.severity === "critical").length,
1532
+ warning: drifts.filter((d) => d.severity === "warning").length,
1533
+ info: drifts.filter((d) => d.severity === "info").length,
1534
+ };
1535
+ }
1536
+ /**
1537
+ * Gather all metrics needed for the 4-pillar health score.
1538
+ */
1539
+ export async function gatherHealthMetrics(config, spin, useCache) {
1540
+ const cwd = process.cwd();
1541
+ // Run drift analysis to get all signals
1542
+ spin.text = "Scanning components and tokens...";
1543
+ const { result } = await withOptionalCache(cwd, useCache, async (cache) => {
1544
+ const orchestrator = new ScanOrchestrator(config, cwd, { cache });
1545
+ const scanResult = await orchestrator.scan({
1546
+ onProgress: (msg) => { spin.text = msg; },
1547
+ });
1548
+ spin.text = "Analyzing drift...";
1549
+ const service = new DriftAnalysisService(config);
1550
+ const driftResult = await service.analyze({
1551
+ onProgress: (msg) => { spin.text = msg; },
1552
+ includeIgnored: false,
1553
+ cache,
1554
+ });
1555
+ return { scanResult, driftResult };
1556
+ });
1557
+ const { scanResult, driftResult } = result;
1558
+ const drifts = driftResult.drifts;
1559
+ // Count drift types
1560
+ const hardcodedValueCount = drifts.filter(d => d.type === "hardcoded-value").length;
1561
+ const unusedTokenCount = drifts.filter(d => d.type === "unused-token").length;
1562
+ const namingInconsistencyCount = drifts.filter(d => d.type === "naming-inconsistency").length;
1563
+ const criticalCount = drifts.filter(d => d.severity === "critical").length;
1564
+ // Detect framework context
1565
+ const monorepoInfo = await detectMonorepo(cwd);
1566
+ const detected = await detectFrameworks(cwd, monorepoInfo ?? undefined);
1567
+ const hasUtilityFramework = detected.some(f => UTILITY_FRAMEWORK_NAMES.includes(f.name))
1568
+ || detected.some(f => DS_WITH_STYLING.includes(f.name));
1569
+ const hasDesignSystemLibrary = detected.some(f => DS_LIBRARY_NAMES.includes(f.name));
1570
+ // Compute rich suggestion context
1571
+ const richContext = computeRichSuggestionContext(drifts);
1572
+ // Count files with high hardcoded value density
1573
+ const fileDriftCounts = new Map();
1574
+ for (const d of drifts) {
1575
+ if (d.type !== "hardcoded-value")
1576
+ continue;
1577
+ const loc = d.source?.location;
1578
+ if (!loc)
1579
+ continue;
1580
+ const file = loc.split(":")[0];
1581
+ if (file)
1582
+ fileDriftCounts.set(file, (fileDriftCounts.get(file) || 0) + 1);
1583
+ }
1584
+ const highDensityFileCount = [...fileDriftCounts.values()].filter(count => count > 2).length;
1585
+ const metrics = {
1586
+ componentCount: scanResult.components.length,
1587
+ tokenCount: scanResult.tokens.length,
1588
+ hardcodedValueCount,
1589
+ unusedTokenCount,
1590
+ namingInconsistencyCount,
1591
+ criticalCount,
1592
+ hasUtilityFramework,
1593
+ hasDesignSystemLibrary,
1594
+ totalDriftCount: drifts.length,
1595
+ unusedComponentCount: drifts.filter(d => d.type === "unused-component").length,
1596
+ repeatedPatternCount: drifts.filter(d => d.type === "repeated-pattern").length,
1597
+ orphanedComponentCount: drifts.filter(d => d.type === "orphaned-component").length,
1598
+ semanticMismatchCount: drifts.filter(d => d.type === "semantic-mismatch").length,
1599
+ deprecatedPatternCount: drifts.filter(d => d.type === "deprecated-pattern").length,
1600
+ highDensityFileCount,
1601
+ vendoredDriftCount: richContext.vendoredDriftCount,
1602
+ topHardcodedColor: richContext.topHardcodedColor,
1603
+ worstFile: richContext.worstFile,
1604
+ uniqueSpacingValues: richContext.uniqueSpacingValues,
1605
+ detectedFrameworkNames: detected.map(f => f.name),
1606
+ };
1607
+ // Development sanity check: totalDriftCount should equal drifts array length
1608
+ if (process.env.BUOY_DEBUG) {
1609
+ const expectedTotal = drifts.length;
1610
+ const reportedTotal = metrics.totalDriftCount ?? 0;
1611
+ if (reportedTotal !== expectedTotal) {
1612
+ console.error(`[buoy debug] Drift count mismatch: metrics.totalDriftCount=${reportedTotal} but drifts.length=${expectedTotal}`);
1613
+ }
1614
+ }
1615
+ return metrics;
1616
+ }
1617
+ function printPillarHealthReport(result) {
1618
+ newline();
1619
+ if (result.score === null) {
1620
+ console.log(` Health Score: ${chalk.dim('N/A')} (no UI surface detected)`);
1621
+ newline();
1622
+ if (result.suggestions.length > 0) {
1623
+ for (const suggestion of result.suggestions) {
1624
+ console.log(` ${chalk.dim("\u2192")} ${suggestion}`);
1625
+ }
1626
+ newline();
1627
+ }
1628
+ return;
1629
+ }
1630
+ const scoreColor = result.score >= 80 ? chalk.green :
1631
+ result.score >= 60 ? chalk.yellow :
1632
+ chalk.red;
1633
+ console.log(` Health Score: ${scoreColor.bold(`${result.score}/100`)} (${result.tier})`);
1634
+ const lastScore = getLastScore(process.cwd());
1635
+ if (lastScore !== null && result.score !== null) {
1636
+ const delta = result.score - lastScore;
1637
+ if (delta > 0) {
1638
+ console.log(` ${chalk.green(`▲ +${delta}`)} from last scan`);
1639
+ }
1640
+ else if (delta < 0) {
1641
+ console.log(` ${chalk.red(`▼ ${delta}`)} from last scan`);
1642
+ }
1643
+ else {
1644
+ console.log(` ${chalk.dim('= no change')} from last scan`);
1645
+ }
1646
+ }
1647
+ newline();
1648
+ // Pillar breakdown with progress bars
1649
+ const pillars = [
1650
+ result.pillars.valueDiscipline,
1651
+ result.pillars.tokenHealth,
1652
+ result.pillars.consistency,
1653
+ result.pillars.criticalIssues,
1654
+ ];
1655
+ for (const pillar of pillars) {
1656
+ const barWidth = 20;
1657
+ const filled = Math.round((pillar.score / pillar.maxScore) * barWidth);
1658
+ const empty = barWidth - filled;
1659
+ const bar = chalk.green("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty));
1660
+ const label = pillar.name.padEnd(20);
1661
+ const scoreStr = `${pillar.score}/${pillar.maxScore}`;
1662
+ console.log(` ${label}${bar} ${scoreStr}`);
1663
+ }
1664
+ newline();
1665
+ // Improvement suggestions
1666
+ if (result.suggestions.length > 0) {
1667
+ console.log(" Improve your score:");
1668
+ for (const suggestion of result.suggestions) {
1669
+ console.log(` ${chalk.yellow("\u2192")} ${suggestion}`);
1670
+ }
1671
+ newline();
1672
+ }
1673
+ // Show upgrade hint after health score
1674
+ const hint = formatUpgradeHint("after-health-score");
1675
+ if (hint) {
1676
+ console.log(hint);
1677
+ console.log("");
1678
+ }
1679
+ }
1680
+ function fuzzyScore(query, target) {
1681
+ const q = query.toLowerCase();
1682
+ const t = target.toLowerCase();
1683
+ if (q === t)
1684
+ return 100;
1685
+ if (t.includes(q)) {
1686
+ const bonus = q.length / t.length * 50;
1687
+ return 70 + bonus;
1688
+ }
1689
+ const queryWords = q.split(/[-_\s]+/);
1690
+ const targetWords = t.split(/[-_\s]+/);
1691
+ const matchedWords = queryWords.filter(qw => targetWords.some(tw => tw.includes(qw) || qw.includes(tw)));
1692
+ if (matchedWords.length > 0) {
1693
+ return 50 + (matchedWords.length / queryWords.length * 30);
1694
+ }
1695
+ return 0;
1696
+ }
1697
+ function walkDir(dir) {
1698
+ const files = [];
1699
+ if (!existsSync(dir))
1700
+ return files;
1701
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1702
+ const fullPath = join(dir, entry.name);
1703
+ if (entry.isDirectory()) {
1704
+ files.push(...walkDir(fullPath));
1705
+ }
1706
+ else {
1707
+ files.push(fullPath);
1708
+ }
1709
+ }
1710
+ return files;
1711
+ }
1712
+ function formatRelativeDate(date) {
1713
+ const now = new Date();
1714
+ const diff = now.getTime() - date.getTime();
1715
+ const seconds = Math.floor(diff / 1000);
1716
+ const minutes = Math.floor(seconds / 60);
1717
+ const hours = Math.floor(minutes / 60);
1718
+ const days = Math.floor(hours / 24);
1719
+ if (days > 7) {
1720
+ return date.toLocaleDateString();
1721
+ }
1722
+ else if (days > 0) {
1723
+ return `${days}d ago`;
1724
+ }
1725
+ else if (hours > 0) {
1726
+ return `${hours}h ago`;
1727
+ }
1728
+ else if (minutes > 0) {
1729
+ return `${minutes}m ago`;
1730
+ }
1731
+ else {
1732
+ return "just now";
1733
+ }
1734
+ }
1735
+ //# sourceMappingURL=show.js.map