@hatem427/code-guard-ci 2.2.1 → 2.2.2

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.
@@ -900,20 +900,52 @@ function promptYesNo(question) {
900
900
  async function uninstallCodeGuard() {
901
901
  showBanner();
902
902
  console.log(c.bold('🗑️ Code Guardian Uninstall\n'));
903
+ const cwd = process.cwd();
904
+ // ── Files created entirely by Code Guardian (always delete) ──────────────
903
905
  const filesToRemove = [
904
- '.husky/_/husky.sh',
905
- 'eslint.config.js',
906
- 'prettier.config.js',
907
906
  '.editorconfig',
907
+ '.lintstagedrc.json',
908
908
  'lint-staged.config.js',
909
909
  'tsconfig.strict.json',
910
+ '.prettierignore',
911
+ ];
912
+ // ── 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
+ const restorableConfigs = [
916
+ // ESLint
917
+ { created: 'eslint.config.mjs', backups: [
918
+ 'eslint.config.mjs.backup', 'eslint.config.js.backup', 'eslint.config.cjs.backup',
919
+ '.eslintrc.backup', '.eslintrc.js.backup', '.eslintrc.cjs.backup',
920
+ '.eslintrc.json.backup', '.eslintrc.yml.backup',
921
+ ] },
922
+ // Prettier
923
+ { created: '.prettierrc.json', backups: [
924
+ '.prettierrc.json.backup', '.prettierrc.backup', '.prettierrc.js.backup',
925
+ '.prettierrc.cjs.backup', '.prettierrc.yml.backup', '.prettierrc.yaml.backup',
926
+ '.prettierrc.toml.backup', 'prettier.config.js.backup',
927
+ ] },
928
+ // ESLint ignore (old format — we delete and backup during init)
929
+ { 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',
910
943
  ];
944
+ // ── Directories to remove entirely ───────────────────────────────────────
911
945
  const dirsToRemove = [
912
- 'config',
913
- 'templates',
914
- 'docs',
915
946
  '.code-guardian',
916
947
  ];
948
+ // ── package.json scripts to remove ───────────────────────────────────────
917
949
  const scriptsToRemove = [
918
950
  'precommit-check',
919
951
  'auto-fix',
@@ -924,10 +956,9 @@ async function uninstallCodeGuard() {
924
956
  'view-bypass-log',
925
957
  'delete-bypass-logs',
926
958
  ];
927
- // ── Detect AI config files via registry ──────────────────────────────
959
+ // ── Detect AI config files via registry ──────────────────────────────────
928
960
  const { defaultRegistry } = requireUtil('ai-config-registry');
929
961
  const aiTemplates = defaultRegistry.getAll();
930
- const cwd = process.cwd();
931
962
  const aiActions = [];
932
963
  for (const t of aiTemplates) {
933
964
  const dir = t.directory ? path.join(cwd, t.directory) : cwd;
@@ -937,43 +968,65 @@ async function uninstallCodeGuard() {
937
968
  continue;
938
969
  const content = fs.readFileSync(filePath, 'utf-8');
939
970
  if (!content.includes(t.marker))
940
- continue; // not ours
941
- // Determine if the file was created by us or if we appended
942
- // If appended, there will be a --- separator before our marker with user content above
971
+ continue;
943
972
  const markerIdx = content.indexOf(t.marker);
944
973
  const beforeMarker = content.substring(0, markerIdx).trimEnd();
945
- // Check if there's substantial user content before our section
946
- // When we append we add "\n\n---\n\n" before our content
947
- // When we create, the marker is at or near the top (possibly after frontmatter)
948
- const stripped = beforeMarker.replace(/^---[\s\S]*?---/, '').trim(); // strip MDC frontmatter
949
- const hasUserContent = stripped.length > 0 && !stripped.endsWith('---');
950
- // A more robust check: if content before marker (minus separator) has real text
951
- const withoutTrailingSep = beforeMarker.replace(/\n*---\s*$/, '').trim();
952
- const realUserContent = withoutTrailingSep.replace(/^---[\s\S]*?---/, '').trim(); // strip frontmatter
953
- if (realUserContent.length > 0) {
974
+ const stripped = beforeMarker.replace(/^---[\s\S]*?---/, '').trim();
975
+ const withoutTrailingSep = stripped.replace(/\n*---\s*$/, '').trim();
976
+ if (withoutTrailingSep.length > 0) {
954
977
  aiActions.push({ name: t.name, filePath, relativePath, marker: t.marker, action: 'strip' });
955
978
  }
956
979
  else {
957
980
  aiActions.push({ name: t.name, filePath, relativePath, marker: t.marker, action: 'delete' });
958
981
  }
959
982
  }
960
- // Check what exists
983
+ // ── Gather what exists ───────────────────────────────────────────────────
961
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)));
962
987
  const existingDirs = dirsToRemove.filter(d => fs.existsSync(path.join(cwd, d)));
963
- if (existingFiles.length === 0 && existingDirs.length === 0 && aiActions.length === 0) {
988
+ const restorables = [];
989
+ for (const rc of restorableConfigs) {
990
+ const createdPath = path.join(cwd, rc.created);
991
+ if (!fs.existsSync(createdPath))
992
+ continue;
993
+ // Find the first backup that exists
994
+ const foundBackup = rc.backups.find(b => fs.existsSync(path.join(cwd, b))) || null;
995
+ restorables.push({ created: rc.created, backupFile: foundBackup });
996
+ }
997
+ const totalFound = existingFiles.length + existingVSCodeFiles.length + existingMCPFiles.length
998
+ + existingDirs.length + aiActions.length + restorables.length;
999
+ if (totalFound === 0) {
964
1000
  console.log(c.yellow('⚠️ No Code Guardian files found to remove.\n'));
965
1001
  return;
966
1002
  }
967
- // Show what will be removed
968
- console.log(c.bold('The following will be removed:\n'));
1003
+ // ── Show what will be removed ────────────────────────────────────────────
1004
+ console.log(c.bold('The following will be cleaned up:\n'));
969
1005
  if (existingFiles.length > 0) {
970
- console.log(c.bold('Files:'));
1006
+ console.log(c.bold('Config Files (created by Code Guardian — will be deleted):'));
971
1007
  existingFiles.forEach(f => console.log(` ${c.red('✗')} ${f}`));
972
1008
  console.log('');
973
1009
  }
974
- if (existingDirs.length > 0) {
975
- console.log(c.bold('Directories:'));
976
- existingDirs.forEach(d => console.log(` ${c.red('✗')} ${d}/`));
1010
+ if (restorables.length > 0) {
1011
+ console.log(c.bold('Config Files (backup/restore):'));
1012
+ for (const r of restorables) {
1013
+ if (r.backupFile) {
1014
+ console.log(` ${c.yellow('↩')} ${r.created} ${c.dim(`→ restore from ${r.backupFile}`)}`);
1015
+ }
1016
+ else {
1017
+ console.log(` ${c.red('✗')} ${r.created} ${c.dim('(no backup found — will delete)')}`);
1018
+ }
1019
+ }
1020
+ console.log('');
1021
+ }
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}`));
977
1030
  console.log('');
978
1031
  }
979
1032
  const aiDeletes = aiActions.filter(a => a.action === 'delete');
@@ -988,51 +1041,70 @@ async function uninstallCodeGuard() {
988
1041
  aiStrips.forEach(a => console.log(` ${c.yellow('⚠')} ${a.relativePath} ${c.dim('(keeping your original content)')}`));
989
1042
  console.log('');
990
1043
  }
1044
+ if (existingDirs.length > 0) {
1045
+ console.log(c.bold('Directories:'));
1046
+ existingDirs.forEach(d => console.log(` ${c.red('✗')} ${d}/`));
1047
+ console.log('');
1048
+ }
1049
+ // ── Confirm ──────────────────────────────────────────────────────────────
991
1050
  if (!hasFlag('yes') && !hasFlag('y')) {
992
- const readline = require('readline');
993
- const rl = readline.createInterface({
994
- input: process.stdin,
995
- output: process.stdout
996
- });
997
- const answer = await new Promise((resolve) => {
998
- rl.question(c.yellow('⚠️ Continue with removal? (y/N): '), (ans) => {
999
- rl.close();
1000
- resolve(ans.trim().toLowerCase());
1001
- });
1002
- });
1003
- if (answer !== 'y' && answer !== 'yes') {
1051
+ const confirmed = await promptYesNo(c.yellow('⚠️ Continue with removal? (y/N): '));
1052
+ if (!confirmed) {
1004
1053
  console.log(c.dim('\nCancelled.\n'));
1005
1054
  return;
1006
1055
  }
1007
1056
  }
1008
1057
  let removedCount = 0;
1009
1058
  console.log('');
1010
- // Backup AI config files before modifying
1011
- const backupDir = path.join(cwd, '.code-guardian-backup-' + Date.now());
1012
- let hasBackup = false;
1013
- if (aiActions.length > 0 && !hasFlag('no-backup')) {
1014
- console.log(c.bold('Creating backup...\n'));
1015
- fs.mkdirSync(backupDir, { recursive: true });
1016
- for (const action of aiActions) {
1017
- if (fs.existsSync(action.filePath)) {
1018
- try {
1019
- const destPath = path.join(backupDir, action.relativePath);
1020
- const destDir = path.dirname(destPath);
1021
- if (!fs.existsSync(destDir))
1022
- fs.mkdirSync(destDir, { recursive: true });
1023
- fs.cpSync(action.filePath, destPath);
1024
- console.log(` ${c.blue('📦')} Backed up ${action.relativePath}`);
1025
- hasBackup = true;
1026
- }
1027
- catch (error) {
1028
- console.log(` ${c.yellow('⚠')} Failed to backup ${action.relativePath}: ${error.message}`);
1059
+ // ── 1. Restore or delete restorable configs ──────────────────────────────
1060
+ console.log(c.bold('Restoring / removing config files...\n'));
1061
+ for (const r of restorables) {
1062
+ try {
1063
+ const createdPath = path.join(cwd, r.created);
1064
+ if (r.backupFile) {
1065
+ // Restore: copy backup over the Code Guardian file, then delete backup
1066
+ 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
+ const restoredName = r.backupFile.replace(/\.backup$/, '');
1069
+ const restoredPath = path.join(cwd, restoredName);
1070
+ // Remove the Code Guardian file first
1071
+ if (fs.existsSync(createdPath)) {
1072
+ fs.unlinkSync(createdPath);
1029
1073
  }
1074
+ // Restore the backup
1075
+ fs.copyFileSync(backupPath, restoredPath);
1076
+ fs.unlinkSync(backupPath);
1077
+ console.log(` ${c.green('✓')} Restored ${restoredName} ${c.dim(`(from ${r.backupFile})`)}`);
1078
+ removedCount++;
1079
+ }
1080
+ else {
1081
+ // No backup → just delete our file
1082
+ fs.unlinkSync(createdPath);
1083
+ console.log(` ${c.green('✓')} Removed ${r.created}`);
1084
+ removedCount++;
1030
1085
  }
1031
1086
  }
1032
- console.log('');
1087
+ catch (error) {
1088
+ console.log(` ${c.red('✗')} Failed to handle ${r.created}: ${error.message}`);
1089
+ }
1033
1090
  }
1034
- // Remove config files created entirely by Code Guardian
1035
- console.log(c.bold('Removing files...\n'));
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)
1093
+ const allBackupPatterns = restorableConfigs.flatMap(rc => rc.backups);
1094
+ for (const bp of allBackupPatterns) {
1095
+ const bpPath = path.join(cwd, bp);
1096
+ if (fs.existsSync(bpPath)) {
1097
+ 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
+ fs.unlinkSync(bpPath);
1101
+ console.log(` ${c.green('✓')} Cleaned up orphan backup ${bp}`);
1102
+ }
1103
+ catch { }
1104
+ }
1105
+ }
1106
+ // ── 2. Remove simple config files ────────────────────────────────────────
1107
+ console.log(c.bold('\nRemoving Code Guardian files...\n'));
1036
1108
  for (const file of filesToRemove) {
1037
1109
  const fullPath = path.join(cwd, file);
1038
1110
  if (fs.existsSync(fullPath)) {
@@ -1046,76 +1118,99 @@ async function uninstallCodeGuard() {
1046
1118
  }
1047
1119
  }
1048
1120
  }
1049
- // Remove directories
1050
- console.log(c.bold('\nRemoving directories...\n'));
1051
- for (const dir of dirsToRemove) {
1052
- const fullPath = path.join(cwd, dir);
1053
- if (fs.existsSync(fullPath)) {
1054
- try {
1055
- fs.rmSync(fullPath, { recursive: true, force: true });
1056
- console.log(` ${c.green('✓')} Removed ${dir}/`);
1057
- removedCount++;
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
+ }
1058
1135
  }
1059
- catch (error) {
1060
- console.log(` ${c.red('✗')} Failed to remove ${dir}: ${error.message}`);
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) {
1144
+ const fullPath = path.join(cwd, file);
1145
+ if (fs.existsSync(fullPath)) {
1146
+ try {
1147
+ fs.unlinkSync(fullPath);
1148
+ console.log(` ${c.green('✓')} Removed ${file}`);
1149
+ removedCount++;
1150
+ }
1151
+ catch (error) {
1152
+ console.log(` ${c.red('✗')} Failed to remove ${file}: ${error.message}`);
1153
+ }
1061
1154
  }
1062
1155
  }
1156
+ // Clean up empty .cursor/ and .gemini/ directories
1157
+ cleanupEmptyDir(path.join(cwd, '.cursor'), cwd);
1158
+ cleanupEmptyDir(path.join(cwd, '.gemini'), cwd);
1063
1159
  }
1064
- // Handle AI config files intelligently
1065
- console.log(c.bold('\nCleaning AI config files...\n'));
1066
- for (const action of aiActions) {
1067
- try {
1068
- if (action.action === 'delete') {
1069
- // Entire file was created by Code Guardian → delete it
1070
- fs.unlinkSync(action.filePath);
1071
- console.log(` ${c.green('✓')} Removed ${action.relativePath}`);
1072
- removedCount++;
1073
- // Remove parent directory if it's now empty
1074
- const parentDir = path.dirname(action.filePath);
1075
- if (parentDir !== cwd) {
1076
- try {
1077
- const remaining = fs.readdirSync(parentDir);
1078
- if (remaining.length === 0) {
1079
- fs.rmdirSync(parentDir);
1080
- // Check grandparent too (e.g., .windsurf/rules → .windsurf)
1081
- const grandparent = path.dirname(parentDir);
1082
- if (grandparent !== cwd && fs.existsSync(grandparent)) {
1083
- const gpRemaining = fs.readdirSync(grandparent);
1084
- if (gpRemaining.length === 0)
1085
- fs.rmdirSync(grandparent);
1086
- }
1087
- }
1160
+ // ── 5. Handle AI config files ────────────────────────────────────────────
1161
+ if (aiActions.length > 0) {
1162
+ console.log(c.bold('\nCleaning AI config files...\n'));
1163
+ for (const action of aiActions) {
1164
+ try {
1165
+ if (action.action === 'delete') {
1166
+ fs.unlinkSync(action.filePath);
1167
+ console.log(` ${c.green('✓')} Removed ${action.relativePath}`);
1168
+ removedCount++;
1169
+ // Clean up empty parent directories
1170
+ const parentDir = path.dirname(action.filePath);
1171
+ cleanupEmptyDir(parentDir, cwd);
1172
+ }
1173
+ else {
1174
+ // Strip only Code Guardian section
1175
+ const content = fs.readFileSync(action.filePath, 'utf-8');
1176
+ const markerIdx = content.indexOf(action.marker);
1177
+ if (markerIdx === -1)
1178
+ continue;
1179
+ let cutStart = markerIdx;
1180
+ const beforeMarker = content.substring(0, markerIdx);
1181
+ const sepMatch = beforeMarker.match(/\n*\s*---\s*\n*$/);
1182
+ if (sepMatch && sepMatch.index !== undefined) {
1183
+ cutStart = sepMatch.index;
1088
1184
  }
1089
- catch { }
1185
+ const cleaned = content.substring(0, cutStart).trimEnd() + '\n';
1186
+ fs.writeFileSync(action.filePath, cleaned);
1187
+ console.log(` ${c.green('✓')} Stripped Code Guardian rules from ${action.relativePath} ${c.dim('(your content preserved)')}`);
1188
+ removedCount++;
1090
1189
  }
1091
1190
  }
1092
- else {
1093
- // File had user content → strip only Code Guardian section
1094
- const content = fs.readFileSync(action.filePath, 'utf-8');
1095
- const markerIdx = content.indexOf(action.marker);
1096
- if (markerIdx === -1)
1097
- continue;
1098
- // Find the separator (--- on its own line) before the marker
1099
- // When we append, we add "\n\n---\n\n" before our content
1100
- let cutStart = markerIdx;
1101
- const beforeMarker = content.substring(0, markerIdx);
1102
- // Look for the --- separator we added
1103
- const sepMatch = beforeMarker.match(/\n*\s*---\s*\n*$/);
1104
- if (sepMatch && sepMatch.index !== undefined) {
1105
- cutStart = sepMatch.index;
1106
- }
1107
- // Remove from cutStart to end of file (our content is always appended at the end)
1108
- const cleaned = content.substring(0, cutStart).trimEnd() + '\n';
1109
- fs.writeFileSync(action.filePath, cleaned);
1110
- console.log(` ${c.green('✓')} Stripped Code Guardian rules from ${action.relativePath} ${c.dim('(your content preserved)')}`);
1111
- removedCount++;
1191
+ catch (error) {
1192
+ console.log(` ${c.red('✗')} Failed to clean ${action.relativePath}: ${error.message}`);
1112
1193
  }
1113
1194
  }
1114
- catch (error) {
1115
- console.log(` ${c.red('✗')} Failed to clean ${action.relativePath}: ${error.message}`);
1195
+ }
1196
+ // ── 6. Remove directories ────────────────────────────────────────────────
1197
+ if (existingDirs.length > 0) {
1198
+ console.log(c.bold('\nRemoving directories...\n'));
1199
+ for (const dir of dirsToRemove) {
1200
+ const fullPath = path.join(cwd, dir);
1201
+ if (fs.existsSync(fullPath)) {
1202
+ try {
1203
+ fs.rmSync(fullPath, { recursive: true, force: true });
1204
+ console.log(` ${c.green('✓')} Removed ${dir}/`);
1205
+ removedCount++;
1206
+ }
1207
+ catch (error) {
1208
+ console.log(` ${c.red('✗')} Failed to remove ${dir}: ${error.message}`);
1209
+ }
1210
+ }
1116
1211
  }
1117
1212
  }
1118
- // Clean up .husky hooks smartly
1213
+ // ── 7. Clean git hooks ───────────────────────────────────────────────────
1119
1214
  console.log(c.bold('\nCleaning git hooks...\n'));
1120
1215
  const huskyDir = path.join(cwd, '.husky');
1121
1216
  const huskyHookFiles = ['pre-commit'];
@@ -1126,49 +1221,43 @@ async function uninstallCodeGuard() {
1126
1221
  if (!fs.existsSync(hookPath))
1127
1222
  continue;
1128
1223
  const content = fs.readFileSync(hookPath, 'utf-8');
1129
- if (!content.includes(HOOK_START))
1130
- continue;
1131
- const startIdx = content.indexOf(HOOK_START);
1132
- const endIdx = content.indexOf(HOOK_END);
1133
- if (endIdx === -1)
1134
- continue;
1135
- const beforeHook = content.substring(0, startIdx).trimEnd();
1136
- const afterHook = content.substring(endIdx + HOOK_END.length).trimEnd();
1137
- // Check if there's real user content outside of our markers
1138
- const remaining = (beforeHook.replace(/^#!\/usr\/bin\/env\s+sh\s*/, '').trim() + afterHook.trim()).trim();
1139
- if (remaining.length === 0) {
1140
- // Entire file is ours → delete it
1141
- fs.unlinkSync(hookPath);
1142
- console.log(` ${c.green('✓')} Removed .husky/${hookFile}`);
1143
- removedCount++;
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');
1242
+ }
1243
+ catch { }
1244
+ console.log(` ${c.green('✓')} Stripped Code Guardian hooks from .husky/${hookFile} ${c.dim('(your hooks preserved)')}`);
1245
+ removedCount++;
1246
+ }
1144
1247
  }
1145
1248
  else {
1146
- // User had their own content strip only our section
1147
- const cleaned = (beforeHook + afterHook).trimEnd() + '\n';
1148
- fs.writeFileSync(hookPath, cleaned);
1149
- try {
1150
- fs.chmodSync(hookPath, '755');
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++;
1151
1255
  }
1152
- catch { }
1153
- console.log(` ${c.green('✓')} Stripped Code Guardian hooks from .husky/${hookFile} ${c.dim('(your hooks preserved)')}`);
1154
- removedCount++;
1155
1256
  }
1156
1257
  }
1157
1258
  // Clean up empty .husky directory
1158
- if (fs.existsSync(huskyDir)) {
1159
- try {
1160
- const huskyContents = fs.readdirSync(huskyDir);
1161
- if (huskyContents.length === 0) {
1162
- fs.rmdirSync(huskyDir);
1163
- console.log(` ${c.green('✓')} Removed empty .husky/ directory`);
1164
- }
1165
- else {
1166
- console.log(` ${c.dim('○')} .husky/ has other files — keeping directory`);
1167
- }
1168
- }
1169
- catch { }
1170
- }
1171
- // Remove scripts from package.json
1259
+ cleanupEmptyDir(huskyDir, cwd);
1260
+ // ── 8. Clean package.json scripts ────────────────────────────────────────
1172
1261
  console.log(c.bold('\nCleaning package.json scripts...\n'));
1173
1262
  const packageJsonPath = path.join(cwd, 'package.json');
1174
1263
  if (fs.existsSync(packageJsonPath)) {
@@ -1183,6 +1272,13 @@ async function uninstallCodeGuard() {
1183
1272
  removedCount++;
1184
1273
  }
1185
1274
  }
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++;
1281
+ }
1186
1282
  if (scriptRemoved) {
1187
1283
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
1188
1284
  console.log(` ${c.green('✓')} Updated package.json`);
@@ -1195,16 +1291,31 @@ async function uninstallCodeGuard() {
1195
1291
  console.log(` ${c.red('✗')} Failed to clean package.json: ${error.message}`);
1196
1292
  }
1197
1293
  }
1294
+ // ── Done ─────────────────────────────────────────────────────────────────
1198
1295
  console.log('');
1199
1296
  console.log(c.green(`✅ Cleanup complete! Removed ${removedCount} item(s).`));
1200
- if (hasBackup) {
1201
- console.log(c.blue(`📦 Backup saved to: ${path.basename(backupDir)}/`));
1202
- }
1203
1297
  console.log('');
1204
1298
  console.log(c.dim('To completely remove the package, run:'));
1205
1299
  console.log(c.dim(' npm uninstall @hatem427/code-guard-ci'));
1206
1300
  console.log('');
1207
1301
  }
1302
+ /**
1303
+ * Recursively remove empty directories up to (but not including) the project root.
1304
+ */
1305
+ function cleanupEmptyDir(dirPath, projectRoot) {
1306
+ if (!fs.existsSync(dirPath) || dirPath === projectRoot)
1307
+ return;
1308
+ try {
1309
+ const contents = fs.readdirSync(dirPath);
1310
+ if (contents.length === 0) {
1311
+ fs.rmdirSync(dirPath);
1312
+ // Check parent too (e.g., .cursor/rules/ → .cursor/)
1313
+ const parent = path.dirname(dirPath);
1314
+ cleanupEmptyDir(parent, projectRoot);
1315
+ }
1316
+ }
1317
+ catch { }
1318
+ }
1208
1319
  // ── Main ────────────────────────────────────────────────────────────────────
1209
1320
  (async () => {
1210
1321
  switch (command) {