@catchdrift/cli 0.1.7 → 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.7",
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,39 +130,84 @@ export async function init(argv) {
130
130
  }
131
131
 
132
132
  // ── Step 3a: Figma ───────────────────────────────────────────────────────────
133
- let figmaFileKey, figmaToken, figmaWIPPages
133
+ // figmaFiles: [{ key: string, wipPages?: string[] }]
134
+ let figmaToken
135
+ const figmaFiles = []
136
+
134
137
  if (sources.includes('figma')) {
135
- figmaFileKey = await p.text({
136
- message: 'Figma file key',
137
- placeholder: 'Found in figma.com/design/THIS_KEY/... (paste just the key)',
138
- validate: v => (!v?.trim() ? 'Required — paste the key from your Figma URL' : undefined),
139
- })
140
- if (p.isCancel(figmaFileKey)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
141
- figmaFileKey = figmaFileKey?.trim() || undefined
138
+ // Token ask once, reused for all files
139
+ console.log('')
140
+ p.log.step('Create a Figma access token — takes about 60 seconds:')
141
+ console.log(`
142
+ 1. Open ${pc.cyan('figma.com')} → click your avatar (top-left) → ${pc.bold('Settings')}
143
+ 2. Go to the ${pc.bold('Security')} tab → click ${pc.bold('Generate new token')}
144
+ 3. Give it a name (e.g. "Drift") and set an expiry
145
+ 4. Enable these scopes:
146
+ ${pc.green('✓')} File content → ${pc.bold('Read only')}
147
+ ${pc.green('✓')} File comments → ${pc.bold('Write')}
148
+ ${pc.green('✓')} File variables → ${pc.bold('Read only')}
149
+ 5. Click ${pc.bold('Generate token')} and copy it — ${pc.yellow("you won't be able to see it again")}
150
+ `)
142
151
 
143
152
  figmaToken = await p.text({
144
- message: 'Figma personal access token',
145
- placeholder: 'figd_... (figma.com → Profile → Settings → Security → Personal access tokens)',
146
- hint: 'Used to fetch your real page list. Store in FIGMA_API_TOKEN env var — not committed to git.',
153
+ message: 'Paste your Figma token here',
154
+ placeholder: 'figd_...',
155
+ hint: 'Stored in your local FIGMA_API_TOKEN env var — never committed to git',
156
+ validate: v => (!v?.trim() ? 'Required — paste the token you just generated' : undefined),
147
157
  })
148
158
  if (p.isCancel(figmaToken)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
149
159
  figmaToken = figmaToken?.trim() || undefined
150
160
 
151
- // Fetch actual pages from Figma so the user picks from real names
152
- if (figmaToken && figmaFileKey) {
153
- spinner.start('Fetching pages from Figma...')
154
- const pages = await fetchFigmaPages(figmaFileKey, figmaToken)
155
- spinner.stop(pages ? `Found ${pages.length} pages` : 'Could not reach Figma — skipping page selection')
156
-
157
- if (pages?.length) {
158
- const selected = await p.multiselect({
159
- message: 'Which pages hold in-progress / not-yet-ready components? (drafts won\'t be added to registry)',
160
- options: pages.map(name => ({ value: name, label: name })),
161
- required: false,
162
- })
163
- if (p.isCancel(selected)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
164
- 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()
165
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
166
211
  }
167
212
  }
168
213
 
@@ -245,8 +290,7 @@ export async function init(argv) {
245
290
  writeDriftConfig(cwd, {
246
291
  storybookUrl: storybookUrl || (storybook.found ? storybook.url : undefined),
247
292
  chromaticUrl,
248
- figmaFileKey,
249
- figmaWIPPages,
293
+ figmaFiles: figmaFiles.length ? figmaFiles : undefined,
250
294
  dsPackages,
251
295
  threshold: Number(threshold) || 80,
252
296
  components,
@@ -259,7 +303,7 @@ export async function init(argv) {
259
303
  tools: Array.isArray(aiToolsSelected) ? aiToolsSelected : [],
260
304
  components,
261
305
  storybookUrl: storybookUrl || '',
262
- figmaFileKey,
306
+ figmaFiles: figmaFiles.length ? figmaFiles : undefined,
263
307
  })
264
308
  spinner.stop(`Written: ${rulesFiles.join(', ')}`)
265
309
 
@@ -331,7 +375,7 @@ ${pc.dim('Docs: https://catchdrift.ai · Issues: https://github.com/dyoon92/de
331
375
  console.log('')
332
376
  }
333
377
 
334
- if (figmaFileKey) {
378
+ if (figmaFiles.length > 0) {
335
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.`)
336
380
  console.log('')
337
381
  }
@@ -339,6 +383,12 @@ ${pc.dim('Docs: https://catchdrift.ai · Issues: https://github.com/dyoon92/de
339
383
 
340
384
  // ── Helpers ───────────────────────────────────────────────────────────────────
341
385
 
386
+ function extractFigmaFileKey(input) {
387
+ // Matches: figma.com/design/KEY/... or figma.com/file/KEY/...
388
+ const match = input.match(/figma\.com\/(?:design|file)\/([a-zA-Z0-9]+)/)
389
+ return match ? match[1] : null
390
+ }
391
+
342
392
  async function fetchFigmaPages(fileKey, token) {
343
393
  try {
344
394
  const res = await fetch(`https://api.figma.com/v1/files/${fileKey}?depth=1`, {
@@ -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 = {