@buoy-design/cli 0.3.32 → 0.3.34
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/dist/commands/check.d.ts.sync-conflict-20260305-170128-6PCZ3ZU.map +1 -0
- package/dist/commands/check.sync-conflict-20260305-155016-6PCZ3ZU.d.ts +26 -0
- package/dist/commands/check.sync-conflict-20260305-155016-6PCZ3ZU.d.ts.map +1 -0
- package/dist/commands/check.sync-conflict-20260305-155016-6PCZ3ZU.js +438 -0
- package/dist/commands/check.sync-conflict-20260305-155016-6PCZ3ZU.js.map +1 -0
- package/dist/commands/dock.sync-conflict-20260309-191923-6PCZ3ZU.js +1006 -0
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.d.ts.sync-conflict-20260306-165917-6PCZ3ZU.map +1 -0
- package/dist/commands/show.js +6 -0
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/show.sync-conflict-20260305-140755-6PCZ3ZU.js +1735 -0
- package/dist/commands/show.sync-conflict-20260309-130326-6PCZ3ZU.d.ts +11 -0
- package/dist/commands/show.sync-conflict-20260309-130326-6PCZ3ZU.d.ts.map +1 -0
- package/dist/commands/show.sync-conflict-20260309-130326-6PCZ3ZU.js +1735 -0
- package/dist/commands/show.sync-conflict-20260309-130326-6PCZ3ZU.js.map +1 -0
- package/dist/config/loader.js +1 -1
- package/dist/config/loader.js.map +1 -1
- package/dist/config/loader.js.sync-conflict-20260309-033512-6PCZ3ZU.map +1 -0
- package/dist/config/loader.sync-conflict-20260307-175208-6PCZ3ZU.d.ts +8 -0
- package/dist/config/loader.sync-conflict-20260307-175208-6PCZ3ZU.d.ts.map +1 -0
- package/dist/config/loader.sync-conflict-20260307-175208-6PCZ3ZU.js +162 -0
- package/dist/config/loader.sync-conflict-20260307-175208-6PCZ3ZU.js.map +1 -0
- package/dist/config/schema.d.ts.sync-conflict-20260309-154654-6PCZ3ZU.map +1 -0
- package/dist/config/schema.sync-conflict-20260309-135703-6PCZ3ZU.js +214 -0
- package/dist/detect/frameworks.js.sync-conflict-20260306-123756-6PCZ3ZU.map +1 -0
- package/dist/detect/monorepo-patterns.js.sync-conflict-20260309-155400-6PCZ3ZU.map +1 -0
- package/dist/hooks/index.d.ts.sync-conflict-20260306-220901-6PCZ3ZU.map +1 -0
- package/dist/output/formatters.js.sync-conflict-20260306-134702-6PCZ3ZU.map +1 -0
- package/dist/output/formatters.sync-conflict-20260306-180804-6PCZ3ZU.js +867 -0
- package/dist/output/formatters.sync-conflict-20260307-131418-5SN2GZG.d.ts +29 -0
- package/dist/output/formatters.sync-conflict-20260307-131418-5SN2GZG.d.ts.map +1 -0
- package/dist/output/formatters.sync-conflict-20260307-131418-5SN2GZG.js +867 -0
- package/dist/output/formatters.sync-conflict-20260307-131418-5SN2GZG.js.map +1 -0
- package/dist/output/formatters.sync-conflict-20260307-143122-5SN2GZG.d.ts +29 -0
- package/dist/output/formatters.sync-conflict-20260307-143122-5SN2GZG.d.ts.map +1 -0
- package/dist/output/formatters.sync-conflict-20260307-143122-5SN2GZG.js +867 -0
- package/dist/output/formatters.sync-conflict-20260307-143122-5SN2GZG.js.map +1 -0
- package/dist/output/index.sync-conflict-20260309-222859-6PCZ3ZU.js +5 -0
- package/dist/output/reporters.d.sync-conflict-20260309-193820-6PCZ3ZU.ts +38 -0
- package/dist/output/reporters.d.ts.sync-conflict-20260306-193811-6PCZ3ZU.map +1 -0
- package/dist/output/reporters.sync-conflict-20260309-030558-6PCZ3ZU.js +182 -0
- package/dist/output/reports.d.ts.sync-conflict-20260307-172149-6PCZ3ZU.map +1 -0
- package/dist/output/reports.js.sync-conflict-20260305-161643-6PCZ3ZU.map +1 -0
- package/dist/output/reports.sync-conflict-20260305-211951-6PCZ3ZU.js +393 -0
- package/dist/output/visuals.d.ts +53 -0
- package/dist/output/visuals.d.ts.map +1 -0
- package/dist/output/visuals.js +194 -0
- package/dist/output/visuals.js.map +1 -0
- package/dist/services/drift-analysis.d.sync-conflict-20260306-151016-6PCZ3ZU.ts +194 -0
- package/dist/services/drift-analysis.d.ts.sync-conflict-20260307-175904-6PCZ3ZU.map +1 -0
- package/dist/services/drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.d.ts +194 -0
- package/dist/services/drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.d.ts.map +1 -0
- package/dist/services/drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.js +1022 -0
- package/dist/services/drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.js.map +1 -0
- package/dist/services/skill-export.d.ts.sync-conflict-20260309-171021-6PCZ3ZU.map +1 -0
- package/dist/services/skill-export.sync-conflict-20260309-144621-6PCZ3ZU.d.ts +109 -0
- package/dist/services/skill-export.sync-conflict-20260309-144621-6PCZ3ZU.d.ts.map +1 -0
- package/dist/services/skill-export.sync-conflict-20260309-144621-6PCZ3ZU.js +737 -0
- package/dist/services/skill-export.sync-conflict-20260309-144621-6PCZ3ZU.js.map +1 -0
- package/package.json +14 -14
- package/LICENSE +0 -21
|
@@ -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.sync-conflict-20260309-130326-6PCZ3ZU.js.map
|