@hatem427/code-guard-ci 3.5.4 → 3.5.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.
@@ -116,7 +116,6 @@ ${c.bold('Init Options:')}
116
116
 
117
117
  ${c.bold('Uninstall Options:')}
118
118
  --yes, -y Skip confirmation prompt
119
- --no-backup Don't create backup of AI configs
120
119
 
121
120
  ${c.bold('Examples:')}
122
121
  ${c.dim('# Full automatic setup')}
@@ -134,11 +133,11 @@ ${c.bold('Examples:')}
134
133
  ${c.dim('# Generate docs')}
135
134
  code-guard doc --name="user-card" --type=ui
136
135
 
137
- ${c.dim('# Clean uninstall with backup')}
136
+ ${c.dim('# Clean uninstall')}
138
137
  code-guard uninstall
139
138
 
140
139
  ${c.dim('# Force uninstall without prompts')}
141
- code-guard uninstall --yes --no-backup
140
+ code-guard uninstall --yes
142
141
  `);
143
142
  }
144
143
  function showVersion() {
@@ -159,6 +158,10 @@ function initProject() {
159
158
  const isDryRun = hasFlag('dry-run');
160
159
  const skipAI = hasFlag('skip-ai');
161
160
  const skipHooks = hasFlag('skip-hooks');
161
+ // ── Initialize manifest for tracking ────────────────────────────────────
162
+ const { createManifest, saveManifest, loadManifest, recordCreatedFile, recordModifiedFile, recordBackup, recordAddedScript, recordModifiedScript, } = requireUtil('manifest');
163
+ // Load existing manifest or create new one
164
+ const manifest = loadManifest(cwd) || createManifest();
162
165
  // ── Step 1: Detect Framework ────────────────────────────────────────────
163
166
  console.log(c.bold('📡 Step 1: Detecting project...'));
164
167
  // Dynamic import (handles both dev and built)
@@ -195,9 +198,23 @@ function initProject() {
195
198
  // ── Step 3: Generate ESLint Config ───────────────────────────────────────
196
199
  console.log(c.bold('⚡ Step 3: Configuring ESLint...'));
197
200
  try {
201
+ const eslintFile = 'eslint.config.mjs';
202
+ const eslintPath = path.join(cwd, eslintFile);
203
+ const eslintExisted = fs.existsSync(eslintPath);
198
204
  const { generateEslintConfig } = requireGenerator('eslint-generator');
199
205
  generateEslintConfig(project);
200
- console.log(` ${c.green('✓')} Created .eslintrc.json`);
206
+ // Track: eslint-generator already creates .backup files for existing configs
207
+ if (eslintExisted) {
208
+ recordModifiedFile(manifest, eslintFile, 'eslint');
209
+ // The generator already backed up the old file — record it
210
+ if (fs.existsSync(eslintPath + '.backup')) {
211
+ recordBackup(manifest, eslintFile, eslintFile + '.backup');
212
+ }
213
+ }
214
+ else {
215
+ recordCreatedFile(manifest, eslintFile);
216
+ }
217
+ console.log(` ${c.green('✓')} Created eslint.config.mjs`);
201
218
  console.log(` ${c.green('✓')} Created .eslintignore\n`);
202
219
  }
203
220
  catch (error) {
@@ -206,8 +223,31 @@ function initProject() {
206
223
  // ── Step 4: Generate Prettier Config ──────────────────────────────────────
207
224
  console.log(c.bold('🎨 Step 4: Configuring Prettier...'));
208
225
  try {
226
+ const prettierFile = '.prettierrc.json';
227
+ const prettierPath = path.join(cwd, prettierFile);
228
+ const prettierExisted = fs.existsSync(prettierPath);
229
+ const ignoreFile = '.prettierignore';
230
+ const ignorePath = path.join(cwd, ignoreFile);
231
+ const ignoreExisted = fs.existsSync(ignorePath);
209
232
  const { generatePrettierConfig } = requireGenerator('prettier-generator');
210
233
  generatePrettierConfig(project);
234
+ // Track: prettier-generator already creates .backup files for existing configs
235
+ if (prettierExisted) {
236
+ recordModifiedFile(manifest, prettierFile, 'prettier');
237
+ // Check for any backed-up prettier files
238
+ const prettierFiles = ['.prettierrc', '.prettierrc.js', '.prettierrc.cjs', '.prettierrc.json', '.prettierrc.yml', '.prettierrc.yaml', '.prettierrc.toml', 'prettier.config.js'];
239
+ for (const pf of prettierFiles) {
240
+ if (fs.existsSync(path.join(cwd, pf + '.backup'))) {
241
+ recordBackup(manifest, pf, pf + '.backup');
242
+ }
243
+ }
244
+ }
245
+ else {
246
+ recordCreatedFile(manifest, prettierFile);
247
+ }
248
+ if (!ignoreExisted && fs.existsSync(ignorePath)) {
249
+ recordCreatedFile(manifest, ignoreFile);
250
+ }
211
251
  console.log(` ${c.green('✓')} Created .prettierrc.json`);
212
252
  console.log(` ${c.green('✓')} Created .prettierignore\n`);
213
253
  }
@@ -218,8 +258,17 @@ function initProject() {
218
258
  if (project.usesTypeScript) {
219
259
  console.log(c.bold('📘 Step 5: Configuring TypeScript...'));
220
260
  try {
261
+ const tsFile = project.existingTooling.hasTypescript ? 'tsconfig.strict.json' : 'tsconfig.json';
262
+ const tsPath = path.join(cwd, tsFile);
263
+ const tsExisted = fs.existsSync(tsPath);
221
264
  const { generateTypescriptConfig } = requireGenerator('typescript-generator');
222
265
  generateTypescriptConfig(project);
266
+ if (tsExisted) {
267
+ recordModifiedFile(manifest, tsFile, 'typescript');
268
+ }
269
+ else {
270
+ recordCreatedFile(manifest, tsFile);
271
+ }
223
272
  if (project.existingTooling.hasTypescript) {
224
273
  console.log(` ${c.green('✓')} Created tsconfig.strict.json (extends existing tsconfig)`);
225
274
  }
@@ -238,18 +287,30 @@ function initProject() {
238
287
  // ── Step 6: Generate lint-staged Config ───────────────────────────────────
239
288
  console.log(c.bold('📋 Step 6: Configuring lint-staged...'));
240
289
  try {
290
+ const lsFile = '.lintstagedrc.json';
291
+ const lsPath = path.join(cwd, lsFile);
292
+ const lsExisted = fs.existsSync(lsPath);
241
293
  const { generateLintStagedConfig } = requireGenerator('lint-staged-generator');
242
294
  generateLintStagedConfig(project);
295
+ if (!lsExisted && fs.existsSync(lsPath)) {
296
+ recordCreatedFile(manifest, lsFile);
297
+ }
243
298
  console.log(` ${c.green('✓')} Created .lintstagedrc.json\n`);
244
299
  }
245
300
  catch (error) {
246
301
  console.warn(c.yellow(` ⚠️ lint-staged config generation failed: ${error.message}\n`));
247
302
  }
248
303
  // ── Step 7: Generate .editorconfig ────────────────────────────────────────
249
- console.log(c.bold('📐 Step 8: Creating .editorconfig...'));
304
+ console.log(c.bold('📐 Step 7: Creating .editorconfig...'));
250
305
  try {
306
+ const ecFile = '.editorconfig';
307
+ const ecPath = path.join(cwd, ecFile);
308
+ const ecExisted = fs.existsSync(ecPath);
251
309
  const { generateEditorConfig } = requireGenerator('editorconfig-generator');
252
310
  generateEditorConfig(project);
311
+ if (!ecExisted && fs.existsSync(ecPath)) {
312
+ recordCreatedFile(manifest, ecFile);
313
+ }
253
314
  console.log(` ${c.green('✓')} Created .editorconfig\n`);
254
315
  }
255
316
  catch (error) {
@@ -258,11 +319,21 @@ function initProject() {
258
319
  // ── Step 8: Generate VS Code Settings ─────────────────────────────────────
259
320
  console.log(c.bold('💻 Step 8: Creating VS Code workspace settings...'));
260
321
  try {
322
+ const vscodeFiles = ['.vscode/settings.json', '.vscode/extensions.json', '.vscode/tasks.json', '.vscode/launch.json'];
323
+ const vscodeExisted = {};
324
+ for (const vf of vscodeFiles) {
325
+ vscodeExisted[vf] = fs.existsSync(path.join(cwd, vf));
326
+ }
261
327
  const { generateVSCodeSettings, generateVSCodeExtensions, generateVSCodeTasks, generateVSCodeLaunch } = requireGenerator('vscode-generator');
262
328
  generateVSCodeSettings(project);
263
329
  generateVSCodeExtensions(project);
264
330
  generateVSCodeTasks(project);
265
331
  generateVSCodeLaunch(project);
332
+ for (const vf of vscodeFiles) {
333
+ if (!vscodeExisted[vf] && fs.existsSync(path.join(cwd, vf))) {
334
+ recordCreatedFile(manifest, vf);
335
+ }
336
+ }
266
337
  console.log(` ${c.green('✓')} Created .vscode/settings.json`);
267
338
  console.log(` ${c.green('✓')} Created .vscode/extensions.json`);
268
339
  console.log(` ${c.green('✓')} Created .vscode/tasks.json`);
@@ -274,7 +345,15 @@ function initProject() {
274
345
  // ── Step 9: Setup Git Hooks ───────────────────────────────────────────────
275
346
  if (!skipHooks) {
276
347
  console.log(c.bold('🐶 Step 9: Setting up Git hooks...'));
348
+ const hookFile = '.husky/pre-commit';
349
+ const hookExisted = fs.existsSync(path.join(cwd, hookFile));
277
350
  setupGitHooks(cwd);
351
+ if (hookExisted) {
352
+ recordModifiedFile(manifest, hookFile, 'husky');
353
+ }
354
+ else if (fs.existsSync(path.join(cwd, hookFile))) {
355
+ recordCreatedFile(manifest, hookFile);
356
+ }
278
357
  console.log('');
279
358
  }
280
359
  else {
@@ -286,16 +365,30 @@ function initProject() {
286
365
  try {
287
366
  const { loadCompanyRules } = requireUtil('custom-rules-loader');
288
367
  const companyRules = loadCompanyRules(cwd);
289
- const { generateAIConfigs } = requireGenerator('ai-config-generator');
290
- generateAIConfigs(project, companyRules.aiGuidelines);
368
+ // Snapshot which AI files exist before generation
291
369
  const { defaultRegistry } = requireUtil('ai-config-registry');
292
370
  const templates = defaultRegistry.getAll();
371
+ const aiExisted = {};
372
+ for (const t of templates) {
373
+ const targetDir = t.directory ? path.join(cwd, t.directory) : cwd;
374
+ const targetPath = path.join(targetDir, t.fileName);
375
+ const relPath = t.directory ? `${t.directory}/${t.fileName}` : t.fileName;
376
+ aiExisted[relPath] = fs.existsSync(targetPath);
377
+ }
378
+ const { generateAIConfigs } = requireGenerator('ai-config-generator');
379
+ generateAIConfigs(project, companyRules.aiGuidelines);
293
380
  for (const t of templates) {
294
381
  const targetDir = t.directory ? path.join(cwd, t.directory) : cwd;
295
382
  const targetPath = path.join(targetDir, t.fileName);
383
+ const relPath = t.directory ? `${t.directory}/${t.fileName}` : t.fileName;
296
384
  if (fs.existsSync(targetPath)) {
297
- const displayPath = t.directory ? `${t.directory}/${t.fileName}` : t.fileName;
298
- console.log(` ${c.green('✓')} ${t.name}: ${displayPath}`);
385
+ if (aiExisted[relPath]) {
386
+ recordModifiedFile(manifest, relPath, t.marker);
387
+ }
388
+ else {
389
+ recordCreatedFile(manifest, relPath);
390
+ }
391
+ console.log(` ${c.green('✓')} ${t.name}: ${relPath}`);
299
392
  }
300
393
  }
301
394
  console.log('');
@@ -310,6 +403,11 @@ function initProject() {
310
403
  // ── Step 11: Custom Rules Template ─────────────────────────────────────────
311
404
  console.log(c.bold('🏢 Step 11: Creating custom rules templates...'));
312
405
  try {
406
+ const cgFiles = ['.code-guardian/custom-rules.json', '.code-guardian/structure-rules.json', '.code-guardian/naming-rules.json'];
407
+ const cgExisted = {};
408
+ for (const cf of cgFiles) {
409
+ cgExisted[cf] = fs.existsSync(path.join(cwd, cf));
410
+ }
313
411
  const { generateCustomRulesTemplate } = requireUtil('custom-rules-loader');
314
412
  generateCustomRulesTemplate(cwd);
315
413
  console.log(` ${c.green('✓')} Created .code-guardian/custom-rules.json`);
@@ -319,18 +417,26 @@ function initProject() {
319
417
  const { generateNamingRulesTemplate } = requireUtil('naming-validator');
320
418
  generateNamingRulesTemplate(cwd);
321
419
  console.log(` ${c.green('✓')} Created .code-guardian/naming-rules.json\n`);
420
+ for (const cf of cgFiles) {
421
+ if (!cgExisted[cf] && fs.existsSync(path.join(cwd, cf))) {
422
+ recordCreatedFile(manifest, cf);
423
+ }
424
+ }
322
425
  }
323
426
  catch (error) {
324
427
  console.warn(c.yellow(` ⚠️ Custom rules template generation failed: ${error.message}\n`));
325
428
  }
326
429
  // ── Step 12: Update package.json ───────────────────────────────────────────
327
430
  console.log(c.bold('📦 Step 12: Updating package.json...'));
328
- updatePackageJson(cwd, project);
431
+ updatePackageJson(cwd, project, manifest);
329
432
  console.log('');
330
433
  // ── Step 13: Copy Code Guardian configs and templates ──────────────────────
331
434
  console.log(c.bold('📁 Step 13: Copying Code Guardian rules and templates...'));
332
- copyCodeGuardianFiles(cwd);
435
+ copyCodeGuardianFiles(cwd, manifest, project.type);
333
436
  console.log('');
437
+ // ── Save manifest ─────────────────────────────────────────────────────────
438
+ saveManifest(cwd, manifest);
439
+ console.log(` ${c.green('✓')} Saved manifest to .code-guardian/manifest.json\n`);
334
440
  // ── Done! ─────────────────────────────────────────────────────────────────
335
441
  console.log(c.bold(c.green('═══════════════════════════════════════════════════════════')));
336
442
  console.log(c.bold(c.green(' ✅ Code Guardian initialized successfully!')));
@@ -364,42 +470,45 @@ function initProject() {
364
470
  // ── Setup Git Hooks ─────────────────────────────────────────────────────────
365
471
  function setupGitHooks(cwd) {
366
472
  try {
367
- // Initialize Husky
473
+ const huskyDir = path.join(cwd, '.husky');
474
+ const preCommitPath = path.join(huskyDir, 'pre-commit');
475
+ // Save existing pre-commit content BEFORE husky init (which overwrites it)
476
+ let existingPreCommit = null;
477
+ if (fs.existsSync(preCommitPath)) {
478
+ existingPreCommit = fs.readFileSync(preCommitPath, 'utf-8');
479
+ }
480
+ // Initialize Husky (creates .husky/ dir and default pre-commit)
368
481
  try {
369
482
  (0, child_process_1.execSync)('npx husky init', { stdio: 'pipe', cwd });
370
483
  }
371
484
  catch {
372
485
  // Fallback: manual setup
373
- const huskyDir = path.join(cwd, '.husky');
374
486
  if (!fs.existsSync(huskyDir)) {
375
487
  fs.mkdirSync(huskyDir, { recursive: true });
376
488
  }
377
489
  }
378
- const huskyDir = path.join(cwd, '.husky');
379
490
  if (!fs.existsSync(huskyDir)) {
380
491
  fs.mkdirSync(huskyDir, { recursive: true });
381
492
  }
382
- // Create or append pre-commit hook (lint-staged + Code Guardian)
493
+ // Restore the original pre-commit content if husky init overwrote it
494
+ if (existingPreCommit !== null) {
495
+ fs.writeFileSync(preCommitPath, existingPreCommit);
496
+ }
497
+ // Create or append pre-commit hook (lint-staged only — Code Guardian checks run via `code-guard check`)
383
498
  const preCommitContent = `
384
- # npm test
385
-
386
499
  # --- code-guardian-hook-start ---
387
- echo "🛡️ Code Guardian — Pre-commit checks..."
500
+ echo "🛡️ Code Guardian — Pre-commit lint & format..."
388
501
 
389
- # Skip lint-staged and all checks when BYPASS_RULES is set
502
+ # Skip lint-staged when BYPASS_RULES is set
390
503
  if [ "$BYPASS_RULES" = "true" ] || [ "$BYPASS_RULES" = "1" ]; then
391
- echo "⚡ BYPASS_RULES detected — skipping lint-staged and Code Guardian."
504
+ echo "⚡ BYPASS_RULES detected — skipping lint-staged."
392
505
  exit 0
393
506
  fi
394
507
 
395
508
  # Run lint-staged (ESLint + Prettier auto-fix)
396
509
  npx lint-staged
397
-
398
- # Run Code Guardian custom rules
399
- npm run precommit-check
400
510
  # --- code-guardian-hook-end ---
401
511
  `;
402
- const preCommitPath = path.join(huskyDir, 'pre-commit');
403
512
  if (fs.existsSync(preCommitPath)) {
404
513
  const existing = fs.readFileSync(preCommitPath, 'utf-8');
405
514
  if (!existing.includes('code-guardian-hook-start')) {
@@ -415,7 +524,7 @@ npm run precommit-check
415
524
  // Create new hook file
416
525
  const preCommitHook = `#!/usr/bin/env sh\n${preCommitContent}`;
417
526
  fs.writeFileSync(preCommitPath, preCommitHook);
418
- console.log(` ${c.green('✓')} Created .husky/pre-commit (lint-staged + Code Guardian)`);
527
+ console.log(` ${c.green('✓')} Created .husky/pre-commit (lint-staged only)`);
419
528
  }
420
529
  try {
421
530
  fs.chmodSync(preCommitPath, '755');
@@ -468,43 +577,74 @@ function buildInstallCommand(packageManager, deps) {
468
577
  }
469
578
  }
470
579
  // ── Update package.json ─────────────────────────────────────────────────────
471
- function updatePackageJson(cwd, project) {
580
+ function updatePackageJson(cwd, project, manifest) {
472
581
  const packageJsonPath = path.join(cwd, 'package.json');
473
582
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
474
583
  const packageName = '@hatem427/code-guard-ci';
475
584
  packageJson.scripts = packageJson.scripts || {};
585
+ // Helper to track script changes in manifest
586
+ const setScript = (name, value) => {
587
+ if (manifest) {
588
+ if (packageJson.scripts[name] && packageJson.scripts[name] !== value) {
589
+ // Script existed with different value — record original
590
+ const { recordModifiedScript } = requireUtil('manifest');
591
+ recordModifiedScript(manifest, name, packageJson.scripts[name]);
592
+ }
593
+ else if (!packageJson.scripts[name]) {
594
+ // New script
595
+ const { recordAddedScript } = requireUtil('manifest');
596
+ recordAddedScript(manifest, name);
597
+ }
598
+ }
599
+ packageJson.scripts[name] = value;
600
+ };
476
601
  // Core Code Guardian scripts
477
- packageJson.scripts['precommit-check'] = 'code-guard check';
478
- packageJson.scripts['generate-doc'] = 'code-guard doc';
479
- packageJson.scripts['generate-pr-checklist'] = 'code-guard checklist';
480
- packageJson.scripts['validate'] = 'code-guard validate';
481
- packageJson.scripts['prepare'] = 'husky';
602
+ setScript('precommit-check', 'code-guard check');
603
+ setScript('generate-doc', 'code-guard doc');
604
+ setScript('generate-pr-checklist', 'code-guard checklist');
605
+ setScript('validate', 'code-guard validate');
606
+ setScript('prepare', 'husky');
482
607
  // Lint and format scripts
483
608
  const extensions = project.usesTypeScript ? '.ts,.tsx,.js,.jsx' : '.js,.jsx';
484
- packageJson.scripts['lint'] = `eslint . --ext ${extensions}`;
485
- packageJson.scripts['lint:fix'] = `eslint . --ext ${extensions} --fix`;
486
- packageJson.scripts['format'] = 'prettier --write .';
487
- packageJson.scripts['format:check'] = 'prettier --check .';
609
+ setScript('lint', `eslint . --ext ${extensions}`);
610
+ setScript('lint:fix', `eslint . --ext ${extensions} --fix`);
611
+ setScript('format', 'prettier --write .');
612
+ setScript('format:check', 'prettier --check .');
488
613
  // Security scripts
489
- packageJson.scripts['set-bypass-password'] = `node node_modules/${packageName}/dist/scripts/set-bypass-password.js`;
490
- packageJson.scripts['set-admin-password'] = `node node_modules/${packageName}/dist/scripts/set-admin-password.js`;
491
- packageJson.scripts['delete-bypass-logs'] = `node node_modules/${packageName}/dist/scripts/delete-bypass-logs.js`;
492
- packageJson.scripts['view-bypass-log'] = `node node_modules/${packageName}/dist/scripts/view-bypass-log.js`;
493
- packageJson.scripts['auto-fix'] = `node node_modules/${packageName}/dist/scripts/auto-fix.js`;
614
+ setScript('set-bypass-password', `node node_modules/${packageName}/dist/scripts/set-bypass-password.js`);
615
+ setScript('set-admin-password', `node node_modules/${packageName}/dist/scripts/set-admin-password.js`);
616
+ setScript('delete-bypass-logs', `node node_modules/${packageName}/dist/scripts/delete-bypass-logs.js`);
617
+ setScript('view-bypass-log', `node node_modules/${packageName}/dist/scripts/view-bypass-log.js`);
618
+ setScript('auto-fix', `node node_modules/${packageName}/dist/scripts/auto-fix.js`);
494
619
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
495
620
  console.log(` ${c.green('✓')} Added npm scripts (lint, format, validate, precommit-check, etc.)`);
496
621
  }
497
622
  // ── Copy Code Guardian files ────────────────────────────────────────────────
498
- function copyCodeGuardianFiles(cwd) {
623
+ function copyCodeGuardianFiles(cwd, manifest, projectType) {
499
624
  // Determine source directories (handles both dev and built package layouts)
500
625
  const distDir = path.join(__dirname, '..');
501
626
  const pkgRootDir = path.join(__dirname, '..', '..');
502
- // Copy config files
627
+ // Copy config files — only guidelines (always) + detected framework config
503
628
  const configDir = path.join(cwd, 'config');
504
629
  if (!fs.existsSync(configDir)) {
505
630
  fs.mkdirSync(configDir, { recursive: true });
506
631
  }
507
- const configFiles = ['guidelines.config.ts', 'angular.config.ts', 'react.config.ts', 'nextjs.config.ts'];
632
+ // Map project type to its config file
633
+ const frameworkConfigMap = {
634
+ angular: 'angular.config.ts',
635
+ react: 'react.config.ts',
636
+ nextjs: 'nextjs.config.ts',
637
+ node: 'node.config.ts',
638
+ };
639
+ // Always include guidelines + detected framework config (+ react for nextjs since it inherits)
640
+ const configFiles = ['guidelines.config.ts'];
641
+ if (projectType && frameworkConfigMap[projectType]) {
642
+ configFiles.push(frameworkConfigMap[projectType]);
643
+ }
644
+ // Next.js inherits React rules, so include react config too
645
+ if (projectType === 'nextjs' && !configFiles.includes('react.config.ts')) {
646
+ configFiles.push('react.config.ts');
647
+ }
508
648
  const sourceConfigDirs = [path.join(distDir, 'config'), path.join(pkgRootDir, 'config')];
509
649
  for (const file of configFiles) {
510
650
  let src = null;
@@ -518,6 +658,10 @@ function copyCodeGuardianFiles(cwd) {
518
658
  const dest = path.join(configDir, file);
519
659
  if (src && !fs.existsSync(dest)) {
520
660
  fs.copyFileSync(src, dest);
661
+ if (manifest) {
662
+ const { recordCreatedFile } = requireUtil('manifest');
663
+ recordCreatedFile(manifest, `config/${file}`);
664
+ }
521
665
  console.log(` ${c.green('✓')} ${file}`);
522
666
  }
523
667
  }
@@ -540,6 +684,10 @@ function copyCodeGuardianFiles(cwd) {
540
684
  const dest = path.join(templatesDir, file);
541
685
  if (src && !fs.existsSync(dest)) {
542
686
  fs.copyFileSync(src, dest);
687
+ if (manifest) {
688
+ const { recordCreatedFile } = requireUtil('manifest');
689
+ recordCreatedFile(manifest, `templates/${file}`);
690
+ }
543
691
  console.log(` ${c.green('✓')} ${file}`);
544
692
  }
545
693
  }
@@ -548,6 +696,10 @@ function copyCodeGuardianFiles(cwd) {
548
696
  if (!fs.existsSync(docsDir)) {
549
697
  fs.mkdirSync(docsDir, { recursive: true });
550
698
  fs.writeFileSync(path.join(docsDir, '.gitkeep'), '');
699
+ if (manifest) {
700
+ const { recordCreatedFile } = requireUtil('manifest');
701
+ recordCreatedFile(manifest, 'docs/features/.gitkeep');
702
+ }
551
703
  console.log(` ${c.green('✓')} Created docs/features/`);
552
704
  }
553
705
  }
@@ -883,11 +1035,20 @@ function promptYesNo(question) {
883
1035
  async function uninstallCodeGuard() {
884
1036
  showBanner();
885
1037
  console.log(c.bold('🗑️ Code Guardian Uninstall\n'));
886
- const filesToRemove = [
1038
+ const cwd = process.cwd();
1039
+ // ── Load manifest for smart uninstall ────────────────────────────────
1040
+ const { loadManifest, removeMarkedContent } = requireUtil('manifest');
1041
+ const manifest = loadManifest(cwd);
1042
+ // Hardcoded fallbacks for files that Code Guardian always creates
1043
+ const fallbackFilesToRemove = [
887
1044
  '.husky/_/husky.sh',
1045
+ 'eslint.config.mjs',
888
1046
  'eslint.config.js',
1047
+ '.prettierrc.json',
1048
+ '.prettierignore',
889
1049
  'prettier.config.js',
890
1050
  '.editorconfig',
1051
+ '.lintstagedrc.json',
891
1052
  'lint-staged.config.js',
892
1053
  'tsconfig.strict.json',
893
1054
  ];
@@ -895,7 +1056,6 @@ async function uninstallCodeGuard() {
895
1056
  'config',
896
1057
  'templates',
897
1058
  'docs',
898
- '.code-guardian',
899
1059
  ];
900
1060
  const scriptsToRemove = [
901
1061
  'precommit-check',
@@ -906,11 +1066,16 @@ async function uninstallCodeGuard() {
906
1066
  'set-admin-password',
907
1067
  'view-bypass-log',
908
1068
  'delete-bypass-logs',
1069
+ 'lint',
1070
+ 'lint:fix',
1071
+ 'format',
1072
+ 'format:check',
1073
+ 'validate',
1074
+ 'prepare',
909
1075
  ];
910
1076
  // ── Detect AI config files via registry ──────────────────────────────
911
1077
  const { defaultRegistry } = requireUtil('ai-config-registry');
912
1078
  const aiTemplates = defaultRegistry.getAll();
913
- const cwd = process.cwd();
914
1079
  const aiActions = [];
915
1080
  for (const t of aiTemplates) {
916
1081
  const dir = t.directory ? path.join(cwd, t.directory) : cwd;
@@ -920,38 +1085,103 @@ async function uninstallCodeGuard() {
920
1085
  continue;
921
1086
  const content = fs.readFileSync(filePath, 'utf-8');
922
1087
  if (!content.includes(t.marker))
923
- continue; // not ours
924
- // Determine if the file was created by us or if we appended
925
- // If appended, there will be a --- separator before our marker with user content above
926
- const markerIdx = content.indexOf(t.marker);
927
- const beforeMarker = content.substring(0, markerIdx).trimEnd();
928
- // Check if there's substantial user content before our section
929
- // When we append we add "\n\n---\n\n" before our content
930
- // When we create, the marker is at or near the top (possibly after frontmatter)
931
- const stripped = beforeMarker.replace(/^---[\s\S]*?---/, '').trim(); // strip MDC frontmatter
932
- const hasUserContent = stripped.length > 0 && !stripped.endsWith('---');
933
- // A more robust check: if content before marker (minus separator) has real text
934
- const withoutTrailingSep = beforeMarker.replace(/\n*---\s*$/, '').trim();
935
- const realUserContent = withoutTrailingSep.replace(/^---[\s\S]*?---/, '').trim(); // strip frontmatter
936
- if (realUserContent.length > 0) {
937
- aiActions.push({ name: t.name, filePath, relativePath, marker: t.marker, action: 'strip' });
1088
+ continue;
1089
+ // Use manifest to determine action if available
1090
+ if (manifest && manifest.files[relativePath]) {
1091
+ const record = manifest.files[relativePath];
1092
+ if (record.action === 'created') {
1093
+ aiActions.push({ name: t.name, filePath, relativePath, marker: t.marker, action: 'delete' });
1094
+ }
1095
+ else {
1096
+ aiActions.push({ name: t.name, filePath, relativePath, marker: t.marker, action: 'strip' });
1097
+ }
938
1098
  }
939
1099
  else {
940
- aiActions.push({ name: t.name, filePath, relativePath, marker: t.marker, action: 'delete' });
1100
+ // Fallback: detect by checking for user content before marker
1101
+ const markerIdx = content.indexOf(t.marker);
1102
+ const beforeMarker = content.substring(0, markerIdx).trimEnd();
1103
+ const withoutTrailingSep = beforeMarker.replace(/\n*---\s*$/, '').trim();
1104
+ const realUserContent = withoutTrailingSep.replace(/^---[\s\S]*?---/, '').trim();
1105
+ if (realUserContent.length > 0) {
1106
+ aiActions.push({ name: t.name, filePath, relativePath, marker: t.marker, action: 'strip' });
1107
+ }
1108
+ else {
1109
+ aiActions.push({ name: t.name, filePath, relativePath, marker: t.marker, action: 'delete' });
1110
+ }
941
1111
  }
942
1112
  }
1113
+ // Determine which files to remove (use manifest if available, fallback to hardcoded)
1114
+ let filesToProcess;
1115
+ if (manifest) {
1116
+ // Use manifest — it knows exactly what we created vs modified
1117
+ filesToProcess = Object.keys(manifest.files).filter(f => {
1118
+ // Skip AI files — handled separately
1119
+ return !aiActions.some(a => a.relativePath === f);
1120
+ });
1121
+ }
1122
+ else {
1123
+ filesToProcess = fallbackFilesToRemove;
1124
+ }
943
1125
  // Check what exists
944
- const existingFiles = filesToRemove.filter(f => fs.existsSync(path.join(cwd, f)));
1126
+ const existingFiles = filesToProcess.filter(f => fs.existsSync(path.join(cwd, f)));
945
1127
  const existingDirs = dirsToRemove.filter(d => fs.existsSync(path.join(cwd, d)));
946
- if (existingFiles.length === 0 && existingDirs.length === 0 && aiActions.length === 0) {
1128
+ // Also check for backup files that need restoring
1129
+ const backupsToRestore = [];
1130
+ if (manifest && manifest.backups) {
1131
+ for (const [original, backup] of Object.entries(manifest.backups)) {
1132
+ if (fs.existsSync(path.join(cwd, backup))) {
1133
+ backupsToRestore.push({ original, backup });
1134
+ }
1135
+ }
1136
+ }
1137
+ // Also find any .backup files from generators (eslint, prettier create them)
1138
+ for (const file of existingFiles) {
1139
+ const backupPath = path.join(cwd, file + '.backup');
1140
+ if (fs.existsSync(backupPath) && !backupsToRestore.some(b => b.backup === file + '.backup')) {
1141
+ backupsToRestore.push({ original: file, backup: file + '.backup' });
1142
+ }
1143
+ }
1144
+ // Check common backup locations from generators
1145
+ const commonBackups = [
1146
+ '.eslintrc.backup', '.eslintrc.js.backup', '.eslintrc.cjs.backup', '.eslintrc.json.backup',
1147
+ 'eslint.config.js.backup', 'eslint.config.mjs.backup', 'eslint.config.cjs.backup',
1148
+ '.prettierrc.backup', '.prettierrc.js.backup', '.prettierrc.cjs.backup', '.prettierrc.json.backup',
1149
+ '.prettierrc.yml.backup', '.prettierrc.yaml.backup', '.prettierrc.toml.backup', 'prettier.config.js.backup',
1150
+ '.eslintignore.backup',
1151
+ ];
1152
+ for (const backup of commonBackups) {
1153
+ if (fs.existsSync(path.join(cwd, backup))) {
1154
+ const original = backup.replace(/\.backup$/, '');
1155
+ if (!backupsToRestore.some(b => b.backup === backup)) {
1156
+ backupsToRestore.push({ original, backup });
1157
+ }
1158
+ }
1159
+ }
1160
+ if (existingFiles.length === 0 && existingDirs.length === 0 && aiActions.length === 0 && backupsToRestore.length === 0) {
947
1161
  console.log(c.yellow('⚠️ No Code Guardian files found to remove.\n'));
948
1162
  return;
949
1163
  }
950
- // Show what will be removed
1164
+ // Show what will be done
951
1165
  console.log(c.bold('The following will be removed:\n'));
952
- if (existingFiles.length > 0) {
953
- console.log(c.bold('Files:'));
954
- existingFiles.forEach(f => console.log(` ${c.red('✗')} ${f}`));
1166
+ // Separate created vs modified files
1167
+ const createdFiles = [];
1168
+ const modifiedFiles = [];
1169
+ for (const f of existingFiles) {
1170
+ if (manifest && manifest.files[f] && manifest.files[f].action === 'modified') {
1171
+ modifiedFiles.push(f);
1172
+ }
1173
+ else {
1174
+ createdFiles.push(f);
1175
+ }
1176
+ }
1177
+ if (createdFiles.length > 0) {
1178
+ console.log(c.bold('Files (created by Code Guardian — will be deleted):'));
1179
+ createdFiles.forEach(f => console.log(` ${c.red('✗')} ${f}`));
1180
+ console.log('');
1181
+ }
1182
+ if (modifiedFiles.length > 0) {
1183
+ console.log(c.bold('Files (modified — Code Guardian content will be stripped):'));
1184
+ modifiedFiles.forEach(f => console.log(` ${c.yellow('⚠')} ${f} ${c.dim('(your content preserved)')}`));
955
1185
  console.log('');
956
1186
  }
957
1187
  if (existingDirs.length > 0) {
@@ -971,6 +1201,11 @@ async function uninstallCodeGuard() {
971
1201
  aiStrips.forEach(a => console.log(` ${c.yellow('⚠')} ${a.relativePath} ${c.dim('(keeping your original content)')}`));
972
1202
  console.log('');
973
1203
  }
1204
+ if (backupsToRestore.length > 0) {
1205
+ console.log(c.bold('Backups to restore (original configs):'));
1206
+ backupsToRestore.forEach(b => console.log(` ${c.blue('↩')} ${b.backup} → ${b.original}`));
1207
+ console.log('');
1208
+ }
974
1209
  if (!hasFlag('yes') && !hasFlag('y')) {
975
1210
  const readline = require('readline');
976
1211
  const rl = readline.createInterface({
@@ -990,47 +1225,72 @@ async function uninstallCodeGuard() {
990
1225
  }
991
1226
  let removedCount = 0;
992
1227
  console.log('');
993
- // Backup AI config files before modifying
994
- const backupDir = path.join(cwd, '.code-guardian-backup-' + Date.now());
995
- let hasBackup = false;
996
- if (aiActions.length > 0 && !hasFlag('no-backup')) {
997
- console.log(c.bold('Creating backup...\n'));
998
- fs.mkdirSync(backupDir, { recursive: true });
999
- for (const action of aiActions) {
1000
- if (fs.existsSync(action.filePath)) {
1228
+ // ── Phase 1: Handle created files (delete) ──────────────────────────
1229
+ if (createdFiles.length > 0) {
1230
+ console.log(c.bold('Removing created files...\n'));
1231
+ for (const file of createdFiles) {
1232
+ const fullPath = path.join(cwd, file);
1233
+ if (fs.existsSync(fullPath)) {
1001
1234
  try {
1002
- const destPath = path.join(backupDir, action.relativePath);
1003
- const destDir = path.dirname(destPath);
1004
- if (!fs.existsSync(destDir))
1005
- fs.mkdirSync(destDir, { recursive: true });
1006
- fs.cpSync(action.filePath, destPath);
1007
- console.log(` ${c.blue('📦')} Backed up ${action.relativePath}`);
1008
- hasBackup = true;
1235
+ fs.unlinkSync(fullPath);
1236
+ console.log(` ${c.green('✓')} Removed ${file}`);
1237
+ removedCount++;
1009
1238
  }
1010
1239
  catch (error) {
1011
- console.log(` ${c.yellow('')} Failed to backup ${action.relativePath}: ${error.message}`);
1240
+ console.log(` ${c.red('')} Failed to remove ${file}: ${error.message}`);
1012
1241
  }
1013
1242
  }
1014
1243
  }
1015
1244
  console.log('');
1016
1245
  }
1017
- // Remove config files created entirely by Code Guardian
1018
- console.log(c.bold('Removing files...\n'));
1019
- for (const file of filesToRemove) {
1020
- const fullPath = path.join(cwd, file);
1021
- if (fs.existsSync(fullPath)) {
1246
+ // ── Phase 2: Handle modified files (strip marked content) ───────────
1247
+ if (modifiedFiles.length > 0) {
1248
+ console.log(c.bold('Stripping Code Guardian content from modified files...\n'));
1249
+ for (const file of modifiedFiles) {
1250
+ const fullPath = path.join(cwd, file);
1251
+ if (fs.existsSync(fullPath)) {
1252
+ try {
1253
+ const record = manifest?.files[file];
1254
+ const stripped = removeMarkedContent(fullPath, record?.marker);
1255
+ if (stripped) {
1256
+ console.log(` ${c.green('✓')} Stripped Code Guardian content from ${file}`);
1257
+ }
1258
+ else {
1259
+ console.log(` ${c.dim('○')} No marked content found in ${file}`);
1260
+ }
1261
+ removedCount++;
1262
+ }
1263
+ catch (error) {
1264
+ console.log(` ${c.red('✗')} Failed to clean ${file}: ${error.message}`);
1265
+ }
1266
+ }
1267
+ }
1268
+ console.log('');
1269
+ }
1270
+ // ── Phase 3: Restore backup files ───────────────────────────────────
1271
+ if (backupsToRestore.length > 0) {
1272
+ console.log(c.bold('Restoring original config files...\n'));
1273
+ for (const { original, backup } of backupsToRestore) {
1274
+ const backupFullPath = path.join(cwd, backup);
1275
+ const originalFullPath = path.join(cwd, original);
1022
1276
  try {
1023
- fs.unlinkSync(fullPath);
1024
- console.log(` ${c.green('✓')} Removed ${file}`);
1277
+ // Remove the Code Guardian version first (if it exists)
1278
+ if (fs.existsSync(originalFullPath)) {
1279
+ fs.unlinkSync(originalFullPath);
1280
+ }
1281
+ // Restore the backup as the original
1282
+ fs.renameSync(backupFullPath, originalFullPath);
1283
+ console.log(` ${c.green('✓')} Restored ${original} from ${backup}`);
1025
1284
  removedCount++;
1026
1285
  }
1027
1286
  catch (error) {
1028
- console.log(` ${c.red('✗')} Failed to remove ${file}: ${error.message}`);
1287
+ console.log(` ${c.red('✗')} Failed to restore ${original}: ${error.message}`);
1029
1288
  }
1030
1289
  }
1290
+ console.log('');
1031
1291
  }
1032
- // Remove directories
1033
- console.log(c.bold('\nRemoving directories...\n'));
1292
+ // ── Phase 4: Remove directories ─────────────────────────────────────
1293
+ console.log(c.bold('Removing directories...\n'));
1034
1294
  for (const dir of dirsToRemove) {
1035
1295
  const fullPath = path.join(cwd, dir);
1036
1296
  if (fs.existsSync(fullPath)) {
@@ -1044,12 +1304,11 @@ async function uninstallCodeGuard() {
1044
1304
  }
1045
1305
  }
1046
1306
  }
1047
- // Handle AI config files intelligently
1307
+ // ── Phase 5: Handle AI config files intelligently ───────────────────
1048
1308
  console.log(c.bold('\nCleaning AI config files...\n'));
1049
1309
  for (const action of aiActions) {
1050
1310
  try {
1051
1311
  if (action.action === 'delete') {
1052
- // Entire file was created by Code Guardian → delete it
1053
1312
  fs.unlinkSync(action.filePath);
1054
1313
  console.log(` ${c.green('✓')} Removed ${action.relativePath}`);
1055
1314
  removedCount++;
@@ -1060,7 +1319,6 @@ async function uninstallCodeGuard() {
1060
1319
  const remaining = fs.readdirSync(parentDir);
1061
1320
  if (remaining.length === 0) {
1062
1321
  fs.rmdirSync(parentDir);
1063
- // Check grandparent too (e.g., .windsurf/rules → .windsurf)
1064
1322
  const grandparent = path.dirname(parentDir);
1065
1323
  if (grandparent !== cwd && fs.existsSync(grandparent)) {
1066
1324
  const gpRemaining = fs.readdirSync(grandparent);
@@ -1078,16 +1336,12 @@ async function uninstallCodeGuard() {
1078
1336
  const markerIdx = content.indexOf(action.marker);
1079
1337
  if (markerIdx === -1)
1080
1338
  continue;
1081
- // Find the separator (--- on its own line) before the marker
1082
- // When we append, we add "\n\n---\n\n" before our content
1083
1339
  let cutStart = markerIdx;
1084
1340
  const beforeMarker = content.substring(0, markerIdx);
1085
- // Look for the --- separator we added
1086
1341
  const sepMatch = beforeMarker.match(/\n*\s*---\s*\n*$/);
1087
1342
  if (sepMatch && sepMatch.index !== undefined) {
1088
1343
  cutStart = sepMatch.index;
1089
1344
  }
1090
- // Remove from cutStart to end of file (our content is always appended at the end)
1091
1345
  const cleaned = content.substring(0, cutStart).trimEnd() + '\n';
1092
1346
  fs.writeFileSync(action.filePath, cleaned);
1093
1347
  console.log(` ${c.green('✓')} Stripped Code Guardian rules from ${action.relativePath} ${c.dim('(your content preserved)')}`);
@@ -1098,7 +1352,7 @@ async function uninstallCodeGuard() {
1098
1352
  console.log(` ${c.red('✗')} Failed to clean ${action.relativePath}: ${error.message}`);
1099
1353
  }
1100
1354
  }
1101
- // Clean up .husky hooks smartly
1355
+ // ── Phase 6: Clean up .husky hooks smartly ──────────────────────────
1102
1356
  console.log(c.bold('\nCleaning git hooks...\n'));
1103
1357
  const huskyDir = path.join(cwd, '.husky');
1104
1358
  const huskyHookFiles = ['pre-commit'];
@@ -1117,7 +1371,6 @@ async function uninstallCodeGuard() {
1117
1371
  continue;
1118
1372
  const beforeHook = content.substring(0, startIdx).trimEnd();
1119
1373
  const afterHook = content.substring(endIdx + HOOK_END.length).trimEnd();
1120
- // Check if there's real user content outside of our markers
1121
1374
  const remaining = (beforeHook.replace(/^#!\/usr\/bin\/env\s+sh\s*/, '').trim() + afterHook.trim()).trim();
1122
1375
  if (remaining.length === 0) {
1123
1376
  // Entire file is ours → delete it
@@ -1151,22 +1404,45 @@ async function uninstallCodeGuard() {
1151
1404
  }
1152
1405
  catch { }
1153
1406
  }
1154
- // Remove scripts from package.json
1407
+ // ── Phase 7: Clean package.json scripts ─────────────────────────────
1155
1408
  console.log(c.bold('\nCleaning package.json scripts...\n'));
1156
1409
  const packageJsonPath = path.join(cwd, 'package.json');
1157
1410
  if (fs.existsSync(packageJsonPath)) {
1158
1411
  try {
1159
1412
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
1160
- let scriptRemoved = false;
1161
- for (const script of scriptsToRemove) {
1162
- if (packageJson.scripts && packageJson.scripts[script]) {
1163
- delete packageJson.scripts[script];
1164
- console.log(` ${c.green('✓')} Removed script: ${script}`);
1165
- scriptRemoved = true;
1166
- removedCount++;
1413
+ let scriptChanged = false;
1414
+ if (manifest && manifest.packageJsonScripts) {
1415
+ // Restore modified scripts to original values
1416
+ for (const [scriptName, originalValue] of Object.entries(manifest.packageJsonScripts.modified)) {
1417
+ if (packageJson.scripts && packageJson.scripts[scriptName]) {
1418
+ packageJson.scripts[scriptName] = originalValue;
1419
+ console.log(` ${c.green('✓')} Restored script: ${scriptName} ${c.dim('(original value)')}`);
1420
+ scriptChanged = true;
1421
+ removedCount++;
1422
+ }
1423
+ }
1424
+ // Remove scripts that were added by Code Guardian
1425
+ for (const scriptName of manifest.packageJsonScripts.added) {
1426
+ if (packageJson.scripts && packageJson.scripts[scriptName]) {
1427
+ delete packageJson.scripts[scriptName];
1428
+ console.log(` ${c.green('✓')} Removed script: ${scriptName}`);
1429
+ scriptChanged = true;
1430
+ removedCount++;
1431
+ }
1432
+ }
1433
+ }
1434
+ else {
1435
+ // Fallback: remove known Code Guardian scripts
1436
+ for (const script of scriptsToRemove) {
1437
+ if (packageJson.scripts && packageJson.scripts[script]) {
1438
+ delete packageJson.scripts[script];
1439
+ console.log(` ${c.green('✓')} Removed script: ${script}`);
1440
+ scriptChanged = true;
1441
+ removedCount++;
1442
+ }
1167
1443
  }
1168
1444
  }
1169
- if (scriptRemoved) {
1445
+ if (scriptChanged) {
1170
1446
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
1171
1447
  console.log(` ${c.green('✓')} Updated package.json`);
1172
1448
  }
@@ -1178,11 +1454,20 @@ async function uninstallCodeGuard() {
1178
1454
  console.log(` ${c.red('✗')} Failed to clean package.json: ${error.message}`);
1179
1455
  }
1180
1456
  }
1457
+ // ── Phase 8: Remove .code-guardian directory (including manifest) ────
1458
+ const codeGuardianDir = path.join(cwd, '.code-guardian');
1459
+ if (fs.existsSync(codeGuardianDir)) {
1460
+ try {
1461
+ fs.rmSync(codeGuardianDir, { recursive: true, force: true });
1462
+ console.log(`\n ${c.green('✓')} Removed .code-guardian/`);
1463
+ removedCount++;
1464
+ }
1465
+ catch (error) {
1466
+ console.log(` ${c.red('✗')} Failed to remove .code-guardian/: ${error.message}`);
1467
+ }
1468
+ }
1181
1469
  console.log('');
1182
1470
  console.log(c.green(`✅ Cleanup complete! Removed ${removedCount} item(s).`));
1183
- if (hasBackup) {
1184
- console.log(c.blue(`📦 Backup saved to: ${path.basename(backupDir)}/`));
1185
- }
1186
1471
  console.log('');
1187
1472
  console.log(c.dim('To completely remove the package, run:'));
1188
1473
  console.log(c.dim(' npm uninstall @hatem427/code-guard-ci'));