@mehuljatiya/troupe 0.1.15

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/src/setup.js ADDED
@@ -0,0 +1,397 @@
1
+ import { execSync, spawnSync } from 'child_process'
2
+ import { existsSync, mkdirSync, copyFileSync, readdirSync, appendFileSync } from 'fs'
3
+ import { join } from 'path'
4
+ import { homedir } from 'os'
5
+ import { fileURLToPath } from 'url'
6
+ import { dirname } from 'path'
7
+ import { confirm } from '@inquirer/prompts'
8
+ import chalk from 'chalk'
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url))
11
+
12
+ export async function runSetup() {
13
+ console.log()
14
+ console.log(chalk.bold('Troupe') + ' ✦')
15
+ console.log("Let's get you set up. This takes about 2 minutes.\n")
16
+
17
+ // ── Node version check ───────────────────────────────────────────────────
18
+ const nodeVersion = process.versions.node
19
+ const major = parseInt(nodeVersion.split('.')[0], 10)
20
+ if (major < 20) {
21
+ console.log(chalk.yellow(` Node.js v${nodeVersion} detected — version 20 or higher is required.\n`))
22
+ await upgradeNode()
23
+ // upgradeNode either re-execs (nvm path) or exits
24
+ process.exit(1)
25
+ }
26
+ console.log(chalk.green(` ✓ Node.js v${nodeVersion}\n`))
27
+
28
+ // ── Step 1: Claude Code ──────────────────────────────────────────────────
29
+ console.log(chalk.bold('Step 1/5') + ' — Claude Code')
30
+
31
+ const claudeInstalled = isClaudeInstalled()
32
+
33
+ if (claudeInstalled) {
34
+ console.log(chalk.green(' ✓ Claude Code is already installed\n'))
35
+ } else {
36
+ console.log(chalk.yellow(' Claude Code is not installed.'))
37
+
38
+ const shouldInstall = await confirm({
39
+ message: 'Install it now? (requires npm)',
40
+ default: true,
41
+ }).catch(() => false)
42
+
43
+ if (!shouldInstall) {
44
+ console.log()
45
+ console.log('No problem. Install it yourself when ready:')
46
+ console.log(' ' + chalk.cyan('npm install -g @anthropic-ai/claude-code'))
47
+ console.log('\nThen re-run: ' + chalk.cyan('npx @mehuljatiya/troupe setup'))
48
+ process.exit(0)
49
+ }
50
+
51
+ console.log(chalk.dim(' Installing...'))
52
+ try {
53
+ npmInstallGlobal('@anthropic-ai/claude-code')
54
+ console.log(chalk.green(' ✓ Claude Code installed\n'))
55
+ } catch {
56
+ console.log()
57
+ console.log(chalk.red(' Installation failed.'))
58
+ console.log(' Try running this manually:')
59
+ console.log(' ' + chalk.cyan('npm install -g @anthropic-ai/claude-code'))
60
+ process.exit(1)
61
+ }
62
+ }
63
+
64
+ // ── Step 2: Slash commands ───────────────────────────────────────────────
65
+ console.log(chalk.bold('Step 2/5') + ' — Installing slash commands')
66
+
67
+ const { installed, skipped } = installSlashCommands()
68
+
69
+ if (skipped.length > 0) {
70
+ console.log(chalk.yellow(` ${skipped.length} workflow(s) already exist — skipping to avoid overwriting`))
71
+ console.log(chalk.dim(' (delete them from ~/.claude/commands/ and re-run to reset)'))
72
+ }
73
+ if (installed.length > 0) {
74
+ console.log(chalk.green(` ✓ Installed ${installed.length} design workflow(s)`))
75
+ }
76
+ console.log()
77
+
78
+ // ── Step 3: Figma MCP ────────────────────────────────────────────────────
79
+ console.log(chalk.bold('Step 3/5') + ' — Figma ' + chalk.dim('(optional)'))
80
+ console.log(chalk.dim(' Lets Claude read your Figma designs directly — no copy-pasting needed.\n'))
81
+
82
+ const wantsFigma = await confirm({
83
+ message: 'Connect Figma?',
84
+ default: true,
85
+ }).catch(() => false)
86
+
87
+ let figmaConnected = false
88
+ let figmaAuthNeeded = false
89
+
90
+ if (wantsFigma) {
91
+ const alreadyConfigured = isFigmaMcpConfigured()
92
+
93
+ if (alreadyConfigured) {
94
+ console.log(chalk.green(' ✓ Figma is already configured\n'))
95
+ figmaConnected = true
96
+ } else {
97
+ try {
98
+ execSync(
99
+ 'claude mcp add --transport http figma https://mcp.figma.com/mcp --scope user',
100
+ { stdio: 'pipe' }
101
+ )
102
+ console.log(chalk.green(' ✓ Figma MCP configured'))
103
+ figmaConnected = true
104
+ figmaAuthNeeded = true
105
+ } catch {
106
+ console.log(chalk.yellow(' Could not auto-configure Figma.'))
107
+ console.log(' Run this manually after setup:')
108
+ console.log(' ' + chalk.cyan('claude mcp add --transport http figma https://mcp.figma.com/mcp --scope user'))
109
+ console.log()
110
+ }
111
+
112
+ if (figmaConnected) {
113
+ console.log()
114
+ console.log(chalk.bold(' Authenticate Figma now') + ' — takes 30 seconds, saves you the step later.\n')
115
+ console.log(' Claude will open. Follow these 3 steps inside it:')
116
+ console.log(' 1. Type ' + chalk.cyan('/mcp') + ' and press Enter')
117
+ console.log(' 2. Select ' + chalk.cyan('figma') + ' → ' + chalk.cyan('Authenticate'))
118
+ console.log(' 3. Log in to Figma in the browser that opens, then come back here')
119
+ console.log(' 4. Type ' + chalk.cyan('/exit') + ' to return to setup\n')
120
+
121
+ const doAuth = await confirm({
122
+ message: 'Open Claude to authenticate Figma now?',
123
+ default: true,
124
+ }).catch(() => false)
125
+
126
+ if (doAuth) {
127
+ try {
128
+ execSync('claude', { stdio: 'inherit' })
129
+ console.log(chalk.green('\n ✓ Figma authentication complete\n'))
130
+ figmaAuthNeeded = false
131
+ } catch {
132
+ console.log(chalk.dim('\n (You can authenticate later — type /mcp inside Claude)\n'))
133
+ }
134
+ } else {
135
+ console.log(chalk.dim(' Skipped. To authenticate later, open Claude and type /mcp → figma → Authenticate\n'))
136
+ }
137
+ }
138
+ }
139
+ } else {
140
+ console.log(chalk.dim(' Skipped. You can always add it later:\n'))
141
+ console.log(chalk.dim(' claude mcp add --transport http figma https://mcp.figma.com/mcp --scope user\n'))
142
+ }
143
+
144
+ // ── Step 4: Browser & Figma plugins ─────────────────────────────────────
145
+ console.log(chalk.bold('Step 4/5') + ' — Browser tools')
146
+ console.log(chalk.dim(' Enables /document-component to open a browser preview and push docs into Figma.\n'))
147
+
148
+ installPlugin('chrome-devtools-mcp', 'chrome-devtools-plugins', 'Chrome DevTools')
149
+ installPlugin('figma-friend', 'figma-friend-marketplace', 'Figma Friend')
150
+
151
+ // ── Step 5: API key ──────────────────────────────────────────────────────
152
+ console.log(chalk.bold('Step 5/5') + ' — API key')
153
+
154
+ if (!claudeInstalled) {
155
+ console.log(' You\'ll need a free Anthropic API key.')
156
+ console.log(' Get one at: ' + chalk.cyan('https://console.anthropic.com'))
157
+ console.log(' Claude will ask for it when you first run ' + chalk.cyan('claude') + '.\n')
158
+ } else {
159
+ console.log(chalk.green(' ✓ Already configured\n'))
160
+ }
161
+
162
+ // ── Register design command globally ────────────────────────────────────
163
+ try {
164
+ npmInstallGlobal('@mehuljatiya/troupe')
165
+ } catch { /* non-critical — claude still works without design command */ }
166
+
167
+ // ── Done ─────────────────────────────────────────────────────────────────
168
+ showNextSteps(figmaConnected, figmaAuthNeeded)
169
+ }
170
+
171
+ function npmInstallGlobal(pkg) {
172
+ try {
173
+ // Pipe stderr so we can inspect it; stdout still streams to terminal
174
+ execSync(`npm install -g ${pkg}`, { stdio: ['inherit', 'inherit', 'pipe'] })
175
+ } catch (err) {
176
+ const stderr = err.stderr?.toString() || ''
177
+ if (!stderr.includes('EACCES') && !stderr.includes('permission denied')) throw err
178
+
179
+ // Permission denied — fix npm prefix to user home and retry
180
+ console.log(chalk.yellow('\n Permission denied. Fixing npm global directory...\n'))
181
+ const npmGlobal = join(homedir(), '.npm-global')
182
+
183
+ try {
184
+ mkdirSync(join(npmGlobal, 'lib', 'node_modules'), { recursive: true })
185
+ mkdirSync(join(npmGlobal, 'bin'), { recursive: true })
186
+ execSync(`npm config set prefix '${npmGlobal}'`, { stdio: 'pipe' })
187
+ process.env.PATH = join(npmGlobal, 'bin') + ':' + (process.env.PATH || '')
188
+ } catch {
189
+ console.log(chalk.red(' Could not fix npm permissions automatically.'))
190
+ console.log(' Try: ' + chalk.cyan('sudo npm install -g ' + pkg))
191
+ throw new Error('permission-fix-failed')
192
+ }
193
+
194
+ // Persist to shell profile
195
+ const shell = process.env.SHELL || ''
196
+ const profileFile = shell.includes('zsh') ? '.zshrc' : '.bashrc'
197
+ const profilePath = join(homedir(), profileFile)
198
+ try {
199
+ appendFileSync(profilePath, `\nexport PATH="$HOME/.npm-global/bin:$PATH"\n`)
200
+ console.log(chalk.dim(` Added PATH export to ~/${profileFile}`))
201
+ } catch { /* non-critical */ }
202
+
203
+ try {
204
+ // Pass --prefix directly so config file overrides can't interfere
205
+ execSync(`npm install -g ${pkg} --prefix="${npmGlobal}"`, {
206
+ stdio: 'inherit',
207
+ env: { ...process.env, npm_config_prefix: npmGlobal }
208
+ })
209
+ } catch {
210
+ console.log(chalk.red('\n Install still failed after fixing permissions.'))
211
+ console.log(' Try opening a new terminal tab and re-running:')
212
+ console.log(' ' + chalk.cyan('npx @mehuljatiya/troupe@latest setup'))
213
+ throw new Error('retry-failed')
214
+ }
215
+ }
216
+ }
217
+
218
+ async function upgradeNode() {
219
+ const nvmScript = join(homedir(), '.nvm', 'nvm.sh')
220
+ const nvmDirEnv = process.env.NVM_DIR ? join(process.env.NVM_DIR, 'nvm.sh') : null
221
+
222
+ const nvmPath = nvmDirEnv && existsSync(nvmDirEnv) ? nvmDirEnv
223
+ : existsSync(nvmScript) ? nvmScript
224
+ : null
225
+
226
+ if (nvmPath) {
227
+ console.log(chalk.dim(' nvm detected.'))
228
+ const go = await confirm({ message: 'Install Node.js 20 via nvm and continue setup?', default: true }).catch(() => false)
229
+ if (go) {
230
+ console.log(chalk.dim('\n Installing Node.js 20 — this takes a minute...\n'))
231
+ try {
232
+ execSync(`. "${nvmPath}" && nvm install 20`, { shell: '/bin/bash', stdio: 'inherit' })
233
+ const newNode = execSync(`. "${nvmPath}" && nvm which 20`, { shell: '/bin/bash', encoding: 'utf8', stdio: 'pipe' }).trim()
234
+ console.log(chalk.green('\n ✓ Node.js 20 installed'))
235
+ console.log(chalk.dim(' Restarting setup with Node.js 20...\n'))
236
+ const result = spawnSync(newNode, process.argv.slice(1), { stdio: 'inherit' })
237
+ process.exit(result.status ?? 0)
238
+ } catch {
239
+ console.log(chalk.red('\n Node.js upgrade failed.'))
240
+ console.log(' Try manually: ' + chalk.cyan('nvm install 20') + ' then re-run setup.\n')
241
+ }
242
+ }
243
+ return
244
+ }
245
+
246
+ try {
247
+ execSync('which brew', { stdio: 'ignore' })
248
+ console.log(chalk.dim(' Homebrew detected.'))
249
+
250
+ // Check if node@20 is already installed but just not linked/active
251
+ let brewNodePath = null
252
+ try {
253
+ const prefix = execSync('brew --prefix node@20 2>/dev/null', { encoding: 'utf8', stdio: 'pipe' }).trim()
254
+ const candidate = join(prefix, 'bin', 'node')
255
+ if (existsSync(candidate)) brewNodePath = candidate
256
+ } catch { /* not installed yet */ }
257
+
258
+ if (brewNodePath) {
259
+ // Already installed — just re-exec with it directly
260
+ console.log(chalk.green(' ✓ Node.js 20 already installed via Homebrew'))
261
+ console.log(chalk.dim(' Restarting setup with Node.js 20...\n'))
262
+ const result = spawnSync(brewNodePath, process.argv.slice(1), { stdio: 'inherit' })
263
+ process.exit(result.status ?? 0)
264
+ }
265
+
266
+ const go = await confirm({ message: 'Install Node.js 20 via Homebrew?', default: true }).catch(() => false)
267
+ if (go) {
268
+ console.log(chalk.dim('\n Installing Node.js 20 — this takes a minute...\n'))
269
+ try {
270
+ execSync('brew install node@20', { shell: '/bin/bash', stdio: 'inherit' })
271
+ execSync('brew link --overwrite --force node@20', { shell: '/bin/bash', stdio: 'pipe' })
272
+ const prefix = execSync('brew --prefix node@20', { encoding: 'utf8', stdio: 'pipe' }).trim()
273
+ brewNodePath = join(prefix, 'bin', 'node')
274
+ console.log(chalk.green('\n ✓ Node.js 20 installed'))
275
+ console.log(chalk.dim(' Restarting setup with Node.js 20...\n'))
276
+ const result = spawnSync(brewNodePath, process.argv.slice(1), { stdio: 'inherit' })
277
+ process.exit(result.status ?? 0)
278
+ } catch {
279
+ console.log(chalk.red('\n Homebrew install failed.'))
280
+ console.log(' Try manually: ' + chalk.cyan('brew install node@20') + '\n')
281
+ }
282
+ }
283
+ return
284
+ } catch { /* no brew */ }
285
+
286
+ console.log(' Download Node.js 20 LTS from: ' + chalk.cyan('https://nodejs.org'))
287
+ console.log(' Install it like any Mac app, then re-run this command.\n')
288
+ }
289
+
290
+ function installPlugin(pluginName, marketplace, label) {
291
+ try {
292
+ const result = execSync('claude plugin list', { encoding: 'utf8', stdio: 'pipe' })
293
+ if (result.toLowerCase().includes(pluginName.toLowerCase())) {
294
+ console.log(chalk.green(` ✓ ${label} already installed`))
295
+ console.log()
296
+ return
297
+ }
298
+ } catch { /* continue */ }
299
+
300
+ try {
301
+ execSync(`claude plugin install ${pluginName}@${marketplace} --scope user`, { stdio: 'pipe' })
302
+ console.log(chalk.green(` ✓ ${label} installed`))
303
+ } catch {
304
+ console.log(chalk.yellow(` Could not install ${label} automatically.`))
305
+ console.log(' Run this manually:')
306
+ console.log(' ' + chalk.cyan(`claude plugin install ${pluginName}@${marketplace}`))
307
+ }
308
+ console.log()
309
+ }
310
+
311
+ function isClaudeInstalled() {
312
+ try {
313
+ execSync('which claude', { stdio: 'ignore' })
314
+ return true
315
+ } catch {
316
+ return false
317
+ }
318
+ }
319
+
320
+ function isFigmaMcpConfigured() {
321
+ try {
322
+ const result = execSync('claude mcp list', { encoding: 'utf8', stdio: 'pipe' })
323
+ return result.toLowerCase().includes('figma')
324
+ } catch {
325
+ return false
326
+ }
327
+ }
328
+
329
+ function installSlashCommands() {
330
+ const commandsDir = join(homedir(), '.claude', 'commands')
331
+ try {
332
+ if (!existsSync(commandsDir)) {
333
+ mkdirSync(commandsDir, { recursive: true })
334
+ }
335
+ } catch {
336
+ console.log(chalk.red(' Could not create ~/.claude/commands/'))
337
+ console.log(chalk.dim(' Check permissions on your home directory.'))
338
+ return { installed: [], skipped: [] }
339
+ }
340
+
341
+ const sourceDir = join(__dirname, '..', 'commands')
342
+ let files = []
343
+ try {
344
+ files = readdirSync(sourceDir).filter(f => f.endsWith('.md'))
345
+ } catch {
346
+ console.log(chalk.red(' Could not read commands from package — it may be corrupted.'))
347
+ console.log(chalk.dim(' Try re-running: npx @mehuljatiya/troupe@latest setup'))
348
+ return { installed: [], skipped: [] }
349
+ }
350
+
351
+ const installed = []
352
+ const skipped = []
353
+
354
+ for (const file of files) {
355
+ const dest = join(commandsDir, file)
356
+ if (existsSync(dest)) {
357
+ skipped.push(file)
358
+ } else {
359
+ try {
360
+ copyFileSync(join(sourceDir, file), dest)
361
+ installed.push(file)
362
+ } catch {
363
+ console.log(chalk.yellow(` Could not copy ${file} — skipping`))
364
+ }
365
+ }
366
+ }
367
+
368
+ return { installed, skipped }
369
+ }
370
+
371
+ function showNextSteps(figmaConnected, figmaAuthNeeded) {
372
+ console.log('─'.repeat(50))
373
+ console.log(chalk.bold('\nYou\'re all set!\n'))
374
+ console.log(chalk.yellow(' Open a new terminal tab first') + ' so PATH updates take effect.\n')
375
+ console.log('To start:')
376
+ console.log(' 1. Open a new terminal tab in your project folder')
377
+ console.log(' 2. Type ' + chalk.cyan('claude') + ' (or ' + chalk.cyan('design') + ' for a command cheat sheet)')
378
+
379
+ if (figmaAuthNeeded) {
380
+ console.log(' 3. Inside Claude, type ' + chalk.cyan('/mcp') + ' → figma → Authenticate')
381
+ console.log(' (one-time step to connect your Figma account)\n')
382
+ } else {
383
+ console.log()
384
+ }
385
+
386
+ console.log('Available workflows (type inside Claude):')
387
+ console.log(' ' + chalk.cyan('/figma') + ' [url] Pull a Figma design and build it')
388
+ console.log(' ' + chalk.cyan('/document-component') + ' [url] Generate full component docs from Figma')
389
+ console.log(' ' + chalk.cyan('/spec') + ' [url] Ticket-ready spec with acceptance criteria')
390
+ console.log(' ' + chalk.cyan('/qa') + ' [url] Compare Figma to built component, flag gaps')
391
+ console.log(' ' + chalk.cyan('/new-component') + ' Start a new component from scratch')
392
+ console.log(' ' + chalk.cyan('/document') + ' Write docs for a component')
393
+ console.log(' ' + chalk.cyan('/review') + ' Design quality and consistency check')
394
+ console.log(' ' + chalk.cyan('/tokens') + ' Explain the design tokens in this project')
395
+ console.log(' ' + chalk.cyan('/handoff') + ' Generate a developer handoff doc')
396
+ console.log()
397
+ }