@hatem427/code-guard-ci 3.5.7 → 3.5.9

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/scripts/cli.ts CHANGED
@@ -453,6 +453,9 @@ function initProject(): void {
453
453
  saveManifest(cwd, manifest);
454
454
  console.log(` ${c.green('✓')} Saved manifest to .code-guardian/manifest.json\n`);
455
455
 
456
+ // ── Update .gitignore for .code-guardian/ ────────────────────────────────
457
+ ensureCodeGuardianGitignore(cwd);
458
+
456
459
  // ── Done! ─────────────────────────────────────────────────────────────────
457
460
  console.log(c.bold(c.green('═══════════════════════════════════════════════════════════')));
458
461
  console.log(c.bold(c.green(' ✅ Code Guardian initialized successfully!')));
@@ -483,6 +486,124 @@ function initProject(): void {
483
486
  console.log('');
484
487
  }
485
488
 
489
+ // ── .gitignore management for .code-guardian/ ───────────────────────────────
490
+
491
+ /**
492
+ * Best-practice .gitignore management for .code-guardian/.
493
+ *
494
+ * SENSITIVE — never commit (passwords, generated reports):
495
+ * .code-guardian/bypass-password.hash
496
+ * .code-guardian/admin-password.hash
497
+ * .code-guardian/CODE_GUARDIAN_REPORT.md
498
+ *
499
+ * TRACKED — must be committed (team config, audit trail):
500
+ * .code-guardian/custom-rules.json
501
+ * .code-guardian/structure-rules.json
502
+ * .code-guardian/naming-rules.json
503
+ * .code-guardian/manifest.json
504
+ * .code-guardian/bypass-log.json
505
+ *
506
+ * STRATEGY:
507
+ * If `.code-guardian` is broadly ignored → replace with surgical pattern:
508
+ * .code-guardian/* (ignore everything inside …)
509
+ * !.code-guardian/custom-rules.json (… except tracked files)
510
+ * …
511
+ * Always add sensitive files to ignore if not present.
512
+ */
513
+ function ensureCodeGuardianGitignore(cwd: string): void {
514
+ const gitignorePath = path.join(cwd, '.gitignore');
515
+
516
+ const sensitiveFiles = [
517
+ '.code-guardian/bypass-password.hash',
518
+ '.code-guardian/admin-password.hash',
519
+ '.code-guardian/CODE_GUARDIAN_REPORT.md',
520
+ ];
521
+
522
+ const trackedFiles = [
523
+ '.code-guardian/custom-rules.json',
524
+ '.code-guardian/structure-rules.json',
525
+ '.code-guardian/naming-rules.json',
526
+ '.code-guardian/manifest.json',
527
+ '.code-guardian/bypass-log.json',
528
+ ];
529
+
530
+ let content = fs.existsSync(gitignorePath)
531
+ ? fs.readFileSync(gitignorePath, 'utf-8')
532
+ : '';
533
+
534
+ let changed = false;
535
+ const messages: string[] = [];
536
+
537
+ // ── 1. Replace broad .code-guardian ignore with surgical pattern ─────────
538
+ const broadPattern = /^\.code-guardian\/?$/m;
539
+ const broadMatch = broadPattern.exec(content);
540
+ if (broadMatch) {
541
+ const surgicalLines = [
542
+ '.code-guardian/*',
543
+ ...trackedFiles.map(f => `!${f}`),
544
+ ];
545
+ content = content.replace(broadPattern, surgicalLines.join('\n'));
546
+ changed = true;
547
+ messages.push(
548
+ ` ${c.green('✓')} .gitignore: replaced broad \`.code-guardian\` with surgical pattern:\n` +
549
+ surgicalLines.map(l =>
550
+ ` ${l.startsWith('!') ? c.green(l) + ' (tracked)' : c.dim(l) + ' (ignored)'}`
551
+ ).join('\n')
552
+ );
553
+ }
554
+
555
+ // ── 2. Always ensure sensitive files are explicitly ignored ──────────────
556
+ const toIgnore = sensitiveFiles.filter(f => !content.includes(f));
557
+ if (toIgnore.length > 0) {
558
+ content += [
559
+ '',
560
+ '# Code Guardian — sensitive/generated (do not commit)',
561
+ ...toIgnore,
562
+ '',
563
+ ].join('\n');
564
+ changed = true;
565
+ messages.push(
566
+ ` ${c.green('✓')} .gitignore: sensitive files added:\n` +
567
+ toIgnore.map(f => ` ${c.dim(f)}`).join('\n')
568
+ );
569
+ }
570
+
571
+ // ── 3. Ensure tracked files are negated if the dir is still pattern-ignored
572
+ if (!broadMatch) {
573
+ // Dir wasn't broadly ignored — check if individual tracked files are ignored
574
+ // and add negations for any that are
575
+ const negationsNeeded = trackedFiles
576
+ .filter(f => {
577
+ const r = spawnSync('git', ['check-ignore', '--no-index', '-q', f], {
578
+ cwd, stdio: 'pipe',
579
+ });
580
+ return r.status === 0; // ignored
581
+ })
582
+ .map(f => `!${f}`)
583
+ .filter(neg => !content.includes(neg));
584
+
585
+ if (negationsNeeded.length > 0) {
586
+ content += [
587
+ '',
588
+ '# Code Guardian — team config files (must be tracked)',
589
+ ...negationsNeeded,
590
+ '',
591
+ ].join('\n');
592
+ changed = true;
593
+ messages.push(
594
+ ` ${c.green('✓')} .gitignore: un-ignored tracked config files:\n` +
595
+ negationsNeeded.map(l => ` ${c.green(l)}`).join('\n')
596
+ );
597
+ }
598
+ }
599
+
600
+ if (changed) {
601
+ fs.writeFileSync(gitignorePath, content, 'utf-8');
602
+ messages.forEach(m => console.log(m));
603
+ console.log('');
604
+ }
605
+ }
606
+
486
607
  // ── Setup Git Hooks ─────────────────────────────────────────────────────────
487
608
 
488
609
  function setupGitHooks(cwd: string): void {
@@ -21,6 +21,7 @@
21
21
 
22
22
  import * as fs from 'fs';
23
23
  import * as path from 'path';
24
+ import { spawnSync } from 'child_process';
24
25
  import { DetectionResult, ProjectType } from '../utils/project-detector';
25
26
  import { AIConfigRegistry, AIConfigTemplate, defaultRegistry } from '../utils/ai-config-registry';
26
27
 
@@ -49,6 +50,9 @@ export function generateAIConfigs(project: DetectionResult, customRulesText?: st
49
50
 
50
51
  // Clean up deprecated .agent/ directory (replaced by AGENTS.md)
51
52
  cleanupDeprecatedAgentDir(project.rootDir);
53
+
54
+ // Ensure .gitignore doesn't swallow the generated AI config files
55
+ ensureGitignoreAllowsAIConfigs(project.rootDir, templates);
52
56
  }
53
57
 
54
58
  /**
@@ -188,6 +192,135 @@ function cleanupDeprecatedAgentDir(rootDir: string): void {
188
192
  }
189
193
  }
190
194
  }
195
+ /**
196
+ * Check every generated AI config file against .gitignore and add negation
197
+ * patterns so the files are tracked by git even when their parent directory
198
+ * is ignored (e.g. .cursor is often in .gitignore).
199
+ */
200
+ /**
201
+ * Best-practice .gitignore management for AI config files.
202
+ *
203
+ * STRATEGY: Instead of piling negation hacks on top of broad ignores, we
204
+ * replace the broad pattern with a surgical one that:
205
+ * - keeps personal/IDE files ignored (e.g. .cursor/*)
206
+ * - explicitly tracks the shared rules folder (e.g. !.cursor/rules/)
207
+ *
208
+ * Example — if .gitignore contains `.cursor`, we replace it with:
209
+ * .cursor/*
210
+ * !.cursor/rules/
211
+ *
212
+ * If the broad pattern can't be replaced safely (e.g. it's inside a complex
213
+ * block), we fall back to appending negation patterns and warn the user.
214
+ */
215
+ function ensureGitignoreAllowsAIConfigs(
216
+ rootDir: string,
217
+ templates: AIConfigTemplate[]
218
+ ): void {
219
+ const gitignorePath = path.join(rootDir, '.gitignore');
220
+ if (!fs.existsSync(gitignorePath)) return;
221
+
222
+ let content = fs.readFileSync(gitignorePath, 'utf-8');
223
+ let changed = false;
224
+ const advisories: string[] = [];
225
+
226
+ // Collect unique top-level directories used by templates
227
+ // e.g. '.cursor', '.github', '.windsurf'
228
+ const topDirs = new Set<string>();
229
+ for (const t of templates) {
230
+ if (t.directory) {
231
+ topDirs.add(t.directory.split('/')[0]);
232
+ }
233
+ }
234
+
235
+ for (const dir of topDirs) {
236
+ // Check if a broad pattern ignores this directory.
237
+ // Matches bare `.cursor`, `.cursor/`, `.cursor/*` on its own line.
238
+ const broadPattern = new RegExp(
239
+ `^(\.?${escapeRegex(dir)})\\/?\\*?$`,
240
+ 'm'
241
+ );
242
+ const match = broadPattern.exec(content);
243
+ if (!match) continue;
244
+
245
+ // Find all rule sub-directories that belong to this top-level dir.
246
+ // e.g. for '.cursor' → ['.cursor/rules']
247
+ const ruleDirs = [...new Set(
248
+ templates
249
+ .filter(t => t.directory?.startsWith(dir + '/'))
250
+ .map(t => t.directory!)
251
+ )];
252
+
253
+ // Build the surgical replacement:
254
+ // .cursor/* ← ignore everything inside
255
+ // !.cursor/rules/ ← except the shared rules folder
256
+ const surgicalLines = [
257
+ `${dir}/*`,
258
+ ...ruleDirs.map(d => `!${d}/`),
259
+ ];
260
+
261
+ const replacement = surgicalLines.join('\n');
262
+ const newContent = content.replace(broadPattern, replacement);
263
+
264
+ if (newContent !== content) {
265
+ content = newContent;
266
+ changed = true;
267
+ advisories.push(
268
+ ` ✅ .gitignore: replaced broad \`${match[0]}\` with surgical pattern:\n` +
269
+ surgicalLines.map(l => ` ${l}`).join('\n')
270
+ );
271
+ }
272
+ }
273
+
274
+ // For any file still ignored after the surgical replacements, fall back to
275
+ // appending explicit negations (rare edge case).
276
+ const fallbackNegations: string[] = [];
277
+ // Write changes so far before running git check-ignore
278
+ if (changed) fs.writeFileSync(gitignorePath, content, 'utf-8');
279
+
280
+ for (const template of templates) {
281
+ const relativePath = template.directory
282
+ ? `${template.directory}/${template.fileName}`
283
+ : template.fileName;
284
+
285
+ const result = spawnSync('git', ['check-ignore', '--no-index', '-q', relativePath], {
286
+ cwd: rootDir,
287
+ stdio: 'pipe',
288
+ });
289
+
290
+ if (result.status !== 0) continue; // not ignored — all good
291
+
292
+ // Still ignored — add explicit negation as fallback
293
+ const neg = `!${relativePath}`;
294
+ if (!content.includes(neg) && !fallbackNegations.includes(neg)) {
295
+ fallbackNegations.push(neg);
296
+ }
297
+ }
298
+
299
+ if (fallbackNegations.length > 0) {
300
+ const section = [
301
+ '',
302
+ '# Code Guardian — AI config files (shared with team, un-ignored)',
303
+ ...fallbackNegations,
304
+ '',
305
+ ].join('\n');
306
+ content += section;
307
+ changed = true;
308
+ advisories.push(
309
+ ` ⚠️ Some AI config files were still gitignored — added explicit negations:\n` +
310
+ fallbackNegations.map(l => ` ${l}`).join('\n')
311
+ );
312
+ }
313
+
314
+ if (changed) {
315
+ fs.writeFileSync(gitignorePath, content, 'utf-8');
316
+ advisories.forEach(msg => console.log(msg));
317
+ }
318
+ }
319
+
320
+ function escapeRegex(s: string): string {
321
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
322
+ }
323
+
191
324
  /**
192
325
  * Generate content for .cursor/rules/cursor.mdc
193
326
  */
@@ -76,11 +76,6 @@ export function generateReport(
76
76
  const content = buildReportContent(report, options);
77
77
  fs.writeFileSync(reportPath, content, 'utf-8');
78
78
 
79
- // Attempt to open in VS Code
80
- if (options.autoOpen !== false) {
81
- openInEditor(reportPath);
82
- }
83
-
84
79
  return reportPath;
85
80
  }
86
81