clauderc 1.0.0

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/bin/cli.js ADDED
@@ -0,0 +1,802 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, cpSync, readdirSync, readFileSync, writeFileSync, renameSync } from 'fs';
4
+ import { join, dirname, sep, basename } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { homedir, platform } from 'os';
7
+ import { createInterface } from 'readline';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+ const TEMPLATES_DIR = join(__dirname, '..', 'templates');
12
+ const CLAUDE_DIR = join(homedir(), '.claude');
13
+ const MANIFEST_FILE = join(CLAUDE_DIR, '.clauderc.json');
14
+ const IS_WINDOWS = platform() === 'win32';
15
+
16
+ const VERSION = '1.0.0';
17
+ const AUTHOR = {
18
+ name: 'Matheus Kindrazki',
19
+ github: 'https://github.com/matheuskindrazki',
20
+ repo: 'https://github.com/matheuskindrazki/clauderc',
21
+ twitter: 'https://x.com/kindraScript',
22
+ };
23
+
24
+ // Windows-compatible colors
25
+ const supportsColors = process.stdout.isTTY &&
26
+ (process.env.FORCE_COLOR !== '0') &&
27
+ (IS_WINDOWS ? process.env.TERM !== 'dumb' : true);
28
+
29
+ const c = supportsColors ? {
30
+ reset: '\x1b[0m',
31
+ green: '\x1b[32m',
32
+ yellow: '\x1b[33m',
33
+ blue: '\x1b[34m',
34
+ cyan: '\x1b[36m',
35
+ magenta: '\x1b[35m',
36
+ red: '\x1b[31m',
37
+ dim: '\x1b[2m',
38
+ bold: '\x1b[1m',
39
+ underline: '\x1b[4m',
40
+ } : {
41
+ reset: '', green: '', yellow: '', blue: '', cyan: '', magenta: '', red: '', dim: '', bold: '', underline: '',
42
+ };
43
+
44
+ const log = {
45
+ info: (msg) => console.log(` ${c.blue}info${c.reset} ${msg}`),
46
+ success: (msg) => console.log(` ${c.green}done${c.reset} ${msg}`),
47
+ warn: (msg) => console.log(` ${c.yellow}skip${c.reset} ${msg}`),
48
+ error: (msg) => console.log(` ${c.red}fail${c.reset} ${msg}`),
49
+ add: (msg) => console.log(` ${c.green} +${c.reset} ${msg}`),
50
+ update: (msg) => console.log(` ${c.cyan} ~${c.reset} ${msg}`),
51
+ remove: (msg) => console.log(` ${c.red} -${c.reset} ${msg}`),
52
+ };
53
+
54
+ function banner() {
55
+ console.log(`
56
+ ${c.cyan}${c.bold}╔═══════════════════════════════════════════════════════════╗${c.reset}
57
+ ${c.cyan}${c.bold}║${c.reset} ${c.cyan}${c.bold}║${c.reset}
58
+ ${c.cyan}${c.bold}║${c.reset} ${c.bold}clauderc${c.reset} ${c.cyan}${c.bold}║${c.reset}
59
+ ${c.cyan}${c.bold}║${c.reset} ${c.dim}Best practices for Claude Code - agents, skills & more${c.reset} ${c.cyan}${c.bold}║${c.reset}
60
+ ${c.cyan}${c.bold}║${c.reset} ${c.cyan}${c.bold}║${c.reset}
61
+ ${c.cyan}${c.bold}╚═══════════════════════════════════════════════════════════╝${c.reset}
62
+ `);
63
+ }
64
+
65
+ function displayPath(fullPath) {
66
+ const home = homedir();
67
+ if (fullPath.startsWith(home)) {
68
+ const relative = fullPath.slice(home.length);
69
+ return IS_WINDOWS ? `%USERPROFILE%${relative}` : `~${relative}`;
70
+ }
71
+ return fullPath;
72
+ }
73
+
74
+ function ensureDir(dir) {
75
+ if (!existsSync(dir)) {
76
+ mkdirSync(dir, { recursive: true });
77
+ }
78
+ }
79
+
80
+ // Manifest functions
81
+ function loadPackageManifest() {
82
+ const manifestPath = join(TEMPLATES_DIR, 'manifest.json');
83
+ if (!existsSync(manifestPath)) return null;
84
+ return JSON.parse(readFileSync(manifestPath, 'utf-8'));
85
+ }
86
+
87
+ function loadInstalledManifest() {
88
+ if (!existsSync(MANIFEST_FILE)) return null;
89
+ try {
90
+ return JSON.parse(readFileSync(MANIFEST_FILE, 'utf-8'));
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function saveInstalledManifest(manifest) {
97
+ writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2));
98
+ }
99
+
100
+ function getSourcePath(fileKey) {
101
+ if (fileKey.startsWith('templates/')) {
102
+ return join(TEMPLATES_DIR, 'project-setup', fileKey.replace('templates/project-setup/', ''));
103
+ }
104
+ return join(TEMPLATES_DIR, fileKey);
105
+ }
106
+
107
+ function getDestPath(fileKey) {
108
+ return join(CLAUDE_DIR, fileKey);
109
+ }
110
+
111
+ function isNewer(v1, v2) {
112
+ const parse = (v) => (v || '0.0.0').split('.').map(Number);
113
+ const [a1, b1, c1] = parse(v1);
114
+ const [a2, b2, c2] = parse(v2);
115
+ if (a1 !== a2) return a1 > a2;
116
+ if (b1 !== b2) return b1 > b2;
117
+ return c1 > c2;
118
+ }
119
+
120
+ function backupFile(filePath) {
121
+ if (existsSync(filePath)) {
122
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
123
+ const backupPath = `${filePath}.backup-${timestamp}`;
124
+ renameSync(filePath, backupPath);
125
+ return backupPath;
126
+ }
127
+ return null;
128
+ }
129
+
130
+ function copyFile(src, dest, options = {}) {
131
+ const { backup = false, dryRun = false } = options;
132
+ ensureDir(dirname(dest));
133
+
134
+ let backupPath = null;
135
+ if (backup && existsSync(dest) && !dryRun) {
136
+ backupPath = backupFile(dest);
137
+ }
138
+
139
+ if (!dryRun) {
140
+ cpSync(src, dest);
141
+ }
142
+ return backupPath;
143
+ }
144
+
145
+ function showFooter() {
146
+ console.log(`
147
+ ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
148
+
149
+ ${c.bold}Created by ${c.cyan}${AUTHOR.name}${c.reset}
150
+
151
+ ${c.dim}GitHub:${c.reset} ${c.underline}${AUTHOR.repo}${c.reset}
152
+ ${c.dim}Twitter:${c.reset} ${c.underline}${AUTHOR.twitter}${c.reset}
153
+
154
+ ${c.yellow}★${c.reset} ${c.dim}If this helped you, consider giving it a star!${c.reset}
155
+ ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
156
+ `);
157
+ }
158
+
159
+ function showSuccessBanner(stats) {
160
+ const { created, updated, skipped } = stats;
161
+
162
+ console.log(`
163
+ ${c.green}${c.bold}╔═══════════════════════════════════════════════════════════╗${c.reset}
164
+ ${c.green}${c.bold}║${c.reset} ${c.green}${c.bold}║${c.reset}
165
+ ${c.green}${c.bold}║${c.reset} ${c.green}${c.bold}✓ Setup Complete!${c.reset} ${c.green}${c.bold}║${c.reset}
166
+ ${c.green}${c.bold}║${c.reset} ${c.green}${c.bold}║${c.reset}
167
+ ${c.green}${c.bold}║${c.reset} ${c.green}+${created} created${c.reset} ${c.cyan}~${updated} updated${c.reset} ${c.yellow}○${skipped} skipped${c.reset} ${c.green}${c.bold}║${c.reset}
168
+ ${c.green}${c.bold}║${c.reset} ${c.green}${c.bold}║${c.reset}
169
+ ${c.green}${c.bold}╚═══════════════════════════════════════════════════════════╝${c.reset}
170
+
171
+ ${c.bold}Try these commands in Claude Code:${c.reset}
172
+
173
+ ${c.cyan}Ask Claude:${c.reset} "Use project-setup-wizard to configure this project"
174
+ ${c.cyan}Slash commands:${c.reset} /test, /lint, /verify, /pr
175
+
176
+ `);
177
+ }
178
+
179
+ function listInstalled() {
180
+ banner();
181
+
182
+ const installed = loadInstalledManifest();
183
+ const pkg = loadPackageManifest();
184
+
185
+ console.log(` ${c.bold}Installed Components${c.reset}\n`);
186
+
187
+ if (installed) {
188
+ console.log(` ${c.dim}Version:${c.reset} ${c.cyan}${installed.version}${c.reset}`);
189
+ console.log(` ${c.dim}Installed:${c.reset} ${new Date(installed.installedAt).toLocaleDateString()}`);
190
+ if (pkg && isNewer(pkg.version, installed.version)) {
191
+ console.log(` ${c.yellow}${c.bold}Update available: v${pkg.version}${c.reset}`);
192
+ console.log(` ${c.dim}Run${c.reset} npx clauderc update`);
193
+ }
194
+ console.log();
195
+ }
196
+
197
+ const components = [
198
+ { name: 'Agents', path: join(CLAUDE_DIR, 'agents') },
199
+ { name: 'Skills', path: join(CLAUDE_DIR, 'skills') },
200
+ { name: 'Commands', path: join(CLAUDE_DIR, 'commands') },
201
+ { name: 'Templates', path: join(CLAUDE_DIR, 'templates') },
202
+ ];
203
+
204
+ for (const comp of components) {
205
+ console.log(` ${c.bold}${comp.name}${c.reset}`);
206
+
207
+ if (!existsSync(comp.path)) {
208
+ console.log(` ${c.dim}(none)${c.reset}\n`);
209
+ continue;
210
+ }
211
+
212
+ const entries = readdirSync(comp.path, { withFileTypes: true });
213
+ let found = false;
214
+
215
+ for (const entry of entries) {
216
+ if (entry.name.startsWith('.')) continue;
217
+
218
+ if (entry.isDirectory() && comp.name === 'Skills') {
219
+ const skillFile = join(comp.path, entry.name, 'SKILL.md');
220
+ if (existsSync(skillFile)) {
221
+ console.log(` ${c.green}●${c.reset} ${entry.name}`);
222
+ found = true;
223
+ }
224
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
225
+ console.log(` ${c.green}●${c.reset} ${entry.name.replace('.md', '')}`);
226
+ found = true;
227
+ } else if (entry.isDirectory()) {
228
+ console.log(` ${c.green}●${c.reset} ${entry.name}/`);
229
+ found = true;
230
+ }
231
+ }
232
+
233
+ if (!found) {
234
+ console.log(` ${c.dim}(none)${c.reset}`);
235
+ }
236
+ console.log();
237
+ }
238
+
239
+ showFooter();
240
+ }
241
+
242
+ function init(options = {}) {
243
+ const { force = false, dryRun = false } = options;
244
+
245
+ banner();
246
+
247
+ const pkg = loadPackageManifest();
248
+ if (!pkg) {
249
+ log.error('Package manifest not found');
250
+ process.exit(1);
251
+ }
252
+
253
+ const installed = loadInstalledManifest();
254
+
255
+ console.log(` ${c.bold}Installing Claude Code Setup${c.reset}\n`);
256
+ console.log(` ${c.dim}Version:${c.reset} ${c.cyan}v${pkg.version}${c.reset}`);
257
+ console.log(` ${c.dim}Path:${c.reset} ${c.cyan}${displayPath(CLAUDE_DIR)}${c.reset}`);
258
+ if (installed) {
259
+ console.log(` ${c.dim}Previous:${c.reset} v${installed.version}`);
260
+ }
261
+ console.log();
262
+
263
+ if (dryRun) {
264
+ console.log(` ${c.yellow}${c.bold}DRY RUN${c.reset} - No files will be modified\n`);
265
+ }
266
+
267
+ // Create base directories
268
+ const dirs = ['agents', 'skills', 'commands', 'templates'];
269
+ for (const dir of dirs) {
270
+ ensureDir(join(CLAUDE_DIR, dir));
271
+ }
272
+
273
+ let created = 0, updated = 0, skipped = 0;
274
+ const newManifest = {
275
+ version: pkg.version,
276
+ installedAt: new Date().toISOString(),
277
+ files: {}
278
+ };
279
+
280
+ console.log(` ${c.bold}Files${c.reset}\n`);
281
+
282
+ for (const [fileKey, fileMeta] of Object.entries(pkg.files)) {
283
+ const srcPath = getSourcePath(fileKey);
284
+ const destPath = getDestPath(fileKey);
285
+ const displayDest = displayPath(destPath);
286
+
287
+ if (!existsSync(srcPath)) {
288
+ continue;
289
+ }
290
+
291
+ const destExists = existsSync(destPath);
292
+ const installedVersion = installed?.files?.[fileKey]?.version;
293
+ const needsUpdate = !installedVersion || isNewer(fileMeta.version, installedVersion);
294
+
295
+ if (!destExists) {
296
+ copyFile(srcPath, destPath, { dryRun });
297
+ log.add(`${displayDest}`);
298
+ created++;
299
+ } else if (force || needsUpdate) {
300
+ copyFile(srcPath, destPath, { backup: true, dryRun });
301
+ log.update(`${displayDest}`);
302
+ updated++;
303
+ } else {
304
+ log.warn(`${displayDest} ${c.dim}(up to date)${c.reset}`);
305
+ skipped++;
306
+ }
307
+
308
+ newManifest.files[fileKey] = {
309
+ version: fileMeta.version,
310
+ installedAt: new Date().toISOString()
311
+ };
312
+ }
313
+
314
+ if (!dryRun) {
315
+ saveInstalledManifest(newManifest);
316
+ }
317
+
318
+ console.log();
319
+ showSuccessBanner({ created, updated, skipped });
320
+
321
+ if (skipped > 0 && !force) {
322
+ console.log(` ${c.dim}Tip: Use${c.reset} --force ${c.dim}to overwrite existing files${c.reset}\n`);
323
+ }
324
+
325
+ showFooter();
326
+ }
327
+
328
+ function update(options = {}) {
329
+ const { dryRun = false } = options;
330
+
331
+ banner();
332
+
333
+ const pkg = loadPackageManifest();
334
+ if (!pkg) {
335
+ log.error('Package manifest not found');
336
+ process.exit(1);
337
+ }
338
+
339
+ const installed = loadInstalledManifest();
340
+
341
+ if (!installed) {
342
+ console.log(` ${c.yellow}No installation found. Running init...${c.reset}\n`);
343
+ return init(options);
344
+ }
345
+
346
+ console.log(` ${c.bold}Updating Claude Code Setup${c.reset}\n`);
347
+ console.log(` ${c.dim}Current:${c.reset} v${installed.version}`);
348
+ console.log(` ${c.dim}Latest:${c.reset} v${pkg.version}`);
349
+ console.log();
350
+
351
+ if (!isNewer(pkg.version, installed.version)) {
352
+ console.log(` ${c.green}${c.bold}✓ Already up to date!${c.reset}\n`);
353
+ showFooter();
354
+ return;
355
+ }
356
+
357
+ if (dryRun) {
358
+ console.log(` ${c.yellow}${c.bold}DRY RUN${c.reset} - No files will be modified\n`);
359
+ }
360
+
361
+ // Show changelog
362
+ const relevantChanges = pkg.changelog?.filter(ch => isNewer(ch.version, installed.version)) || [];
363
+ if (relevantChanges.length > 0) {
364
+ console.log(` ${c.bold}What's New${c.reset}\n`);
365
+ for (const change of relevantChanges) {
366
+ console.log(` ${c.cyan}v${change.version}${c.reset} ${c.dim}(${change.date})${c.reset}`);
367
+ if (change.changes.added?.length) {
368
+ change.changes.added.forEach(a => console.log(` ${c.green}+${c.reset} ${a}`));
369
+ }
370
+ if (change.changes.changed?.length) {
371
+ change.changes.changed.forEach(ch => console.log(` ${c.cyan}~${c.reset} ${ch}`));
372
+ }
373
+ if (change.changes.removed?.length) {
374
+ change.changes.removed.forEach(r => console.log(` ${c.red}-${c.reset} ${r}`));
375
+ }
376
+ }
377
+ console.log();
378
+ }
379
+
380
+ // Calculate changes
381
+ const installedFiles = new Set(Object.keys(installed.files || {}));
382
+ const packageFiles = new Set(Object.keys(pkg.files));
383
+
384
+ const toAdd = [...packageFiles].filter(f => !installedFiles.has(f));
385
+ const toRemove = [...installedFiles].filter(f => !packageFiles.has(f));
386
+ const toUpdate = [...packageFiles].filter(f => {
387
+ if (!installedFiles.has(f)) return false;
388
+ return isNewer(pkg.files[f]?.version, installed.files[f]?.version);
389
+ });
390
+
391
+ console.log(` ${c.bold}Files${c.reset}\n`);
392
+
393
+ let added = 0, updated = 0, removed = 0;
394
+ const newManifest = {
395
+ version: pkg.version,
396
+ installedAt: new Date().toISOString(),
397
+ updatedFrom: installed.version,
398
+ files: {}
399
+ };
400
+
401
+ // Add new files
402
+ for (const fileKey of toAdd) {
403
+ const srcPath = getSourcePath(fileKey);
404
+ const destPath = getDestPath(fileKey);
405
+ if (!existsSync(srcPath)) continue;
406
+
407
+ copyFile(srcPath, destPath, { dryRun });
408
+ log.add(`${displayPath(destPath)}`);
409
+ added++;
410
+ newManifest.files[fileKey] = { version: pkg.files[fileKey].version, installedAt: new Date().toISOString() };
411
+ }
412
+
413
+ // Update changed files
414
+ for (const fileKey of toUpdate) {
415
+ const srcPath = getSourcePath(fileKey);
416
+ const destPath = getDestPath(fileKey);
417
+ if (!existsSync(srcPath)) continue;
418
+
419
+ const backupPath = copyFile(srcPath, destPath, { backup: true, dryRun });
420
+ log.update(`${displayPath(destPath)}`);
421
+ if (backupPath) {
422
+ console.log(` ${c.dim}backup: ${displayPath(backupPath)}${c.reset}`);
423
+ }
424
+ updated++;
425
+ newManifest.files[fileKey] = {
426
+ version: pkg.files[fileKey].version,
427
+ installedAt: new Date().toISOString(),
428
+ updatedFrom: installed.files[fileKey]?.version
429
+ };
430
+ }
431
+
432
+ // Keep unchanged files
433
+ for (const fileKey of packageFiles) {
434
+ if (!toAdd.includes(fileKey) && !toUpdate.includes(fileKey)) {
435
+ newManifest.files[fileKey] = installed.files[fileKey];
436
+ }
437
+ }
438
+
439
+ // Note deprecated files
440
+ for (const fileKey of toRemove) {
441
+ log.remove(`${fileKey} ${c.dim}(deprecated - delete manually if not needed)${c.reset}`);
442
+ removed++;
443
+ }
444
+
445
+ if (!dryRun) {
446
+ saveInstalledManifest(newManifest);
447
+ }
448
+
449
+ console.log();
450
+ showSuccessBanner({ created: added, updated, skipped: 0 });
451
+
452
+ if (removed > 0) {
453
+ console.log(` ${c.yellow}Note:${c.reset} ${removed} file(s) deprecated. Delete manually if not needed.\n`);
454
+ }
455
+
456
+ showFooter();
457
+ }
458
+
459
+ function showChangelog() {
460
+ banner();
461
+
462
+ const pkg = loadPackageManifest();
463
+ if (!pkg?.changelog) {
464
+ log.error('Changelog not found');
465
+ return;
466
+ }
467
+
468
+ const installed = loadInstalledManifest();
469
+
470
+ console.log(` ${c.bold}Changelog${c.reset}\n`);
471
+
472
+ for (const change of pkg.changelog) {
473
+ const isCurrent = installed?.version === change.version;
474
+ const marker = isCurrent ? ` ${c.green}● installed${c.reset}` : '';
475
+
476
+ console.log(` ${c.cyan}${c.bold}v${change.version}${c.reset}${marker} ${c.dim}(${change.date})${c.reset}`);
477
+
478
+ if (change.changes.added?.length) {
479
+ change.changes.added.forEach(a => console.log(` ${c.green}+${c.reset} ${a}`));
480
+ }
481
+ if (change.changes.changed?.length) {
482
+ change.changes.changed.forEach(ch => console.log(` ${c.cyan}~${c.reset} ${ch}`));
483
+ }
484
+ if (change.changes.removed?.length) {
485
+ change.changes.removed.forEach(r => console.log(` ${c.red}-${c.reset} ${r}`));
486
+ }
487
+ if (change.changes.deprecated?.length) {
488
+ change.changes.deprecated.forEach(d => console.log(` ${c.yellow}!${c.reset} ${d}`));
489
+ }
490
+ console.log();
491
+ }
492
+
493
+ showFooter();
494
+ }
495
+
496
+ // ============================================================
497
+ // PROJECT COMMAND - Setup .claude/ in current project
498
+ // ============================================================
499
+
500
+ import { STACKS, MONOREPO_TOOLS, CI_PLATFORMS } from '../src/stacks.js';
501
+ import { detectStack, generateCommands } from '../src/detector.js';
502
+
503
+ function createPrompt() {
504
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
505
+ return {
506
+ ask: (q) => new Promise(r => rl.question(q, a => r(a.trim()))),
507
+ confirm: async (q) => {
508
+ const a = await new Promise(r => rl.question(`${q} (Y/n): `, r));
509
+ return a.toLowerCase() !== 'n';
510
+ },
511
+ close: () => rl.close(),
512
+ };
513
+ }
514
+
515
+ function generateClaudeMd(config) {
516
+ const { projectName, stack, commands, customRules } = config;
517
+ let content = `# ${projectName}\n\n## Stack\n`;
518
+
519
+ if (stack.stacks.length > 0) content += `- **Language**: ${stack.stacks.map(s => s.name).join(', ')}\n`;
520
+ if (stack.framework) content += `- **Framework**: ${stack.framework.name}\n`;
521
+ if (stack.packageManager) content += `- **Package Manager**: ${stack.packageManager.name}\n`;
522
+ if (stack.monorepo) content += `- **Monorepo**: ${stack.monorepo.name}\n`;
523
+
524
+ content += `\n## Commands\n\n\`\`\`bash\n`;
525
+ if (commands.setup) content += `# Setup\n${commands.setup}\n\n`;
526
+ if (commands.dev) content += `# Development\n${commands.dev}\n\n`;
527
+ if (commands.test) content += `# Test\n${commands.test}\n\n`;
528
+ if (commands.lint) content += `# Lint\n${commands.lint}\n\n`;
529
+ if (commands.build) content += `# Build\n${commands.build}\n\n`;
530
+ if (commands.verify) content += `# Full verification\n${commands.verify}\n`;
531
+ content += `\`\`\`\n\n`;
532
+
533
+ content += `## Workflow\n\n### Plan Mode\nUse Plan Mode (Shift+Tab 2x) for:\n- Refactoring > 3 files\n- New feature implementation\n- Architecture changes\n\n`;
534
+ content += `### Verification\n- ALWAYS run tests before committing\n- NEVER skip tests without explicit approval\n`;
535
+
536
+ if (customRules?.length > 0) {
537
+ content += `\n## Project Rules\n\n${customRules.map(r => `- ${r}`).join('\n')}\n`;
538
+ }
539
+
540
+ content += `\n## Documentation\n@README.md\n`;
541
+ return content;
542
+ }
543
+
544
+ function generateProjectSettings(config) {
545
+ const { stack, commands } = config;
546
+ const pm = stack.packageManager?.name || 'npm';
547
+ const allowedCommands = ['Bash(git:*)', 'Bash(gh:*)'];
548
+
549
+ // Add package manager
550
+ const pmCommands = { bun: 'bun', pnpm: 'pnpm', yarn: 'yarn', npm: 'npm', poetry: 'poetry', cargo: 'cargo' };
551
+ if (pmCommands[pm]) allowedCommands.push(`Bash(${pmCommands[pm]}:*)`);
552
+
553
+ // Add stack-specific
554
+ const stackId = stack.stacks[0]?.id;
555
+ const stackCommands = {
556
+ go: ['go', 'golangci-lint'],
557
+ rust: ['cargo'],
558
+ python: ['pytest', 'ruff', 'mypy', 'python'],
559
+ dotnet: ['dotnet'],
560
+ elixir: ['mix'],
561
+ ruby: ['bundle', 'rails'],
562
+ php: ['composer', 'php'],
563
+ java: ['mvn', 'gradle'],
564
+ };
565
+ (stackCommands[stackId] || []).forEach(cmd => allowedCommands.push(`Bash(${cmd}:*)`));
566
+
567
+ const settings = { permissions: { allow: allowedCommands } };
568
+
569
+ if (commands.format || commands.lint) {
570
+ settings.hooks = {
571
+ PostToolUse: [{
572
+ matcher: 'Edit|Write',
573
+ hooks: [{ type: 'command', command: `${commands.format || commands.lint} || true` }],
574
+ }],
575
+ };
576
+ }
577
+
578
+ return settings;
579
+ }
580
+
581
+ function generateCommandFile(name, commands, stackName) {
582
+ const templates = {
583
+ test: `# Run tests\n\n\`\`\`bash\n${commands.test || '# Configure test command'}\n\`\`\``,
584
+ lint: `# Lint code\n\n\`\`\`bash\n${commands.lint || '# Configure lint command'}\n\`\`\``,
585
+ verify: `# Full verification\n\nRuns lint, test, and build.\n\n\`\`\`bash\n${commands.verify || '# Configure verify command'}\n\`\`\``,
586
+ setup: `# Setup project\n\n\`\`\`bash\n${commands.setup || '# Configure setup command'}\n\`\`\``,
587
+ pr: `# Create Pull Request\n\n1. Run /verify\n2. Commit changes\n3. Push to remote\n4. Create PR with \`gh pr create\``,
588
+ };
589
+ return templates[name] || '';
590
+ }
591
+
592
+ async function projectSetup(options = {}) {
593
+ const { dryRun = false } = options;
594
+ const projectPath = process.cwd();
595
+ const projectName = basename(projectPath);
596
+
597
+ banner();
598
+ console.log(` ${c.bold}Project Setup Wizard${c.reset}\n`);
599
+ console.log(` ${c.dim}Project:${c.reset} ${c.cyan}${projectName}${c.reset}`);
600
+ console.log(` ${c.dim}Path:${c.reset} ${projectPath}\n`);
601
+
602
+ console.log(` ${c.bold}Analyzing...${c.reset}\n`);
603
+
604
+ const stack = detectStack(projectPath);
605
+ const commands = generateCommands(stack);
606
+
607
+ // Show detection
608
+ console.log(` ${c.bold}Detected Stack${c.reset}\n`);
609
+ if (stack.stacks.length > 0) {
610
+ console.log(` Language: ${c.green}${stack.stacks.map(s => s.name).join(', ')}${c.reset}`);
611
+ } else {
612
+ console.log(` Language: ${c.yellow}Not detected${c.reset}`);
613
+ }
614
+ if (stack.framework) console.log(` Framework: ${c.green}${stack.framework.name}${c.reset}`);
615
+ if (stack.packageManager) console.log(` Package Manager: ${c.green}${stack.packageManager.name}${c.reset}`);
616
+ if (stack.monorepo) console.log(` Monorepo: ${c.green}${stack.monorepo.name}${c.reset}`);
617
+ if (stack.ci) console.log(` CI/CD: ${c.green}${stack.ci.name}${c.reset}`);
618
+
619
+ console.log(`\n ${c.bold}Generated Commands${c.reset}\n`);
620
+ if (commands.setup) console.log(` ${c.dim}setup:${c.reset} ${commands.setup}`);
621
+ if (commands.dev) console.log(` ${c.dim}dev:${c.reset} ${commands.dev}`);
622
+ if (commands.test) console.log(` ${c.dim}test:${c.reset} ${commands.test}`);
623
+ if (commands.lint) console.log(` ${c.dim}lint:${c.reset} ${commands.lint}`);
624
+ if (commands.build) console.log(` ${c.dim}build:${c.reset} ${commands.build}`);
625
+
626
+ const prompt = createPrompt();
627
+
628
+ try {
629
+ console.log('');
630
+ const confirmed = await prompt.confirm(' Proceed with this configuration?');
631
+
632
+ if (!confirmed) {
633
+ console.log(`\n ${c.yellow}Setup cancelled.${c.reset}\n`);
634
+ prompt.close();
635
+ return;
636
+ }
637
+
638
+ // Custom rules
639
+ console.log(`\n ${c.dim}Add project-specific rules (empty to skip):${c.reset}\n`);
640
+ const customRules = [];
641
+ let rule = await prompt.ask(' Rule: ');
642
+ while (rule) {
643
+ customRules.push(rule);
644
+ rule = await prompt.ask(' Rule: ');
645
+ }
646
+
647
+ prompt.close();
648
+
649
+ const config = { projectName, stack, commands, customRules };
650
+ const claudeDir = join(projectPath, '.claude');
651
+
652
+ if (dryRun) {
653
+ console.log(`\n ${c.yellow}DRY RUN${c.reset} - Would create:\n`);
654
+ console.log(' CLAUDE.md');
655
+ console.log(' .claude/settings.json');
656
+ console.log(' .claude/commands/test.md');
657
+ console.log(' .claude/commands/lint.md');
658
+ console.log(' .claude/commands/verify.md');
659
+ console.log(' .claude/commands/setup.md');
660
+ console.log(' .claude/commands/pr.md\n');
661
+ return;
662
+ }
663
+
664
+ // Create files
665
+ mkdirSync(join(claudeDir, 'commands'), { recursive: true });
666
+
667
+ const stackName = stack.stacks[0]?.name || 'Project';
668
+ const files = [
669
+ { path: 'CLAUDE.md', content: generateClaudeMd(config) },
670
+ { path: '.claude/settings.json', content: JSON.stringify(generateProjectSettings(config), null, 2) },
671
+ { path: '.claude/commands/test.md', content: generateCommandFile('test', commands, stackName) },
672
+ { path: '.claude/commands/lint.md', content: generateCommandFile('lint', commands, stackName) },
673
+ { path: '.claude/commands/verify.md', content: generateCommandFile('verify', commands, stackName) },
674
+ { path: '.claude/commands/setup.md', content: generateCommandFile('setup', commands, stackName) },
675
+ { path: '.claude/commands/pr.md', content: generateCommandFile('pr', commands, stackName) },
676
+ ];
677
+
678
+ console.log(`\n ${c.bold}Creating files${c.reset}\n`);
679
+ for (const file of files) {
680
+ writeFileSync(join(projectPath, file.path), file.content);
681
+ console.log(` ${c.green}+${c.reset} ${file.path}`);
682
+ }
683
+
684
+ console.log(`
685
+ ${c.green}${c.bold}╔═══════════════════════════════════════════════════════════╗${c.reset}
686
+ ${c.green}${c.bold}║${c.reset} ${c.green}${c.bold}║${c.reset}
687
+ ${c.green}${c.bold}║${c.reset} ${c.green}${c.bold}✓ Project Setup Complete!${c.reset} ${c.green}${c.bold}║${c.reset}
688
+ ${c.green}${c.bold}║${c.reset} ${c.green}${c.bold}║${c.reset}
689
+ ${c.green}${c.bold}╚═══════════════════════════════════════════════════════════╝${c.reset}
690
+
691
+ ${c.bold}Next steps:${c.reset}
692
+
693
+ 1. Review ${c.cyan}CLAUDE.md${c.reset} and adjust as needed
694
+ 2. Commit ${c.cyan}.claude/${c.reset} to your repository
695
+ 3. Use ${c.cyan}/test${c.reset}, ${c.cyan}/lint${c.reset}, ${c.cyan}/verify${c.reset} in Claude Code
696
+
697
+ `);
698
+ showFooter();
699
+
700
+ } catch (error) {
701
+ prompt.close();
702
+ throw error;
703
+ }
704
+ }
705
+
706
+ function showHelp() {
707
+ banner();
708
+
709
+ console.log(` ${c.bold}Usage${c.reset}
710
+
711
+ npx clauderc ${c.cyan}<command>${c.reset} [options]
712
+
713
+ ${c.bold}Commands${c.reset}
714
+
715
+ ${c.cyan}init${c.reset} Install global components (~/.claude/)
716
+ ${c.cyan}project${c.reset} Setup current project (.claude/ + CLAUDE.md)
717
+ ${c.cyan}update${c.reset} Update global components to latest version
718
+ ${c.cyan}list${c.reset} Show installed components
719
+ ${c.cyan}changelog${c.reset} Show version history
720
+ ${c.cyan}help${c.reset} Show this message
721
+
722
+ ${c.bold}Options${c.reset}
723
+
724
+ ${c.yellow}--force, -f${c.reset} Overwrite all files
725
+ ${c.yellow}--dry-run${c.reset} Preview changes without applying
726
+
727
+ ${c.bold}Examples${c.reset}
728
+
729
+ ${c.dim}# First time global setup${c.reset}
730
+ npx clauderc init
731
+
732
+ ${c.dim}# Setup current project (interactive)${c.reset}
733
+ npx clauderc project
734
+
735
+ ${c.dim}# Update global components${c.reset}
736
+ npx clauderc update
737
+
738
+ ${c.bold}Two-Level Setup${c.reset}
739
+
740
+ ${c.cyan}Global (~/.claude/)${c.reset}
741
+ ├── agents/ ${c.dim}# Reusable agents${c.reset}
742
+ ├── skills/ ${c.dim}# Reusable skills${c.reset}
743
+ ├── commands/ ${c.dim}# Default commands${c.reset}
744
+ └── templates/ ${c.dim}# Templates for project setup${c.reset}
745
+
746
+ ${c.cyan}Project (.claude/)${c.reset}
747
+ ├── commands/ ${c.dim}# Project-specific commands${c.reset}
748
+ ├── settings.json ${c.dim}# Permissions & hooks${c.reset}
749
+ └── CLAUDE.md ${c.dim}# Project context for Claude${c.reset}
750
+
751
+ ${c.bold}Supported Stacks${c.reset}
752
+
753
+ Node.js/TypeScript, Python, Go, Rust, Java/Kotlin,
754
+ PHP, Ruby, C#/.NET, Elixir, Swift, Dart/Flutter
755
+
756
+ `);
757
+ showFooter();
758
+ }
759
+
760
+ // Check Node.js version
761
+ const nodeVersion = process.versions.node.split('.').map(Number);
762
+ if (nodeVersion[0] < 16 || (nodeVersion[0] === 16 && nodeVersion[1] < 7)) {
763
+ console.error(`\n ${c.red}Error:${c.reset} Node.js 16.7+ required (you have ${process.versions.node})\n`);
764
+ process.exit(1);
765
+ }
766
+
767
+ // Parse arguments
768
+ const args = process.argv.slice(2);
769
+ const command = args.find(a => !a.startsWith('-')) || 'help';
770
+ const flags = {
771
+ force: args.includes('--force') || args.includes('-f'),
772
+ dryRun: args.includes('--dry-run'),
773
+ };
774
+
775
+ switch (command) {
776
+ case 'init':
777
+ case 'install':
778
+ init({ force: flags.force, dryRun: flags.dryRun });
779
+ break;
780
+ case 'project':
781
+ case 'setup':
782
+ projectSetup({ dryRun: flags.dryRun });
783
+ break;
784
+ case 'update':
785
+ case 'upgrade':
786
+ update({ dryRun: flags.dryRun });
787
+ break;
788
+ case 'list':
789
+ case 'ls':
790
+ listInstalled();
791
+ break;
792
+ case 'changelog':
793
+ case 'changes':
794
+ showChangelog();
795
+ break;
796
+ case 'help':
797
+ case '--help':
798
+ case '-h':
799
+ default:
800
+ showHelp();
801
+ break;
802
+ }