agentic-team-templates 0.5.0 → 0.6.1

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 CHANGED
@@ -93,6 +93,42 @@ npx cursor-templates web-frontend --ide=codex
93
93
  npx cursor-templates web-frontend --ide=cursor --ide=codex
94
94
  ```
95
95
 
96
+ ### Remove Specific Templates
97
+
98
+ Remove templates you no longer need while keeping shared rules and other templates:
99
+
100
+ ```bash
101
+ # Remove a single template
102
+ npx cursor-templates --remove web-frontend
103
+
104
+ # Remove multiple templates
105
+ npx cursor-templates --remove web-frontend web-backend
106
+
107
+ # Remove from specific IDE only
108
+ npx cursor-templates --remove web-frontend --ide=cursor
109
+
110
+ # Skip confirmation prompt
111
+ npx cursor-templates --remove web-frontend --yes
112
+ ```
113
+
114
+ ### Reset (Remove Everything)
115
+
116
+ Remove all installed content (shared rules, templates, generated files):
117
+
118
+ ```bash
119
+ # Reset all installed content
120
+ npx cursor-templates --reset
121
+
122
+ # Reset for specific IDE only
123
+ npx cursor-templates --reset --ide=cursor
124
+
125
+ # Skip confirmation prompt
126
+ npx cursor-templates --reset --yes
127
+
128
+ # Force remove modified files
129
+ npx cursor-templates --reset --force
130
+ ```
131
+
96
132
  ### CLI Options
97
133
 
98
134
  | Option | Description |
@@ -100,7 +136,10 @@ npx cursor-templates web-frontend --ide=cursor --ide=codex
100
136
  | `--ide=<name>` | Target IDE: `cursor`, `claude`, or `codex` (can be used multiple times) |
101
137
  | `--list`, `-l` | List all available templates |
102
138
  | `--dry-run` | Preview changes without writing files |
103
- | `--force`, `-f` | Overwrite existing files |
139
+ | `--force`, `-f` | Overwrite/remove even if files were modified |
140
+ | `--remove` | Remove specified templates |
141
+ | `--reset` | Remove ALL installed content |
142
+ | `--yes`, `-y` | Skip confirmation prompt (for `--remove` and `--reset`) |
104
143
  | `--help`, `-h` | Show help message |
105
144
 
106
145
  ## Available Templates
package/bin/cli.js CHANGED
@@ -2,4 +2,7 @@
2
2
 
3
3
  import { run } from '../src/index.js';
4
4
 
5
- run(process.argv.slice(2));
5
+ run(process.argv.slice(2)).catch((err) => {
6
+ console.error(err);
7
+ process.exit(1);
8
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-team-templates",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "AI coding assistant templates for Cursor IDE. Pre-configured rules and guidelines that help AI assistants write better code. - use at your own risk",
5
5
  "keywords": [
6
6
  "cursor",
@@ -38,10 +38,15 @@
38
38
  "node": ">=18.0.0"
39
39
  },
40
40
  "scripts": {
41
- "test": "echo \"No tests required - template library\""
41
+ "test": "vitest run",
42
+ "test:watch": "vitest",
43
+ "test:coverage": "vitest run --coverage",
44
+ "prepare": "husky"
42
45
  },
43
46
  "devDependencies": {
44
47
  "@commitlint/cli": "^19.0.0",
45
- "@commitlint/config-conventional": "^19.0.0"
48
+ "@commitlint/config-conventional": "^19.0.0",
49
+ "husky": "^9.1.7",
50
+ "vitest": "^4.0.18"
46
51
  }
47
52
  }
package/src/index.js CHANGED
@@ -74,6 +74,8 @@ function printBanner() {
74
74
  function printHelp() {
75
75
  console.log(`${colors.yellow('Usage:')}
76
76
  npx cursor-templates <templates...> [options]
77
+ npx cursor-templates --remove <templates...> [options]
78
+ npx cursor-templates --reset [options]
77
79
 
78
80
  ${colors.yellow('Options:')}
79
81
  --ide=<name> Install for specific IDE (cursor, claude, codex)
@@ -81,8 +83,13 @@ ${colors.yellow('Options:')}
81
83
  Default: all (cursor, claude, codex)
82
84
  --list, -l List available templates
83
85
  --help, -h Show this help message
84
- --dry-run Show what would be installed
85
- --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)
86
93
 
87
94
  ${colors.yellow('IDE Targets:')}
88
95
  cursor .cursorrules/ directory (Cursor IDE)
@@ -96,6 +103,14 @@ ${colors.yellow('Examples:')}
96
103
  npx cursor-templates fullstack --ide=codex
97
104
  npx cursor-templates web-backend --force
98
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
+
99
114
  ${colors.dim('Shared rules (code-quality, security, git-workflow, etc.) are always included.')}
100
115
  ${colors.dim('Identical files are skipped. Modified files are preserved; ours saved as *-1.md.')}
101
116
  ${colors.dim('CLAUDE.md: missing sections are intelligently merged (not overwritten).')}
@@ -823,11 +838,382 @@ function install(targetDir, templates, dryRun = false, force = false, ides = DEF
823
838
  console.log();
824
839
  }
825
840
 
826
- export function run(args) {
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
+ }
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
+ }
1207
+ }
1208
+
1209
+ export async function run(args) {
827
1210
  const templates = [];
828
1211
  const ides = [];
829
1212
  let dryRun = false;
830
1213
  let force = false;
1214
+ let skipConfirm = false;
1215
+ let removeMode = false;
1216
+ let resetMode = false;
831
1217
 
832
1218
  // Parse arguments
833
1219
  for (const arg of args) {
@@ -843,6 +1229,12 @@ export function run(args) {
843
1229
  dryRun = true;
844
1230
  } else if (arg === '--force' || arg === '-f') {
845
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;
846
1238
  } else if (arg.startsWith('--ide=')) {
847
1239
  const ide = arg.slice(6).toLowerCase();
848
1240
  if (!SUPPORTED_IDES.includes(ide)) {
@@ -864,7 +1256,61 @@ export function run(args) {
864
1256
 
865
1257
  printBanner();
866
1258
 
867
- // 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)
868
1314
  if (templates.length === 0) {
869
1315
  console.error(colors.red('Error: No templates specified\n'));
870
1316
  printHelp();
@@ -879,9 +1325,6 @@ export function run(args) {
879
1325
  }
880
1326
  }
881
1327
 
882
- // Use default IDEs if none specified
883
- const targetIdes = ides.length > 0 ? ides : DEFAULT_IDES;
884
-
885
1328
  if (dryRun) {
886
1329
  console.log(colors.yellow('DRY RUN - No changes will be made\n'));
887
1330
  }
@@ -893,3 +1336,24 @@ export function run(args) {
893
1336
  // Install to current directory
894
1337
  install(process.cwd(), templates, dryRun, force, targetIdes);
895
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
+ };