ccsetup 1.0.6 → 1.0.7

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.
@@ -4,16 +4,111 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const readline = require('readline');
6
6
 
7
- const projectName = process.argv[2] || '.';
7
+ // Parse CLI arguments
8
+ const args = process.argv.slice(2);
9
+ const flags = {
10
+ force: false,
11
+ dryRun: false,
12
+ help: false,
13
+ allAgents: false,
14
+ noAgents: false,
15
+ agents: false
16
+ };
17
+
18
+ let projectName = '.';
19
+
20
+ // Process arguments
21
+ for (let i = 0; i < args.length; i++) {
22
+ const arg = args[i];
23
+ if (arg === '--force' || arg === '-f') {
24
+ flags.force = true;
25
+ } else if (arg === '--dry-run' || arg === '-d') {
26
+ flags.dryRun = true;
27
+ } else if (arg === '--help' || arg === '-h') {
28
+ flags.help = true;
29
+ } else if (arg === '--all-agents') {
30
+ flags.allAgents = true;
31
+ } else if (arg === '--no-agents') {
32
+ flags.noAgents = true;
33
+ } else if (arg === '--agents') {
34
+ flags.agents = true;
35
+ } else if (!arg.startsWith('-')) {
36
+ projectName = arg;
37
+ }
38
+ }
39
+
40
+ // Show help if requested
41
+ if (flags.help) {
42
+ console.log(`
43
+ Usage: ccsetup [project-name] [options]
44
+
45
+ Options:
46
+ --force, -f Skip all prompts and overwrite existing files
47
+ --dry-run, -d Show what would be done without making changes
48
+ --agents Interactive agent selection mode
49
+ --all-agents Include all agents without prompting
50
+ --no-agents Skip agent selection entirely
51
+ --help, -h Show this help message
52
+
53
+ Examples:
54
+ ccsetup # Create in current directory
55
+ ccsetup my-project # Create in new directory
56
+ ccsetup . --force # Overwrite files in current directory
57
+ ccsetup my-app --dry-run # Preview changes without creating files
58
+ ccsetup --agents # Interactive agent selection only
59
+ ccsetup my-app --all-agents # Include all agents automatically
60
+ `);
61
+ process.exit(0);
62
+ }
63
+
64
+ // Validate project name
65
+ function validateProjectName(name) {
66
+ // Check for path traversal attempts
67
+ if (name.includes('..') || path.isAbsolute(name)) {
68
+ throw new Error('Invalid project name: Path traversal or absolute paths are not allowed');
69
+ }
70
+
71
+ // Check for invalid characters
72
+ const invalidChars = /[<>:"|?*\0]/;
73
+ if (invalidChars.test(name)) {
74
+ throw new Error('Invalid project name: Contains invalid characters');
75
+ }
76
+
77
+ return true;
78
+ }
79
+
80
+ // Validate conflicting flags
81
+ if (flags.allAgents && flags.noAgents) {
82
+ console.error('Error: Cannot use --all-agents and --no-agents together');
83
+ process.exit(1);
84
+ }
85
+
86
+ // Validate the project name
87
+ if (projectName !== '.') {
88
+ try {
89
+ validateProjectName(projectName);
90
+ } catch (error) {
91
+ console.error(`Error: ${error.message}`);
92
+ process.exit(1);
93
+ }
94
+ }
95
+
8
96
  const targetDir = path.resolve(process.cwd(), projectName);
9
97
  const templateDir = path.join(__dirname, '..', 'template');
10
98
 
11
- const rl = readline.createInterface({
12
- input: process.stdin,
13
- output: process.stdout
14
- });
99
+ // Create readline interface only if needed
100
+ let rl = null;
101
+ if (!flags.force && !flags.dryRun) {
102
+ rl = readline.createInterface({
103
+ input: process.stdin,
104
+ output: process.stdout
105
+ });
106
+ }
15
107
 
16
108
  function prompt(question) {
109
+ if (!rl) {
110
+ throw new Error('Readline interface not initialized');
111
+ }
17
112
  return new Promise((resolve) => {
18
113
  rl.question(question, (answer) => {
19
114
  resolve(answer.toLowerCase().trim());
@@ -32,105 +127,781 @@ function normalizeConflictStrategy(input) {
32
127
  return null;
33
128
  }
34
129
 
35
- async function main() {
36
- console.log(`Creating Claude Code project in ${targetDir}...`);
130
+ // Function to validate agent files for security
131
+ function validateAgentFile(file) {
132
+ // Ensure the file is just a basename without directory separators
133
+ const basename = path.basename(file);
134
+ if (file !== basename) {
135
+ return false;
136
+ }
137
+ // Additional validation: ensure it's a markdown file
138
+ if (!file.endsWith('.md')) {
139
+ return false;
140
+ }
141
+ // Reject files with suspicious patterns
142
+ if (file.includes('..') || file.includes('./') || file.includes('\\')) {
143
+ return false;
144
+ }
145
+ return true;
146
+ }
147
+
148
+ // Function to check if Claude Code is installed
149
+ function checkClaudeCode() {
150
+ const claudeDir = path.join(targetDir, '.claude');
151
+ const claudeCodeFile = path.join(targetDir, 'claude_code.txt');
152
+ const hasClaudeDir = fs.existsSync(claudeDir);
153
+ const hasClaudeCodeFile = fs.existsSync(claudeCodeFile);
154
+
155
+ return {
156
+ isInstalled: hasClaudeDir || hasClaudeCodeFile,
157
+ hasClaudeDir: hasClaudeDir,
158
+ hasClaudeCodeFile: hasClaudeCodeFile
159
+ };
160
+ }
37
161
 
38
- if (projectName !== '.') {
39
- if (!fs.existsSync(targetDir)) {
40
- fs.mkdirSync(targetDir, { recursive: true });
162
+ // Function to initialize .claude directory structure
163
+ async function initializeClaudeDirectory(selectedAgentFiles, conflictStrategy, dryRun) {
164
+ const claudeDir = path.join(targetDir, '.claude');
165
+ const claudeAgentsDir = path.join(claudeDir, 'agents');
166
+ const templateClaudeDir = path.join(templateDir, '.claude');
167
+
168
+ const createdItems = [];
169
+ const skippedItems = [];
170
+
171
+ try {
172
+ // Create .claude directory
173
+ if (!fs.existsSync(claudeDir)) {
174
+ if (!dryRun) {
175
+ fs.mkdirSync(claudeDir, { recursive: true });
176
+ }
177
+ createdItems.push('.claude/');
178
+ if (dryRun) {
179
+ console.log(' 📁 Would create directory: .claude/');
180
+ }
181
+ } else {
182
+ skippedItems.push('.claude/');
41
183
  }
184
+
185
+ // Create .claude/agents directory
186
+ if (!fs.existsSync(claudeAgentsDir)) {
187
+ if (!dryRun) {
188
+ fs.mkdirSync(claudeAgentsDir, { recursive: true });
189
+ }
190
+ createdItems.push('.claude/agents/');
191
+ if (dryRun) {
192
+ console.log(' 📁 Would create directory: .claude/agents/');
193
+ }
194
+ } else {
195
+ skippedItems.push('.claude/agents/');
196
+ }
197
+
198
+ // Copy .claude/README.md
199
+ const claudeReadmeSrc = path.join(templateClaudeDir, 'README.md');
200
+ const claudeReadmeDest = path.join(claudeDir, 'README.md');
201
+ if (fs.existsSync(claudeReadmeSrc)) {
202
+ if (!fs.existsSync(claudeReadmeDest)) {
203
+ if (!dryRun) {
204
+ fs.copyFileSync(claudeReadmeSrc, claudeReadmeDest);
205
+ }
206
+ createdItems.push('.claude/README.md');
207
+ if (dryRun) {
208
+ console.log(' ✨ Would copy: .claude/README.md');
209
+ }
210
+ } else {
211
+ if (conflictStrategy === 'overwrite') {
212
+ if (!dryRun) {
213
+ fs.copyFileSync(claudeReadmeSrc, claudeReadmeDest);
214
+ }
215
+ if (dryRun) {
216
+ console.log(' ♻️ Would replace: .claude/README.md');
217
+ }
218
+ } else if (conflictStrategy === 'rename') {
219
+ const ext = path.extname(claudeReadmeDest);
220
+ const baseName = path.basename(claudeReadmeDest, ext);
221
+ const dirName = path.dirname(claudeReadmeDest);
222
+ let newDest = path.join(dirName, `${baseName}-ccsetup${ext}`);
223
+ let counter = 1;
224
+ while (fs.existsSync(newDest)) {
225
+ newDest = path.join(dirName, `${baseName}-ccsetup-${counter}${ext}`);
226
+ counter++;
227
+ }
228
+ if (!dryRun) {
229
+ fs.copyFileSync(claudeReadmeSrc, newDest);
230
+ }
231
+ const relativePath = path.relative(claudeDir, newDest);
232
+ createdItems.push(relativePath);
233
+ if (dryRun) {
234
+ console.log(` 📄 Would create: ${relativePath}`);
235
+ } else {
236
+ console.log(` 📄 Created: ${relativePath}`);
237
+ }
238
+ } else {
239
+ skippedItems.push('.claude/README.md');
240
+ if (dryRun) {
241
+ console.log(' ⏭️ Would skip: .claude/README.md');
242
+ }
243
+ }
244
+ }
245
+ }
246
+
247
+ // Copy .claude/agents/README.md
248
+ const agentsReadmeSrc = path.join(templateClaudeDir, 'agents', 'README.md');
249
+ const agentsReadmeDest = path.join(claudeAgentsDir, 'README.md');
250
+ if (fs.existsSync(agentsReadmeSrc)) {
251
+ if (!fs.existsSync(agentsReadmeDest)) {
252
+ if (!dryRun) {
253
+ fs.copyFileSync(agentsReadmeSrc, agentsReadmeDest);
254
+ }
255
+ createdItems.push('.claude/agents/README.md');
256
+ if (dryRun) {
257
+ console.log(' ✨ Would copy: .claude/agents/README.md');
258
+ }
259
+ } else {
260
+ if (conflictStrategy === 'overwrite') {
261
+ if (!dryRun) {
262
+ fs.copyFileSync(agentsReadmeSrc, agentsReadmeDest);
263
+ }
264
+ if (dryRun) {
265
+ console.log(' ♻️ Would replace: .claude/agents/README.md');
266
+ }
267
+ } else if (conflictStrategy === 'rename') {
268
+ const ext = path.extname(agentsReadmeDest);
269
+ const baseName = path.basename(agentsReadmeDest, ext);
270
+ const dirName = path.dirname(agentsReadmeDest);
271
+ let newDest = path.join(dirName, `${baseName}-ccsetup${ext}`);
272
+ let counter = 1;
273
+ while (fs.existsSync(newDest)) {
274
+ newDest = path.join(dirName, `${baseName}-ccsetup-${counter}${ext}`);
275
+ counter++;
276
+ }
277
+ if (!dryRun) {
278
+ fs.copyFileSync(agentsReadmeSrc, newDest);
279
+ }
280
+ const relativePath = path.relative(claudeDir, newDest);
281
+ createdItems.push(relativePath);
282
+ if (dryRun) {
283
+ console.log(` 📄 Would create: ${relativePath}`);
284
+ } else {
285
+ console.log(` 📄 Created: ${relativePath}`);
286
+ }
287
+ } else {
288
+ skippedItems.push('.claude/agents/README.md');
289
+ if (dryRun) {
290
+ console.log(' ⏭️ Would skip: .claude/agents/README.md');
291
+ }
292
+ }
293
+ }
294
+ }
295
+
296
+ // Copy selected agents to .claude/agents
297
+ const templateAgentsDir = path.join(templateDir, 'agents');
298
+ let copiedAgents = 0;
299
+ let skippedAgents = 0;
300
+
301
+ for (const agentFile of selectedAgentFiles) {
302
+ // Validate agent file before processing
303
+ if (!validateAgentFile(agentFile)) {
304
+ console.warn(`⚠️ Skipping invalid agent file: ${agentFile}`);
305
+ continue;
306
+ }
307
+
308
+ // Use basename to ensure safety
309
+ const safeAgentFile = path.basename(agentFile);
310
+ const agentSrc = path.join(templateAgentsDir, safeAgentFile);
311
+ const agentDest = path.join(claudeAgentsDir, safeAgentFile);
312
+
313
+ // Additional validation: ensure source is within template directory
314
+ const normalizedAgentSrc = path.normalize(agentSrc);
315
+ const normalizedTemplateAgentsDir = path.normalize(templateAgentsDir);
316
+ if (!normalizedAgentSrc.startsWith(normalizedTemplateAgentsDir)) {
317
+ console.warn(`⚠️ Skipping agent file outside template directory: ${agentFile}`);
318
+ continue;
319
+ }
320
+
321
+ if (fs.existsSync(agentSrc)) {
322
+ if (!fs.existsSync(agentDest)) {
323
+ if (!dryRun) {
324
+ fs.copyFileSync(agentSrc, agentDest);
325
+ }
326
+ copiedAgents++;
327
+ createdItems.push(`.claude/agents/${safeAgentFile}`);
328
+ if (dryRun) {
329
+ console.log(` ✨ Would copy: .claude/agents/${safeAgentFile}`);
330
+ }
331
+ } else {
332
+ if (conflictStrategy === 'overwrite') {
333
+ if (!dryRun) {
334
+ fs.copyFileSync(agentSrc, agentDest);
335
+ }
336
+ copiedAgents++;
337
+ if (dryRun) {
338
+ console.log(` ♻️ Would replace: .claude/agents/${safeAgentFile}`);
339
+ }
340
+ } else if (conflictStrategy === 'rename') {
341
+ const ext = path.extname(agentDest);
342
+ const baseName = path.basename(agentDest, ext);
343
+ const dirName = path.dirname(agentDest);
344
+ let newDest = path.join(dirName, `${baseName}-ccsetup${ext}`);
345
+ let counter = 1;
346
+ while (fs.existsSync(newDest)) {
347
+ newDest = path.join(dirName, `${baseName}-ccsetup-${counter}${ext}`);
348
+ counter++;
349
+ }
350
+ if (!dryRun) {
351
+ fs.copyFileSync(agentSrc, newDest);
352
+ }
353
+ copiedAgents++;
354
+ const relativePath = path.relative(claudeDir, newDest);
355
+ createdItems.push(relativePath);
356
+ if (dryRun) {
357
+ console.log(` 📄 Would create: ${relativePath}`);
358
+ } else {
359
+ console.log(` 📄 Created: ${relativePath}`);
360
+ }
361
+ } else {
362
+ skippedAgents++;
363
+ skippedItems.push(`.claude/agents/${safeAgentFile}`);
364
+ if (dryRun) {
365
+ console.log(` ⏭️ Would skip: .claude/agents/${safeAgentFile}`);
366
+ }
367
+ }
368
+ }
369
+ }
370
+ }
371
+
372
+ return {
373
+ createdItems,
374
+ skippedItems,
375
+ copiedAgents,
376
+ skippedAgents
377
+ };
378
+
379
+ } catch (error) {
380
+ throw new Error(`Failed to initialize .claude directory: ${error.message}`);
42
381
  }
382
+ }
383
+
384
+ // Function to parse agent frontmatter
385
+ function parseAgentFrontmatter(filePath) {
386
+ try {
387
+ const content = fs.readFileSync(filePath, 'utf8');
388
+ const lines = content.split('\n');
389
+
390
+ if (lines[0] !== '---') {
391
+ return null;
392
+ }
393
+
394
+ let name = '';
395
+ let description = '';
396
+ let inFrontmatter = true;
397
+ let lineIndex = 1;
398
+
399
+ while (lineIndex < lines.length && inFrontmatter) {
400
+ const line = lines[lineIndex];
401
+ if (line === '---') {
402
+ inFrontmatter = false;
403
+ } else if (line.startsWith('name:')) {
404
+ name = line.substring(5).trim();
405
+ } else if (line.startsWith('description:')) {
406
+ description = line.substring(12).trim();
407
+ }
408
+ lineIndex++;
409
+ }
410
+
411
+ return { name, description };
412
+ } catch (error) {
413
+ return null;
414
+ }
415
+ }
416
+
417
+ // Function to get available agents
418
+ function getAvailableAgents() {
419
+ const agentsDir = path.join(templateDir, 'agents');
420
+ const agents = [];
421
+
422
+ try {
423
+ const files = fs.readdirSync(agentsDir);
424
+ for (const file of files) {
425
+ if (file.endsWith('.md') && file !== 'README.md') {
426
+ const filePath = path.join(agentsDir, file);
427
+ const metadata = parseAgentFrontmatter(filePath);
428
+ if (metadata && metadata.name) {
429
+ agents.push({
430
+ file,
431
+ name: metadata.name,
432
+ description: metadata.description || 'No description available'
433
+ });
434
+ }
435
+ }
436
+ }
437
+ } catch (error) {
438
+ console.error('Warning: Could not read agents directory:', error.message);
439
+ }
440
+
441
+ return agents.sort((a, b) => a.name.localeCompare(b.name));
442
+ }
443
+
444
+ // Dynamic import for ESM module
445
+ async function importCheckbox() {
446
+ try {
447
+ const module = await import('@inquirer/checkbox');
448
+ return module.default;
449
+ } catch (error) {
450
+ console.error('Error: Failed to load @inquirer/checkbox. Please ensure it is installed.');
451
+ console.error('Run: npm install @inquirer/checkbox');
452
+ process.exit(1);
453
+ }
454
+ }
455
+
456
+ async function selectAgents(availableAgents) {
457
+ const checkbox = await importCheckbox();
458
+
459
+ // ANSI color codes
460
+ const colors = {
461
+ cyan: '\x1b[36m',
462
+ yellow: '\x1b[33m',
463
+ green: '\x1b[32m',
464
+ blue: '\x1b[34m',
465
+ magenta: '\x1b[35m',
466
+ bold: '\x1b[1m',
467
+ dim: '\x1b[2m',
468
+ reset: '\x1b[0m'
469
+ };
470
+
471
+ const choices = availableAgents.map(agent => ({
472
+ name: `${colors.cyan}${colors.bold}${agent.name}${colors.reset}\n ${colors.dim}${agent.description}${colors.reset}`,
473
+ value: agent.file,
474
+ checked: false
475
+ }));
476
+
477
+ console.log('\n🤖 Select agents to include in your Claude Code project\n');
478
+ console.log(`${colors.dim}Use arrow keys to navigate, space to select/deselect, 'a' to toggle all${colors.reset}\n`);
479
+
480
+ const selectedFiles = await checkbox({
481
+ message: `${colors.bold}Choose your agents:${colors.reset}`,
482
+ choices,
483
+ pageSize: 10,
484
+ loop: false
485
+ });
486
+
487
+ // Validate selected files to prevent path traversal
488
+ const validatedFiles = selectedFiles.filter(file => {
489
+ // Use centralized validation function
490
+ if (!validateAgentFile(file)) {
491
+ console.warn(`⚠️ Skipping invalid agent file: ${file}`);
492
+ return false;
493
+ }
494
+ // Ensure it exists in available agents
495
+ if (!availableAgents.some(agent => agent.file === file)) {
496
+ console.warn(`⚠️ Skipping unknown agent file: ${file}`);
497
+ return false;
498
+ }
499
+ return true;
500
+ });
501
+
502
+ return validatedFiles;
503
+ }
504
+
505
+ async function main() {
506
+ try {
507
+ // Ensure template directory exists
508
+ if (!fs.existsSync(templateDir)) {
509
+ throw new Error(`Template directory not found: ${templateDir}`);
510
+ }
511
+
512
+ // Handle --agents flag for agent selection only
513
+ if (flags.agents) {
514
+ console.log('🤖 Interactive Agent Selection\n');
515
+ const availableAgents = getAvailableAgents();
516
+
517
+ if (availableAgents.length === 0) {
518
+ console.log('No agents available for selection.');
519
+ process.exit(0);
520
+ }
521
+
522
+ // Perform interactive selection
523
+ const selectedAgentFiles = await selectAgents(availableAgents);
524
+
525
+ if (selectedAgentFiles.length === 0) {
526
+ console.log('\n❌ No agents selected.');
527
+ } else {
528
+ // ANSI color codes
529
+ const colors = {
530
+ cyan: '\x1b[36m',
531
+ green: '\x1b[32m',
532
+ yellow: '\x1b[33m',
533
+ bold: '\x1b[1m',
534
+ dim: '\x1b[2m',
535
+ reset: '\x1b[0m'
536
+ };
537
+
538
+ console.log(`\n${colors.green}${colors.bold}✅ You selected ${selectedAgentFiles.length} agent${selectedAgentFiles.length === 1 ? '' : 's'}:${colors.reset}\n`);
539
+
540
+ // Show selected agents with descriptions
541
+ selectedAgentFiles.forEach(file => {
542
+ const agent = availableAgents.find(a => a.file === file);
543
+ if (agent) {
544
+ console.log(` ${colors.cyan}${colors.bold}${agent.name}${colors.reset}`);
545
+ console.log(` ${colors.dim}${agent.description}${colors.reset}\n`);
546
+ }
547
+ });
548
+
549
+ console.log(`${colors.yellow}${colors.bold}📝 Next Steps:${colors.reset}`);
550
+ console.log(`${colors.dim}1. Make sure Claude Code is initialized: ${colors.reset}${colors.cyan}claude init${colors.reset}`);
551
+ console.log(`${colors.dim}2. Run: ${colors.reset}${colors.cyan}npx ccsetup${colors.reset}`);
552
+ console.log(`${colors.dim}3. Select the same agents when prompted${colors.reset}\n`);
553
+ }
554
+
555
+ process.exit(0);
556
+ }
557
+
558
+ // Additional path validation
559
+ const normalizedTarget = path.normalize(targetDir);
560
+ const normalizedCwd = path.normalize(process.cwd());
561
+
562
+ // Ensure target is within or equal to cwd for safety
563
+ if (projectName !== '.' && !normalizedTarget.startsWith(normalizedCwd)) {
564
+ throw new Error('Target directory must be within the current working directory');
565
+ }
566
+
567
+ if (flags.dryRun) {
568
+ console.log('🔍 DRY RUN MODE - No files will be created or modified\n');
569
+ }
570
+
571
+ // Check for Claude Code installation
572
+ const claudeStatus = checkClaudeCode();
573
+ if (projectName === '.') {
574
+ if (flags.dryRun) {
575
+ console.log('⚠️ Note: Claude Code detection skipped in dry-run mode for current directory\n');
576
+ } else if (!claudeStatus.isInstalled && !claudeStatus.hasClaudeDir) {
577
+ // Offer to create .claude directory
578
+ if (flags.force) {
579
+ console.log('📁 Creating .claude directory structure...\n');
580
+ } else {
581
+ console.log('⚠️ Claude Code not detected in this project.');
582
+ console.log('ccsetup can create the .claude directory structure for you.\n');
583
+ const answer = await prompt('Would you like to create .claude directory? (yes/no): ');
584
+ if (answer !== 'yes') {
585
+ console.log('\nTo manually initialize Claude Code:');
586
+ console.log('1. Install Claude Code CLI: https://docs.anthropic.com/claude-code/quickstart');
587
+ console.log('2. Run \'claude init\' in your project directory');
588
+ console.log('3. Then run \'npx ccsetup\' again\n');
589
+ console.log('Aborting setup.');
590
+ if (rl) rl.close();
591
+ process.exit(0);
592
+ }
593
+ console.log('');
594
+ }
595
+ }
596
+ }
597
+
598
+ // Escape targetDir for safe display
599
+ const safeTargetDir = targetDir.replace(/[^\w\s\-./\\:]/g, '');
600
+ console.log(`${flags.dryRun ? 'Would create' : 'Creating'} Claude Code project in ${safeTargetDir}...`);
601
+
602
+ if (projectName !== '.') {
603
+ if (!fs.existsSync(targetDir)) {
604
+ if (!flags.dryRun) {
605
+ try {
606
+ fs.mkdirSync(targetDir, { recursive: true });
607
+ } catch (error) {
608
+ if (error.code === 'EACCES') {
609
+ throw new Error(`Permission denied: Cannot create directory ${targetDir}`);
610
+ } else if (error.code === 'ENOSPC') {
611
+ throw new Error('No space left on device');
612
+ }
613
+ throw error;
614
+ }
615
+ } else {
616
+ console.log(`Would create directory: ${targetDir}`);
617
+ }
618
+ }
619
+
620
+ // Check Claude Code in new directory after creation
621
+ const newDirClaudeStatus = checkClaudeCode();
622
+ if (!flags.dryRun && !newDirClaudeStatus.isInstalled) {
623
+ console.log('\n⚠️ Note: Claude Code is not initialized in the new project directory.');
624
+ console.log('After setup, remember to:');
625
+ console.log(`1. cd ${JSON.stringify(projectName)}`);
626
+ console.log('2. Run \'claude init\' to initialize Claude Code\n');
627
+ }
628
+ }
43
629
 
44
630
  const fileConflicts = [];
45
631
  const dirConflicts = [];
46
632
  const allItems = [];
633
+
634
+ // Group conflicts by category
635
+ const conflictsByCategory = {
636
+ 'CLAUDE.md': [],
637
+ 'agents': [],
638
+ 'docs': [],
639
+ 'plans': [],
640
+ 'tickets': []
641
+ };
642
+
643
+ // Store conflict strategies per category
644
+ const conflictStrategies = {
645
+ 'CLAUDE.md': 'skip',
646
+ 'agents': 'skip',
647
+ 'docs': 'skip',
648
+ 'plans': 'skip',
649
+ 'tickets': 'skip'
650
+ };
47
651
 
48
- function scanTemplate(src, dest, relativePath = '') {
49
- const exists = fs.existsSync(src);
50
- const stats = exists && fs.statSync(src);
51
- const isDirectory = exists && stats.isDirectory();
52
-
53
- if (isDirectory) {
54
- allItems.push({
55
- src,
56
- dest,
57
- type: 'directory',
58
- relativePath,
59
- exists: fs.existsSync(dest)
652
+ // Get available agents for selection
653
+ const availableAgents = getAvailableAgents();
654
+ let selectedAgentFiles = [];
655
+
656
+
657
+ // Determine which agents to include
658
+ if (flags.noAgents) {
659
+ selectedAgentFiles = [];
660
+ console.log('\n⏭️ Skipping agent selection (--no-agents flag)');
661
+ } else if (flags.allAgents) {
662
+ selectedAgentFiles = availableAgents.map(a => a.file).filter(validateAgentFile);
663
+ console.log(`\n✅ Including all ${selectedAgentFiles.length} agents (--all-agents flag)`);
664
+ } else if (!flags.dryRun) {
665
+ // Close readline interface before using inquirer
666
+ if (rl) {
667
+ rl.close();
668
+ rl = null;
669
+ }
670
+
671
+ // Interactive selection
672
+ selectedAgentFiles = await selectAgents(availableAgents);
673
+ console.log(`\n✅ Selected ${selectedAgentFiles.length} agent${selectedAgentFiles.length === 1 ? '' : 's'}`);
674
+
675
+ // Recreate readline interface after agent selection
676
+ if (!flags.force && !flags.dryRun) {
677
+ rl = readline.createInterface({
678
+ input: process.stdin,
679
+ output: process.stdout
60
680
  });
681
+ }
682
+ } else {
683
+ // In dry-run mode, show what would happen
684
+ console.log(`\nWould prompt for agent selection from ${availableAgents.length} available agents`);
685
+ selectedAgentFiles = availableAgents.map(a => a.file).filter(validateAgentFile); // Include all for scanning purposes
686
+ }
687
+
688
+ function scanTemplate(src, dest, relativePath = '', skipAgents = false) {
689
+ try {
690
+ // Validate paths to prevent traversal
691
+ const normalizedSrc = path.normalize(src);
692
+ const normalizedDest = path.normalize(dest);
61
693
 
62
- if (fs.existsSync(dest)) {
63
- dirConflicts.push(relativePath || '.');
694
+ if (!normalizedSrc.startsWith(path.normalize(templateDir))) {
695
+ throw new Error('Source path escapes template directory');
64
696
  }
65
697
 
66
- fs.readdirSync(src).forEach(childItem => {
67
- scanTemplate(
68
- path.join(src, childItem),
69
- path.join(dest, childItem),
70
- path.join(relativePath, childItem)
71
- );
72
- });
73
- } else {
74
- allItems.push({
75
- src,
76
- dest,
77
- type: 'file',
78
- relativePath,
79
- exists: fs.existsSync(dest)
80
- });
698
+ if (!normalizedDest.startsWith(path.normalize(targetDir))) {
699
+ throw new Error('Destination path escapes target directory');
700
+ }
81
701
 
82
- if (fs.existsSync(dest)) {
83
- fileConflicts.push(relativePath);
702
+ const exists = fs.existsSync(src);
703
+ const stats = exists && fs.statSync(src);
704
+ const isDirectory = exists && stats.isDirectory();
705
+
706
+ if (isDirectory) {
707
+ // Skip .claude directory as it will be handled separately
708
+ if (path.basename(src) === '.claude') {
709
+ return; // Don't process .claude directory in regular template scan
710
+ }
711
+
712
+ // Skip agents directory if we're handling it separately
713
+ if (skipAgents && path.basename(src) === 'agents') {
714
+ // Only scan selected agents
715
+ allItems.push({
716
+ src,
717
+ dest,
718
+ type: 'directory',
719
+ relativePath,
720
+ exists: fs.existsSync(dest)
721
+ });
722
+
723
+ if (fs.existsSync(dest)) {
724
+ dirConflicts.push(relativePath || '.');
725
+ }
726
+
727
+ // Add selected agent files
728
+ for (const agentFile of selectedAgentFiles) {
729
+ // Additional validation before processing
730
+ if (!validateAgentFile(agentFile)) {
731
+ console.warn(`⚠️ Skipping invalid agent file: ${agentFile}`);
732
+ continue;
733
+ }
734
+
735
+ // Ensure we're only dealing with basenames
736
+ const safeAgentFile = path.basename(agentFile);
737
+ const agentSrc = path.join(src, safeAgentFile);
738
+ const agentDest = path.join(dest, safeAgentFile);
739
+ const agentRelPath = path.join(relativePath, safeAgentFile);
740
+
741
+ // Validate that the source path is within the template agents directory
742
+ const normalizedAgentSrc = path.normalize(agentSrc);
743
+ const normalizedTemplateAgentsDir = path.normalize(src);
744
+ if (!normalizedAgentSrc.startsWith(normalizedTemplateAgentsDir)) {
745
+ console.warn(`⚠️ Skipping agent file outside template directory: ${agentFile}`);
746
+ continue;
747
+ }
748
+
749
+ if (fs.existsSync(agentSrc)) {
750
+ allItems.push({
751
+ src: agentSrc,
752
+ dest: agentDest,
753
+ type: 'file',
754
+ relativePath: agentRelPath,
755
+ exists: fs.existsSync(agentDest)
756
+ });
757
+
758
+ if (fs.existsSync(agentDest)) {
759
+ fileConflicts.push(agentRelPath);
760
+ conflictsByCategory['agents'].push(agentRelPath);
761
+ }
762
+ }
763
+ }
764
+
765
+ // Always include README.md
766
+ const readmeSrc = path.join(src, 'README.md');
767
+ const readmeDest = path.join(dest, 'README.md');
768
+ if (fs.existsSync(readmeSrc)) {
769
+ allItems.push({
770
+ src: readmeSrc,
771
+ dest: readmeDest,
772
+ type: 'file',
773
+ relativePath: path.join(relativePath, 'README.md'),
774
+ exists: fs.existsSync(readmeDest)
775
+ });
776
+
777
+ if (fs.existsSync(readmeDest)) {
778
+ const readmePath = path.join(relativePath, 'README.md');
779
+ fileConflicts.push(readmePath);
780
+ conflictsByCategory['agents'].push(readmePath);
781
+ }
782
+ }
783
+
784
+ return; // Don't recurse into agents directory
785
+ }
786
+
787
+ allItems.push({
788
+ src,
789
+ dest,
790
+ type: 'directory',
791
+ relativePath,
792
+ exists: fs.existsSync(dest)
793
+ });
794
+
795
+ if (fs.existsSync(dest)) {
796
+ dirConflicts.push(relativePath || '.');
797
+ }
798
+
799
+ fs.readdirSync(src).forEach(childItem => {
800
+ scanTemplate(
801
+ path.join(src, childItem),
802
+ path.join(dest, childItem),
803
+ path.join(relativePath, childItem),
804
+ skipAgents
805
+ );
806
+ });
807
+ } else {
808
+ allItems.push({
809
+ src,
810
+ dest,
811
+ type: 'file',
812
+ relativePath,
813
+ exists: fs.existsSync(dest)
814
+ });
815
+
816
+ if (fs.existsSync(dest)) {
817
+ fileConflicts.push(relativePath);
818
+
819
+ // Categorize the conflict
820
+ if (relativePath === 'CLAUDE.md') {
821
+ conflictsByCategory['CLAUDE.md'].push(relativePath);
822
+ } else if (relativePath.startsWith('agents/')) {
823
+ conflictsByCategory['agents'].push(relativePath);
824
+ } else if (relativePath.startsWith('docs/')) {
825
+ conflictsByCategory['docs'].push(relativePath);
826
+ } else if (relativePath.startsWith('plans/')) {
827
+ conflictsByCategory['plans'].push(relativePath);
828
+ } else if (relativePath.startsWith('tickets/')) {
829
+ conflictsByCategory['tickets'].push(relativePath);
830
+ }
831
+ }
84
832
  }
833
+ } catch (error) {
834
+ throw new Error(`Error scanning template: ${error.message}`);
85
835
  }
86
836
  }
87
837
 
88
- scanTemplate(templateDir, targetDir);
89
-
90
- let conflictStrategy = 'skip';
91
-
92
- if (dirConflicts.length > 0) {
93
- console.log('\n⚠️ The following directories already exist:');
94
- dirConflicts.forEach(dir => console.log(` - ${dir}/`));
95
- }
838
+ scanTemplate(templateDir, targetDir, '', true);
96
839
 
97
- if (fileConflicts.length > 0) {
98
- console.log('\n⚠️ The following files already exist:');
99
- fileConflicts.forEach(file => console.log(` - ${file}`));
100
-
101
- console.log('\n📋 This choice will apply to ALL conflicting files listed above.');
102
- console.log('\nConflict resolution options:');
103
- console.log(' 1) skip (s) - Keep your existing files, only copy new files');
104
- console.log(' 2) rename (r) - Keep your files, save template files with -ccsetup suffix');
105
- console.log(' 3) overwrite (o) - Replace ALL existing files with template versions');
106
- console.log('\nExamples: type "skip", "s", or "1" for the first option');
107
-
108
- const userInput = await prompt('\nYour choice [skip/rename/overwrite or s/r/o]: ');
109
- conflictStrategy = normalizeConflictStrategy(userInput);
110
-
111
- if (!conflictStrategy) {
112
- console.log(`\n❌ Invalid option: "${userInput}". Please use: skip/s/1, rename/r/2, or overwrite/o/3`);
113
- rl.close();
114
- process.exit(1);
840
+ // Handle force flag
841
+ if (flags.force) {
842
+ // Set all strategies to overwrite
843
+ Object.keys(conflictStrategies).forEach(key => {
844
+ conflictStrategies[key] = 'overwrite';
845
+ });
846
+ if (fileConflicts.length > 0 || dirConflicts.length > 0) {
847
+ console.log('\n⚠️ Force mode enabled - existing files will be overwritten');
115
848
  }
116
-
117
- if (conflictStrategy === 'overwrite') {
118
- const confirm = await prompt('⚠️ Are you sure you want to overwrite existing files? This cannot be undone! (yes/no): ');
119
- if (confirm !== 'yes') {
120
- console.log('Operation cancelled.');
121
- rl.close();
122
- process.exit(0);
849
+ } else {
850
+ // Show conflicts if not in force mode
851
+ if (dirConflicts.length > 0) {
852
+ console.log('\n⚠️ The following directories already exist:');
853
+ dirConflicts.forEach(dir => console.log(` - ${dir}/`));
854
+ }
855
+
856
+ if (fileConflicts.length > 0) {
857
+ console.log('\n⚠️ File conflicts detected. You will be asked how to handle each category.');
858
+
859
+ if (!flags.dryRun) {
860
+ // Ask for resolution strategy for each category with conflicts
861
+ const categories = [
862
+ { key: 'CLAUDE.md', name: 'CLAUDE.md', emoji: '📄' },
863
+ { key: 'agents', name: 'Agents', emoji: '🤖' },
864
+ { key: 'docs', name: 'Documentation', emoji: '📚' },
865
+ { key: 'plans', name: 'Plans', emoji: '📋' },
866
+ { key: 'tickets', name: 'Tickets', emoji: '🎫' }
867
+ ];
868
+
869
+ for (const category of categories) {
870
+ if (conflictsByCategory[category.key].length > 0) {
871
+ console.log(`\n${category.emoji} ${category.name} conflicts:`);
872
+ conflictsByCategory[category.key].forEach(file => console.log(` - ${file}`));
873
+
874
+ console.log('\nConflict resolution options:');
875
+ console.log(' 1) skip (s) - Keep your existing files');
876
+ console.log(' 2) rename (r) - Save template files with -ccsetup suffix');
877
+ console.log(' 3) overwrite (o) - Replace with template versions');
878
+
879
+ const userInput = await prompt(`Your choice for ${category.name} [s/r/o]: `);
880
+ const strategy = normalizeConflictStrategy(userInput);
881
+
882
+ if (!strategy) {
883
+ console.log(`\n❌ Invalid option: "${userInput}". Please use: skip/s/1, rename/r/2, or overwrite/o/3`);
884
+ if (rl) rl.close();
885
+ process.exit(1);
886
+ }
887
+
888
+ if (strategy === 'overwrite' && category.key === 'CLAUDE.md') {
889
+ const confirm = await prompt('⚠️ Are you sure you want to overwrite CLAUDE.md? This will lose your project instructions! (yes/no): ');
890
+ if (confirm !== 'yes') {
891
+ conflictStrategies[category.key] = 'skip';
892
+ console.log('Keeping existing CLAUDE.md');
893
+ continue;
894
+ }
895
+ }
896
+
897
+ conflictStrategies[category.key] = strategy;
898
+ }
899
+ }
123
900
  }
124
901
  }
125
902
  }
126
903
 
127
- const strategyDescriptions = {
128
- skip: 'Keeping all existing files, copying only new files',
129
- rename: 'Keeping existing files, saving templates with -ccsetup suffix',
130
- overwrite: 'Replacing existing files with template versions'
131
- };
132
-
133
- console.log(`\n✨ ${strategyDescriptions[conflictStrategy]}...`);
904
+ console.log(`\n✨ ${flags.dryRun ? 'Would apply' : 'Applying'} conflict resolution strategies...`);
134
905
 
135
906
  let skippedCount = 0;
136
907
  let copiedCount = 0;
@@ -138,71 +909,154 @@ async function main() {
138
909
  let overwrittenCount = 0;
139
910
 
140
911
  for (const item of allItems) {
141
- if (item.type === 'directory') {
142
- if (!item.exists && !fs.existsSync(item.dest)) {
143
- fs.mkdirSync(item.dest, { recursive: true });
144
- }
145
- } else {
146
- if (item.exists) {
147
- if (conflictStrategy === 'skip') {
148
- skippedCount++;
149
- continue;
150
- } else if (conflictStrategy === 'rename') {
151
- const ext = path.extname(item.dest);
152
- const baseName = path.basename(item.dest, ext);
153
- const dirName = path.dirname(item.dest);
154
- let newDest = path.join(dirName, `${baseName}-ccsetup${ext}`);
155
- let counter = 1;
156
- while (fs.existsSync(newDest)) {
157
- newDest = path.join(dirName, `${baseName}-ccsetup-${counter}${ext}`);
158
- counter++;
912
+ try {
913
+ if (item.type === 'directory') {
914
+ if (!item.exists && !fs.existsSync(item.dest)) {
915
+ if (!flags.dryRun) {
916
+ fs.mkdirSync(item.dest, { recursive: true });
917
+ } else {
918
+ console.log(` 📁 Would create directory: ${item.relativePath || '.'}/`);
159
919
  }
160
- fs.copyFileSync(item.src, newDest);
161
- renamedCount++;
162
- console.log(` 📄 Created: ${path.relative(targetDir, newDest)}`);
163
- } else if (conflictStrategy === 'overwrite') {
164
- fs.copyFileSync(item.src, item.dest);
165
- overwrittenCount++;
166
- console.log(` ♻️ Replaced: ${item.relativePath}`);
167
920
  }
168
921
  } else {
169
- fs.copyFileSync(item.src, item.dest);
170
- copiedCount++;
922
+ if (item.exists) {
923
+ // Determine which category this file belongs to
924
+ let strategy = 'skip'; // default
925
+ if (item.relativePath === 'CLAUDE.md') {
926
+ strategy = conflictStrategies['CLAUDE.md'];
927
+ } else if (item.relativePath.startsWith('agents/')) {
928
+ strategy = conflictStrategies['agents'];
929
+ } else if (item.relativePath.startsWith('docs/')) {
930
+ strategy = conflictStrategies['docs'];
931
+ } else if (item.relativePath.startsWith('plans/')) {
932
+ strategy = conflictStrategies['plans'];
933
+ } else if (item.relativePath.startsWith('tickets/')) {
934
+ strategy = conflictStrategies['tickets'];
935
+ }
936
+
937
+ if (strategy === 'skip') {
938
+ skippedCount++;
939
+ if (flags.dryRun) {
940
+ console.log(` ⏭️ Would skip: ${item.relativePath}`);
941
+ }
942
+ continue;
943
+ } else if (strategy === 'rename') {
944
+ const ext = path.extname(item.dest);
945
+ const baseName = path.basename(item.dest, ext);
946
+ const dirName = path.dirname(item.dest);
947
+ let newDest = path.join(dirName, `${baseName}-ccsetup${ext}`);
948
+ let counter = 1;
949
+ while (fs.existsSync(newDest)) {
950
+ newDest = path.join(dirName, `${baseName}-ccsetup-${counter}${ext}`);
951
+ counter++;
952
+ }
953
+ if (!flags.dryRun) {
954
+ fs.copyFileSync(item.src, newDest);
955
+ }
956
+ renamedCount++;
957
+ console.log(` 📄 ${flags.dryRun ? 'Would create' : 'Created'}: ${path.relative(targetDir, newDest)}`);
958
+ } else if (strategy === 'overwrite') {
959
+ if (!flags.dryRun) {
960
+ fs.copyFileSync(item.src, item.dest);
961
+ }
962
+ overwrittenCount++;
963
+ console.log(` ♻️ ${flags.dryRun ? 'Would replace' : 'Replaced'}: ${item.relativePath}`);
964
+ }
965
+ } else {
966
+ if (!flags.dryRun) {
967
+ // Ensure directory exists before copying file
968
+ const destDir = path.dirname(item.dest);
969
+ if (!fs.existsSync(destDir)) {
970
+ fs.mkdirSync(destDir, { recursive: true });
971
+ }
972
+ fs.copyFileSync(item.src, item.dest);
973
+ } else {
974
+ console.log(` ✨ Would copy: ${item.relativePath}`);
975
+ }
976
+ copiedCount++;
977
+ }
978
+ }
979
+ } catch (error) {
980
+ if (error.code === 'EACCES') {
981
+ throw new Error(`Permission denied: ${item.relativePath}`);
982
+ } else if (error.code === 'ENOSPC') {
983
+ throw new Error('No space left on device');
984
+ } else if (error.code === 'EISDIR') {
985
+ throw new Error(`Cannot overwrite directory with file: ${item.relativePath}`);
171
986
  }
987
+ throw new Error(`Failed to process ${item.relativePath}: ${error.message}`);
172
988
  }
173
989
  }
174
990
 
175
- console.log('\n✅ Claude Code project created successfully!');
991
+ // Initialize .claude directory and copy agents
992
+ let claudeInitResult = null;
993
+ // Always initialize .claude directory structure (it will handle existing directories)
994
+ console.log(`\n🔧 ${claudeStatus.hasClaudeDir ? 'Updating' : 'Initializing'} .claude directory structure...`);
995
+ claudeInitResult = await initializeClaudeDirectory(selectedAgentFiles, conflictStrategies['agents'], flags.dryRun);
996
+
997
+ if (claudeInitResult.createdItems.length > 0) {
998
+ console.log(` ✅ Created ${claudeInitResult.createdItems.length} items in .claude directory`);
999
+ }
1000
+ if (claudeInitResult.copiedAgents > 0) {
1001
+ console.log(` 🤖 Copied ${claudeInitResult.copiedAgents} agents to .claude/agents`);
1002
+ }
1003
+ if (claudeInitResult.skippedAgents > 0) {
1004
+ console.log(` ⏭️ Skipped ${claudeInitResult.skippedAgents} existing agents in .claude/agents`);
1005
+ }
1006
+
1007
+ console.log(`\n✅ Claude Code project ${flags.dryRun ? 'would be' : ''} created successfully!`);
176
1008
 
177
1009
  // Show summary of what happened
178
- if (fileConflicts.length > 0 || copiedCount > 0) {
1010
+ if (fileConflicts.length > 0 || copiedCount > 0 || selectedAgentFiles.length > 0 || claudeInitResult) {
179
1011
  console.log('\n📊 Summary:');
180
- if (copiedCount > 0) console.log(` ✨ ${copiedCount} new files copied`);
181
- if (skippedCount > 0) console.log(` ⏭️ ${skippedCount} existing files kept unchanged`);
182
- if (renamedCount > 0) console.log(` 📄 ${renamedCount} template files saved with -ccsetup suffix`);
183
- if (overwrittenCount > 0) console.log(` ♻️ ${overwrittenCount} files replaced with template versions`);
1012
+ if (copiedCount > 0) console.log(` ✨ ${copiedCount} new files ${flags.dryRun ? 'would be' : ''} copied`);
1013
+ if (skippedCount > 0) console.log(` ⏭️ ${skippedCount} existing files ${flags.dryRun ? 'would be' : ''} kept unchanged`);
1014
+ if (renamedCount > 0) console.log(` 📄 ${renamedCount} template files ${flags.dryRun ? 'would be' : ''} saved with -ccsetup suffix`);
1015
+ if (overwrittenCount > 0) console.log(` ♻️ ${overwrittenCount} files ${flags.dryRun ? 'would be' : ''} replaced with template versions`);
1016
+ if (!flags.noAgents && !flags.dryRun) {
1017
+ console.log(` 🤖 ${selectedAgentFiles.length} agent${selectedAgentFiles.length === 1 ? '' : 's'} ${flags.dryRun ? 'would be' : ''} included in /agents`);
1018
+ }
1019
+ if (claudeInitResult && claudeInitResult.createdItems.length > 0) {
1020
+ console.log(` 📁 ${claudeInitResult.createdItems.length} items created in .claude directory`);
1021
+ }
184
1022
  }
185
1023
 
186
- console.log('\nNext steps:');
187
- if (projectName !== '.') {
188
- console.log(` cd ${projectName}`);
1024
+ if (!flags.dryRun) {
1025
+ console.log('\nNext steps:');
1026
+ if (projectName !== '.') {
1027
+ // Escape project name to prevent command injection
1028
+ const escapedProjectName = JSON.stringify(projectName);
1029
+ console.log(` cd ${escapedProjectName}`);
1030
+ const finalClaudeStatus = checkClaudeCode();
1031
+ if (!finalClaudeStatus.isInstalled) {
1032
+ console.log(' claude init # Initialize Claude Code in the project');
1033
+ }
1034
+ }
1035
+ console.log(' 1. Edit CLAUDE.md to add your project-specific instructions');
1036
+ console.log(' 2. Update docs/ROADMAP.md with your project goals');
1037
+ console.log(' 3. Start creating tickets in the tickets/ directory');
1038
+
1039
+ if (renamedCount > 0) {
1040
+ console.log('\n💡 Tip: Review the -ccsetup files to see template examples');
1041
+ console.log(' You can compare them with your existing files or copy sections you need');
1042
+ }
1043
+
1044
+ console.log('\nHappy coding with Claude! 🎉');
189
1045
  }
190
- console.log(' 1. Edit CLAUDE.md to add your project-specific instructions');
191
- console.log(' 2. Update docs/ROADMAP.md with your project goals');
192
- console.log(' 3. Start creating tickets in the tickets/ directory');
193
1046
 
194
- if (fileConflicts.length > 0 && conflictStrategy === 'rename') {
195
- console.log('\n💡 Tip: Review the -ccsetup files to see template examples');
196
- console.log(' You can compare them with your existing files or copy sections you need');
1047
+ } catch (error) {
1048
+ throw error;
1049
+ } finally {
1050
+ if (rl) {
1051
+ rl.close();
1052
+ }
197
1053
  }
198
-
199
- console.log('\nHappy coding with Claude! 🎉');
200
-
201
- rl.close();
202
1054
  }
203
1055
 
204
1056
  main().catch(err => {
205
- console.error('Error:', err);
206
- rl.close();
1057
+ console.error('Error:', err.message || err);
1058
+ if (rl) {
1059
+ rl.close();
1060
+ }
207
1061
  process.exit(1);
208
1062
  });