@catchdrift/cli 0.1.8 → 0.1.10

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.10",
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(`
@@ -155,6 +144,7 @@ export async function init(argv) {
155
144
  3. Give it a name (e.g. "Drift") and set an expiry
156
145
  4. Enable these scopes:
157
146
  ${pc.green('✓')} File content → ${pc.bold('Read only')}
147
+ ${pc.green('✓')} Library content → ${pc.bold('Read only')}
158
148
  ${pc.green('✓')} File comments → ${pc.bold('Write')}
159
149
  ${pc.green('✓')} File variables → ${pc.bold('Read only')}
160
150
  5. Click ${pc.bold('Generate token')} and copy it — ${pc.yellow("you won't be able to see it again")}
@@ -169,24 +159,56 @@ export async function init(argv) {
169
159
  if (p.isCancel(figmaToken)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
170
160
  figmaToken = figmaToken?.trim() || undefined
171
161
 
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
162
+ // Loop: add one file at a time
163
+ let addingFiles = true
164
+ while (addingFiles) {
165
+ const fileLabel = figmaFiles.length === 0 ? 'Paste your Figma file URL (or just the file key)' : 'Add another Figma file URL (or key)'
166
+ const figmaInput = await p.text({
167
+ message: fileLabel,
168
+ placeholder: 'https://www.figma.com/design/ABC123.../My-Design-File',
169
+ hint: figmaFiles.length === 0
170
+ ? 'Open your Figma file in a browser and copy the full URL from the address bar'
171
+ : 'Add files for each area where DS components live (e.g. Core DS, Icons, Patterns)',
172
+ validate: v => (!v?.trim() ? 'Required' : undefined),
173
+ })
174
+ if (p.isCancel(figmaInput)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
175
+
176
+ let fileKey = extractFigmaFileKey(figmaInput.trim())
177
+ if (!fileKey) {
178
+ p.log.warn(`Could not extract a file key — using as-is.`)
179
+ fileKey = figmaInput.trim()
189
180
  }
181
+
182
+ // Fetch pages for this file
183
+ let wipPages
184
+ if (figmaToken) {
185
+ spinner.start('Connecting to Figma...')
186
+ const pages = await fetchFigmaPages(fileKey, figmaToken)
187
+ spinner.stop(pages
188
+ ? pc.green(`Connected ✓ Found ${pages.length} pages`)
189
+ : pc.yellow('Could not reach Figma — skipping page selection for this file.')
190
+ )
191
+
192
+ if (pages?.length) {
193
+ const selected = await p.multiselect({
194
+ message: 'Which pages in this file hold in-progress / not-yet-ready components?',
195
+ options: pages.map(name => ({ value: name, label: name })),
196
+ required: false,
197
+ })
198
+ if (p.isCancel(selected)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
199
+ wipPages = Array.isArray(selected) && selected.length ? selected : undefined
200
+ }
201
+ }
202
+
203
+ figmaFiles.push({ key: fileKey, wipPages })
204
+
205
+ // Ask whether to add another
206
+ const another = await p.confirm({
207
+ message: `${figmaFiles.length} file${figmaFiles.length > 1 ? 's' : ''} added. Add another Figma file?`,
208
+ initialValue: false,
209
+ })
210
+ if (p.isCancel(another)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
211
+ addingFiles = another
190
212
  }
191
213
  }
192
214
 
@@ -269,8 +291,7 @@ export async function init(argv) {
269
291
  writeDriftConfig(cwd, {
270
292
  storybookUrl: storybookUrl || (storybook.found ? storybook.url : undefined),
271
293
  chromaticUrl,
272
- figmaFileKey,
273
- figmaWIPPages,
294
+ figmaFiles: figmaFiles.length ? figmaFiles : undefined,
274
295
  dsPackages,
275
296
  threshold: Number(threshold) || 80,
276
297
  components,
@@ -283,7 +304,7 @@ export async function init(argv) {
283
304
  tools: Array.isArray(aiToolsSelected) ? aiToolsSelected : [],
284
305
  components,
285
306
  storybookUrl: storybookUrl || '',
286
- figmaFileKey,
307
+ figmaFiles: figmaFiles.length ? figmaFiles : undefined,
287
308
  })
288
309
  spinner.stop(`Written: ${rulesFiles.join(', ')}`)
289
310
 
@@ -355,7 +376,7 @@ ${pc.dim('Docs: https://catchdrift.ai · Issues: https://github.com/dyoon92/de
355
376
  console.log('')
356
377
  }
357
378
 
358
- if (figmaFileKey) {
379
+ if (figmaFiles.length > 0) {
359
380
  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
381
  console.log('')
361
382
  }
@@ -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 = {