@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 +1 -1
- package/src/commands/init.mjs +79 -29
- package/src/lib/writers.mjs +27 -7
package/package.json
CHANGED
package/src/commands/init.mjs
CHANGED
|
@@ -130,39 +130,84 @@ export async function init(argv) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// ── Step 3a: Figma ───────────────────────────────────────────────────────────
|
|
133
|
-
|
|
133
|
+
// figmaFiles: [{ key: string, wipPages?: string[] }]
|
|
134
|
+
let figmaToken
|
|
135
|
+
const figmaFiles = []
|
|
136
|
+
|
|
134
137
|
if (sources.includes('figma')) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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: '
|
|
145
|
-
placeholder: 'figd_...
|
|
146
|
-
hint: '
|
|
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
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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`, {
|
package/src/lib/writers.mjs
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
${
|
|
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,
|
|
114
|
-
const content = buildAIRulesContent(components, storybookUrl,
|
|
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 = {
|