@hatem427/code-guard-ci 2.2.2 → 2.2.5

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.
@@ -159,6 +159,22 @@ function initProject() {
159
159
  const isDryRun = hasFlag('dry-run');
160
160
  const skipAI = hasFlag('skip-ai');
161
161
  const skipHooks = hasFlag('skip-hooks');
162
+ // ── Manifest: track all files we might create/modify ─────────────────────
163
+ // Snapshot which files exist BEFORE init so uninstall knows what we created
164
+ const trackedPaths = [
165
+ // Config files
166
+ 'eslint.config.mjs', '.prettierrc.json', '.prettierignore', '.eslintignore',
167
+ 'tsconfig.strict.json', 'tsconfig.json', '.lintstagedrc.json',
168
+ '.editorconfig', 'commitlint.config.js', 'lint-staged.config.js',
169
+ // VS Code
170
+ '.vscode/settings.json', '.vscode/extensions.json',
171
+ '.vscode/tasks.json', '.vscode/launch.json', '.vscode/mcp.json',
172
+ // MCP
173
+ '.cursor/mcp.json', '.gemini/settings.json',
174
+ // Husky
175
+ '.husky/pre-commit',
176
+ ];
177
+ const preExisting = new Set(trackedPaths.filter(f => fs.existsSync(path.join(cwd, f))));
162
178
  // ── Step 1: Detect Framework ────────────────────────────────────────────
163
179
  console.log(c.bold('📡 Step 1: Detecting project...'));
164
180
  // Dynamic import (handles both dev and built)
@@ -352,6 +368,53 @@ function initProject() {
352
368
  console.log(c.bold('📁 Step 14: Copying Code Guardian rules and templates...'));
353
369
  copyCodeGuardianFiles(cwd);
354
370
  console.log('');
371
+ // ── Write manifest: record which files Code Guardian created ──────────────
372
+ try {
373
+ const manifestDir = path.join(cwd, '.code-guardian');
374
+ if (!fs.existsSync(manifestDir)) {
375
+ fs.mkdirSync(manifestDir, { recursive: true });
376
+ }
377
+ // Files newly created by Code Guardian (didn't exist before init)
378
+ const createdFiles = trackedPaths.filter(f => !preExisting.has(f) && fs.existsSync(path.join(cwd, f)));
379
+ // Files that existed before and were backed up / overwritten
380
+ const backedUpFiles = trackedPaths.filter(f => preExisting.has(f) && (fs.existsSync(path.join(cwd, f + '.backup')) ||
381
+ // ESLint: old config names may have been backed up
382
+ (f === 'eslint.config.mjs' && [
383
+ '.eslintrc.backup', '.eslintrc.js.backup', '.eslintrc.cjs.backup',
384
+ '.eslintrc.json.backup', '.eslintrc.yml.backup',
385
+ 'eslint.config.js.backup', 'eslint.config.mjs.backup', 'eslint.config.cjs.backup',
386
+ ].some(b => fs.existsSync(path.join(cwd, b))))));
387
+ // Husky: track if we created or appended
388
+ const huskyAction = !skipHooks && fs.existsSync(path.join(cwd, '.husky/pre-commit'))
389
+ ? (preExisting.has('.husky/pre-commit') ? 'appended' : 'created')
390
+ : 'skipped';
391
+ // AI configs: track via registry
392
+ const aiFiles = [];
393
+ try {
394
+ const { defaultRegistry: reg } = requireUtil('ai-config-registry');
395
+ for (const t of reg.getAll()) {
396
+ const rel = t.directory ? `${t.directory}/${t.fileName}` : t.fileName;
397
+ const full = path.join(cwd, rel);
398
+ if (fs.existsSync(full)) {
399
+ aiFiles.push(rel);
400
+ }
401
+ }
402
+ }
403
+ catch { }
404
+ const manifest = {
405
+ version: VERSION,
406
+ timestamp: new Date().toISOString(),
407
+ createdFiles,
408
+ backedUpFiles,
409
+ huskyAction,
410
+ aiFiles,
411
+ directories: ['config', 'templates', 'docs', '.code-guardian'],
412
+ };
413
+ fs.writeFileSync(path.join(manifestDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
414
+ }
415
+ catch {
416
+ // Non-critical — uninstall will fall back to heuristics
417
+ }
355
418
  // ── Done! ─────────────────────────────────────────────────────────────────
356
419
  console.log(c.bold(c.green('═══════════════════════════════════════════════════════════')));
357
420
  console.log(c.bold(c.green(' ✅ Code Guardian initialized successfully!')));
@@ -901,61 +964,75 @@ async function uninstallCodeGuard() {
901
964
  showBanner();
902
965
  console.log(c.bold('🗑️ Code Guardian Uninstall\n'));
903
966
  const cwd = process.cwd();
904
- // ── Files created entirely by Code Guardian (always delete) ──────────────
905
- const filesToRemove = [
967
+ let manifest = null;
968
+ const manifestPath = path.join(cwd, '.code-guardian', 'manifest.json');
969
+ if (fs.existsSync(manifestPath)) {
970
+ try {
971
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
972
+ console.log(c.dim('ℹ Using manifest to identify Code Guardian files.\n'));
973
+ }
974
+ catch {
975
+ manifest = null;
976
+ }
977
+ }
978
+ const manifestCreated = new Set(manifest?.createdFiles || []);
979
+ // ── Helper: was file created by Code Guardian? ───────────────────────────
980
+ // If manifest exists, check it. Otherwise assume all tracked files are ours.
981
+ function wasCreatedByUs(file) {
982
+ if (!manifest)
983
+ return true; // no manifest → legacy fallback, assume ours
984
+ return manifestCreated.has(file);
985
+ }
986
+ // ── Files created entirely by Code Guardian (always safe to delete) ──────
987
+ const allPossibleFiles = [
906
988
  '.editorconfig',
907
989
  '.lintstagedrc.json',
908
990
  'lint-staged.config.js',
909
991
  'tsconfig.strict.json',
992
+ 'tsconfig.json',
993
+ 'commitlint.config.js',
910
994
  '.prettierignore',
911
995
  ];
996
+ // Only include files that exist AND were created by us (or no manifest)
997
+ const filesToRemove = allPossibleFiles.filter(f => fs.existsSync(path.join(cwd, f)) && wasCreatedByUs(f));
912
998
  // ── Files that may have had a pre-existing version (backup/restore) ──────
913
- // During init these get renamed to <file>.backup before we overwrite.
914
- // On uninstall: if .backup exists → restore it, else → delete our file.
915
999
  const restorableConfigs = [
916
- // ESLint
917
1000
  { created: 'eslint.config.mjs', backups: [
918
1001
  'eslint.config.mjs.backup', 'eslint.config.js.backup', 'eslint.config.cjs.backup',
919
1002
  '.eslintrc.backup', '.eslintrc.js.backup', '.eslintrc.cjs.backup',
920
1003
  '.eslintrc.json.backup', '.eslintrc.yml.backup',
921
1004
  ] },
922
- // Prettier
923
1005
  { created: '.prettierrc.json', backups: [
924
1006
  '.prettierrc.json.backup', '.prettierrc.backup', '.prettierrc.js.backup',
925
1007
  '.prettierrc.cjs.backup', '.prettierrc.yml.backup', '.prettierrc.yaml.backup',
926
1008
  '.prettierrc.toml.backup', 'prettier.config.js.backup',
927
1009
  ] },
928
- // ESLint ignore (old format — we delete and backup during init)
929
1010
  { created: '.eslintignore', backups: ['.eslintignore.backup'] },
930
- ];
931
- // ── VS Code files created by Code Guardian ───────────────────────────────
932
- const vscodeFiles = [
933
- '.vscode/settings.json',
934
- '.vscode/extensions.json',
935
- '.vscode/tasks.json',
936
- '.vscode/launch.json',
937
- '.vscode/mcp.json',
938
- ];
939
- // ── MCP config files ─────────────────────────────────────────────────────
940
- const mcpFiles = [
941
- '.cursor/mcp.json',
942
- '.gemini/settings.json',
1011
+ // VS Code files (backed up + merged during init)
1012
+ { created: '.vscode/settings.json', backups: ['.vscode/settings.json.backup'] },
1013
+ { created: '.vscode/extensions.json', backups: ['.vscode/extensions.json.backup'] },
1014
+ { created: '.vscode/tasks.json', backups: ['.vscode/tasks.json.backup'] },
1015
+ { created: '.vscode/launch.json', backups: ['.vscode/launch.json.backup'] },
1016
+ { created: '.vscode/mcp.json', backups: ['.vscode/mcp.json.backup'] },
1017
+ // MCP config files (backed up + merged during init)
1018
+ { created: '.cursor/mcp.json', backups: ['.cursor/mcp.json.backup'] },
1019
+ { created: '.gemini/settings.json', backups: ['.gemini/settings.json.backup'] },
943
1020
  ];
944
1021
  // ── Directories to remove entirely ───────────────────────────────────────
945
- const dirsToRemove = [
946
- '.code-guardian',
947
- ];
948
- // ── package.json scripts to remove ───────────────────────────────────────
1022
+ const dirsToRemove = ['.code-guardian', 'config', 'templates', 'docs'];
1023
+ // ── package.json scripts ─────────────────────────────────────────────────
949
1024
  const scriptsToRemove = [
950
- 'precommit-check',
951
- 'auto-fix',
952
- 'generate-doc',
953
- 'generate-pr-checklist',
954
- 'set-bypass-password',
955
- 'set-admin-password',
956
- 'view-bypass-log',
957
- 'delete-bypass-logs',
1025
+ 'precommit-check', 'auto-fix', 'generate-doc', 'generate-pr-checklist',
1026
+ 'set-bypass-password', 'set-admin-password', 'view-bypass-log', 'delete-bypass-logs',
958
1027
  ];
1028
+ const conditionalScriptsToRemove = {
1029
+ 'validate': (v) => v === 'code-guard validate',
1030
+ 'lint': (v) => v.startsWith('eslint'),
1031
+ 'lint:fix': (v) => v.startsWith('eslint') && v.includes('--fix'),
1032
+ 'format': (v) => v.startsWith('prettier --write'),
1033
+ 'format:check': (v) => v.startsWith('prettier --check'),
1034
+ 'prepare': (v) => v === 'husky' || v === 'husky install',
1035
+ };
959
1036
  // ── Detect AI config files via registry ──────────────────────────────────
960
1037
  const { defaultRegistry } = requireUtil('ai-config-registry');
961
1038
  const aiTemplates = defaultRegistry.getAll();
@@ -980,31 +1057,59 @@ async function uninstallCodeGuard() {
980
1057
  aiActions.push({ name: t.name, filePath, relativePath, marker: t.marker, action: 'delete' });
981
1058
  }
982
1059
  }
983
- // ── Gather what exists ───────────────────────────────────────────────────
984
- const existingFiles = filesToRemove.filter(f => fs.existsSync(path.join(cwd, f)));
985
- const existingVSCodeFiles = vscodeFiles.filter(f => fs.existsSync(path.join(cwd, f)));
986
- const existingMCPFiles = mcpFiles.filter(f => fs.existsSync(path.join(cwd, f)));
987
- const existingDirs = dirsToRemove.filter(d => fs.existsSync(path.join(cwd, d)));
1060
+ // ── Detect husky pre-commit status ───────────────────────────────────────
1061
+ const HOOK_START = '# --- code-guardian-hook-start ---';
1062
+ const HOOK_END = '# --- code-guardian-hook-end ---';
1063
+ const huskyDir = path.join(cwd, '.husky');
1064
+ const preCommitPath = path.join(huskyDir, 'pre-commit');
1065
+ let huskyAction = 'none';
1066
+ if (fs.existsSync(preCommitPath)) {
1067
+ const huskyContent = fs.readFileSync(preCommitPath, 'utf-8');
1068
+ if (huskyContent.includes(HOOK_START) || huskyContent.includes('code-guard')) {
1069
+ // Check if it was created by us or we appended to existing
1070
+ if (manifest) {
1071
+ huskyAction = manifest.huskyAction === 'created' ? 'delete' : 'strip';
1072
+ }
1073
+ else {
1074
+ // No manifest — check if only our content exists
1075
+ if (huskyContent.includes(HOOK_START)) {
1076
+ const startIdx = huskyContent.indexOf(HOOK_START);
1077
+ const endIdx = huskyContent.indexOf(HOOK_END);
1078
+ if (endIdx !== -1) {
1079
+ const before = huskyContent.substring(0, startIdx).replace(/^#!\/usr\/bin\/env\s+sh\s*/, '').trim();
1080
+ const after = huskyContent.substring(endIdx + HOOK_END.length).trim();
1081
+ huskyAction = (before.length === 0 && after.length === 0) ? 'delete' : 'strip';
1082
+ }
1083
+ }
1084
+ else {
1085
+ huskyAction = 'delete'; // no markers but has code-guard references
1086
+ }
1087
+ }
1088
+ }
1089
+ }
1090
+ // ── Gather restorables ───────────────────────────────────────────────────
988
1091
  const restorables = [];
989
1092
  for (const rc of restorableConfigs) {
990
1093
  const createdPath = path.join(cwd, rc.created);
991
1094
  if (!fs.existsSync(createdPath))
992
1095
  continue;
993
- // Find the first backup that exists
994
1096
  const foundBackup = rc.backups.find(b => fs.existsSync(path.join(cwd, b))) || null;
995
1097
  restorables.push({ created: rc.created, backupFile: foundBackup });
996
1098
  }
997
- const totalFound = existingFiles.length + existingVSCodeFiles.length + existingMCPFiles.length
998
- + existingDirs.length + aiActions.length + restorables.length;
1099
+ // ── Check if anything to do ──────────────────────────────────────────────
1100
+ const existingDirs = dirsToRemove.filter(d => fs.existsSync(path.join(cwd, d)));
1101
+ const totalFound = filesToRemove.length
1102
+ + existingDirs.length + aiActions.length + restorables.length
1103
+ + (huskyAction !== 'none' ? 1 : 0);
999
1104
  if (totalFound === 0) {
1000
1105
  console.log(c.yellow('⚠️ No Code Guardian files found to remove.\n'));
1001
1106
  return;
1002
1107
  }
1003
1108
  // ── Show what will be removed ────────────────────────────────────────────
1004
1109
  console.log(c.bold('The following will be cleaned up:\n'));
1005
- if (existingFiles.length > 0) {
1110
+ if (filesToRemove.length > 0) {
1006
1111
  console.log(c.bold('Config Files (created by Code Guardian — will be deleted):'));
1007
- existingFiles.forEach(f => console.log(` ${c.red('✗')} ${f}`));
1112
+ filesToRemove.forEach(f => console.log(` ${c.red('✗')} ${f}`));
1008
1113
  console.log('');
1009
1114
  }
1010
1115
  if (restorables.length > 0) {
@@ -1019,14 +1124,14 @@ async function uninstallCodeGuard() {
1019
1124
  }
1020
1125
  console.log('');
1021
1126
  }
1022
- if (existingVSCodeFiles.length > 0) {
1023
- console.log(c.bold('VS Code Files (created by Code Guardian):'));
1024
- existingVSCodeFiles.forEach(f => console.log(` ${c.red('')} ${f}`));
1025
- console.log('');
1026
- }
1027
- if (existingMCPFiles.length > 0) {
1028
- console.log(c.bold('MCP Config Files:'));
1029
- existingMCPFiles.forEach(f => console.log(` ${c.red('✗')} ${f}`));
1127
+ if (huskyAction !== 'none') {
1128
+ console.log(c.bold('Git Hooks:'));
1129
+ if (huskyAction === 'delete') {
1130
+ console.log(` ${c.red('')} .husky/pre-commit ${c.dim('(created by Code Guardian)')}`);
1131
+ }
1132
+ else {
1133
+ console.log(` ${c.yellow('⚠')} .husky/pre-commit ${c.dim('(will strip Code Guardian hooks, keep your hooks)')}`);
1134
+ }
1030
1135
  console.log('');
1031
1136
  }
1032
1137
  const aiDeletes = aiActions.filter(a => a.action === 'delete');
@@ -1062,23 +1167,17 @@ async function uninstallCodeGuard() {
1062
1167
  try {
1063
1168
  const createdPath = path.join(cwd, r.created);
1064
1169
  if (r.backupFile) {
1065
- // Restore: copy backup over the Code Guardian file, then delete backup
1066
1170
  const backupPath = path.join(cwd, r.backupFile);
1067
- // The backup might have a different name (e.g., .eslintrc.json.backup → restore as .eslintrc.json)
1068
1171
  const restoredName = r.backupFile.replace(/\.backup$/, '');
1069
1172
  const restoredPath = path.join(cwd, restoredName);
1070
- // Remove the Code Guardian file first
1071
- if (fs.existsSync(createdPath)) {
1173
+ if (fs.existsSync(createdPath))
1072
1174
  fs.unlinkSync(createdPath);
1073
- }
1074
- // Restore the backup
1075
1175
  fs.copyFileSync(backupPath, restoredPath);
1076
1176
  fs.unlinkSync(backupPath);
1077
1177
  console.log(` ${c.green('✓')} Restored ${restoredName} ${c.dim(`(from ${r.backupFile})`)}`);
1078
1178
  removedCount++;
1079
1179
  }
1080
1180
  else {
1081
- // No backup → just delete our file
1082
1181
  fs.unlinkSync(createdPath);
1083
1182
  console.log(` ${c.green('✓')} Removed ${r.created}`);
1084
1183
  removedCount++;
@@ -1088,59 +1187,26 @@ async function uninstallCodeGuard() {
1088
1187
  console.log(` ${c.red('✗')} Failed to handle ${r.created}: ${error.message}`);
1089
1188
  }
1090
1189
  }
1091
- // Also clean up any leftover .backup files for configs that might not have
1092
- // been caught above (e.g., user already manually restored but .backup remains)
1190
+ // Clean up orphan .backup files
1093
1191
  const allBackupPatterns = restorableConfigs.flatMap(rc => rc.backups);
1094
1192
  for (const bp of allBackupPatterns) {
1095
1193
  const bpPath = path.join(cwd, bp);
1096
1194
  if (fs.existsSync(bpPath)) {
1097
1195
  try {
1098
- // Check if the original (non-backup) already exists and is NOT a Code Guardian file
1099
- // If so, just delete the orphan backup
1100
1196
  fs.unlinkSync(bpPath);
1101
1197
  console.log(` ${c.green('✓')} Cleaned up orphan backup ${bp}`);
1102
1198
  }
1103
1199
  catch { }
1104
1200
  }
1105
1201
  }
1202
+ // Clean up empty IDE directories after restore/delete
1203
+ cleanupEmptyDir(path.join(cwd, '.vscode'), cwd);
1204
+ cleanupEmptyDir(path.join(cwd, '.cursor'), cwd);
1205
+ cleanupEmptyDir(path.join(cwd, '.gemini'), cwd);
1106
1206
  // ── 2. Remove simple config files ────────────────────────────────────────
1107
- console.log(c.bold('\nRemoving Code Guardian files...\n'));
1108
- for (const file of filesToRemove) {
1109
- const fullPath = path.join(cwd, file);
1110
- if (fs.existsSync(fullPath)) {
1111
- try {
1112
- fs.unlinkSync(fullPath);
1113
- console.log(` ${c.green('✓')} Removed ${file}`);
1114
- removedCount++;
1115
- }
1116
- catch (error) {
1117
- console.log(` ${c.red('✗')} Failed to remove ${file}: ${error.message}`);
1118
- }
1119
- }
1120
- }
1121
- // ── 3. Remove VS Code files ──────────────────────────────────────────────
1122
- if (existingVSCodeFiles.length > 0) {
1123
- console.log(c.bold('\nRemoving VS Code files...\n'));
1124
- for (const file of existingVSCodeFiles) {
1125
- const fullPath = path.join(cwd, file);
1126
- if (fs.existsSync(fullPath)) {
1127
- try {
1128
- fs.unlinkSync(fullPath);
1129
- console.log(` ${c.green('✓')} Removed ${file}`);
1130
- removedCount++;
1131
- }
1132
- catch (error) {
1133
- console.log(` ${c.red('✗')} Failed to remove ${file}: ${error.message}`);
1134
- }
1135
- }
1136
- }
1137
- // Remove .vscode/ if empty
1138
- cleanupEmptyDir(path.join(cwd, '.vscode'), cwd);
1139
- }
1140
- // ── 4. Remove MCP config files ───────────────────────────────────────────
1141
- if (existingMCPFiles.length > 0) {
1142
- console.log(c.bold('\nRemoving MCP config files...\n'));
1143
- for (const file of existingMCPFiles) {
1207
+ if (filesToRemove.length > 0) {
1208
+ console.log(c.bold('\nRemoving Code Guardian files...\n'));
1209
+ for (const file of filesToRemove) {
1144
1210
  const fullPath = path.join(cwd, file);
1145
1211
  if (fs.existsSync(fullPath)) {
1146
1212
  try {
@@ -1153,11 +1219,8 @@ async function uninstallCodeGuard() {
1153
1219
  }
1154
1220
  }
1155
1221
  }
1156
- // Clean up empty .cursor/ and .gemini/ directories
1157
- cleanupEmptyDir(path.join(cwd, '.cursor'), cwd);
1158
- cleanupEmptyDir(path.join(cwd, '.gemini'), cwd);
1159
1222
  }
1160
- // ── 5. Handle AI config files ────────────────────────────────────────────
1223
+ // ── 3. Handle AI config files ────────────────────────────────────────────
1161
1224
  if (aiActions.length > 0) {
1162
1225
  console.log(c.bold('\nCleaning AI config files...\n'));
1163
1226
  for (const action of aiActions) {
@@ -1166,12 +1229,9 @@ async function uninstallCodeGuard() {
1166
1229
  fs.unlinkSync(action.filePath);
1167
1230
  console.log(` ${c.green('✓')} Removed ${action.relativePath}`);
1168
1231
  removedCount++;
1169
- // Clean up empty parent directories
1170
- const parentDir = path.dirname(action.filePath);
1171
- cleanupEmptyDir(parentDir, cwd);
1232
+ cleanupEmptyDir(path.dirname(action.filePath), cwd);
1172
1233
  }
1173
1234
  else {
1174
- // Strip only Code Guardian section
1175
1235
  const content = fs.readFileSync(action.filePath, 'utf-8');
1176
1236
  const markerIdx = content.indexOf(action.marker);
1177
1237
  if (markerIdx === -1)
@@ -1193,7 +1253,7 @@ async function uninstallCodeGuard() {
1193
1253
  }
1194
1254
  }
1195
1255
  }
1196
- // ── 6. Remove directories ────────────────────────────────────────────────
1256
+ // ── 4. Remove directories ────────────────────────────────────────────────
1197
1257
  if (existingDirs.length > 0) {
1198
1258
  console.log(c.bold('\nRemoving directories...\n'));
1199
1259
  for (const dir of dirsToRemove) {
@@ -1210,54 +1270,41 @@ async function uninstallCodeGuard() {
1210
1270
  }
1211
1271
  }
1212
1272
  }
1213
- // ── 7. Clean git hooks ───────────────────────────────────────────────────
1214
- console.log(c.bold('\nCleaning git hooks...\n'));
1215
- const huskyDir = path.join(cwd, '.husky');
1216
- const huskyHookFiles = ['pre-commit'];
1217
- const HOOK_START = '# --- code-guardian-hook-start ---';
1218
- const HOOK_END = '# --- code-guardian-hook-end ---';
1219
- for (const hookFile of huskyHookFiles) {
1220
- const hookPath = path.join(huskyDir, hookFile);
1221
- if (!fs.existsSync(hookPath))
1222
- continue;
1223
- const content = fs.readFileSync(hookPath, 'utf-8');
1224
- if (content.includes(HOOK_START)) {
1225
- const startIdx = content.indexOf(HOOK_START);
1226
- const endIdx = content.indexOf(HOOK_END);
1227
- if (endIdx === -1)
1228
- continue;
1229
- const beforeHook = content.substring(0, startIdx).trimEnd();
1230
- const afterHook = content.substring(endIdx + HOOK_END.length).trimEnd();
1231
- const remaining = (beforeHook.replace(/^#!\/usr\/bin\/env\s+sh\s*/, '').trim() + afterHook.trim()).trim();
1232
- if (remaining.length === 0) {
1233
- fs.unlinkSync(hookPath);
1234
- console.log(` ${c.green('✓')} Removed .husky/${hookFile}`);
1235
- removedCount++;
1236
- }
1237
- else {
1238
- const cleaned = (beforeHook + afterHook).trimEnd() + '\n';
1239
- fs.writeFileSync(hookPath, cleaned);
1240
- try {
1241
- fs.chmodSync(hookPath, '755');
1273
+ // ── 5. Clean git hooks ───────────────────────────────────────────────────
1274
+ if (huskyAction !== 'none') {
1275
+ console.log(c.bold('\nCleaning git hooks...\n'));
1276
+ if (fs.existsSync(preCommitPath)) {
1277
+ try {
1278
+ if (huskyAction === 'delete') {
1279
+ fs.unlinkSync(preCommitPath);
1280
+ console.log(` ${c.green('✓')} Removed .husky/pre-commit`);
1281
+ removedCount++;
1282
+ }
1283
+ else {
1284
+ const content = fs.readFileSync(preCommitPath, 'utf-8');
1285
+ const startIdx = content.indexOf(HOOK_START);
1286
+ const endIdx = content.indexOf(HOOK_END);
1287
+ if (startIdx !== -1 && endIdx !== -1) {
1288
+ const before = content.substring(0, startIdx).trimEnd();
1289
+ const after = content.substring(endIdx + HOOK_END.length).trimEnd();
1290
+ const cleaned = (before + after).trimEnd() + '\n';
1291
+ fs.writeFileSync(preCommitPath, cleaned);
1292
+ try {
1293
+ fs.chmodSync(preCommitPath, '755');
1294
+ }
1295
+ catch { }
1296
+ console.log(` ${c.green('✓')} Stripped Code Guardian hooks from .husky/pre-commit ${c.dim('(your hooks preserved)')}`);
1297
+ removedCount++;
1298
+ }
1242
1299
  }
1243
- catch { }
1244
- console.log(` ${c.green('✓')} Stripped Code Guardian hooks from .husky/${hookFile} ${c.dim('(your hooks preserved)')}`);
1245
- removedCount++;
1246
1300
  }
1247
- }
1248
- else {
1249
- // No markers — check if the hook was entirely written by Code Guardian
1250
- // (contains "code-guard check" or "code-guard" references)
1251
- if (content.includes('code-guard') || content.includes('code_guard')) {
1252
- fs.unlinkSync(hookPath);
1253
- console.log(` ${c.green('✓')} Removed .husky/${hookFile}`);
1254
- removedCount++;
1301
+ catch (error) {
1302
+ console.log(` ${c.red('✗')} Failed to clean .husky/pre-commit: ${error.message}`);
1255
1303
  }
1256
1304
  }
1305
+ cleanupEmptyDir(huskyDir, cwd);
1257
1306
  }
1258
- // Clean up empty .husky directory
1259
- cleanupEmptyDir(huskyDir, cwd);
1260
- // ── 8. Clean package.json scripts ────────────────────────────────────────
1307
+ // ── 6. Clean package.json scripts ────────────────────────────────────────
1261
1308
  console.log(c.bold('\nCleaning package.json scripts...\n'));
1262
1309
  const packageJsonPath = path.join(cwd, 'package.json');
1263
1310
  if (fs.existsSync(packageJsonPath)) {
@@ -1272,12 +1319,13 @@ async function uninstallCodeGuard() {
1272
1319
  removedCount++;
1273
1320
  }
1274
1321
  }
1275
- // Also remove "prepare": "husky" if we added it
1276
- if (packageJson.scripts?.prepare === 'husky' || packageJson.scripts?.prepare === 'husky install') {
1277
- delete packageJson.scripts.prepare;
1278
- console.log(` ${c.green('✓')} Removed script: prepare (husky)`);
1279
- scriptRemoved = true;
1280
- removedCount++;
1322
+ for (const [script, matcher] of Object.entries(conditionalScriptsToRemove)) {
1323
+ if (packageJson.scripts && packageJson.scripts[script] && matcher(packageJson.scripts[script])) {
1324
+ delete packageJson.scripts[script];
1325
+ console.log(` ${c.green('✓')} Removed script: ${script}`);
1326
+ scriptRemoved = true;
1327
+ removedCount++;
1328
+ }
1281
1329
  }
1282
1330
  if (scriptRemoved) {
1283
1331
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');