@catchdrift/cli 0.2.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catchdrift/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "CLI for Drift — install, check, and manage design system coverage for any React app.",
5
5
  "keywords": [
6
6
  "design-system",
@@ -121,12 +121,12 @@ export async function init(argv) {
121
121
  // ── Step 2c: Bootstrap foundation picker ─────────────────────────────────────
122
122
  // Fires when user has no existing DS
123
123
  const DS_FOUNDATIONS = [
124
- { 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' },
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', docs: 'https://mui.com/material-ui/' },
126
- { value: 'antd', label: 'Ant Design', hint: 'Enterprise-grade — rich component set', pkg: 'antd', install: 'npm install antd', docs: 'https://ant.design/components/overview/' },
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', docs: 'https://chakra-ui.com/docs/components' },
128
- { 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' },
129
- { 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' },
130
130
  ]
131
131
 
132
132
  let chosenFoundation = null
@@ -146,8 +146,59 @@ export async function init(argv) {
146
146
  chosenFoundation = DS_FOUNDATIONS.find(f => f.value === foundationChoice)
147
147
 
148
148
  p.log.success(`${chosenFoundation.label} selected — Drift will track it via dsPackages.`)
149
- p.log.info(`Install it now or later:\n ${pc.bold(chosenFoundation.install)}`)
150
- p.log.info(`Docs + live preview: ${pc.cyan(chosenFoundation.docs)}`)
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
+ }
151
202
  }
152
203
 
153
204
  // ── Storybook nudge — fire whenever Storybook isn't in the setup ─────────────
@@ -165,14 +216,34 @@ export async function init(argv) {
165
216
  if (p.isCancel(setupSB)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
166
217
 
167
218
  if (setupSB) {
168
- console.log(pc.dim('\n Running npx storybook@latest init follow the prompts:\n'))
169
- try {
170
- // Use inherit so the user can interact with Storybook's framework prompts
171
- execSync('npx storybook@latest init', { cwd, stdio: 'inherit' })
172
- p.log.success('Storybook installed run `npm run storybook` to start it on :6006')
173
- sources.push('storybook')
174
- } catch {
175
- p.log.warn('Storybook install failed or was cancelled. Run `npx storybook@latest init` manually when ready.')
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
+ }
176
247
  }
177
248
  } else {
178
249
  p.log.info('Skipping Storybook. Run `npx storybook@latest init` when ready, then re-run `npx catchdrift init`.')
@@ -188,14 +259,33 @@ export async function init(argv) {
188
259
  if (p.isCancel(installNow)) { p.cancel('Setup cancelled.'); process.exit(EXIT_CANCELED) }
189
260
 
190
261
  if (installNow) {
191
- console.log(pc.dim('\n Running npx storybook@latest init — follow the prompts:\n'))
192
- try {
193
- execSync('npx storybook@latest init', { cwd, stdio: 'inherit' })
194
- p.log.success('Storybook installed run `npm run storybook` to start it on :6006')
195
- } catch {
196
- p.log.warn('Storybook install failed or was cancelled. Run `npx storybook@latest init` manually when ready.')
197
- // Remove storybook from sources so we don't ask for a URL that doesn't exist yet
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
+ )
198
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
+ }
199
289
  }
200
290
  } else {
201
291
  // User declined — remove from sources so the URL step is skipped
@@ -494,17 +584,18 @@ ${pc.bold('What was set up:')}
494
584
  console.log(`\n${pc.bold('Do this now:')}`)
495
585
 
496
586
  if (chosenFoundation) {
587
+ const alreadyInstalled = chosenFoundation.installAuto !== null
497
588
  console.log(`
498
589
  You chose ${pc.bold(chosenFoundation.label)} as your foundation.
499
-
590
+ ${alreadyInstalled ? '' : `
500
591
  ${pc.cyan('1.')} Install it:
501
- ${pc.bold(chosenFoundation.install)}
502
- ${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')}
503
594
  Scans your codebase for imports from ${pc.bold(chosenFoundation.pkg)} and registers them.
504
- ${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
505
596
 
506
597
  ${pc.dim('Docs + live preview: ' + chosenFoundation.docs)}
507
- ${pc.dim('Figma community library: search "' + chosenFoundation.label + '" at figma.com/community')}
598
+ ${pc.dim('Figma community library: search "' + chosenFoundation.figmaCommunity + '" at figma.com/community')}
508
599
  `)
509
600
  } else if (figmaFiles.length > 0 && skillFiles.length > 0) {
510
601
  console.log(`
@@ -569,6 +660,18 @@ ${pc.bold('What was set up:')}
569
660
 
570
661
  // ── Helpers ───────────────────────────────────────────────────────────────────
571
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
+
572
675
  function extractFigmaFileKey(input) {
573
676
  // Matches: figma.com/design/KEY/... or figma.com/file/KEY/...
574
677
  const match = input.match(/figma\.com\/(?:design|file)\/([a-zA-Z0-9]+)/)
package/src/index.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  import pc from 'picocolors'
7
7
 
8
- const VERSION = '0.2.1'
8
+ const VERSION = '0.2.2'
9
9
 
10
10
  const HELP = `
11
11
  ${pc.bold(pc.blue('catchdrift'))} — Design system compliance for teams shipping with AI