agentic-team-templates 0.4.2 → 0.6.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/src/index.js CHANGED
@@ -50,6 +50,10 @@ const SHARED_RULES = [
50
50
  'security-fundamentals.md'
51
51
  ];
52
52
 
53
+ // Supported IDEs/tools
54
+ const SUPPORTED_IDES = ['cursor', 'claude', 'codex'];
55
+ const DEFAULT_IDES = ['cursor', 'claude', 'codex']; // Default: install for all IDEs
56
+
53
57
  // Colors
54
58
  const colors = {
55
59
  red: (s) => `\x1b[31m${s}\x1b[0m`,
@@ -69,21 +73,44 @@ function printBanner() {
69
73
 
70
74
  function printHelp() {
71
75
  console.log(`${colors.yellow('Usage:')}
72
- npx cursor-templates <templates...>
76
+ npx cursor-templates <templates...> [options]
77
+ npx cursor-templates --remove <templates...> [options]
78
+ npx cursor-templates --reset [options]
73
79
 
74
80
  ${colors.yellow('Options:')}
81
+ --ide=<name> Install for specific IDE (cursor, claude, codex)
82
+ Can be specified multiple times: --ide=cursor --ide=claude
83
+ Default: all (cursor, claude, codex)
75
84
  --list, -l List available templates
76
85
  --help, -h Show this help message
77
- --dry-run Show what would be installed
78
- --force, -f Overwrite existing files (default: skip)
86
+ --dry-run Show what would be changed
87
+ --force, -f Overwrite/remove even if files were modified
88
+ --yes, -y Skip confirmation prompt (for --remove and --reset)
89
+
90
+ ${colors.yellow('Removal Options:')}
91
+ --remove Remove specified templates (keeps shared rules and other templates)
92
+ --reset Remove ALL installed content (shared rules, templates, generated files)
93
+
94
+ ${colors.yellow('IDE Targets:')}
95
+ cursor .cursorrules/ directory (Cursor IDE)
96
+ claude CLAUDE.md file (Claude Code, Cursor with Claude)
97
+ codex .github/copilot-instructions.md (GitHub Copilot)
79
98
 
80
99
  ${colors.yellow('Examples:')}
81
100
  npx cursor-templates web-frontend
82
- npx cursor-templates web-frontend web-backend
83
- npx cursor-templates fullstack
84
- npx cursor-templates mobile utility-agent
101
+ npx cursor-templates web-frontend --ide=cursor
102
+ npx cursor-templates web-frontend --ide=claude --ide=codex
103
+ npx cursor-templates fullstack --ide=codex
85
104
  npx cursor-templates web-backend --force
86
105
 
106
+ ${colors.yellow('Removal Examples:')}
107
+ npx cursor-templates --remove web-frontend
108
+ npx cursor-templates --remove web-frontend web-backend
109
+ npx cursor-templates --remove web-frontend --ide=cursor
110
+ npx cursor-templates --reset
111
+ npx cursor-templates --reset --ide=cursor
112
+ npx cursor-templates --reset --yes
113
+
87
114
  ${colors.dim('Shared rules (code-quality, security, git-workflow, etc.) are always included.')}
88
115
  ${colors.dim('Identical files are skipped. Modified files are preserved; ours saved as *-1.md.')}
89
116
  ${colors.dim('CLAUDE.md: missing sections are intelligently merged (not overwritten).')}
@@ -469,146 +496,291 @@ function generateClaudeMdToPath(targetDir, installedTemplates, destPath) {
469
496
  fs.writeFileSync(destPath, content);
470
497
  }
471
498
 
472
- function install(targetDir, templates, dryRun = false, force = false) {
473
- const cursorrules = path.join(targetDir, '.cursorrules');
474
-
475
- if (!dryRun && !fs.existsSync(cursorrules)) {
476
- fs.mkdirSync(cursorrules, { recursive: true });
477
- }
499
+ /**
500
+ * Generate content for GitHub Copilot instructions file
501
+ */
502
+ function generateCopilotInstructionsContent(installedTemplates) {
503
+ const templateList = installedTemplates
504
+ .map(t => `- ${t}: ${TEMPLATES[t].description}`)
505
+ .join('\n');
506
+
507
+ // Read and concatenate all shared rules
508
+ const sharedRulesContent = SHARED_RULES.map(rule => {
509
+ const rulePath = path.join(TEMPLATES_DIR, '_shared', rule);
510
+ try {
511
+ return fs.readFileSync(rulePath, 'utf8');
512
+ } catch {
513
+ return '';
514
+ }
515
+ }).filter(Boolean).join('\n\n---\n\n');
516
+
517
+ // Read and concatenate template-specific rules
518
+ const templateRulesContent = installedTemplates.map(template => {
519
+ return TEMPLATES[template].rules.map(rule => {
520
+ const rulePath = path.join(TEMPLATES_DIR, template, '.cursorrules', rule);
521
+ try {
522
+ return fs.readFileSync(rulePath, 'utf8');
523
+ } catch {
524
+ return '';
525
+ }
526
+ }).filter(Boolean).join('\n\n');
527
+ }).join('\n\n---\n\n');
528
+
529
+ return `# Copilot Instructions
530
+
531
+ This file provides coding guidelines for GitHub Copilot in this project.
532
+
533
+ ## Project Configuration
534
+
535
+ **Installed Templates:** ${installedTemplates.join(', ')}
536
+
537
+ ${templateList}
538
+
539
+ ---
540
+
541
+ ## Core Principles
542
+
543
+ ### Honesty Over Output
544
+ - If something doesn't work, say it doesn't work
545
+ - If you don't know, say you don't know
546
+ - Never hide errors or suppress warnings
547
+
548
+ ### Security First
549
+ - Zero trust: Every input is hostile until proven otherwise
550
+ - Validate and sanitize all inputs
551
+ - No secrets in code or logs
552
+
553
+ ### Tests Are Required
554
+ - No feature is complete without tests
555
+ - Test behavior, not implementation
556
+
557
+ ### Code Quality
558
+ - SOLID principles
559
+ - DRY (Don't Repeat Yourself)
560
+ - Explicit over implicit
561
+
562
+ ---
563
+
564
+ ## Shared Guidelines
565
+
566
+ ${sharedRulesContent}
567
+
568
+ ---
569
+
570
+ ## Template-Specific Guidelines
571
+
572
+ ${templateRulesContent}
573
+
574
+ ---
575
+
576
+ ## Definition of Done
577
+
578
+ A feature is complete when:
579
+ - [ ] Code written and reviewed
580
+ - [ ] Tests written and passing
581
+ - [ ] No linting errors
582
+ - [ ] Security reviewed
583
+ - [ ] Documentation updated
584
+ `;
585
+ }
586
+
587
+ function install(targetDir, templates, dryRun = false, force = false, ides = DEFAULT_IDES) {
588
+ const stats = { copied: 0, skipped: 0, updated: 0, renamed: 0 };
589
+ const renamedFiles = [];
590
+ const installedFor = [];
478
591
 
479
592
  console.log(`${colors.blue('Installing to:')} ${targetDir}`);
593
+ console.log(`${colors.blue('Target IDEs:')} ${ides.join(', ')}`);
480
594
  if (!force) {
481
595
  console.log(colors.dim('(identical files skipped, modified files preserved with ours saved as *-1.md)'));
482
- console.log(colors.dim('(CLAUDE.md: missing sections merged intelligently)'));
483
596
  }
484
597
  console.log();
485
598
 
486
- const stats = { copied: 0, skipped: 0, updated: 0, renamed: 0 };
487
- const renamedFiles = [];
488
-
489
- // 1. Install shared rules
490
- console.log(colors.green('► Installing shared rules...'));
491
- for (const rule of SHARED_RULES) {
492
- const src = path.join(TEMPLATES_DIR, '_shared', rule);
493
- const dest = path.join(cursorrules, rule);
599
+ // 1. Install .cursorrules/ for Cursor IDE
600
+ if (ides.includes('cursor')) {
601
+ installedFor.push('cursor');
602
+ const cursorrules = path.join(targetDir, '.cursorrules');
494
603
 
495
- if (dryRun) {
496
- const exists = fs.existsSync(dest);
497
- if (!exists) {
498
- console.log(` ${colors.dim('[copy]')} ${rule}`);
499
- } else if (force) {
500
- console.log(` ${colors.dim('[update]')} ${rule}`);
501
- } else if (filesMatch(src, dest)) {
502
- console.log(` ${colors.yellow('[skip]')} ${rule} (identical)`);
503
- } else {
504
- const altName = path.basename(getAlternateFilename(dest));
505
- console.log(` ${colors.blue('[rename]')} ${rule} → ${altName}`);
506
- }
507
- } else {
508
- const result = copyFile(src, dest, force);
509
- stats[result.status]++;
510
- if (result.status === 'skipped') {
511
- console.log(` ${colors.yellow('[skip]')} ${rule} (identical)`);
512
- } else if (result.status === 'renamed') {
513
- const altName = path.basename(result.destFile);
514
- renamedFiles.push({ original: rule, renamed: altName });
515
- console.log(` ${colors.blue('[rename]')} ${rule} → ${altName}`);
516
- } else {
517
- console.log(` ${colors.dim(`[${result.status}]`)} ${rule}`);
518
- }
604
+ if (!dryRun && !fs.existsSync(cursorrules)) {
605
+ fs.mkdirSync(cursorrules, { recursive: true });
519
606
  }
520
- }
521
- console.log();
522
607
 
523
- // 2. Install template-specific rules
524
- for (const template of templates) {
525
- console.log(colors.green(`► Installing ${template} template...`));
526
-
527
- for (const rule of TEMPLATES[template].rules) {
528
- const src = path.join(TEMPLATES_DIR, template, '.cursorrules', rule);
529
- const dest = path.join(cursorrules, `${template}-${rule}`);
530
- const destName = `${template}-${rule}`;
608
+ // Install shared rules
609
+ console.log(colors.green('► Installing shared rules (.cursorrules/)...'));
610
+ for (const rule of SHARED_RULES) {
611
+ const src = path.join(TEMPLATES_DIR, '_shared', rule);
612
+ const dest = path.join(cursorrules, rule);
531
613
 
532
614
  if (dryRun) {
533
615
  const exists = fs.existsSync(dest);
534
616
  if (!exists) {
535
- console.log(` ${colors.dim('[copy]')} ${destName}`);
617
+ console.log(` ${colors.dim('[copy]')} ${rule}`);
536
618
  } else if (force) {
537
- console.log(` ${colors.dim('[update]')} ${destName}`);
619
+ console.log(` ${colors.dim('[update]')} ${rule}`);
538
620
  } else if (filesMatch(src, dest)) {
539
- console.log(` ${colors.yellow('[skip]')} ${destName} (identical)`);
621
+ console.log(` ${colors.yellow('[skip]')} ${rule} (identical)`);
540
622
  } else {
541
623
  const altName = path.basename(getAlternateFilename(dest));
542
- console.log(` ${colors.blue('[rename]')} ${destName} → ${altName}`);
624
+ console.log(` ${colors.blue('[rename]')} ${rule} → ${altName}`);
543
625
  }
544
626
  } else {
545
627
  const result = copyFile(src, dest, force);
546
628
  stats[result.status]++;
547
629
  if (result.status === 'skipped') {
548
- console.log(` ${colors.yellow('[skip]')} ${destName} (identical)`);
630
+ console.log(` ${colors.yellow('[skip]')} ${rule} (identical)`);
549
631
  } else if (result.status === 'renamed') {
550
632
  const altName = path.basename(result.destFile);
551
- renamedFiles.push({ original: destName, renamed: altName });
552
- console.log(` ${colors.blue('[rename]')} ${destName} → ${altName}`);
633
+ renamedFiles.push({ original: rule, renamed: altName });
634
+ console.log(` ${colors.blue('[rename]')} ${rule} → ${altName}`);
553
635
  } else {
554
- console.log(` ${colors.dim(`[${result.status}]`)} ${destName}`);
636
+ console.log(` ${colors.dim(`[${result.status}]`)} ${rule}`);
555
637
  }
556
638
  }
557
639
  }
558
640
  console.log();
641
+
642
+ // Install template-specific rules
643
+ for (const template of templates) {
644
+ console.log(colors.green(`► Installing ${template} template (.cursorrules/)...`));
645
+
646
+ for (const rule of TEMPLATES[template].rules) {
647
+ const src = path.join(TEMPLATES_DIR, template, '.cursorrules', rule);
648
+ const dest = path.join(cursorrules, `${template}-${rule}`);
649
+ const destName = `${template}-${rule}`;
650
+
651
+ if (dryRun) {
652
+ const exists = fs.existsSync(dest);
653
+ if (!exists) {
654
+ console.log(` ${colors.dim('[copy]')} ${destName}`);
655
+ } else if (force) {
656
+ console.log(` ${colors.dim('[update]')} ${destName}`);
657
+ } else if (filesMatch(src, dest)) {
658
+ console.log(` ${colors.yellow('[skip]')} ${destName} (identical)`);
659
+ } else {
660
+ const altName = path.basename(getAlternateFilename(dest));
661
+ console.log(` ${colors.blue('[rename]')} ${destName} → ${altName}`);
662
+ }
663
+ } else {
664
+ const result = copyFile(src, dest, force);
665
+ stats[result.status]++;
666
+ if (result.status === 'skipped') {
667
+ console.log(` ${colors.yellow('[skip]')} ${destName} (identical)`);
668
+ } else if (result.status === 'renamed') {
669
+ const altName = path.basename(result.destFile);
670
+ renamedFiles.push({ original: destName, renamed: altName });
671
+ console.log(` ${colors.blue('[rename]')} ${destName} → ${altName}`);
672
+ } else {
673
+ console.log(` ${colors.dim(`[${result.status}]`)} ${destName}`);
674
+ }
675
+ }
676
+ }
677
+ console.log();
678
+ }
559
679
  }
560
680
 
561
- // 3. Generate CLAUDE.md
562
- const claudePath = path.join(targetDir, 'CLAUDE.md');
563
- const claudeExists = fs.existsSync(claudePath);
564
- const templateContent = generateClaudeMdContent(templates);
565
-
566
- console.log(colors.green('► Generating CLAUDE.md...'));
567
- if (dryRun) {
568
- if (!claudeExists) {
569
- console.log(` ${colors.dim('[copy]')} CLAUDE.md`);
681
+ // 2. Generate CLAUDE.md for Claude Code
682
+ if (ides.includes('claude')) {
683
+ installedFor.push('claude');
684
+ const claudePath = path.join(targetDir, 'CLAUDE.md');
685
+ const claudeExists = fs.existsSync(claudePath);
686
+ const templateContent = generateClaudeMdContent(templates);
687
+
688
+ console.log(colors.green('► Generating CLAUDE.md (Claude Code)...'));
689
+ if (dryRun) {
690
+ if (!claudeExists) {
691
+ console.log(` ${colors.dim('[copy]')} CLAUDE.md`);
692
+ } else if (force) {
693
+ console.log(` ${colors.dim('[update]')} CLAUDE.md`);
694
+ } else {
695
+ const existingContent = fs.readFileSync(claudePath, 'utf8');
696
+ const { missing } = findMissingSections(existingContent, templateContent);
697
+ if (missing.length === 0) {
698
+ console.log(` ${colors.yellow('[skip]')} CLAUDE.md (all sections present)`);
699
+ } else {
700
+ console.log(` ${colors.blue('[merge]')} CLAUDE.md (would add ${missing.length} section(s))`);
701
+ for (const section of missing) {
702
+ console.log(` ${colors.dim('+')} ${section.heading}`);
703
+ }
704
+ }
705
+ }
706
+ } else if (!claudeExists) {
707
+ fs.writeFileSync(claudePath, templateContent);
708
+ console.log(` ${colors.dim('[copied]')} CLAUDE.md`);
709
+ stats.copied++;
570
710
  } else if (force) {
571
- console.log(` ${colors.dim('[update]')} CLAUDE.md`);
711
+ fs.writeFileSync(claudePath, templateContent);
712
+ console.log(` ${colors.dim('[updated]')} CLAUDE.md`);
713
+ stats.updated++;
572
714
  } else {
573
- // Check what would be merged
574
715
  const existingContent = fs.readFileSync(claudePath, 'utf8');
575
- const { missing } = findMissingSections(existingContent, templateContent);
576
- if (missing.length === 0) {
716
+ const { merged, addedSections } = mergeClaudeContent(existingContent, templateContent);
717
+
718
+ if (addedSections.length === 0) {
577
719
  console.log(` ${colors.yellow('[skip]')} CLAUDE.md (all sections present)`);
720
+ stats.skipped++;
578
721
  } else {
579
- console.log(` ${colors.blue('[merge]')} CLAUDE.md (would add ${missing.length} section(s))`);
580
- for (const section of missing) {
581
- console.log(` ${colors.dim('+')} ${section.heading}`);
722
+ fs.writeFileSync(claudePath, merged);
723
+ console.log(` ${colors.blue('[merged]')} CLAUDE.md`);
724
+ console.log(` ${colors.green('Added sections:')}`);
725
+ for (const heading of addedSections) {
726
+ console.log(` ${colors.dim('+')} ${heading}`);
582
727
  }
728
+ stats.updated++;
583
729
  }
584
730
  }
585
- } else if (!claudeExists) {
586
- fs.writeFileSync(claudePath, templateContent);
587
- console.log(` ${colors.dim('[copied]')} CLAUDE.md`);
588
- stats.copied++;
589
- } else if (force) {
590
- fs.writeFileSync(claudePath, templateContent);
591
- console.log(` ${colors.dim('[updated]')} CLAUDE.md`);
592
- stats.updated++;
593
- } else {
594
- // Intelligent merge: append only missing sections
595
- const existingContent = fs.readFileSync(claudePath, 'utf8');
596
- const { merged, addedSections } = mergeClaudeContent(existingContent, templateContent);
731
+ console.log();
732
+ }
733
+
734
+ // 3. Generate .github/copilot-instructions.md for GitHub Copilot (Codex)
735
+ if (ides.includes('codex')) {
736
+ installedFor.push('codex');
737
+ const githubDir = path.join(targetDir, '.github');
738
+ const copilotPath = path.join(githubDir, 'copilot-instructions.md');
739
+ const copilotExists = fs.existsSync(copilotPath);
740
+ const copilotContent = generateCopilotInstructionsContent(templates);
597
741
 
598
- if (addedSections.length === 0) {
599
- console.log(` ${colors.yellow('[skip]')} CLAUDE.md (all sections present)`);
600
- stats.skipped++;
601
- } else {
602
- fs.writeFileSync(claudePath, merged);
603
- console.log(` ${colors.blue('[merged]')} CLAUDE.md`);
604
- console.log(` ${colors.green('Added sections:')}`);
605
- for (const heading of addedSections) {
606
- console.log(` ${colors.dim('+')} ${heading}`);
742
+ if (!dryRun && !fs.existsSync(githubDir)) {
743
+ fs.mkdirSync(githubDir, { recursive: true });
744
+ }
745
+
746
+ console.log(colors.green('► Generating .github/copilot-instructions.md (GitHub Copilot)...'));
747
+ if (dryRun) {
748
+ if (!copilotExists) {
749
+ console.log(` ${colors.dim('[copy]')} .github/copilot-instructions.md`);
750
+ } else if (force) {
751
+ console.log(` ${colors.dim('[update]')} .github/copilot-instructions.md`);
752
+ } else {
753
+ const existingContent = fs.readFileSync(copilotPath, 'utf8');
754
+ const { missing } = findMissingSections(existingContent, copilotContent);
755
+ if (missing.length === 0) {
756
+ console.log(` ${colors.yellow('[skip]')} .github/copilot-instructions.md (all sections present)`);
757
+ } else {
758
+ console.log(` ${colors.blue('[merge]')} .github/copilot-instructions.md (would add ${missing.length} section(s))`);
759
+ }
607
760
  }
761
+ } else if (!copilotExists) {
762
+ fs.writeFileSync(copilotPath, copilotContent);
763
+ console.log(` ${colors.dim('[copied]')} .github/copilot-instructions.md`);
764
+ stats.copied++;
765
+ } else if (force) {
766
+ fs.writeFileSync(copilotPath, copilotContent);
767
+ console.log(` ${colors.dim('[updated]')} .github/copilot-instructions.md`);
608
768
  stats.updated++;
769
+ } else {
770
+ const existingContent = fs.readFileSync(copilotPath, 'utf8');
771
+ const { merged, addedSections } = mergeClaudeContent(existingContent, copilotContent);
772
+
773
+ if (addedSections.length === 0) {
774
+ console.log(` ${colors.yellow('[skip]')} .github/copilot-instructions.md (all sections present)`);
775
+ stats.skipped++;
776
+ } else {
777
+ fs.writeFileSync(copilotPath, merged);
778
+ console.log(` ${colors.blue('[merged]')} .github/copilot-instructions.md`);
779
+ stats.updated++;
780
+ }
609
781
  }
782
+ console.log();
610
783
  }
611
- console.log();
612
784
 
613
785
  // Summary
614
786
  console.log(colors.green('════════════════════════════════════════════════════════════'));
@@ -627,7 +799,18 @@ function install(targetDir, templates, dryRun = false, force = false) {
627
799
  }
628
800
  console.log();
629
801
 
630
- console.log(colors.yellow('Templates installed:'));
802
+ console.log(colors.yellow('Installed for:'));
803
+ for (const ide of installedFor) {
804
+ const ideInfo = {
805
+ cursor: '.cursorrules/ (Cursor IDE)',
806
+ claude: 'CLAUDE.md (Claude Code)',
807
+ codex: '.github/copilot-instructions.md (GitHub Copilot)'
808
+ };
809
+ console.log(` - ${ideInfo[ide]}`);
810
+ }
811
+ console.log();
812
+
813
+ console.log(colors.yellow('Templates:'));
631
814
  console.log(' - _shared (always included)');
632
815
  for (const template of templates) {
633
816
  console.log(` - ${template}`);
@@ -645,51 +828,489 @@ function install(targetDir, templates, dryRun = false, force = false) {
645
828
  }
646
829
 
647
830
  console.log(colors.blue('Next steps:'));
648
- console.log(' 1. Review CLAUDE.md for any customization');
649
- console.log(' 2. Commit the new files to your repository');
831
+ if (installedFor.includes('claude')) {
832
+ console.log(' 1. Review CLAUDE.md for any customization');
833
+ }
834
+ if (installedFor.includes('codex')) {
835
+ console.log(' 2. Review .github/copilot-instructions.md');
836
+ }
837
+ console.log(' 3. Commit the new files to your repository');
838
+ console.log();
839
+ }
840
+
841
+ /**
842
+ * Prompt user for confirmation
843
+ * @param {string} message - The prompt message
844
+ * @returns {Promise<boolean>}
845
+ */
846
+ async function confirm(message) {
847
+ const readline = await import('readline');
848
+ const rl = readline.createInterface({
849
+ input: process.stdin,
850
+ output: process.stdout
851
+ });
852
+
853
+ return new Promise((resolve) => {
854
+ rl.question(`${message} [y/N] `, (answer) => {
855
+ rl.close();
856
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
857
+ });
858
+ });
859
+ }
860
+
861
+ /**
862
+ * Check if a file was created by our installer (matches template content)
863
+ * @param {string} filePath - Path to the file
864
+ * @param {string} templatePath - Path to the template file
865
+ * @returns {boolean}
866
+ */
867
+ function isOurFile(filePath, templatePath) {
868
+ if (!fs.existsSync(filePath)) return false;
869
+ if (!fs.existsSync(templatePath)) return true; // No template to compare, assume ours
870
+ return filesMatch(filePath, templatePath);
871
+ }
872
+
873
+ /**
874
+ * Remove specific templates from the installation
875
+ */
876
+ async function remove(targetDir, templates, dryRun = false, force = false, skipConfirm = false, ides = DEFAULT_IDES) {
877
+ const stats = { removed: 0, skipped: 0, notFound: 0 };
878
+ const filesToRemove = [];
879
+ const modifiedFiles = [];
880
+
881
+ console.log(`${colors.blue('Removing from:')} ${targetDir}`);
882
+ console.log(`${colors.blue('Target IDEs:')} ${ides.join(', ')}`);
883
+ console.log(`${colors.blue('Templates:')} ${templates.join(', ')}`);
884
+ console.log();
885
+
886
+ // 1. Collect files to remove from .cursorrules/
887
+ if (ides.includes('cursor')) {
888
+ const cursorrules = path.join(targetDir, '.cursorrules');
889
+
890
+ if (fs.existsSync(cursorrules)) {
891
+ for (const template of templates) {
892
+ console.log(colors.yellow(`► Scanning ${template} template files...`));
893
+
894
+ for (const rule of TEMPLATES[template].rules) {
895
+ const destName = `${template}-${rule}`;
896
+ const destPath = path.join(cursorrules, destName);
897
+ const srcPath = path.join(TEMPLATES_DIR, template, '.cursorrules', rule);
898
+
899
+ if (!fs.existsSync(destPath)) {
900
+ console.log(` ${colors.dim('[not found]')} ${destName}`);
901
+ stats.notFound++;
902
+ continue;
903
+ }
904
+
905
+ const isUnmodified = isOurFile(destPath, srcPath);
906
+
907
+ if (!isUnmodified && !force) {
908
+ console.log(` ${colors.yellow('[modified]')} ${destName} (use --force to remove)`);
909
+ modifiedFiles.push(destName);
910
+ stats.skipped++;
911
+ } else {
912
+ console.log(` ${colors.red('[remove]')} ${destName}${!isUnmodified ? ' (modified, --force)' : ''}`);
913
+ filesToRemove.push({ path: destPath, name: destName });
914
+ }
915
+ }
916
+
917
+ // Also check for -1 variant files
918
+ for (const rule of TEMPLATES[template].rules) {
919
+ const altName = `${template}-${rule.replace('.md', '-1.md')}`;
920
+ const altPath = path.join(cursorrules, altName);
921
+
922
+ if (fs.existsSync(altPath)) {
923
+ console.log(` ${colors.red('[remove]')} ${altName} (alternate file)`);
924
+ filesToRemove.push({ path: altPath, name: altName });
925
+ }
926
+ }
927
+
928
+ console.log();
929
+ }
930
+ } else {
931
+ console.log(colors.dim('No .cursorrules/ directory found.\n'));
932
+ }
933
+ }
934
+
935
+ // 2. Note about CLAUDE.md and copilot-instructions.md
936
+ // These are regenerated, not patched, so we can't easily remove just one template's content
937
+ // We'll warn the user about this
938
+ if (ides.includes('claude') || ides.includes('codex')) {
939
+ console.log(colors.yellow('Note: CLAUDE.md and copilot-instructions.md contain merged content.'));
940
+ console.log(colors.dim('To update these files, re-run the installer with the remaining templates.\n'));
941
+ }
942
+
943
+ if (filesToRemove.length === 0) {
944
+ console.log(colors.yellow('Nothing to remove.\n'));
945
+ return;
946
+ }
947
+
948
+ // Confirmation
949
+ if (!dryRun && !skipConfirm) {
950
+ console.log(colors.yellow(`\nAbout to remove ${filesToRemove.length} file(s).`));
951
+ const confirmed = await confirm(colors.red('Proceed with removal?'));
952
+ if (!confirmed) {
953
+ console.log(colors.dim('\nAborted.\n'));
954
+ return;
955
+ }
956
+ console.log();
957
+ }
958
+
959
+ // Execute removal
960
+ if (dryRun) {
961
+ console.log(colors.yellow('DRY RUN - No files were removed.\n'));
962
+ } else {
963
+ for (const file of filesToRemove) {
964
+ try {
965
+ fs.unlinkSync(file.path);
966
+ stats.removed++;
967
+ } catch (err) {
968
+ console.error(colors.red(`Failed to remove ${file.name}: ${err.message}`));
969
+ }
970
+ }
971
+ }
972
+
973
+ // Summary
974
+ console.log(colors.green('════════════════════════════════════════════════════════════'));
975
+ console.log(colors.green(`✓ Removal complete!\n`));
976
+
977
+ console.log(colors.yellow('Summary:'));
978
+ console.log(` - ${stats.removed} files removed`);
979
+ if (stats.skipped > 0) {
980
+ console.log(` - ${stats.skipped} files skipped (modified, use --force)`);
981
+ }
982
+ if (stats.notFound > 0) {
983
+ console.log(` - ${stats.notFound} files not found`);
984
+ }
985
+ console.log();
986
+
987
+ if (modifiedFiles.length > 0) {
988
+ console.log(colors.yellow('Modified files preserved:'));
989
+ for (const file of modifiedFiles) {
990
+ console.log(` - ${file}`);
991
+ }
992
+ console.log(colors.dim('\nUse --force to remove modified files.\n'));
993
+ }
994
+ }
995
+
996
+ /**
997
+ * Reset - remove all installed content
998
+ */
999
+ async function reset(targetDir, dryRun = false, force = false, skipConfirm = false, ides = DEFAULT_IDES) {
1000
+ const stats = { removed: 0, skipped: 0 };
1001
+ const filesToRemove = [];
1002
+ const modifiedFiles = [];
1003
+ const dirsToRemove = [];
1004
+
1005
+ console.log(`${colors.blue('Resetting:')} ${targetDir}`);
1006
+ console.log(`${colors.blue('Target IDEs:')} ${ides.join(', ')}`);
1007
+ console.log();
1008
+
1009
+ // 1. Remove .cursorrules/ contents for Cursor
1010
+ if (ides.includes('cursor')) {
1011
+ const cursorrules = path.join(targetDir, '.cursorrules');
1012
+
1013
+ if (fs.existsSync(cursorrules)) {
1014
+ console.log(colors.yellow('► Scanning .cursorrules/ directory...'));
1015
+
1016
+ // Check shared rules
1017
+ for (const rule of SHARED_RULES) {
1018
+ const destPath = path.join(cursorrules, rule);
1019
+ const srcPath = path.join(TEMPLATES_DIR, '_shared', rule);
1020
+
1021
+ if (!fs.existsSync(destPath)) continue;
1022
+
1023
+ const isUnmodified = isOurFile(destPath, srcPath);
1024
+
1025
+ if (!isUnmodified && !force) {
1026
+ console.log(` ${colors.yellow('[modified]')} ${rule} (use --force to remove)`);
1027
+ modifiedFiles.push(rule);
1028
+ stats.skipped++;
1029
+ } else {
1030
+ console.log(` ${colors.red('[remove]')} ${rule}${!isUnmodified ? ' (modified, --force)' : ''}`);
1031
+ filesToRemove.push({ path: destPath, name: rule });
1032
+ }
1033
+
1034
+ // Check for -1 variant
1035
+ const altPath = path.join(cursorrules, rule.replace('.md', '-1.md'));
1036
+ if (fs.existsSync(altPath)) {
1037
+ console.log(` ${colors.red('[remove]')} ${rule.replace('.md', '-1.md')} (alternate file)`);
1038
+ filesToRemove.push({ path: altPath, name: rule.replace('.md', '-1.md') });
1039
+ }
1040
+ }
1041
+
1042
+ // Check template-specific rules
1043
+ for (const [templateName, templateInfo] of Object.entries(TEMPLATES)) {
1044
+ for (const rule of templateInfo.rules) {
1045
+ const destName = `${templateName}-${rule}`;
1046
+ const destPath = path.join(cursorrules, destName);
1047
+ const srcPath = path.join(TEMPLATES_DIR, templateName, '.cursorrules', rule);
1048
+
1049
+ if (!fs.existsSync(destPath)) continue;
1050
+
1051
+ const isUnmodified = isOurFile(destPath, srcPath);
1052
+
1053
+ if (!isUnmodified && !force) {
1054
+ console.log(` ${colors.yellow('[modified]')} ${destName} (use --force to remove)`);
1055
+ modifiedFiles.push(destName);
1056
+ stats.skipped++;
1057
+ } else {
1058
+ console.log(` ${colors.red('[remove]')} ${destName}${!isUnmodified ? ' (modified, --force)' : ''}`);
1059
+ filesToRemove.push({ path: destPath, name: destName });
1060
+ }
1061
+
1062
+ // Check for -1 variant
1063
+ const altName = destName.replace('.md', '-1.md');
1064
+ const altPath = path.join(cursorrules, altName);
1065
+ if (fs.existsSync(altPath)) {
1066
+ console.log(` ${colors.red('[remove]')} ${altName} (alternate file)`);
1067
+ filesToRemove.push({ path: altPath, name: altName });
1068
+ }
1069
+ }
1070
+ }
1071
+
1072
+ // Check if we should remove the directory itself (only if it would be empty)
1073
+ const remainingFiles = fs.readdirSync(cursorrules).filter(f => {
1074
+ const fullPath = path.join(cursorrules, f);
1075
+ const willBeRemoved = filesToRemove.some(fr => fr.path === fullPath);
1076
+ return !willBeRemoved;
1077
+ });
1078
+
1079
+ if (remainingFiles.length === 0 || force) {
1080
+ console.log(` ${colors.red('[remove]')} .cursorrules/ directory`);
1081
+ dirsToRemove.push(cursorrules);
1082
+ } else if (remainingFiles.length > 0) {
1083
+ console.log(colors.dim(` .cursorrules/ will be kept (${remainingFiles.length} non-template file(s) remain)`));
1084
+ }
1085
+
1086
+ console.log();
1087
+ } else {
1088
+ console.log(colors.dim('No .cursorrules/ directory found.\n'));
1089
+ }
1090
+ }
1091
+
1092
+ // 2. Remove CLAUDE.md for Claude
1093
+ if (ides.includes('claude')) {
1094
+ const claudePath = path.join(targetDir, 'CLAUDE.md');
1095
+
1096
+ if (fs.existsSync(claudePath)) {
1097
+ console.log(colors.yellow('► Checking CLAUDE.md...'));
1098
+
1099
+ // Check if it contains our signature content
1100
+ const content = fs.readFileSync(claudePath, 'utf8');
1101
+ const isOurs = content.includes('# CLAUDE.md - Development Guide') &&
1102
+ content.includes('.cursorrules/');
1103
+
1104
+ if (!isOurs && !force) {
1105
+ console.log(` ${colors.yellow('[modified]')} CLAUDE.md (doesn't match template, use --force)`);
1106
+ modifiedFiles.push('CLAUDE.md');
1107
+ stats.skipped++;
1108
+ } else {
1109
+ console.log(` ${colors.red('[remove]')} CLAUDE.md${!isOurs ? ' (modified, --force)' : ''}`);
1110
+ filesToRemove.push({ path: claudePath, name: 'CLAUDE.md' });
1111
+ }
1112
+ console.log();
1113
+ }
1114
+ }
1115
+
1116
+ // 3. Remove .github/copilot-instructions.md for Codex
1117
+ if (ides.includes('codex')) {
1118
+ const copilotPath = path.join(targetDir, '.github', 'copilot-instructions.md');
1119
+
1120
+ if (fs.existsSync(copilotPath)) {
1121
+ console.log(colors.yellow('► Checking .github/copilot-instructions.md...'));
1122
+
1123
+ // Check if it contains our signature content
1124
+ const content = fs.readFileSync(copilotPath, 'utf8');
1125
+ const isOurs = content.includes('# Copilot Instructions') &&
1126
+ content.includes('Installed Templates:');
1127
+
1128
+ if (!isOurs && !force) {
1129
+ console.log(` ${colors.yellow('[modified]')} .github/copilot-instructions.md (doesn't match template, use --force)`);
1130
+ modifiedFiles.push('.github/copilot-instructions.md');
1131
+ stats.skipped++;
1132
+ } else {
1133
+ console.log(` ${colors.red('[remove]')} .github/copilot-instructions.md${!isOurs ? ' (modified, --force)' : ''}`);
1134
+ filesToRemove.push({ path: copilotPath, name: '.github/copilot-instructions.md' });
1135
+ }
1136
+ console.log();
1137
+ }
1138
+ }
1139
+
1140
+ if (filesToRemove.length === 0 && dirsToRemove.length === 0) {
1141
+ console.log(colors.yellow('Nothing to remove.\n'));
1142
+ return;
1143
+ }
1144
+
1145
+ // Confirmation
1146
+ if (!dryRun && !skipConfirm) {
1147
+ const totalItems = filesToRemove.length + dirsToRemove.length;
1148
+ console.log(colors.yellow(`\nAbout to remove ${totalItems} item(s).`));
1149
+ const confirmed = await confirm(colors.red('Proceed with reset?'));
1150
+ if (!confirmed) {
1151
+ console.log(colors.dim('\nAborted.\n'));
1152
+ return;
1153
+ }
1154
+ console.log();
1155
+ }
1156
+
1157
+ // Execute removal
1158
+ if (dryRun) {
1159
+ console.log(colors.yellow('DRY RUN - No files were removed.\n'));
1160
+ } else {
1161
+ // Remove files first
1162
+ for (const file of filesToRemove) {
1163
+ try {
1164
+ fs.unlinkSync(file.path);
1165
+ stats.removed++;
1166
+ } catch (err) {
1167
+ console.error(colors.red(`Failed to remove ${file.name}: ${err.message}`));
1168
+ }
1169
+ }
1170
+
1171
+ // Then remove directories
1172
+ for (const dir of dirsToRemove) {
1173
+ try {
1174
+ // Check if directory is now empty
1175
+ const remaining = fs.existsSync(dir) ? fs.readdirSync(dir) : [];
1176
+ if (remaining.length === 0) {
1177
+ fs.rmdirSync(dir);
1178
+ stats.removed++;
1179
+ } else if (force) {
1180
+ fs.rmSync(dir, { recursive: true });
1181
+ stats.removed++;
1182
+ }
1183
+ } catch (err) {
1184
+ console.error(colors.red(`Failed to remove directory: ${err.message}`));
1185
+ }
1186
+ }
1187
+ }
1188
+
1189
+ // Summary
1190
+ console.log(colors.green('════════════════════════════════════════════════════════════'));
1191
+ console.log(colors.green(`✓ Reset complete!\n`));
1192
+
1193
+ console.log(colors.yellow('Summary:'));
1194
+ console.log(` - ${stats.removed} items removed`);
1195
+ if (stats.skipped > 0) {
1196
+ console.log(` - ${stats.skipped} files skipped (modified, use --force)`);
1197
+ }
650
1198
  console.log();
1199
+
1200
+ if (modifiedFiles.length > 0) {
1201
+ console.log(colors.yellow('Modified files preserved:'));
1202
+ for (const file of modifiedFiles) {
1203
+ console.log(` - ${file}`);
1204
+ }
1205
+ console.log(colors.dim('\nUse --force to remove modified files.\n'));
1206
+ }
651
1207
  }
652
1208
 
653
- export function run(args) {
1209
+ export async function run(args) {
654
1210
  const templates = [];
1211
+ const ides = [];
655
1212
  let dryRun = false;
656
1213
  let force = false;
1214
+ let skipConfirm = false;
1215
+ let removeMode = false;
1216
+ let resetMode = false;
657
1217
 
658
1218
  // Parse arguments
659
1219
  for (const arg of args) {
660
- switch (arg) {
661
- case '--list':
662
- case '-l':
663
- printBanner();
664
- printTemplates();
665
- process.exit(0);
666
- break;
667
- case '--help':
668
- case '-h':
669
- printBanner();
670
- printHelp();
671
- process.exit(0);
672
- break;
673
- case '--dry-run':
674
- dryRun = true;
675
- break;
676
- case '--force':
677
- case '-f':
678
- force = true;
679
- break;
680
- default:
681
- if (arg.startsWith('-')) {
682
- console.error(colors.red(`Error: Unknown option '${arg}'`));
683
- printHelp();
684
- process.exit(1);
685
- }
686
- templates.push(arg);
1220
+ if (arg === '--list' || arg === '-l') {
1221
+ printBanner();
1222
+ printTemplates();
1223
+ process.exit(0);
1224
+ } else if (arg === '--help' || arg === '-h') {
1225
+ printBanner();
1226
+ printHelp();
1227
+ process.exit(0);
1228
+ } else if (arg === '--dry-run') {
1229
+ dryRun = true;
1230
+ } else if (arg === '--force' || arg === '-f') {
1231
+ force = true;
1232
+ } else if (arg === '--yes' || arg === '-y') {
1233
+ skipConfirm = true;
1234
+ } else if (arg === '--remove') {
1235
+ removeMode = true;
1236
+ } else if (arg === '--reset') {
1237
+ resetMode = true;
1238
+ } else if (arg.startsWith('--ide=')) {
1239
+ const ide = arg.slice(6).toLowerCase();
1240
+ if (!SUPPORTED_IDES.includes(ide)) {
1241
+ console.error(colors.red(`Error: Unknown IDE '${ide}'`));
1242
+ console.error(colors.dim(`Supported: ${SUPPORTED_IDES.join(', ')}`));
1243
+ process.exit(1);
1244
+ }
1245
+ if (!ides.includes(ide)) {
1246
+ ides.push(ide);
1247
+ }
1248
+ } else if (arg.startsWith('-')) {
1249
+ console.error(colors.red(`Error: Unknown option '${arg}'`));
1250
+ printHelp();
1251
+ process.exit(1);
1252
+ } else {
1253
+ templates.push(arg);
687
1254
  }
688
1255
  }
689
1256
 
690
1257
  printBanner();
691
1258
 
692
- // Validate
1259
+ // Use default IDEs if none specified
1260
+ const targetIdes = ides.length > 0 ? ides : DEFAULT_IDES;
1261
+
1262
+ // Handle reset mode
1263
+ if (resetMode) {
1264
+ if (removeMode) {
1265
+ console.error(colors.red('Error: Cannot use --remove and --reset together\n'));
1266
+ process.exit(1);
1267
+ }
1268
+ if (templates.length > 0) {
1269
+ console.error(colors.red('Error: --reset does not accept template arguments\n'));
1270
+ console.error(colors.dim('Use --remove <templates...> to remove specific templates.\n'));
1271
+ process.exit(1);
1272
+ }
1273
+
1274
+ if (dryRun) {
1275
+ console.log(colors.yellow('DRY RUN - No changes will be made\n'));
1276
+ }
1277
+ if (force) {
1278
+ console.log(colors.yellow('FORCE MODE - Modified files will be removed\n'));
1279
+ }
1280
+
1281
+ await reset(process.cwd(), dryRun, force, skipConfirm, targetIdes);
1282
+ return;
1283
+ }
1284
+
1285
+ // Handle remove mode
1286
+ if (removeMode) {
1287
+ if (templates.length === 0) {
1288
+ console.error(colors.red('Error: No templates specified for removal\n'));
1289
+ console.error(colors.dim('Usage: npx cursor-templates --remove <templates...>\n'));
1290
+ printTemplates();
1291
+ process.exit(1);
1292
+ }
1293
+
1294
+ for (const template of templates) {
1295
+ if (!TEMPLATES[template]) {
1296
+ console.error(colors.red(`Error: Unknown template '${template}'\n`));
1297
+ printTemplates();
1298
+ process.exit(1);
1299
+ }
1300
+ }
1301
+
1302
+ if (dryRun) {
1303
+ console.log(colors.yellow('DRY RUN - No changes will be made\n'));
1304
+ }
1305
+ if (force) {
1306
+ console.log(colors.yellow('FORCE MODE - Modified files will be removed\n'));
1307
+ }
1308
+
1309
+ await remove(process.cwd(), templates, dryRun, force, skipConfirm, targetIdes);
1310
+ return;
1311
+ }
1312
+
1313
+ // Install mode (default)
693
1314
  if (templates.length === 0) {
694
1315
  console.error(colors.red('Error: No templates specified\n'));
695
1316
  printHelp();
@@ -713,5 +1334,26 @@ export function run(args) {
713
1334
  }
714
1335
 
715
1336
  // Install to current directory
716
- install(process.cwd(), templates, dryRun, force);
1337
+ install(process.cwd(), templates, dryRun, force, targetIdes);
717
1338
  }
1339
+
1340
+ // Export internals for testing
1341
+ export const _internals = {
1342
+ TEMPLATES,
1343
+ SHARED_RULES,
1344
+ SUPPORTED_IDES,
1345
+ DEFAULT_IDES,
1346
+ filesMatch,
1347
+ parseMarkdownSections,
1348
+ generateSectionSignature,
1349
+ findMissingSections,
1350
+ mergeClaudeContent,
1351
+ getAlternateFilename,
1352
+ copyFile,
1353
+ generateClaudeMdContent,
1354
+ generateCopilotInstructionsContent,
1355
+ isOurFile,
1356
+ install,
1357
+ remove,
1358
+ reset,
1359
+ };