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,1107 @@
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: ccstart [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
+ ccstart # Create in current directory
55
+ ccstart my-project # Create in new directory
56
+ ccstart . --force # Overwrite files in current directory
57
+ ccstart my-app --dry-run # Preview changes without creating files
58
+ ccstart --agents # Interactive agent selection only
59
+ ccstart 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}-ccstart${ext}`);
223
+ let counter = 1;
224
+ while (fs.existsSync(newDest)) {
225
+ newDest = path.join(dirName, `${baseName}-ccstart-${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}-ccstart${ext}`);
272
+ let counter = 1;
273
+ while (fs.existsSync(newDest)) {
274
+ newDest = path.join(dirName, `${baseName}-ccstart-${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, 'claude', '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}-ccstart${ext}`);
345
+ let counter = 1;
346
+ while (fs.existsSync(newDest)) {
347
+ newDest = path.join(dirName, `${baseName}-ccstart-${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, 'claude', '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 ccstart${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('ccstart 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 ccstart\' 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
+ 'claude': [],
638
+ 'agents': [],
639
+ 'docs': [],
640
+ 'plans': [],
641
+ 'tickets': []
642
+ };
643
+
644
+ // Store conflict strategies per category
645
+ const conflictStrategies = {
646
+ 'CLAUDE.md': 'skip',
647
+ 'claude': 'skip',
648
+ 'agents': 'skip',
649
+ 'docs': 'skip',
650
+ 'plans': 'skip',
651
+ 'tickets': 'skip'
652
+ };
653
+
654
+ // Get available agents for selection
655
+ const availableAgents = getAvailableAgents();
656
+ let selectedAgentFiles = [];
657
+
658
+
659
+ // Determine which agents to include
660
+ if (flags.noAgents) {
661
+ selectedAgentFiles = [];
662
+ console.log('\nā­ļø Skipping agent selection (--no-agents flag)');
663
+ } else if (flags.allAgents) {
664
+ selectedAgentFiles = availableAgents.map(a => a.file).filter(validateAgentFile);
665
+ console.log(`\nāœ… Including all ${selectedAgentFiles.length} agents (--all-agents flag)`);
666
+ } else if (!flags.dryRun) {
667
+ // Close readline interface before using inquirer
668
+ if (rl) {
669
+ rl.close();
670
+ rl = null;
671
+ }
672
+
673
+ // Interactive selection
674
+ selectedAgentFiles = await selectAgents(availableAgents);
675
+ console.log(`\nāœ… Selected ${selectedAgentFiles.length} agent${selectedAgentFiles.length === 1 ? '' : 's'}`);
676
+
677
+ // Recreate readline interface after agent selection
678
+ if (!flags.force && !flags.dryRun) {
679
+ rl = readline.createInterface({
680
+ input: process.stdin,
681
+ output: process.stdout
682
+ });
683
+ }
684
+ } else {
685
+ // In dry-run mode, show what would happen
686
+ console.log(`\nWould prompt for agent selection from ${availableAgents.length} available agents`);
687
+ selectedAgentFiles = availableAgents.map(a => a.file).filter(validateAgentFile); // Include all for scanning purposes
688
+ }
689
+
690
+ function scanTemplate(src, dest, relativePath = '') {
691
+ try {
692
+ // Validate paths to prevent traversal
693
+ const normalizedSrc = path.normalize(src);
694
+ const normalizedDest = path.normalize(dest);
695
+
696
+ if (!normalizedSrc.startsWith(path.normalize(templateDir))) {
697
+ throw new Error('Source path escapes template directory');
698
+ }
699
+
700
+ if (!normalizedDest.startsWith(path.normalize(targetDir))) {
701
+ throw new Error('Destination path escapes target directory');
702
+ }
703
+
704
+ const exists = fs.existsSync(src);
705
+ const stats = exists && fs.statSync(src);
706
+ const isDirectory = exists && stats.isDirectory();
707
+
708
+ if (isDirectory) {
709
+ // Skip .claude directory as it will be handled separately
710
+ if (path.basename(src) === '.claude') {
711
+ return; // Don't process .claude directory in regular template scan
712
+ }
713
+
714
+ // Handle claude directory specially
715
+ if (path.basename(src) === 'claude') {
716
+ // Add the claude directory
717
+ allItems.push({
718
+ src,
719
+ dest,
720
+ type: 'directory',
721
+ relativePath,
722
+ exists: fs.existsSync(dest)
723
+ });
724
+
725
+ if (fs.existsSync(dest)) {
726
+ dirConflicts.push(relativePath || '.');
727
+ conflictsByCategory['claude'].push(relativePath);
728
+ }
729
+
730
+ // Process claude subdirectories (except agents which we handle specially)
731
+ const claudeSubdirs = ['docs', 'plans', 'tickets'];
732
+ claudeSubdirs.forEach(subdir => {
733
+ const subdirSrc = path.join(src, subdir);
734
+ const subdirDest = path.join(dest, subdir);
735
+ const subdirRelPath = path.join(relativePath, subdir);
736
+
737
+ if (fs.existsSync(subdirSrc)) {
738
+ scanTemplate(subdirSrc, subdirDest, subdirRelPath);
739
+ }
740
+ });
741
+
742
+ // Handle agents directory specially with selected agents
743
+ const agentsSrc = path.join(src, 'agents');
744
+ const agentsDest = path.join(dest, 'agents');
745
+ const agentsRelPath = path.join(relativePath, 'agents');
746
+
747
+ allItems.push({
748
+ src: agentsSrc,
749
+ dest: agentsDest,
750
+ type: 'directory',
751
+ relativePath: agentsRelPath,
752
+ exists: fs.existsSync(agentsDest)
753
+ });
754
+
755
+ if (fs.existsSync(agentsDest)) {
756
+ dirConflicts.push(agentsRelPath);
757
+ }
758
+
759
+ // Add selected agent files
760
+ for (const agentFile of selectedAgentFiles) {
761
+ if (!validateAgentFile(agentFile)) {
762
+ console.warn(`āš ļø Skipping invalid agent file: ${agentFile}`);
763
+ continue;
764
+ }
765
+
766
+ const safeAgentFile = path.basename(agentFile);
767
+ const agentSrc = path.join(agentsSrc, safeAgentFile);
768
+ const agentDest = path.join(agentsDest, safeAgentFile);
769
+ const agentRelPath = path.join(agentsRelPath, safeAgentFile);
770
+
771
+ if (fs.existsSync(agentSrc)) {
772
+ allItems.push({
773
+ src: agentSrc,
774
+ dest: agentDest,
775
+ type: 'file',
776
+ relativePath: agentRelPath,
777
+ exists: fs.existsSync(agentDest)
778
+ });
779
+
780
+ if (fs.existsSync(agentDest)) {
781
+ fileConflicts.push(agentRelPath);
782
+ conflictsByCategory['agents'].push(agentRelPath);
783
+ }
784
+ }
785
+ }
786
+
787
+ // Always include agents README.md
788
+ const agentsReadmeSrc = path.join(agentsSrc, 'README.md');
789
+ const agentsReadmeDest = path.join(agentsDest, 'README.md');
790
+ if (fs.existsSync(agentsReadmeSrc)) {
791
+ allItems.push({
792
+ src: agentsReadmeSrc,
793
+ dest: agentsReadmeDest,
794
+ type: 'file',
795
+ relativePath: path.join(agentsRelPath, 'README.md'),
796
+ exists: fs.existsSync(agentsReadmeDest)
797
+ });
798
+
799
+ if (fs.existsSync(agentsReadmeDest)) {
800
+ const readmePath = path.join(agentsRelPath, 'README.md');
801
+ fileConflicts.push(readmePath);
802
+ conflictsByCategory['agents'].push(readmePath);
803
+ }
804
+ }
805
+
806
+ // Handle CLAUDE.md from claude directory - it goes to root
807
+ const claudeMdSrc = path.join(src, 'CLAUDE.md');
808
+ const claudeMdDest = path.join(targetDir, 'CLAUDE.md'); // Note: goes to root
809
+ if (fs.existsSync(claudeMdSrc)) {
810
+ allItems.push({
811
+ src: claudeMdSrc,
812
+ dest: claudeMdDest,
813
+ type: 'file',
814
+ relativePath: 'CLAUDE.md',
815
+ exists: fs.existsSync(claudeMdDest),
816
+ isClaudeMdReplacement: true
817
+ });
818
+
819
+ if (fs.existsSync(claudeMdDest)) {
820
+ fileConflicts.push('CLAUDE.md');
821
+ conflictsByCategory['CLAUDE.md'].push('CLAUDE.md');
822
+ }
823
+ }
824
+
825
+ return; // Don't recurse into claude directory subdirs as we handled them above
826
+ }
827
+
828
+ allItems.push({
829
+ src,
830
+ dest,
831
+ type: 'directory',
832
+ relativePath,
833
+ exists: fs.existsSync(dest)
834
+ });
835
+
836
+ if (fs.existsSync(dest)) {
837
+ dirConflicts.push(relativePath || '.');
838
+ }
839
+
840
+ fs.readdirSync(src).forEach(childItem => {
841
+ scanTemplate(
842
+ path.join(src, childItem),
843
+ path.join(dest, childItem),
844
+ path.join(relativePath, childItem)
845
+ );
846
+ });
847
+ } else {
848
+ allItems.push({
849
+ src,
850
+ dest,
851
+ type: 'file',
852
+ relativePath,
853
+ exists: fs.existsSync(dest)
854
+ });
855
+
856
+ if (fs.existsSync(dest)) {
857
+ fileConflicts.push(relativePath);
858
+
859
+ // Categorize the conflict
860
+ if (relativePath === 'CLAUDE.md') {
861
+ conflictsByCategory['CLAUDE.md'].push(relativePath);
862
+ } else if (relativePath.startsWith('claude/agents/')) {
863
+ conflictsByCategory['agents'].push(relativePath);
864
+ } else if (relativePath.startsWith('claude/docs/')) {
865
+ conflictsByCategory['docs'].push(relativePath);
866
+ } else if (relativePath.startsWith('claude/plans/')) {
867
+ conflictsByCategory['plans'].push(relativePath);
868
+ } else if (relativePath.startsWith('claude/tickets/')) {
869
+ conflictsByCategory['tickets'].push(relativePath);
870
+ }
871
+ }
872
+ }
873
+ } catch (error) {
874
+ throw new Error(`Error scanning template: ${error.message}`);
875
+ }
876
+ }
877
+
878
+ scanTemplate(templateDir, targetDir, '');
879
+
880
+ // Handle force flag
881
+ if (flags.force) {
882
+ // Set all strategies to overwrite
883
+ Object.keys(conflictStrategies).forEach(key => {
884
+ conflictStrategies[key] = 'overwrite';
885
+ });
886
+ if (fileConflicts.length > 0 || dirConflicts.length > 0) {
887
+ console.log('\nāš ļø Force mode enabled - existing files will be overwritten');
888
+ }
889
+ } else {
890
+ // Show conflicts if not in force mode
891
+ if (dirConflicts.length > 0) {
892
+ console.log('\nāš ļø The following directories already exist:');
893
+ dirConflicts.forEach(dir => console.log(` - ${dir}/`));
894
+ }
895
+
896
+ if (fileConflicts.length > 0) {
897
+ console.log('\nāš ļø File conflicts detected. You will be asked how to handle each category.');
898
+
899
+ if (!flags.dryRun) {
900
+ // Ask for resolution strategy for each category with conflicts
901
+ const categories = [
902
+ { key: 'CLAUDE.md', name: 'CLAUDE.md', emoji: 'šŸ“„' },
903
+ { key: 'claude', name: 'Claude Directory', emoji: 'šŸ“' },
904
+ { key: 'agents', name: 'Agents', emoji: 'šŸ¤–' },
905
+ { key: 'docs', name: 'Documentation', emoji: 'šŸ“š' },
906
+ { key: 'plans', name: 'Plans', emoji: 'šŸ“‹' },
907
+ { key: 'tickets', name: 'Tickets', emoji: 'šŸŽ«' }
908
+ ];
909
+
910
+ for (const category of categories) {
911
+ if (conflictsByCategory[category.key].length > 0) {
912
+ console.log(`\n${category.emoji} ${category.name} conflicts:`);
913
+ conflictsByCategory[category.key].forEach(file => console.log(` - ${file}`));
914
+
915
+ console.log('\nConflict resolution options:');
916
+ console.log(' 1) skip (s) - Keep your existing files');
917
+ console.log(' 2) rename (r) - Save template files with -ccstart suffix');
918
+ console.log(' 3) overwrite (o) - Replace with template versions');
919
+
920
+ const userInput = await prompt(`Your choice for ${category.name} [s/r/o]: `);
921
+ const strategy = normalizeConflictStrategy(userInput);
922
+
923
+ if (!strategy) {
924
+ console.log(`\nāŒ Invalid option: "${userInput}". Please use: skip/s/1, rename/r/2, or overwrite/o/3`);
925
+ if (rl) rl.close();
926
+ process.exit(1);
927
+ }
928
+
929
+ if (strategy === 'overwrite' && category.key === 'CLAUDE.md') {
930
+ const confirm = await prompt('āš ļø Are you sure you want to overwrite CLAUDE.md? This will replace your existing project instructions with the ccstart template! (yes/no): ');
931
+ if (confirm !== 'yes') {
932
+ conflictStrategies[category.key] = 'skip';
933
+ console.log('Keeping existing CLAUDE.md');
934
+ continue;
935
+ }
936
+ }
937
+
938
+ conflictStrategies[category.key] = strategy;
939
+ }
940
+ }
941
+ }
942
+ }
943
+ }
944
+
945
+ console.log(`\n✨ ${flags.dryRun ? 'Would apply' : 'Applying'} conflict resolution strategies...`);
946
+
947
+ let skippedCount = 0;
948
+ let copiedCount = 0;
949
+ let renamedCount = 0;
950
+ let overwrittenCount = 0;
951
+
952
+ for (const item of allItems) {
953
+ try {
954
+ if (item.type === 'directory') {
955
+ if (!item.exists && !fs.existsSync(item.dest)) {
956
+ if (!flags.dryRun) {
957
+ fs.mkdirSync(item.dest, { recursive: true });
958
+ } else {
959
+ console.log(` šŸ“ Would create directory: ${item.relativePath || '.'}/`);
960
+ }
961
+ }
962
+ } else {
963
+ if (item.exists) {
964
+ // Determine which category this file belongs to
965
+ let strategy = 'skip'; // default
966
+ if (item.relativePath === 'CLAUDE.md' || item.isClaudeMdReplacement) {
967
+ strategy = conflictStrategies['CLAUDE.md'];
968
+ } else if (item.relativePath.startsWith('claude/')) {
969
+ if (item.relativePath.startsWith('claude/agents/')) {
970
+ strategy = conflictStrategies['agents'];
971
+ } else if (item.relativePath.startsWith('claude/docs/')) {
972
+ strategy = conflictStrategies['docs'];
973
+ } else if (item.relativePath.startsWith('claude/plans/')) {
974
+ strategy = conflictStrategies['plans'];
975
+ } else if (item.relativePath.startsWith('claude/tickets/')) {
976
+ strategy = conflictStrategies['tickets'];
977
+ } else {
978
+ strategy = conflictStrategies['claude'];
979
+ }
980
+ }
981
+
982
+ if (strategy === 'skip') {
983
+ skippedCount++;
984
+ if (flags.dryRun) {
985
+ console.log(` ā­ļø Would skip: ${item.relativePath}`);
986
+ }
987
+ continue;
988
+ } else if (strategy === 'rename') {
989
+ const ext = path.extname(item.dest);
990
+ const baseName = path.basename(item.dest, ext);
991
+ const dirName = path.dirname(item.dest);
992
+ let newDest = path.join(dirName, `${baseName}-ccstart${ext}`);
993
+ let counter = 1;
994
+ while (fs.existsSync(newDest)) {
995
+ newDest = path.join(dirName, `${baseName}-ccstart-${counter}${ext}`);
996
+ counter++;
997
+ }
998
+ if (!flags.dryRun) {
999
+ fs.copyFileSync(item.src, newDest);
1000
+ }
1001
+ renamedCount++;
1002
+ console.log(` šŸ“„ ${flags.dryRun ? 'Would create' : 'Created'}: ${path.relative(targetDir, newDest)}`);
1003
+ } else if (strategy === 'overwrite') {
1004
+ if (!flags.dryRun) {
1005
+ fs.copyFileSync(item.src, item.dest);
1006
+ }
1007
+ overwrittenCount++;
1008
+ console.log(` ā™»ļø ${flags.dryRun ? 'Would replace' : 'Replaced'}: ${item.relativePath}`);
1009
+ }
1010
+ } else {
1011
+ if (!flags.dryRun) {
1012
+ // Ensure directory exists before copying file
1013
+ const destDir = path.dirname(item.dest);
1014
+ if (!fs.existsSync(destDir)) {
1015
+ fs.mkdirSync(destDir, { recursive: true });
1016
+ }
1017
+ fs.copyFileSync(item.src, item.dest);
1018
+ } else {
1019
+ console.log(` ✨ Would copy: ${item.relativePath}`);
1020
+ }
1021
+ copiedCount++;
1022
+ }
1023
+ }
1024
+ } catch (error) {
1025
+ if (error.code === 'EACCES') {
1026
+ throw new Error(`Permission denied: ${item.relativePath}`);
1027
+ } else if (error.code === 'ENOSPC') {
1028
+ throw new Error('No space left on device');
1029
+ } else if (error.code === 'EISDIR') {
1030
+ throw new Error(`Cannot overwrite directory with file: ${item.relativePath}`);
1031
+ }
1032
+ throw new Error(`Failed to process ${item.relativePath}: ${error.message}`);
1033
+ }
1034
+ }
1035
+
1036
+ // Initialize .claude directory and copy agents
1037
+ let claudeInitResult = null;
1038
+ // Always initialize .claude directory structure (it will handle existing directories)
1039
+ console.log(`\nšŸ”§ ${claudeStatus.hasClaudeDir ? 'Updating' : 'Initializing'} .claude directory structure...`);
1040
+ claudeInitResult = await initializeClaudeDirectory(selectedAgentFiles, conflictStrategies['agents'], flags.dryRun);
1041
+
1042
+ if (claudeInitResult.createdItems.length > 0) {
1043
+ console.log(` āœ… Created ${claudeInitResult.createdItems.length} items in .claude directory`);
1044
+ }
1045
+ if (claudeInitResult.copiedAgents > 0) {
1046
+ console.log(` šŸ¤– Copied ${claudeInitResult.copiedAgents} agents to .claude/agents`);
1047
+ }
1048
+ if (claudeInitResult.skippedAgents > 0) {
1049
+ console.log(` ā­ļø Skipped ${claudeInitResult.skippedAgents} existing agents in .claude/agents`);
1050
+ }
1051
+
1052
+ console.log(`\nāœ… Claude Code project ${flags.dryRun ? 'would be' : ''} created successfully!`);
1053
+
1054
+ // Show summary of what happened
1055
+ if (fileConflicts.length > 0 || copiedCount > 0 || selectedAgentFiles.length > 0 || claudeInitResult) {
1056
+ console.log('\nšŸ“Š Summary:');
1057
+ if (copiedCount > 0) console.log(` ✨ ${copiedCount} new files ${flags.dryRun ? 'would be' : ''} copied`);
1058
+ if (skippedCount > 0) console.log(` ā­ļø ${skippedCount} existing files ${flags.dryRun ? 'would be' : ''} kept unchanged`);
1059
+ if (renamedCount > 0) console.log(` šŸ“„ ${renamedCount} template files ${flags.dryRun ? 'would be' : ''} saved with -ccstart suffix`);
1060
+ if (overwrittenCount > 0) console.log(` ā™»ļø ${overwrittenCount} files ${flags.dryRun ? 'would be' : ''} replaced with template versions`);
1061
+ if (!flags.noAgents && !flags.dryRun) {
1062
+ console.log(` šŸ¤– ${selectedAgentFiles.length} agent${selectedAgentFiles.length === 1 ? '' : 's'} ${flags.dryRun ? 'would be' : ''} included in claude/agents`);
1063
+ }
1064
+ if (claudeInitResult && claudeInitResult.createdItems.length > 0) {
1065
+ console.log(` šŸ“ ${claudeInitResult.createdItems.length} items created in .claude directory`);
1066
+ }
1067
+ }
1068
+
1069
+ if (!flags.dryRun) {
1070
+ console.log('\nNext steps:');
1071
+ if (projectName !== '.') {
1072
+ // Escape project name to prevent command injection
1073
+ const escapedProjectName = JSON.stringify(projectName);
1074
+ console.log(` cd ${escapedProjectName}`);
1075
+ const finalClaudeStatus = checkClaudeCode();
1076
+ if (!finalClaudeStatus.isInstalled) {
1077
+ console.log(' claude init # Initialize Claude Code in the project');
1078
+ }
1079
+ }
1080
+ console.log(' 1. Edit CLAUDE.md to add your project-specific instructions');
1081
+ console.log(' 2. Update claude/docs/ROADMAP.md with your project goals');
1082
+ console.log(' 3. Start creating tickets in the claude/tickets/ directory');
1083
+
1084
+ if (renamedCount > 0) {
1085
+ console.log('\nšŸ’” Tip: Review the -ccstart files to see template examples');
1086
+ console.log(' You can compare them with your existing files or copy sections you need');
1087
+ }
1088
+
1089
+ console.log('\nHappy coding with Claude! šŸŽ‰');
1090
+ }
1091
+
1092
+ } catch (error) {
1093
+ throw error;
1094
+ } finally {
1095
+ if (rl) {
1096
+ rl.close();
1097
+ }
1098
+ }
1099
+ }
1100
+
1101
+ main().catch(err => {
1102
+ console.error('Error:', err.message || err);
1103
+ if (rl) {
1104
+ rl.close();
1105
+ }
1106
+ process.exit(1);
1107
+ });