ccstart 2.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.
@@ -0,0 +1,1062 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+
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
+
96
+ const targetDir = path.resolve(process.cwd(), projectName);
97
+ const templateDir = path.join(__dirname, '..', 'template');
98
+
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
+ }
107
+
108
+ function prompt(question) {
109
+ if (!rl) {
110
+ throw new Error('Readline interface not initialized');
111
+ }
112
+ return new Promise((resolve) => {
113
+ rl.question(question, (answer) => {
114
+ resolve(answer.toLowerCase().trim());
115
+ });
116
+ });
117
+ }
118
+
119
+ function normalizeConflictStrategy(input) {
120
+ const normalized = input.toLowerCase().trim();
121
+
122
+ // Accept abbreviations and variations
123
+ if (normalized === 's' || normalized === 'skip' || normalized === '1') return 'skip';
124
+ if (normalized === 'r' || normalized === 'rename' || normalized === '2') return 'rename';
125
+ if (normalized === 'o' || normalized === 'overwrite' || normalized === '3') return 'overwrite';
126
+
127
+ return null;
128
+ }
129
+
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
+ }
161
+
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/');
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}`);
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
+ }
629
+
630
+ const fileConflicts = [];
631
+ const dirConflicts = [];
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
+ };
651
+
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
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);
693
+
694
+ if (!normalizedSrc.startsWith(path.normalize(templateDir))) {
695
+ throw new Error('Source path escapes template directory');
696
+ }
697
+
698
+ if (!normalizedDest.startsWith(path.normalize(targetDir))) {
699
+ throw new Error('Destination path escapes target directory');
700
+ }
701
+
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
+ }
832
+ }
833
+ } catch (error) {
834
+ throw new Error(`Error scanning template: ${error.message}`);
835
+ }
836
+ }
837
+
838
+ scanTemplate(templateDir, targetDir, '', true);
839
+
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');
848
+ }
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
+ }
900
+ }
901
+ }
902
+ }
903
+
904
+ console.log(`\n✨ ${flags.dryRun ? 'Would apply' : 'Applying'} conflict resolution strategies...`);
905
+
906
+ let skippedCount = 0;
907
+ let copiedCount = 0;
908
+ let renamedCount = 0;
909
+ let overwrittenCount = 0;
910
+
911
+ for (const item of allItems) {
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 || '.'}/`);
919
+ }
920
+ }
921
+ } else {
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}`);
986
+ }
987
+ throw new Error(`Failed to process ${item.relativePath}: ${error.message}`);
988
+ }
989
+ }
990
+
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!`);
1008
+
1009
+ // Show summary of what happened
1010
+ if (fileConflicts.length > 0 || copiedCount > 0 || selectedAgentFiles.length > 0 || claudeInitResult) {
1011
+ console.log('\nšŸ“Š Summary:');
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
+ }
1022
+ }
1023
+
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! šŸŽ‰');
1045
+ }
1046
+
1047
+ } catch (error) {
1048
+ throw error;
1049
+ } finally {
1050
+ if (rl) {
1051
+ rl.close();
1052
+ }
1053
+ }
1054
+ }
1055
+
1056
+ main().catch(err => {
1057
+ console.error('Error:', err.message || err);
1058
+ if (rl) {
1059
+ rl.close();
1060
+ }
1061
+ process.exit(1);
1062
+ });