@catchdrift/cli 0.2.0 → 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.
- package/package.json +1 -1
- package/src/commands/init.mjs +162 -46
- package/src/index.mjs +1 -1
package/package.json
CHANGED
package/src/commands/init.mjs
CHANGED
|
@@ -85,35 +85,48 @@ export async function init(argv) {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// ── Step 2:
|
|
88
|
+
// ── Step 2: Do they have a DS at all? ────────────────────────────────────────
|
|
89
89
|
console.log('')
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const dsSources = await p.multiselect({
|
|
94
|
-
message: 'DS source (you can pick more than one)',
|
|
95
|
-
options: [
|
|
96
|
-
{ value: 'figma', label: 'Figma', hint: 'components published in a Figma file' },
|
|
97
|
-
{ value: 'storybook', label: 'Storybook', hint: 'stories at a URL' },
|
|
98
|
-
{ value: 'package', label: 'npm package / path', hint: 'e.g. @acme/ui or ./src/components' },
|
|
99
|
-
{ value: 'fresh', label: 'No DS yet — bootstrap from scratch', hint: 'pick Material UI, shadcn, Ant Design, etc.' },
|
|
100
|
-
{ value: 'manual', label: 'I\'ll type component names myself', hint: 'you can always run /drift-sync later' },
|
|
101
|
-
],
|
|
102
|
-
required: false,
|
|
90
|
+
const hasDS = await p.confirm({
|
|
91
|
+
message: 'Do you already have a design system?',
|
|
92
|
+
initialValue: true,
|
|
103
93
|
})
|
|
104
|
-
if (p.isCancel(
|
|
94
|
+
if (p.isCancel(hasDS)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
|
|
105
95
|
|
|
106
|
-
|
|
96
|
+
let dsSources = []
|
|
97
|
+
if (hasDS) {
|
|
98
|
+
// ── Step 2b: Where does it live? ───────────────────────────────────────────
|
|
99
|
+
console.log('')
|
|
100
|
+
p.log.step('Where does your design system live? Select all that apply.')
|
|
101
|
+
console.log('')
|
|
107
102
|
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
const picked = await p.multiselect({
|
|
104
|
+
message: 'DS source (you can pick more than one)',
|
|
105
|
+
options: [
|
|
106
|
+
{ value: 'figma', label: 'Figma', hint: 'components published in a Figma file' },
|
|
107
|
+
{ value: 'storybook', label: 'Storybook', hint: 'stories at a URL' },
|
|
108
|
+
{ value: 'package', label: 'npm package / path', hint: 'e.g. @acme/ui or ./src/components' },
|
|
109
|
+
{ value: 'manual', label: 'Type names manually', hint: 'you can always run /drift-sync later' },
|
|
110
|
+
],
|
|
111
|
+
required: false,
|
|
112
|
+
})
|
|
113
|
+
if (p.isCancel(picked)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
|
|
114
|
+
dsSources = Array.isArray(picked) ? picked : []
|
|
115
|
+
} else {
|
|
116
|
+
dsSources = ['fresh']
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const sources = dsSources
|
|
120
|
+
|
|
121
|
+
// ── Step 2c: Bootstrap foundation picker ─────────────────────────────────────
|
|
122
|
+
// Fires when user has no existing DS
|
|
110
123
|
const DS_FOUNDATIONS = [
|
|
111
|
-
{ value: 'shadcn', label: 'shadcn/ui', hint: 'Radix + Tailwind — copy-paste components', pkg: '@radix-ui/react-dialog', install: 'npx shadcn@latest init', docs: 'https://ui.shadcn.com' },
|
|
112
|
-
{ value: 'mui', label: 'Material UI', hint: 'Google Material Design — battle-tested ecosystem', pkg: '@mui/material', install: 'npm install @mui/material @emotion/react @emotion/styled', docs: 'https://mui.com/material-ui/' },
|
|
113
|
-
{ value: 'antd', label: 'Ant Design', hint: 'Enterprise-grade — rich component set', pkg: 'antd', install: 'npm install antd', docs: 'https://ant.design/components/overview/' },
|
|
114
|
-
{ value: 'chakra', label: 'Chakra UI', hint: 'Accessible, composable — great DX', pkg: '@chakra-ui/react', install: 'npm install @chakra-ui/react @emotion/react @emotion/styled framer-motion', docs: 'https://chakra-ui.com/docs/components' },
|
|
115
|
-
{ value: 'radix', label: 'Radix Themes', hint: 'Headless primitives + opinionated theme layer', pkg: '@radix-ui/themes', install: 'npm install @radix-ui/themes', docs: 'https://www.radix-ui.com/themes' },
|
|
116
|
-
{ value: 'primer', label: 'Primer (GitHub)', hint: "GitHub's DS — clean, minimal, production-grade", pkg: '@primer/react', install: 'npm install @primer/react styled-components', docs: 'https://primer.style/components' },
|
|
124
|
+
{ value: 'shadcn', label: 'shadcn/ui', hint: 'Radix + Tailwind — copy-paste components', pkg: '@radix-ui/react-dialog', install: 'npx shadcn@latest init', installAuto: null, docs: 'https://ui.shadcn.com', figmaCommunity: 'shadcn/ui' },
|
|
125
|
+
{ value: 'mui', label: 'Material UI', hint: 'Google Material Design — battle-tested ecosystem', pkg: '@mui/material', install: 'npm install @mui/material @emotion/react @emotion/styled', installAuto: '@mui/material @emotion/react @emotion/styled', docs: 'https://mui.com/material-ui/', figmaCommunity: 'Material UI MUI' },
|
|
126
|
+
{ value: 'antd', label: 'Ant Design', hint: 'Enterprise-grade — rich component set', pkg: 'antd', install: 'npm install antd', installAuto: 'antd', docs: 'https://ant.design/components/overview/', figmaCommunity: 'Ant Design' },
|
|
127
|
+
{ value: 'chakra', label: 'Chakra UI', hint: 'Accessible, composable — great DX', pkg: '@chakra-ui/react', install: 'npm install @chakra-ui/react @emotion/react @emotion/styled framer-motion', installAuto: '@chakra-ui/react @emotion/react @emotion/styled framer-motion', docs: 'https://chakra-ui.com/docs/components', figmaCommunity: 'Chakra UI' },
|
|
128
|
+
{ value: 'radix', label: 'Radix Themes', hint: 'Headless primitives + opinionated theme layer', pkg: '@radix-ui/themes', install: 'npm install @radix-ui/themes', installAuto: '@radix-ui/themes', docs: 'https://www.radix-ui.com/themes', figmaCommunity: 'Radix Themes' },
|
|
129
|
+
{ value: 'primer', label: 'Primer (GitHub)', hint: "GitHub's DS — clean, minimal, production-grade", pkg: '@primer/react', install: 'npm install @primer/react styled-components', installAuto: '@primer/react styled-components', docs: 'https://primer.style/components', figmaCommunity: 'GitHub Primer' },
|
|
117
130
|
]
|
|
118
131
|
|
|
119
132
|
let chosenFoundation = null
|
|
@@ -133,8 +146,59 @@ export async function init(argv) {
|
|
|
133
146
|
chosenFoundation = DS_FOUNDATIONS.find(f => f.value === foundationChoice)
|
|
134
147
|
|
|
135
148
|
p.log.success(`${chosenFoundation.label} selected — Drift will track it via dsPackages.`)
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
|
|
150
|
+
// ── Auto-install the foundation ─────────────────────────────────────────────
|
|
151
|
+
if (chosenFoundation.installAuto) {
|
|
152
|
+
// Non-interactive: npm install — run automatically
|
|
153
|
+
const doInstall = await p.confirm({
|
|
154
|
+
message: `Install ${chosenFoundation.label} now? (${chosenFoundation.install})`,
|
|
155
|
+
initialValue: true,
|
|
156
|
+
})
|
|
157
|
+
if (p.isCancel(doInstall)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
|
|
158
|
+
|
|
159
|
+
if (doInstall) {
|
|
160
|
+
const pkgManager =
|
|
161
|
+
existsSync(join(cwd, 'pnpm-lock.yaml')) ? 'pnpm add' :
|
|
162
|
+
existsSync(join(cwd, 'yarn.lock')) ? 'yarn add' : 'npm install'
|
|
163
|
+
const installCmd = `${pkgManager} ${chosenFoundation.installAuto}`
|
|
164
|
+
spinner.start(`Installing ${chosenFoundation.label}...`)
|
|
165
|
+
try {
|
|
166
|
+
execSync(installCmd, { cwd, stdio: 'pipe' })
|
|
167
|
+
spinner.stop(`${chosenFoundation.label} installed ✓`)
|
|
168
|
+
} catch (err) {
|
|
169
|
+
const stderr = err.stderr?.toString() || ''
|
|
170
|
+
if (stderr.includes('EACCES') || stderr.includes('root-owned')) {
|
|
171
|
+
spinner.stop(pc.yellow(`Permission error — run: sudo chown -R $(id -u):$(id -g) ~/.npm then re-run catchdrift init`))
|
|
172
|
+
} else {
|
|
173
|
+
spinner.stop(pc.yellow(`Install failed — run manually: ${chosenFoundation.install}`))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
// shadcn is interactive — user must run it themselves
|
|
179
|
+
p.log.info(`shadcn/ui requires its own interactive setup:\n ${pc.bold(chosenFoundation.install)}\n Run this after catchdrift finishes, then run ${pc.bold('npx catchdrift sync')}`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Figma for fresh users ────────────────────────────────────────────────────
|
|
183
|
+
console.log('')
|
|
184
|
+
p.log.info(
|
|
185
|
+
`Want to connect a Figma file? This lets Drift link every component back to its Figma source.\n` +
|
|
186
|
+
pc.dim(` ${chosenFoundation.label} has an official Figma community library — great starting point.\n`) +
|
|
187
|
+
pc.dim(` 1. Go to figma.com/community and search "${chosenFoundation.figmaCommunity}"\n`) +
|
|
188
|
+
pc.dim(` 2. Click "Open in Figma" to duplicate it to your workspace\n`) +
|
|
189
|
+
pc.dim(` 3. Paste the file URL below`)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
const wantFigmaFresh = await p.confirm({
|
|
193
|
+
message: 'Connect a Figma file now?',
|
|
194
|
+
initialValue: false,
|
|
195
|
+
})
|
|
196
|
+
if (p.isCancel(wantFigmaFresh)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
|
|
197
|
+
if (wantFigmaFresh) {
|
|
198
|
+
sources.push('figma')
|
|
199
|
+
} else {
|
|
200
|
+
console.log(pc.dim(' Skipping Figma. Re-run npx catchdrift init later to add it.'))
|
|
201
|
+
}
|
|
138
202
|
}
|
|
139
203
|
|
|
140
204
|
// ── Storybook nudge — fire whenever Storybook isn't in the setup ─────────────
|
|
@@ -152,14 +216,34 @@ export async function init(argv) {
|
|
|
152
216
|
if (p.isCancel(setupSB)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
|
|
153
217
|
|
|
154
218
|
if (setupSB) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
219
|
+
// Detect root-owned npm cache before spawning Storybook
|
|
220
|
+
const cacheOk = checkNpmCache()
|
|
221
|
+
if (!cacheOk) {
|
|
222
|
+
p.log.warn(
|
|
223
|
+
'npm cache is owned by root — Storybook install will fail.\n' +
|
|
224
|
+
pc.bold(' Fix it first by running:\n') +
|
|
225
|
+
` sudo chown -R $(id -u):$(id -g) ~/.npm\n` +
|
|
226
|
+
` Then re-run: ${pc.bold('npx catchdrift init')}`
|
|
227
|
+
)
|
|
228
|
+
} else {
|
|
229
|
+
console.log(pc.dim('\n Running npx storybook@latest init — follow the prompts:\n'))
|
|
230
|
+
try {
|
|
231
|
+
// Use inherit so the user can interact with Storybook's framework prompts
|
|
232
|
+
execSync('npx storybook@latest init', { cwd, stdio: 'inherit' })
|
|
233
|
+
p.log.success('Storybook installed — run `npm run storybook` to start it on :6006')
|
|
234
|
+
sources.push('storybook')
|
|
235
|
+
} catch (err) {
|
|
236
|
+
const stderr = (err.stderr?.toString() || '') + (err.stdout?.toString() || '')
|
|
237
|
+
if (stderr.includes('EACCES') || stderr.includes('root-owned')) {
|
|
238
|
+
p.log.warn(
|
|
239
|
+
'npm cache permission error.\n' +
|
|
240
|
+
` Fix: sudo chown -R $(id -u):$(id -g) ~/.npm\n` +
|
|
241
|
+
` Then re-run: npx catchdrift init`
|
|
242
|
+
)
|
|
243
|
+
} else {
|
|
244
|
+
p.log.warn('Storybook install failed or was cancelled. Run `npx storybook@latest init` manually when ready.')
|
|
245
|
+
}
|
|
246
|
+
}
|
|
163
247
|
}
|
|
164
248
|
} else {
|
|
165
249
|
p.log.info('Skipping Storybook. Run `npx storybook@latest init` when ready, then re-run `npx catchdrift init`.')
|
|
@@ -175,14 +259,33 @@ export async function init(argv) {
|
|
|
175
259
|
if (p.isCancel(installNow)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
|
|
176
260
|
|
|
177
261
|
if (installNow) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
262
|
+
const cacheOk = checkNpmCache()
|
|
263
|
+
if (!cacheOk) {
|
|
264
|
+
p.log.warn(
|
|
265
|
+
'npm cache is owned by root — Storybook install will fail.\n' +
|
|
266
|
+
pc.bold(' Fix it first:\n') +
|
|
267
|
+
` sudo chown -R $(id -u):$(id -g) ~/.npm\n` +
|
|
268
|
+
` Then re-run: ${pc.bold('npx catchdrift init')}`
|
|
269
|
+
)
|
|
185
270
|
sources.splice(sources.indexOf('storybook'), 1)
|
|
271
|
+
} else {
|
|
272
|
+
console.log(pc.dim('\n Running npx storybook@latest init — follow the prompts:\n'))
|
|
273
|
+
try {
|
|
274
|
+
execSync('npx storybook@latest init', { cwd, stdio: 'inherit' })
|
|
275
|
+
p.log.success('Storybook installed — run `npm run storybook` to start it on :6006')
|
|
276
|
+
} catch (err) {
|
|
277
|
+
const stderr = (err.stderr?.toString() || '') + (err.stdout?.toString() || '')
|
|
278
|
+
if (stderr.includes('EACCES') || stderr.includes('root-owned')) {
|
|
279
|
+
p.log.warn(
|
|
280
|
+
'npm cache permission error.\n' +
|
|
281
|
+
` Fix: sudo chown -R $(id -u):$(id -g) ~/.npm\n` +
|
|
282
|
+
` Then re-run: npx catchdrift init`
|
|
283
|
+
)
|
|
284
|
+
} else {
|
|
285
|
+
p.log.warn('Storybook install failed or was cancelled. Run `npx storybook@latest init` manually when ready.')
|
|
286
|
+
}
|
|
287
|
+
sources.splice(sources.indexOf('storybook'), 1)
|
|
288
|
+
}
|
|
186
289
|
}
|
|
187
290
|
} else {
|
|
188
291
|
// User declined — remove from sources so the URL step is skipped
|
|
@@ -481,17 +584,18 @@ ${pc.bold('What was set up:')}
|
|
|
481
584
|
console.log(`\n${pc.bold('Do this now:')}`)
|
|
482
585
|
|
|
483
586
|
if (chosenFoundation) {
|
|
587
|
+
const alreadyInstalled = chosenFoundation.installAuto !== null
|
|
484
588
|
console.log(`
|
|
485
589
|
You chose ${pc.bold(chosenFoundation.label)} as your foundation.
|
|
486
|
-
|
|
590
|
+
${alreadyInstalled ? '' : `
|
|
487
591
|
${pc.cyan('1.')} Install it:
|
|
488
|
-
${pc.bold(chosenFoundation.install)}
|
|
489
|
-
${pc.cyan('2.')} Run: ${pc.bold('npx catchdrift sync')}
|
|
592
|
+
${pc.bold(chosenFoundation.install)}`}
|
|
593
|
+
${alreadyInstalled ? pc.cyan('1.') : pc.cyan('2.')} Run: ${pc.bold('npx catchdrift sync')}
|
|
490
594
|
Scans your codebase for imports from ${pc.bold(chosenFoundation.pkg)} and registers them.
|
|
491
|
-
${pc.cyan('3.')} Run: ${pc.bold('npm run dev')} → press ${pc.bold('D')} → see live coverage
|
|
595
|
+
${alreadyInstalled ? pc.cyan('2.') : pc.cyan('3.')} Run: ${pc.bold('npm run dev')} → press ${pc.bold('D')} → see live coverage
|
|
492
596
|
|
|
493
597
|
${pc.dim('Docs + live preview: ' + chosenFoundation.docs)}
|
|
494
|
-
${pc.dim('Figma community library: search "' + chosenFoundation.
|
|
598
|
+
${pc.dim('Figma community library: search "' + chosenFoundation.figmaCommunity + '" at figma.com/community')}
|
|
495
599
|
`)
|
|
496
600
|
} else if (figmaFiles.length > 0 && skillFiles.length > 0) {
|
|
497
601
|
console.log(`
|
|
@@ -556,6 +660,18 @@ ${pc.bold('What was set up:')}
|
|
|
556
660
|
|
|
557
661
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
558
662
|
|
|
663
|
+
/** Returns false if the npm cache is root-owned (common after sudo npm install -g) */
|
|
664
|
+
function checkNpmCache() {
|
|
665
|
+
try {
|
|
666
|
+
execSync('npm cache verify', { stdio: 'pipe', timeout: 8000 })
|
|
667
|
+
return true
|
|
668
|
+
} catch (err) {
|
|
669
|
+
const msg = (err.stderr?.toString() || '') + (err.stdout?.toString() || '')
|
|
670
|
+
if (msg.includes('EACCES') || msg.includes('root-owned')) return false
|
|
671
|
+
return true // other errors are unrelated to ownership
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
559
675
|
function extractFigmaFileKey(input) {
|
|
560
676
|
// Matches: figma.com/design/KEY/... or figma.com/file/KEY/...
|
|
561
677
|
const match = input.match(/figma\.com\/(?:design|file)\/([a-zA-Z0-9]+)/)
|