eslint-plugin-ai-guard 1.1.14 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -53,14 +53,14 @@ __export(config_manager_exports, {
53
53
  });
54
54
  function backupConfig(configPath) {
55
55
  const backupPath = `${configPath}.bak`;
56
- import_fs3.default.copyFileSync(configPath, backupPath);
56
+ import_fs6.default.copyFileSync(configPath, backupPath);
57
57
  return backupPath;
58
58
  }
59
59
  function readConfig(configPath) {
60
- return import_fs3.default.readFileSync(configPath, "utf-8");
60
+ return import_fs6.default.readFileSync(configPath, "utf-8");
61
61
  }
62
62
  function writeConfig(configPath, content) {
63
- import_fs3.default.writeFileSync(configPath, content, "utf-8");
63
+ import_fs6.default.writeFileSync(configPath, content, "utf-8");
64
64
  }
65
65
  function generateFlatConfig(preset) {
66
66
  return `import aiGuard from 'eslint-plugin-ai-guard';
@@ -266,19 +266,19 @@ function addIgnoresToLegacyConfig(existing) {
266
266
  function getConfigFilePath(configType, cwd = process.cwd()) {
267
267
  switch (configType) {
268
268
  case "flat-js":
269
- return import_path3.default.join(cwd, "eslint.config.js");
269
+ return import_path5.default.join(cwd, "eslint.config.js");
270
270
  case "flat-mjs":
271
- return import_path3.default.join(cwd, "eslint.config.mjs");
271
+ return import_path5.default.join(cwd, "eslint.config.mjs");
272
272
  case "flat-cjs":
273
- return import_path3.default.join(cwd, "eslint.config.cjs");
273
+ return import_path5.default.join(cwd, "eslint.config.cjs");
274
274
  case "eslintrc-js":
275
- return import_path3.default.join(cwd, ".eslintrc.js");
275
+ return import_path5.default.join(cwd, ".eslintrc.js");
276
276
  case "eslintrc-cjs":
277
- return import_path3.default.join(cwd, ".eslintrc.cjs");
277
+ return import_path5.default.join(cwd, ".eslintrc.cjs");
278
278
  case "eslintrc-json":
279
- return import_path3.default.join(cwd, ".eslintrc.json");
279
+ return import_path5.default.join(cwd, ".eslintrc.json");
280
280
  default:
281
- return import_path3.default.join(cwd, ".eslintrc.js");
281
+ return import_path5.default.join(cwd, ".eslintrc.js");
282
282
  }
283
283
  }
284
284
  function validateFlatConfigText(content) {
@@ -313,12 +313,12 @@ function validateLegacyConfigText(content) {
313
313
  }
314
314
  return problems;
315
315
  }
316
- var import_fs3, import_path3, AI_GUARD_MARKER, DEFAULT_FLAT_IGNORES, DEFAULT_LEGACY_IGNORES;
316
+ var import_fs6, import_path5, AI_GUARD_MARKER, DEFAULT_FLAT_IGNORES, DEFAULT_LEGACY_IGNORES;
317
317
  var init_config_manager = __esm({
318
318
  "cli/utils/config-manager.ts"() {
319
319
  "use strict";
320
- import_fs3 = __toESM(require("fs"));
321
- import_path3 = __toESM(require("path"));
320
+ import_fs6 = __toESM(require("fs"));
321
+ import_path5 = __toESM(require("path"));
322
322
  AI_GUARD_MARKER = "eslint-plugin-ai-guard";
323
323
  DEFAULT_FLAT_IGNORES = [
324
324
  "node_modules/**",
@@ -341,17 +341,18 @@ var init_config_manager = __esm({
341
341
 
342
342
  // cli/index.ts
343
343
  var import_commander = require("commander");
344
- var import_fs8 = __toESM(require("fs"));
345
- var import_path11 = __toESM(require("path"));
344
+ var import_fs11 = __toESM(require("fs"));
345
+ var import_path13 = __toESM(require("path"));
346
346
 
347
347
  // cli/commands/run.ts
348
348
  var import_ora = __toESM(require("ora"));
349
- var import_chalk2 = __toESM(require("chalk"));
349
+ var import_chalk3 = __toESM(require("chalk"));
350
350
 
351
351
  // cli/utils/eslint-runner.ts
352
352
  var import_fs = __toESM(require("fs"));
353
353
  var import_path = __toESM(require("path"));
354
354
  var import_url = require("url");
355
+ var import_chalk2 = __toESM(require("chalk"));
355
356
 
356
357
  // cli/utils/logger.ts
357
358
  var import_chalk = __toESM(require("chalk"));
@@ -382,13 +383,54 @@ var RULE_CATEGORY = {
382
383
  "ai-guard/no-duplicate-logic-block": "AI Patterns",
383
384
  "ai-guard/no-dead-branch": "AI Patterns"
384
385
  };
386
+ var CONFIDENCE_TIER = {
387
+ // High confidence — near-zero false positive rate
388
+ "ai-guard/no-hardcoded-secret": "high",
389
+ "ai-guard/no-eval-dynamic": "high",
390
+ "ai-guard/no-floating-promise": "high",
391
+ "ai-guard/no-empty-catch": "high",
392
+ // Medium confidence — context matters, usually valid
393
+ "ai-guard/no-sql-string-concat": "medium",
394
+ "ai-guard/no-await-in-loop": "medium",
395
+ "ai-guard/require-auth-middleware": "medium",
396
+ "ai-guard/require-authz-check": "medium",
397
+ "ai-guard/no-catch-log-rethrow": "medium",
398
+ "ai-guard/no-catch-without-use": "medium",
399
+ "ai-guard/no-unsafe-deserialize": "medium",
400
+ // Low confidence — review case-by-case
401
+ "ai-guard/no-async-array-callback": "low",
402
+ "ai-guard/no-dead-branch": "low",
403
+ "ai-guard/no-broad-exception": "low",
404
+ "ai-guard/no-console-in-handler": "low",
405
+ "ai-guard/no-duplicate-logic-block": "low",
406
+ // Informational — mostly stylistic, high framework FP rate, collapsed by default
407
+ "ai-guard/no-async-without-await": "informational",
408
+ "ai-guard/no-redundant-await": "informational"
409
+ };
410
+ var ECOSYSTEM_FIX_HINTS = {
411
+ "react-hooks/exhaustive-deps": "npm install --save-dev eslint-plugin-react-hooks",
412
+ "react-hooks/rules-of-hooks": "npm install --save-dev eslint-plugin-react-hooks",
413
+ "@typescript-eslint/": "npm install --save-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser",
414
+ "import/": "npm install --save-dev eslint-plugin-import",
415
+ "jsx-a11y/": "npm install --save-dev eslint-plugin-jsx-a11y",
416
+ "prettier/": "npm install --save-dev eslint-plugin-prettier"
417
+ };
418
+ function getEcosystemFix(ruleId) {
419
+ if (!ruleId) return null;
420
+ for (const [prefix, fix] of Object.entries(ECOSYSTEM_FIX_HINTS)) {
421
+ if (ruleId.startsWith(prefix)) return fix;
422
+ }
423
+ return null;
424
+ }
385
425
  var PREFIX = {
386
426
  success: import_chalk.default.green("\u2714"),
387
427
  error: import_chalk.default.red("\u2716"),
388
428
  warn: import_chalk.default.yellow("\u26A0"),
389
429
  info: import_chalk.default.cyan("\u2139"),
390
430
  section: import_chalk.default.bold.white,
391
- bullet: import_chalk.default.gray("\u2022")
431
+ bullet: import_chalk.default.gray("\u2022"),
432
+ ecosystem: import_chalk.default.yellow("\u26A0"),
433
+ parser: import_chalk.default.magenta("\u26A1")
392
434
  };
393
435
  var log = {
394
436
  info(msg) {
@@ -408,6 +450,14 @@ var log = {
408
450
  console.log(import_chalk.default.bold.cyan(` \u2500\u2500 ${title} \u2500\u2500`));
409
451
  console.log("");
410
452
  },
453
+ /**
454
+ * Ecosystem section — distinct styling to communicate it's NOT ai-guard output
455
+ */
456
+ ecosystemSection(title) {
457
+ console.log("");
458
+ console.log(import_chalk.default.bold.yellow(` \u2500\u2500 ${title} \u2500\u2500`) + import_chalk.default.dim(" (not ai-guard findings)"));
459
+ console.log("");
460
+ },
411
461
  rule(ruleName, count) {
412
462
  console.log(
413
463
  ` ${PREFIX.bullet} ${import_chalk.default.yellow(ruleName)} ${import_chalk.default.gray(`(${count} issue${count !== 1 ? "s" : ""})`)}`
@@ -424,9 +474,36 @@ var log = {
424
474
  ` ${import_chalk.default.gray(`${line}:${col}`).padEnd(12)} ${icon} ${import_chalk.default.white(msg)}`
425
475
  );
426
476
  },
477
+ /**
478
+ * Print an ESLint ecosystem issue (missing rule definition, foreign plugin rule, etc.)
479
+ * with a clear disclaimer that this is NOT an ai-guard finding.
480
+ */
481
+ ecosystemIssue(issue, count = 1) {
482
+ const ruleLabel = issue.ruleId ? import_chalk.default.yellow(issue.ruleId) : import_chalk.default.gray("(no rule id)");
483
+ const typeLabel = issue.type === "missing-rule" ? import_chalk.default.dim("[missing rule definition]") : issue.type === "config-error" ? import_chalk.default.dim("[config error]") : import_chalk.default.dim("[foreign rule]");
484
+ const countLabel = count > 1 ? import_chalk.default.gray(` \xD7 ${count}`) : "";
485
+ console.log(` ${PREFIX.ecosystem} ${ruleLabel} ${typeLabel}${countLabel}`);
486
+ console.log(` ${import_chalk.default.gray("File:")} ${import_chalk.default.dim(issue.filePath)}${issue.line > 0 ? import_chalk.default.dim(`:${issue.line}`) : ""}`);
487
+ console.log(` ${import_chalk.default.gray("Msg:")} ${import_chalk.default.dim(issue.message.slice(0, 120))}`);
488
+ const fix = getEcosystemFix(issue.ruleId);
489
+ if (fix) {
490
+ console.log(` ${import_chalk.default.cyan("\u2192 Fix:")} ${import_chalk.default.yellow(fix)}`);
491
+ }
492
+ console.log(
493
+ import_chalk.default.dim(` Note: This comes from your ESLint config, not from ai-guard.`)
494
+ );
495
+ console.log("");
496
+ },
497
+ /**
498
+ * Print a parser/syntax error — clearly NOT an ai-guard finding.
499
+ */
500
+ parserError(pe) {
501
+ console.log(` ${PREFIX.parser} ${import_chalk.default.magenta("Parse failure")} ${import_chalk.default.dim(pe.filePath)}${pe.line > 0 ? import_chalk.default.dim(`:${pe.line}`) : ""}`);
502
+ console.log(` ${import_chalk.default.dim(pe.message.slice(0, 120))}`);
503
+ console.log("");
504
+ },
427
505
  /**
428
506
  * Print a category summary line with icon, label, and count.
429
- * e.g. 🔴 Security 3 errors · 0 warnings
430
507
  */
431
508
  category(icon, label, errors, warnings) {
432
509
  const parts = [];
@@ -437,7 +514,7 @@ var log = {
437
514
  console.log(` ${icon} ${import_chalk.default.bold.white(labelPadded)} ${counts}`);
438
515
  },
439
516
  /**
440
- * Print the scan stats line: files scanned, duration, preset.
517
+ * Print the scan stats line.
441
518
  */
442
519
  scanStats(filesScanned, filesWithIssues, durationMs, preset) {
443
520
  console.log(
@@ -466,6 +543,74 @@ var log = {
466
543
  };
467
544
 
468
545
  // cli/utils/eslint-runner.ts
546
+ var ISSUE_CONFIDENCE = {
547
+ "ai-guard/no-hardcoded-secret": "high",
548
+ "ai-guard/no-eval-dynamic": "high",
549
+ "ai-guard/no-floating-promise": "high",
550
+ "ai-guard/no-empty-catch": "high",
551
+ "ai-guard/no-sql-string-concat": "medium",
552
+ "ai-guard/no-await-in-loop": "medium",
553
+ "ai-guard/require-auth-middleware": "medium",
554
+ "ai-guard/require-authz-check": "medium",
555
+ "ai-guard/no-catch-log-rethrow": "medium",
556
+ "ai-guard/no-catch-without-use": "medium",
557
+ "ai-guard/no-unsafe-deserialize": "medium",
558
+ "ai-guard/no-async-array-callback": "low",
559
+ "ai-guard/no-dead-branch": "low",
560
+ "ai-guard/no-broad-exception": "low",
561
+ "ai-guard/no-console-in-handler": "low",
562
+ "ai-guard/no-duplicate-logic-block": "low",
563
+ "ai-guard/no-async-without-await": "informational",
564
+ "ai-guard/no-redundant-await": "informational"
565
+ };
566
+ var ISSUE_CATEGORY = {
567
+ "ai-guard/no-floating-promise": "Async Reliability",
568
+ "ai-guard/no-await-in-loop": "Async Reliability",
569
+ "ai-guard/no-async-without-await": "Async Reliability",
570
+ "ai-guard/no-async-array-callback": "Async Reliability",
571
+ "ai-guard/no-redundant-await": "Async Reliability",
572
+ "ai-guard/no-empty-catch": "Reliability",
573
+ "ai-guard/no-broad-exception": "Reliability",
574
+ "ai-guard/no-catch-log-rethrow": "Reliability",
575
+ "ai-guard/no-catch-without-use": "Reliability",
576
+ "ai-guard/no-hardcoded-secret": "Security",
577
+ "ai-guard/no-eval-dynamic": "Security",
578
+ "ai-guard/no-sql-string-concat": "Security",
579
+ "ai-guard/no-unsafe-deserialize": "Security",
580
+ "ai-guard/require-auth-middleware": "Security",
581
+ "ai-guard/require-authz-check": "Security",
582
+ "ai-guard/no-console-in-handler": "AI Patterns",
583
+ "ai-guard/no-duplicate-logic-block": "AI Patterns",
584
+ "ai-guard/no-dead-branch": "AI Patterns"
585
+ };
586
+ var ISSUE_ASYNC_RISK_TYPE = {
587
+ "ai-guard/no-floating-promise": "floating-promise",
588
+ "ai-guard/no-await-in-loop": "sequential-await",
589
+ "ai-guard/no-async-without-await": "unnecessary-async",
590
+ "ai-guard/no-async-array-callback": "async-array-callback",
591
+ "ai-guard/no-redundant-await": "redundant-await"
592
+ };
593
+ var ISSUE_REMEDIATION = {
594
+ "ai-guard/no-floating-promise": "Add `await` before the call, assign to a variable, or add `.catch()` to handle rejection.",
595
+ "ai-guard/no-await-in-loop": "Collect promises in an array and use `await Promise.all(...)` outside the loop.",
596
+ "ai-guard/no-async-without-await": "Remove `async` if no async operations are needed, or add an `await` expression.",
597
+ "ai-guard/no-async-array-callback": "Use `Promise.all(array.map(async item => ...))` to ensure all async callbacks are awaited.",
598
+ "ai-guard/no-redundant-await": "Remove the `await` \u2014 the value is already resolved.",
599
+ "ai-guard/no-empty-catch": "Add error handling in the catch block, or at minimum log the error.",
600
+ "ai-guard/no-broad-exception": "Catch specific error types or re-throw after handling.",
601
+ "ai-guard/no-catch-log-rethrow": "Either log the error OR rethrow it \u2014 doing both duplicates the error in logs.",
602
+ "ai-guard/no-catch-without-use": "Use the caught error variable or rename to `_` to signal intentional suppression.",
603
+ "ai-guard/no-hardcoded-secret": "Move credentials to environment variables: `process.env.SECRET_KEY`.",
604
+ "ai-guard/no-eval-dynamic": "Avoid eval and Function constructors. Use safer alternatives like JSON.parse.",
605
+ "ai-guard/no-sql-string-concat": "Use parameterized queries or an ORM to prevent SQL injection.",
606
+ "ai-guard/no-unsafe-deserialize": "Validate input with a schema library (zod, joi, yup) before parsing.",
607
+ "ai-guard/require-auth-middleware": "Add an authentication middleware before the route handler.",
608
+ "ai-guard/require-authz-check": "Verify the requesting user has permission to access this resource.",
609
+ "ai-guard/no-console-in-handler": "Replace console.log with a structured logger (pino, winston, etc.).",
610
+ "ai-guard/no-duplicate-logic-block": "Extract the duplicated logic into a shared function.",
611
+ "ai-guard/no-dead-branch": "Remove the unreachable branch or fix the condition."
612
+ };
613
+ var _runnerCache = /* @__PURE__ */ new Map();
469
614
  function isRecord(value) {
470
615
  return typeof value === "object" && value !== null;
471
616
  }
@@ -537,19 +682,64 @@ function getRules(preset) {
537
682
  return RECOMMENDED_RULES;
538
683
  }
539
684
  var DEFAULT_IGNORE_PATTERNS = [
685
+ // Core — always skip
540
686
  "**/node_modules/**",
541
687
  "**/.next/**",
542
688
  "**/dist/**",
543
689
  "**/build/**",
544
690
  "**/coverage/**",
545
691
  "**/out/**",
546
- "**/.git/**"
692
+ "**/.git/**",
693
+ // Build artifacts and caches
694
+ "**/.cache/**",
695
+ "**/.turbo/**",
696
+ "**/.nx/**",
697
+ "**/.parcel-cache/**",
698
+ "**/.webpack/**",
699
+ "**/tmp/**",
700
+ "**/temp/**",
701
+ // Package manager artifacts
702
+ "**/.yarn/**",
703
+ "**/.pnp.*",
704
+ // Framework generated
705
+ "**/.expo/**",
706
+ "**/.svelte-kit/**",
707
+ "**/storybook-static/**",
708
+ "**/.storybook/generated/**",
709
+ // Generated code
710
+ "**/generated/**",
711
+ "**/__generated__/**",
712
+ "**/vendor/**",
713
+ // Test outputs
714
+ "**/test-results/**",
715
+ "**/playwright-report/**",
716
+ // Misc
717
+ "**/.docusaurus/**",
718
+ "**/.vitepress/cache/**"
547
719
  ];
548
720
  function isSkippablePatternError(error) {
549
721
  const msg = error.message.toLowerCase();
550
- const hasNoFilesSignal = msg.includes("no files") || msg.includes("no files matching");
551
- const hasIgnoredSignal = msg.includes("ignored") || msg.includes("all files matched by") || msg.includes("are ignored") || msg.includes("was ignored") || msg.includes("file ignored");
552
- return hasNoFilesSignal || hasIgnoredSignal;
722
+ return msg.includes("no files") || msg.includes("no files matching") || msg.includes("ignored") || msg.includes("all files matched by") || msg.includes("are ignored") || msg.includes("was ignored") || msg.includes("file ignored");
723
+ }
724
+ function classifyMessage(msg) {
725
+ if (msg.fatal === true) return "parser-error";
726
+ if (msg.ruleId === null && (msg.message.startsWith("Parsing error") || msg.message.includes("Unexpected token") || msg.message.includes("SyntaxError"))) {
727
+ return "parser-error";
728
+ }
729
+ if (msg.ruleId && msg.ruleId.startsWith("ai-guard/")) {
730
+ return "ai-guard";
731
+ }
732
+ return "ecosystem";
733
+ }
734
+ function classifyEcosystemType(msg) {
735
+ const lower = msg.message.toLowerCase();
736
+ if (lower.includes("definition for rule") && lower.includes("was not found")) {
737
+ return "missing-rule";
738
+ }
739
+ if (lower.includes("configuration") || lower.includes("config")) {
740
+ return "config-error";
741
+ }
742
+ return "foreign-rule";
553
743
  }
554
744
  async function loadPluginModuleFromCwd(cwd) {
555
745
  const { createRequire: createRequire2 } = await import("module");
@@ -576,23 +766,44 @@ async function loadPluginModuleFromCwd(cwd) {
576
766
  "eslint-plugin-ai-guard is not installed. Run: npm install --save-dev eslint-plugin-ai-guard"
577
767
  );
578
768
  }
579
- async function runEslint(options) {
580
- const { ESLint } = await import("eslint").catch(() => {
581
- throw new Error(
582
- "ESLint is not installed. Run: npm install --save-dev eslint"
583
- );
584
- });
585
- const rawPlugin = await loadPluginModuleFromCwd(process.cwd());
586
- const plugin = normalizePlugin(rawPlugin);
587
- const rules = getRules(options.preset);
588
- const resolvedTargetPath = import_path.default.resolve(options.targetPath);
589
- if (!import_fs.default.existsSync(resolvedTargetPath)) {
590
- throw new Error(`Path not found: ${options.targetPath}`);
769
+ async function getOrBuildRunner(cwd, preset, eslintCwd) {
770
+ const cacheKey = `${eslintCwd}::${preset}`;
771
+ const cached = _runnerCache.get(cacheKey);
772
+ if (cached) {
773
+ return { runner: cached, fromCache: true };
591
774
  }
592
- const targetStat = import_fs.default.statSync(resolvedTargetPath);
593
- const isSingleFileTarget = targetStat.isFile();
594
- const eslintCwd = isSingleFileTarget ? import_path.default.dirname(resolvedTargetPath) : resolvedTargetPath;
595
- const startMs = Date.now();
775
+ const [{ ESLint }, rawPlugin, tsParserResult] = await Promise.all([
776
+ // 1. Load ESLint
777
+ import("eslint").catch(() => {
778
+ throw new Error("ESLint is not installed. Run: npm install --save-dev eslint");
779
+ }),
780
+ // 2. Load plugin
781
+ loadPluginModuleFromCwd(cwd),
782
+ // 3. Load TS parser (optional — errors are caught)
783
+ Promise.resolve().then(() => {
784
+ const parserStart = Date.now();
785
+ try {
786
+ let tsParser2;
787
+ try {
788
+ tsParser2 = require(import_path.default.join(
789
+ cwd,
790
+ "node_modules",
791
+ "@typescript-eslint",
792
+ "parser"
793
+ ));
794
+ } catch {
795
+ tsParser2 = require("@typescript-eslint/parser");
796
+ }
797
+ return { tsParser: tsParser2, available: true, loadMs: Date.now() - parserStart };
798
+ } catch {
799
+ log.debug("@typescript-eslint/parser not found \u2014 TypeScript files will use espree fallback");
800
+ return { tsParser: null, available: false, loadMs: Date.now() - parserStart };
801
+ }
802
+ })
803
+ ]);
804
+ const plugin = normalizePlugin(rawPlugin);
805
+ const rules = getRules(preset);
806
+ const { tsParser, available: tsParserAvailable } = tsParserResult;
596
807
  const JS_TS_FILES = [
597
808
  "**/*.js",
598
809
  "**/*.jsx",
@@ -603,22 +814,10 @@ async function runEslint(options) {
603
814
  "**/*.mjs",
604
815
  "**/*.cjs"
605
816
  ];
606
- let tsParser = null;
607
- try {
608
- try {
609
- tsParser = require(import_path.default.join(
610
- process.cwd(),
611
- "node_modules",
612
- "@typescript-eslint",
613
- "parser"
614
- ));
615
- } catch {
616
- tsParser = require("@typescript-eslint/parser");
617
- }
618
- } catch {
619
- }
620
817
  const configBlocks = [
621
- // JS/JSX files — default espree parser
818
+ // Ignore block first — ESLint can skip excluded files without processing
819
+ { ignores: DEFAULT_IGNORE_PATTERNS },
820
+ // JS/JSX — espree parser with JSX support
622
821
  {
623
822
  files: ["**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"],
624
823
  plugins: { "ai-guard": plugin },
@@ -630,277 +829,1356 @@ async function runEslint(options) {
630
829
  }
631
830
  },
632
831
  rules
633
- },
634
- // Ignore generated directories
635
- { ignores: DEFAULT_IGNORE_PATTERNS }
832
+ }
636
833
  ];
637
834
  if (tsParser) {
638
- configBlocks.splice(1, 0, {
835
+ configBlocks.push({
639
836
  files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
640
837
  plugins: { "ai-guard": plugin },
641
838
  languageOptions: {
642
839
  parser: tsParser,
643
840
  parserOptions: {
644
- // project: true would be needed for type-aware rules — skip for speed
645
841
  ecmaVersion: "latest",
646
- sourceType: "module"
842
+ sourceType: "module",
843
+ ecmaFeatures: { jsx: true }
647
844
  }
648
845
  },
649
846
  rules
650
847
  });
651
848
  } else {
652
- configBlocks.splice(1, 0, {
849
+ configBlocks.push({
653
850
  files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
654
851
  plugins: { "ai-guard": plugin },
852
+ languageOptions: {
853
+ parserOptions: {
854
+ ecmaVersion: "latest",
855
+ sourceType: "module",
856
+ ecmaFeatures: { jsx: true }
857
+ }
858
+ },
655
859
  rules
656
860
  });
657
861
  }
658
- const eslint = new ESLint({
659
- cwd: eslintCwd,
660
- overrideConfigFile: true,
661
- overrideConfig: configBlocks
662
- });
663
- const patterns = isSingleFileTarget ? [import_path.default.basename(resolvedTargetPath)] : JS_TS_FILES;
664
- const perPatternResults = await Promise.all(
665
- patterns.map(async (pattern) => {
666
- try {
667
- return await eslint.lintFiles([pattern]);
668
- } catch (err) {
669
- const error = err instanceof Error ? err : new Error(String(err));
670
- if (isSkippablePatternError(error)) {
671
- log.debug(`Skipping pattern '${pattern}' (no lintable files)`);
672
- return [];
862
+ const eslint = new ESLint({
863
+ cwd: eslintCwd,
864
+ overrideConfigFile: true,
865
+ overrideConfig: configBlocks
866
+ });
867
+ const runner = {
868
+ eslint,
869
+ tsParserAvailable,
870
+ pluginLoadMs: 0,
871
+ // accounted in parallel Promise.all
872
+ parserLoadMs: tsParserResult.loadMs
873
+ };
874
+ _runnerCache.set(cacheKey, runner);
875
+ return { runner, fromCache: false };
876
+ }
877
+ var JS_TS_FILE_PATTERNS = [
878
+ "**/*.js",
879
+ "**/*.jsx",
880
+ "**/*.ts",
881
+ "**/*.tsx",
882
+ "**/*.mts",
883
+ "**/*.cts",
884
+ "**/*.mjs",
885
+ "**/*.cjs"
886
+ ];
887
+ async function runEslint(options) {
888
+ const overallStart = Date.now();
889
+ const resolvedTargetPath = import_path.default.resolve(options.targetPath);
890
+ if (!import_fs.default.existsSync(resolvedTargetPath)) {
891
+ throw new Error(`Path not found: ${options.targetPath}`);
892
+ }
893
+ const targetStat = import_fs.default.statSync(resolvedTargetPath);
894
+ const isSingleFileTarget = targetStat.isFile();
895
+ const eslintCwd = isSingleFileTarget ? import_path.default.dirname(resolvedTargetPath) : resolvedTargetPath;
896
+ const cwd = process.cwd();
897
+ const initStart = Date.now();
898
+ const { runner, fromCache } = await getOrBuildRunner(cwd, options.preset, eslintCwd);
899
+ const initMs = Date.now() - initStart;
900
+ const timing = {
901
+ pluginLoadMs: fromCache ? 0 : runner.pluginLoadMs,
902
+ parserLoadMs: fromCache ? 0 : runner.parserLoadMs,
903
+ lintMs: 0,
904
+ processMs: 0,
905
+ fromCache
906
+ };
907
+ const hasExplicitFiles = options.files && options.files.length > 0;
908
+ const patterns = hasExplicitFiles ? options.files : isSingleFileTarget ? [import_path.default.basename(resolvedTargetPath)] : JS_TS_FILE_PATTERNS;
909
+ const lintStart = Date.now();
910
+ let rawResults;
911
+ const eslint = runner.eslint;
912
+ try {
913
+ rawResults = await eslint.lintFiles(patterns);
914
+ } catch (err) {
915
+ const error = err instanceof Error ? err : new Error(String(err));
916
+ if (isSkippablePatternError(error)) {
917
+ log.debug("Single-call lintFiles failed, falling back to per-pattern");
918
+ const perPatternResults = await Promise.allSettled(
919
+ patterns.map(async (pattern) => {
920
+ try {
921
+ return await eslint.lintFiles([pattern]);
922
+ } catch (patternErr) {
923
+ const patternError = patternErr instanceof Error ? patternErr : new Error(String(patternErr));
924
+ if (isSkippablePatternError(patternError)) {
925
+ log.debug(`Skipping pattern '${pattern}' (no lintable files)`);
926
+ return [];
927
+ }
928
+ throw patternError;
929
+ }
930
+ })
931
+ );
932
+ rawResults = perPatternResults.filter((r) => r.status === "fulfilled").flatMap((r) => r.value);
933
+ } else {
934
+ const cacheKey = `${eslintCwd}::${options.preset}`;
935
+ _runnerCache.delete(cacheKey);
936
+ throw error;
937
+ }
938
+ }
939
+ timing.lintMs = Date.now() - lintStart;
940
+ const processStart = Date.now();
941
+ const files = [];
942
+ const ecosystemIssues = [];
943
+ const parserErrors = [];
944
+ const ruleBreakdown = /* @__PURE__ */ new Map();
945
+ let totalErrors = 0;
946
+ let totalWarnings = 0;
947
+ const filesScanned = rawResults.length;
948
+ for (const result of rawResults) {
949
+ if (result.messages.length === 0) continue;
950
+ const relPath = import_path.default.relative(cwd, result.filePath);
951
+ const aiGuardIssues = [];
952
+ let fileErrors = 0;
953
+ let fileWarnings = 0;
954
+ for (const m of result.messages) {
955
+ const source = classifyMessage(m);
956
+ if (source === "parser-error") {
957
+ parserErrors.push({
958
+ filePath: relPath,
959
+ message: m.message,
960
+ line: m.line ?? 0
961
+ });
962
+ continue;
963
+ }
964
+ if (source === "ecosystem") {
965
+ ecosystemIssues.push({
966
+ type: classifyEcosystemType(m),
967
+ ruleId: m.ruleId,
968
+ message: m.message,
969
+ filePath: relPath,
970
+ line: m.line ?? 0,
971
+ column: m.column ?? 0
972
+ });
973
+ continue;
974
+ }
975
+ const ruleId = m.ruleId ?? "unknown";
976
+ const issue = {
977
+ ruleId,
978
+ severity: m.severity,
979
+ message: m.message,
980
+ line: m.line,
981
+ column: m.column,
982
+ endLine: m.endLine,
983
+ endColumn: m.endColumn,
984
+ confidence: ISSUE_CONFIDENCE[ruleId],
985
+ category: ISSUE_CATEGORY[ruleId],
986
+ asyncRiskType: ISSUE_ASYNC_RISK_TYPE[ruleId],
987
+ remediation: ISSUE_REMEDIATION[ruleId]
988
+ };
989
+ aiGuardIssues.push(issue);
990
+ if (m.severity === 2) {
991
+ fileErrors++;
992
+ totalErrors++;
993
+ } else {
994
+ fileWarnings++;
995
+ totalWarnings++;
996
+ }
997
+ ruleBreakdown.set(
998
+ issue.ruleId,
999
+ (ruleBreakdown.get(issue.ruleId) ?? 0) + 1
1000
+ );
1001
+ }
1002
+ if (aiGuardIssues.length > 0) {
1003
+ files.push({
1004
+ filePath: relPath,
1005
+ issues: aiGuardIssues,
1006
+ errorCount: fileErrors,
1007
+ warningCount: fileWarnings
1008
+ });
1009
+ }
1010
+ }
1011
+ const topFiles = [...files].sort((a, b) => b.issues.length - a.issues.length).slice(0, 10).map((f) => ({ path: f.filePath, count: f.issues.length }));
1012
+ timing.processMs = Date.now() - processStart;
1013
+ if (options.debugTiming) {
1014
+ const totalMs = Date.now() - overallStart;
1015
+ const cacheLabel = fromCache ? import_chalk2.default.green("warm cache") : import_chalk2.default.yellow("cold start");
1016
+ log.debug(`Init: ${initMs}ms (${cacheLabel})`);
1017
+ log.debug(`Lint: ${timing.lintMs}ms`);
1018
+ log.debug(`Process: ${timing.processMs}ms`);
1019
+ log.debug(`Total: ${totalMs}ms | Files: ${filesScanned} | ${filesScanned > 0 ? Math.round(totalMs / filesScanned) : 0}ms/file`);
1020
+ }
1021
+ return {
1022
+ files,
1023
+ totalErrors,
1024
+ totalWarnings,
1025
+ totalIssues: totalErrors + totalWarnings,
1026
+ filesScanned,
1027
+ ruleBreakdown,
1028
+ topFiles,
1029
+ durationMs: Date.now() - overallStart,
1030
+ timing,
1031
+ ecosystemIssues,
1032
+ parserErrors,
1033
+ tsParserAvailable: runner.tsParserAvailable
1034
+ };
1035
+ }
1036
+
1037
+ // cli/utils/sarif.ts
1038
+ var RULE_DOCS = {
1039
+ "ai-guard/no-empty-catch": { shortDesc: "Empty catch block silently swallows errors", tags: ["reliability"] },
1040
+ "ai-guard/no-broad-exception": { shortDesc: "Catching broad Exception/Error masks specific failures", tags: ["reliability"] },
1041
+ "ai-guard/no-catch-log-rethrow": { shortDesc: "Catch block logs and rethrows \u2014 use one or the other", tags: ["reliability"] },
1042
+ "ai-guard/no-catch-without-use": { shortDesc: "Caught error variable is not used", tags: ["reliability"] },
1043
+ "ai-guard/no-floating-promise": { shortDesc: "Promise not awaited or error-handled \u2014 async failures will be silently swallowed", tags: ["async", "async-reliability"] },
1044
+ "ai-guard/no-await-in-loop": { shortDesc: "Sequential await in loop \u2014 consider Promise.all for parallel execution", tags: ["async", "async-reliability", "performance"] },
1045
+ "ai-guard/no-async-without-await": { shortDesc: "Async function contains no await expressions", tags: ["async", "async-reliability"] },
1046
+ "ai-guard/no-async-array-callback": { shortDesc: "Async callback in array method may not behave as expected", tags: ["async", "async-reliability"] },
1047
+ "ai-guard/no-redundant-await": { shortDesc: "Redundant await on already-resolved value", tags: ["async"] },
1048
+ "ai-guard/no-hardcoded-secret": { shortDesc: "Hardcoded secret or credential detected", tags: ["security", "secrets"] },
1049
+ "ai-guard/no-eval-dynamic": { shortDesc: "Dynamic eval or Function constructor is dangerous", tags: ["security"] },
1050
+ "ai-guard/no-sql-string-concat": { shortDesc: "SQL query built via string concatenation \u2014 SQL injection risk", tags: ["security", "injection"] },
1051
+ "ai-guard/no-unsafe-deserialize": { shortDesc: "Unsafe deserialization of untrusted data", tags: ["security"] },
1052
+ "ai-guard/require-auth-middleware": { shortDesc: "Route handler appears to lack authentication middleware", tags: ["security", "auth"] },
1053
+ "ai-guard/require-authz-check": { shortDesc: "Handler lacks authorization check", tags: ["security", "auth"] },
1054
+ "ai-guard/no-console-in-handler": { shortDesc: "console.log in request handler \u2014 use structured logging", tags: ["ai-patterns"] },
1055
+ "ai-guard/no-duplicate-logic-block": { shortDesc: "Duplicate logic block detected", tags: ["ai-patterns"] },
1056
+ "ai-guard/no-dead-branch": { shortDesc: "Dead code branch that can never execute", tags: ["ai-patterns"] }
1057
+ };
1058
+ var BASE_DOCS_URL = "https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules";
1059
+ function confidenceToSarifLevel(confidence) {
1060
+ switch (confidence) {
1061
+ case "high":
1062
+ return "error";
1063
+ case "medium":
1064
+ return "warning";
1065
+ case "low":
1066
+ return "warning";
1067
+ case "informational":
1068
+ return "note";
1069
+ default:
1070
+ return "warning";
1071
+ }
1072
+ }
1073
+ function confidenceToRank(confidence) {
1074
+ switch (confidence) {
1075
+ case "high":
1076
+ return 90;
1077
+ case "medium":
1078
+ return 60;
1079
+ case "low":
1080
+ return 30;
1081
+ case "informational":
1082
+ return 10;
1083
+ default:
1084
+ return 30;
1085
+ }
1086
+ }
1087
+ function confidenceToGitHubSeverity(confidence) {
1088
+ switch (confidence) {
1089
+ case "high":
1090
+ return "high";
1091
+ case "medium":
1092
+ return "medium";
1093
+ case "low":
1094
+ return "low";
1095
+ case "informational":
1096
+ return "recommendation";
1097
+ default:
1098
+ return "low";
1099
+ }
1100
+ }
1101
+ function filePathToUri(filePath) {
1102
+ return filePath.replace(/\\/g, "/");
1103
+ }
1104
+ function collectUsedRuleIds(result) {
1105
+ const ids = /* @__PURE__ */ new Set();
1106
+ for (const file of result.files) {
1107
+ for (const issue of file.issues) {
1108
+ ids.add(issue.ruleId);
1109
+ }
1110
+ }
1111
+ return [...ids].sort();
1112
+ }
1113
+ function buildRuleDescriptors(ruleIds) {
1114
+ return ruleIds.map((id) => {
1115
+ const meta = RULE_DOCS[id];
1116
+ const shortName = id.replace("ai-guard/", "");
1117
+ const confidence = ISSUE_CONFIDENCE[id];
1118
+ const category = ISSUE_CATEGORY[id] ?? "AI Patterns";
1119
+ const remediation = ISSUE_REMEDIATION[id];
1120
+ const helpMarkdown = [
1121
+ `**${meta?.shortDesc ?? shortName}**`,
1122
+ "",
1123
+ remediation ? `**Fix:** ${remediation}` : "",
1124
+ "",
1125
+ `[View rule documentation](${BASE_DOCS_URL}/${shortName}.md)`
1126
+ ].filter(Boolean).join("\n");
1127
+ return {
1128
+ id,
1129
+ shortDescription: {
1130
+ text: meta?.shortDesc ?? `ai-guard rule: ${shortName}`
1131
+ },
1132
+ fullDescription: {
1133
+ text: meta?.shortDesc ?? `ai-guard rule: ${shortName}`
1134
+ },
1135
+ help: {
1136
+ text: remediation ?? meta?.shortDesc ?? shortName,
1137
+ markdown: helpMarkdown
1138
+ },
1139
+ helpUri: `${BASE_DOCS_URL}/${shortName}.md`,
1140
+ defaultConfiguration: {
1141
+ level: confidenceToSarifLevel(confidence)
1142
+ },
1143
+ properties: {
1144
+ tags: [...meta?.tags ?? ["ai-guard"], category.toLowerCase().replace(/ /g, "-")],
1145
+ confidence: confidence ?? "low",
1146
+ category,
1147
+ "problem.severity": confidenceToGitHubSeverity(confidence)
1148
+ }
1149
+ };
1150
+ });
1151
+ }
1152
+ function buildSarifResult(issue, file) {
1153
+ const confidence = ISSUE_CONFIDENCE[issue.ruleId];
1154
+ const asyncRiskType = ISSUE_ASYNC_RISK_TYPE[issue.ruleId];
1155
+ const category = ISSUE_CATEGORY[issue.ruleId];
1156
+ return {
1157
+ ruleId: issue.ruleId,
1158
+ level: confidenceToSarifLevel(confidence),
1159
+ kind: confidence === "informational" ? "informational" : "fail",
1160
+ rank: confidenceToRank(confidence),
1161
+ message: { text: issue.message },
1162
+ locations: [
1163
+ {
1164
+ physicalLocation: {
1165
+ artifactLocation: {
1166
+ uri: filePathToUri(file.filePath),
1167
+ uriBaseId: "%SRCROOT%"
1168
+ },
1169
+ region: {
1170
+ startLine: Math.max(1, issue.line),
1171
+ startColumn: issue.column > 0 ? issue.column : 1,
1172
+ ...issue.endLine !== void 0 && { endLine: issue.endLine },
1173
+ ...issue.endColumn !== void 0 && { endColumn: issue.endColumn }
1174
+ }
1175
+ },
1176
+ message: { text: issue.message }
1177
+ }
1178
+ ],
1179
+ properties: {
1180
+ confidence: confidence ?? "low",
1181
+ ...asyncRiskType && { asyncRiskType },
1182
+ ...category && { category }
1183
+ }
1184
+ };
1185
+ }
1186
+ function buildSarifLog(result, version = "1.3.0") {
1187
+ const usedRuleIds = collectUsedRuleIds(result);
1188
+ const rules = buildRuleDescriptors(usedRuleIds);
1189
+ const sarifResults = [];
1190
+ for (const file of result.files) {
1191
+ for (const issue of file.issues) {
1192
+ sarifResults.push(buildSarifResult(issue, file));
1193
+ }
1194
+ }
1195
+ return {
1196
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
1197
+ version: "2.1.0",
1198
+ runs: [
1199
+ {
1200
+ tool: {
1201
+ driver: {
1202
+ name: "ai-guard",
1203
+ version,
1204
+ informationUri: "https://github.com/YashJadhav21/eslint-plugin-ai-guard",
1205
+ rules
1206
+ }
1207
+ },
1208
+ results: sarifResults,
1209
+ properties: {
1210
+ preset: "recommended",
1211
+ filesScanned: result.filesScanned,
1212
+ durationMs: result.durationMs,
1213
+ ecosystemIssuesCount: result.ecosystemIssues.length,
1214
+ parserErrorsCount: result.parserErrors.length
1215
+ }
1216
+ }
1217
+ ]
1218
+ };
1219
+ }
1220
+ function sarifToJson(log2) {
1221
+ return JSON.stringify(log2, null, 2);
1222
+ }
1223
+
1224
+ // cli/utils/github-summary.ts
1225
+ var import_fs2 = __toESM(require("fs"));
1226
+ function escapeMarkdown(text) {
1227
+ return text.replace(/[|`*_[\]<>]/g, "\\$&");
1228
+ }
1229
+ function tierEmoji(tier) {
1230
+ switch (tier) {
1231
+ case "high":
1232
+ return "\u{1F534}";
1233
+ case "medium":
1234
+ return "\u{1F7E1}";
1235
+ case "low":
1236
+ return "\u{1F535}";
1237
+ case "informational":
1238
+ return "\u2B1C";
1239
+ default:
1240
+ return "\u2B1C";
1241
+ }
1242
+ }
1243
+ function tierLabel(tier) {
1244
+ switch (tier) {
1245
+ case "high":
1246
+ return "High";
1247
+ case "medium":
1248
+ return "Medium";
1249
+ case "low":
1250
+ return "Low";
1251
+ case "informational":
1252
+ return "Info";
1253
+ default:
1254
+ return "Low";
1255
+ }
1256
+ }
1257
+ function emitGitHubAnnotations(result) {
1258
+ if (process.env.GITHUB_ACTIONS !== "true") return;
1259
+ for (const file of result.files) {
1260
+ for (const issue of file.issues) {
1261
+ const tier = CONFIDENCE_TIER[issue.ruleId];
1262
+ if (tier === "informational") continue;
1263
+ const level = issue.severity === 2 ? "error" : "warning";
1264
+ const filePath = file.filePath.replace(/\\/g, "/");
1265
+ const shortRule = issue.ruleId.replace("ai-guard/", "");
1266
+ const parts = [`file=${filePath}`, `line=${issue.line}`];
1267
+ if (issue.endLine) parts.push(`endLine=${issue.endLine}`);
1268
+ if (issue.column > 0) parts.push(`col=${issue.column}`);
1269
+ if (issue.endColumn) parts.push(`endColumn=${issue.endColumn}`);
1270
+ parts.push(`title=ai-guard/${shortRule}`);
1271
+ const msg = issue.message.replace(/\n/g, " ");
1272
+ process.stdout.write(`::${level} ${parts.join(",")}::${msg}
1273
+ `);
1274
+ }
1275
+ }
1276
+ }
1277
+ function buildGitHubSummaryMarkdown(result, options) {
1278
+ const lines = [];
1279
+ lines.push("## AI Guard \u2014 Async Reliability Report");
1280
+ lines.push("");
1281
+ lines.push(`> **Preset:** \`${options.preset}\` &nbsp;|&nbsp; **Mode:** ${options.scanMode === "changed" ? "Changed files only" : options.scanMode === "staged" ? "Staged files" : "Full scan"}`);
1282
+ if (options.changedFilesCount !== void 0) {
1283
+ lines.push(`> **Files scanned:** ${result.filesScanned} (${options.changedFilesCount} changed)`);
1284
+ } else {
1285
+ lines.push(`> **Files scanned:** ${result.filesScanned}`);
1286
+ }
1287
+ lines.push(`> **Duration:** ${result.durationMs}ms`);
1288
+ lines.push("");
1289
+ if (result.totalIssues === 0) {
1290
+ lines.push("### \u2705 No issues found");
1291
+ lines.push("");
1292
+ lines.push("All scanned files passed AI Guard reliability checks.");
1293
+ lines.push("");
1294
+ return lines.join("\n");
1295
+ }
1296
+ const { high = 0, medium = 0, low = 0, informational = 0 } = buildSignalCounts(result);
1297
+ if (high > 0) {
1298
+ lines.push(`### \u26A0\uFE0F ${high} high-confidence issue${high !== 1 ? "s" : ""} found`);
1299
+ } else if (medium > 0) {
1300
+ lines.push(`### \u{1F535} ${medium} medium-confidence issue${medium !== 1 ? "s" : ""} found`);
1301
+ } else {
1302
+ lines.push("### \u2139\uFE0F Informational hints only");
1303
+ }
1304
+ lines.push("");
1305
+ lines.push("| Category | \u{1F534} High | \u{1F7E1} Medium | \u{1F535} Low | \u2B1C Info |");
1306
+ lines.push("|----------|---------|----------|-------|--------|");
1307
+ const categoryBreakdown = buildCategoryBreakdown(result);
1308
+ for (const [cat, counts] of Object.entries(categoryBreakdown)) {
1309
+ if (counts.high + counts.medium + counts.low + counts.informational === 0) continue;
1310
+ lines.push(`| ${cat} | ${counts.high} | ${counts.medium} | ${counts.low} | ${counts.informational} |`);
1311
+ }
1312
+ lines.push("");
1313
+ const allIssues = result.files.flatMap(
1314
+ (f) => f.issues.map((i) => ({ ...i, filePath: f.filePath }))
1315
+ );
1316
+ const actionableIssues = allIssues.filter((i) => {
1317
+ const tier = CONFIDENCE_TIER[i.ruleId];
1318
+ return tier !== "informational";
1319
+ }).sort((a, b) => {
1320
+ const tierOrder = { high: 0, medium: 1, low: 2, informational: 3 };
1321
+ const ta = CONFIDENCE_TIER[a.ruleId];
1322
+ const tb = CONFIDENCE_TIER[b.ruleId];
1323
+ return (tierOrder[ta ?? "low"] ?? 2) - (tierOrder[tb ?? "low"] ?? 2);
1324
+ }).slice(0, 10);
1325
+ if (actionableIssues.length > 0) {
1326
+ lines.push("### Top Findings");
1327
+ lines.push("");
1328
+ lines.push("| File | Line | Rule | Confidence |");
1329
+ lines.push("|------|------|------|------------|");
1330
+ for (const issue of actionableIssues) {
1331
+ const tier = CONFIDENCE_TIER[issue.ruleId];
1332
+ const shortFile = issue.filePath.replace(/\\/g, "/").split("/").slice(-2).join("/");
1333
+ const shortRule = issue.ruleId.replace("ai-guard/", "");
1334
+ lines.push(
1335
+ `| \`${escapeMarkdown(shortFile)}\` | ${issue.line} | \`${shortRule}\` | ${tierEmoji(tier)} ${tierLabel(tier)} |`
1336
+ );
1337
+ }
1338
+ lines.push("");
1339
+ }
1340
+ if (informational > 0) {
1341
+ lines.push("<details>");
1342
+ lines.push(`<summary>\u{1F4A1} ${informational} informational hint${informational !== 1 ? "s" : ""} (click to expand)</summary>`);
1343
+ lines.push("");
1344
+ lines.push("These are stylistic hints with higher false-positive rates in frameworks like Next.js and React. Review manually before acting.");
1345
+ lines.push("");
1346
+ const infoIssues = allIssues.filter((i) => {
1347
+ const tier = CONFIDENCE_TIER[i.ruleId];
1348
+ return tier === "informational";
1349
+ }).slice(0, 20);
1350
+ lines.push("| File | Line | Rule |");
1351
+ lines.push("|------|------|------|");
1352
+ for (const issue of infoIssues) {
1353
+ const shortFile = issue.filePath.replace(/\\/g, "/").split("/").slice(-2).join("/");
1354
+ const shortRule = issue.ruleId.replace("ai-guard/", "");
1355
+ lines.push(`| \`${escapeMarkdown(shortFile)}\` | ${issue.line} | \`${shortRule}\` |`);
1356
+ }
1357
+ lines.push("</details>");
1358
+ lines.push("");
1359
+ }
1360
+ if (result.ecosystemIssues.length > 0) {
1361
+ lines.push("<details>");
1362
+ lines.push(`<summary>\u{1F527} ${result.ecosystemIssues.length} ESLint config issue${result.ecosystemIssues.length !== 1 ? "s" : ""} (not ai-guard findings)</summary>`);
1363
+ lines.push("");
1364
+ lines.push("These are from your project's ESLint configuration, not from ai-guard rules. Run `ai-guard doctor` to diagnose.");
1365
+ lines.push("</details>");
1366
+ lines.push("");
1367
+ }
1368
+ lines.push("---");
1369
+ lines.push("");
1370
+ lines.push("**Next steps:**");
1371
+ if (high > 0 || medium > 0) {
1372
+ lines.push("- Review high/medium confidence findings in the **Files changed** tab");
1373
+ lines.push("- Run `ai-guard run --verbose` locally to see full details");
1374
+ }
1375
+ if (informational > 0) {
1376
+ lines.push("- Run `ai-guard run --verbose` to expand informational hints");
1377
+ }
1378
+ lines.push("- Run `ai-guard baseline` to suppress known issues and track only new ones");
1379
+ lines.push("");
1380
+ lines.push("*Powered by [eslint-plugin-ai-guard](https://github.com/YashJadhav21/eslint-plugin-ai-guard)*");
1381
+ return lines.join("\n");
1382
+ }
1383
+ function buildSignalCounts(result) {
1384
+ let high = 0, medium = 0, low = 0, informational = 0;
1385
+ for (const file of result.files) {
1386
+ for (const issue of file.issues) {
1387
+ const tier = CONFIDENCE_TIER[issue.ruleId];
1388
+ if (tier === "high") high++;
1389
+ else if (tier === "medium") medium++;
1390
+ else if (tier === "informational") informational++;
1391
+ else low++;
1392
+ }
1393
+ }
1394
+ return { high, medium, low, informational };
1395
+ }
1396
+ function buildCategoryBreakdown(result) {
1397
+ const breakdown = {};
1398
+ for (const file of result.files) {
1399
+ for (const issue of file.issues) {
1400
+ const cat = RULE_CATEGORY[issue.ruleId] ?? "Other";
1401
+ const tier = CONFIDENCE_TIER[issue.ruleId];
1402
+ if (!breakdown[cat]) breakdown[cat] = { high: 0, medium: 0, low: 0, informational: 0 };
1403
+ if (tier === "high") breakdown[cat].high++;
1404
+ else if (tier === "medium") breakdown[cat].medium++;
1405
+ else if (tier === "informational") breakdown[cat].informational++;
1406
+ else breakdown[cat].low++;
1407
+ }
1408
+ }
1409
+ return breakdown;
1410
+ }
1411
+ function writeGitHubSummary(result, options) {
1412
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
1413
+ if (!summaryPath) return;
1414
+ try {
1415
+ const markdown = buildGitHubSummaryMarkdown(result, options);
1416
+ import_fs2.default.appendFileSync(summaryPath, markdown + "\n");
1417
+ } catch {
1418
+ }
1419
+ }
1420
+ function writeGitHubOutputs(result, sarifPath) {
1421
+ const outputPath = process.env.GITHUB_OUTPUT;
1422
+ if (!outputPath) return;
1423
+ const { high, medium, low, informational } = buildSignalCounts(result);
1424
+ const outputs = [
1425
+ `issues-found=${result.totalIssues}`,
1426
+ `high-confidence-count=${high}`,
1427
+ `medium-confidence-count=${medium}`,
1428
+ `low-confidence-count=${low}`,
1429
+ `informational-count=${informational}`,
1430
+ `files-scanned=${result.filesScanned}`,
1431
+ `duration-ms=${result.durationMs}`,
1432
+ ...sarifPath ? [`sarif-file=${sarifPath}`] : []
1433
+ ];
1434
+ try {
1435
+ import_fs2.default.appendFileSync(outputPath, outputs.join("\n") + "\n");
1436
+ } catch {
1437
+ }
1438
+ }
1439
+
1440
+ // cli/utils/git-diff.ts
1441
+ var import_child_process = require("child_process");
1442
+ var import_path2 = __toESM(require("path"));
1443
+ var import_fs3 = __toESM(require("fs"));
1444
+ var DEFAULT_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"];
1445
+ var IGNORE_PREFIXES = [
1446
+ "node_modules/",
1447
+ "dist/",
1448
+ "build/",
1449
+ ".next/",
1450
+ "out/",
1451
+ "coverage/",
1452
+ ".cache/",
1453
+ ".turbo/",
1454
+ ".nx/",
1455
+ "vendor/",
1456
+ "tmp/",
1457
+ "temp/",
1458
+ "__generated__/",
1459
+ "generated/",
1460
+ "storybook-static/",
1461
+ ".expo/",
1462
+ ".svelte-kit/"
1463
+ ];
1464
+ function runGit(args, cwd) {
1465
+ try {
1466
+ return (0, import_child_process.execSync)(`git ${args}`, {
1467
+ cwd,
1468
+ encoding: "utf-8",
1469
+ stdio: ["pipe", "pipe", "pipe"]
1470
+ }).trim();
1471
+ } catch {
1472
+ return null;
1473
+ }
1474
+ }
1475
+ function isGitAvailable(cwd) {
1476
+ return runGit("rev-parse --is-inside-work-tree", cwd) === "true";
1477
+ }
1478
+ function filterFiles(rawFiles, extensions, workingDirectory, cwd) {
1479
+ let dropped = 0;
1480
+ const kept = [];
1481
+ for (const rel of rawFiles) {
1482
+ const ext = import_path2.default.extname(rel).toLowerCase();
1483
+ if (!extensions.includes(ext)) {
1484
+ dropped++;
1485
+ continue;
1486
+ }
1487
+ const normalized = rel.replace(/\\/g, "/");
1488
+ if (IGNORE_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
1489
+ dropped++;
1490
+ continue;
1491
+ }
1492
+ if (workingDirectory) {
1493
+ const wdNorm = workingDirectory.replace(/\\/g, "/").replace(/\/$/, "");
1494
+ if (!normalized.startsWith(wdNorm + "/") && normalized !== wdNorm) {
1495
+ dropped++;
1496
+ continue;
1497
+ }
1498
+ }
1499
+ const abs = import_path2.default.resolve(cwd, rel);
1500
+ if (!import_fs3.default.existsSync(abs)) {
1501
+ dropped++;
1502
+ continue;
1503
+ }
1504
+ kept.push(abs);
1505
+ }
1506
+ return { kept, dropped };
1507
+ }
1508
+ function parseFileList(output) {
1509
+ return output.split("\n").map((f) => f.trim()).filter(Boolean);
1510
+ }
1511
+ function getChangedFiles(options = {}) {
1512
+ const {
1513
+ staged = false,
1514
+ base,
1515
+ cwd = process.cwd(),
1516
+ extensions = DEFAULT_EXTENSIONS,
1517
+ workingDirectory
1518
+ } = options;
1519
+ const effectiveBase = base ?? process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF ?? ""}` : void 0;
1520
+ if (!isGitAvailable(cwd)) {
1521
+ return { files: [], mode: "fallback", totalDetected: 0, filteredOut: 0 };
1522
+ }
1523
+ let rawOutput = null;
1524
+ let mode;
1525
+ let usedBase;
1526
+ if (effectiveBase && !staged) {
1527
+ const mergeBase = runGit(`merge-base HEAD ${effectiveBase}`, cwd);
1528
+ if (mergeBase) {
1529
+ rawOutput = runGit(`diff --name-only --diff-filter=ACMRT ${mergeBase}...HEAD`, cwd);
1530
+ }
1531
+ if (!rawOutput) {
1532
+ rawOutput = runGit(`diff --name-only --diff-filter=ACMRT ${effectiveBase}`, cwd);
1533
+ }
1534
+ mode = "pr-diff";
1535
+ usedBase = effectiveBase;
1536
+ } else if (staged) {
1537
+ rawOutput = runGit("diff --name-only --cached --diff-filter=ACMRT", cwd);
1538
+ mode = "staged";
1539
+ } else {
1540
+ const staged_ = runGit("diff --name-only --cached --diff-filter=ACMRT", cwd) ?? "";
1541
+ const unstaged = runGit("diff --name-only --diff-filter=ACMRT", cwd) ?? "";
1542
+ const combined = /* @__PURE__ */ new Set([...parseFileList(staged_), ...parseFileList(unstaged)]);
1543
+ rawOutput = [...combined].join("\n");
1544
+ mode = "uncommitted";
1545
+ }
1546
+ if (!rawOutput) {
1547
+ return { files: [], mode, base: usedBase, totalDetected: 0, filteredOut: 0 };
1548
+ }
1549
+ const rawFiles = parseFileList(rawOutput);
1550
+ const { kept, dropped } = filterFiles(rawFiles, extensions, workingDirectory, cwd);
1551
+ return {
1552
+ files: kept,
1553
+ mode,
1554
+ base: usedBase,
1555
+ totalDetected: rawFiles.length,
1556
+ filteredOut: dropped
1557
+ };
1558
+ }
1559
+ function isGitHubActions() {
1560
+ return process.env.GITHUB_ACTIONS === "true";
1561
+ }
1562
+ function getGitHubBaseRef() {
1563
+ return process.env.GITHUB_BASE_REF || void 0;
1564
+ }
1565
+
1566
+ // cli/commands/run.ts
1567
+ function registerRunCommand(program2) {
1568
+ program2.command("run").description("Run ai-guard rules on your project (zero ESLint config required)").option("--path <dir>", "Directory or file to scan", ".").option("--strict", "Use the strict rule preset (all rules at error)").option("--security", "Use the security-only rule preset").option("--json", "Output results as JSON (CI-friendly)").option("--sarif", "Output results as SARIF 2.1.0 (for GitHub Code Scanning)").option("--fail-on <level>", "Fail CI on: errors (default) | high | medium | any | none", "errors").option(
1569
+ "--max-warnings <n>",
1570
+ "Fail with exit code 1 if warnings exceed this count",
1571
+ (value) => Number.parseInt(value, 10)
1572
+ ).option("--verbose", "Show all issues \u2014 disable grouping of repeated warnings").option("--quiet", "Only show errors \u2014 suppress warnings and informational hints").option("--debug-timing", "Print per-phase timing diagnostics").action(async (opts) => {
1573
+ if (opts.maxWarnings !== void 0 && (!Number.isInteger(opts.maxWarnings) || opts.maxWarnings < 0)) {
1574
+ log.blank();
1575
+ log.error("--max-warnings must be a non-negative integer.");
1576
+ log.blank();
1577
+ process.exit(1);
1578
+ return;
1579
+ }
1580
+ const preset = opts.strict ? "strict" : opts.security ? "security" : "recommended";
1581
+ if (opts.strict && opts.security && !opts.json) {
1582
+ log.warn("Both --strict and --security were provided. Using --strict.");
1583
+ log.blank();
1584
+ }
1585
+ if (!opts.json) {
1586
+ log.banner("AI GUARD");
1587
+ log.blank();
1588
+ }
1589
+ const spinner = opts.json ? null : (0, import_ora.default)({ text: "Scanning\u2026", color: "cyan" }).start();
1590
+ let result;
1591
+ try {
1592
+ result = await runEslint({
1593
+ preset,
1594
+ targetPath: opts.path,
1595
+ debugTiming: opts.debugTiming
1596
+ });
1597
+ spinner?.stop();
1598
+ } catch (err) {
1599
+ spinner?.stop();
1600
+ const msg = err instanceof Error ? err.message : String(err);
1601
+ if (msg.toLowerCase().includes("typescript") && msg.toLowerCase().includes("parser")) {
1602
+ log.blank();
1603
+ log.error("TypeScript detected but parser not found.");
1604
+ log.blank();
1605
+ log.print(` ${import_chalk3.default.bold("Install the TypeScript parser:")}`);
1606
+ log.blank();
1607
+ log.print(` ${import_chalk3.default.cyan("npm install --save-dev @typescript-eslint/parser")}`);
1608
+ log.blank();
1609
+ } else {
1610
+ log.blank();
1611
+ log.error(msg);
1612
+ log.blank();
1613
+ log.print(` ${import_chalk3.default.bold("Fix:")} Run ${import_chalk3.default.cyan("ai-guard doctor")} to diagnose your setup.`);
1614
+ log.blank();
1615
+ }
1616
+ process.exit(1);
1617
+ return;
1618
+ }
1619
+ if (isGitHubActions()) {
1620
+ emitGitHubAnnotations(result);
1621
+ writeGitHubSummary(result, { preset, scanMode: "full" });
1622
+ writeGitHubOutputs(result);
1623
+ }
1624
+ if (opts.sarif) {
1625
+ const sarifLog = buildSarifLog(result);
1626
+ console.log(sarifToJson(sarifLog));
1627
+ process.exit(resolveExitCode(result, opts.failOn ?? "errors", opts.maxWarnings));
1628
+ return;
1629
+ }
1630
+ if (opts.json) {
1631
+ const signalSummary = buildSignalSummary(result);
1632
+ const jsonOutput = {
1633
+ preset,
1634
+ scannedPath: opts.path,
1635
+ filesScanned: result.filesScanned,
1636
+ totalErrors: result.totalErrors,
1637
+ totalWarnings: result.totalWarnings,
1638
+ totalIssues: result.totalIssues,
1639
+ durationMs: result.durationMs,
1640
+ timing: result.timing,
1641
+ signalSummary,
1642
+ ruleBreakdown: Object.fromEntries(result.ruleBreakdown),
1643
+ topFiles: result.topFiles,
1644
+ files: result.files,
1645
+ ecosystemIssues: result.ecosystemIssues,
1646
+ parserErrors: result.parserErrors,
1647
+ tsParserAvailable: result.tsParserAvailable
1648
+ };
1649
+ console.log(JSON.stringify(jsonOutput, null, 2));
1650
+ process.exit(resolveExitCode(result, opts.failOn ?? "errors", opts.maxWarnings));
1651
+ return;
1652
+ }
1653
+ log.scanStats(result.filesScanned, result.files.length, result.durationMs, preset);
1654
+ if (!result.tsParserAvailable) {
1655
+ log.print(
1656
+ import_chalk3.default.dim(` \u2139 TypeScript parser not found \u2014 .ts/.tsx files use espree fallback`) + import_chalk3.default.dim(` (install @typescript-eslint/parser for full TS support)`)
1657
+ );
1658
+ }
1659
+ log.blank();
1660
+ if (result.totalIssues === 0 && result.ecosystemIssues.length === 0 && result.parserErrors.length === 0) {
1661
+ log.print(
1662
+ ` ${import_chalk3.default.green("\u2714")} ${import_chalk3.default.bold.green("No AI issues found")} ${import_chalk3.default.gray(`\u2014 ${result.filesScanned} file${result.filesScanned !== 1 ? "s" : ""} scanned, all clean`)}`
1663
+ );
1664
+ log.blank();
1665
+ process.exit(0);
1666
+ return;
1667
+ }
1668
+ if (result.totalIssues === 0) {
1669
+ log.print(
1670
+ ` ${import_chalk3.default.green("\u2714")} ${import_chalk3.default.bold.green("No AI issues found")} ${import_chalk3.default.gray(`\u2014 ${result.filesScanned} file${result.filesScanned !== 1 ? "s" : ""} scanned, all clean`)}`
1671
+ );
1672
+ log.blank();
1673
+ }
1674
+ if (result.totalIssues > 0) {
1675
+ const verbose = opts.verbose ?? false;
1676
+ const quiet = opts.quiet ?? false;
1677
+ const { actionableFiles, informationalFiles } = partitionByTier(result, quiet);
1678
+ log.section("Summary by Category");
1679
+ const categoryErrors = {};
1680
+ const categoryWarnings = {};
1681
+ for (const file of actionableFiles) {
1682
+ for (const issue of file.issues) {
1683
+ const cat = RULE_CATEGORY[issue.ruleId] ?? "Other";
1684
+ if (issue.severity === 2) {
1685
+ categoryErrors[cat] = (categoryErrors[cat] ?? 0) + 1;
1686
+ } else {
1687
+ categoryWarnings[cat] = (categoryWarnings[cat] ?? 0) + 1;
1688
+ }
1689
+ }
1690
+ }
1691
+ const allCategories = /* @__PURE__ */ new Set([
1692
+ ...CATEGORY_ORDER,
1693
+ ...Object.keys(categoryErrors),
1694
+ ...Object.keys(categoryWarnings)
1695
+ ]);
1696
+ const sortedCategories = [...allCategories].filter(
1697
+ (cat) => (categoryErrors[cat] ?? 0) + (categoryWarnings[cat] ?? 0) > 0
1698
+ );
1699
+ sortedCategories.sort((a, b) => {
1700
+ const ai = CATEGORY_ORDER.indexOf(a);
1701
+ const bi = CATEGORY_ORDER.indexOf(b);
1702
+ if (ai !== -1 && bi !== -1) return ai - bi;
1703
+ if (ai !== -1) return -1;
1704
+ if (bi !== -1) return 1;
1705
+ return a.localeCompare(b);
1706
+ });
1707
+ for (const cat of sortedCategories) {
1708
+ const icon = CATEGORY_ICONS[cat] ?? "\u26AA";
1709
+ log.category(icon, cat, categoryErrors[cat] ?? 0, categoryWarnings[cat] ?? 0);
1710
+ }
1711
+ log.blank();
1712
+ log.print(
1713
+ ` ${import_chalk3.default.bold("Total:")} ${formatIssueCount(result.totalErrors, result.totalWarnings)}`
1714
+ );
1715
+ log.blank();
1716
+ if (result.ruleBreakdown.size > 0) {
1717
+ log.section("By Rule");
1718
+ const sorted = [...result.ruleBreakdown.entries()].sort(
1719
+ (a, b) => b[1] - a[1]
1720
+ );
1721
+ for (const [rule, count] of sorted) {
1722
+ const shortRule = rule.replace(/^ai-guard\//, "");
1723
+ const tier = CONFIDENCE_TIER[rule];
1724
+ const tierLabel2 = tier === "high" ? import_chalk3.default.red.dim("[high]") : tier === "medium" ? import_chalk3.default.yellow.dim("[medium]") : tier === "informational" ? import_chalk3.default.gray.dim("[info]") : import_chalk3.default.gray.dim("[low]");
1725
+ if (quiet && tier === "informational") continue;
1726
+ log.print(
1727
+ ` ${import_chalk3.default.gray("\u2022")} ${import_chalk3.default.yellow(shortRule)} ${tierLabel2}${import_chalk3.default.gray(":")} ${import_chalk3.default.white(String(count))}`
1728
+ );
1729
+ }
1730
+ log.blank();
1731
+ }
1732
+ if (result.topFiles.length > 0) {
1733
+ log.section("Top Files");
1734
+ for (const { path: fp, count } of result.topFiles) {
1735
+ log.print(
1736
+ ` ${import_chalk3.default.gray("\u2022")} ${import_chalk3.default.white(fp)} ${import_chalk3.default.gray(`(${count})`)}`
1737
+ );
1738
+ }
1739
+ log.blank();
1740
+ }
1741
+ if (actionableFiles.length > 0) {
1742
+ log.section("Issues by File");
1743
+ log.blank();
1744
+ renderIssuesByFile({ ...result, files: actionableFiles }, verbose, quiet);
1745
+ }
1746
+ const totalInfoCount = informationalFiles.reduce((n, f) => n + f.issues.length, 0);
1747
+ if (totalInfoCount > 0 && !quiet) {
1748
+ if (verbose) {
1749
+ log.section("Informational Hints");
1750
+ log.print(import_chalk3.default.dim(` Stylistic hints with higher false-positive rate \u2014 review before acting.`));
1751
+ log.blank();
1752
+ renderIssuesByFile({ ...result, files: informationalFiles }, true, false);
1753
+ } else {
1754
+ log.blank();
1755
+ log.print(
1756
+ ` ${import_chalk3.default.gray("\u25B8")} ${import_chalk3.default.gray(`${totalInfoCount} informational hint${totalInfoCount !== 1 ? "s" : ""} \u2014 run with ${import_chalk3.default.cyan("--verbose")} to expand`)}`
1757
+ );
1758
+ }
1759
+ }
1760
+ }
1761
+ if (result.ecosystemIssues.length > 0) {
1762
+ log.ecosystemSection("ESLint Config Issues");
1763
+ log.print(
1764
+ import_chalk3.default.dim(` These issues come from your project's ESLint config, not from ai-guard.`)
1765
+ );
1766
+ log.blank();
1767
+ const seen = /* @__PURE__ */ new Map();
1768
+ for (const issue of result.ecosystemIssues) {
1769
+ const key = `${issue.ruleId ?? "null"}::${issue.message}`;
1770
+ const existing = seen.get(key);
1771
+ if (existing) {
1772
+ existing.count++;
1773
+ } else {
1774
+ seen.set(key, { issue, count: 1 });
1775
+ }
1776
+ }
1777
+ for (const { issue, count } of seen.values()) {
1778
+ log.ecosystemIssue(issue, count);
1779
+ }
1780
+ log.blank();
1781
+ }
1782
+ if (result.parserErrors.length > 0) {
1783
+ log.ecosystemSection("Parser Errors");
1784
+ log.print(
1785
+ import_chalk3.default.dim(` These files could not be parsed \u2014 not ai-guard findings.`)
1786
+ );
1787
+ log.blank();
1788
+ for (const pe of result.parserErrors) {
1789
+ log.parserError(pe);
1790
+ }
1791
+ if (!result.tsParserAvailable) {
1792
+ log.blank();
1793
+ log.print(
1794
+ ` ${import_chalk3.default.cyan("\u2192 Fix:")} ${import_chalk3.default.yellow("npm install --save-dev @typescript-eslint/parser")}`
1795
+ );
1796
+ log.print(
1797
+ import_chalk3.default.dim(` TypeScript parser resolves most .ts/.tsx parse failures.`)
1798
+ );
1799
+ }
1800
+ log.blank();
1801
+ }
1802
+ log.divider();
1803
+ log.blank();
1804
+ log.section("Next Steps");
1805
+ if (result.totalIssues > 0) {
1806
+ log.info(`Run ${import_chalk3.default.cyan("ai-guard baseline")} to save these issues and track only new ones`);
1807
+ log.info(`Run ${import_chalk3.default.cyan("ai-guard report")} to generate a shareable HTML report`);
1808
+ log.info(`Run ${import_chalk3.default.cyan("ai-guard ignore")} to suppress dist/build noise`);
1809
+ }
1810
+ if (result.ecosystemIssues.length > 0) {
1811
+ log.info(`Run ${import_chalk3.default.cyan("ai-guard doctor")} to diagnose ESLint config issues`);
1812
+ }
1813
+ if (!opts.verbose && hasGroupedWarnings(result)) {
1814
+ log.info(`Run with ${import_chalk3.default.cyan("--verbose")} to expand grouped warnings`);
1815
+ }
1816
+ log.blank();
1817
+ process.exit(getRunExitCode(result, opts.maxWarnings));
1818
+ });
1819
+ }
1820
+ var GROUP_THRESHOLD = 4;
1821
+ function partitionByTier(result, quiet) {
1822
+ const actionableFiles = [];
1823
+ const informationalFiles = [];
1824
+ for (const file of result.files) {
1825
+ const actionableIssues = file.issues.filter((i) => {
1826
+ const tier = CONFIDENCE_TIER[i.ruleId];
1827
+ if (quiet && (tier === "informational" || i.severity === 1)) return false;
1828
+ return tier !== "informational";
1829
+ });
1830
+ const infoIssues = file.issues.filter((i) => {
1831
+ const tier = CONFIDENCE_TIER[i.ruleId];
1832
+ return tier === "informational";
1833
+ });
1834
+ if (actionableIssues.length > 0) {
1835
+ actionableFiles.push({ ...file, issues: actionableIssues });
1836
+ }
1837
+ if (infoIssues.length > 0) {
1838
+ informationalFiles.push({ ...file, issues: infoIssues });
1839
+ }
1840
+ }
1841
+ return { actionableFiles, informationalFiles };
1842
+ }
1843
+ function buildSignalSummary(result) {
1844
+ let high = 0, medium = 0, low = 0, informational = 0;
1845
+ for (const file of result.files) {
1846
+ for (const issue of file.issues) {
1847
+ const tier = CONFIDENCE_TIER[issue.ruleId];
1848
+ if (tier === "high") high++;
1849
+ else if (tier === "medium") medium++;
1850
+ else if (tier === "informational") informational++;
1851
+ else low++;
1852
+ }
1853
+ }
1854
+ return {
1855
+ high,
1856
+ medium,
1857
+ low,
1858
+ informational,
1859
+ ecosystemIssues: result.ecosystemIssues.length,
1860
+ parserErrors: result.parserErrors.length
1861
+ };
1862
+ }
1863
+ function hasGroupedWarnings(result) {
1864
+ for (const file of result.files) {
1865
+ const ruleCounts = /* @__PURE__ */ new Map();
1866
+ for (const issue of file.issues) {
1867
+ ruleCounts.set(issue.ruleId, (ruleCounts.get(issue.ruleId) ?? 0) + 1);
1868
+ }
1869
+ for (const count of ruleCounts.values()) {
1870
+ if (count >= GROUP_THRESHOLD) return true;
1871
+ }
1872
+ }
1873
+ return false;
1874
+ }
1875
+ function renderIssuesByFile(result, verbose, quiet = false) {
1876
+ for (const file of result.files) {
1877
+ log.print(
1878
+ ` ${import_chalk3.default.bold.white(file.filePath)} ` + import_chalk3.default.gray(
1879
+ `(${file.errorCount} error${file.errorCount !== 1 ? "s" : ""}, ${file.warningCount} warning${file.warningCount !== 1 ? "s" : ""})`
1880
+ )
1881
+ );
1882
+ if (!verbose) {
1883
+ const ruleCounts = /* @__PURE__ */ new Map();
1884
+ for (const issue of file.issues) {
1885
+ const arr = ruleCounts.get(issue.ruleId) ?? [];
1886
+ arr.push(issue);
1887
+ ruleCounts.set(issue.ruleId, arr);
1888
+ }
1889
+ for (const issues of ruleCounts.values()) {
1890
+ const tier = CONFIDENCE_TIER[issues[0].ruleId] ?? "low";
1891
+ if (tier === "low" && issues.length >= GROUP_THRESHOLD) {
1892
+ const sev = issues[0].severity === 2 ? import_chalk3.default.red("error") : import_chalk3.default.yellow(" warn");
1893
+ const shortRule = import_chalk3.default.gray(issues[0].ruleId.replace(/^ai-guard\//, ""));
1894
+ log.print(
1895
+ ` ${sev} ${import_chalk3.default.white(issues[0].message)} ${shortRule}`
1896
+ );
1897
+ log.print(
1898
+ import_chalk3.default.dim(` \u21B3 ${issues.length} occurrences \u2014 run with --verbose to expand`)
1899
+ );
1900
+ } else {
1901
+ for (const issue of issues) {
1902
+ renderSingleIssue(issue);
1903
+ }
673
1904
  }
674
- throw error;
675
1905
  }
676
- })
1906
+ } else {
1907
+ for (const issue of file.issues) {
1908
+ renderSingleIssue(issue);
1909
+ }
1910
+ }
1911
+ log.blank();
1912
+ }
1913
+ }
1914
+ function renderSingleIssue(issue) {
1915
+ const sev = issue.severity === 2 ? import_chalk3.default.red("error") : import_chalk3.default.yellow(" warn");
1916
+ const loc = import_chalk3.default.gray(`${String(issue.line)}:${String(issue.column)}`).padEnd(12);
1917
+ const ruleShort = import_chalk3.default.gray(issue.ruleId.replace(/^ai-guard\//, ""));
1918
+ const tier = CONFIDENCE_TIER[issue.ruleId];
1919
+ const tierTag = tier === "informational" ? import_chalk3.default.dim(" [info]") : "";
1920
+ log.print(
1921
+ ` ${loc} ${sev} ${import_chalk3.default.white(issue.message)}${tierTag} ${ruleShort}`
677
1922
  );
678
- const rawResults = perPatternResults.flat();
679
- const durationMs = Date.now() - startMs;
680
- const files = [];
681
- const ruleBreakdown = /* @__PURE__ */ new Map();
682
- let totalErrors = 0;
683
- let totalWarnings = 0;
684
- const filesScanned = rawResults.length;
685
- for (const result of rawResults) {
686
- if (result.messages.length === 0) continue;
687
- const issues = result.messages.map((m) => ({
688
- ruleId: m.ruleId ?? "unknown",
689
- severity: m.severity,
690
- message: m.message,
691
- line: m.line,
692
- column: m.column
693
- }));
694
- const errorCount = result.errorCount;
695
- const warningCount = result.warningCount;
696
- totalErrors += errorCount;
697
- totalWarnings += warningCount;
698
- for (const issue of issues) {
699
- ruleBreakdown.set(
700
- issue.ruleId,
701
- (ruleBreakdown.get(issue.ruleId) ?? 0) + 1
702
- );
1923
+ }
1924
+ function getRunExitCode(result, maxWarnings) {
1925
+ if (result.totalErrors > 0) return 1;
1926
+ if (maxWarnings !== void 0 && result.totalWarnings > maxWarnings) return 1;
1927
+ return 0;
1928
+ }
1929
+ function formatIssueCount(errors, warnings) {
1930
+ const parts = [];
1931
+ if (errors > 0) parts.push(import_chalk3.default.red.bold(`${errors} error${errors !== 1 ? "s" : ""}`));
1932
+ if (warnings > 0) parts.push(import_chalk3.default.yellow.bold(`${warnings} warning${warnings !== 1 ? "s" : ""}`));
1933
+ return parts.join(import_chalk3.default.gray(" \xB7 "));
1934
+ }
1935
+ function resolveExitCode(result, failOn, maxWarnings) {
1936
+ if (maxWarnings !== void 0 && result.totalWarnings > maxWarnings) return 1;
1937
+ switch (failOn) {
1938
+ case "none":
1939
+ return 0;
1940
+ case "errors":
1941
+ return result.totalErrors > 0 ? 1 : 0;
1942
+ case "high": {
1943
+ const high = result.files.flatMap((f) => f.issues).filter((i) => CONFIDENCE_TIER[i.ruleId] === "high").length;
1944
+ return high > 0 ? 1 : 0;
1945
+ }
1946
+ case "medium": {
1947
+ const highMed = result.files.flatMap((f) => f.issues).filter((i) => CONFIDENCE_TIER[i.ruleId] === "high" || CONFIDENCE_TIER[i.ruleId] === "medium").length;
1948
+ return highMed > 0 ? 1 : 0;
703
1949
  }
704
- const relPath = import_path.default.relative(process.cwd(), result.filePath);
705
- files.push({ filePath: relPath, issues, errorCount, warningCount });
1950
+ case "any": {
1951
+ const actionable = result.files.flatMap((f) => f.issues).filter((i) => CONFIDENCE_TIER[i.ruleId] !== "informational").length;
1952
+ return actionable > 0 ? 1 : 0;
1953
+ }
1954
+ default:
1955
+ return getRunExitCode(result, maxWarnings);
706
1956
  }
707
- const topFiles = [...files].sort((a, b) => b.issues.length - a.issues.length).slice(0, 10).map((f) => ({ path: f.filePath, count: f.issues.length }));
708
- return {
709
- files,
710
- totalErrors,
711
- totalWarnings,
712
- totalIssues: totalErrors + totalWarnings,
713
- filesScanned,
714
- ruleBreakdown,
715
- topFiles,
716
- durationMs
717
- };
718
1957
  }
719
1958
 
720
- // cli/commands/run.ts
721
- function registerRunCommand(program2) {
722
- program2.command("run").description("Run ai-guard rules on your project (zero ESLint config required)").option("--path <dir>", "Directory or file to scan", ".").option("--strict", "Use the strict rule preset (all rules at error)").option("--security", "Use the security-only rule preset").option("--json", "Output results as JSON (CI-friendly)").option(
1959
+ // cli/commands/changed.ts
1960
+ var import_path3 = __toESM(require("path"));
1961
+ var import_fs4 = __toESM(require("fs"));
1962
+ var import_ora2 = __toESM(require("ora"));
1963
+ var import_chalk4 = __toESM(require("chalk"));
1964
+ function registerChangedCommand(program2) {
1965
+ program2.command("changed").description("Scan only changed files in the current PR or working tree (fastest CI mode)").option("--pr", "Scan files changed vs base branch (auto-detects GITHUB_BASE_REF in CI)").option("--staged", "Scan only staged (git add'd) files").option("--base <branch>", "Base branch to diff against (e.g. main, origin/main)").option("--path <dir>", "Root path / working directory for scanning", ".").option("--strict", "Use the strict rule preset (all rules at error)").option("--security", "Use the security-only rule preset").option("--fail-on <level>", "Fail CI on: high | medium | any | none", "high").option("--json", "Output results as JSON").option("--sarif", "Output results as SARIF 2.1.0 (for GitHub Code Scanning)").option("--sarif-output <file>", "Write SARIF to file instead of stdout").option("--github-summary", "Write GitHub step summary (auto-enabled in GitHub Actions)").option(
723
1966
  "--max-warnings <n>",
724
- "Fail with exit code 1 if warnings exceed this count",
725
- (value) => Number.parseInt(value, 10)
726
- ).action(async (opts) => {
727
- if (opts.maxWarnings !== void 0 && (!Number.isInteger(opts.maxWarnings) || opts.maxWarnings < 0)) {
728
- log.blank();
729
- log.error("--max-warnings must be a non-negative integer.");
730
- log.blank();
731
- process.exit(1);
732
- return;
733
- }
1967
+ "Fail if warnings exceed this count",
1968
+ (v) => Number.parseInt(v, 10)
1969
+ ).option("--verbose", "Expand all findings (disable grouping)").option("--quiet", "Show errors only").option("--debug-timing", "Print per-phase timing diagnostics").action(async (opts) => {
734
1970
  const preset = opts.strict ? "strict" : opts.security ? "security" : "recommended";
735
- if (opts.strict && opts.security && !opts.json) {
736
- log.warn("Both --strict and --security were provided. Using --strict.");
737
- log.blank();
738
- }
739
- if (!opts.json) {
1971
+ const inCI = isGitHubActions();
1972
+ const baseRef = opts.base ?? (opts.pr || inCI ? getGitHubBaseRef() : void 0);
1973
+ if (!opts.json && !opts.sarif) {
740
1974
  log.banner("AI GUARD");
741
- log.blank();
742
1975
  }
743
- const spinner = opts.json ? null : (0, import_ora.default)({ text: "Scanning\u2026", color: "cyan" }).start();
744
- let result;
745
- try {
746
- result = await runEslint({ preset, targetPath: opts.path });
747
- spinner?.stop();
748
- } catch (err) {
1976
+ const cwd = process.cwd();
1977
+ const resolvedPath = import_path3.default.resolve(opts.path);
1978
+ const workingDirectory = opts.path !== "." ? opts.path : void 0;
1979
+ const spinner = opts.json || opts.sarif ? null : (0, import_ora2.default)({ text: import_chalk4.default.dim("Detecting changed files\u2026"), color: "cyan" }).start();
1980
+ const changedResult = getChangedFiles({
1981
+ staged: opts.staged ?? false,
1982
+ base: baseRef ? `origin/${baseRef}` : opts.base,
1983
+ cwd,
1984
+ workingDirectory
1985
+ });
1986
+ if (changedResult.files.length === 0) {
749
1987
  spinner?.stop();
750
- const msg = err instanceof Error ? err.message : String(err);
751
- if (msg.toLowerCase().includes("typescript") && msg.toLowerCase().includes("parser")) {
752
- log.blank();
753
- log.error("TypeScript detected but parser not found.");
754
- log.blank();
755
- log.print(` ${import_chalk2.default.bold("Install the TypeScript parser:")}`);
756
- log.blank();
757
- log.print(` ${import_chalk2.default.cyan("npm install --save-dev @typescript-eslint/parser")}`);
758
- log.blank();
759
- } else {
760
- log.blank();
761
- log.error(msg);
1988
+ if (changedResult.mode === "fallback") {
1989
+ if (!opts.json && !opts.sarif) {
1990
+ log.warn("Git not available. Falling back to full scan.");
1991
+ log.blank();
1992
+ }
1993
+ const result2 = await runEslint({
1994
+ preset,
1995
+ targetPath: resolvedPath.toString(),
1996
+ debugTiming: opts.debugTiming
1997
+ });
1998
+ handleResults(result2, opts, preset, "full", 0, inCI);
1999
+ return;
2000
+ }
2001
+ if (!opts.json && !opts.sarif) {
762
2002
  log.blank();
763
- log.print(` ${import_chalk2.default.bold("Fix:")} Run ${import_chalk2.default.cyan("ai-guard doctor")} to diagnose your setup.`);
2003
+ log.print(` ${import_chalk4.default.green("\u2714")} No changed JS/TS files detected \u2014 nothing to scan`);
2004
+ if (changedResult.filteredOut > 0) {
2005
+ log.print(import_chalk4.default.dim(` (${changedResult.filteredOut} changed file${changedResult.filteredOut !== 1 ? "s" : ""} skipped \u2014 not JS/TS)`));
2006
+ }
764
2007
  log.blank();
2008
+ } else if (opts.json) {
2009
+ console.log(JSON.stringify({
2010
+ preset,
2011
+ scanMode: changedResult.mode,
2012
+ changedFiles: 0,
2013
+ filesScanned: 0,
2014
+ totalIssues: 0,
2015
+ totalErrors: 0,
2016
+ totalWarnings: 0,
2017
+ signalSummary: { high: 0, medium: 0, low: 0, informational: 0, ecosystemIssues: 0, parserErrors: 0 }
2018
+ }, null, 2));
765
2019
  }
766
- process.exit(1);
2020
+ process.exit(0);
767
2021
  return;
768
2022
  }
769
- if (opts.json) {
770
- const jsonOutput = {
771
- preset,
772
- scannedPath: opts.path,
773
- filesScanned: result.filesScanned,
774
- totalErrors: result.totalErrors,
775
- totalWarnings: result.totalWarnings,
776
- totalIssues: result.totalIssues,
777
- durationMs: result.durationMs,
778
- ruleBreakdown: Object.fromEntries(result.ruleBreakdown),
779
- topFiles: result.topFiles,
780
- files: result.files
781
- };
782
- console.log(JSON.stringify(jsonOutput, null, 2));
783
- process.exit(getRunExitCode(result, opts.maxWarnings));
784
- return;
2023
+ if (spinner) {
2024
+ spinner.text = import_chalk4.default.dim(`Scanning ${changedResult.files.length} changed file${changedResult.files.length !== 1 ? "s" : ""}\u2026`);
785
2025
  }
786
- log.scanStats(result.filesScanned, result.files.length, result.durationMs, preset);
787
- log.blank();
788
- if (result.totalIssues === 0) {
789
- log.print(
790
- ` ${import_chalk2.default.green("\u2714")} ${import_chalk2.default.bold.green("No AI issues found")} ${import_chalk2.default.gray(`\u2014 ${result.filesScanned} file${result.filesScanned !== 1 ? "s" : ""} scanned, all clean`)}`
791
- );
792
- log.blank();
793
- process.exit(0);
794
- return;
2026
+ const result = await runEslint({
2027
+ preset,
2028
+ targetPath: resolvedPath.toString(),
2029
+ files: changedResult.files,
2030
+ debugTiming: opts.debugTiming
2031
+ });
2032
+ spinner?.stop();
2033
+ handleResults(result, opts, preset, changedResult.mode === "pr-diff" ? "changed" : changedResult.mode === "staged" ? "staged" : "changed", changedResult.files.length, inCI);
2034
+ });
2035
+ }
2036
+ function handleResults(result, opts, preset, scanMode, changedFilesCount, inCI) {
2037
+ if (inCI || opts.githubSummary) {
2038
+ writeGitHubSummary(result, { preset, scanMode, changedFilesCount });
2039
+ writeGitHubOutputs(result, opts.sarifOutput);
2040
+ emitGitHubAnnotations(result);
2041
+ }
2042
+ if (opts.sarif) {
2043
+ const sarifLog = buildSarifLog(result);
2044
+ const sarifJson = sarifToJson(sarifLog);
2045
+ if (opts.sarifOutput) {
2046
+ import_fs4.default.writeFileSync(opts.sarifOutput, sarifJson, "utf-8");
2047
+ } else {
2048
+ console.log(sarifJson);
795
2049
  }
2050
+ process.exit(getFailOnExitCode(result, opts.failOn, opts.maxWarnings));
2051
+ return;
2052
+ }
2053
+ if (opts.json) {
2054
+ const signalSummary = buildSignalSummary(result);
2055
+ const jsonOutput = {
2056
+ version: "1.3.0",
2057
+ preset,
2058
+ scanMode,
2059
+ scannedPath: opts.path,
2060
+ filesScanned: result.filesScanned,
2061
+ changedFilesCount,
2062
+ totalErrors: result.totalErrors,
2063
+ totalWarnings: result.totalWarnings,
2064
+ totalIssues: result.totalIssues,
2065
+ durationMs: result.durationMs,
2066
+ signalSummary,
2067
+ ruleBreakdown: Object.fromEntries(result.ruleBreakdown),
2068
+ topFiles: result.topFiles,
2069
+ files: result.files,
2070
+ ecosystemIssues: result.ecosystemIssues,
2071
+ parserErrors: result.parserErrors,
2072
+ tsParserAvailable: result.tsParserAvailable
2073
+ };
2074
+ console.log(JSON.stringify(jsonOutput, null, 2));
2075
+ process.exit(getFailOnExitCode(result, opts.failOn, opts.maxWarnings));
2076
+ return;
2077
+ }
2078
+ const scanModeLabel = scanMode === "changed" ? "Changed files" : scanMode === "staged" ? "Staged files" : "Full scan";
2079
+ const modeTag = changedFilesCount > 0 ? `${changedFilesCount} changed` : "";
2080
+ log.summary(
2081
+ result.filesScanned,
2082
+ result.totalIssues,
2083
+ result.files.filter((f) => f.issues.length > 0).length,
2084
+ result.durationMs,
2085
+ `${preset} \xB7 ${scanModeLabel}${modeTag ? ` (${modeTag})` : ""}`
2086
+ );
2087
+ if (result.totalIssues === 0) {
2088
+ log.blank();
2089
+ log.success(`No AI issues found \u2014 ${result.filesScanned} file${result.filesScanned !== 1 ? "s" : ""} scanned, all clean`);
2090
+ log.blank();
2091
+ } else {
2092
+ const verbose = opts.verbose ?? false;
2093
+ const quiet = opts.quiet ?? false;
2094
+ const { actionableFiles, informationalFiles } = partitionByTier(result, quiet);
796
2095
  log.section("Summary by Category");
797
2096
  const categoryErrors = {};
798
2097
  const categoryWarnings = {};
799
- for (const file of result.files) {
2098
+ for (const file of actionableFiles) {
800
2099
  for (const issue of file.issues) {
801
2100
  const cat = RULE_CATEGORY[issue.ruleId] ?? "Other";
802
- if (issue.severity === 2) {
803
- categoryErrors[cat] = (categoryErrors[cat] ?? 0) + 1;
804
- } else {
805
- categoryWarnings[cat] = (categoryWarnings[cat] ?? 0) + 1;
806
- }
2101
+ if (issue.severity === 2) categoryErrors[cat] = (categoryErrors[cat] ?? 0) + 1;
2102
+ else categoryWarnings[cat] = (categoryWarnings[cat] ?? 0) + 1;
807
2103
  }
808
2104
  }
809
- const allCategories = /* @__PURE__ */ new Set([
810
- ...CATEGORY_ORDER,
811
- ...Object.keys(categoryErrors),
812
- ...Object.keys(categoryWarnings)
813
- ]);
814
- const sortedCategories = [...allCategories].filter(
815
- (cat) => (categoryErrors[cat] ?? 0) + (categoryWarnings[cat] ?? 0) > 0
816
- );
817
- sortedCategories.sort((a, b) => {
818
- const ai = CATEGORY_ORDER.indexOf(a);
819
- const bi = CATEGORY_ORDER.indexOf(b);
2105
+ const allCategories = /* @__PURE__ */ new Set([...CATEGORY_ORDER, ...Object.keys(categoryErrors), ...Object.keys(categoryWarnings)]);
2106
+ const sortedCats = [...allCategories].filter((c) => (categoryErrors[c] ?? 0) + (categoryWarnings[c] ?? 0) > 0).sort((a, b) => {
2107
+ const ai = CATEGORY_ORDER.indexOf(a), bi = CATEGORY_ORDER.indexOf(b);
820
2108
  if (ai !== -1 && bi !== -1) return ai - bi;
821
2109
  if (ai !== -1) return -1;
822
2110
  if (bi !== -1) return 1;
823
2111
  return a.localeCompare(b);
824
2112
  });
825
- for (const cat of sortedCategories) {
2113
+ for (const cat of sortedCats) {
826
2114
  const icon = CATEGORY_ICONS[cat] ?? "\u26AA";
827
2115
  log.category(icon, cat, categoryErrors[cat] ?? 0, categoryWarnings[cat] ?? 0);
828
2116
  }
829
2117
  log.blank();
830
- log.print(
831
- ` ${import_chalk2.default.bold("Total:")} ${formatIssueCount(result.totalErrors, result.totalWarnings)}`
832
- );
2118
+ log.print(` ${import_chalk4.default.bold("Total:")} ${formatIssueCount(result.totalErrors, result.totalWarnings)}`);
833
2119
  log.blank();
834
- if (result.ruleBreakdown.size > 0) {
835
- log.section("By Rule");
836
- const sorted = [...result.ruleBreakdown.entries()].sort(
837
- (a, b) => b[1] - a[1]
838
- );
839
- for (const [rule, count] of sorted) {
840
- const shortRule = rule.replace(/^ai-guard\//, "");
841
- log.print(
842
- ` ${import_chalk2.default.gray("\u2022")} ${import_chalk2.default.yellow(shortRule)}${import_chalk2.default.gray(":")} ${import_chalk2.default.white(String(count))}`
843
- );
844
- }
2120
+ if (actionableFiles.length > 0) {
2121
+ log.section("Issues by File");
845
2122
  log.blank();
2123
+ renderIssuesByFile({ ...result, files: actionableFiles }, verbose, quiet);
846
2124
  }
847
- if (result.topFiles.length > 0) {
848
- log.section("Top Files");
849
- for (const { path: fp, count } of result.topFiles) {
850
- log.print(
851
- ` ${import_chalk2.default.gray("\u2022")} ${import_chalk2.default.white(fp)} ${import_chalk2.default.gray(`(${count})`)}`
852
- );
2125
+ const totalInfoCount = informationalFiles.reduce((n, f) => n + f.issues.length, 0);
2126
+ if (totalInfoCount > 0 && !quiet) {
2127
+ if (verbose) {
2128
+ log.section("Informational Hints");
2129
+ log.print(import_chalk4.default.dim(" Stylistic hints with higher false-positive rate \u2014 review before acting."));
2130
+ log.blank();
2131
+ renderIssuesByFile({ ...result, files: informationalFiles }, true, false);
2132
+ } else {
2133
+ log.blank();
2134
+ log.print(` ${import_chalk4.default.gray("\u25B8")} ${import_chalk4.default.gray(`${totalInfoCount} informational hint${totalInfoCount !== 1 ? "s" : ""} \u2014 run with ${import_chalk4.default.cyan("--verbose")} to expand`)}`);
853
2135
  }
854
- log.blank();
855
2136
  }
856
- log.section("Issues by File");
2137
+ }
2138
+ if (result.ecosystemIssues.length > 0) {
2139
+ log.ecosystemSection("ESLint Config Issues");
2140
+ log.print(import_chalk4.default.dim(" These issues come from your project's ESLint config, not from ai-guard."));
857
2141
  log.blank();
858
- for (const file of result.files) {
859
- log.print(
860
- ` ${import_chalk2.default.bold.white(file.filePath)} ` + import_chalk2.default.gray(
861
- `(${file.errorCount} error${file.errorCount !== 1 ? "s" : ""}, ${file.warningCount} warning${file.warningCount !== 1 ? "s" : ""})`
862
- )
863
- );
864
- for (const issue of file.issues) {
865
- const sev = issue.severity === 2 ? import_chalk2.default.red("error") : import_chalk2.default.yellow(" warn");
866
- const loc = import_chalk2.default.gray(`${String(issue.line)}:${String(issue.column)}`).padEnd(12);
867
- const ruleShort = import_chalk2.default.gray(issue.ruleId.replace(/^ai-guard\//, ""));
868
- log.print(
869
- ` ${loc} ${sev} ${import_chalk2.default.white(issue.message)} ${ruleShort}`
870
- );
2142
+ const seen = /* @__PURE__ */ new Map();
2143
+ for (const issue of result.ecosystemIssues) {
2144
+ const key = `${issue.ruleId ?? "null"}::${issue.message}`;
2145
+ const existing = seen.get(key);
2146
+ if (existing) {
2147
+ existing.count++;
2148
+ } else {
2149
+ seen.set(key, { issue, count: 1 });
871
2150
  }
872
- log.blank();
873
2151
  }
874
- log.divider();
875
- log.blank();
876
- log.section("Next Steps");
877
- log.info(`Run ${import_chalk2.default.cyan("ai-guard baseline")} to save these issues and track only new ones`);
878
- log.info(`Run ${import_chalk2.default.cyan("ai-guard report")} to generate a shareable HTML report`);
879
- log.info(`Run ${import_chalk2.default.cyan("ai-guard ignore")} to suppress dist/build noise`);
2152
+ for (const { issue, count } of seen.values()) {
2153
+ log.ecosystemIssue(issue, count);
2154
+ }
880
2155
  log.blank();
881
- process.exit(getRunExitCode(result, opts.maxWarnings));
882
- });
883
- }
884
- function getRunExitCode(result, maxWarnings) {
885
- if (result.totalErrors > 0) return 1;
886
- if (maxWarnings !== void 0 && result.totalWarnings > maxWarnings) return 1;
887
- return 0;
2156
+ }
2157
+ log.divider();
2158
+ log.blank();
2159
+ log.section("Next Steps");
2160
+ if (result.totalIssues > 0) {
2161
+ log.info(`Run ${import_chalk4.default.cyan("ai-guard baseline")} to save these issues and track only new ones`);
2162
+ log.info(`Run ${import_chalk4.default.cyan("ai-guard report")} to generate a shareable HTML report`);
2163
+ }
2164
+ if (result.ecosystemIssues.length > 0) {
2165
+ log.info(`Run ${import_chalk4.default.cyan("ai-guard doctor")} to diagnose ESLint config issues`);
2166
+ }
2167
+ log.blank();
2168
+ process.exit(getFailOnExitCode(result, opts.failOn, opts.maxWarnings));
888
2169
  }
889
- function formatIssueCount(errors, warnings) {
890
- const parts = [];
891
- if (errors > 0) parts.push(import_chalk2.default.red.bold(`${errors} error${errors !== 1 ? "s" : ""}`));
892
- if (warnings > 0) parts.push(import_chalk2.default.yellow.bold(`${warnings} warning${warnings !== 1 ? "s" : ""}`));
893
- return parts.join(import_chalk2.default.gray(" \xB7 "));
2170
+ function getFailOnExitCode(result, failOn, maxWarnings) {
2171
+ return resolveExitCode(result, failOn, maxWarnings);
894
2172
  }
895
2173
 
896
2174
  // cli/commands/init.ts
897
- var import_chalk3 = __toESM(require("chalk"));
898
- var import_path4 = __toESM(require("path"));
899
- var import_fs4 = __toESM(require("fs"));
2175
+ var import_chalk5 = __toESM(require("chalk"));
2176
+ var import_path6 = __toESM(require("path"));
2177
+ var import_fs7 = __toESM(require("fs"));
900
2178
 
901
2179
  // cli/utils/detector.ts
902
- var import_fs2 = __toESM(require("fs"));
903
- var import_path2 = __toESM(require("path"));
2180
+ var import_fs5 = __toESM(require("fs"));
2181
+ var import_path4 = __toESM(require("path"));
904
2182
  var import_module = require("module");
905
2183
  var FLAT_CONFIG_FILES = [
906
2184
  { file: "eslint.config.js", type: "flat-js" },
@@ -917,11 +2195,11 @@ var LEGACY_CONFIG_FILES = [
917
2195
  var ALL_CONFIG_FILES = [...FLAT_CONFIG_FILES, ...LEGACY_CONFIG_FILES];
918
2196
  function isPackageInstalled(pkgName, cwd = process.cwd()) {
919
2197
  try {
920
- const pkgPath = import_path2.default.join(cwd, "node_modules", pkgName, "package.json");
921
- if (import_fs2.default.existsSync(pkgPath)) {
2198
+ const pkgPath = import_path4.default.join(cwd, "node_modules", pkgName, "package.json");
2199
+ if (import_fs5.default.existsSync(pkgPath)) {
922
2200
  return true;
923
2201
  }
924
- const req = (0, import_module.createRequire)(import_path2.default.join(cwd, "package.json"));
2202
+ const req = (0, import_module.createRequire)(import_path4.default.join(cwd, "package.json"));
925
2203
  req.resolve(`${pkgName}/package.json`);
926
2204
  return true;
927
2205
  } catch {
@@ -930,9 +2208,9 @@ function isPackageInstalled(pkgName, cwd = process.cwd()) {
930
2208
  }
931
2209
  function getPackageVersion(pkgName, cwd = process.cwd()) {
932
2210
  try {
933
- const pkgPath = import_path2.default.join(cwd, "node_modules", pkgName, "package.json");
934
- const resolvedPath = import_fs2.default.existsSync(pkgPath) ? pkgPath : (0, import_module.createRequire)(import_path2.default.join(cwd, "package.json")).resolve(`${pkgName}/package.json`);
935
- const raw = import_fs2.default.readFileSync(resolvedPath, "utf-8");
2211
+ const pkgPath = import_path4.default.join(cwd, "node_modules", pkgName, "package.json");
2212
+ const resolvedPath = import_fs5.default.existsSync(pkgPath) ? pkgPath : (0, import_module.createRequire)(import_path4.default.join(cwd, "package.json")).resolve(`${pkgName}/package.json`);
2213
+ const raw = import_fs5.default.readFileSync(resolvedPath, "utf-8");
936
2214
  const json = JSON.parse(raw);
937
2215
  return json.version ?? null;
938
2216
  } catch {
@@ -948,8 +2226,8 @@ function detectConfigType(cwd = process.cwd()) {
948
2226
  const found = [];
949
2227
  let first = null;
950
2228
  for (const { file, type } of ALL_CONFIG_FILES) {
951
- const fullPath = import_path2.default.join(cwd, file);
952
- if (import_fs2.default.existsSync(fullPath)) {
2229
+ const fullPath = import_path4.default.join(cwd, file);
2230
+ if (import_fs5.default.existsSync(fullPath)) {
953
2231
  found.push(fullPath);
954
2232
  if (!first) first = { type, path: fullPath };
955
2233
  }
@@ -972,7 +2250,7 @@ function requiresFlatConfig(eslintMajor) {
972
2250
  function detectNukeIgnore(configPath) {
973
2251
  if (!configPath) return false;
974
2252
  try {
975
- const content = import_fs2.default.readFileSync(configPath, "utf-8");
2253
+ const content = import_fs5.default.readFileSync(configPath, "utf-8");
976
2254
  return content.includes('"**/*"') || content.includes("'**/*'");
977
2255
  } catch {
978
2256
  return false;
@@ -1028,7 +2306,7 @@ async function verifyConfigLoads(configPath, cwd) {
1028
2306
  overrideConfigFile: configPath
1029
2307
  });
1030
2308
  await eslint.lintText("", {
1031
- filePath: import_path4.default.join(cwd, "_ai_guard_probe_.js")
2309
+ filePath: import_path6.default.join(cwd, "_ai_guard_probe_.js")
1032
2310
  });
1033
2311
  return null;
1034
2312
  } catch (err) {
@@ -1046,7 +2324,7 @@ function registerInitCommand(program2) {
1046
2324
  const isDryRun = opts.dryRun === true;
1047
2325
  log.banner("AI GUARD INIT");
1048
2326
  if (isDryRun) {
1049
- log.print(` ${import_chalk3.default.yellow("\u2691 DRY RUN \u2014 no files will be written")}`);
2327
+ log.print(` ${import_chalk5.default.yellow("\u2691 DRY RUN \u2014 no files will be written")}`);
1050
2328
  log.blank();
1051
2329
  }
1052
2330
  log.info("Detecting your project setup\u2026");
@@ -1064,7 +2342,7 @@ function registerInitCommand(program2) {
1064
2342
  log.warn("eslint-plugin-ai-guard not found");
1065
2343
  }
1066
2344
  if (env.configType !== "none") {
1067
- log.success(`ESLint config found: ${import_chalk3.default.white(import_path4.default.relative(cwd, env.configPath ?? env.configType))}`);
2345
+ log.success(`ESLint config found: ${import_chalk5.default.white(import_path6.default.relative(cwd, env.configPath ?? env.configType))}`);
1068
2346
  } else {
1069
2347
  log.info("No ESLint config found \u2014 will generate one");
1070
2348
  }
@@ -1073,9 +2351,9 @@ function registerInitCommand(program2) {
1073
2351
  log.blank();
1074
2352
  log.warn("Conflicting configs detected:");
1075
2353
  for (const p of env.allConfigPaths) {
1076
- log.print(` ${import_chalk3.default.yellow("\u2192")} ${import_chalk3.default.white(import_path4.default.relative(cwd, p))}`);
2354
+ log.print(` ${import_chalk5.default.yellow("\u2192")} ${import_chalk5.default.white(import_path6.default.relative(cwd, p))}`);
1077
2355
  }
1078
- log.print(` ${import_chalk3.default.gray("Multiple config formats found \u2014 this can cause unpredictable behavior.")}`);
2356
+ log.print(` ${import_chalk5.default.gray("Multiple config formats found \u2014 this can cause unpredictable behavior.")}`);
1079
2357
  }
1080
2358
  log.blank();
1081
2359
  const toInstall = [];
@@ -1086,14 +2364,14 @@ function registerInitCommand(program2) {
1086
2364
  log.warn("The following packages are not installed:");
1087
2365
  log.blank();
1088
2366
  for (const pkg of toInstall) {
1089
- log.print(` ${import_chalk3.default.yellow("\u2192")} ${import_chalk3.default.white(pkg)}`);
2367
+ log.print(` ${import_chalk5.default.yellow("\u2192")} ${import_chalk5.default.white(pkg)}`);
1090
2368
  }
1091
2369
  log.blank();
1092
- log.print(` ${import_chalk3.default.bold("Run this first:")}`);
2370
+ log.print(` ${import_chalk5.default.bold("Run this first:")}`);
1093
2371
  log.blank();
1094
- log.print(` ${import_chalk3.default.cyan(`npm install --save-dev ${toInstall.join(" ")}`)}`);
2372
+ log.print(` ${import_chalk5.default.cyan(`npm install --save-dev ${toInstall.join(" ")}`)}`);
1095
2373
  log.blank();
1096
- log.print(` Then re-run ${import_chalk3.default.cyan("ai-guard init")} to complete setup.`);
2374
+ log.print(` Then re-run ${import_chalk5.default.cyan("ai-guard init")} to complete setup.`);
1097
2375
  log.blank();
1098
2376
  process.exit(1);
1099
2377
  return;
@@ -1101,7 +2379,7 @@ function registerInitCommand(program2) {
1101
2379
  try {
1102
2380
  const { createRequire: createRequire2 } = await import("module");
1103
2381
  const { pathToFileURL: pathToFileURL2 } = await import("url");
1104
- const req = createRequire2(import_path4.default.join(cwd, "_probe.js"));
2382
+ const req = createRequire2(import_path6.default.join(cwd, "_probe.js"));
1105
2383
  const resolvedPath = req.resolve("eslint-plugin-ai-guard");
1106
2384
  const aiGuard = await import(pathToFileURL2(resolvedPath).href);
1107
2385
  const plugin = aiGuard.default || aiGuard;
@@ -1120,9 +2398,9 @@ function registerInitCommand(program2) {
1120
2398
  if (eslintNeedsFlat && existingIsLegacy) {
1121
2399
  log.section("Config Migration Required");
1122
2400
  log.warn(`ESLint v${env.eslintMajor} requires flat config, but a legacy config was found:`);
1123
- log.print(` ${import_chalk3.default.white(import_path4.default.relative(cwd, env.configPath))}`);
2401
+ log.print(` ${import_chalk5.default.white(import_path6.default.relative(cwd, env.configPath))}`);
1124
2402
  log.blank();
1125
- log.print(` ${import_chalk3.default.bold("ai-guard will create a new flat config.")}`);
2403
+ log.print(` ${import_chalk5.default.bold("ai-guard will create a new flat config.")}`);
1126
2404
  log.blank();
1127
2405
  }
1128
2406
  log.section(`Configuring ESLint${isDryRun ? " (dry run)" : ""}`);
@@ -1132,34 +2410,34 @@ function registerInitCommand(program2) {
1132
2410
  const raw = readConfig(env.configPath);
1133
2411
  const { content: fixed, changed } = removeNukeIgnore(raw);
1134
2412
  if (changed) {
1135
- log.warn(`Removed "**/*" ignore pattern from ${import_path4.default.relative(cwd, env.configPath)}`);
2413
+ log.warn(`Removed "**/*" ignore pattern from ${import_path6.default.relative(cwd, env.configPath)}`);
1136
2414
  if (!isDryRun) {
1137
2415
  backupConfig(env.configPath);
1138
2416
  writeConfig(env.configPath, fixed);
1139
2417
  log.success("Ignore pattern fixed");
1140
2418
  } else {
1141
- log.print(` ${import_chalk3.default.gray("[dry-run] would remove nuke-ignore from existing config")}`);
2419
+ log.print(` ${import_chalk5.default.gray("[dry-run] would remove nuke-ignore from existing config")}`);
1142
2420
  }
1143
2421
  }
1144
2422
  }
1145
2423
  if (env.configType === "none" || eslintNeedsFlat && existingIsLegacy) {
1146
2424
  const targetName = useFlat ? "eslint.config.mjs" : ".eslintrc.js";
1147
- finalConfigPath = import_path4.default.join(cwd, targetName);
2425
+ finalConfigPath = import_path6.default.join(cwd, targetName);
1148
2426
  const content2 = useFlat ? generateFlatConfig(preset) : generateLegacyConfig(preset);
1149
2427
  if (isDryRun) {
1150
- log.print(` ${import_chalk3.default.gray(`[dry-run] would create: ${targetName}`)}`);
2428
+ log.print(` ${import_chalk5.default.gray(`[dry-run] would create: ${targetName}`)}`);
1151
2429
  log.blank();
1152
- log.print(import_chalk3.default.gray("\u2500\u2500\u2500 Preview \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2430
+ log.print(import_chalk5.default.gray("\u2500\u2500\u2500 Preview \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1153
2431
  log.blank();
1154
2432
  for (const line of content2.split("\n")) {
1155
- log.print(import_chalk3.default.gray(` ${line}`));
2433
+ log.print(import_chalk5.default.gray(` ${line}`));
1156
2434
  }
1157
2435
  log.blank();
1158
2436
  } else {
1159
2437
  writeConfig(finalConfigPath, content2);
1160
- log.success(`Created ${import_chalk3.default.white(targetName)}`);
1161
- log.info(` Preset: ${import_chalk3.default.cyan(preset)}`);
1162
- log.info(` Format: ${import_chalk3.default.cyan(useFlat ? "ESLint v9 flat config (eslint.config.mjs)" : "ESLint v8 legacy config (.eslintrc.js)")}`);
2438
+ log.success(`Created ${import_chalk5.default.white(targetName)}`);
2439
+ log.info(` Preset: ${import_chalk5.default.cyan(preset)}`);
2440
+ log.info(` Format: ${import_chalk5.default.cyan(useFlat ? "ESLint v9 flat config (eslint.config.mjs)" : "ESLint v8 legacy config (.eslintrc.js)")}`);
1163
2441
  configWritten = true;
1164
2442
  }
1165
2443
  } else if (useFlat && existingIsFlat) {
@@ -1172,15 +2450,15 @@ function registerInitCommand(program2) {
1172
2450
  finalConfigPath = env.configPath;
1173
2451
  } else if (isDryRun) {
1174
2452
  finalConfigPath = env.configPath;
1175
- log.print(` ${import_chalk3.default.gray(`[dry-run] would patch: ${import_path4.default.relative(cwd, env.configPath)}`)}`);
2453
+ log.print(` ${import_chalk5.default.gray(`[dry-run] would patch: ${import_path6.default.relative(cwd, env.configPath)}`)}`);
1176
2454
  } else {
1177
2455
  const bak = backupConfig(env.configPath);
1178
- log.info(`Backed up \u2192 ${import_chalk3.default.gray(import_path4.default.relative(cwd, bak))}`);
2456
+ log.info(`Backed up \u2192 ${import_chalk5.default.gray(import_path6.default.relative(cwd, bak))}`);
1179
2457
  writeConfig(env.configPath, patched);
1180
2458
  if (isInvalid) {
1181
- log.success(`Detected invalid ai-guard config \u2014 repairing ${import_chalk3.default.white(import_path4.default.relative(cwd, env.configPath))}`);
2459
+ log.success(`Detected invalid ai-guard config \u2014 repairing ${import_chalk5.default.white(import_path6.default.relative(cwd, env.configPath))}`);
1182
2460
  } else {
1183
- log.success(`Patched ${import_chalk3.default.white(import_path4.default.relative(cwd, env.configPath))}`);
2461
+ log.success(`Patched ${import_chalk5.default.white(import_path6.default.relative(cwd, env.configPath))}`);
1184
2462
  }
1185
2463
  finalConfigPath = env.configPath;
1186
2464
  configWritten = true;
@@ -1193,12 +2471,12 @@ function registerInitCommand(program2) {
1193
2471
  finalConfigPath = env.configPath;
1194
2472
  } else if (isDryRun) {
1195
2473
  finalConfigPath = env.configPath;
1196
- log.print(` ${import_chalk3.default.gray(`[dry-run] would patch: ${import_path4.default.relative(cwd, env.configPath)}`)}`);
2474
+ log.print(` ${import_chalk5.default.gray(`[dry-run] would patch: ${import_path6.default.relative(cwd, env.configPath)}`)}`);
1197
2475
  } else {
1198
2476
  const bak = backupConfig(env.configPath);
1199
- log.info(`Backed up \u2192 ${import_chalk3.default.gray(import_path4.default.relative(cwd, bak))}`);
2477
+ log.info(`Backed up \u2192 ${import_chalk5.default.gray(import_path6.default.relative(cwd, bak))}`);
1200
2478
  writeConfig(env.configPath, patched);
1201
- log.success(`Patched ${import_chalk3.default.white(import_path4.default.relative(cwd, env.configPath))}`);
2479
+ log.success(`Patched ${import_chalk5.default.white(import_path6.default.relative(cwd, env.configPath))}`);
1202
2480
  finalConfigPath = env.configPath;
1203
2481
  configWritten = true;
1204
2482
  }
@@ -1207,18 +2485,18 @@ function registerInitCommand(program2) {
1207
2485
  const legacyConfigs = env.allConfigPaths.filter((p) => !p.includes("eslint.config."));
1208
2486
  let cleanedUp = false;
1209
2487
  for (const p of legacyConfigs) {
1210
- if (import_fs4.default.existsSync(p)) {
2488
+ if (import_fs7.default.existsSync(p)) {
1211
2489
  if (!isDryRun) {
1212
2490
  try {
1213
- import_fs4.default.renameSync(p, `${p}.bak`);
2491
+ import_fs7.default.renameSync(p, `${p}.bak`);
1214
2492
  cleanedUp = true;
1215
2493
  } catch (err) {
1216
2494
  const reason = err instanceof Error ? err.message : String(err);
1217
- log.warn(`Could not back up ${import_path4.default.relative(cwd, p)}: ${reason}`);
2495
+ log.warn(`Could not back up ${import_path6.default.relative(cwd, p)}: ${reason}`);
1218
2496
  }
1219
2497
  } else {
1220
2498
  cleanedUp = true;
1221
- log.print(` ${import_chalk3.default.gray(`[dry-run] would rename ${import_path4.default.relative(cwd, p)} to ${import_path4.default.basename(p)}.bak`)}`);
2499
+ log.print(` ${import_chalk5.default.gray(`[dry-run] would rename ${import_path6.default.relative(cwd, p)} to ${import_path6.default.basename(p)}.bak`)}`);
1222
2500
  }
1223
2501
  }
1224
2502
  }
@@ -1228,19 +2506,19 @@ function registerInitCommand(program2) {
1228
2506
  }
1229
2507
  if (isDryRun) {
1230
2508
  log.blank();
1231
- log.print(` ${import_chalk3.default.yellow("Dry run complete \u2014 no files written.")}`);
1232
- log.print(` Re-run without ${import_chalk3.default.cyan("--dry-run")} to apply.`);
2509
+ log.print(` ${import_chalk5.default.yellow("Dry run complete \u2014 no files written.")}`);
2510
+ log.print(` Re-run without ${import_chalk5.default.cyan("--dry-run")} to apply.`);
1233
2511
  log.blank();
1234
2512
  process.exit(0);
1235
2513
  return;
1236
2514
  }
1237
2515
  log.blank();
1238
2516
  log.section("Validation");
1239
- if (!import_fs4.default.existsSync(finalConfigPath)) {
1240
- log.error(`Config file not found after write: ${import_path4.default.relative(cwd, finalConfigPath)}`);
2517
+ if (!import_fs7.default.existsSync(finalConfigPath)) {
2518
+ log.error(`Config file not found after write: ${import_path6.default.relative(cwd, finalConfigPath)}`);
1241
2519
  log.blank();
1242
- log.print(` ${import_chalk3.default.bold("Problem:")} File write may have failed.`);
1243
- log.print(` ${import_chalk3.default.bold("Fix:")} Check disk space and file permissions, then re-run.`);
2520
+ log.print(` ${import_chalk5.default.bold("Problem:")} File write may have failed.`);
2521
+ log.print(` ${import_chalk5.default.bold("Fix:")} Check disk space and file permissions, then re-run.`);
1244
2522
  log.blank();
1245
2523
  process.exit(1);
1246
2524
  return;
@@ -1250,36 +2528,36 @@ function registerInitCommand(program2) {
1250
2528
  if (validationErrors.length > 0) {
1251
2529
  log.warn("Config validation found issues:");
1252
2530
  for (const e of validationErrors) {
1253
- log.print(` ${import_chalk3.default.red("\u2716")} ${e}`);
2531
+ log.print(` ${import_chalk5.default.red("\u2716")} ${e}`);
1254
2532
  }
1255
2533
  } else {
1256
- log.success(`Config structure valid: ${import_chalk3.default.white(import_path4.default.relative(cwd, finalConfigPath))}`);
2534
+ log.success(`Config structure valid: ${import_chalk5.default.white(import_path6.default.relative(cwd, finalConfigPath))}`);
1257
2535
  }
1258
2536
  log.info("Verifying ESLint can load the config\u2026");
1259
2537
  const loadError = await verifyConfigLoads(finalConfigPath, cwd);
1260
2538
  if (loadError !== null) {
1261
2539
  log.blank();
1262
2540
  if (loadError.kind === "module-not-found") {
1263
- log.warn(`Could not verify: module not found: ${import_chalk3.default.yellow(loadError.pkg)}`);
2541
+ log.warn(`Could not verify: module not found: ${import_chalk5.default.yellow(loadError.pkg)}`);
1264
2542
  log.blank();
1265
- log.print(` ${import_chalk3.default.bold("Install the missing package:")}`);
1266
- log.print(` ${import_chalk3.default.cyan(`npm install --save-dev ${loadError.pkg}`)}`);
1267
- log.print(` Then run ${import_chalk3.default.cyan("ai-guard doctor")} to confirm.`);
2543
+ log.print(` ${import_chalk5.default.bold("Install the missing package:")}`);
2544
+ log.print(` ${import_chalk5.default.cyan(`npm install --save-dev ${loadError.pkg}`)}`);
2545
+ log.print(` Then run ${import_chalk5.default.cyan("ai-guard doctor")} to confirm.`);
1268
2546
  log.blank();
1269
2547
  } else {
1270
2548
  log.error("ESLint failed to load the generated config.");
1271
2549
  log.blank();
1272
- log.print(` ${import_chalk3.default.bold("Problem:")}`);
1273
- log.print(` ${import_chalk3.default.red(loadError.message)}`);
2550
+ log.print(` ${import_chalk5.default.bold("Problem:")}`);
2551
+ log.print(` ${import_chalk5.default.red(loadError.message)}`);
1274
2552
  if (loadError.kind === "unknown") {
1275
2553
  log.blank();
1276
- log.print(` ${import_chalk3.default.yellow("Generated config may be using incorrect plugin structure.")}`);
2554
+ log.print(` ${import_chalk5.default.yellow("Generated config may be using incorrect plugin structure.")}`);
1277
2555
  }
1278
2556
  log.blank();
1279
- log.print(` ${import_chalk3.default.bold("Fix:")}`);
1280
- log.print(` 1. Check the syntax in ${import_chalk3.default.cyan(import_path4.default.relative(cwd, finalConfigPath))}`);
1281
- log.print(` 2. Make sure ${import_chalk3.default.cyan("eslint-plugin-ai-guard")} is installed`);
1282
- log.print(` 3. Run ${import_chalk3.default.cyan("ai-guard doctor")} for full diagnostics`);
2557
+ log.print(` ${import_chalk5.default.bold("Fix:")}`);
2558
+ log.print(` 1. Check the syntax in ${import_chalk5.default.cyan(import_path6.default.relative(cwd, finalConfigPath))}`);
2559
+ log.print(` 2. Make sure ${import_chalk5.default.cyan("eslint-plugin-ai-guard")} is installed`);
2560
+ log.print(` 3. Run ${import_chalk5.default.cyan("ai-guard doctor")} for full diagnostics`);
1283
2561
  log.blank();
1284
2562
  process.exit(1);
1285
2563
  return;
@@ -1293,33 +2571,34 @@ function registerInitCommand(program2) {
1293
2571
  let cleanedUpInfo = false;
1294
2572
  if (eslintNeedsFlat) {
1295
2573
  const legacyConfigs = env.allConfigPaths.filter((p) => !p.includes("eslint.config."));
1296
- cleanedUpInfo = legacyConfigs.some((p) => import_fs4.default.existsSync(`${p}.bak`) && !import_fs4.default.existsSync(p));
2574
+ cleanedUpInfo = legacyConfigs.some((p) => import_fs7.default.existsSync(`${p}.bak`) && !import_fs7.default.existsSync(p));
1297
2575
  }
1298
2576
  const lingeringConflicts = env.hasConflictingConfigs && !eslintNeedsFlat && !cleanedUpInfo;
1299
2577
  if (loadError === null && !lingeringConflicts) {
1300
- log.print(` ${import_chalk3.default.green("\u2714")} ${import_chalk3.default.bold.green("Configuration validated successfully")}`);
2578
+ log.print(` ${import_chalk5.default.green("\u2714")} ${import_chalk5.default.bold.green("Configuration validated successfully")}`);
1301
2579
  }
1302
- log.print(` ${import_chalk3.default.green("\u2714")} ${import_chalk3.default.bold("ESLint config:")} ${import_chalk3.default.cyan(import_path4.default.relative(cwd, finalConfigPath))}`);
1303
- log.print(` ${import_chalk3.default.green("\u2714")} ${import_chalk3.default.bold("Mode:")} ${import_chalk3.default.cyan(useFlat ? `Flat config (ESLint v${env.eslintMajor ?? 9})` : `Legacy config (ESLint v${env.eslintMajor ?? 8})`)}`);
1304
- log.print(` ${import_chalk3.default.green("\u2714")} ${import_chalk3.default.bold("Preset:")} ${import_chalk3.default.cyan(preset)}`);
2580
+ log.print(` ${import_chalk5.default.green("\u2714")} ${import_chalk5.default.bold("ESLint config:")} ${import_chalk5.default.cyan(import_path6.default.relative(cwd, finalConfigPath))}`);
2581
+ log.print(` ${import_chalk5.default.green("\u2714")} ${import_chalk5.default.bold("Mode:")} ${import_chalk5.default.cyan(useFlat ? `Flat config (ESLint v${env.eslintMajor ?? 9})` : `Legacy config (ESLint v${env.eslintMajor ?? 8})`)}`);
2582
+ log.print(` ${import_chalk5.default.green("\u2714")} ${import_chalk5.default.bold("Preset:")} ${import_chalk5.default.cyan(preset)}`);
1305
2583
  log.blank();
1306
- log.info(`Run ${import_chalk3.default.cyan("npx eslint .")} \u2192 lint with your editor integration`);
1307
- log.info(`Run ${import_chalk3.default.cyan("ai-guard run")} \u2192 zero-config scan`);
1308
- log.info(`Run ${import_chalk3.default.cyan("ai-guard doctor")} \u2192 verify the full setup`);
1309
- log.info(`Run ${import_chalk3.default.cyan("ai-guard baseline")} \u2192 save baseline, track only new issues`);
2584
+ log.info(`Run ${import_chalk5.default.cyan("npx eslint .")} \u2192 lint with your editor integration`);
2585
+ log.info(`Run ${import_chalk5.default.cyan("ai-guard run")} \u2192 zero-config scan`);
2586
+ log.info(`Run ${import_chalk5.default.cyan("ai-guard doctor")} \u2192 verify the full setup`);
2587
+ log.info(`Run ${import_chalk5.default.cyan("ai-guard baseline")} \u2192 save baseline, track only new issues`);
1310
2588
  log.blank();
1311
2589
  });
1312
2590
  }
1313
2591
 
1314
2592
  // cli/commands/doctor.ts
1315
- var import_chalk4 = __toESM(require("chalk"));
1316
- var import_path5 = __toESM(require("path"));
2593
+ var import_chalk6 = __toESM(require("chalk"));
2594
+ var import_path7 = __toESM(require("path"));
1317
2595
  init_config_manager();
2596
+ var import_meta = {};
1318
2597
  async function probeConfigLoad(configPath, cwd) {
1319
2598
  try {
1320
2599
  const { ESLint } = await import("eslint");
1321
2600
  const eslint = new ESLint({ cwd, overrideConfigFile: configPath });
1322
- await eslint.lintText("", { filePath: import_path5.default.join(cwd, "_probe_.js") });
2601
+ await eslint.lintText("", { filePath: import_path7.default.join(cwd, "_probe_.js") });
1323
2602
  return { ok: true, error: null };
1324
2603
  } catch (err) {
1325
2604
  const msg = err instanceof Error ? err.message : String(err);
@@ -1354,7 +2633,7 @@ function registerDoctorCommand(program2) {
1354
2633
  checks.push({
1355
2634
  label: "ESLint config present",
1356
2635
  pass: hasConfig,
1357
- detail: hasConfig ? `${import_path5.default.relative(cwd, env.configPath ?? "")} (${env.configType})` : "No eslint.config.* or .eslintrc.* found",
2636
+ detail: hasConfig ? `${import_path7.default.relative(cwd, env.configPath ?? "")} (${env.configType})` : "No eslint.config.* or .eslintrc.* found",
1358
2637
  fix: hasConfig ? void 0 : "ai-guard init",
1359
2638
  note: hasConfig ? void 0 : "This will generate the correct config format for your ESLint version"
1360
2639
  });
@@ -1386,7 +2665,7 @@ function registerDoctorCommand(program2) {
1386
2665
  checks.push({
1387
2666
  label: "Plugin wired in config",
1388
2667
  pass: pluginWired && !isInvalidConfig,
1389
- detail: readError ? `Could not read config: ${readError}` : isInvalidConfig ? "Invalid ai-guard config detected (wrong export usage)" : pluginWired ? "eslint-plugin-ai-guard referenced in config" : `Config exists but ai-guard plugin not found in ${import_path5.default.relative(cwd, env.configPath)}`,
2668
+ detail: readError ? `Could not read config: ${readError}` : isInvalidConfig ? "Invalid ai-guard config detected (wrong export usage)" : pluginWired ? "eslint-plugin-ai-guard referenced in config" : `Config exists but ai-guard plugin not found in ${import_path7.default.relative(cwd, env.configPath)}`,
1390
2669
  fix: isInvalidConfig || !pluginWired && !readError ? "ai-guard init (will patch/repair your existing config)" : void 0
1391
2670
  });
1392
2671
  }
@@ -1394,7 +2673,7 @@ function registerDoctorCommand(program2) {
1394
2673
  label: "No conflicting config files",
1395
2674
  pass: !env.hasConflictingConfigs,
1396
2675
  detail: env.hasConflictingConfigs ? `Conflicting config detected:
1397
- ${env.allConfigPaths.map((p) => ` ${import_path5.default.relative(cwd, p)}`).join("\n")}` : "Only one config format detected",
2676
+ ${env.allConfigPaths.map((p) => ` ${import_path7.default.relative(cwd, p)}`).join("\n")}` : "Only one config format detected",
1398
2677
  fix: env.hasConflictingConfigs ? "Remove or backup legacy config" : void 0,
1399
2678
  note: env.hasConflictingConfigs ? "ESLint v9 silently ignores .eslintrc.* when a flat config exists. This can cause confusion." : void 0
1400
2679
  });
@@ -1421,24 +2700,91 @@ ${env.allConfigPaths.map((p) => ` ${import_path5.default.relative(cwd, p)}
1421
2700
  label: "ESLint config loads without errors",
1422
2701
  pass: probe.ok,
1423
2702
  detail: probe.ok ? "ESLint loaded config successfully" : probe.error ?? "Unknown error",
1424
- fix: probe.ok ? void 0 : `Check syntax in ${import_path5.default.relative(cwd, env.configPath)}`,
2703
+ fix: probe.ok ? void 0 : `Check syntax in ${import_path7.default.relative(cwd, env.configPath)}`,
1425
2704
  note: probe.ok ? void 0 : "Run: ai-guard init --dry-run to preview a fresh config"
1426
2705
  });
1427
2706
  }
2707
+ {
2708
+ let utilsOk = false;
2709
+ let utilsVersion = null;
2710
+ try {
2711
+ const { createRequire: createRequire2 } = await import("module");
2712
+ const req = createRequire2(import_path7.default.join(cwd, "package.json"));
2713
+ const pkgPath = req.resolve("@typescript-eslint/utils/package.json");
2714
+ const pkg = JSON.parse(
2715
+ (await import("fs")).readFileSync(pkgPath, "utf-8")
2716
+ );
2717
+ utilsVersion = pkg.version ?? null;
2718
+ utilsOk = true;
2719
+ } catch {
2720
+ }
2721
+ if (!utilsOk) {
2722
+ try {
2723
+ const selfDir = import_path7.default.dirname(
2724
+ new URL(import_meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")
2725
+ );
2726
+ const pkgPath = import_path7.default.join(selfDir, "../../node_modules/@typescript-eslint/utils/package.json");
2727
+ if ((await import("fs")).existsSync(pkgPath)) {
2728
+ utilsOk = true;
2729
+ }
2730
+ } catch {
2731
+ }
2732
+ }
2733
+ checks.push({
2734
+ label: "@typescript-eslint/utils available at runtime",
2735
+ pass: utilsOk,
2736
+ detail: utilsOk ? `Found${utilsVersion ? ` v${utilsVersion}` : ""}` : "Not found \u2014 rule engine may crash on fresh installs",
2737
+ fix: utilsOk ? void 0 : "npm install @typescript-eslint/utils",
2738
+ note: utilsOk ? void 0 : "This is a runtime dependency of eslint-plugin-ai-guard rules."
2739
+ });
2740
+ }
2741
+ {
2742
+ let parserOk = false;
2743
+ let parserVersion = null;
2744
+ try {
2745
+ const { createRequire: createRequire2 } = await import("module");
2746
+ const req = createRequire2(import_path7.default.join(cwd, "package.json"));
2747
+ const pkgPath = req.resolve("@typescript-eslint/parser/package.json");
2748
+ const pkg = JSON.parse(
2749
+ (await import("fs")).readFileSync(pkgPath, "utf-8")
2750
+ );
2751
+ parserVersion = pkg.version ?? null;
2752
+ parserOk = true;
2753
+ } catch {
2754
+ }
2755
+ const hasTsFiles = (() => {
2756
+ try {
2757
+ const { readdirSync } = require("fs");
2758
+ const files = readdirSync(cwd);
2759
+ return files.some((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
2760
+ } catch {
2761
+ return false;
2762
+ }
2763
+ })();
2764
+ if (!parserOk && hasTsFiles) {
2765
+ checks.push({
2766
+ label: "@typescript-eslint/parser installed (TypeScript project)",
2767
+ pass: false,
2768
+ detail: ".ts/.tsx files detected but TypeScript parser not found",
2769
+ fix: "npm install --save-dev @typescript-eslint/parser",
2770
+ note: "TypeScript files will use espree fallback without this \u2014 some rules may not work correctly."
2771
+ });
2772
+ }
2773
+ }
1428
2774
  log.section("Diagnostics");
1429
2775
  let allPassed = true;
1430
2776
  for (const check of checks) {
1431
- const icon = check.pass ? import_chalk4.default.green("\u2714") : import_chalk4.default.red("\u2716");
1432
- const label = check.pass ? import_chalk4.default.white(check.label) : import_chalk4.default.red.bold(check.label);
2777
+ const icon = check.pass ? import_chalk6.default.green("\u2714") : import_chalk6.default.red("\u2716");
2778
+ const label = check.pass ? import_chalk6.default.white(check.label) : import_chalk6.default.red.bold(check.label);
1433
2779
  log.print(` ${icon} ${label}`);
1434
- log.print(` ${import_chalk4.default.gray(check.detail)}`);
2780
+ log.print(` ${import_chalk6.default.gray(check.detail)}`);
1435
2781
  if (!check.pass) {
1436
2782
  allPassed = false;
1437
2783
  if (check.fix) {
1438
- log.print(` ${import_chalk4.default.cyan("\u2192 Fix:")} ${import_chalk4.default.yellow(check.fix)}`);
2784
+ log.print(` ${import_chalk6.default.cyan("\u2192 Fix:")} ${import_chalk6.default.yellow(check.fix)}`);
1439
2785
  }
1440
2786
  if (check.note) {
1441
- log.print(` ${import_chalk4.default.gray(check.note)}`);
2787
+ log.print(` ${import_chalk6.default.gray(check.note)}`);
1442
2788
  }
1443
2789
  }
1444
2790
  log.blank();
@@ -1446,10 +2792,10 @@ ${env.allConfigPaths.map((p) => ` ${import_path5.default.relative(cwd, p)}
1446
2792
  log.divider();
1447
2793
  log.blank();
1448
2794
  if (allPassed) {
1449
- log.print(` ${import_chalk4.default.green("\u2714")} ${import_chalk4.default.bold.green("All checks passed! Your setup is correct.")}`);
2795
+ log.print(` ${import_chalk6.default.green("\u2714")} ${import_chalk6.default.bold.green("All checks passed! Your setup is correct.")}`);
1450
2796
  log.blank();
1451
- log.info(`Run ${import_chalk4.default.cyan("ai-guard run")} to start scanning.`);
1452
- log.info(`Run ${import_chalk4.default.cyan("npx eslint .")} to lint with ESLint directly.`);
2797
+ log.info(`Run ${import_chalk6.default.cyan("ai-guard run")} to start scanning.`);
2798
+ log.info(`Run ${import_chalk6.default.cyan("npx eslint .")} to lint with ESLint directly.`);
1453
2799
  } else {
1454
2800
  log.error("Some checks failed. Follow the fixes above.");
1455
2801
  log.blank();
@@ -1464,15 +2810,15 @@ ${env.allConfigPaths.map((p) => ` ${import_path5.default.relative(cwd, p)}
1464
2810
  );
1465
2811
  if (pluginMissing) {
1466
2812
  log.print(
1467
- ` Install missing packages first, then re-run ${import_chalk4.default.cyan("ai-guard doctor")}.`
2813
+ ` Install missing packages first, then re-run ${import_chalk6.default.cyan("ai-guard doctor")}.`
1468
2814
  );
1469
2815
  } else if (configMissing || configFormatMismatch) {
1470
2816
  log.print(
1471
- ` Run ${import_chalk4.default.cyan("ai-guard init")} to generate the correct config for your ESLint version.`
2817
+ ` Run ${import_chalk6.default.cyan("ai-guard init")} to generate the correct config for your ESLint version.`
1472
2818
  );
1473
2819
  } else {
1474
2820
  log.print(
1475
- ` Run ${import_chalk4.default.cyan("ai-guard init --dry-run")} to preview what would change.`
2821
+ ` Run ${import_chalk6.default.cyan("ai-guard init --dry-run")} to preview what would change.`
1476
2822
  );
1477
2823
  }
1478
2824
  }
@@ -1483,9 +2829,9 @@ ${env.allConfigPaths.map((p) => ` ${import_path5.default.relative(cwd, p)}
1483
2829
 
1484
2830
  // cli/commands/preset.ts
1485
2831
  var import_prompts = require("@inquirer/prompts");
1486
- var import_chalk5 = __toESM(require("chalk"));
2832
+ var import_chalk7 = __toESM(require("chalk"));
1487
2833
  init_config_manager();
1488
- var import_path6 = __toESM(require("path"));
2834
+ var import_path8 = __toESM(require("path"));
1489
2835
  function registerPresetCommand(program2) {
1490
2836
  program2.command("preset").description("Interactively select and apply an ai-guard preset to your ESLint config").action(async () => {
1491
2837
  const cwd = process.cwd();
@@ -1495,32 +2841,32 @@ function registerPresetCommand(program2) {
1495
2841
  message: "Choose a preset:",
1496
2842
  choices: [
1497
2843
  {
1498
- name: `${import_chalk5.default.green("recommended")} \u2014 balanced defaults, low noise. Best starting point.`,
2844
+ name: `${import_chalk7.default.green("recommended")} \u2014 balanced defaults, low noise. Best starting point.`,
1499
2845
  value: "recommended"
1500
2846
  },
1501
2847
  {
1502
- name: `${import_chalk5.default.yellow("strict")} \u2014 all rules at error. For mature codebases.`,
2848
+ name: `${import_chalk7.default.yellow("strict")} \u2014 all rules at error. For mature codebases.`,
1503
2849
  value: "strict"
1504
2850
  },
1505
2851
  {
1506
- name: `${import_chalk5.default.red("security")} \u2014 security rules only. For AppSec teams.`,
2852
+ name: `${import_chalk7.default.red("security")} \u2014 security rules only. For AppSec teams.`,
1507
2853
  value: "security"
1508
2854
  }
1509
2855
  ]
1510
2856
  });
1511
2857
  log.blank();
1512
- log.info(`Applying preset: ${import_chalk5.default.cyan(preset)}`);
2858
+ log.info(`Applying preset: ${import_chalk7.default.cyan(preset)}`);
1513
2859
  const env = detect(cwd);
1514
2860
  const useFlat = env.eslintMajor !== null && env.eslintMajor >= 9 || isFlat(env.configType);
1515
2861
  if (env.configType === "none") {
1516
- const configPath = useFlat ? import_path6.default.join(cwd, "eslint.config.mjs") : import_path6.default.join(cwd, ".eslintrc.js");
2862
+ const configPath = useFlat ? import_path8.default.join(cwd, "eslint.config.mjs") : import_path8.default.join(cwd, ".eslintrc.js");
1517
2863
  const content = useFlat ? generateFlatConfig(preset) : generateLegacyConfig(preset);
1518
2864
  writeConfig(configPath, content);
1519
- log.success(`Created ${import_chalk5.default.white(import_path6.default.relative(cwd, configPath))} with ${import_chalk5.default.cyan(preset)} preset`);
2865
+ log.success(`Created ${import_chalk7.default.white(import_path8.default.relative(cwd, configPath))} with ${import_chalk7.default.cyan(preset)} preset`);
1520
2866
  } else {
1521
2867
  const configPath = env.configPath;
1522
2868
  const backupPath = backupConfig(configPath);
1523
- log.info(`Backed up \u2192 ${import_chalk5.default.gray(import_path6.default.relative(cwd, backupPath))}`);
2869
+ log.info(`Backed up \u2192 ${import_chalk7.default.gray(import_path8.default.relative(cwd, backupPath))}`);
1524
2870
  const existing = readConfig(configPath);
1525
2871
  const withPlugin = isFlat(env.configType) ? patchFlatConfig(existing, preset) : patchLegacyConfig(existing, preset);
1526
2872
  const patched = isFlat(env.configType) ? switchFlatPreset(withPlugin, preset) : switchLegacyPreset(withPlugin, preset);
@@ -1528,47 +2874,47 @@ function registerPresetCommand(program2) {
1528
2874
  log.warn("No preset changes needed. Config already matches selected preset.");
1529
2875
  } else {
1530
2876
  writeConfig(configPath, patched);
1531
- log.success(`Updated ${import_chalk5.default.white(import_path6.default.relative(cwd, configPath))} to ${import_chalk5.default.cyan(preset)} preset`);
2877
+ log.success(`Updated ${import_chalk7.default.white(import_path8.default.relative(cwd, configPath))} to ${import_chalk7.default.cyan(preset)} preset`);
1532
2878
  }
1533
2879
  }
1534
2880
  log.blank();
1535
2881
  log.section("Preset Details");
1536
2882
  const presetDetails = {
1537
2883
  recommended: [
1538
- { rule: "no-empty-catch", level: import_chalk5.default.red("error") },
1539
- { rule: "no-floating-promise", level: import_chalk5.default.red("error") },
1540
- { rule: "no-hardcoded-secret", level: import_chalk5.default.red("error") },
1541
- { rule: "no-eval-dynamic", level: import_chalk5.default.red("error") },
1542
- { rule: "no-broad-exception", level: import_chalk5.default.yellow("warn") },
1543
- { rule: "no-async-array-callback", level: import_chalk5.default.yellow("warn") },
1544
- { rule: "no-await-in-loop", level: import_chalk5.default.yellow("warn") },
1545
- { rule: "+ 5 more\u2026", level: import_chalk5.default.gray("warn") }
2884
+ { rule: "no-empty-catch", level: import_chalk7.default.red("error") },
2885
+ { rule: "no-floating-promise", level: import_chalk7.default.red("error") },
2886
+ { rule: "no-hardcoded-secret", level: import_chalk7.default.red("error") },
2887
+ { rule: "no-eval-dynamic", level: import_chalk7.default.red("error") },
2888
+ { rule: "no-broad-exception", level: import_chalk7.default.yellow("warn") },
2889
+ { rule: "no-async-array-callback", level: import_chalk7.default.yellow("warn") },
2890
+ { rule: "no-await-in-loop", level: import_chalk7.default.yellow("warn") },
2891
+ { rule: "+ 5 more\u2026", level: import_chalk7.default.gray("warn") }
1546
2892
  ],
1547
2893
  strict: [
1548
- { rule: "All 17 rules", level: import_chalk5.default.red("error") }
2894
+ { rule: "All 17 rules", level: import_chalk7.default.red("error") }
1549
2895
  ],
1550
2896
  security: [
1551
- { rule: "no-hardcoded-secret", level: import_chalk5.default.red("error") },
1552
- { rule: "no-eval-dynamic", level: import_chalk5.default.red("error") },
1553
- { rule: "no-sql-string-concat", level: import_chalk5.default.red("error") },
1554
- { rule: "no-unsafe-deserialize", level: import_chalk5.default.yellow("warn") },
1555
- { rule: "require-auth-middleware", level: import_chalk5.default.yellow("warn") },
1556
- { rule: "require-authz-check", level: import_chalk5.default.yellow("warn") }
2897
+ { rule: "no-hardcoded-secret", level: import_chalk7.default.red("error") },
2898
+ { rule: "no-eval-dynamic", level: import_chalk7.default.red("error") },
2899
+ { rule: "no-sql-string-concat", level: import_chalk7.default.red("error") },
2900
+ { rule: "no-unsafe-deserialize", level: import_chalk7.default.yellow("warn") },
2901
+ { rule: "require-auth-middleware", level: import_chalk7.default.yellow("warn") },
2902
+ { rule: "require-authz-check", level: import_chalk7.default.yellow("warn") }
1557
2903
  ]
1558
2904
  };
1559
2905
  for (const { rule, level } of presetDetails[preset]) {
1560
- log.print(` ${import_chalk5.default.gray("\u2022")} ${import_chalk5.default.white(rule).padEnd(32)}${level}`);
2906
+ log.print(` ${import_chalk7.default.gray("\u2022")} ${import_chalk7.default.white(rule).padEnd(32)}${level}`);
1561
2907
  }
1562
2908
  log.blank();
1563
- log.info(`Run ${import_chalk5.default.cyan("ai-guard run")} to see results with the new preset.`);
2909
+ log.info(`Run ${import_chalk7.default.cyan("ai-guard run")} to see results with the new preset.`);
1564
2910
  log.blank();
1565
2911
  });
1566
2912
  }
1567
2913
 
1568
2914
  // cli/commands/ignore.ts
1569
- var import_chalk6 = __toESM(require("chalk"));
2915
+ var import_chalk8 = __toESM(require("chalk"));
1570
2916
  init_config_manager();
1571
- var import_path7 = __toESM(require("path"));
2917
+ var import_path9 = __toESM(require("path"));
1572
2918
  var DEFAULT_IGNORES = [
1573
2919
  ".next/",
1574
2920
  "dist/",
@@ -1584,41 +2930,41 @@ function registerIgnoreCommand(program2) {
1584
2930
  log.blank();
1585
2931
  log.section("Default Ignores");
1586
2932
  for (const p of DEFAULT_IGNORES) {
1587
- log.print(` ${import_chalk6.default.gray("\u2022")} ${import_chalk6.default.yellow(p)}`);
2933
+ log.print(` ${import_chalk8.default.gray("\u2022")} ${import_chalk8.default.yellow(p)}`);
1588
2934
  }
1589
2935
  log.blank();
1590
2936
  const env = detect(cwd);
1591
2937
  if (env.configType === "none") {
1592
2938
  const useFlat = env.eslintMajor !== null && env.eslintMajor >= 9;
1593
- const configPath = useFlat ? import_path7.default.join(cwd, "eslint.config.mjs") : import_path7.default.join(cwd, ".eslintrc.js");
2939
+ const configPath = useFlat ? import_path9.default.join(cwd, "eslint.config.mjs") : import_path9.default.join(cwd, ".eslintrc.js");
1594
2940
  const content = useFlat ? generateFlatConfig("recommended") : generateLegacyConfig("recommended");
1595
2941
  writeConfig(configPath, content);
1596
- log.success(`No config found \u2014 generated ${import_chalk6.default.white(import_path7.default.relative(cwd, configPath))} with ignores included`);
2942
+ log.success(`No config found \u2014 generated ${import_chalk8.default.white(import_path9.default.relative(cwd, configPath))} with ignores included`);
1597
2943
  } else {
1598
2944
  const configPath = env.configPath;
1599
2945
  const backupPath = backupConfig(configPath);
1600
- log.info(`Backed up \u2192 ${import_chalk6.default.gray(import_path7.default.relative(cwd, backupPath))}`);
2946
+ log.info(`Backed up \u2192 ${import_chalk8.default.gray(import_path9.default.relative(cwd, backupPath))}`);
1601
2947
  const existing = readConfig(configPath);
1602
2948
  const patched = isFlat(env.configType) ? addIgnoresToFlatConfig(existing) : addIgnoresToLegacyConfig(existing);
1603
2949
  if (patched === existing) {
1604
2950
  log.warn("Ignore patterns already present in config \u2014 no changes made.");
1605
2951
  } else {
1606
2952
  writeConfig(configPath, patched);
1607
- log.success(`Patched ${import_chalk6.default.white(import_path7.default.relative(cwd, configPath))} with ignore patterns`);
2953
+ log.success(`Patched ${import_chalk8.default.white(import_path9.default.relative(cwd, configPath))} with ignore patterns`);
1608
2954
  }
1609
2955
  }
1610
2956
  log.blank();
1611
2957
  log.info("Noisy directories will now be skipped in all future ESLint runs.");
1612
- log.info(`Run ${import_chalk6.default.cyan("ai-guard run")} to verify cleaner results.`);
2958
+ log.info(`Run ${import_chalk8.default.cyan("ai-guard run")} to verify cleaner results.`);
1613
2959
  log.blank();
1614
2960
  });
1615
2961
  }
1616
2962
 
1617
2963
  // cli/commands/baseline.ts
1618
- var import_fs5 = __toESM(require("fs"));
1619
- var import_path8 = __toESM(require("path"));
1620
- var import_chalk7 = __toESM(require("chalk"));
1621
- var import_ora2 = __toESM(require("ora"));
2964
+ var import_fs8 = __toESM(require("fs"));
2965
+ var import_path10 = __toESM(require("path"));
2966
+ var import_chalk9 = __toESM(require("chalk"));
2967
+ var import_ora3 = __toESM(require("ora"));
1622
2968
  var BASELINE_FILE = ".ai-guard-baseline.json";
1623
2969
  function saveBaseline(result, preset, mode, cwd) {
1624
2970
  const entries = result.files.map((f) => ({
@@ -1637,15 +2983,15 @@ function saveBaseline(result, preset, mode, cwd) {
1637
2983
  totalIssues: result.totalIssues,
1638
2984
  entries
1639
2985
  };
1640
- const baselinePath = import_path8.default.join(cwd, BASELINE_FILE);
1641
- import_fs5.default.writeFileSync(baselinePath, JSON.stringify(baseline, null, 2), "utf-8");
2986
+ const baselinePath = import_path10.default.join(cwd, BASELINE_FILE);
2987
+ import_fs8.default.writeFileSync(baselinePath, JSON.stringify(baseline, null, 2), "utf-8");
1642
2988
  return baselinePath;
1643
2989
  }
1644
2990
  function loadBaseline(cwd) {
1645
- const baselinePath = import_path8.default.join(cwd, BASELINE_FILE);
1646
- if (!import_fs5.default.existsSync(baselinePath)) return null;
2991
+ const baselinePath = import_path10.default.join(cwd, BASELINE_FILE);
2992
+ if (!import_fs8.default.existsSync(baselinePath)) return null;
1647
2993
  try {
1648
- const raw = import_fs5.default.readFileSync(baselinePath, "utf-8");
2994
+ const raw = import_fs8.default.readFileSync(baselinePath, "utf-8");
1649
2995
  return JSON.parse(raw);
1650
2996
  } catch {
1651
2997
  return null;
@@ -1717,7 +3063,7 @@ function registerBaselineCommand(program2) {
1717
3063
  const doCheck = opts.check ?? (existingBaseline !== null && !opts.save);
1718
3064
  if (doSave) {
1719
3065
  log.info("Scanning project to save baseline\u2026");
1720
- const spinner = (0, import_ora2.default)({ text: "Running analysis\u2026", color: "cyan" }).start();
3066
+ const spinner = (0, import_ora3.default)({ text: "Running analysis\u2026", color: "cyan" }).start();
1721
3067
  let result;
1722
3068
  try {
1723
3069
  result = await runEslint({ preset, targetPath: opts.path });
@@ -1729,30 +3075,30 @@ function registerBaselineCommand(program2) {
1729
3075
  }
1730
3076
  const baselinePath = saveBaseline(result, preset, mode, cwd);
1731
3077
  log.success(
1732
- `Baseline saved \u2192 ${import_chalk7.default.white(import_path8.default.relative(cwd, baselinePath))}`
3078
+ `Baseline saved \u2192 ${import_chalk9.default.white(import_path10.default.relative(cwd, baselinePath))}`
1733
3079
  );
1734
3080
  log.info(` ${result.totalIssues} issues recorded (${result.totalErrors} errors, ${result.totalWarnings} warnings)`);
1735
- log.info(` Preset: ${import_chalk7.default.cyan(preset)}`);
1736
- log.info(` Mode: ${import_chalk7.default.cyan(mode)}`);
3081
+ log.info(` Preset: ${import_chalk9.default.cyan(preset)}`);
3082
+ log.info(` Mode: ${import_chalk9.default.cyan(mode)}`);
1737
3083
  log.blank();
1738
- log.info("Future runs of " + import_chalk7.default.cyan("ai-guard baseline --check") + " will show only NEW issues.");
1739
- log.info("Add " + import_chalk7.default.white(BASELINE_FILE) + " to your git repository to share baseline with your team.");
3084
+ log.info("Future runs of " + import_chalk9.default.cyan("ai-guard baseline --check") + " will show only NEW issues.");
3085
+ log.info("Add " + import_chalk9.default.white(BASELINE_FILE) + " to your git repository to share baseline with your team.");
1740
3086
  log.blank();
1741
3087
  return;
1742
3088
  }
1743
3089
  if (doCheck) {
1744
3090
  if (!existingBaseline) {
1745
- log.error(`No baseline found at ${import_chalk7.default.white(BASELINE_FILE)}`);
1746
- log.info(`Run ${import_chalk7.default.cyan("ai-guard baseline --save")} first to create one.`);
3091
+ log.error(`No baseline found at ${import_chalk9.default.white(BASELINE_FILE)}`);
3092
+ log.info(`Run ${import_chalk9.default.cyan("ai-guard baseline --save")} first to create one.`);
1747
3093
  process.exit(1);
1748
3094
  }
1749
3095
  const baselineDate = new Date(existingBaseline.createdAt).toLocaleString();
1750
- log.info(`Baseline from: ${import_chalk7.default.gray(baselineDate)}`);
1751
- log.info(`Baseline preset: ${import_chalk7.default.cyan(existingBaseline.preset)}`);
1752
- log.info(`Baseline mode: ${import_chalk7.default.cyan(existingBaseline.mode ?? "strict")}`);
1753
- log.info(`Baseline issues: ${import_chalk7.default.gray(existingBaseline.totalIssues)}`);
3096
+ log.info(`Baseline from: ${import_chalk9.default.gray(baselineDate)}`);
3097
+ log.info(`Baseline preset: ${import_chalk9.default.cyan(existingBaseline.preset)}`);
3098
+ log.info(`Baseline mode: ${import_chalk9.default.cyan(existingBaseline.mode ?? "strict")}`);
3099
+ log.info(`Baseline issues: ${import_chalk9.default.gray(existingBaseline.totalIssues)}`);
1754
3100
  log.blank();
1755
- const spinner = (0, import_ora2.default)({ text: "Scanning for new issues\u2026", color: "cyan" }).start();
3101
+ const spinner = (0, import_ora3.default)({ text: "Scanning for new issues\u2026", color: "cyan" }).start();
1756
3102
  let result;
1757
3103
  try {
1758
3104
  result = await runEslint({ preset, targetPath: opts.path });
@@ -1775,7 +3121,7 @@ function registerBaselineCommand(program2) {
1775
3121
  return;
1776
3122
  }
1777
3123
  log.print(
1778
- import_chalk7.default.bold.red(` \u2716 ${newResult.totalIssues} new issue${newResult.totalIssues !== 1 ? "s" : ""} since baseline`)
3124
+ import_chalk9.default.bold.red(` \u2716 ${newResult.totalIssues} new issue${newResult.totalIssues !== 1 ? "s" : ""} since baseline`)
1779
3125
  );
1780
3126
  log.blank();
1781
3127
  if (newResult.ruleBreakdown.size > 0) {
@@ -1791,7 +3137,7 @@ function registerBaselineCommand(program2) {
1791
3137
  log.blank();
1792
3138
  for (const file of newResult.files) {
1793
3139
  log.print(
1794
- ` ${import_chalk7.default.bold.white(file.filePath)} ` + import_chalk7.default.gray(`(${file.issues.length} new)`)
3140
+ ` ${import_chalk9.default.bold.white(file.filePath)} ` + import_chalk9.default.gray(`(${file.issues.length} new)`)
1795
3141
  );
1796
3142
  for (const issue of file.issues) {
1797
3143
  log.issue(issue.message, issue.severity, issue.line, issue.column);
@@ -1804,7 +3150,7 @@ function registerBaselineCommand(program2) {
1804
3150
  log.error(
1805
3151
  `${newResult.totalIssues} new issue${newResult.totalIssues !== 1 ? "s" : ""} introduced since baseline.`
1806
3152
  );
1807
- log.info(`Run ${import_chalk7.default.cyan("ai-guard baseline --save")} to update the baseline after fixing them.`);
3153
+ log.info(`Run ${import_chalk9.default.cyan("ai-guard baseline --save")} to update the baseline after fixing them.`);
1808
3154
  log.blank();
1809
3155
  process.exit(1);
1810
3156
  }
@@ -1812,9 +3158,9 @@ function registerBaselineCommand(program2) {
1812
3158
  }
1813
3159
 
1814
3160
  // cli/commands/init-context.ts
1815
- var import_fs6 = __toESM(require("fs"));
1816
- var import_path9 = __toESM(require("path"));
1817
- var import_chalk8 = __toESM(require("chalk"));
3161
+ var import_fs9 = __toESM(require("fs"));
3162
+ var import_path11 = __toESM(require("path"));
3163
+ var import_chalk10 = __toESM(require("chalk"));
1818
3164
  var import_prompts2 = require("@inquirer/prompts");
1819
3165
  var RULE_CATEGORIES = {
1820
3166
  "error-handling": [
@@ -2177,23 +3523,23 @@ var AGENTS = [
2177
3523
  {
2178
3524
  id: "copilot",
2179
3525
  label: "GitHub Copilot",
2180
- filePath: import_path9.default.join(".github", "copilot-instructions.md"),
3526
+ filePath: import_path11.default.join(".github", "copilot-instructions.md"),
2181
3527
  generate: generateCopilotFile
2182
3528
  }
2183
3529
  ];
2184
3530
  function getPackageVersion2(cwd) {
2185
3531
  try {
2186
- const pkgPath = import_path9.default.join(cwd, "node_modules", "eslint-plugin-ai-guard", "package.json");
2187
- if (import_fs6.default.existsSync(pkgPath)) {
2188
- const pkg = JSON.parse(import_fs6.default.readFileSync(pkgPath, "utf-8"));
3532
+ const pkgPath = import_path11.default.join(cwd, "node_modules", "eslint-plugin-ai-guard", "package.json");
3533
+ if (import_fs9.default.existsSync(pkgPath)) {
3534
+ const pkg = JSON.parse(import_fs9.default.readFileSync(pkgPath, "utf-8"));
2189
3535
  if (pkg.version) return pkg.version;
2190
3536
  }
2191
3537
  } catch {
2192
3538
  }
2193
3539
  try {
2194
- const pkgPath = import_path9.default.join(cwd, "package.json");
2195
- if (import_fs6.default.existsSync(pkgPath)) {
2196
- const pkg = JSON.parse(import_fs6.default.readFileSync(pkgPath, "utf-8"));
3540
+ const pkgPath = import_path11.default.join(cwd, "package.json");
3541
+ if (import_fs9.default.existsSync(pkgPath)) {
3542
+ const pkg = JSON.parse(import_fs9.default.readFileSync(pkgPath, "utf-8"));
2197
3543
  if (pkg.version) return pkg.version;
2198
3544
  }
2199
3545
  } catch {
@@ -2222,7 +3568,7 @@ function registerInitContextCommand(program2) {
2222
3568
  const isForce = opts.force === true;
2223
3569
  log.banner("AI GUARD INIT-CONTEXT");
2224
3570
  if (isDryRun) {
2225
- log.print(` ${import_chalk8.default.yellow("\u2691 DRY RUN \u2014 no files will be written")}`);
3571
+ log.print(` ${import_chalk10.default.yellow("\u2691 DRY RUN \u2014 no files will be written")}`);
2226
3572
  log.blank();
2227
3573
  }
2228
3574
  let selectedAgents;
@@ -2255,13 +3601,13 @@ function registerInitContextCommand(program2) {
2255
3601
  selectedAgents = AGENTS.filter((a) => selected.includes(a.id));
2256
3602
  }
2257
3603
  }
2258
- log.info(`Categories: ${import_chalk8.default.cyan(categories.join(", "))}`);
2259
- log.info(`Version: ${import_chalk8.default.cyan(version)}`);
3604
+ log.info(`Categories: ${import_chalk10.default.cyan(categories.join(", "))}`);
3605
+ log.info(`Version: ${import_chalk10.default.cyan(version)}`);
2260
3606
  log.blank();
2261
3607
  const results = [];
2262
3608
  for (const agent of selectedAgents) {
2263
- const fullPath = import_path9.default.join(cwd, agent.filePath);
2264
- const exists = import_fs6.default.existsSync(fullPath);
3609
+ const fullPath = import_path11.default.join(cwd, agent.filePath);
3610
+ const exists = import_fs9.default.existsSync(fullPath);
2265
3611
  if (exists && !isForce && !isDryRun) {
2266
3612
  const overwrite = await (0, import_prompts2.confirm)({
2267
3613
  message: `${agent.filePath} already exists. Overwrite?`,
@@ -2275,41 +3621,41 @@ function registerInitContextCommand(program2) {
2275
3621
  const content = agent.generate(version, categories);
2276
3622
  if (isDryRun) {
2277
3623
  log.section(`Preview: ${agent.filePath}`);
2278
- log.print(import_chalk8.default.gray("\u2500".repeat(60)));
3624
+ log.print(import_chalk10.default.gray("\u2500".repeat(60)));
2279
3625
  for (const line of content.split("\n").slice(0, 20)) {
2280
- log.print(import_chalk8.default.gray(` ${line}`));
3626
+ log.print(import_chalk10.default.gray(` ${line}`));
2281
3627
  }
2282
- log.print(import_chalk8.default.gray(" ... (truncated)"));
2283
- log.print(import_chalk8.default.gray("\u2500".repeat(60)));
3628
+ log.print(import_chalk10.default.gray(" ... (truncated)"));
3629
+ log.print(import_chalk10.default.gray("\u2500".repeat(60)));
2284
3630
  log.blank();
2285
3631
  results.push({ file: agent.filePath, status: "dry-run" });
2286
3632
  continue;
2287
3633
  }
2288
- const parentDir = import_path9.default.dirname(fullPath);
2289
- if (!import_fs6.default.existsSync(parentDir)) {
2290
- import_fs6.default.mkdirSync(parentDir, { recursive: true });
3634
+ const parentDir = import_path11.default.dirname(fullPath);
3635
+ if (!import_fs9.default.existsSync(parentDir)) {
3636
+ import_fs9.default.mkdirSync(parentDir, { recursive: true });
2291
3637
  }
2292
- import_fs6.default.writeFileSync(fullPath, content, "utf-8");
3638
+ import_fs9.default.writeFileSync(fullPath, content, "utf-8");
2293
3639
  results.push({ file: agent.filePath, status: "created" });
2294
3640
  }
2295
3641
  log.section("Summary");
2296
3642
  for (const result of results) {
2297
3643
  switch (result.status) {
2298
3644
  case "created":
2299
- log.print(` ${import_chalk8.default.green("\u2705")} Created ${import_chalk8.default.white(result.file)}`);
3645
+ log.print(` ${import_chalk10.default.green("\u2705")} Created ${import_chalk10.default.white(result.file)}`);
2300
3646
  break;
2301
3647
  case "skipped":
2302
- log.print(` ${import_chalk8.default.yellow("\u23ED")} Skipped ${import_chalk8.default.white(result.file)} ${import_chalk8.default.gray("(already exists \u2014 use --force to overwrite)")}`);
3648
+ log.print(` ${import_chalk10.default.yellow("\u23ED")} Skipped ${import_chalk10.default.white(result.file)} ${import_chalk10.default.gray("(already exists \u2014 use --force to overwrite)")}`);
2303
3649
  break;
2304
3650
  case "dry-run":
2305
- log.print(` ${import_chalk8.default.cyan("\u{1F4DD}")} Would create ${import_chalk8.default.white(result.file)}`);
3651
+ log.print(` ${import_chalk10.default.cyan("\u{1F4DD}")} Would create ${import_chalk10.default.white(result.file)}`);
2306
3652
  break;
2307
3653
  }
2308
3654
  }
2309
3655
  log.blank();
2310
3656
  if (isDryRun) {
2311
3657
  log.info("Dry run complete \u2014 no files were written.");
2312
- log.info(`Re-run without ${import_chalk8.default.cyan("--dry-run")} to apply.`);
3658
+ log.info(`Re-run without ${import_chalk10.default.cyan("--dry-run")} to apply.`);
2313
3659
  } else {
2314
3660
  const createdCount = results.filter((r) => r.status === "created").length;
2315
3661
  if (createdCount > 0) {
@@ -2323,9 +3669,9 @@ function registerInitContextCommand(program2) {
2323
3669
  }
2324
3670
 
2325
3671
  // cli/commands/report.ts
2326
- var import_fs7 = __toESM(require("fs"));
2327
- var import_path10 = __toESM(require("path"));
2328
- var import_ora3 = __toESM(require("ora"));
3672
+ var import_fs10 = __toESM(require("fs"));
3673
+ var import_path12 = __toESM(require("path"));
3674
+ var import_ora4 = __toESM(require("ora"));
2329
3675
  var RULE_META = {
2330
3676
  "ai-guard/no-empty-catch": { description: "Empty catch blocks silently swallow errors", category: "Reliability" },
2331
3677
  "ai-guard/no-broad-exception": { description: "Catching all exceptions hides bugs", category: "Reliability" },
@@ -2348,28 +3694,28 @@ var RULE_META = {
2348
3694
  };
2349
3695
  function getProjectName(cwd) {
2350
3696
  try {
2351
- const pkgPath = import_path10.default.join(cwd, "package.json");
2352
- if (import_fs7.default.existsSync(pkgPath)) {
2353
- const pkg = JSON.parse(import_fs7.default.readFileSync(pkgPath, "utf-8"));
3697
+ const pkgPath = import_path12.default.join(cwd, "package.json");
3698
+ if (import_fs10.default.existsSync(pkgPath)) {
3699
+ const pkg = JSON.parse(import_fs10.default.readFileSync(pkgPath, "utf-8"));
2354
3700
  if (pkg.name) return pkg.name;
2355
3701
  }
2356
3702
  } catch {
2357
3703
  }
2358
- return import_path10.default.basename(cwd);
3704
+ return import_path12.default.basename(cwd);
2359
3705
  }
2360
3706
  function getPluginVersion(cwd) {
2361
3707
  try {
2362
- const pkgPath = import_path10.default.resolve(__dirname, "../../package.json");
2363
- if (import_fs7.default.existsSync(pkgPath)) {
2364
- const pkg = JSON.parse(import_fs7.default.readFileSync(pkgPath, "utf-8"));
3708
+ const pkgPath = import_path12.default.resolve(__dirname, "../../package.json");
3709
+ if (import_fs10.default.existsSync(pkgPath)) {
3710
+ const pkg = JSON.parse(import_fs10.default.readFileSync(pkgPath, "utf-8"));
2365
3711
  if (pkg.version) return pkg.version;
2366
3712
  }
2367
3713
  } catch {
2368
3714
  }
2369
3715
  try {
2370
- const pkgPath = import_path10.default.join(cwd, "node_modules", "eslint-plugin-ai-guard", "package.json");
2371
- if (import_fs7.default.existsSync(pkgPath)) {
2372
- const pkg = JSON.parse(import_fs7.default.readFileSync(pkgPath, "utf-8"));
3716
+ const pkgPath = import_path12.default.join(cwd, "node_modules", "eslint-plugin-ai-guard", "package.json");
3717
+ if (import_fs10.default.existsSync(pkgPath)) {
3718
+ const pkg = JSON.parse(import_fs10.default.readFileSync(pkgPath, "utf-8"));
2373
3719
  if (pkg.version) return pkg.version;
2374
3720
  }
2375
3721
  } catch {
@@ -2419,6 +3765,37 @@ function buildReportData(result, preset, scannedPath, cwd) {
2419
3765
  example
2420
3766
  };
2421
3767
  });
3768
+ const CONFIDENCE_ASSIGNMENT = {
3769
+ "ai-guard/no-hardcoded-secret": "high",
3770
+ "ai-guard/no-eval-dynamic": "high",
3771
+ "ai-guard/no-floating-promise": "high",
3772
+ "ai-guard/no-empty-catch": "high",
3773
+ "ai-guard/no-sql-string-concat": "medium",
3774
+ "ai-guard/no-await-in-loop": "medium",
3775
+ "ai-guard/require-auth-middleware": "medium",
3776
+ "ai-guard/require-authz-check": "medium",
3777
+ "ai-guard/no-catch-log-rethrow": "medium",
3778
+ "ai-guard/no-catch-without-use": "medium",
3779
+ "ai-guard/no-unsafe-deserialize": "medium",
3780
+ "ai-guard/no-async-without-await": "low",
3781
+ "ai-guard/no-async-array-callback": "low",
3782
+ "ai-guard/no-dead-branch": "low",
3783
+ "ai-guard/no-broad-exception": "low",
3784
+ "ai-guard/no-console-in-handler": "low",
3785
+ "ai-guard/no-duplicate-logic-block": "low",
3786
+ "ai-guard/no-redundant-await": "low"
3787
+ };
3788
+ let highCount = 0;
3789
+ let mediumCount = 0;
3790
+ let lowCount = 0;
3791
+ for (const [rule, count] of result.ruleBreakdown.entries()) {
3792
+ const tier = CONFIDENCE_ASSIGNMENT[rule] ?? "low";
3793
+ if (tier === "high") highCount += count;
3794
+ else if (tier === "medium") mediumCount += count;
3795
+ else lowCount += count;
3796
+ }
3797
+ const ecosystemCount = result.ecosystemIssues?.length ?? 0;
3798
+ const parserErrorCount = result.parserErrors?.length ?? 0;
2422
3799
  return {
2423
3800
  projectName,
2424
3801
  version,
@@ -2433,7 +3810,12 @@ function buildReportData(result, preset, scannedPath, cwd) {
2433
3810
  ruleBreakdown: Object.fromEntries(result.ruleBreakdown),
2434
3811
  topFiles,
2435
3812
  topRules,
2436
- categoryBreakdown
3813
+ categoryBreakdown,
3814
+ highCount,
3815
+ mediumCount,
3816
+ lowCount,
3817
+ ecosystemCount,
3818
+ parserErrorCount
2437
3819
  };
2438
3820
  }
2439
3821
  function generateHtml(data) {
@@ -2533,12 +3915,23 @@ function generateHtml(data) {
2533
3915
  <p style="color:#6b7280;font-size:0.85rem;margin-top:0.25rem;">Scanned: <code style="color:#94a3b8;">${escHtml(data.scannedPath)}</code> \xB7 Preset: <code style="color:#94a3b8;">${escHtml(data.preset)}</code> \xB7 ${data.durationMs}ms</p>
2534
3916
  </div>
2535
3917
  <div style="text-align:center;">
2536
- <div style="font-size:0.75rem;color:#6b7280;margin-bottom:0.5rem;letter-spacing:0.05em;">AI CODE HEALTH SCORE</div>
2537
- <div style="width:100px;height:100px;border-radius:50%;background:conic-gradient(${scoreColor} ${data.score}%, #1f2937 0%);display:flex;align-items:center;justify-content:center;box-shadow:0 0 30px ${scoreColor}33;">
2538
- <div style="width:80px;height:80px;border-radius:50%;background:#030712;display:flex;flex-direction:column;align-items:center;justify-content:center;">
2539
- <span style="font-size:1.6rem;font-weight:800;color:${scoreColor};">${data.score}</span>
2540
- <span style="font-size:0.65rem;color:${scoreColor};font-weight:600;">${scoreLabel}</span>
3918
+ <div style="font-size:0.75rem;color:#6b7280;margin-bottom:0.75rem;letter-spacing:0.05em;">SIGNAL CONFIDENCE</div>
3919
+ <div style="display:flex;flex-direction:column;gap:0.4rem;">
3920
+ <div style="display:flex;align-items:center;gap:0.5rem;">
3921
+ <span style="width:8px;height:8px;border-radius:50%;background:#ef4444;display:inline-block;"></span>
3922
+ <span style="font-size:0.8rem;color:#f87171;font-weight:600;">${data.highCount} high-confidence</span>
3923
+ </div>
3924
+ <div style="display:flex;align-items:center;gap:0.5rem;">
3925
+ <span style="width:8px;height:8px;border-radius:50%;background:#f59e0b;display:inline-block;"></span>
3926
+ <span style="font-size:0.8rem;color:#fbbf24;font-weight:600;">${data.mediumCount} medium-confidence</span>
3927
+ </div>
3928
+ <div style="display:flex;align-items:center;gap:0.5rem;">
3929
+ <span style="width:8px;height:8px;border-radius:50%;background:#6b7280;display:inline-block;"></span>
3930
+ <span style="font-size:0.8rem;color:#9ca3af;font-weight:600;">${data.lowCount} suggestions</span>
2541
3931
  </div>
3932
+ ${data.ecosystemCount > 0 ? `<div style="display:flex;align-items:center;gap:0.5rem;margin-top:0.25rem;">
3933
+ <span style="font-size:0.75rem;color:#4b5563;">\u26A0 ${data.ecosystemCount} config issue${data.ecosystemCount !== 1 ? "s" : ""} (not ai-guard)</span>
3934
+ </div>` : ""}
2542
3935
  </div>
2543
3936
  </div>
2544
3937
  </div>
@@ -2677,7 +4070,7 @@ function registerReportCommand(program2) {
2677
4070
  "Generate a beautiful HTML report of AI Guard scan results\nPerfect for sharing with your team or in Slack."
2678
4071
  ).option("--path <dir>", "Directory or file to scan", ".").option("--preset <preset>", "Rule preset: recommended | strict | security", "recommended").option("--html", "Generate HTML report (default behavior)").option("--json", "Output raw JSON to stdout instead of writing HTML").option("--output <filename>", "Output file name", "ai-guard-report.html").option("--no-open", "Do not open the report in the browser after generating").action(async (opts) => {
2679
4072
  const preset = ["recommended", "strict", "security"].includes(opts.preset) ? opts.preset : "recommended";
2680
- const spinner = opts.json ? null : (0, import_ora3.default)({ text: "Scanning\u2026", color: "cyan" }).start();
4073
+ const spinner = opts.json ? null : (0, import_ora4.default)({ text: "Scanning\u2026", color: "cyan" }).start();
2681
4074
  let result;
2682
4075
  try {
2683
4076
  result = await runEslint({ preset, targetPath: opts.path });
@@ -2699,8 +4092,8 @@ function registerReportCommand(program2) {
2699
4092
  log.banner("AI GUARD REPORT");
2700
4093
  log.blank();
2701
4094
  const html = generateHtml(reportData);
2702
- const outputPath = import_path10.default.resolve(cwd, opts.output);
2703
- import_fs7.default.writeFileSync(outputPath, html, "utf-8");
4095
+ const outputPath = import_path12.default.resolve(cwd, opts.output);
4096
+ import_fs10.default.writeFileSync(outputPath, html, "utf-8");
2704
4097
  log.success(`Report saved: ${opts.output}`);
2705
4098
  log.info(
2706
4099
  `Score: ${reportData.score}/100 \xB7 ${reportData.totalErrors} errors \xB7 ${reportData.totalWarnings} warnings`
@@ -2715,9 +4108,9 @@ function registerReportCommand(program2) {
2715
4108
  await open(outputPath);
2716
4109
  log.info("Opened in your browser.");
2717
4110
  } else {
2718
- const { execSync } = await import("child_process");
4111
+ const { execSync: execSync2 } = await import("child_process");
2719
4112
  const cmd = process.platform === "win32" ? `start "" "${outputPath}"` : process.platform === "darwin" ? `open "${outputPath}"` : `xdg-open "${outputPath}"`;
2720
- execSync(cmd);
4113
+ execSync2(cmd);
2721
4114
  log.info("Opened in your browser.");
2722
4115
  }
2723
4116
  } catch {
@@ -2733,8 +4126,8 @@ function registerReportCommand(program2) {
2733
4126
  // cli/index.ts
2734
4127
  function getVersion() {
2735
4128
  try {
2736
- const pkgPath = import_path11.default.resolve(__dirname, "../../package.json");
2737
- const raw = import_fs8.default.readFileSync(pkgPath, "utf-8");
4129
+ const pkgPath = import_path13.default.resolve(__dirname, "../../package.json");
4130
+ const raw = import_fs11.default.readFileSync(pkgPath, "utf-8");
2738
4131
  const pkg = JSON.parse(raw);
2739
4132
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
2740
4133
  } catch {
@@ -2743,9 +4136,10 @@ function getVersion() {
2743
4136
  }
2744
4137
  var program = new import_commander.Command();
2745
4138
  program.name("ai-guard").description(
2746
- "Production-grade CLI for eslint-plugin-ai-guard\nCatch AI-generated code issues instantly \u2014 no ESLint config required.\n\nQuick start:\n npx ai-guard run Scan current project\n npx ai-guard init Auto-configure ESLint\n npx ai-guard init-context Generate AI agent rules\n npx ai-guard report Generate shareable HTML report\n npx ai-guard doctor Check your setup\n npx ai-guard baseline Save baseline, track new issues only"
4139
+ "Production-grade CLI for eslint-plugin-ai-guard\nCatch AI-generated code issues instantly \u2014 no ESLint config required.\n\nQuick start:\n npx ai-guard run Scan current project\n npx ai-guard changed Scan only changed files (CI mode)\n npx ai-guard init Auto-configure ESLint\n npx ai-guard init-context Generate AI agent rules\n npx ai-guard report Generate shareable HTML report\n npx ai-guard doctor Check your setup\n npx ai-guard baseline Save baseline, track new issues only"
2747
4140
  ).version(getVersion(), "-v, --version", "Print version number").helpOption("-h, --help", "Show help");
2748
4141
  registerRunCommand(program);
4142
+ registerChangedCommand(program);
2749
4143
  registerInitCommand(program);
2750
4144
  registerDoctorCommand(program);
2751
4145
  registerPresetCommand(program);