ccstart 2.2.0 → 3.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.
@@ -12,7 +12,11 @@ const flags = {
12
12
  help: false,
13
13
  allAgents: false,
14
14
  noAgents: false,
15
- agents: false
15
+ agents: false,
16
+ allSkills: false,
17
+ noSkills: false,
18
+ allHooks: false,
19
+ noHooks: false
16
20
  };
17
21
 
18
22
  let projectName = '.';
@@ -32,6 +36,14 @@ for (let i = 0; i < args.length; i++) {
32
36
  flags.noAgents = true;
33
37
  } else if (arg === '--agents') {
34
38
  flags.agents = true;
39
+ } else if (arg === '--all-skills') {
40
+ flags.allSkills = true;
41
+ } else if (arg === '--no-skills') {
42
+ flags.noSkills = true;
43
+ } else if (arg === '--all-hooks') {
44
+ flags.allHooks = true;
45
+ } else if (arg === '--no-hooks') {
46
+ flags.noHooks = true;
35
47
  } else if (!arg.startsWith('-')) {
36
48
  projectName = arg;
37
49
  }
@@ -48,6 +60,10 @@ Options:
48
60
  --agents Interactive agent selection mode
49
61
  --all-agents Include all agents without prompting
50
62
  --no-agents Skip agent selection entirely
63
+ --all-skills Include all skills without prompting
64
+ --no-skills Skip skill selection entirely
65
+ --all-hooks Include all hooks without prompting
66
+ --no-hooks Skip hook selection entirely
51
67
  --help, -h Show this help message
52
68
 
53
69
  Examples:
@@ -57,6 +73,7 @@ Examples:
57
73
  ccstart my-app --dry-run # Preview changes without creating files
58
74
  ccstart --agents # Interactive agent selection only
59
75
  ccstart my-app --all-agents # Include all agents automatically
76
+ ccstart my-app --all-skills # Include all skills automatically
60
77
  `);
61
78
  process.exit(0);
62
79
  }
@@ -83,6 +100,16 @@ if (flags.allAgents && flags.noAgents) {
83
100
  process.exit(1);
84
101
  }
85
102
 
103
+ if (flags.allSkills && flags.noSkills) {
104
+ console.error('Error: Cannot use --all-skills and --no-skills together');
105
+ process.exit(1);
106
+ }
107
+
108
+ if (flags.allHooks && flags.noHooks) {
109
+ console.error('Error: Cannot use --all-hooks and --no-hooks together');
110
+ process.exit(1);
111
+ }
112
+
86
113
  // Validate the project name
87
114
  if (projectName !== '.') {
88
115
  try {
@@ -160,9 +187,10 @@ function checkClaudeCode() {
160
187
  }
161
188
 
162
189
  // Function to initialize .claude directory structure
163
- async function initializeClaudeDirectory(selectedAgentFiles, conflictStrategy, dryRun) {
190
+ async function initializeClaudeDirectory(selectedAgentFiles, selectedSkillDirs, selectedHookFiles, conflictStrategy, dryRun) {
164
191
  const claudeDir = path.join(targetDir, '.claude');
165
192
  const claudeAgentsDir = path.join(claudeDir, 'agents');
193
+ const claudeSkillsDir = path.join(claudeDir, 'skills');
166
194
  const templateClaudeDir = path.join(templateDir, '.claude');
167
195
 
168
196
  const createdItems = [];
@@ -368,7 +396,106 @@ async function initializeClaudeDirectory(selectedAgentFiles, conflictStrategy, d
368
396
  }
369
397
  }
370
398
  }
371
-
399
+
400
+ // Create .claude/skills directory
401
+ if (!fs.existsSync(claudeSkillsDir)) {
402
+ if (!dryRun) {
403
+ fs.mkdirSync(claudeSkillsDir, { recursive: true });
404
+ }
405
+ createdItems.push('.claude/skills/');
406
+ if (dryRun) {
407
+ console.log(' šŸ“ Would create directory: .claude/skills/');
408
+ }
409
+ } else {
410
+ skippedItems.push('.claude/skills/');
411
+ }
412
+
413
+ // Copy selected skills to .claude/skills (copy entire directories)
414
+ const templateSkillsDir = path.join(templateDir, 'claude', 'skills');
415
+ let copiedSkills = 0;
416
+ let skippedSkillsCount = 0;
417
+
418
+ function copyDirRecursive(src, dest) {
419
+ if (!fs.existsSync(dest)) {
420
+ fs.mkdirSync(dest, { recursive: true });
421
+ }
422
+ const entries = fs.readdirSync(src, { withFileTypes: true });
423
+ for (const entry of entries) {
424
+ const srcPath = path.join(src, entry.name);
425
+ const destPath = path.join(dest, entry.name);
426
+ if (entry.isDirectory()) {
427
+ copyDirRecursive(srcPath, destPath);
428
+ } else {
429
+ fs.copyFileSync(srcPath, destPath);
430
+ }
431
+ }
432
+ }
433
+
434
+ for (const skillDir of selectedSkillDirs) {
435
+ if (!validateSkillDir(skillDir)) {
436
+ console.warn(`āš ļø Skipping invalid skill directory: ${skillDir}`);
437
+ continue;
438
+ }
439
+
440
+ const safeSkillDir = path.basename(skillDir);
441
+ const skillSrc = path.join(templateSkillsDir, safeSkillDir);
442
+ const skillDest = path.join(claudeSkillsDir, safeSkillDir);
443
+
444
+ const normalizedSkillSrc = path.normalize(skillSrc);
445
+ const normalizedTemplateSkillsDir = path.normalize(templateSkillsDir);
446
+ if (!normalizedSkillSrc.startsWith(normalizedTemplateSkillsDir)) {
447
+ console.warn(`āš ļø Skipping skill directory outside template directory: ${skillDir}`);
448
+ continue;
449
+ }
450
+
451
+ if (fs.existsSync(skillSrc) && fs.statSync(skillSrc).isDirectory()) {
452
+ if (!fs.existsSync(skillDest)) {
453
+ if (!dryRun) {
454
+ copyDirRecursive(skillSrc, skillDest);
455
+ }
456
+ copiedSkills++;
457
+ createdItems.push(`.claude/skills/${safeSkillDir}/`);
458
+ if (dryRun) {
459
+ console.log(` ✨ Would copy: .claude/skills/${safeSkillDir}/`);
460
+ }
461
+ } else {
462
+ if (conflictStrategy === 'overwrite') {
463
+ if (!dryRun) {
464
+ copyDirRecursive(skillSrc, skillDest);
465
+ }
466
+ copiedSkills++;
467
+ if (dryRun) {
468
+ console.log(` ā™»ļø Would replace: .claude/skills/${safeSkillDir}/`);
469
+ }
470
+ } else if (conflictStrategy === 'rename') {
471
+ let newDest = path.join(claudeSkillsDir, `${safeSkillDir}-ccstart`);
472
+ let counter = 1;
473
+ while (fs.existsSync(newDest)) {
474
+ newDest = path.join(claudeSkillsDir, `${safeSkillDir}-ccstart-${counter}`);
475
+ counter++;
476
+ }
477
+ if (!dryRun) {
478
+ copyDirRecursive(skillSrc, newDest);
479
+ }
480
+ copiedSkills++;
481
+ const relativePath = path.relative(claudeDir, newDest);
482
+ createdItems.push(`${relativePath}/`);
483
+ if (dryRun) {
484
+ console.log(` šŸ“„ Would create: ${relativePath}/`);
485
+ } else {
486
+ console.log(` šŸ“„ Created: ${relativePath}/`);
487
+ }
488
+ } else {
489
+ skippedSkillsCount++;
490
+ skippedItems.push(`.claude/skills/${safeSkillDir}/`);
491
+ if (dryRun) {
492
+ console.log(` ā­ļø Would skip: .claude/skills/${safeSkillDir}/`);
493
+ }
494
+ }
495
+ }
496
+ }
497
+ }
498
+
372
499
  // Copy .claude/commands directory
373
500
  const claudeCommandsDir = path.join(claudeDir, 'commands');
374
501
  const templateCommandsDir = path.join(templateDir, '.claude', 'commands');
@@ -438,7 +565,7 @@ async function initializeClaudeDirectory(selectedAgentFiles, conflictStrategy, d
438
565
 
439
566
  // Copy .claude/hooks directory
440
567
  const claudeHooksDir = path.join(claudeDir, 'hooks');
441
- const templateHooksDir = path.join(templateDir, '.claude', 'hooks');
568
+ const templateHooksDir = path.join(templateDir, 'claude', 'hooks');
442
569
 
443
570
  if (fs.existsSync(templateHooksDir)) {
444
571
  // Create .claude/hooks directory
@@ -514,7 +641,7 @@ async function initializeClaudeDirectory(selectedAgentFiles, conflictStrategy, d
514
641
  }
515
642
 
516
643
  // Copy settings.json.example if it doesn't exist
517
- const settingsExampleSrc = path.join(templateDir, '.claude', 'settings.json.example');
644
+ const settingsExampleSrc = path.join(templateDir, 'settings.json.example');
518
645
  const settingsExampleDest = path.join(claudeDir, 'settings.json.example');
519
646
 
520
647
  if (fs.existsSync(settingsExampleSrc) && !fs.existsSync(settingsExampleDest)) {
@@ -543,7 +670,9 @@ async function initializeClaudeDirectory(selectedAgentFiles, conflictStrategy, d
543
670
  createdItems,
544
671
  skippedItems,
545
672
  copiedAgents,
546
- skippedAgents
673
+ skippedAgents,
674
+ copiedSkills,
675
+ skippedSkillsCount
547
676
  };
548
677
 
549
678
  } catch (error) {
@@ -556,16 +685,27 @@ function parseAgentFrontmatter(filePath) {
556
685
  try {
557
686
  const content = fs.readFileSync(filePath, 'utf8');
558
687
  const lines = content.split('\n');
559
-
560
- if (lines[0] !== '---') {
688
+
689
+ // Find the start of frontmatter (---) within the first 5 lines
690
+ // This handles: markdown files, shell scripts with shebang, heredoc wrappers
691
+ let startIndex = -1;
692
+ const maxSearchLines = Math.min(5, lines.length);
693
+ for (let i = 0; i < maxSearchLines; i++) {
694
+ if (lines[i] === '---') {
695
+ startIndex = i;
696
+ break;
697
+ }
698
+ }
699
+
700
+ if (startIndex === -1) {
561
701
  return null;
562
702
  }
563
-
703
+
564
704
  let name = '';
565
705
  let description = '';
566
706
  let inFrontmatter = true;
567
- let lineIndex = 1;
568
-
707
+ let lineIndex = startIndex + 1;
708
+
569
709
  while (lineIndex < lines.length && inFrontmatter) {
570
710
  const line = lines[lineIndex];
571
711
  if (line === '---') {
@@ -577,7 +717,7 @@ function parseAgentFrontmatter(filePath) {
577
717
  }
578
718
  lineIndex++;
579
719
  }
580
-
720
+
581
721
  return { name, description };
582
722
  } catch (error) {
583
723
  return null;
@@ -611,6 +751,81 @@ function getAvailableAgents() {
611
751
  return agents.sort((a, b) => a.name.localeCompare(b.name));
612
752
  }
613
753
 
754
+ // Function to validate skill directories for security
755
+ function validateSkillDir(dir) {
756
+ const basename = path.basename(dir);
757
+ if (dir !== basename) {
758
+ return false;
759
+ }
760
+ if (dir.includes('..') || dir.includes('./') || dir.includes('\\')) {
761
+ return false;
762
+ }
763
+ return true;
764
+ }
765
+
766
+ // Function to get available skills
767
+ function getAvailableSkills() {
768
+ const skillsDir = path.join(templateDir, 'claude', 'skills');
769
+ const skills = [];
770
+
771
+ try {
772
+ const dirs = fs.readdirSync(skillsDir);
773
+ for (const dir of dirs) {
774
+ const skillDirPath = path.join(skillsDir, dir);
775
+ if (fs.statSync(skillDirPath).isDirectory()) {
776
+ const skillFiles = fs.readdirSync(skillDirPath);
777
+ const skillMdFile = skillFiles.find(f => f.toLowerCase() === 'skill.md');
778
+ if (skillMdFile) {
779
+ const filePath = path.join(skillDirPath, skillMdFile);
780
+ const metadata = parseAgentFrontmatter(filePath);
781
+ if (metadata && metadata.name) {
782
+ skills.push({
783
+ dir,
784
+ name: metadata.name,
785
+ description: metadata.description || 'No description available'
786
+ });
787
+ }
788
+ }
789
+ }
790
+ }
791
+ } catch (error) {
792
+ console.error('Warning: Could not read skills directory:', error.message);
793
+ }
794
+
795
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
796
+ }
797
+
798
+ // Function to get available hooks
799
+ function getAvailableHooks() {
800
+ const hooksDir = path.join(templateDir, 'claude', 'hooks');
801
+ const hooks = [];
802
+
803
+ try {
804
+ if (!fs.existsSync(hooksDir)) {
805
+ return hooks;
806
+ }
807
+ const files = fs.readdirSync(hooksDir);
808
+ for (const file of files) {
809
+ if (file === 'README.md' || file.startsWith('.')) {
810
+ continue;
811
+ }
812
+ const filePath = path.join(hooksDir, file);
813
+ if (fs.statSync(filePath).isFile()) {
814
+ const metadata = parseAgentFrontmatter(filePath);
815
+ hooks.push({
816
+ file,
817
+ name: metadata && metadata.name ? metadata.name : file.replace(/\.[^.]+$/, ''),
818
+ description: metadata && metadata.description ? metadata.description : 'No description available'
819
+ });
820
+ }
821
+ }
822
+ } catch (error) {
823
+ console.error('Warning: Could not read hooks directory:', error.message);
824
+ }
825
+
826
+ return hooks.sort((a, b) => a.name.localeCompare(b.name));
827
+ }
828
+
614
829
  // Dynamic import for ESM module
615
830
  async function importCheckbox() {
616
831
  try {
@@ -645,17 +860,21 @@ async function selectAgents(availableAgents) {
645
860
  }));
646
861
 
647
862
  console.log('\nšŸ¤– Select agents to include in your Claude Code project\n');
648
- console.log(`${colors.dim}Use arrow keys to navigate, space to select/deselect, 'a' to toggle all${colors.reset}\n`);
649
-
863
+ console.log(`${colors.dim}Agents run in separate context windows with their own conversation history,${colors.reset}`);
864
+ console.log(`${colors.dim}ideal for specialized tasks that don't need your main session context.${colors.reset}\n`);
865
+ console.log(`${colors.dim}Use arrow keys to navigate, space to select, <a> to toggle all${colors.reset}\n`);
866
+
650
867
  const selectedFiles = await checkbox({
651
868
  message: `${colors.bold}Choose your agents:${colors.reset}`,
652
869
  choices,
653
870
  pageSize: 10,
654
871
  loop: false
655
872
  });
656
-
873
+
874
+ const filesToProcess = selectedFiles;
875
+
657
876
  // Validate selected files to prevent path traversal
658
- const validatedFiles = selectedFiles.filter(file => {
877
+ const validatedFiles = filesToProcess.filter(file => {
659
878
  // Use centralized validation function
660
879
  if (!validateAgentFile(file)) {
661
880
  console.warn(`āš ļø Skipping invalid agent file: ${file}`);
@@ -668,7 +887,101 @@ async function selectAgents(availableAgents) {
668
887
  }
669
888
  return true;
670
889
  });
671
-
890
+
891
+ return validatedFiles;
892
+ }
893
+
894
+ async function selectSkills(availableSkills) {
895
+ const checkbox = await importCheckbox();
896
+
897
+ const colors = {
898
+ cyan: '\x1b[36m',
899
+ yellow: '\x1b[33m',
900
+ green: '\x1b[32m',
901
+ blue: '\x1b[34m',
902
+ magenta: '\x1b[35m',
903
+ bold: '\x1b[1m',
904
+ dim: '\x1b[2m',
905
+ reset: '\x1b[0m'
906
+ };
907
+
908
+ const choices = availableSkills.map(skill => ({
909
+ name: `${colors.magenta}${colors.bold}${skill.name}${colors.reset}\n ${colors.dim}${skill.description}${colors.reset}`,
910
+ value: skill.dir,
911
+ checked: false
912
+ }));
913
+
914
+ console.log('\n⚔ Select skills to include in your Claude Code project\n');
915
+ console.log(`${colors.dim}Think of skills as macros - best suited for repetitive actions like${colors.reset}`);
916
+ console.log(`${colors.dim}committing code, creating PRs, or generating tickets.${colors.reset}\n`);
917
+ console.log(`${colors.dim}Use arrow keys to navigate, space to select, <a> to toggle all${colors.reset}\n`);
918
+
919
+ const selectedDirs = await checkbox({
920
+ message: `${colors.bold}Choose your skills:${colors.reset}`,
921
+ choices,
922
+ pageSize: 10,
923
+ loop: false
924
+ });
925
+
926
+ const dirsToProcess = selectedDirs;
927
+
928
+ const validatedDirs = dirsToProcess.filter(dir => {
929
+ if (!validateSkillDir(dir)) {
930
+ console.warn(`āš ļø Skipping invalid skill directory: ${dir}`);
931
+ return false;
932
+ }
933
+ if (!availableSkills.some(skill => skill.dir === dir)) {
934
+ console.warn(`āš ļø Skipping unknown skill directory: ${dir}`);
935
+ return false;
936
+ }
937
+ return true;
938
+ });
939
+
940
+ return validatedDirs;
941
+ }
942
+
943
+ async function selectHooks(availableHooks) {
944
+ const checkbox = await importCheckbox();
945
+
946
+ const colors = {
947
+ cyan: '\x1b[36m',
948
+ yellow: '\x1b[33m',
949
+ green: '\x1b[32m',
950
+ blue: '\x1b[34m',
951
+ magenta: '\x1b[35m',
952
+ bold: '\x1b[1m',
953
+ dim: '\x1b[2m',
954
+ reset: '\x1b[0m'
955
+ };
956
+
957
+ const choices = availableHooks.map(hook => ({
958
+ name: `${colors.yellow}${colors.bold}${hook.name}${colors.reset}\n ${colors.dim}${hook.description}${colors.reset}`,
959
+ value: hook.file,
960
+ checked: false
961
+ }));
962
+
963
+ console.log('\nšŸŖ Select hooks to include in your Claude Code project\n');
964
+ console.log(`${colors.dim}Hooks are shell scripts that run automatically before or after${colors.reset}`);
965
+ console.log(`${colors.dim}Claude Code actions, enabling custom validation and workflows.${colors.reset}\n`);
966
+ console.log(`${colors.dim}Use arrow keys to navigate, space to select, <a> to toggle all${colors.reset}\n`);
967
+
968
+ const selectedFiles = await checkbox({
969
+ message: `${colors.bold}Choose your hooks:${colors.reset}`,
970
+ choices,
971
+ pageSize: 10,
972
+ loop: false
973
+ });
974
+
975
+ const filesToProcess = selectedFiles;
976
+
977
+ const validatedFiles = filesToProcess.filter(file => {
978
+ if (!availableHooks.some(hook => hook.file === file)) {
979
+ console.warn(`āš ļø Skipping unknown hook file: ${file}`);
980
+ return false;
981
+ }
982
+ return true;
983
+ });
984
+
672
985
  return validatedFiles;
673
986
  }
674
987
 
@@ -831,28 +1144,80 @@ async function main() {
831
1144
  } else if (flags.allAgents) {
832
1145
  selectedAgentFiles = availableAgents.map(a => a.file).filter(validateAgentFile);
833
1146
  console.log(`\nāœ… Including all ${selectedAgentFiles.length} agents (--all-agents flag)`);
834
- } else if (!flags.dryRun) {
835
- // Close readline interface before using inquirer
1147
+ } else {
836
1148
  if (rl) {
837
1149
  rl.close();
838
1150
  rl = null;
839
1151
  }
840
-
841
- // Interactive selection
1152
+
842
1153
  selectedAgentFiles = await selectAgents(availableAgents);
843
1154
  console.log(`\nāœ… Selected ${selectedAgentFiles.length} agent${selectedAgentFiles.length === 1 ? '' : 's'}`);
844
-
845
- // Recreate readline interface after agent selection
1155
+
846
1156
  if (!flags.force && !flags.dryRun) {
847
1157
  rl = readline.createInterface({
848
1158
  input: process.stdin,
849
1159
  output: process.stdout
850
1160
  });
851
1161
  }
1162
+ }
1163
+
1164
+ // Get available skills for selection
1165
+ const availableSkills = getAvailableSkills();
1166
+ let selectedSkillDirs = [];
1167
+
1168
+ // Determine which skills to include
1169
+ if (flags.noSkills) {
1170
+ selectedSkillDirs = [];
1171
+ console.log('ā­ļø Skipping skill selection (--no-skills flag)');
1172
+ } else if (flags.allSkills) {
1173
+ selectedSkillDirs = availableSkills.map(s => s.dir).filter(validateSkillDir);
1174
+ console.log(`āœ… Including all ${selectedSkillDirs.length} skills (--all-skills flag)`);
852
1175
  } else {
853
- // In dry-run mode, show what would happen
854
- console.log(`\nWould prompt for agent selection from ${availableAgents.length} available agents`);
855
- selectedAgentFiles = availableAgents.map(a => a.file).filter(validateAgentFile); // Include all for scanning purposes
1176
+ if (rl) {
1177
+ rl.close();
1178
+ rl = null;
1179
+ }
1180
+
1181
+ selectedSkillDirs = await selectSkills(availableSkills);
1182
+ console.log(`\nāœ… Selected ${selectedSkillDirs.length} skill${selectedSkillDirs.length === 1 ? '' : 's'}`);
1183
+
1184
+ if (!flags.force && !flags.dryRun) {
1185
+ rl = readline.createInterface({
1186
+ input: process.stdin,
1187
+ output: process.stdout
1188
+ });
1189
+ }
1190
+ }
1191
+
1192
+ // Get available hooks for selection
1193
+ const availableHooks = getAvailableHooks();
1194
+ let selectedHookFiles = [];
1195
+
1196
+ // Determine which hooks to include
1197
+ if (flags.noHooks) {
1198
+ selectedHookFiles = [];
1199
+ console.log('ā­ļø Skipping hook selection (--no-hooks flag)');
1200
+ } else if (flags.allHooks) {
1201
+ selectedHookFiles = availableHooks.map(h => h.file);
1202
+ console.log(`āœ… Including all ${selectedHookFiles.length} hooks (--all-hooks flag)`);
1203
+ } else if (availableHooks.length === 0) {
1204
+ console.log('\nšŸ“ No hooks available yet');
1205
+ selectedHookFiles = [];
1206
+ } else {
1207
+ if (rl) {
1208
+ rl.close();
1209
+ rl = null;
1210
+ }
1211
+
1212
+ selectedHookFiles = await selectHooks(availableHooks);
1213
+ console.log(`\nāœ… Selected ${selectedHookFiles.length} hook${selectedHookFiles.length === 1 ? '' : 's'}`);
1214
+
1215
+ if (!flags.force && !flags.dryRun) {
1216
+ rl = readline.createInterface({
1217
+ input: process.stdin,
1218
+ output: process.stdout
1219
+ });
1220
+ }
856
1221
  }
857
1222
 
858
1223
  function scanTemplate(src, dest, relativePath = '') {
@@ -895,82 +1260,22 @@ async function main() {
895
1260
  conflictsByCategory['claude'].push(relativePath);
896
1261
  }
897
1262
 
898
- // Process claude subdirectories (except agents which we handle specially)
899
- const claudeSubdirs = ['docs', 'skills', 'tickets'];
1263
+ // Process claude subdirectories - only tickets now (agents and skills go to .claude/)
1264
+ const claudeSubdirs = ['tickets'];
900
1265
  claudeSubdirs.forEach(subdir => {
901
1266
  const subdirSrc = path.join(src, subdir);
902
1267
  const subdirDest = path.join(dest, subdir);
903
1268
  const subdirRelPath = path.join(relativePath, subdir);
904
-
1269
+
905
1270
  if (fs.existsSync(subdirSrc)) {
906
1271
  scanTemplate(subdirSrc, subdirDest, subdirRelPath);
907
1272
  }
908
1273
  });
909
-
910
- // Handle agents directory specially with selected agents
911
- const agentsSrc = path.join(src, 'agents');
912
- const agentsDest = path.join(dest, 'agents');
913
- const agentsRelPath = path.join(relativePath, 'agents');
914
-
915
- allItems.push({
916
- src: agentsSrc,
917
- dest: agentsDest,
918
- type: 'directory',
919
- relativePath: agentsRelPath,
920
- exists: fs.existsSync(agentsDest)
921
- });
922
-
923
- if (fs.existsSync(agentsDest)) {
924
- dirConflicts.push(agentsRelPath);
925
- }
926
-
927
- // Add selected agent files
928
- for (const agentFile of selectedAgentFiles) {
929
- if (!validateAgentFile(agentFile)) {
930
- console.warn(`āš ļø Skipping invalid agent file: ${agentFile}`);
931
- continue;
932
- }
933
-
934
- const safeAgentFile = path.basename(agentFile);
935
- const agentSrc = path.join(agentsSrc, safeAgentFile);
936
- const agentDest = path.join(agentsDest, safeAgentFile);
937
- const agentRelPath = path.join(agentsRelPath, safeAgentFile);
938
-
939
- if (fs.existsSync(agentSrc)) {
940
- allItems.push({
941
- src: agentSrc,
942
- dest: agentDest,
943
- type: 'file',
944
- relativePath: agentRelPath,
945
- exists: fs.existsSync(agentDest)
946
- });
947
-
948
- if (fs.existsSync(agentDest)) {
949
- fileConflicts.push(agentRelPath);
950
- conflictsByCategory['agents'].push(agentRelPath);
951
- }
952
- }
953
- }
954
-
955
- // Always include agents README.md
956
- const agentsReadmeSrc = path.join(agentsSrc, 'README.md');
957
- const agentsReadmeDest = path.join(agentsDest, 'README.md');
958
- if (fs.existsSync(agentsReadmeSrc)) {
959
- allItems.push({
960
- src: agentsReadmeSrc,
961
- dest: agentsReadmeDest,
962
- type: 'file',
963
- relativePath: path.join(agentsRelPath, 'README.md'),
964
- exists: fs.existsSync(agentsReadmeDest)
965
- });
966
-
967
- if (fs.existsSync(agentsReadmeDest)) {
968
- const readmePath = path.join(agentsRelPath, 'README.md');
969
- fileConflicts.push(readmePath);
970
- conflictsByCategory['agents'].push(readmePath);
971
- }
972
- }
973
-
1274
+
1275
+ // Note: skills are now only copied to .claude/skills/ via initializeClaudeDirectory
1276
+
1277
+ // Note: agents are now only copied to .claude/agents/ via initializeClaudeDirectory
1278
+
974
1279
  // Handle CLAUDE.md from claude directory - it goes to root
975
1280
  const claudeMdSrc = path.join(src, 'CLAUDE.md');
976
1281
  const claudeMdDest = path.join(targetDir, 'CLAUDE.md'); // Note: goes to root
@@ -1116,6 +1421,23 @@ async function main() {
1116
1421
  let renamedCount = 0;
1117
1422
  let overwrittenCount = 0;
1118
1423
 
1424
+ // Helper function to copy directories recursively
1425
+ function copyDirRecursiveSync(src, dest) {
1426
+ if (!fs.existsSync(dest)) {
1427
+ fs.mkdirSync(dest, { recursive: true });
1428
+ }
1429
+ const entries = fs.readdirSync(src, { withFileTypes: true });
1430
+ for (const entry of entries) {
1431
+ const srcPath = path.join(src, entry.name);
1432
+ const destPath = path.join(dest, entry.name);
1433
+ if (entry.isDirectory()) {
1434
+ copyDirRecursiveSync(srcPath, destPath);
1435
+ } else {
1436
+ fs.copyFileSync(srcPath, destPath);
1437
+ }
1438
+ }
1439
+ }
1440
+
1119
1441
  for (const item of allItems) {
1120
1442
  try {
1121
1443
  if (item.type === 'directory') {
@@ -1126,6 +1448,47 @@ async function main() {
1126
1448
  console.log(` šŸ“ Would create directory: ${item.relativePath || '.'}/`);
1127
1449
  }
1128
1450
  }
1451
+ } else if (item.type === 'skill-directory') {
1452
+ // Handle skill directory (copy entire directory)
1453
+ let strategy = 'skip';
1454
+ if (item.relativePath.startsWith('claude/skills/')) {
1455
+ strategy = conflictStrategies['docs'];
1456
+ }
1457
+
1458
+ if (item.exists) {
1459
+ if (strategy === 'skip') {
1460
+ skippedCount++;
1461
+ if (flags.dryRun) {
1462
+ console.log(` ā­ļø Would skip: ${item.relativePath}/`);
1463
+ }
1464
+ continue;
1465
+ } else if (strategy === 'rename') {
1466
+ let newDest = `${item.dest}-ccstart`;
1467
+ let counter = 1;
1468
+ while (fs.existsSync(newDest)) {
1469
+ newDest = `${item.dest}-ccstart-${counter}`;
1470
+ counter++;
1471
+ }
1472
+ if (!flags.dryRun) {
1473
+ copyDirRecursiveSync(item.src, newDest);
1474
+ }
1475
+ renamedCount++;
1476
+ console.log(` šŸ“„ ${flags.dryRun ? 'Would create' : 'Created'}: ${path.relative(targetDir, newDest)}/`);
1477
+ } else if (strategy === 'overwrite') {
1478
+ if (!flags.dryRun) {
1479
+ copyDirRecursiveSync(item.src, item.dest);
1480
+ }
1481
+ overwrittenCount++;
1482
+ console.log(` ā™»ļø ${flags.dryRun ? 'Would replace' : 'Replaced'}: ${item.relativePath}/`);
1483
+ }
1484
+ } else {
1485
+ if (!flags.dryRun) {
1486
+ copyDirRecursiveSync(item.src, item.dest);
1487
+ } else {
1488
+ console.log(` ✨ Would copy: ${item.relativePath}/`);
1489
+ }
1490
+ copiedCount++;
1491
+ }
1129
1492
  } else {
1130
1493
  if (item.exists) {
1131
1494
  // Determine which category this file belongs to
@@ -1200,12 +1563,12 @@ async function main() {
1200
1563
  }
1201
1564
  }
1202
1565
 
1203
- // Initialize .claude directory and copy agents
1566
+ // Initialize .claude directory and copy agents, skills, hooks
1204
1567
  let claudeInitResult = null;
1205
1568
  // Always initialize .claude directory structure (it will handle existing directories)
1206
1569
  console.log(`\nšŸ”§ ${claudeStatus.hasClaudeDir ? 'Updating' : 'Initializing'} .claude directory structure...`);
1207
- claudeInitResult = await initializeClaudeDirectory(selectedAgentFiles, conflictStrategies['agents'], flags.dryRun);
1208
-
1570
+ claudeInitResult = await initializeClaudeDirectory(selectedAgentFiles, selectedSkillDirs, selectedHookFiles, conflictStrategies['agents'], flags.dryRun);
1571
+
1209
1572
  if (claudeInitResult.createdItems.length > 0) {
1210
1573
  console.log(` āœ… Created ${claudeInitResult.createdItems.length} items in .claude directory`);
1211
1574
  }
@@ -1215,18 +1578,27 @@ async function main() {
1215
1578
  if (claudeInitResult.skippedAgents > 0) {
1216
1579
  console.log(` ā­ļø Skipped ${claudeInitResult.skippedAgents} existing agents in .claude/agents`);
1217
1580
  }
1581
+ if (claudeInitResult.copiedSkills > 0) {
1582
+ console.log(` ⚔ Copied ${claudeInitResult.copiedSkills} skills to .claude/skills`);
1583
+ }
1584
+ if (claudeInitResult.skippedSkillsCount > 0) {
1585
+ console.log(` ā­ļø Skipped ${claudeInitResult.skippedSkillsCount} existing skills in .claude/skills`);
1586
+ }
1218
1587
 
1219
1588
  console.log(`\nāœ… Claude Code project ${flags.dryRun ? 'would be ' : ''}created successfully!`);
1220
1589
 
1221
1590
  // Show summary of what happened
1222
- if (fileConflicts.length > 0 || copiedCount > 0 || selectedAgentFiles.length > 0 || claudeInitResult) {
1591
+ if (fileConflicts.length > 0 || copiedCount > 0 || selectedAgentFiles.length > 0 || selectedSkillDirs.length > 0 || claudeInitResult) {
1223
1592
  console.log('\nšŸ“Š Summary:');
1224
1593
  if (copiedCount > 0) console.log(` ✨ ${copiedCount} new files ${flags.dryRun ? 'would be ' : ''}copied`);
1225
1594
  if (skippedCount > 0) console.log(` ā­ļø ${skippedCount} existing files ${flags.dryRun ? 'would be ' : ''}kept unchanged`);
1226
1595
  if (renamedCount > 0) console.log(` šŸ“„ ${renamedCount} template files ${flags.dryRun ? 'would be ' : ''}saved with -ccstart suffix`);
1227
1596
  if (overwrittenCount > 0) console.log(` ā™»ļø ${overwrittenCount} files ${flags.dryRun ? 'would be ' : ''}replaced with template versions`);
1228
1597
  if (!flags.noAgents && !flags.dryRun) {
1229
- console.log(` šŸ¤– ${selectedAgentFiles.length} agent${selectedAgentFiles.length === 1 ? '' : 's'} ${flags.dryRun ? 'would be ' : ''}included in claude/agents`);
1598
+ console.log(` šŸ¤– ${selectedAgentFiles.length} agent${selectedAgentFiles.length === 1 ? '' : 's'} ${flags.dryRun ? 'would be ' : ''}included in .claude/agents`);
1599
+ }
1600
+ if (!flags.noSkills && !flags.dryRun) {
1601
+ console.log(` ⚔ ${selectedSkillDirs.length} skill${selectedSkillDirs.length === 1 ? '' : 's'} ${flags.dryRun ? 'would be ' : ''}included in .claude/skills`);
1230
1602
  }
1231
1603
  if (claudeInitResult && claudeInitResult.createdItems.length > 0) {
1232
1604
  console.log(` šŸ“ ${claudeInitResult.createdItems.length} items created in .claude directory`);
@@ -1244,9 +1616,8 @@ async function main() {
1244
1616
  console.log(' claude init # Initialize Claude Code in the project');
1245
1617
  }
1246
1618
  }
1247
- console.log(' 1. Edit CLAUDE.md to add your project-specific instructions');
1248
- console.log(' 2. Update claude/docs/ROADMAP.md with your project goals');
1249
- console.log(' 3. Start creating tickets in the claude/tickets/ directory');
1619
+ console.log(' 1. Use /update-claude-md to update your CLAUDE.md with your repository\'s information');
1620
+ console.log(' 2. Use /create-ticket to start creating tickets');
1250
1621
 
1251
1622
  if (renamedCount > 0) {
1252
1623
  console.log('\nšŸ’” Tip: Review the -ccstart files to see template examples');