@catchdrift/cli 0.1.8 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catchdrift/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "CLI for Drift — install, check, and manage design system coverage for any React app.",
5
5
  "keywords": [
6
6
  "design-system",
@@ -130,23 +130,12 @@ export async function init(argv) {
130
130
  }
131
131
 
132
132
  // ── Step 3a: Figma ───────────────────────────────────────────────────────────
133
- let figmaFileKey, figmaToken, figmaWIPPages
134
- if (sources.includes('figma')) {
135
- // Accept full URL or raw key
136
- const figmaInput = await p.text({
137
- message: 'Paste your Figma file URL (or just the file key)',
138
- placeholder: 'https://www.figma.com/design/ABC123.../My-Design-File',
139
- hint: 'Open your Figma file in a browser and copy the full URL from the address bar',
140
- validate: v => (!v?.trim() ? 'Required' : undefined),
141
- })
142
- if (p.isCancel(figmaInput)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
143
- figmaFileKey = extractFigmaFileKey(figmaInput.trim())
144
- if (!figmaFileKey) {
145
- p.log.warn(`Could not extract a file key from "${figmaInput.trim()}". Using it as-is.`)
146
- figmaFileKey = figmaInput.trim()
147
- }
133
+ // figmaFiles: [{ key: string, wipPages?: string[] }]
134
+ let figmaToken
135
+ const figmaFiles = []
148
136
 
149
- // Token — show exact steps + required scopes
137
+ if (sources.includes('figma')) {
138
+ // Token — ask once, reused for all files
150
139
  console.log('')
151
140
  p.log.step('Create a Figma access token — takes about 60 seconds:')
152
141
  console.log(`
@@ -169,24 +158,56 @@ export async function init(argv) {
169
158
  if (p.isCancel(figmaToken)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
170
159
  figmaToken = figmaToken?.trim() || undefined
171
160
 
172
- // Validate token + fetch pages in one call
173
- if (figmaToken && figmaFileKey) {
174
- spinner.start('Connecting to Figma...')
175
- const pages = await fetchFigmaPages(figmaFileKey, figmaToken)
176
- spinner.stop(pages
177
- ? pc.green(`Connected ✓ Found ${pages.length} pages`)
178
- : pc.yellow('Could not reach Figma — check your token scopes. You can re-run `npx catchdrift init` to retry.')
179
- )
180
-
181
- if (pages?.length) {
182
- const selected = await p.multiselect({
183
- message: 'Which pages hold in-progress / not-yet-ready components? (drafts — won\'t be added to registry)',
184
- options: pages.map(name => ({ value: name, label: name })),
185
- required: false,
186
- })
187
- if (p.isCancel(selected)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
188
- figmaWIPPages = Array.isArray(selected) && selected.length ? selected : undefined
161
+ // Loop: add one file at a time
162
+ let addingFiles = true
163
+ while (addingFiles) {
164
+ const fileLabel = figmaFiles.length === 0 ? 'Paste your Figma file URL (or just the file key)' : 'Add another Figma file URL (or key)'
165
+ const figmaInput = await p.text({
166
+ message: fileLabel,
167
+ placeholder: 'https://www.figma.com/design/ABC123.../My-Design-File',
168
+ hint: figmaFiles.length === 0
169
+ ? 'Open your Figma file in a browser and copy the full URL from the address bar'
170
+ : 'Add files for each area where DS components live (e.g. Core DS, Icons, Patterns)',
171
+ validate: v => (!v?.trim() ? 'Required' : undefined),
172
+ })
173
+ if (p.isCancel(figmaInput)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
174
+
175
+ let fileKey = extractFigmaFileKey(figmaInput.trim())
176
+ if (!fileKey) {
177
+ p.log.warn(`Could not extract a file key — using as-is.`)
178
+ fileKey = figmaInput.trim()
189
179
  }
180
+
181
+ // Fetch pages for this file
182
+ let wipPages
183
+ if (figmaToken) {
184
+ spinner.start('Connecting to Figma...')
185
+ const pages = await fetchFigmaPages(fileKey, figmaToken)
186
+ spinner.stop(pages
187
+ ? pc.green(`Connected ✓ Found ${pages.length} pages`)
188
+ : pc.yellow('Could not reach Figma — skipping page selection for this file.')
189
+ )
190
+
191
+ if (pages?.length) {
192
+ const selected = await p.multiselect({
193
+ message: 'Which pages in this file hold in-progress / not-yet-ready components?',
194
+ options: pages.map(name => ({ value: name, label: name })),
195
+ required: false,
196
+ })
197
+ if (p.isCancel(selected)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
198
+ wipPages = Array.isArray(selected) && selected.length ? selected : undefined
199
+ }
200
+ }
201
+
202
+ figmaFiles.push({ key: fileKey, wipPages })
203
+
204
+ // Ask whether to add another
205
+ const another = await p.confirm({
206
+ message: `${figmaFiles.length} file${figmaFiles.length > 1 ? 's' : ''} added. Add another Figma file?`,
207
+ initialValue: false,
208
+ })
209
+ if (p.isCancel(another)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
210
+ addingFiles = another
190
211
  }
191
212
  }
192
213
 
@@ -269,8 +290,7 @@ export async function init(argv) {
269
290
  writeDriftConfig(cwd, {
270
291
  storybookUrl: storybookUrl || (storybook.found ? storybook.url : undefined),
271
292
  chromaticUrl,
272
- figmaFileKey,
273
- figmaWIPPages,
293
+ figmaFiles: figmaFiles.length ? figmaFiles : undefined,
274
294
  dsPackages,
275
295
  threshold: Number(threshold) || 80,
276
296
  components,
@@ -283,7 +303,7 @@ export async function init(argv) {
283
303
  tools: Array.isArray(aiToolsSelected) ? aiToolsSelected : [],
284
304
  components,
285
305
  storybookUrl: storybookUrl || '',
286
- figmaFileKey,
306
+ figmaFiles: figmaFiles.length ? figmaFiles : undefined,
287
307
  })
288
308
  spinner.stop(`Written: ${rulesFiles.join(', ')}`)
289
309
 
@@ -355,7 +375,7 @@ ${pc.dim('Docs: https://catchdrift.ai · Issues: https://github.com/dyoon92/de
355
375
  console.log('')
356
376
  }
357
377
 
358
- if (figmaFileKey) {
378
+ if (figmaFiles.length > 0) {
359
379
  console.log(`${pc.blue('Tip:')} Open the Drift overlay (press D), go to Settings, and paste your Figma personal access token to enable Figma component sync.`)
360
380
  console.log('')
361
381
  }
@@ -10,21 +10,38 @@ import { findAppEntry } from './detect.mjs'
10
10
 
11
11
  // ── drift.config.ts ───────────────────────────────────────────────────────────
12
12
 
13
- export function writeDriftConfig(cwd, { storybookUrl, chromaticUrl, figmaFileKey, figmaWIPPages, dsPackages, threshold, components }) {
13
+ export function writeDriftConfig(cwd, { storybookUrl, chromaticUrl, figmaFiles, dsPackages, threshold, components }) {
14
14
  const registry = buildComponentRegistry(components)
15
15
 
16
16
  const dsPackagesLine = dsPackages?.length
17
17
  ? ` dsPackages: [${dsPackages.map(p => `'${p}'`).join(', ')}],`
18
18
  : null
19
19
 
20
+ // Build figmaFiles block — single file gets a compact shape, multiple get an array
21
+ let figmaFilesBlock = null
22
+ if (figmaFiles?.length === 1) {
23
+ const f = figmaFiles[0]
24
+ figmaFilesBlock = ` figmaFileKey: '${f.key}',`
25
+ if (f.wipPages?.length) {
26
+ figmaFilesBlock += `\n figmaWIPPages: [${f.wipPages.map(p => `'${p}'`).join(', ')}], // components on these pages are drafts — not added to registry`
27
+ }
28
+ } else if (figmaFiles?.length > 1) {
29
+ const entries = figmaFiles.map(f => {
30
+ const wipLine = f.wipPages?.length
31
+ ? `, wipPages: [${f.wipPages.map(p => `'${p}'`).join(', ')}]`
32
+ : ''
33
+ return ` { key: '${f.key}'${wipLine} },`
34
+ }).join('\n')
35
+ figmaFilesBlock = ` figmaFiles: [\n${entries}\n ],`
36
+ }
37
+
20
38
  const lines = [
21
39
  `import type { DesignDriftConfig } from '@catchdrift/overlay'`,
22
40
  ``,
23
41
  `const config: DesignDriftConfig = {`,
24
42
  storybookUrl ? ` storybookUrl: '${storybookUrl}',` : null,
25
43
  chromaticUrl ? ` chromaticUrl: '${chromaticUrl}',` : null,
26
- figmaFileKey ? ` figmaFileKey: '${figmaFileKey}',` : null,
27
- figmaWIPPages?.length ? ` figmaWIPPages: [${figmaWIPPages.map(p => `'${p}'`).join(', ')}], // components on these pages are drafts — not added to registry` : null,
44
+ figmaFilesBlock,
28
45
  ` threshold: ${threshold},`,
29
46
  dsPackagesLine,
30
47
  ` components: {`,
@@ -61,12 +78,15 @@ function buildComponentTable(components, storybookUrl) {
61
78
  ].join('\n')
62
79
  }
63
80
 
64
- function buildAIRulesContent(components, storybookUrl, figmaFileKey) {
81
+ function buildAIRulesContent(components, storybookUrl, figmaFiles) {
82
+ const figmaLines = figmaFiles?.length
83
+ ? figmaFiles.map(f => `- Figma: https://figma.com/design/${f.key}`).join('\n') + '\n'
84
+ : ''
65
85
  return `# Design System Rules
66
86
 
67
87
  ## Source of truth
68
88
  - Storybook: ${storybookUrl}
69
- ${figmaFileKey ? `- Figma: https://figma.com/design/${figmaFileKey}\n` : ''}- Tokens: src/tokens/variables.css — use CSS variables only
89
+ ${figmaLines}- Tokens: src/tokens/variables.css — use CSS variables only
70
90
 
71
91
  ## The #1 rule: never invent UI from scratch
72
92
  Only use components from the table below. If a component you need is missing:
@@ -110,8 +130,8 @@ ${buildComponentTable(components, storybookUrl)}
110
130
  `
111
131
  }
112
132
 
113
- export function writeAIRulesFiles(cwd, { tools, components, storybookUrl, figmaFileKey }) {
114
- const content = buildAIRulesContent(components, storybookUrl, figmaFileKey)
133
+ export function writeAIRulesFiles(cwd, { tools, components, storybookUrl, figmaFiles }) {
134
+ const content = buildAIRulesContent(components, storybookUrl, figmaFiles)
115
135
  const written = []
116
136
 
117
137
  const toolFileMap = {