@fragments-sdk/cli 0.2.2
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/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- package/src/viewer/vite-plugin.ts +2143 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { BRAND } from '../core/index.js';
|
|
4
|
+
import { createDevServer } from './server.js';
|
|
5
|
+
import { runCommand } from './cli/index.js';
|
|
6
|
+
|
|
7
|
+
async function startDevServer(): Promise<void> {
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const portArg = args.find((arg) => arg.startsWith('--port='));
|
|
10
|
+
const port = portArg ? parseInt(portArg.split('=')[1], 10) : 6006;
|
|
11
|
+
const noOpen = args.includes('--no-open');
|
|
12
|
+
|
|
13
|
+
console.log(pc.cyan(`\n${BRAND.name} Dev Server\n`));
|
|
14
|
+
console.log(pc.dim('Starting development server...\n'));
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const server = await createDevServer({
|
|
18
|
+
port,
|
|
19
|
+
open: !noOpen,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const address = server.httpServer?.address();
|
|
23
|
+
const actualPort = typeof address === 'object' && address ? address.port : port;
|
|
24
|
+
|
|
25
|
+
console.log(pc.green(`✓ Server running at http://localhost:${actualPort}\n`));
|
|
26
|
+
console.log(pc.dim('Press Ctrl+C to stop\n'));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(pc.red('Failed to start server:'), error);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function showHelp(): void {
|
|
34
|
+
console.log(pc.cyan(`\n${BRAND.name} CLI\n`));
|
|
35
|
+
console.log(`Usage: ${BRAND.cliCommand} <command> [options]\n`);
|
|
36
|
+
console.log('Commands:');
|
|
37
|
+
console.log(' dev Start the development server (default)');
|
|
38
|
+
console.log(' scan <dir> Scan codebase for component usage');
|
|
39
|
+
console.log(' health Generate design system health report');
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log('Dev Server Options:');
|
|
42
|
+
console.log(' --port=<port> Port to run on (default: 6006)');
|
|
43
|
+
console.log(' --no-open Don\'t open browser automatically');
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log('Scan Options:');
|
|
46
|
+
console.log(' --format=<fmt> Output format: table, json (default: table)');
|
|
47
|
+
console.log(' --component=<n> Filter to specific component');
|
|
48
|
+
console.log(' --verbose Show detailed usage info');
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log('Health Options:');
|
|
51
|
+
console.log(' --format=<fmt> Output format: table, json, markdown (default: table)');
|
|
52
|
+
console.log(' --scan=<dir> Include codebase usage scan');
|
|
53
|
+
console.log(' --ci CI mode: exit 1 if score below threshold');
|
|
54
|
+
console.log(' --threshold=<n> Minimum score for CI mode (default: 80)');
|
|
55
|
+
console.log('');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function main(): Promise<void> {
|
|
59
|
+
const args = process.argv.slice(2);
|
|
60
|
+
const command = args[0];
|
|
61
|
+
|
|
62
|
+
// Show help
|
|
63
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
64
|
+
showHelp();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Route to subcommands
|
|
69
|
+
if (command === 'scan' || command === 'health') {
|
|
70
|
+
await runCommand(command, args.slice(1));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Default: start dev server (handles 'dev' command or no command)
|
|
75
|
+
if (!command || command === 'dev' || command.startsWith('--')) {
|
|
76
|
+
await startDevServer();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Unknown command
|
|
81
|
+
console.error(pc.red(`Unknown command: ${command}`));
|
|
82
|
+
console.log(pc.dim(`Run "${BRAND.cliCommand} --help" for usage information.\n`));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
main();
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Command
|
|
3
|
+
* Generates a comprehensive design system health report
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import pc from 'picocolors';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { BRAND } from '../../core/index.js';
|
|
10
|
+
import { loadConfig } from '../../core/node.js';
|
|
11
|
+
import { scanForUsages } from '../intelligence/usageScanner.js';
|
|
12
|
+
import { generateHealthReport, formatHealthReportAsMarkdown, type HealthReport } from '../intelligence/healthReport.js';
|
|
13
|
+
import {
|
|
14
|
+
parseArgs,
|
|
15
|
+
createSpinner,
|
|
16
|
+
formatScore,
|
|
17
|
+
printHeader,
|
|
18
|
+
printKV,
|
|
19
|
+
printWarning,
|
|
20
|
+
printSuccess,
|
|
21
|
+
formatList,
|
|
22
|
+
} from './utils.js';
|
|
23
|
+
|
|
24
|
+
type OutputFormat = 'table' | 'json' | 'markdown';
|
|
25
|
+
|
|
26
|
+
interface HealthCommandOptions {
|
|
27
|
+
format: OutputFormat;
|
|
28
|
+
scanDir?: string;
|
|
29
|
+
ci: boolean;
|
|
30
|
+
threshold: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseOptions(args: string[]): HealthCommandOptions {
|
|
34
|
+
const { options } = parseArgs(args);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
format: (options.format as OutputFormat) || 'table',
|
|
38
|
+
scanDir: options.scan as string | undefined,
|
|
39
|
+
ci: !!options.ci,
|
|
40
|
+
threshold: options.threshold ? parseInt(options.threshold as string, 10) : 80,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load segments from the project
|
|
46
|
+
*/
|
|
47
|
+
async function loadSegments(): Promise<Array<{ path: string; segment: any }>> {
|
|
48
|
+
const configResult = await loadConfig();
|
|
49
|
+
|
|
50
|
+
if (!configResult) {
|
|
51
|
+
throw new Error('No fragments.config.ts found. Run this command from a Fragments project.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { config } = configResult;
|
|
55
|
+
|
|
56
|
+
// Try to load compiled segments
|
|
57
|
+
const segmentsPath = path.resolve(process.cwd(), config.outFile || `${BRAND.dataDir}/${BRAND.outFile}`);
|
|
58
|
+
|
|
59
|
+
if (fs.existsSync(segmentsPath)) {
|
|
60
|
+
const data = JSON.parse(fs.readFileSync(segmentsPath, 'utf-8'));
|
|
61
|
+
return Object.entries(data.segments || {}).map(([name, segment]: [string, any]) => ({
|
|
62
|
+
path: segment.filePath || name,
|
|
63
|
+
segment: {
|
|
64
|
+
meta: segment.meta || { name, description: '', category: '' },
|
|
65
|
+
usage: segment.usage || { when: [], whenNot: [] },
|
|
66
|
+
props: segment.props || {},
|
|
67
|
+
variants: segment.variants || [],
|
|
68
|
+
relations: segment.relations,
|
|
69
|
+
contract: segment.contract,
|
|
70
|
+
_generated: segment._generated,
|
|
71
|
+
component: () => null, // Placeholder
|
|
72
|
+
},
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Fallback: return empty if no compiled segments
|
|
77
|
+
console.log(pc.yellow(`No compiled segments found. Run \`${BRAND.cliCommand} build\` first for complete analysis.`));
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatGrade(grade: string): string {
|
|
82
|
+
switch (grade) {
|
|
83
|
+
case 'A':
|
|
84
|
+
return pc.green(pc.bold(grade));
|
|
85
|
+
case 'B':
|
|
86
|
+
return pc.cyan(grade);
|
|
87
|
+
case 'C':
|
|
88
|
+
return pc.yellow(grade);
|
|
89
|
+
case 'D':
|
|
90
|
+
return pc.magenta(grade);
|
|
91
|
+
case 'F':
|
|
92
|
+
return pc.red(pc.bold(grade));
|
|
93
|
+
default:
|
|
94
|
+
return grade;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatTableOutput(report: HealthReport): void {
|
|
99
|
+
console.log(pc.cyan(`\n${BRAND.name} Health Report\n`));
|
|
100
|
+
console.log(pc.dim(`Generated: ${new Date(report.generatedAt).toLocaleString()}\n`));
|
|
101
|
+
|
|
102
|
+
// Overall score
|
|
103
|
+
const scoreColor = report.overallScore >= 80 ? pc.green : report.overallScore >= 60 ? pc.yellow : pc.red;
|
|
104
|
+
console.log(` ${pc.bold('Overall Score:')} ${scoreColor(pc.bold(`${report.overallScore}%`))}\n`);
|
|
105
|
+
|
|
106
|
+
// Grade summary
|
|
107
|
+
console.log(pc.bold(' Category Grades:\n'));
|
|
108
|
+
console.log(` Usage: ${formatGrade(report.grades.usage)} (${report.usage.adoptionRate}% adoption)`);
|
|
109
|
+
console.log(` Style Drift: ${formatGrade(report.grades.drift)} (${report.styleDrift.averageCompliance}% compliance)`);
|
|
110
|
+
console.log(` Documentation: ${formatGrade(report.grades.documentation)} (${report.documentation.documentationRate}% documented)`);
|
|
111
|
+
console.log(` Token Usage: ${formatGrade(report.grades.tokens)} (${report.tokens.tokenizationRate}% tokenized)`);
|
|
112
|
+
console.log('');
|
|
113
|
+
|
|
114
|
+
// Usage section
|
|
115
|
+
printHeader('Usage Statistics');
|
|
116
|
+
printKV('Total Components', report.usage.totalComponents.toString());
|
|
117
|
+
printKV('Used Components', report.usage.usedComponents.toString());
|
|
118
|
+
printKV('Adoption Rate', formatScore(report.usage.adoptionRate));
|
|
119
|
+
|
|
120
|
+
if (report.usage.mostUsed.length > 0) {
|
|
121
|
+
console.log(pc.dim('\n Most Used:'));
|
|
122
|
+
for (const item of report.usage.mostUsed.slice(0, 5)) {
|
|
123
|
+
console.log(` ${pc.cyan(item.component)}: ${item.count} uses`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (report.usage.unusedComponents.length > 0 && report.usage.unusedComponents.length <= 5) {
|
|
128
|
+
console.log(pc.dim('\n Unused:'));
|
|
129
|
+
for (const name of report.usage.unusedComponents) {
|
|
130
|
+
printWarning(name);
|
|
131
|
+
}
|
|
132
|
+
} else if (report.usage.unusedComponents.length > 5) {
|
|
133
|
+
printWarning(`${report.usage.unusedComponents.length} unused components`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Style Drift section
|
|
137
|
+
printHeader('Style Drift Analysis');
|
|
138
|
+
printKV('Components with Drift', report.styleDrift.componentsWithDrift.toString());
|
|
139
|
+
printKV('Total Drifts', report.styleDrift.totalDrifts.toString());
|
|
140
|
+
printKV('Average Compliance', formatScore(report.styleDrift.averageCompliance));
|
|
141
|
+
|
|
142
|
+
if (report.styleDrift.bySeverity.high > 0) {
|
|
143
|
+
console.log('');
|
|
144
|
+
printWarning(`${report.styleDrift.bySeverity.high} high-severity drift(s) - affects brand identity`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (report.styleDrift.worstOffenders.length > 0) {
|
|
148
|
+
console.log(pc.dim('\n Needs Attention:'));
|
|
149
|
+
for (const item of report.styleDrift.worstOffenders.slice(0, 3)) {
|
|
150
|
+
console.log(` ${pc.red('!')} ${item.component} (${item.variant}): ${item.driftCount} drifts`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Documentation section
|
|
155
|
+
printHeader('Documentation Coverage');
|
|
156
|
+
printKV('Documented', `${report.documentation.documented} components`);
|
|
157
|
+
printKV('Coverage', formatScore(report.documentation.documentationRate));
|
|
158
|
+
|
|
159
|
+
if (report.documentation.missingDescriptions.length > 0) {
|
|
160
|
+
printWarning(`${report.documentation.missingDescriptions.length} missing descriptions`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (report.documentation.missingUsage.length > 0) {
|
|
164
|
+
printWarning(`${report.documentation.missingUsage.length} missing usage guidelines`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Token section
|
|
168
|
+
printHeader('Design Token Usage');
|
|
169
|
+
printKV('Token Compliance', formatScore(report.tokens.complianceScore));
|
|
170
|
+
printKV('Tokenized Values', report.tokens.tokenizedValues.toString());
|
|
171
|
+
printKV('Hardcoded Values', report.tokens.hardcodedValues.toString(), report.tokens.hardcodedValues > 0 ? pc.yellow : undefined);
|
|
172
|
+
|
|
173
|
+
// Recommendations
|
|
174
|
+
if (report.recommendations.length > 0) {
|
|
175
|
+
printHeader('Recommendations');
|
|
176
|
+
console.log('');
|
|
177
|
+
for (const rec of report.recommendations) {
|
|
178
|
+
console.log(` ${pc.yellow('→')} ${rec}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log('');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function formatJsonOutput(report: HealthReport): void {
|
|
186
|
+
console.log(JSON.stringify(report, null, 2));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function formatMarkdownOutput(report: HealthReport): void {
|
|
190
|
+
console.log(formatHealthReportAsMarkdown(report));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Run the health command
|
|
195
|
+
*/
|
|
196
|
+
export async function healthCommand(args: string[]): Promise<void> {
|
|
197
|
+
const options = parseOptions(args);
|
|
198
|
+
|
|
199
|
+
const spinner = createSpinner('Analyzing design system health...');
|
|
200
|
+
spinner.start();
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
// Load segments
|
|
204
|
+
spinner.update('Loading segments...');
|
|
205
|
+
const segments = await loadSegments();
|
|
206
|
+
|
|
207
|
+
if (segments.length === 0) {
|
|
208
|
+
spinner.stop(false, 'No segments found');
|
|
209
|
+
console.log(pc.yellow(`\nNo components to analyze. Create segment files or run \`${BRAND.cliCommand} build\`.\n`));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Run usage scan if directory provided
|
|
214
|
+
let usageScan;
|
|
215
|
+
if (options.scanDir) {
|
|
216
|
+
spinner.update(`Scanning ${options.scanDir} for component usage...`);
|
|
217
|
+
usageScan = await scanForUsages({ directory: options.scanDir });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Generate report
|
|
221
|
+
spinner.update('Generating health report...');
|
|
222
|
+
const report = generateHealthReport({
|
|
223
|
+
segments,
|
|
224
|
+
usageScan,
|
|
225
|
+
// Note: Drift reports would require running the viewer and extracting styles
|
|
226
|
+
// For CLI, we focus on what can be statically analyzed
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
spinner.stop(true, 'Health analysis complete');
|
|
230
|
+
|
|
231
|
+
// Output
|
|
232
|
+
switch (options.format) {
|
|
233
|
+
case 'json':
|
|
234
|
+
formatJsonOutput(report);
|
|
235
|
+
break;
|
|
236
|
+
case 'markdown':
|
|
237
|
+
formatMarkdownOutput(report);
|
|
238
|
+
break;
|
|
239
|
+
default:
|
|
240
|
+
formatTableOutput(report);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// CI mode: exit with error if below threshold
|
|
244
|
+
if (options.ci) {
|
|
245
|
+
if (report.overallScore < options.threshold) {
|
|
246
|
+
console.log(pc.red(`\nCI Check Failed: Score ${report.overallScore}% is below threshold ${options.threshold}%\n`));
|
|
247
|
+
process.exit(1);
|
|
248
|
+
} else {
|
|
249
|
+
console.log(pc.green(`\nCI Check Passed: Score ${report.overallScore}% meets threshold ${options.threshold}%\n`));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
spinner.stop(false, 'Health analysis failed');
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Command Router
|
|
3
|
+
* Routes subcommands to their respective handlers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import pc from 'picocolors';
|
|
7
|
+
import { scanCommand } from './scan.js';
|
|
8
|
+
import { healthCommand } from './health.js';
|
|
9
|
+
|
|
10
|
+
export type CommandName = 'scan' | 'health';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Run a CLI command with the given arguments
|
|
14
|
+
*/
|
|
15
|
+
export async function runCommand(command: CommandName, args: string[]): Promise<void> {
|
|
16
|
+
try {
|
|
17
|
+
switch (command) {
|
|
18
|
+
case 'scan':
|
|
19
|
+
await scanCommand(args);
|
|
20
|
+
break;
|
|
21
|
+
case 'health':
|
|
22
|
+
await healthCommand(args);
|
|
23
|
+
break;
|
|
24
|
+
default:
|
|
25
|
+
console.error(pc.red(`Unknown command: ${command}`));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
30
|
+
console.error(pc.red(`\nError: ${message}\n`));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan Command
|
|
3
|
+
* Scans a codebase for component usage
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import pc from 'picocolors';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { scanForUsages, type FullScanResult, type UsageScanResult } from '../intelligence/usageScanner.js';
|
|
9
|
+
import { parseArgs, createSpinner, formatTable, formatDuration, printHeader, printKV } from './utils.js';
|
|
10
|
+
|
|
11
|
+
type OutputFormat = 'table' | 'json';
|
|
12
|
+
|
|
13
|
+
interface ScanCommandOptions {
|
|
14
|
+
format: OutputFormat;
|
|
15
|
+
component?: string;
|
|
16
|
+
verbose: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseOptions(args: string[]): { directory: string; options: ScanCommandOptions } {
|
|
20
|
+
const { positional, options } = parseArgs(args);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
directory: positional[0] || '.',
|
|
24
|
+
options: {
|
|
25
|
+
format: (options.format as OutputFormat) || 'table',
|
|
26
|
+
component: options.component as string | undefined,
|
|
27
|
+
verbose: !!options.verbose || !!options.v,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatTableOutput(result: FullScanResult, options: ScanCommandOptions): void {
|
|
33
|
+
const { results, summary } = result;
|
|
34
|
+
|
|
35
|
+
printHeader('Component Usage Scan');
|
|
36
|
+
printKV('Directory scanned', path.resolve(result.results[0]?.usages[0]?.file || '.').split(path.sep).slice(0, -1).join(path.sep) || '.');
|
|
37
|
+
printKV('Files scanned', summary.totalFiles.toString());
|
|
38
|
+
printKV('Files with usage', summary.filesWithUsage.toString());
|
|
39
|
+
printKV('Scan time', formatDuration(summary.scanTimeMs));
|
|
40
|
+
console.log('');
|
|
41
|
+
|
|
42
|
+
if (results.length === 0) {
|
|
43
|
+
console.log(pc.yellow(' No component usages found.\n'));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Summary table
|
|
48
|
+
const headers = ['Component', 'Files', 'Total Uses'];
|
|
49
|
+
const rows = results.map((r) => [
|
|
50
|
+
r.component,
|
|
51
|
+
String(r.usages.length),
|
|
52
|
+
String(r.totalUsages),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
console.log(formatTable(headers, rows));
|
|
56
|
+
console.log('');
|
|
57
|
+
|
|
58
|
+
// Verbose: show file details
|
|
59
|
+
if (options.verbose) {
|
|
60
|
+
console.log(pc.bold('\nDetailed Usage:\n'));
|
|
61
|
+
|
|
62
|
+
for (const r of results) {
|
|
63
|
+
console.log(pc.cyan(` ${r.component}`));
|
|
64
|
+
for (const usage of r.usages) {
|
|
65
|
+
const relativePath = path.relative(process.cwd(), usage.file);
|
|
66
|
+
const importIcon = usage.importType === 'named' ? '{}' : usage.importType === 'default' ? '=>' : '*';
|
|
67
|
+
console.log(pc.dim(` ${importIcon} ${relativePath}:${usage.line} (${usage.usageCount} uses)`));
|
|
68
|
+
}
|
|
69
|
+
console.log('');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Summary footer
|
|
74
|
+
console.log(pc.dim('─'.repeat(50)));
|
|
75
|
+
console.log(` ${pc.bold(String(summary.totalComponents))} components used ${pc.bold(String(summary.totalUsages))} times across ${pc.bold(String(summary.filesWithUsage))} files`);
|
|
76
|
+
console.log('');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatJsonOutput(result: FullScanResult): void {
|
|
80
|
+
const output = {
|
|
81
|
+
summary: result.summary,
|
|
82
|
+
components: result.results.map((r) => ({
|
|
83
|
+
name: r.component,
|
|
84
|
+
totalUsages: r.totalUsages,
|
|
85
|
+
fileCount: r.usages.length,
|
|
86
|
+
usages: r.usages.map((u) => ({
|
|
87
|
+
file: path.relative(process.cwd(), u.file),
|
|
88
|
+
line: u.line,
|
|
89
|
+
importType: u.importType,
|
|
90
|
+
usageCount: u.usageCount,
|
|
91
|
+
})),
|
|
92
|
+
})),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
console.log(JSON.stringify(output, null, 2));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Run the scan command
|
|
100
|
+
*/
|
|
101
|
+
export async function scanCommand(args: string[]): Promise<void> {
|
|
102
|
+
const { directory, options } = parseOptions(args);
|
|
103
|
+
|
|
104
|
+
const spinner = createSpinner(`Scanning ${directory} for component usage...`);
|
|
105
|
+
spinner.start();
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const result = await scanForUsages({
|
|
109
|
+
directory,
|
|
110
|
+
components: options.component ? [options.component] : undefined,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
spinner.stop(true, `Scanned ${result.summary.totalFiles} files in ${formatDuration(result.summary.scanTimeMs)}`);
|
|
114
|
+
|
|
115
|
+
if (options.format === 'json') {
|
|
116
|
+
formatJsonOutput(result);
|
|
117
|
+
} else {
|
|
118
|
+
formatTableOutput(result, options);
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
spinner.stop(false, 'Scan failed');
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI Utilities
|
|
3
|
+
* Common helpers for CLI commands
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import pc from 'picocolors';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse command line arguments into options object
|
|
10
|
+
*/
|
|
11
|
+
export function parseArgs(args: string[]): {
|
|
12
|
+
positional: string[];
|
|
13
|
+
options: Record<string, string | boolean>;
|
|
14
|
+
} {
|
|
15
|
+
const positional: string[] = [];
|
|
16
|
+
const options: Record<string, string | boolean> = {};
|
|
17
|
+
|
|
18
|
+
for (const arg of args) {
|
|
19
|
+
if (arg.startsWith('--')) {
|
|
20
|
+
const [key, value] = arg.slice(2).split('=');
|
|
21
|
+
options[key] = value ?? true;
|
|
22
|
+
} else if (arg.startsWith('-')) {
|
|
23
|
+
const key = arg.slice(1);
|
|
24
|
+
options[key] = true;
|
|
25
|
+
} else {
|
|
26
|
+
positional.push(arg);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { positional, options };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Spinner for async operations
|
|
35
|
+
*/
|
|
36
|
+
export function createSpinner(message: string): {
|
|
37
|
+
start: () => void;
|
|
38
|
+
stop: (success?: boolean, finalMessage?: string) => void;
|
|
39
|
+
update: (message: string) => void;
|
|
40
|
+
} {
|
|
41
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
42
|
+
let frameIndex = 0;
|
|
43
|
+
let interval: NodeJS.Timeout | null = null;
|
|
44
|
+
let currentMessage = message;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
start() {
|
|
48
|
+
process.stdout.write('\n');
|
|
49
|
+
interval = setInterval(() => {
|
|
50
|
+
process.stdout.write(`\r${pc.cyan(frames[frameIndex])} ${currentMessage}`);
|
|
51
|
+
frameIndex = (frameIndex + 1) % frames.length;
|
|
52
|
+
}, 80);
|
|
53
|
+
},
|
|
54
|
+
stop(success = true, finalMessage?: string) {
|
|
55
|
+
if (interval) {
|
|
56
|
+
clearInterval(interval);
|
|
57
|
+
interval = null;
|
|
58
|
+
}
|
|
59
|
+
const icon = success ? pc.green('✓') : pc.red('✗');
|
|
60
|
+
process.stdout.write(`\r${icon} ${finalMessage || currentMessage}\n`);
|
|
61
|
+
},
|
|
62
|
+
update(message: string) {
|
|
63
|
+
currentMessage = message;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Format a table for console output
|
|
70
|
+
*/
|
|
71
|
+
export function formatTable(
|
|
72
|
+
headers: string[],
|
|
73
|
+
rows: string[][],
|
|
74
|
+
options: { padding?: number; maxWidth?: number } = {}
|
|
75
|
+
): string {
|
|
76
|
+
const { padding = 2, maxWidth = 50 } = options;
|
|
77
|
+
|
|
78
|
+
// Calculate column widths
|
|
79
|
+
const widths = headers.map((h, i) => {
|
|
80
|
+
const columnValues = [h, ...rows.map((r) => r[i] || '')];
|
|
81
|
+
return Math.min(maxWidth, Math.max(...columnValues.map((v) => v.length)));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Format header
|
|
85
|
+
const headerLine = headers
|
|
86
|
+
.map((h, i) => h.padEnd(widths[i] + padding))
|
|
87
|
+
.join('');
|
|
88
|
+
const separator = widths.map((w) => '─'.repeat(w + padding)).join('');
|
|
89
|
+
|
|
90
|
+
// Format rows
|
|
91
|
+
const formattedRows = rows.map((row) =>
|
|
92
|
+
row.map((cell, i) => (cell || '').padEnd(widths[i] + padding)).join('')
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
pc.bold(headerLine),
|
|
97
|
+
pc.dim(separator),
|
|
98
|
+
...formattedRows,
|
|
99
|
+
].join('\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Format bytes as human-readable size
|
|
104
|
+
*/
|
|
105
|
+
export function formatBytes(bytes: number): string {
|
|
106
|
+
if (bytes === 0) return '0 B';
|
|
107
|
+
const k = 1024;
|
|
108
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
109
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
110
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Format duration in milliseconds
|
|
115
|
+
*/
|
|
116
|
+
export function formatDuration(ms: number): string {
|
|
117
|
+
if (ms < 1000) return `${ms}ms`;
|
|
118
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
119
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Format a score with color based on thresholds
|
|
124
|
+
*/
|
|
125
|
+
export function formatScore(score: number, thresholds = { good: 80, warn: 50 }): string {
|
|
126
|
+
const formatted = `${score}%`;
|
|
127
|
+
if (score >= thresholds.good) return pc.green(formatted);
|
|
128
|
+
if (score >= thresholds.warn) return pc.yellow(formatted);
|
|
129
|
+
return pc.red(formatted);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Print a section header
|
|
134
|
+
*/
|
|
135
|
+
export function printHeader(title: string): void {
|
|
136
|
+
console.log(`\n${pc.bold(pc.cyan(title))}`);
|
|
137
|
+
console.log(pc.dim('─'.repeat(title.length + 4)));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Print a key-value pair
|
|
142
|
+
*/
|
|
143
|
+
export function printKV(key: string, value: string | number, color?: (s: string) => string): void {
|
|
144
|
+
const valueStr = String(value);
|
|
145
|
+
console.log(` ${pc.dim(key + ':')} ${color ? color(valueStr) : valueStr}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Print a warning message
|
|
150
|
+
*/
|
|
151
|
+
export function printWarning(message: string): void {
|
|
152
|
+
console.log(pc.yellow(` ⚠ ${message}`));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Print an error message
|
|
157
|
+
*/
|
|
158
|
+
export function printError(message: string): void {
|
|
159
|
+
console.log(pc.red(` ✗ ${message}`));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Print a success message
|
|
164
|
+
*/
|
|
165
|
+
export function printSuccess(message: string): void {
|
|
166
|
+
console.log(pc.green(` ✓ ${message}`));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Format a list for bullet points
|
|
171
|
+
*/
|
|
172
|
+
export function formatList(items: string[], bullet = '•'): string {
|
|
173
|
+
return items.map((item) => ` ${pc.dim(bullet)} ${item}`).join('\n');
|
|
174
|
+
}
|