@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.
Files changed (61) hide show
  1. package/dist/commands/check.d.ts.sync-conflict-20260305-170128-6PCZ3ZU.map +1 -0
  2. package/dist/commands/check.sync-conflict-20260305-155016-6PCZ3ZU.d.ts +26 -0
  3. package/dist/commands/check.sync-conflict-20260305-155016-6PCZ3ZU.d.ts.map +1 -0
  4. package/dist/commands/check.sync-conflict-20260305-155016-6PCZ3ZU.js +438 -0
  5. package/dist/commands/check.sync-conflict-20260305-155016-6PCZ3ZU.js.map +1 -0
  6. package/dist/commands/dock.sync-conflict-20260309-191923-6PCZ3ZU.js +1006 -0
  7. package/dist/commands/show.d.ts.map +1 -1
  8. package/dist/commands/show.d.ts.sync-conflict-20260306-165917-6PCZ3ZU.map +1 -0
  9. package/dist/commands/show.js +6 -0
  10. package/dist/commands/show.js.map +1 -1
  11. package/dist/commands/show.sync-conflict-20260305-140755-6PCZ3ZU.js +1735 -0
  12. package/dist/commands/show.sync-conflict-20260309-130326-6PCZ3ZU.d.ts +11 -0
  13. package/dist/commands/show.sync-conflict-20260309-130326-6PCZ3ZU.d.ts.map +1 -0
  14. package/dist/commands/show.sync-conflict-20260309-130326-6PCZ3ZU.js +1735 -0
  15. package/dist/commands/show.sync-conflict-20260309-130326-6PCZ3ZU.js.map +1 -0
  16. package/dist/config/loader.js +1 -1
  17. package/dist/config/loader.js.map +1 -1
  18. package/dist/config/loader.js.sync-conflict-20260309-033512-6PCZ3ZU.map +1 -0
  19. package/dist/config/loader.sync-conflict-20260307-175208-6PCZ3ZU.d.ts +8 -0
  20. package/dist/config/loader.sync-conflict-20260307-175208-6PCZ3ZU.d.ts.map +1 -0
  21. package/dist/config/loader.sync-conflict-20260307-175208-6PCZ3ZU.js +162 -0
  22. package/dist/config/loader.sync-conflict-20260307-175208-6PCZ3ZU.js.map +1 -0
  23. package/dist/config/schema.d.ts.sync-conflict-20260309-154654-6PCZ3ZU.map +1 -0
  24. package/dist/config/schema.sync-conflict-20260309-135703-6PCZ3ZU.js +214 -0
  25. package/dist/detect/frameworks.js.sync-conflict-20260306-123756-6PCZ3ZU.map +1 -0
  26. package/dist/detect/monorepo-patterns.js.sync-conflict-20260309-155400-6PCZ3ZU.map +1 -0
  27. package/dist/hooks/index.d.ts.sync-conflict-20260306-220901-6PCZ3ZU.map +1 -0
  28. package/dist/output/formatters.js.sync-conflict-20260306-134702-6PCZ3ZU.map +1 -0
  29. package/dist/output/formatters.sync-conflict-20260306-180804-6PCZ3ZU.js +867 -0
  30. package/dist/output/formatters.sync-conflict-20260307-131418-5SN2GZG.d.ts +29 -0
  31. package/dist/output/formatters.sync-conflict-20260307-131418-5SN2GZG.d.ts.map +1 -0
  32. package/dist/output/formatters.sync-conflict-20260307-131418-5SN2GZG.js +867 -0
  33. package/dist/output/formatters.sync-conflict-20260307-131418-5SN2GZG.js.map +1 -0
  34. package/dist/output/formatters.sync-conflict-20260307-143122-5SN2GZG.d.ts +29 -0
  35. package/dist/output/formatters.sync-conflict-20260307-143122-5SN2GZG.d.ts.map +1 -0
  36. package/dist/output/formatters.sync-conflict-20260307-143122-5SN2GZG.js +867 -0
  37. package/dist/output/formatters.sync-conflict-20260307-143122-5SN2GZG.js.map +1 -0
  38. package/dist/output/index.sync-conflict-20260309-222859-6PCZ3ZU.js +5 -0
  39. package/dist/output/reporters.d.sync-conflict-20260309-193820-6PCZ3ZU.ts +38 -0
  40. package/dist/output/reporters.d.ts.sync-conflict-20260306-193811-6PCZ3ZU.map +1 -0
  41. package/dist/output/reporters.sync-conflict-20260309-030558-6PCZ3ZU.js +182 -0
  42. package/dist/output/reports.d.ts.sync-conflict-20260307-172149-6PCZ3ZU.map +1 -0
  43. package/dist/output/reports.js.sync-conflict-20260305-161643-6PCZ3ZU.map +1 -0
  44. package/dist/output/reports.sync-conflict-20260305-211951-6PCZ3ZU.js +393 -0
  45. package/dist/output/visuals.d.ts +53 -0
  46. package/dist/output/visuals.d.ts.map +1 -0
  47. package/dist/output/visuals.js +194 -0
  48. package/dist/output/visuals.js.map +1 -0
  49. package/dist/services/drift-analysis.d.sync-conflict-20260306-151016-6PCZ3ZU.ts +194 -0
  50. package/dist/services/drift-analysis.d.ts.sync-conflict-20260307-175904-6PCZ3ZU.map +1 -0
  51. package/dist/services/drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.d.ts +194 -0
  52. package/dist/services/drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.d.ts.map +1 -0
  53. package/dist/services/drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.js +1022 -0
  54. package/dist/services/drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.js.map +1 -0
  55. package/dist/services/skill-export.d.ts.sync-conflict-20260309-171021-6PCZ3ZU.map +1 -0
  56. package/dist/services/skill-export.sync-conflict-20260309-144621-6PCZ3ZU.d.ts +109 -0
  57. package/dist/services/skill-export.sync-conflict-20260309-144621-6PCZ3ZU.d.ts.map +1 -0
  58. package/dist/services/skill-export.sync-conflict-20260309-144621-6PCZ3ZU.js +737 -0
  59. package/dist/services/skill-export.sync-conflict-20260309-144621-6PCZ3ZU.js.map +1 -0
  60. package/package.json +14 -14
  61. package/LICENSE +0 -21
@@ -0,0 +1,867 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import { sortDriftsBySeverity } from '../services/drift-analysis.js';
4
+ // Severity colors
5
+ export function getSeverityColor(severity) {
6
+ switch (severity) {
7
+ case 'critical':
8
+ return chalk.red;
9
+ case 'warning':
10
+ return chalk.yellow;
11
+ case 'info':
12
+ return chalk.blue;
13
+ }
14
+ }
15
+ export function getSeverityIcon(severity) {
16
+ switch (severity) {
17
+ case 'critical':
18
+ return chalk.red('!');
19
+ case 'warning':
20
+ return chalk.yellow('~');
21
+ case 'info':
22
+ return chalk.blue('i');
23
+ }
24
+ }
25
+ // Format component table
26
+ export function formatComponentTable(components) {
27
+ if (components.length === 0) {
28
+ return chalk.dim('No components found.');
29
+ }
30
+ const table = new Table({
31
+ head: [
32
+ chalk.bold('Name'),
33
+ chalk.bold('Source'),
34
+ chalk.bold('Props'),
35
+ chalk.bold('Variants'),
36
+ ],
37
+ style: { head: [], border: [] },
38
+ });
39
+ for (const comp of components) {
40
+ table.push([
41
+ comp.name,
42
+ comp.source.type,
43
+ String(comp.props.length),
44
+ String(comp.variants.length),
45
+ ]);
46
+ }
47
+ return table.toString();
48
+ }
49
+ // Format token table
50
+ export function formatTokenTable(tokens) {
51
+ if (tokens.length === 0) {
52
+ return chalk.dim('No tokens found.');
53
+ }
54
+ const table = new Table({
55
+ head: [
56
+ chalk.bold('Name'),
57
+ chalk.bold('Category'),
58
+ chalk.bold('Source'),
59
+ chalk.bold('Value'),
60
+ ],
61
+ style: { head: [], border: [] },
62
+ });
63
+ for (const token of tokens) {
64
+ let value = '';
65
+ switch (token.value.type) {
66
+ case 'color':
67
+ value = token.value.hex;
68
+ break;
69
+ case 'spacing':
70
+ value = `${token.value.value}${token.value.unit}`;
71
+ break;
72
+ case 'typography':
73
+ value = `${token.value.fontFamily} ${token.value.fontSize}px`;
74
+ break;
75
+ case 'shadow':
76
+ value = `${token.value.x}px ${token.value.y}px ${token.value.blur}px ${token.value.color}`;
77
+ break;
78
+ case 'border':
79
+ value = `${token.value.width}px ${token.value.style} ${token.value.color}`;
80
+ break;
81
+ case 'raw':
82
+ // Show the actual value, truncated if needed
83
+ value = token.value.value.length > 40
84
+ ? token.value.value.slice(0, 37) + '...'
85
+ : token.value.value;
86
+ break;
87
+ default:
88
+ value = JSON.stringify(token.value).slice(0, 30);
89
+ }
90
+ table.push([
91
+ token.name,
92
+ token.category,
93
+ token.source.type,
94
+ value,
95
+ ]);
96
+ }
97
+ return table.toString();
98
+ }
99
+ // Format drift table
100
+ export function formatDriftTable(drifts) {
101
+ if (drifts.length === 0) {
102
+ return chalk.green('No drift detected. Your design system is aligned.');
103
+ }
104
+ const table = new Table({
105
+ head: [
106
+ '',
107
+ chalk.bold('Type'),
108
+ chalk.bold('Entity'),
109
+ chalk.bold('Message'),
110
+ ],
111
+ style: { head: [], border: [] },
112
+ colWidths: [3, 25, 25, 50],
113
+ wordWrap: true,
114
+ });
115
+ // Sort by severity (critical first)
116
+ const sorted = sortDriftsBySeverity(drifts);
117
+ for (const drift of sorted) {
118
+ const color = getSeverityColor(drift.severity);
119
+ const icon = getSeverityIcon(drift.severity);
120
+ table.push([
121
+ icon,
122
+ color(drift.type),
123
+ drift.source.entityName,
124
+ drift.message,
125
+ ]);
126
+ }
127
+ return table.toString();
128
+ }
129
+ // Format drift details
130
+ export function formatDriftDetails(drift) {
131
+ const color = getSeverityColor(drift.severity);
132
+ const lines = [];
133
+ lines.push(color.bold(`[${drift.severity.toUpperCase()}] ${drift.type}`));
134
+ lines.push('');
135
+ lines.push(chalk.bold('Entity: ') + drift.source.entityName);
136
+ lines.push(chalk.bold('Location: ') + drift.source.location);
137
+ lines.push('');
138
+ lines.push(chalk.bold('Message:'));
139
+ lines.push(drift.message);
140
+ if (drift.details.suggestions && drift.details.suggestions.length > 0) {
141
+ lines.push('');
142
+ lines.push(chalk.bold('Suggestions:'));
143
+ for (const suggestion of drift.details.suggestions) {
144
+ lines.push(` - ${suggestion}`);
145
+ }
146
+ }
147
+ if (drift.details.claudeAnalysis) {
148
+ lines.push('');
149
+ lines.push(chalk.bold('Analysis:'));
150
+ lines.push(drift.details.claudeAnalysis);
151
+ }
152
+ return lines.join('\n');
153
+ }
154
+ // Format summary
155
+ export function formatSummary(stats) {
156
+ const lines = [];
157
+ lines.push(chalk.bold('Summary'));
158
+ lines.push(` Components: ${stats.components}`);
159
+ lines.push(` Tokens: ${stats.tokens}`);
160
+ lines.push('');
161
+ lines.push(chalk.bold('Drift Signals'));
162
+ lines.push(` ${chalk.red('Critical:')} ${stats.drifts.critical}`);
163
+ lines.push(` ${chalk.yellow('Warning:')} ${stats.drifts.warning}`);
164
+ lines.push(` ${chalk.blue('Info:')} ${stats.drifts.info}`);
165
+ return lines.join('\n');
166
+ }
167
+ // Format as JSON
168
+ export function formatJson(data) {
169
+ return JSON.stringify(data, null, 2);
170
+ }
171
+ // Format drift list with full details and action items
172
+ export function formatDriftList(drifts) {
173
+ if (drifts.length === 0) {
174
+ return chalk.green('No drift detected. Your design system is aligned.');
175
+ }
176
+ const lines = [];
177
+ // Sort by severity (critical first)
178
+ const sorted = sortDriftsBySeverity(drifts);
179
+ // Group by severity for better readability
180
+ const critical = sorted.filter(d => d.severity === 'critical');
181
+ const warning = sorted.filter(d => d.severity === 'warning');
182
+ const info = sorted.filter(d => d.severity === 'info');
183
+ const formatIssue = (drift, index) => {
184
+ const color = getSeverityColor(drift.severity);
185
+ const icon = getSeverityIcon(drift.severity);
186
+ lines.push('');
187
+ lines.push(`${icon} ${color.bold(`#${index + 1} ${formatDriftType(drift.type)}`)}`);
188
+ lines.push(` ${chalk.dim('Component:')} ${drift.source.entityName}`);
189
+ if (drift.source.location) {
190
+ lines.push(` ${chalk.dim('Location:')} ${drift.source.location}`);
191
+ }
192
+ lines.push(` ${chalk.dim('Issue:')} ${drift.message}`);
193
+ // Show expected vs actual if available
194
+ if (drift.details.expected !== undefined && drift.details.actual !== undefined) {
195
+ lines.push(` ${chalk.dim('Expected:')} ${chalk.green(String(drift.details.expected))}`);
196
+ lines.push(` ${chalk.dim('Actual:')} ${chalk.red(String(drift.details.actual))}`);
197
+ }
198
+ // Show affected files/locations if available
199
+ if (drift.details.affectedFiles && drift.details.affectedFiles.length > 0) {
200
+ lines.push(` ${chalk.dim('Details:')}`);
201
+ for (const file of drift.details.affectedFiles.slice(0, 5)) {
202
+ lines.push(` ${chalk.dim('•')} ${file}`);
203
+ }
204
+ if (drift.details.affectedFiles.length > 5) {
205
+ lines.push(` ${chalk.dim(`... and ${drift.details.affectedFiles.length - 5} more`)}`);
206
+ }
207
+ }
208
+ // Show related components if available
209
+ if (drift.details.relatedComponents && drift.details.relatedComponents.length > 0) {
210
+ lines.push(` ${chalk.dim('Related:')} ${drift.details.relatedComponents.join(', ')}`);
211
+ }
212
+ // Show action items
213
+ const actions = getActionItems(drift);
214
+ if (actions.length > 0) {
215
+ lines.push('');
216
+ lines.push(` ${chalk.cyan.bold('Actions:')}`);
217
+ for (let i = 0; i < actions.length; i++) {
218
+ lines.push(` ${chalk.cyan(`${i + 1}.`)} ${actions[i]}`);
219
+ }
220
+ }
221
+ };
222
+ let issueNumber = 0;
223
+ if (critical.length > 0) {
224
+ lines.push('');
225
+ lines.push(chalk.red.bold(`━━━ CRITICAL (${critical.length}) ━━━`));
226
+ for (const drift of critical) {
227
+ formatIssue(drift, issueNumber++);
228
+ }
229
+ }
230
+ if (warning.length > 0) {
231
+ lines.push('');
232
+ lines.push(chalk.yellow.bold(`━━━ WARNING (${warning.length}) ━━━`));
233
+ for (const drift of warning) {
234
+ formatIssue(drift, issueNumber++);
235
+ }
236
+ }
237
+ if (info.length > 0) {
238
+ lines.push('');
239
+ lines.push(chalk.blue.bold(`━━━ INFO (${info.length}) ━━━`));
240
+ for (const drift of info) {
241
+ formatIssue(drift, issueNumber++);
242
+ }
243
+ }
244
+ return lines.join('\n');
245
+ }
246
+ // Get specific action items for a drift signal
247
+ function getActionItems(drift) {
248
+ const actions = [];
249
+ switch (drift.type) {
250
+ case 'hardcoded-value':
251
+ if (drift.message.includes('color')) {
252
+ actions.push('Replace hardcoded colors with design tokens');
253
+ actions.push('Example: Change #3b82f6 → var(--color-primary) or theme.colors.primary');
254
+ actions.push('If no token exists, add it to your design system first');
255
+ }
256
+ else {
257
+ actions.push('Replace hardcoded values with spacing/size tokens');
258
+ actions.push('Example: Change 16px → var(--spacing-4) or theme.spacing.md');
259
+ }
260
+ break;
261
+ case 'naming-inconsistency':
262
+ if (drift.details.relatedComponents) {
263
+ actions.push(`Consolidate duplicate components: ${drift.details.relatedComponents.join(', ')}`);
264
+ actions.push('Keep the most complete version and update imports');
265
+ actions.push('Document the canonical component in your design system');
266
+ }
267
+ else {
268
+ actions.push('Rename component to match project conventions');
269
+ if (drift.details.suggestions?.[0]) {
270
+ actions.push(drift.details.suggestions[0]);
271
+ }
272
+ }
273
+ break;
274
+ case 'semantic-mismatch':
275
+ actions.push('Standardize prop types across components');
276
+ if (drift.details.expected && drift.details.actual) {
277
+ actions.push(`Change prop type from "${drift.details.actual}" to "${drift.details.expected}"`);
278
+ }
279
+ if (drift.details.usedIn && drift.details.usedIn.length > 0) {
280
+ actions.push(`Reference: ${drift.details.usedIn.join(', ')} use the expected type`);
281
+ }
282
+ break;
283
+ case 'deprecated-pattern':
284
+ actions.push('Migrate away from deprecated component');
285
+ if (drift.details.suggestions?.[0]) {
286
+ actions.push(drift.details.suggestions[0]);
287
+ }
288
+ actions.push('Search codebase for usages and update imports');
289
+ break;
290
+ case 'orphaned-component':
291
+ if (drift.message.includes('not in design')) {
292
+ actions.push('Add component to Figma design system');
293
+ actions.push('Or document as intentional code-only component');
294
+ actions.push('Or remove if truly unused');
295
+ }
296
+ else {
297
+ actions.push('Implement the designed component in code');
298
+ actions.push('Or remove from Figma if no longer needed');
299
+ }
300
+ break;
301
+ case 'orphaned-token':
302
+ if (drift.message.includes('not in design')) {
303
+ actions.push('Add token to design system (Figma/Tokens Studio)');
304
+ actions.push('Or remove from code if unused');
305
+ }
306
+ else {
307
+ actions.push('Implement token in code (CSS variables or theme)');
308
+ actions.push('Or remove from design if deprecated');
309
+ }
310
+ break;
311
+ case 'value-divergence':
312
+ actions.push('Align token values between design and code');
313
+ if (drift.details.expected && drift.details.actual) {
314
+ actions.push(`Design value: ${JSON.stringify(drift.details.expected)}`);
315
+ actions.push(`Code value: ${JSON.stringify(drift.details.actual)}`);
316
+ }
317
+ actions.push('Update whichever source is outdated');
318
+ break;
319
+ case 'accessibility-conflict':
320
+ actions.push('Add missing accessibility attributes');
321
+ actions.push('For interactive elements: add aria-label or visible text');
322
+ actions.push('Run accessibility audit: npx axe-core or use browser devtools');
323
+ break;
324
+ case 'framework-sprawl':
325
+ actions.push('Document which framework is primary');
326
+ actions.push('Create migration plan for legacy framework code');
327
+ if (drift.details.frameworks) {
328
+ const frameworks = drift.details.frameworks;
329
+ actions.push(`Frameworks detected: ${frameworks.map(f => f.name).join(', ')}`);
330
+ }
331
+ break;
332
+ default:
333
+ // Fall back to generic suggestions from the drift signal
334
+ if (drift.details.suggestions) {
335
+ actions.push(...drift.details.suggestions);
336
+ }
337
+ }
338
+ return actions;
339
+ }
340
+ // Format drift type for display (technical)
341
+ function formatDriftType(type) {
342
+ const labels = {
343
+ 'hardcoded-value': 'Hardcoded Value',
344
+ 'naming-inconsistency': 'Naming Inconsistency',
345
+ 'semantic-mismatch': 'Prop Type Mismatch',
346
+ 'deprecated-pattern': 'Deprecated Component',
347
+ 'orphaned-component': 'Orphaned Component',
348
+ 'orphaned-token': 'Orphaned Token',
349
+ 'value-divergence': 'Token Value Mismatch',
350
+ 'accessibility-conflict': 'Accessibility Issue',
351
+ 'framework-sprawl': 'Framework Sprawl',
352
+ 'missing-documentation': 'Missing Documentation',
353
+ };
354
+ return labels[type] || type;
355
+ }
356
+ // Designer-friendly labels (plain English, non-technical)
357
+ export function formatDriftTypeForDesigners(type) {
358
+ const labels = {
359
+ 'hardcoded-value': 'Using wrong color/size',
360
+ 'naming-inconsistency': 'Inconsistent naming',
361
+ 'semantic-mismatch': 'Component behaves differently',
362
+ 'deprecated-pattern': 'Using outdated component',
363
+ 'orphaned-component': 'Component not in design file',
364
+ 'orphaned-token': 'Style not in design system',
365
+ 'value-divergence': 'Design doesn\'t match code',
366
+ 'accessibility-conflict': 'Accessibility problem',
367
+ 'framework-sprawl': 'Mixed technologies',
368
+ 'missing-documentation': 'Missing documentation',
369
+ 'unused-component': 'Component never used',
370
+ 'unused-token': 'Style never used',
371
+ 'color-contrast': 'Hard to read (contrast)',
372
+ };
373
+ return labels[type] || type;
374
+ }
375
+ // Designer-friendly explanations
376
+ export function getDriftExplanationForDesigners(type) {
377
+ const explanations = {
378
+ 'hardcoded-value': 'A developer typed a specific color or size value instead of using the design system. This makes it harder to update the design later.',
379
+ 'naming-inconsistency': 'Similar components have different names, which makes it confusing to know which one to use.',
380
+ 'semantic-mismatch': 'The same component works differently in different places, which creates an inconsistent experience.',
381
+ 'deprecated-pattern': 'This component has been replaced with a newer version, but someone is still using the old one.',
382
+ 'orphaned-component': 'This component exists in code but isn\'t documented in Figma, so designers can\'t see it.',
383
+ 'orphaned-token': 'This style exists in code but isn\'t in the design system, so it might be inconsistent.',
384
+ 'value-divergence': 'The design file says one thing, but the code says something different. They need to be synced.',
385
+ 'accessibility-conflict': 'This might be hard for some users to see or interact with.',
386
+ 'framework-sprawl': 'The code uses multiple different technologies, which can cause inconsistencies.',
387
+ 'missing-documentation': 'This component doesn\'t have documentation, so developers might use it incorrectly.',
388
+ 'unused-component': 'This component was built but never actually used anywhere.',
389
+ 'unused-token': 'This style was defined but never applied anywhere.',
390
+ 'color-contrast': 'The text and background colors don\'t have enough contrast for everyone to read easily.',
391
+ };
392
+ return explanations[type] || 'A design consistency issue was detected.';
393
+ }
394
+ // Format for AI agents - concise, actionable, easy to parse
395
+ export function formatAgent(drifts) {
396
+ if (drifts.length === 0) {
397
+ return JSON.stringify({ status: 'clean', fixes: [] });
398
+ }
399
+ // Focus on actionable signals (warning and critical only)
400
+ const actionable = drifts.filter(d => d.severity === 'warning' || d.severity === 'critical');
401
+ if (actionable.length === 0) {
402
+ return JSON.stringify({
403
+ status: 'clean',
404
+ fixes: [],
405
+ info: `${drifts.length} info-level signals (run with --verbose to see)`
406
+ });
407
+ }
408
+ // Group by file for efficient fixes
409
+ const byFile = new Map();
410
+ for (const drift of actionable) {
411
+ const file = drift.source.location?.split(':')[0] || drift.source.entityName;
412
+ const existing = byFile.get(file) || [];
413
+ existing.push(drift);
414
+ byFile.set(file, existing);
415
+ }
416
+ const fixes = [];
417
+ for (const [file, fileDrifts] of byFile) {
418
+ for (const drift of fileDrifts) {
419
+ const line = drift.source.location?.includes(':')
420
+ ? parseInt(drift.source.location.split(':')[1] || '0', 10)
421
+ : undefined;
422
+ // Generate specific fix suggestion
423
+ let fix = '';
424
+ if (drift.type === 'hardcoded-value') {
425
+ if (drift.message.includes('color')) {
426
+ const match = drift.message.match(/#[0-9a-fA-F]{3,8}/);
427
+ if (match) {
428
+ fix = `Replace ${match[0]} with design token (e.g., bg-muted, text-primary)`;
429
+ }
430
+ else {
431
+ fix = 'Replace hardcoded color with design token';
432
+ }
433
+ }
434
+ else if (drift.message.includes('spacing')) {
435
+ fix = 'Replace arbitrary spacing with theme value (e.g., p-4, gap-2)';
436
+ }
437
+ else if (drift.message.includes('size')) {
438
+ fix = 'Replace arbitrary size with theme value (e.g., w-full, h-10)';
439
+ }
440
+ else {
441
+ fix = 'Replace arbitrary value with design token';
442
+ }
443
+ }
444
+ else if (drift.type === 'semantic-mismatch') {
445
+ fix = `Standardize prop type: ${drift.details.actual} → ${drift.details.expected}`;
446
+ }
447
+ else if (drift.details.suggestions?.[0]) {
448
+ fix = drift.details.suggestions[0];
449
+ }
450
+ else {
451
+ fix = drift.message;
452
+ }
453
+ fixes.push({
454
+ file,
455
+ severity: drift.severity,
456
+ type: drift.type,
457
+ issue: drift.message,
458
+ fix,
459
+ ...(line && { line }),
460
+ });
461
+ }
462
+ }
463
+ // Sort by severity (critical first) then by file
464
+ fixes.sort((a, b) => {
465
+ if (a.severity !== b.severity) {
466
+ return a.severity === 'critical' ? -1 : 1;
467
+ }
468
+ return a.file.localeCompare(b.file);
469
+ });
470
+ return JSON.stringify({
471
+ status: 'drift_detected',
472
+ summary: {
473
+ critical: drifts.filter(d => d.severity === 'critical').length,
474
+ warning: drifts.filter(d => d.severity === 'warning').length,
475
+ info: drifts.filter(d => d.severity === 'info').length,
476
+ },
477
+ fixes: fixes.slice(0, 20), // Limit to top 20 for context efficiency
478
+ ...(fixes.length > 20 && { truncated: fixes.length - 20 }),
479
+ }, null, 2);
480
+ }
481
+ // Format as HTML (shareable with designers)
482
+ export function formatHtml(drifts, options) {
483
+ const useDesignerLanguage = options?.designerFriendly ?? true;
484
+ const getLabel = useDesignerLanguage ? formatDriftTypeForDesigners : formatDriftType;
485
+ const getExplanation = useDesignerLanguage ? getDriftExplanationForDesigners : () => '';
486
+ const critical = drifts.filter(d => d.severity === 'critical');
487
+ const warning = drifts.filter(d => d.severity === 'warning');
488
+ const info = drifts.filter(d => d.severity === 'info');
489
+ const severityColors = {
490
+ critical: '#dc2626',
491
+ warning: '#d97706',
492
+ info: '#2563eb',
493
+ };
494
+ const renderDrift = (drift) => {
495
+ const color = severityColors[drift.severity];
496
+ const explanation = getExplanation(drift.type);
497
+ return `
498
+ <div style="border-left: 4px solid ${color}; padding: 12px 16px; margin: 12px 0; background: #f9fafb; border-radius: 0 8px 8px 0;">
499
+ <div style="font-weight: 600; color: ${color}; margin-bottom: 4px;">${getLabel(drift.type)}</div>
500
+ <div style="font-size: 14px; color: #374151; margin-bottom: 8px;">${drift.source.entityName}</div>
501
+ ${drift.source.location ? `<div style="font-size: 12px; color: #6b7280; margin-bottom: 8px;">📍 ${drift.source.location}</div>` : ''}
502
+ <div style="font-size: 14px; color: #111827;">${drift.message}</div>
503
+ ${explanation ? `<div style="font-size: 13px; color: #6b7280; margin-top: 8px; font-style: italic;">${explanation}</div>` : ''}
504
+ ${drift.details.suggestions && drift.details.suggestions.length > 0 ? `
505
+ <div style="margin-top: 12px; padding: 8px 12px; background: #ecfdf5; border-radius: 4px;">
506
+ <div style="font-size: 12px; font-weight: 600; color: #059669; margin-bottom: 4px;">💡 How to fix:</div>
507
+ <ul style="margin: 0; padding-left: 20px; font-size: 13px; color: #047857;">
508
+ ${drift.details.suggestions.map(s => `<li>${s}</li>`).join('')}
509
+ </ul>
510
+ </div>
511
+ ` : ''}
512
+ </div>
513
+ `;
514
+ };
515
+ return `<!DOCTYPE html>
516
+ <html lang="en">
517
+ <head>
518
+ <meta charset="UTF-8">
519
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
520
+ <title>Buoy Design Drift Report</title>
521
+ <style>
522
+ * { box-sizing: border-box; margin: 0; padding: 0; }
523
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #fff; color: #111827; line-height: 1.5; padding: 40px; max-width: 900px; margin: 0 auto; }
524
+ h1 { font-size: 24px; margin-bottom: 8px; }
525
+ .subtitle { color: #6b7280; font-size: 14px; margin-bottom: 24px; }
526
+ .summary { display: flex; gap: 16px; margin-bottom: 32px; }
527
+ .summary-card { padding: 16px 24px; border-radius: 8px; text-align: center; }
528
+ .summary-card.critical { background: #fef2f2; border: 1px solid #fecaca; }
529
+ .summary-card.warning { background: #fffbeb; border: 1px solid #fde68a; }
530
+ .summary-card.info { background: #eff6ff; border: 1px solid #bfdbfe; }
531
+ .summary-number { font-size: 32px; font-weight: 700; }
532
+ .summary-label { font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; color: #6b7280; }
533
+ .section-title { font-size: 18px; font-weight: 600; margin: 24px 0 16px; padding-bottom: 8px; border-bottom: 2px solid; }
534
+ .section-title.critical { border-color: #dc2626; color: #dc2626; }
535
+ .section-title.warning { border-color: #d97706; color: #d97706; }
536
+ .section-title.info { border-color: #2563eb; color: #2563eb; }
537
+ .empty { text-align: center; padding: 48px; color: #059669; background: #ecfdf5; border-radius: 8px; }
538
+ .empty-icon { font-size: 48px; margin-bottom: 12px; }
539
+ footer { margin-top: 48px; padding-top: 24px; border-top: 1px solid #e5e7eb; font-size: 12px; color: #9ca3af; text-align: center; }
540
+ </style>
541
+ </head>
542
+ <body>
543
+ <h1>🚢 Design Drift Report</h1>
544
+ <div class="subtitle">Generated ${new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })} at ${new Date().toLocaleTimeString()}</div>
545
+
546
+ ${drifts.length === 0 ? `
547
+ <div class="empty">
548
+ <div class="empty-icon">✨</div>
549
+ <div style="font-size: 18px; font-weight: 600;">No drift detected!</div>
550
+ <div style="margin-top: 8px;">Your design system is perfectly aligned.</div>
551
+ </div>
552
+ ` : `
553
+ <div class="summary">
554
+ <div class="summary-card critical">
555
+ <div class="summary-number" style="color: #dc2626;">${critical.length}</div>
556
+ <div class="summary-label">Critical</div>
557
+ </div>
558
+ <div class="summary-card warning">
559
+ <div class="summary-number" style="color: #d97706;">${warning.length}</div>
560
+ <div class="summary-label">Warnings</div>
561
+ </div>
562
+ <div class="summary-card info">
563
+ <div class="summary-number" style="color: #2563eb;">${info.length}</div>
564
+ <div class="summary-label">Info</div>
565
+ </div>
566
+ </div>
567
+
568
+ ${critical.length > 0 ? `
569
+ <h2 class="section-title critical">🔴 Critical Issues (${critical.length})</h2>
570
+ ${critical.map(renderDrift).join('')}
571
+ ` : ''}
572
+
573
+ ${warning.length > 0 ? `
574
+ <h2 class="section-title warning">🟡 Warnings (${warning.length})</h2>
575
+ ${warning.map(renderDrift).join('')}
576
+ ` : ''}
577
+
578
+ ${info.length > 0 ? `
579
+ <h2 class="section-title info">🔵 Info (${info.length})</h2>
580
+ ${info.map(renderDrift).join('')}
581
+ ` : ''}
582
+ `}
583
+
584
+ <footer>
585
+ Generated by <strong>Buoy</strong> — Design drift detection for AI-generated code<br>
586
+ <a href="https://github.com/ahoybuoy/buoy" style="color: #6b7280;">github.com/ahoybuoy/buoy</a>
587
+ </footer>
588
+ </body>
589
+ </html>`;
590
+ }
591
+ // Format like the buoy.design homepage - compact tree view
592
+ export function formatDriftTree(drifts, fileCount = 0) {
593
+ if (drifts.length === 0) {
594
+ return chalk.green('✓ No drift detected. Your design system is aligned.');
595
+ }
596
+ const lines = [];
597
+ // Summary line
598
+ const fileText = fileCount > 0 ? ` in ${fileCount} files` : '';
599
+ lines.push(chalk.white.bold(`Found ${drifts.length} issues${fileText}`));
600
+ lines.push('');
601
+ // Group by drift type for cleaner display
602
+ const grouped = new Map();
603
+ for (const drift of drifts) {
604
+ const key = drift.type;
605
+ if (!grouped.has(key)) {
606
+ grouped.set(key, []);
607
+ }
608
+ grouped.get(key).push(drift);
609
+ }
610
+ // Category display names and colors
611
+ const categoryConfig = {
612
+ 'hardcoded-value': { label: 'HARDCODED VALUES', color: chalk.yellow },
613
+ 'naming-inconsistency': { label: 'NAMING', color: chalk.yellow },
614
+ 'semantic-mismatch': { label: 'TYPE MISMATCHES', color: chalk.yellow },
615
+ 'deprecated-pattern': { label: 'DEPRECATED', color: chalk.red },
616
+ 'orphaned-component': { label: 'ORPHANED', color: chalk.yellow },
617
+ 'orphaned-token': { label: 'ORPHANED TOKENS', color: chalk.yellow },
618
+ 'value-divergence': { label: 'VALUE DRIFT', color: chalk.yellow },
619
+ 'accessibility-conflict': { label: 'ACCESSIBILITY', color: chalk.red },
620
+ 'framework-sprawl': { label: 'FRAMEWORK SPRAWL', color: chalk.yellow },
621
+ 'missing-documentation': { label: 'MISSING DOCS', color: chalk.blue },
622
+ };
623
+ // Sort groups: critical severity types first
624
+ const criticalTypes = ['accessibility-conflict', 'deprecated-pattern'];
625
+ const sortedGroups = [...grouped.entries()].sort((a, b) => {
626
+ const aIsCritical = criticalTypes.includes(a[0]);
627
+ const bIsCritical = criticalTypes.includes(b[0]);
628
+ if (aIsCritical && !bIsCritical)
629
+ return -1;
630
+ if (!aIsCritical && bIsCritical)
631
+ return 1;
632
+ return b[1].length - a[1].length; // Then by count
633
+ });
634
+ for (const [type, typeDrifts] of sortedGroups) {
635
+ const config = categoryConfig[type] || { label: type.toUpperCase().replace(/-/g, ' '), color: chalk.yellow };
636
+ lines.push(config.color.bold(`${config.label} (${typeDrifts.length})`));
637
+ // Sort by file occurrence — files with most issues shown first
638
+ const fileCounts = new Map();
639
+ for (const d of typeDrifts) {
640
+ const file = (d.source.location || d.source.entityName).split(':')[0] ?? '';
641
+ fileCounts.set(file, (fileCounts.get(file) ?? 0) + 1);
642
+ }
643
+ typeDrifts.sort((a, b) => {
644
+ const fileA = (a.source.location || a.source.entityName).split(':')[0] ?? '';
645
+ const fileB = (b.source.location || b.source.entityName).split(':')[0] ?? '';
646
+ return (fileCounts.get(fileB) ?? 0) - (fileCounts.get(fileA) ?? 0);
647
+ });
648
+ // Show up to 5 items per category
649
+ const shown = typeDrifts.slice(0, 5);
650
+ const remaining = typeDrifts.length - shown.length;
651
+ shown.forEach((drift, i) => {
652
+ const isLast = i === shown.length - 1 && remaining === 0;
653
+ const prefix = isLast ? '└─' : '├─';
654
+ // Extract file:line from location or use entity name
655
+ const location = drift.source.location || drift.source.entityName;
656
+ const parts = location.split(':');
657
+ const file = parts[0] || drift.source.entityName;
658
+ const lineNum = parts[1];
659
+ const shortFile = file.length > 30 ? '...' + file.slice(-27) : file;
660
+ const fileLoc = lineNum ? `${shortFile}:${lineNum}` : shortFile;
661
+ // Build the issue description
662
+ let issueText = '';
663
+ if (drift.type === 'hardcoded-value') {
664
+ const tokenSuggs = drift.details?.tokenSuggestions;
665
+ const colorMatch = drift.message.match(/#[0-9a-fA-F]{3,8}/g);
666
+ const sizeMatches = drift.message.match(/\d+px/g);
667
+ const firstSugg = tokenSuggs?.[0];
668
+ if (firstSugg) {
669
+ // Parse: "value → tokenName (N% match)"
670
+ const suggMatch = firstSugg.match(/^(.+?)\s*→\s*(.+?)\s*\((\d+)%/);
671
+ if (suggMatch) {
672
+ const value = suggMatch[1] ?? '';
673
+ const tokenName = suggMatch[2] ?? '';
674
+ const confidence = suggMatch[3] ?? '';
675
+ const displayValue = colorMatch?.[0]
676
+ ? chalk.hex(colorMatch[0])(value.trim())
677
+ : chalk.dim(value.trim());
678
+ issueText = `${displayValue} → ${chalk.cyan(tokenName.trim())} ${chalk.dim(`(${confidence}%)`)}`;
679
+ }
680
+ else {
681
+ issueText = chalk.cyan(firstSugg);
682
+ }
683
+ }
684
+ else if (colorMatch && colorMatch.length > 0) {
685
+ const colorVal = colorMatch[0];
686
+ issueText = `${chalk.hex(colorVal)(colorVal)} → ${chalk.cyan('use var(--color-*)')}`;
687
+ }
688
+ else if (sizeMatches && sizeMatches.length > 0) {
689
+ const sizeVal = sizeMatches[0];
690
+ issueText = `${chalk.dim(sizeVal)} → ${chalk.cyan('use var(--spacing-*)')}`;
691
+ }
692
+ else {
693
+ issueText = chalk.dim('hardcoded value detected');
694
+ }
695
+ }
696
+ else if (drift.type === 'accessibility-conflict') {
697
+ // Extract key issue
698
+ if (drift.message.includes('aria-label')) {
699
+ issueText = 'Missing aria-label';
700
+ }
701
+ else if (drift.message.includes('focus')) {
702
+ issueText = 'Focus trap not implemented';
703
+ }
704
+ else {
705
+ issueText = drift.message.slice(0, 35);
706
+ }
707
+ }
708
+ else if (drift.type === 'naming-inconsistency') {
709
+ const suggestions = drift.details?.suggestions;
710
+ if (suggestions?.[0]) {
711
+ issueText = `→ rename to ${chalk.cyan(suggestions[0])}`;
712
+ }
713
+ else {
714
+ issueText = 'Inconsistent naming';
715
+ }
716
+ }
717
+ else if (drift.type === 'deprecated-pattern') {
718
+ issueText = chalk.red('deprecated') + ' - migrate to new API';
719
+ }
720
+ else {
721
+ // For other types, show a brief message
722
+ const suggestions = drift.details?.suggestions;
723
+ if (suggestions?.[0]) {
724
+ issueText = `→ ${suggestions[0].slice(0, 30)}`;
725
+ }
726
+ else {
727
+ issueText = drift.message.slice(0, 35);
728
+ }
729
+ }
730
+ lines.push(chalk.dim(prefix) + ' ' + chalk.white(fileLoc) + ' ' + issueText);
731
+ });
732
+ if (remaining > 0) {
733
+ lines.push(chalk.dim('└─ ... and ' + remaining + ' more'));
734
+ }
735
+ const topFile = [...fileCounts.entries()].sort((a, b) => b[1] - a[1])[0];
736
+ if (topFile && topFile[1] >= 3) {
737
+ const shortName = topFile[0].length > 25 ? '...' + topFile[0].slice(-22) : topFile[0];
738
+ lines.push(chalk.dim(` Fix ${shortName} first (${topFile[1]} issues)`));
739
+ }
740
+ lines.push('');
741
+ }
742
+ // Footer
743
+ lines.push(chalk.dim('Add to CI to catch drift on every PR'));
744
+ return lines.join('\n');
745
+ }
746
+ // Format scan results as markdown
747
+ export function formatScanMarkdown(components, tokens) {
748
+ const lines = [];
749
+ lines.push('# Scan Results');
750
+ lines.push('');
751
+ lines.push(`Generated: ${new Date().toISOString()}`);
752
+ lines.push('');
753
+ lines.push(`## Components (${components.length})`);
754
+ lines.push('');
755
+ if (components.length > 0) {
756
+ lines.push('| Name | Source | Props | Variants |');
757
+ lines.push('|------|--------|-------|----------|');
758
+ for (const comp of components) {
759
+ const sourcePath = 'path' in comp.source ? comp.source.path : comp.source.type;
760
+ lines.push(`| ${comp.name} | ${sourcePath} | ${comp.props.length} | ${comp.variants.length} |`);
761
+ }
762
+ }
763
+ else {
764
+ lines.push('No components found.');
765
+ }
766
+ lines.push('');
767
+ lines.push(`## Tokens (${tokens.length})`);
768
+ lines.push('');
769
+ if (tokens.length > 0) {
770
+ lines.push('| Name | Category | Source | Value |');
771
+ lines.push('|------|----------|--------|-------|');
772
+ for (const token of tokens) {
773
+ let value = '';
774
+ switch (token.value.type) {
775
+ case 'color':
776
+ value = token.value.hex;
777
+ break;
778
+ case 'spacing':
779
+ value = `${token.value.value}${token.value.unit}`;
780
+ break;
781
+ case 'typography':
782
+ value = `${token.value.fontFamily} ${token.value.fontSize}px`;
783
+ break;
784
+ case 'shadow':
785
+ value = `${token.value.x}px ${token.value.y}px ${token.value.blur}px ${token.value.color}`;
786
+ break;
787
+ case 'border':
788
+ value = `${token.value.width}px ${token.value.style} ${token.value.color}`;
789
+ break;
790
+ case 'raw':
791
+ value = token.value.value.length > 40
792
+ ? token.value.value.slice(0, 37) + '...'
793
+ : token.value.value;
794
+ break;
795
+ default:
796
+ value = JSON.stringify(token.value).slice(0, 30);
797
+ }
798
+ const sourcePath = 'path' in token.source ? token.source.path : token.source.type;
799
+ lines.push(`| ${token.name} | ${token.category} | ${sourcePath} | ${value} |`);
800
+ }
801
+ }
802
+ else {
803
+ lines.push('No tokens found.');
804
+ }
805
+ return lines.join('\n');
806
+ }
807
+ // Format as markdown
808
+ export function formatMarkdown(drifts) {
809
+ if (drifts.length === 0) {
810
+ return '# Drift Report\n\nNo drift detected.';
811
+ }
812
+ const lines = [];
813
+ lines.push('# Drift Report');
814
+ lines.push('');
815
+ lines.push(`Generated: ${new Date().toISOString()}`);
816
+ lines.push('');
817
+ const critical = drifts.filter(d => d.severity === 'critical');
818
+ const warning = drifts.filter(d => d.severity === 'warning');
819
+ const info = drifts.filter(d => d.severity === 'info');
820
+ const formatDriftMarkdown = (drift) => {
821
+ lines.push(`### ${drift.source.entityName}`);
822
+ lines.push(`- **Type:** ${formatDriftType(drift.type)}`);
823
+ if (drift.source.location) {
824
+ lines.push(`- **Location:** \`${drift.source.location}\``);
825
+ }
826
+ lines.push(`- **Issue:** ${drift.message}`);
827
+ if (drift.details.expected !== undefined && drift.details.actual !== undefined) {
828
+ lines.push(`- **Expected:** ${drift.details.expected}`);
829
+ lines.push(`- **Actual:** ${drift.details.actual}`);
830
+ }
831
+ if (drift.details.relatedComponents && drift.details.relatedComponents.length > 0) {
832
+ lines.push(`- **Related:** ${drift.details.relatedComponents.join(', ')}`);
833
+ }
834
+ const actions = getActionItems(drift);
835
+ if (actions.length > 0) {
836
+ lines.push('');
837
+ lines.push('**Actions:**');
838
+ for (let i = 0; i < actions.length; i++) {
839
+ lines.push(`${i + 1}. ${actions[i]}`);
840
+ }
841
+ }
842
+ lines.push('');
843
+ };
844
+ if (critical.length > 0) {
845
+ lines.push('## Critical');
846
+ lines.push('');
847
+ for (const drift of critical) {
848
+ formatDriftMarkdown(drift);
849
+ }
850
+ }
851
+ if (warning.length > 0) {
852
+ lines.push('## Warnings');
853
+ lines.push('');
854
+ for (const drift of warning) {
855
+ formatDriftMarkdown(drift);
856
+ }
857
+ }
858
+ if (info.length > 0) {
859
+ lines.push('## Info');
860
+ lines.push('');
861
+ for (const drift of info) {
862
+ formatDriftMarkdown(drift);
863
+ }
864
+ }
865
+ return lines.join('\n');
866
+ }
867
+ //# sourceMappingURL=formatters.sync-conflict-20260307-131418-5SN2GZG.js.map