@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.
Files changed (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,172 @@
1
+ import { createHash } from "node:crypto";
2
+ import { BRAND } from "../core/index.js";
3
+
4
+ /**
5
+ * Base error class for all service errors.
6
+ * Includes error code and optional suggestion for resolution.
7
+ */
8
+ export class ServiceError extends Error {
9
+ constructor(
10
+ message: string,
11
+ public readonly code: string,
12
+ public readonly suggestion?: string
13
+ ) {
14
+ super(message);
15
+ this.name = `${BRAND.name}Error`;
16
+ }
17
+
18
+ /**
19
+ * Format error for CLI output
20
+ */
21
+ toCliString(): string {
22
+ let str = `Error: ${this.message}`;
23
+ if (this.suggestion) {
24
+ str += `\n Suggestion: ${this.suggestion}`;
25
+ }
26
+ return str;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Compute SHA-256 hash of a buffer
32
+ */
33
+ export function computeHash(data: Buffer): string {
34
+ return `sha256:${createHash("sha256").update(data).digest("hex")}`;
35
+ }
36
+
37
+ /**
38
+ * Simple timer utility for measuring operations
39
+ */
40
+ export class Timer {
41
+ private startTime: number;
42
+
43
+ constructor() {
44
+ this.startTime = performance.now();
45
+ }
46
+
47
+ /**
48
+ * Get elapsed time in milliseconds
49
+ */
50
+ elapsed(): number {
51
+ return Math.round(performance.now() - this.startTime);
52
+ }
53
+
54
+ /**
55
+ * Reset timer and return elapsed time
56
+ */
57
+ reset(): number {
58
+ const elapsed = this.elapsed();
59
+ this.startTime = performance.now();
60
+ return elapsed;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Create a deferred promise with external resolve/reject
66
+ */
67
+ export function createDeferred<T>(): {
68
+ promise: Promise<T>;
69
+ resolve: (value: T) => void;
70
+ reject: (error: Error) => void;
71
+ } {
72
+ let resolve!: (value: T) => void;
73
+ let reject!: (error: Error) => void;
74
+
75
+ const promise = new Promise<T>((res, rej) => {
76
+ resolve = res;
77
+ reject = rej;
78
+ });
79
+
80
+ return { promise, resolve, reject };
81
+ }
82
+
83
+ /**
84
+ * Sleep for a given number of milliseconds
85
+ */
86
+ export function sleep(ms: number): Promise<void> {
87
+ return new Promise((resolve) => setTimeout(resolve, ms));
88
+ }
89
+
90
+ /**
91
+ * Convert buffer to base64 data URL
92
+ */
93
+ export function bufferToBase64Url(
94
+ buffer: Buffer,
95
+ mimeType = "image/png"
96
+ ): string {
97
+ return `data:${mimeType};base64,${buffer.toString("base64")}`;
98
+ }
99
+
100
+ /**
101
+ * Convert base64 data URL to buffer
102
+ */
103
+ export function base64UrlToBuffer(dataUrl: string): Buffer {
104
+ const base64 = dataUrl.replace(/^data:[^;]+;base64,/, "");
105
+ return Buffer.from(base64, "base64");
106
+ }
107
+
108
+ /**
109
+ * Sanitize a string for use as a filename
110
+ */
111
+ export function sanitizeFilename(name: string): string {
112
+ return name
113
+ .replace(/[/\\?%*:|"<>]/g, "-")
114
+ .replace(/\s+/g, "-")
115
+ .replace(/-+/g, "-")
116
+ .replace(/^-|-$/g, "");
117
+ }
118
+
119
+ /**
120
+ * Build path for screenshot file
121
+ */
122
+ export function buildScreenshotPath(
123
+ component: string,
124
+ variant: string,
125
+ theme: string
126
+ ): string {
127
+ const safeComponent = sanitizeFilename(component);
128
+ const safeVariant = sanitizeFilename(variant);
129
+ return `${theme}/${safeComponent}/${safeVariant}.png`;
130
+ }
131
+
132
+ /**
133
+ * Format milliseconds for display
134
+ */
135
+ export function formatMs(ms: number): string {
136
+ if (ms < 1000) {
137
+ return `${ms}ms`;
138
+ }
139
+ return `${(ms / 1000).toFixed(1)}s`;
140
+ }
141
+
142
+ /**
143
+ * Retry an async operation with exponential backoff
144
+ */
145
+ export async function retry<T>(
146
+ fn: () => Promise<T>,
147
+ options: {
148
+ maxAttempts?: number;
149
+ initialDelayMs?: number;
150
+ maxDelayMs?: number;
151
+ } = {}
152
+ ): Promise<T> {
153
+ const { maxAttempts = 3, initialDelayMs = 100, maxDelayMs = 1000 } = options;
154
+
155
+ let lastError: Error | undefined;
156
+ let delay = initialDelayMs;
157
+
158
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
159
+ try {
160
+ return await fn();
161
+ } catch (error) {
162
+ lastError = error instanceof Error ? error : new Error(String(error));
163
+
164
+ if (attempt < maxAttempts) {
165
+ await sleep(delay);
166
+ delay = Math.min(delay * 2, maxDelayMs);
167
+ }
168
+ }
169
+ }
170
+
171
+ throw lastError;
172
+ }
package/src/setup.ts ADDED
@@ -0,0 +1,241 @@
1
+ import pc from 'picocolors';
2
+ import { BRAND } from './core/index.js';
3
+ import { loadConfig, discoverSegmentFiles } from './core/node.js';
4
+ import { buildSegments } from './build.js';
5
+ import {
6
+ detectStorybookConfig,
7
+ discoverStoryFiles as discoverStorybookFiles,
8
+ parseStoryFile,
9
+ convertToSegment,
10
+ } from './migrate/index.js';
11
+
12
+ export interface SetupOptions {
13
+ skipStorybook?: boolean;
14
+ skipFigma?: boolean;
15
+ skipBuild?: boolean;
16
+ silent?: boolean;
17
+ configPath?: string;
18
+ }
19
+
20
+ export interface SetupResult {
21
+ segmentFilesCreated: number;
22
+ segmentsBuilt: number;
23
+ figmaLinked: number;
24
+ errors: string[];
25
+ }
26
+
27
+ interface SegmentInfo {
28
+ name: string;
29
+ filePath: string;
30
+ hasFigma: boolean;
31
+ }
32
+
33
+ /**
34
+ * Check if segments.json exists and is newer than all segment files.
35
+ */
36
+ async function isSegmentsJsonStale(configDir: string, outFile: string): Promise<{ stale: boolean; missing: boolean }> {
37
+ const fs = await import('node:fs/promises');
38
+ const path = await import('node:path');
39
+ const fg = await import('fast-glob');
40
+
41
+ const segmentsJsonPath = path.join(configDir, outFile);
42
+
43
+ try {
44
+ const segmentsJsonStat = await fs.stat(segmentsJsonPath);
45
+
46
+ // Find all segment files
47
+ const segmentFiles = await fg.default(`**/*${BRAND.fileExtension}`, {
48
+ cwd: configDir,
49
+ ignore: ['**/node_modules/**'],
50
+ absolute: true,
51
+ });
52
+
53
+ // Check if any segment file is newer than segments.json
54
+ for (const file of segmentFiles) {
55
+ const stat = await fs.stat(file);
56
+ if (stat.mtimeMs > segmentsJsonStat.mtimeMs) {
57
+ return { stale: true, missing: false };
58
+ }
59
+ }
60
+
61
+ return { stale: false, missing: false };
62
+ } catch {
63
+ // segments.json doesn't exist
64
+ return { stale: false, missing: true };
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Load segment files and check which ones have Figma links.
70
+ */
71
+ async function loadSegmentInfo(segmentFiles: Array<{ absolutePath: string; relativePath: string }>): Promise<SegmentInfo[]> {
72
+ const fs = await import('node:fs/promises');
73
+ const segments: SegmentInfo[] = [];
74
+
75
+ for (const file of segmentFiles) {
76
+ try {
77
+ const content = await fs.readFile(file.absolutePath, 'utf-8');
78
+ const nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/);
79
+ const hasFigma = /meta:\s*\{[^}]*figma:\s*['"]https?:/.test(content);
80
+
81
+ if (nameMatch) {
82
+ segments.push({
83
+ name: nameMatch[1],
84
+ filePath: file.absolutePath,
85
+ hasFigma,
86
+ });
87
+ }
88
+ } catch {
89
+ // Skip files that can't be read
90
+ }
91
+ }
92
+
93
+ return segments;
94
+ }
95
+
96
+ /**
97
+ * Run auto-setup for segments dev server.
98
+ *
99
+ * This function:
100
+ * 1. Checks for segment files, imports from Storybook if none found
101
+ * 2. Builds segments.json if missing or stale
102
+ * 3. Links Figma if configured but no segments are linked
103
+ */
104
+ export async function runSetup(options: SetupOptions = {}): Promise<SetupResult> {
105
+ const fs = await import('node:fs/promises');
106
+ const path = await import('node:path');
107
+
108
+ const result: SetupResult = {
109
+ segmentFilesCreated: 0,
110
+ segmentsBuilt: 0,
111
+ figmaLinked: 0,
112
+ errors: [],
113
+ };
114
+
115
+ const log = (msg: string) => {
116
+ if (!options.silent) console.log(msg);
117
+ };
118
+
119
+ try {
120
+ // Load config
121
+ const { config, configDir } = await loadConfig(options.configPath);
122
+
123
+ // Step 1: Check for segment files
124
+ log(pc.dim('Checking for fragment files...'));
125
+ let segmentFiles = await discoverSegmentFiles(config, configDir);
126
+
127
+ if (segmentFiles.length === 0 && !options.skipStorybook) {
128
+ // No fragment files - check for Storybook
129
+ log(pc.yellow('\n No fragment files found'));
130
+
131
+ const sbConfig = await detectStorybookConfig(configDir);
132
+ if (sbConfig) {
133
+ log(pc.dim(` Found Storybook at ${sbConfig.configPath}`));
134
+ log(pc.dim(' Converting stories to fragments...\n'));
135
+
136
+ // Discover and convert stories
137
+ const storyFiles = await discoverStorybookFiles(configDir, sbConfig.storyPatterns);
138
+
139
+ if (storyFiles.length > 0) {
140
+ let converted = 0;
141
+ for (const storyFile of storyFiles) {
142
+ try {
143
+ const parsed = await parseStoryFile(storyFile);
144
+ const segmentResult = convertToSegment(parsed);
145
+
146
+ // Create directory and write file
147
+ await fs.mkdir(path.dirname(segmentResult.outputFile), { recursive: true });
148
+ await fs.writeFile(segmentResult.outputFile, segmentResult.code);
149
+ converted++;
150
+ } catch {
151
+ // Skip files that can't be converted
152
+ }
153
+ }
154
+
155
+ result.segmentFilesCreated = converted;
156
+ log(pc.green(` Generated ${converted} fragment file(s)`));
157
+
158
+ // Refresh segment files list
159
+ segmentFiles = await discoverSegmentFiles(config, configDir);
160
+ }
161
+ } else {
162
+ log(pc.dim(' No Storybook config found'));
163
+ log(pc.dim(` Run ${pc.cyan(`${BRAND.cliCommand} add <ComponentName>`)} to create your first fragment`));
164
+ }
165
+ } else if (segmentFiles.length > 0) {
166
+ log(pc.green(` Found ${segmentFiles.length} fragment file(s)`));
167
+ }
168
+
169
+ // Step 2: Build fragments.json if needed
170
+ if (segmentFiles.length > 0 && !options.skipBuild) {
171
+ const outFile = config.outFile || BRAND.outFile;
172
+ const { stale, missing } = await isSegmentsJsonStale(configDir, outFile);
173
+
174
+ if (missing || stale) {
175
+ const reason = missing ? 'Building' : 'Rebuilding';
176
+ log(pc.dim(`\n${reason} ${BRAND.outFile}...`));
177
+
178
+ try {
179
+ const buildResult = await buildSegments(config, configDir);
180
+ result.segmentsBuilt = buildResult.segmentCount;
181
+
182
+ if (buildResult.errors.length > 0) {
183
+ for (const err of buildResult.errors) {
184
+ result.errors.push(`${err.file}: ${err.error}`);
185
+ }
186
+ }
187
+
188
+ log(pc.green(` Built ${buildResult.segmentCount} fragment(s)`));
189
+ } catch (error) {
190
+ result.errors.push(`Build failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
191
+ }
192
+ } else {
193
+ log(pc.dim(`\n ${BRAND.outFile} is up to date`));
194
+ }
195
+ }
196
+
197
+ // Step 3: Link Figma if configured
198
+ if (!options.skipFigma && config.figmaFile && process.env.FIGMA_ACCESS_TOKEN) {
199
+ const segments = await loadSegmentInfo(segmentFiles);
200
+ const linkedCount = segments.filter((s) => s.hasFigma).length;
201
+
202
+ if (linkedCount === 0 && segments.length > 0) {
203
+ log(pc.dim('\n Figma configured but no fragments linked'));
204
+ log(pc.dim(` Run ${pc.cyan(`${BRAND.cliCommand} link figma --auto`)} to auto-link components`));
205
+ // Note: We don't auto-link here because it requires API calls and user may want control
206
+ // But we inform them about the option
207
+ } else if (linkedCount > 0) {
208
+ log(pc.dim(`\n ${linkedCount}/${segments.length} fragment(s) linked to Figma`));
209
+ }
210
+ } else if (!options.skipFigma && config.figmaFile && !process.env.FIGMA_ACCESS_TOKEN) {
211
+ log(pc.dim('\n Figma file configured but FIGMA_ACCESS_TOKEN not set'));
212
+ }
213
+
214
+ } catch (error) {
215
+ result.errors.push(error instanceof Error ? error.message : 'Unknown error');
216
+ }
217
+
218
+ return result;
219
+ }
220
+
221
+ /**
222
+ * Print setup summary with user-friendly output.
223
+ */
224
+ export function printSetupSummary(result: SetupResult): void {
225
+ console.log();
226
+
227
+ if (result.segmentFilesCreated > 0) {
228
+ console.log(pc.green(` Imported ${result.segmentFilesCreated} segments from Storybook`));
229
+ }
230
+
231
+ if (result.segmentsBuilt > 0) {
232
+ console.log(pc.green(` Built ${result.segmentsBuilt} segments`));
233
+ }
234
+
235
+ if (result.errors.length > 0) {
236
+ console.log(pc.yellow('\n Warnings:'));
237
+ for (const error of result.errors) {
238
+ console.log(pc.dim(` ${error}`));
239
+ }
240
+ }
241
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Command Wrapper
3
+ *
4
+ * Provides error handling and common patterns for CLI commands.
5
+ */
6
+
7
+ import pc from 'picocolors';
8
+ import { BRAND } from '../core/index.js';
9
+ import { DevServerConnectionError, DevServerError } from './dev-server-client.js';
10
+
11
+ /**
12
+ * Options for command execution
13
+ */
14
+ export interface CommandOptions {
15
+ /** CI mode - output JSON and exit non-zero on failure */
16
+ ci?: boolean;
17
+ /** Port for dev server */
18
+ port?: number | string;
19
+ }
20
+
21
+ /**
22
+ * Format and print a dev server connection error with helpful suggestions
23
+ */
24
+ export function formatConnectionError(error: DevServerConnectionError, port: number | string): string {
25
+ return (
26
+ `Cannot connect to dev server at ${error.serverUrl}\n\n` +
27
+ `The command requires the dev server to be running.\n` +
28
+ `Start it with: ${pc.cyan(`${BRAND.cliCommand} dev`)}\n\n` +
29
+ `Alternatively, run with a different port:\n` +
30
+ ` ${pc.cyan(`${BRAND.cliCommand} <command> --port ${port}`)}`
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Handle errors in a command, formatting appropriately for CI vs interactive mode
36
+ */
37
+ export function handleCommandError(
38
+ error: unknown,
39
+ options: CommandOptions = {}
40
+ ): never {
41
+ const { ci = false, port = 6006 } = options;
42
+
43
+ if (error instanceof DevServerConnectionError) {
44
+ if (ci) {
45
+ console.log(JSON.stringify({ error: `Cannot connect to dev server at ${error.serverUrl}` }));
46
+ } else {
47
+ console.error(pc.red('Error:'), formatConnectionError(error, port));
48
+ }
49
+ } else if (error instanceof DevServerError) {
50
+ if (ci) {
51
+ console.log(JSON.stringify({ error: error.message }));
52
+ } else {
53
+ console.error(pc.red('Error:'), error.message);
54
+ }
55
+ } else {
56
+ const message = error instanceof Error ? error.message : 'Unknown error';
57
+ if (ci) {
58
+ console.log(JSON.stringify({ error: message }));
59
+ } else {
60
+ console.error(pc.red('Error:'), message);
61
+ }
62
+ }
63
+
64
+ process.exit(1);
65
+ }
66
+
67
+ /**
68
+ * Wrap a command function with standard error handling
69
+ */
70
+ export function withErrorHandling<T extends unknown[], R>(
71
+ fn: (...args: T) => Promise<R>,
72
+ options: CommandOptions = {}
73
+ ): (...args: T) => Promise<R> {
74
+ return async (...args: T): Promise<R> => {
75
+ try {
76
+ return await fn(...args);
77
+ } catch (error) {
78
+ handleCommandError(error, options);
79
+ }
80
+ };
81
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Dev Server Client
3
+ *
4
+ * HTTP client for communicating with the fragments dev server.
5
+ * Provides typed methods for all server endpoints.
6
+ */
7
+
8
+ import type {
9
+ ComplianceResult,
10
+ SegmentInfo,
11
+ ContextData,
12
+ ViolationItem,
13
+ A11yResult,
14
+ } from './types.js';
15
+
16
+ export interface DevServerClientOptions {
17
+ /** Base URL of the dev server */
18
+ baseUrl: string;
19
+ /** Request timeout in milliseconds */
20
+ timeout?: number;
21
+ }
22
+
23
+ /**
24
+ * Request body for compliance endpoint
25
+ */
26
+ export interface ComplianceRequest {
27
+ /** Component name */
28
+ component: string;
29
+ /** Variant name (optional) */
30
+ variant?: string;
31
+ /** Theme to use (default: "default") */
32
+ theme?: string;
33
+ }
34
+
35
+ /**
36
+ * Client for interacting with the fragments dev server
37
+ */
38
+ export class DevServerClient {
39
+ private baseUrl: string;
40
+ private timeout: number;
41
+
42
+ constructor(options: DevServerClientOptions) {
43
+ this.baseUrl = options.baseUrl.replace(/\/$/, ''); // Remove trailing slash
44
+ this.timeout = options.timeout ?? 30000;
45
+ }
46
+
47
+ /**
48
+ * Check if the dev server is reachable
49
+ */
50
+ async ping(): Promise<boolean> {
51
+ try {
52
+ const controller = new AbortController();
53
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
54
+
55
+ const response = await fetch(`${this.baseUrl}/segments/context?format=json`, {
56
+ signal: controller.signal,
57
+ });
58
+
59
+ clearTimeout(timeoutId);
60
+ return response.ok;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Get all segments from the context endpoint
68
+ */
69
+ async getSegments(): Promise<SegmentInfo[]> {
70
+ const response = await this.fetch('/segments/context?format=json');
71
+ const data = await response.json() as {
72
+ components: Record<string, { category?: string; description?: string; status?: string; figma?: string }>;
73
+ };
74
+
75
+ // Transform object to array of SegmentInfo
76
+ const components = data.components || {};
77
+ return Object.entries(components).map(([name, info]) => ({
78
+ name,
79
+ category: info.category || 'components',
80
+ description: info.description,
81
+ status: info.status,
82
+ figma: info.figma,
83
+ }));
84
+ }
85
+
86
+ /**
87
+ * Get compliance data for a component
88
+ */
89
+ async getCompliance(request: ComplianceRequest): Promise<ComplianceResult> {
90
+ const response = await this.fetch('/segments/compliance', {
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/json' },
93
+ body: JSON.stringify(request),
94
+ });
95
+
96
+ const data = await response.json() as ComplianceResult;
97
+ return data;
98
+ }
99
+
100
+ /**
101
+ * Get accessibility results for a component
102
+ */
103
+ async getA11y(component: string, variant?: string): Promise<A11yResult> {
104
+ const response = await this.fetch('/fragments/a11y', {
105
+ method: 'POST',
106
+ headers: { 'Content-Type': 'application/json' },
107
+ body: JSON.stringify({ component, variant }),
108
+ });
109
+
110
+ const data = await response.json() as A11yResult;
111
+ return data;
112
+ }
113
+
114
+ /**
115
+ * Internal fetch wrapper with error handling
116
+ */
117
+ private async fetch(path: string, options?: RequestInit): Promise<Response> {
118
+ const url = `${this.baseUrl}${path}`;
119
+
120
+ try {
121
+ const controller = new AbortController();
122
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
123
+
124
+ const response = await fetch(url, {
125
+ ...options,
126
+ signal: controller.signal,
127
+ });
128
+
129
+ clearTimeout(timeoutId);
130
+
131
+ if (!response.ok) {
132
+ const errorBody = await response.text().catch(() => '');
133
+ throw new DevServerError(
134
+ `Server returned ${response.status}: ${response.statusText}`,
135
+ response.status,
136
+ errorBody
137
+ );
138
+ }
139
+
140
+ return response;
141
+ } catch (error) {
142
+ if (error instanceof DevServerError) {
143
+ throw error;
144
+ }
145
+
146
+ // Handle connection errors
147
+ const errMsg = error instanceof Error && error.cause
148
+ ? String((error.cause as { code?: string }).code || error.message)
149
+ : error instanceof Error
150
+ ? error.message
151
+ : 'Unknown error';
152
+
153
+ if (errMsg.includes('ECONNREFUSED') || errMsg.includes('fetch failed')) {
154
+ throw new DevServerConnectionError(
155
+ `Cannot connect to dev server at ${this.baseUrl}`,
156
+ this.baseUrl
157
+ );
158
+ }
159
+
160
+ throw error;
161
+ }
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Error thrown when the dev server returns an error response
167
+ */
168
+ export class DevServerError extends Error {
169
+ constructor(
170
+ message: string,
171
+ public readonly statusCode: number,
172
+ public readonly body?: string
173
+ ) {
174
+ super(message);
175
+ this.name = 'DevServerError';
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Error thrown when we can't connect to the dev server
181
+ */
182
+ export class DevServerConnectionError extends Error {
183
+ constructor(
184
+ message: string,
185
+ public readonly serverUrl: string
186
+ ) {
187
+ super(message);
188
+ this.name = 'DevServerConnectionError';
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Create a dev server client with default options
194
+ */
195
+ export function createDevServerClient(port: number | string = 6006): DevServerClient {
196
+ return new DevServerClient({
197
+ baseUrl: `http://localhost:${port}`,
198
+ });
199
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared utilities for CLI commands
3
+ */
4
+
5
+ export * from './types.js';
6
+ export * from './dev-server-client.js';
7
+ export * from './command-wrapper.js';
8
+ export * from './segment-loader.js';