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.
- package/README.md +50 -27
- package/bin/create-project.js +473 -102
- package/package.json +3 -12
- package/template/claude/CLAUDE.md +11 -92
- package/template/claude/agents/backend.md +1 -1
- package/template/claude/agents/frontend.md +1 -1
- package/template/claude/hooks/claude-md-reminder.sh +15 -0
- package/template/claude/hooks/ticket-reminder.sh +19 -0
- package/template/claude/skills/create-script/SKILL.md +362 -0
- package/template/claude/skills/design-feature/SKILL.md +3 -3
- package/template/claude/skills/skill-creator/SKILL.md +1 -1
- package/template/claude/docs/ROADMAP.md +0 -62
- package/template/claude/docs/agent-orchestration.md +0 -183
- package/template/claude/skills/design-principles/skill.md +0 -237
package/bin/create-project.js
CHANGED
|
@@ -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, '
|
|
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, '
|
|
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
|
-
|
|
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}
|
|
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 =
|
|
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
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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 (
|
|
899
|
-
const claudeSubdirs = ['
|
|
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
|
-
//
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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.
|
|
1248
|
-
console.log(' 2.
|
|
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');
|