@buoy-design/cli 0.3.2 → 0.3.4
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/build.d.ts +8 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +26 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/dock.d.ts.map +1 -1
- package/dist/commands/dock.js +181 -2
- package/dist/commands/dock.js.map +1 -1
- package/dist/commands/drift.d.ts +5 -0
- package/dist/commands/drift.d.ts.map +1 -1
- package/dist/commands/drift.js +14 -151
- package/dist/commands/drift.js.map +1 -1
- package/dist/commands/graph.d.ts.map +1 -1
- package/dist/commands/graph.js +6 -8
- package/dist/commands/graph.js.map +1 -1
- package/dist/commands/index.d.ts +5 -9
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +6 -11
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +701 -109
- package/dist/commands/show.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/show.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { writeFileSync } from "fs";
|
|
3
5
|
import { loadConfig, getConfigPath } from "../config/loader.js";
|
|
4
6
|
import { buildAutoConfig } from "../config/auto-detect.js";
|
|
5
|
-
import { spinner, error, setJsonMode, } from "../output/reporters.js";
|
|
7
|
+
import { spinner, error, info, success, header, keyValue, newline, setJsonMode, } from "../output/reporters.js";
|
|
8
|
+
import { formatDriftTable, formatDriftList, formatDriftTree, formatMarkdown, formatHtml, formatAgent, } from "../output/formatters.js";
|
|
6
9
|
import { ScanOrchestrator } from "../scan/orchestrator.js";
|
|
7
10
|
import { DriftAnalysisService } from "../services/drift-analysis.js";
|
|
8
11
|
import { withOptionalCache } from "@buoy-design/scanners";
|
|
9
12
|
import { formatUpgradeHint } from "../utils/upgrade-hints.js";
|
|
10
|
-
import { generateAuditReport, DriftAggregator } from "@buoy-design/core";
|
|
13
|
+
import { generateAuditReport, findCloseMatches, DriftAggregator } from "@buoy-design/core";
|
|
11
14
|
import { extractStyles, extractCssFileStyles } from "@buoy-design/scanners";
|
|
12
15
|
import { parseCssValues } from "@buoy-design/core";
|
|
13
16
|
import { glob } from "glob";
|
|
@@ -17,12 +20,16 @@ export function createShowCommand() {
|
|
|
17
20
|
.description("Show design system information")
|
|
18
21
|
.option("--json", "Output as JSON (default)")
|
|
19
22
|
.option("--no-cache", "Disable incremental scanning cache");
|
|
20
|
-
// show components
|
|
23
|
+
// show components [query]
|
|
21
24
|
cmd
|
|
22
25
|
.command("components")
|
|
23
|
-
.description("Show components found in the codebase")
|
|
26
|
+
.description("Show components found in the codebase (with optional search)")
|
|
27
|
+
.argument("[query]", "Search query (component name or partial match)")
|
|
24
28
|
.option("--json", "Output as JSON")
|
|
25
|
-
.
|
|
29
|
+
.option("--prop <propName>", "Search by prop name (e.g., \"onClick\")")
|
|
30
|
+
.option("--pattern <pattern>", "Search by pattern (checks name, props, variants)")
|
|
31
|
+
.option("-n, --limit <number>", "Maximum results to show", "10")
|
|
32
|
+
.action(async (query, options, command) => {
|
|
26
33
|
const parentOpts = command.parent?.opts() || {};
|
|
27
34
|
const json = options.json || parentOpts.json !== false;
|
|
28
35
|
if (json)
|
|
@@ -37,7 +44,117 @@ export function createShowCommand() {
|
|
|
37
44
|
});
|
|
38
45
|
});
|
|
39
46
|
spin.stop();
|
|
40
|
-
|
|
47
|
+
const components = scanResult.components;
|
|
48
|
+
// If no search query/options, list all components
|
|
49
|
+
if (!query && !options.prop && !options.pattern) {
|
|
50
|
+
console.log(JSON.stringify({ components }, null, 2));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Search mode
|
|
54
|
+
let results = [];
|
|
55
|
+
if (options.prop) {
|
|
56
|
+
const lowerProp = options.prop.toLowerCase();
|
|
57
|
+
for (const component of components) {
|
|
58
|
+
const props = component.props || [];
|
|
59
|
+
const matchingProp = props.find((p) => p.name.toLowerCase().includes(lowerProp));
|
|
60
|
+
if (matchingProp) {
|
|
61
|
+
results.push({
|
|
62
|
+
component,
|
|
63
|
+
score: matchingProp.name.toLowerCase() === lowerProp ? 100 : 80,
|
|
64
|
+
matchType: "prop",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (options.pattern) {
|
|
70
|
+
const lowerPattern = options.pattern.toLowerCase();
|
|
71
|
+
for (const component of components) {
|
|
72
|
+
let score = 0;
|
|
73
|
+
const nameScore = fuzzyScore(options.pattern, component.name);
|
|
74
|
+
if (nameScore > 0)
|
|
75
|
+
score += nameScore * 0.5;
|
|
76
|
+
const props = component.props || [];
|
|
77
|
+
if (props.some((p) => p.name.toLowerCase().includes(lowerPattern) ||
|
|
78
|
+
(p.type && p.type.toLowerCase().includes(lowerPattern))))
|
|
79
|
+
score += 30;
|
|
80
|
+
const variants = component.variants || [];
|
|
81
|
+
if (variants.some((v) => v.name.toLowerCase().includes(lowerPattern)))
|
|
82
|
+
score += 20;
|
|
83
|
+
if (score >= 30) {
|
|
84
|
+
results.push({ component, score: Math.min(100, score), matchType: "pattern" });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (query) {
|
|
89
|
+
for (const component of components) {
|
|
90
|
+
const score = fuzzyScore(query, component.name);
|
|
91
|
+
if (score >= 30) {
|
|
92
|
+
results.push({
|
|
93
|
+
component,
|
|
94
|
+
score,
|
|
95
|
+
matchType: score === 100 ? "exact" : "fuzzy",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
results.sort((a, b) => b.score - a.score);
|
|
101
|
+
const limit = parseInt(options.limit, 10);
|
|
102
|
+
results = results.slice(0, limit);
|
|
103
|
+
if (json) {
|
|
104
|
+
console.log(JSON.stringify({
|
|
105
|
+
query: query || null,
|
|
106
|
+
prop: options.prop || null,
|
|
107
|
+
pattern: options.pattern || null,
|
|
108
|
+
results: results.map(r => ({
|
|
109
|
+
name: r.component.name,
|
|
110
|
+
path: "path" in r.component.source ? r.component.source.path : "unknown",
|
|
111
|
+
props: r.component.props?.map((p) => ({
|
|
112
|
+
name: p.name, type: p.type, required: p.required,
|
|
113
|
+
})),
|
|
114
|
+
variants: r.component.variants?.map((v) => v.name),
|
|
115
|
+
score: Math.round(r.score),
|
|
116
|
+
matchType: r.matchType,
|
|
117
|
+
})),
|
|
118
|
+
totalComponents: components.length,
|
|
119
|
+
}, null, 2));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (results.length === 0) {
|
|
123
|
+
newline();
|
|
124
|
+
info("No matching components found");
|
|
125
|
+
if (query)
|
|
126
|
+
info("Try a different search term or use --pattern for broader search");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
newline();
|
|
130
|
+
header(`Found ${results.length} component${results.length === 1 ? "" : "s"}`);
|
|
131
|
+
newline();
|
|
132
|
+
for (const { component, score, matchType } of results) {
|
|
133
|
+
const scoreLabel = matchType === "exact" ? chalk.green("exact")
|
|
134
|
+
: matchType === "prop" ? chalk.cyan("prop match")
|
|
135
|
+
: matchType === "pattern" ? chalk.yellow("pattern")
|
|
136
|
+
: chalk.dim(`${Math.round(score)}%`);
|
|
137
|
+
console.log(` ${chalk.bold(component.name)} ${scoreLabel}`);
|
|
138
|
+
keyValue(" Path", "path" in component.source ? component.source.path : "unknown");
|
|
139
|
+
const props = component.props || [];
|
|
140
|
+
const propStr = props.length === 0 ? chalk.dim("(no props)") : props.slice(0, 5).map((p) => {
|
|
141
|
+
const req = p.required ? "*" : "";
|
|
142
|
+
const t = p.type ? chalk.dim(`: ${p.type}`) : "";
|
|
143
|
+
return `${p.name}${req}${t}`;
|
|
144
|
+
}).join(", ") + (props.length > 5 ? chalk.dim(` +${props.length - 5} more`) : "");
|
|
145
|
+
keyValue(" Props", propStr);
|
|
146
|
+
const variants = component.variants || [];
|
|
147
|
+
if (variants.length > 0) {
|
|
148
|
+
const variantNames = variants.slice(0, 4).map((v) => v.name).join(", ");
|
|
149
|
+
const more = variants.length > 4 ? chalk.dim(` +${variants.length - 4} more`) : "";
|
|
150
|
+
keyValue(" Variants", variantNames + more);
|
|
151
|
+
}
|
|
152
|
+
newline();
|
|
153
|
+
}
|
|
154
|
+
console.log(chalk.dim("─".repeat(40)));
|
|
155
|
+
info(`${components.length} total components in project`);
|
|
156
|
+
if (results.length === limit)
|
|
157
|
+
info(`Showing first ${limit} results. Use --limit to see more.`);
|
|
41
158
|
}
|
|
42
159
|
catch (err) {
|
|
43
160
|
spin.stop();
|
|
@@ -79,13 +196,19 @@ export function createShowCommand() {
|
|
|
79
196
|
.description("Show drift signals (design system violations)")
|
|
80
197
|
.option("--json", "Output as JSON")
|
|
81
198
|
.option("--raw", "Output raw signals without grouping")
|
|
199
|
+
.option("-f, --format <type>", "Output format: json, markdown, html, table, tree, agent")
|
|
82
200
|
.option("-S, --severity <level>", "Filter by minimum severity (info, warning, critical)")
|
|
83
201
|
.option("-t, --type <type>", "Filter by drift type")
|
|
202
|
+
.option("-v, --verbose", "Verbose output with full details")
|
|
203
|
+
.option("--include-baseline", "Include baselined drifts (show all)")
|
|
204
|
+
.option("--clear-cache", "Clear cache before scanning")
|
|
84
205
|
.action(async (options, command) => {
|
|
85
206
|
const parentOpts = command.parent?.opts() || {};
|
|
86
207
|
const json = options.json || parentOpts.json !== false;
|
|
87
|
-
|
|
208
|
+
const useJson = json && !options.format && !options.verbose;
|
|
209
|
+
if (useJson || options.format === "json" || options.format === "agent") {
|
|
88
210
|
setJsonMode(true);
|
|
211
|
+
}
|
|
89
212
|
const spin = spinner("Analyzing drift...");
|
|
90
213
|
try {
|
|
91
214
|
const { config } = await loadConfig();
|
|
@@ -93,14 +216,37 @@ export function createShowCommand() {
|
|
|
93
216
|
const service = new DriftAnalysisService(config);
|
|
94
217
|
return service.analyze({
|
|
95
218
|
onProgress: (msg) => { spin.text = msg; },
|
|
96
|
-
includeBaseline: false,
|
|
219
|
+
includeBaseline: options.includeBaseline ?? false,
|
|
97
220
|
minSeverity: options.severity,
|
|
98
221
|
filterType: options.type,
|
|
99
222
|
cache,
|
|
100
223
|
});
|
|
224
|
+
}, {
|
|
225
|
+
clearCache: options.clearCache,
|
|
226
|
+
onVerbose: options.verbose ? info : undefined,
|
|
101
227
|
});
|
|
228
|
+
const drifts = result.drifts;
|
|
229
|
+
const sourceComponents = result.components;
|
|
230
|
+
const baselinedCount = result.baselinedCount;
|
|
102
231
|
spin.stop();
|
|
103
|
-
//
|
|
232
|
+
// Determine output format
|
|
233
|
+
const format = options.format;
|
|
234
|
+
if (format === "agent") {
|
|
235
|
+
console.log(formatAgent(drifts));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (format === "markdown") {
|
|
239
|
+
console.log(formatMarkdown(drifts));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (format === "html") {
|
|
243
|
+
const htmlContent = formatHtml(drifts, { designerFriendly: true });
|
|
244
|
+
const filename = "drift-report.html";
|
|
245
|
+
writeFileSync(filename, htmlContent);
|
|
246
|
+
success(`HTML report saved to ${filename}`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// Raw mode: output signals without grouping
|
|
104
250
|
if (options.raw) {
|
|
105
251
|
const output = {
|
|
106
252
|
drifts: result.drifts,
|
|
@@ -114,48 +260,109 @@ export function createShowCommand() {
|
|
|
114
260
|
console.log(JSON.stringify(output, null, 2));
|
|
115
261
|
return;
|
|
116
262
|
}
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
263
|
+
// JSON output (explicit --json or --format json)
|
|
264
|
+
if (format === "json" || useJson) {
|
|
265
|
+
// Grouped mode: aggregate signals for actionability
|
|
266
|
+
const aggregationConfig = config.drift?.aggregation ?? {};
|
|
267
|
+
const aggregator = new DriftAggregator({
|
|
268
|
+
strategies: aggregationConfig.strategies,
|
|
269
|
+
minGroupSize: aggregationConfig.minGroupSize,
|
|
270
|
+
pathPatterns: aggregationConfig.pathPatterns,
|
|
271
|
+
});
|
|
272
|
+
const aggregated = aggregator.aggregate(result.drifts);
|
|
273
|
+
const output = {
|
|
274
|
+
groups: aggregated.groups.map(g => ({
|
|
275
|
+
id: g.id,
|
|
276
|
+
strategy: g.groupingKey.strategy,
|
|
277
|
+
key: g.groupingKey.value,
|
|
278
|
+
summary: g.summary,
|
|
279
|
+
count: g.totalCount,
|
|
280
|
+
severity: g.bySeverity,
|
|
281
|
+
representative: {
|
|
282
|
+
type: g.representative.type,
|
|
283
|
+
message: g.representative.message,
|
|
284
|
+
location: g.representative.source.location,
|
|
285
|
+
},
|
|
286
|
+
})),
|
|
287
|
+
ungrouped: aggregated.ungrouped.map(d => ({
|
|
288
|
+
id: d.id,
|
|
289
|
+
type: d.type,
|
|
290
|
+
severity: d.severity,
|
|
291
|
+
message: d.message,
|
|
292
|
+
location: d.source.location,
|
|
293
|
+
})),
|
|
294
|
+
summary: {
|
|
295
|
+
totalSignals: aggregated.totalSignals,
|
|
296
|
+
totalGroups: aggregated.totalGroups,
|
|
297
|
+
ungroupedCount: aggregated.ungrouped.length,
|
|
298
|
+
reductionRatio: Math.round(aggregated.reductionRatio * 10) / 10,
|
|
299
|
+
bySeverity: {
|
|
300
|
+
critical: result.drifts.filter((d) => d.severity === "critical").length,
|
|
301
|
+
warning: result.drifts.filter((d) => d.severity === "warning").length,
|
|
302
|
+
info: result.drifts.filter((d) => d.severity === "info").length,
|
|
303
|
+
},
|
|
155
304
|
},
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
305
|
+
baselinedCount,
|
|
306
|
+
};
|
|
307
|
+
console.log(JSON.stringify(output, null, 2));
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
// Human-readable output formats
|
|
311
|
+
const uniqueFiles = new Set(drifts.map(d => d.source.location?.split(':')[0] || d.source.entityName));
|
|
312
|
+
if (options.verbose) {
|
|
313
|
+
header("Drift Analysis");
|
|
314
|
+
newline();
|
|
315
|
+
const summary = getSummary(drifts);
|
|
316
|
+
keyValue("Components scanned", String(sourceComponents.length));
|
|
317
|
+
keyValue("Critical", String(summary.critical));
|
|
318
|
+
keyValue("Warning", String(summary.warning));
|
|
319
|
+
keyValue("Info", String(summary.info));
|
|
320
|
+
if (baselinedCount > 0) {
|
|
321
|
+
keyValue("Baselined (hidden)", String(baselinedCount));
|
|
322
|
+
}
|
|
323
|
+
newline();
|
|
324
|
+
console.log(formatDriftList(drifts));
|
|
325
|
+
}
|
|
326
|
+
else if (format === "table") {
|
|
327
|
+
header("Drift Analysis");
|
|
328
|
+
newline();
|
|
329
|
+
console.log(formatDriftTable(drifts));
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
// Default: tree view
|
|
333
|
+
newline();
|
|
334
|
+
console.log(formatDriftTree(drifts, uniqueFiles.size));
|
|
335
|
+
}
|
|
336
|
+
// Handle empty results
|
|
337
|
+
if (drifts.length === 0) {
|
|
338
|
+
if (sourceComponents.length === 0) {
|
|
339
|
+
newline();
|
|
340
|
+
info("No components found to analyze.");
|
|
341
|
+
info("To find hardcoded inline styles:");
|
|
342
|
+
info(" " + chalk.cyan("buoy show health") + " # See all hardcoded values");
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
const hasTokens = config.sources.tokens?.enabled &&
|
|
346
|
+
(config.sources.tokens.files?.length ?? 0) > 0;
|
|
347
|
+
const hasFigma = config.sources.figma?.enabled;
|
|
348
|
+
const hasStorybook = config.sources.storybook?.enabled;
|
|
349
|
+
const hasDesignTokensFile = existsSync('design-tokens.css') ||
|
|
350
|
+
existsSync('design-tokens.json');
|
|
351
|
+
if (!hasTokens && !hasFigma && !hasStorybook && !hasDesignTokensFile) {
|
|
352
|
+
newline();
|
|
353
|
+
info("No reference source configured.");
|
|
354
|
+
info("Run " + chalk.cyan("buoy tokens") + " to extract design tokens.");
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Show upgrade hint when drifts found
|
|
359
|
+
if (drifts.length > 0) {
|
|
360
|
+
const hint = formatUpgradeHint('after-drift-found');
|
|
361
|
+
if (hint) {
|
|
362
|
+
newline();
|
|
363
|
+
console.log(hint);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
159
366
|
}
|
|
160
367
|
catch (err) {
|
|
161
368
|
spin.stop();
|
|
@@ -168,6 +375,7 @@ export function createShowCommand() {
|
|
|
168
375
|
.command("health")
|
|
169
376
|
.description("Show design system health score")
|
|
170
377
|
.option("--json", "Output as JSON")
|
|
378
|
+
.option("--tokens <path>", "Path to design tokens file for close-match detection")
|
|
171
379
|
.action(async (options, command) => {
|
|
172
380
|
const parentOpts = command.parent?.opts() || {};
|
|
173
381
|
const json = options.json ?? parentOpts.json;
|
|
@@ -188,7 +396,7 @@ export function createShowCommand() {
|
|
|
188
396
|
}
|
|
189
397
|
else {
|
|
190
398
|
console.log('');
|
|
191
|
-
console.log(chalk.green.bold('
|
|
399
|
+
console.log(chalk.green.bold(' Health Score: 100/100 (Good)'));
|
|
192
400
|
console.log('');
|
|
193
401
|
console.log(chalk.dim(' No hardcoded design values found.'));
|
|
194
402
|
console.log(chalk.dim(' Your codebase is using design tokens correctly!'));
|
|
@@ -197,54 +405,39 @@ export function createShowCommand() {
|
|
|
197
405
|
return;
|
|
198
406
|
}
|
|
199
407
|
const report = generateAuditReport(extractedValues);
|
|
408
|
+
// Load design tokens for close-match detection if provided
|
|
409
|
+
if (options.tokens) {
|
|
410
|
+
try {
|
|
411
|
+
const tokenContent = await readFile(options.tokens, "utf-8");
|
|
412
|
+
const tokenData = JSON.parse(tokenContent);
|
|
413
|
+
const colorTokens = extractTokenValues(tokenData, "color");
|
|
414
|
+
const spacingTokens = extractTokenValues(tokenData, "spacing");
|
|
415
|
+
const colorValues = extractedValues
|
|
416
|
+
.filter((v) => v.category === "color")
|
|
417
|
+
.map((v) => v.value);
|
|
418
|
+
const spacingValues = extractedValues
|
|
419
|
+
.filter((v) => v.category === "spacing")
|
|
420
|
+
.map((v) => v.value);
|
|
421
|
+
report.closeMatches = [
|
|
422
|
+
...findCloseMatches(colorValues, colorTokens, "color"),
|
|
423
|
+
...findCloseMatches(spacingValues, spacingTokens, "spacing"),
|
|
424
|
+
];
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// Ignore token loading errors
|
|
428
|
+
}
|
|
429
|
+
}
|
|
200
430
|
if (json) {
|
|
201
431
|
console.log(JSON.stringify({
|
|
202
432
|
score: report.score,
|
|
203
433
|
categories: report.categories,
|
|
204
434
|
worstFiles: report.worstFiles,
|
|
205
435
|
totals: report.totals,
|
|
436
|
+
closeMatches: report.closeMatches,
|
|
206
437
|
}, null, 2));
|
|
207
438
|
}
|
|
208
439
|
else {
|
|
209
|
-
|
|
210
|
-
console.log('');
|
|
211
|
-
const scoreColor = report.score >= 80 ? chalk.green :
|
|
212
|
-
report.score >= 50 ? chalk.yellow :
|
|
213
|
-
chalk.red;
|
|
214
|
-
console.log(` ${chalk.bold('Health Score:')} ${scoreColor.bold(report.score + '/100')}`);
|
|
215
|
-
console.log('');
|
|
216
|
-
// Categories
|
|
217
|
-
console.log(chalk.bold(' By Category:'));
|
|
218
|
-
for (const [category, data] of Object.entries(report.categories)) {
|
|
219
|
-
const catData = data;
|
|
220
|
-
console.log(` ${category}: ${catData.uniqueCount} unique values, ${catData.totalUsages} usages`);
|
|
221
|
-
}
|
|
222
|
-
console.log('');
|
|
223
|
-
// Worst files
|
|
224
|
-
if (report.worstFiles.length > 0) {
|
|
225
|
-
console.log(chalk.bold(' Files with most hardcoded values:'));
|
|
226
|
-
for (const file of report.worstFiles.slice(0, 5)) {
|
|
227
|
-
console.log(` ${chalk.dim('•')} ${file.file}: ${file.issueCount} values`);
|
|
228
|
-
}
|
|
229
|
-
if (report.worstFiles.length > 5) {
|
|
230
|
-
console.log(chalk.dim(` ...and ${report.worstFiles.length - 5} more files`));
|
|
231
|
-
}
|
|
232
|
-
console.log('');
|
|
233
|
-
}
|
|
234
|
-
// Totals
|
|
235
|
-
console.log(chalk.dim(` Total: ${report.totals.uniqueValues} unique values across ${report.totals.filesAffected} files`));
|
|
236
|
-
console.log('');
|
|
237
|
-
// Suggestion
|
|
238
|
-
if (report.score < 80) {
|
|
239
|
-
console.log(chalk.dim(' Run `buoy tokens` to generate tokens from these values'));
|
|
240
|
-
console.log('');
|
|
241
|
-
}
|
|
242
|
-
// Show upgrade hint after health score
|
|
243
|
-
const hint = formatUpgradeHint('after-health-score');
|
|
244
|
-
if (hint) {
|
|
245
|
-
console.log(hint);
|
|
246
|
-
console.log('');
|
|
247
|
-
}
|
|
440
|
+
printHealthReport(report);
|
|
248
441
|
}
|
|
249
442
|
}
|
|
250
443
|
catch (err) {
|
|
@@ -254,12 +447,14 @@ export function createShowCommand() {
|
|
|
254
447
|
}
|
|
255
448
|
});
|
|
256
449
|
// show history
|
|
257
|
-
cmd
|
|
450
|
+
const historyCmd = cmd
|
|
258
451
|
.command("history")
|
|
259
|
-
.description("Show scan history")
|
|
452
|
+
.description("Show scan history and trends")
|
|
453
|
+
.argument("[scan-id]", "Show details of a specific scan")
|
|
260
454
|
.option("--json", "Output as JSON")
|
|
261
455
|
.option("-n, --limit <number>", "Number of entries to show", "10")
|
|
262
|
-
.
|
|
456
|
+
.option("-v, --verbose", "Show detailed information")
|
|
457
|
+
.action(async (scanId, options, command) => {
|
|
263
458
|
const parentOpts = command.parent?.opts() || {};
|
|
264
459
|
const json = options.json || parentOpts.json !== false;
|
|
265
460
|
if (json)
|
|
@@ -267,30 +462,283 @@ export function createShowCommand() {
|
|
|
267
462
|
try {
|
|
268
463
|
// Import store dynamically to avoid circular deps
|
|
269
464
|
const { createStore, getProjectName } = await import("../store/index.js");
|
|
465
|
+
// If a scan ID was provided, show details for that scan
|
|
466
|
+
if (scanId) {
|
|
467
|
+
const spin = spinner("Loading scan details...");
|
|
468
|
+
const store = createStore({ forceLocal: true });
|
|
469
|
+
const scan = await store.getScan(scanId);
|
|
470
|
+
if (!scan) {
|
|
471
|
+
spin.stop();
|
|
472
|
+
error(`Scan not found: ${scanId}`);
|
|
473
|
+
store.close();
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
const components = await store.getComponents(scanId);
|
|
477
|
+
const tokens = await store.getTokens(scanId);
|
|
478
|
+
const drifts = await store.getDriftSignals(scanId);
|
|
479
|
+
spin.stop();
|
|
480
|
+
if (json) {
|
|
481
|
+
console.log(JSON.stringify({
|
|
482
|
+
scan: {
|
|
483
|
+
id: scan.id,
|
|
484
|
+
status: scan.status,
|
|
485
|
+
sources: scan.sources,
|
|
486
|
+
stats: scan.stats,
|
|
487
|
+
startedAt: scan.startedAt,
|
|
488
|
+
completedAt: scan.completedAt,
|
|
489
|
+
},
|
|
490
|
+
components,
|
|
491
|
+
tokens,
|
|
492
|
+
drifts,
|
|
493
|
+
}, null, 2));
|
|
494
|
+
store.close();
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
header(`Scan: ${scan.id}`);
|
|
498
|
+
newline();
|
|
499
|
+
keyValue("Status", scan.status);
|
|
500
|
+
keyValue("Sources", scan.sources.join(", "));
|
|
501
|
+
if (scan.startedAt) {
|
|
502
|
+
keyValue("Started", scan.startedAt.toLocaleString());
|
|
503
|
+
}
|
|
504
|
+
if (scan.completedAt) {
|
|
505
|
+
keyValue("Completed", scan.completedAt.toLocaleString());
|
|
506
|
+
}
|
|
507
|
+
if (scan.stats?.duration) {
|
|
508
|
+
keyValue("Duration", `${(scan.stats.duration / 1000).toFixed(1)}s`);
|
|
509
|
+
}
|
|
510
|
+
newline();
|
|
511
|
+
keyValue("Components", String(components.length));
|
|
512
|
+
keyValue("Tokens", String(tokens.length));
|
|
513
|
+
keyValue("Drift signals", String(drifts.length));
|
|
514
|
+
if (components.length > 0) {
|
|
515
|
+
const componentsWithDrift = new Set(drifts.map(d => d.source?.location).filter(Boolean)).size;
|
|
516
|
+
const coveragePercent = Math.round(((components.length - componentsWithDrift) / components.length) * 100);
|
|
517
|
+
const coverageColor = coveragePercent >= 80 ? chalk.green : coveragePercent >= 50 ? chalk.yellow : chalk.red;
|
|
518
|
+
keyValue("Coverage", coverageColor(`${coveragePercent}%`));
|
|
519
|
+
}
|
|
520
|
+
newline();
|
|
521
|
+
if (drifts.length > 0) {
|
|
522
|
+
const critical = drifts.filter((d) => d.severity === "critical").length;
|
|
523
|
+
const warning = drifts.filter((d) => d.severity === "warning").length;
|
|
524
|
+
const infoCount = drifts.filter((d) => d.severity === "info").length;
|
|
525
|
+
console.log(chalk.bold("Drift Breakdown"));
|
|
526
|
+
console.log(` ${chalk.red("Critical:")} ${critical} ` +
|
|
527
|
+
`${chalk.yellow("Warning:")} ${warning} ` +
|
|
528
|
+
`${chalk.blue("Info:")} ${infoCount}`);
|
|
529
|
+
}
|
|
530
|
+
store.close();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
// Default: list all scans
|
|
534
|
+
const spin = spinner("Loading scan history...");
|
|
270
535
|
const store = createStore({ forceLocal: true });
|
|
271
536
|
const projectName = getProjectName();
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
537
|
+
try {
|
|
538
|
+
const project = await store.getOrCreateProject(projectName);
|
|
539
|
+
const limit = parseInt(options.limit, 10) || 10;
|
|
540
|
+
const scans = await store.getScans(project.id, limit);
|
|
541
|
+
const snapshots = await store.getSnapshots(project.id, limit);
|
|
542
|
+
spin.stop();
|
|
543
|
+
if (json) {
|
|
544
|
+
console.log(JSON.stringify({
|
|
545
|
+
project: {
|
|
546
|
+
id: project.id,
|
|
547
|
+
name: project.name,
|
|
548
|
+
},
|
|
549
|
+
scans: scans.map((s) => ({
|
|
550
|
+
id: s.id,
|
|
551
|
+
status: s.status,
|
|
552
|
+
sources: s.sources,
|
|
553
|
+
stats: s.stats,
|
|
554
|
+
startedAt: s.startedAt,
|
|
555
|
+
completedAt: s.completedAt,
|
|
556
|
+
})),
|
|
557
|
+
snapshots: snapshots.map((s) => ({
|
|
558
|
+
scanId: s.scanId,
|
|
559
|
+
componentCount: s.componentCount,
|
|
560
|
+
tokenCount: s.tokenCount,
|
|
561
|
+
driftCount: s.driftCount,
|
|
562
|
+
summary: s.summary,
|
|
563
|
+
createdAt: s.createdAt,
|
|
564
|
+
})),
|
|
565
|
+
}, null, 2));
|
|
566
|
+
store.close();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
header("Scan History");
|
|
570
|
+
newline();
|
|
571
|
+
keyValue("Project", project.name);
|
|
572
|
+
keyValue("Total scans", String(scans.length));
|
|
573
|
+
newline();
|
|
574
|
+
if (scans.length === 0) {
|
|
575
|
+
info("No scans recorded yet. Run " + chalk.cyan("buoy show all") + " to start tracking.");
|
|
576
|
+
store.close();
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
// Display scans in a table format
|
|
580
|
+
console.log(chalk.dim("ID".padEnd(15)) +
|
|
581
|
+
chalk.dim("Status".padEnd(12)) +
|
|
582
|
+
chalk.dim("Components".padEnd(12)) +
|
|
583
|
+
chalk.dim("Drift".padEnd(8)) +
|
|
584
|
+
chalk.dim("Coverage".padEnd(10)) +
|
|
585
|
+
chalk.dim("Date"));
|
|
586
|
+
console.log(chalk.dim("─".repeat(70)));
|
|
587
|
+
for (const scan of scans) {
|
|
588
|
+
const snapshot = snapshots.find((s) => s.scanId === scan.id);
|
|
589
|
+
const statusColor = scan.status === "completed"
|
|
590
|
+
? chalk.green
|
|
591
|
+
: scan.status === "failed"
|
|
592
|
+
? chalk.red
|
|
593
|
+
: chalk.yellow;
|
|
594
|
+
const date = scan.completedAt || scan.startedAt || scan.createdAt;
|
|
595
|
+
const dateStr = date ? formatRelativeDate(date) : "—";
|
|
596
|
+
const compCount = snapshot?.componentCount ?? scan.stats?.componentCount ?? "—";
|
|
597
|
+
const driftCount = snapshot?.driftCount ?? scan.stats?.driftCount ?? "—";
|
|
598
|
+
const coverage = snapshot?.coverageScore != null
|
|
599
|
+
? `${snapshot.coverageScore}%`
|
|
600
|
+
: "—";
|
|
601
|
+
console.log(chalk.cyan(scan.id.padEnd(15)) +
|
|
602
|
+
statusColor(scan.status.padEnd(12)) +
|
|
603
|
+
String(compCount).padEnd(12) +
|
|
604
|
+
String(driftCount).padEnd(8) +
|
|
605
|
+
coverage.padEnd(10) +
|
|
606
|
+
chalk.dim(dateStr));
|
|
607
|
+
if (options.verbose && snapshot) {
|
|
608
|
+
console.log(chalk.dim(" ") +
|
|
609
|
+
`Critical: ${snapshot.summary.critical}, ` +
|
|
610
|
+
`Warning: ${snapshot.summary.warning}, ` +
|
|
611
|
+
`Info: ${snapshot.summary.info}`);
|
|
612
|
+
if (snapshot.summary.frameworks?.length > 0) {
|
|
613
|
+
console.log(chalk.dim(" Frameworks: ") +
|
|
614
|
+
snapshot.summary.frameworks.join(", "));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
newline();
|
|
619
|
+
// Show trend summary
|
|
620
|
+
if (snapshots.length >= 2) {
|
|
621
|
+
const latest = snapshots[0];
|
|
622
|
+
const previous = snapshots[1];
|
|
623
|
+
const driftDelta = latest.driftCount - previous.driftCount;
|
|
624
|
+
const compDelta = latest.componentCount - previous.componentCount;
|
|
625
|
+
console.log(chalk.bold("Trend Summary"));
|
|
626
|
+
console.log(chalk.dim("─".repeat(30)));
|
|
627
|
+
if (driftDelta > 0) {
|
|
628
|
+
console.log(`Drift: ${chalk.red("+" + driftDelta)} since last scan`);
|
|
629
|
+
}
|
|
630
|
+
else if (driftDelta < 0) {
|
|
631
|
+
console.log(`Drift: ${chalk.green(driftDelta)} since last scan`);
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
console.log(`Drift: ${chalk.dim("no change")}`);
|
|
635
|
+
}
|
|
636
|
+
if (compDelta > 0) {
|
|
637
|
+
console.log(`Components: ${chalk.green("+" + compDelta)} since last scan`);
|
|
638
|
+
}
|
|
639
|
+
else if (compDelta < 0) {
|
|
640
|
+
console.log(`Components: ${chalk.red(compDelta)} since last scan`);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
console.log(`Components: ${chalk.dim("no change")}`);
|
|
644
|
+
}
|
|
645
|
+
if (latest.coverageScore != null && previous.coverageScore != null) {
|
|
646
|
+
const covDelta = latest.coverageScore - previous.coverageScore;
|
|
647
|
+
if (covDelta > 0) {
|
|
648
|
+
console.log(`Coverage: ${chalk.green("+" + covDelta + "%")} since last scan`);
|
|
649
|
+
}
|
|
650
|
+
else if (covDelta < 0) {
|
|
651
|
+
console.log(`Coverage: ${chalk.red(covDelta + "%")} since last scan`);
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
console.log(`Coverage: ${chalk.dim("no change")}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
store.close();
|
|
659
|
+
}
|
|
660
|
+
catch (storeErr) {
|
|
661
|
+
spin.stop();
|
|
662
|
+
store.close();
|
|
663
|
+
const msg = storeErr instanceof Error ? storeErr.message : String(storeErr);
|
|
664
|
+
error(`Failed to load history: ${msg}`);
|
|
665
|
+
info("Run " + chalk.cyan("buoy show all") + " first to start tracking history.");
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
288
668
|
}
|
|
289
669
|
catch (err) {
|
|
290
670
|
error(err instanceof Error ? err.message : String(err));
|
|
291
671
|
process.exit(1);
|
|
292
672
|
}
|
|
293
673
|
});
|
|
674
|
+
// show history compare <scan1> <scan2>
|
|
675
|
+
historyCmd
|
|
676
|
+
.command("compare <scan1> <scan2>")
|
|
677
|
+
.description("Compare two scans")
|
|
678
|
+
.option("--json", "Output as JSON")
|
|
679
|
+
.action(async (scan1, scan2, options) => {
|
|
680
|
+
if (options.json) {
|
|
681
|
+
setJsonMode(true);
|
|
682
|
+
}
|
|
683
|
+
const spin = spinner("Comparing scans...");
|
|
684
|
+
try {
|
|
685
|
+
const { createStore } = await import("../store/index.js");
|
|
686
|
+
const store = createStore({ forceLocal: true });
|
|
687
|
+
const diff = await store.compareScan(scan1, scan2);
|
|
688
|
+
spin.stop();
|
|
689
|
+
if (options.json) {
|
|
690
|
+
console.log(JSON.stringify(diff, null, 2));
|
|
691
|
+
store.close();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
header(`Comparing ${scan1} → ${scan2}`);
|
|
695
|
+
newline();
|
|
696
|
+
console.log(chalk.bold("Components"));
|
|
697
|
+
console.log(` ${chalk.green("Added:")} ${diff.added.components.length} ` +
|
|
698
|
+
`${chalk.red("Removed:")} ${diff.removed.components.length} ` +
|
|
699
|
+
`${chalk.yellow("Modified:")} ${diff.modified.components.length}`);
|
|
700
|
+
if (diff.added.components.length > 0) {
|
|
701
|
+
console.log(chalk.green(" + ") + diff.added.components.map((c) => c.name).join(", "));
|
|
702
|
+
}
|
|
703
|
+
if (diff.removed.components.length > 0) {
|
|
704
|
+
console.log(chalk.red(" - ") + diff.removed.components.map((c) => c.name).join(", "));
|
|
705
|
+
}
|
|
706
|
+
newline();
|
|
707
|
+
console.log(chalk.bold("Tokens"));
|
|
708
|
+
console.log(` ${chalk.green("Added:")} ${diff.added.tokens.length} ` +
|
|
709
|
+
`${chalk.red("Removed:")} ${diff.removed.tokens.length} ` +
|
|
710
|
+
`${chalk.yellow("Modified:")} ${diff.modified.tokens.length}`);
|
|
711
|
+
newline();
|
|
712
|
+
console.log(chalk.bold("Drift Signals"));
|
|
713
|
+
console.log(` ${chalk.green("New:")} ${diff.added.drifts.length} ` +
|
|
714
|
+
`${chalk.red("Resolved:")} ${diff.removed.drifts.length}`);
|
|
715
|
+
if (diff.added.drifts.length > 0) {
|
|
716
|
+
newline();
|
|
717
|
+
console.log(chalk.yellow("New drift signals:"));
|
|
718
|
+
for (const d of diff.added.drifts.slice(0, 5)) {
|
|
719
|
+
console.log(` ${d.severity === "critical" ? chalk.red("!") : chalk.yellow("~")} ${d.message}`);
|
|
720
|
+
}
|
|
721
|
+
if (diff.added.drifts.length > 5) {
|
|
722
|
+
console.log(chalk.dim(` ... and ${diff.added.drifts.length - 5} more`));
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
newline();
|
|
726
|
+
console.log(chalk.bold("Summary"));
|
|
727
|
+
if (diff.removed.drifts.length > 0) {
|
|
728
|
+
console.log(` ${chalk.green("\u2193")} ${diff.removed.drifts.length} issues resolved`);
|
|
729
|
+
}
|
|
730
|
+
if (diff.added.drifts.length > 0) {
|
|
731
|
+
console.log(` ${chalk.red("\u2191")} ${diff.added.drifts.length} new issues introduced`);
|
|
732
|
+
}
|
|
733
|
+
store.close();
|
|
734
|
+
}
|
|
735
|
+
catch (err) {
|
|
736
|
+
spin.stop();
|
|
737
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
738
|
+
error(`Compare failed: ${message}`);
|
|
739
|
+
process.exit(1);
|
|
740
|
+
}
|
|
741
|
+
});
|
|
294
742
|
// show all
|
|
295
743
|
cmd
|
|
296
744
|
.command("all")
|
|
@@ -451,4 +899,148 @@ function mapCategory(property) {
|
|
|
451
899
|
return "spacing";
|
|
452
900
|
return "spacing";
|
|
453
901
|
}
|
|
902
|
+
function getSummary(drifts) {
|
|
903
|
+
return {
|
|
904
|
+
critical: drifts.filter((d) => d.severity === "critical").length,
|
|
905
|
+
warning: drifts.filter((d) => d.severity === "warning").length,
|
|
906
|
+
info: drifts.filter((d) => d.severity === "info").length,
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
function printHealthReport(report) {
|
|
910
|
+
newline();
|
|
911
|
+
header("Design System Health Report");
|
|
912
|
+
newline();
|
|
913
|
+
const scoreColor = report.score >= 80 ? chalk.green :
|
|
914
|
+
report.score >= 50 ? chalk.yellow :
|
|
915
|
+
chalk.red;
|
|
916
|
+
const scoreLabel = report.score >= 80 ? "Good" :
|
|
917
|
+
report.score >= 50 ? "Fair" :
|
|
918
|
+
report.score < 30 ? "Poor" : "Needs Work";
|
|
919
|
+
console.log(`Overall Score: ${scoreColor.bold(`${report.score}/100`)} (${scoreLabel})`);
|
|
920
|
+
newline();
|
|
921
|
+
// Category breakdown
|
|
922
|
+
for (const [category, stats] of Object.entries(report.categories)) {
|
|
923
|
+
const expected = getExpectedCount(category);
|
|
924
|
+
const drift = stats.uniqueCount - expected;
|
|
925
|
+
const driftColor = drift > 5 ? chalk.red : drift > 0 ? chalk.yellow : chalk.green;
|
|
926
|
+
console.log(chalk.bold(capitalize(category)));
|
|
927
|
+
keyValue(" Found", `${stats.uniqueCount} unique values`);
|
|
928
|
+
keyValue(" Expected", `~${expected}`);
|
|
929
|
+
if (drift > 0) {
|
|
930
|
+
keyValue(" Drift", driftColor(`+${drift} extra values`));
|
|
931
|
+
}
|
|
932
|
+
if (stats.mostCommon.length > 0) {
|
|
933
|
+
console.log(chalk.dim(" Most common:"));
|
|
934
|
+
for (const { value, count } of stats.mostCommon.slice(0, 3)) {
|
|
935
|
+
console.log(chalk.dim(` ${value} (${count} usages)`));
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
newline();
|
|
939
|
+
}
|
|
940
|
+
// Close matches (typos)
|
|
941
|
+
if (report.closeMatches.length > 0) {
|
|
942
|
+
console.log(chalk.bold.yellow("Possible Typos"));
|
|
943
|
+
for (const match of report.closeMatches.slice(0, 5)) {
|
|
944
|
+
console.log(` ${chalk.yellow("⚠")} ${match.value} → close to ${chalk.cyan(match.closeTo)}`);
|
|
945
|
+
}
|
|
946
|
+
if (report.closeMatches.length > 5) {
|
|
947
|
+
console.log(chalk.dim(` ... and ${report.closeMatches.length - 5} more`));
|
|
948
|
+
}
|
|
949
|
+
newline();
|
|
950
|
+
}
|
|
951
|
+
// Worst files
|
|
952
|
+
if (report.worstFiles.length > 0) {
|
|
953
|
+
console.log(chalk.bold("Worst Offenders"));
|
|
954
|
+
for (const { file, issueCount } of report.worstFiles.slice(0, 5)) {
|
|
955
|
+
console.log(` ${chalk.red(issueCount.toString().padStart(3))} issues ${file}`);
|
|
956
|
+
}
|
|
957
|
+
newline();
|
|
958
|
+
}
|
|
959
|
+
// Summary
|
|
960
|
+
console.log(chalk.dim("─".repeat(50)));
|
|
961
|
+
keyValue("Total unique values", String(report.totals.uniqueValues));
|
|
962
|
+
keyValue("Total usages", String(report.totals.totalUsages));
|
|
963
|
+
keyValue("Files affected", String(report.totals.filesAffected));
|
|
964
|
+
newline();
|
|
965
|
+
if (report.score < 50) {
|
|
966
|
+
console.log(chalk.yellow("Run `buoy show drift` for detailed fixes."));
|
|
967
|
+
}
|
|
968
|
+
// Show upgrade hint after health score
|
|
969
|
+
const hint = formatUpgradeHint("after-health-score");
|
|
970
|
+
if (hint) {
|
|
971
|
+
console.log(hint);
|
|
972
|
+
console.log("");
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
function extractTokenValues(tokenData, _category) {
|
|
976
|
+
const values = [];
|
|
977
|
+
function traverse(obj) {
|
|
978
|
+
if (typeof obj !== "object" || obj === null)
|
|
979
|
+
return;
|
|
980
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
981
|
+
if (key === "$value" || key === "value") {
|
|
982
|
+
if (typeof value === "string") {
|
|
983
|
+
values.push(value);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
else if (typeof value === "object") {
|
|
987
|
+
traverse(value);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
traverse(tokenData);
|
|
992
|
+
return values;
|
|
993
|
+
}
|
|
994
|
+
function getExpectedCount(category) {
|
|
995
|
+
const expected = {
|
|
996
|
+
color: 12,
|
|
997
|
+
spacing: 8,
|
|
998
|
+
typography: 6,
|
|
999
|
+
radius: 4,
|
|
1000
|
+
};
|
|
1001
|
+
return expected[category] || 10;
|
|
1002
|
+
}
|
|
1003
|
+
function capitalize(str) {
|
|
1004
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
1005
|
+
}
|
|
1006
|
+
function fuzzyScore(query, target) {
|
|
1007
|
+
const q = query.toLowerCase();
|
|
1008
|
+
const t = target.toLowerCase();
|
|
1009
|
+
if (q === t)
|
|
1010
|
+
return 100;
|
|
1011
|
+
if (t.includes(q)) {
|
|
1012
|
+
const bonus = q.length / t.length * 50;
|
|
1013
|
+
return 70 + bonus;
|
|
1014
|
+
}
|
|
1015
|
+
const queryWords = q.split(/[-_\s]+/);
|
|
1016
|
+
const targetWords = t.split(/[-_\s]+/);
|
|
1017
|
+
const matchedWords = queryWords.filter(qw => targetWords.some(tw => tw.includes(qw) || qw.includes(tw)));
|
|
1018
|
+
if (matchedWords.length > 0) {
|
|
1019
|
+
return 50 + (matchedWords.length / queryWords.length * 30);
|
|
1020
|
+
}
|
|
1021
|
+
return 0;
|
|
1022
|
+
}
|
|
1023
|
+
function formatRelativeDate(date) {
|
|
1024
|
+
const now = new Date();
|
|
1025
|
+
const diff = now.getTime() - date.getTime();
|
|
1026
|
+
const seconds = Math.floor(diff / 1000);
|
|
1027
|
+
const minutes = Math.floor(seconds / 60);
|
|
1028
|
+
const hours = Math.floor(minutes / 60);
|
|
1029
|
+
const days = Math.floor(hours / 24);
|
|
1030
|
+
if (days > 7) {
|
|
1031
|
+
return date.toLocaleDateString();
|
|
1032
|
+
}
|
|
1033
|
+
else if (days > 0) {
|
|
1034
|
+
return `${days}d ago`;
|
|
1035
|
+
}
|
|
1036
|
+
else if (hours > 0) {
|
|
1037
|
+
return `${hours}h ago`;
|
|
1038
|
+
}
|
|
1039
|
+
else if (minutes > 0) {
|
|
1040
|
+
return `${minutes}m ago`;
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
return "just now";
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
454
1046
|
//# sourceMappingURL=show.js.map
|