eslint-plugin-ai-guard 1.1.13 → 1.2.0

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
@@ -341,8 +341,8 @@ var init_config_manager = __esm({
341
341
 
342
342
  // cli/index.ts
343
343
  var import_commander = require("commander");
344
- var import_fs7 = __toESM(require("fs"));
345
- var import_path10 = __toESM(require("path"));
344
+ var import_fs8 = __toESM(require("fs"));
345
+ var import_path11 = __toESM(require("path"));
346
346
 
347
347
  // cli/commands/run.ts
348
348
  var import_ora = __toESM(require("ora"));
@@ -355,13 +355,80 @@ var import_url = require("url");
355
355
 
356
356
  // cli/utils/logger.ts
357
357
  var import_chalk = __toESM(require("chalk"));
358
+ var CATEGORY_ICONS = {
359
+ Security: "\u{1F534}",
360
+ Reliability: "\u{1F7E0}",
361
+ "Async Stability": "\u{1F7E1}",
362
+ "AI Patterns": "\u{1F535}"
363
+ };
364
+ var CATEGORY_ORDER = ["Security", "Reliability", "Async Stability", "AI Patterns"];
365
+ var RULE_CATEGORY = {
366
+ "ai-guard/no-empty-catch": "Reliability",
367
+ "ai-guard/no-broad-exception": "Reliability",
368
+ "ai-guard/no-catch-log-rethrow": "Reliability",
369
+ "ai-guard/no-catch-without-use": "Reliability",
370
+ "ai-guard/no-floating-promise": "Async Stability",
371
+ "ai-guard/no-await-in-loop": "Async Stability",
372
+ "ai-guard/no-async-array-callback": "Async Stability",
373
+ "ai-guard/no-async-without-await": "Async Stability",
374
+ "ai-guard/no-redundant-await": "Async Stability",
375
+ "ai-guard/no-hardcoded-secret": "Security",
376
+ "ai-guard/no-eval-dynamic": "Security",
377
+ "ai-guard/no-sql-string-concat": "Security",
378
+ "ai-guard/no-unsafe-deserialize": "Security",
379
+ "ai-guard/require-auth-middleware": "Security",
380
+ "ai-guard/require-authz-check": "Security",
381
+ "ai-guard/no-console-in-handler": "AI Patterns",
382
+ "ai-guard/no-duplicate-logic-block": "AI Patterns",
383
+ "ai-guard/no-dead-branch": "AI Patterns"
384
+ };
385
+ var CONFIDENCE_TIER = {
386
+ // High confidence — near-zero false positive rate
387
+ "ai-guard/no-hardcoded-secret": "high",
388
+ "ai-guard/no-eval-dynamic": "high",
389
+ "ai-guard/no-floating-promise": "high",
390
+ "ai-guard/no-empty-catch": "high",
391
+ // Medium confidence — context matters, usually valid
392
+ "ai-guard/no-sql-string-concat": "medium",
393
+ "ai-guard/no-await-in-loop": "medium",
394
+ "ai-guard/require-auth-middleware": "medium",
395
+ "ai-guard/require-authz-check": "medium",
396
+ "ai-guard/no-catch-log-rethrow": "medium",
397
+ "ai-guard/no-catch-without-use": "medium",
398
+ "ai-guard/no-unsafe-deserialize": "medium",
399
+ // Low confidence — suggestions, may be intentional
400
+ "ai-guard/no-async-without-await": "low",
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
+ "ai-guard/no-redundant-await": "low"
407
+ };
408
+ var ECOSYSTEM_FIX_HINTS = {
409
+ "react-hooks/exhaustive-deps": "npm install --save-dev eslint-plugin-react-hooks",
410
+ "react-hooks/rules-of-hooks": "npm install --save-dev eslint-plugin-react-hooks",
411
+ "@typescript-eslint/": "npm install --save-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser",
412
+ "import/": "npm install --save-dev eslint-plugin-import",
413
+ "jsx-a11y/": "npm install --save-dev eslint-plugin-jsx-a11y",
414
+ "prettier/": "npm install --save-dev eslint-plugin-prettier"
415
+ };
416
+ function getEcosystemFix(ruleId) {
417
+ if (!ruleId) return null;
418
+ for (const [prefix, fix] of Object.entries(ECOSYSTEM_FIX_HINTS)) {
419
+ if (ruleId.startsWith(prefix)) return fix;
420
+ }
421
+ return null;
422
+ }
358
423
  var PREFIX = {
359
424
  success: import_chalk.default.green("\u2714"),
360
425
  error: import_chalk.default.red("\u2716"),
361
426
  warn: import_chalk.default.yellow("\u26A0"),
362
427
  info: import_chalk.default.cyan("\u2139"),
363
428
  section: import_chalk.default.bold.white,
364
- bullet: import_chalk.default.gray("\u2022")
429
+ bullet: import_chalk.default.gray("\u2022"),
430
+ ecosystem: import_chalk.default.yellow("\u26A0"),
431
+ parser: import_chalk.default.magenta("\u26A1")
365
432
  };
366
433
  var log = {
367
434
  info(msg) {
@@ -381,6 +448,14 @@ var log = {
381
448
  console.log(import_chalk.default.bold.cyan(` \u2500\u2500 ${title} \u2500\u2500`));
382
449
  console.log("");
383
450
  },
451
+ /**
452
+ * Ecosystem section — distinct styling to communicate it's NOT ai-guard output
453
+ */
454
+ ecosystemSection(title) {
455
+ console.log("");
456
+ console.log(import_chalk.default.bold.yellow(` \u2500\u2500 ${title} \u2500\u2500`) + import_chalk.default.dim(" (not ai-guard findings)"));
457
+ console.log("");
458
+ },
384
459
  rule(ruleName, count) {
385
460
  console.log(
386
461
  ` ${PREFIX.bullet} ${import_chalk.default.yellow(ruleName)} ${import_chalk.default.gray(`(${count} issue${count !== 1 ? "s" : ""})`)}`
@@ -397,6 +472,53 @@ var log = {
397
472
  ` ${import_chalk.default.gray(`${line}:${col}`).padEnd(12)} ${icon} ${import_chalk.default.white(msg)}`
398
473
  );
399
474
  },
475
+ /**
476
+ * Print an ESLint ecosystem issue (missing rule definition, foreign plugin rule, etc.)
477
+ * with a clear disclaimer that this is NOT an ai-guard finding.
478
+ */
479
+ ecosystemIssue(issue, count = 1) {
480
+ const ruleLabel = issue.ruleId ? import_chalk.default.yellow(issue.ruleId) : import_chalk.default.gray("(no rule id)");
481
+ 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]");
482
+ const countLabel = count > 1 ? import_chalk.default.gray(` \xD7 ${count}`) : "";
483
+ console.log(` ${PREFIX.ecosystem} ${ruleLabel} ${typeLabel}${countLabel}`);
484
+ console.log(` ${import_chalk.default.gray("File:")} ${import_chalk.default.dim(issue.filePath)}${issue.line > 0 ? import_chalk.default.dim(`:${issue.line}`) : ""}`);
485
+ console.log(` ${import_chalk.default.gray("Msg:")} ${import_chalk.default.dim(issue.message.slice(0, 120))}`);
486
+ const fix = getEcosystemFix(issue.ruleId);
487
+ if (fix) {
488
+ console.log(` ${import_chalk.default.cyan("\u2192 Fix:")} ${import_chalk.default.yellow(fix)}`);
489
+ }
490
+ console.log(
491
+ import_chalk.default.dim(` Note: This comes from your ESLint config, not from ai-guard.`)
492
+ );
493
+ console.log("");
494
+ },
495
+ /**
496
+ * Print a parser/syntax error — clearly NOT an ai-guard finding.
497
+ */
498
+ parserError(pe) {
499
+ 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}`) : ""}`);
500
+ console.log(` ${import_chalk.default.dim(pe.message.slice(0, 120))}`);
501
+ console.log("");
502
+ },
503
+ /**
504
+ * Print a category summary line with icon, label, and count.
505
+ */
506
+ category(icon, label, errors, warnings) {
507
+ const parts = [];
508
+ if (errors > 0) parts.push(import_chalk.default.red(`${errors} error${errors !== 1 ? "s" : ""}`));
509
+ if (warnings > 0) parts.push(import_chalk.default.yellow(`${warnings} warning${warnings !== 1 ? "s" : ""}`));
510
+ const counts = parts.length > 0 ? parts.join(import_chalk.default.gray(" \xB7 ")) : import_chalk.default.gray("0");
511
+ const labelPadded = label.padEnd(18);
512
+ console.log(` ${icon} ${import_chalk.default.bold.white(labelPadded)} ${counts}`);
513
+ },
514
+ /**
515
+ * Print the scan stats line.
516
+ */
517
+ scanStats(filesScanned, filesWithIssues, durationMs, preset) {
518
+ console.log(
519
+ ` ${import_chalk.default.gray("Files scanned:")} ${import_chalk.default.white(String(filesScanned))} ${import_chalk.default.gray("\xB7")} ${import_chalk.default.gray("Issues in:")} ${import_chalk.default.white(String(filesWithIssues))} file${filesWithIssues !== 1 ? "s" : ""} ${import_chalk.default.gray("\xB7")} ${import_chalk.default.gray("Duration:")} ${import_chalk.default.white(durationMs + "ms")} ${import_chalk.default.gray("\xB7")} ${import_chalk.default.gray("Preset:")} ${import_chalk.default.cyan(preset)}`
520
+ );
521
+ },
400
522
  divider() {
401
523
  console.log(import_chalk.default.gray(" " + "\u2500".repeat(60)));
402
524
  },
@@ -434,37 +556,47 @@ function normalizePlugin(raw) {
434
556
  );
435
557
  }
436
558
  var RECOMMENDED_RULES = {
559
+ // Reliability
437
560
  "ai-guard/no-empty-catch": "error",
438
- "ai-guard/no-floating-promise": "error",
439
- "ai-guard/no-hardcoded-secret": "error",
440
- "ai-guard/no-eval-dynamic": "error",
441
561
  "ai-guard/no-broad-exception": "warn",
442
- "ai-guard/require-auth-middleware": "warn",
562
+ // Async
563
+ "ai-guard/no-floating-promise": "error",
443
564
  "ai-guard/no-await-in-loop": "warn",
444
565
  "ai-guard/no-async-without-await": "warn",
445
- "ai-guard/no-sql-string-concat": "warn",
446
566
  "ai-guard/no-async-array-callback": "warn",
567
+ // Security
568
+ "ai-guard/no-hardcoded-secret": "error",
569
+ "ai-guard/no-eval-dynamic": "error",
570
+ "ai-guard/no-sql-string-concat": "warn",
447
571
  "ai-guard/no-unsafe-deserialize": "warn",
448
- "ai-guard/require-authz-check": "warn"
572
+ "ai-guard/require-auth-middleware": "warn",
573
+ "ai-guard/require-authz-check": "warn",
574
+ // AI Patterns
575
+ "ai-guard/no-dead-branch": "warn"
449
576
  };
450
577
  var STRICT_RULES = {
578
+ // Reliability
451
579
  "ai-guard/no-empty-catch": "error",
452
580
  "ai-guard/no-broad-exception": "error",
453
581
  "ai-guard/no-catch-log-rethrow": "error",
454
582
  "ai-guard/no-catch-without-use": "error",
583
+ // Async
455
584
  "ai-guard/no-async-array-callback": "error",
456
585
  "ai-guard/no-floating-promise": "error",
457
586
  "ai-guard/no-await-in-loop": "error",
458
587
  "ai-guard/no-async-without-await": "error",
459
588
  "ai-guard/no-redundant-await": "error",
589
+ // Security
460
590
  "ai-guard/no-hardcoded-secret": "error",
461
591
  "ai-guard/no-eval-dynamic": "error",
462
592
  "ai-guard/no-sql-string-concat": "error",
463
593
  "ai-guard/no-unsafe-deserialize": "error",
464
594
  "ai-guard/require-auth-middleware": "error",
465
595
  "ai-guard/require-authz-check": "error",
596
+ // AI Patterns
466
597
  "ai-guard/no-console-in-handler": "error",
467
- "ai-guard/no-duplicate-logic-block": "error"
598
+ "ai-guard/no-duplicate-logic-block": "error",
599
+ "ai-guard/no-dead-branch": "error"
468
600
  };
469
601
  var SECURITY_RULES = {
470
602
  "ai-guard/no-hardcoded-secret": "error",
@@ -490,9 +622,27 @@ var DEFAULT_IGNORE_PATTERNS = [
490
622
  ];
491
623
  function isSkippablePatternError(error) {
492
624
  const msg = error.message.toLowerCase();
493
- const hasNoFilesSignal = msg.includes("no files") || msg.includes("no files matching");
494
- const hasIgnoredSignal = msg.includes("ignored") || msg.includes("all files matched by") || msg.includes("are ignored") || msg.includes("was ignored") || msg.includes("file ignored");
495
- return hasNoFilesSignal || hasIgnoredSignal;
625
+ 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");
626
+ }
627
+ function classifyMessage(msg) {
628
+ if (msg.fatal === true) return "parser-error";
629
+ if (msg.ruleId === null && (msg.message.startsWith("Parsing error") || msg.message.includes("Unexpected token") || msg.message.includes("SyntaxError"))) {
630
+ return "parser-error";
631
+ }
632
+ if (msg.ruleId && msg.ruleId.startsWith("ai-guard/")) {
633
+ return "ai-guard";
634
+ }
635
+ return "ecosystem";
636
+ }
637
+ function classifyEcosystemType(msg) {
638
+ const lower = msg.message.toLowerCase();
639
+ if (lower.includes("definition for rule") && lower.includes("was not found")) {
640
+ return "missing-rule";
641
+ }
642
+ if (lower.includes("configuration") || lower.includes("config")) {
643
+ return "config-error";
644
+ }
645
+ return "foreign-rule";
496
646
  }
497
647
  async function loadPluginModuleFromCwd(cwd) {
498
648
  const { createRequire: createRequire2 } = await import("module");
@@ -520,13 +670,22 @@ async function loadPluginModuleFromCwd(cwd) {
520
670
  );
521
671
  }
522
672
  async function runEslint(options) {
673
+ const overallStart = Date.now();
674
+ const timing = {
675
+ pluginLoadMs: 0,
676
+ parserLoadMs: 0,
677
+ lintMs: 0,
678
+ processMs: 0
679
+ };
523
680
  const { ESLint } = await import("eslint").catch(() => {
524
681
  throw new Error(
525
682
  "ESLint is not installed. Run: npm install --save-dev eslint"
526
683
  );
527
684
  });
685
+ const pluginStart = Date.now();
528
686
  const rawPlugin = await loadPluginModuleFromCwd(process.cwd());
529
687
  const plugin = normalizePlugin(rawPlugin);
688
+ timing.pluginLoadMs = Date.now() - pluginStart;
530
689
  const rules = getRules(options.preset);
531
690
  const resolvedTargetPath = import_path.default.resolve(options.targetPath);
532
691
  if (!import_fs.default.existsSync(resolvedTargetPath)) {
@@ -535,18 +694,9 @@ async function runEslint(options) {
535
694
  const targetStat = import_fs.default.statSync(resolvedTargetPath);
536
695
  const isSingleFileTarget = targetStat.isFile();
537
696
  const eslintCwd = isSingleFileTarget ? import_path.default.dirname(resolvedTargetPath) : resolvedTargetPath;
538
- const startMs = Date.now();
539
- const JS_TS_FILES = [
540
- "**/*.js",
541
- "**/*.jsx",
542
- "**/*.ts",
543
- "**/*.tsx",
544
- "**/*.mts",
545
- "**/*.cts",
546
- "**/*.mjs",
547
- "**/*.cjs"
548
- ];
697
+ const parserStart = Date.now();
549
698
  let tsParser = null;
699
+ let tsParserAvailable = false;
550
700
  try {
551
701
  try {
552
702
  tsParser = require(import_path.default.join(
@@ -558,10 +708,23 @@ async function runEslint(options) {
558
708
  } catch {
559
709
  tsParser = require("@typescript-eslint/parser");
560
710
  }
711
+ tsParserAvailable = true;
561
712
  } catch {
713
+ log.debug("@typescript-eslint/parser not found \u2014 TypeScript files will use espree fallback");
562
714
  }
715
+ timing.parserLoadMs = Date.now() - parserStart;
716
+ const JS_TS_FILES = [
717
+ "**/*.js",
718
+ "**/*.jsx",
719
+ "**/*.ts",
720
+ "**/*.tsx",
721
+ "**/*.mts",
722
+ "**/*.cts",
723
+ "**/*.mjs",
724
+ "**/*.cjs"
725
+ ];
563
726
  const configBlocks = [
564
- // JS/JSX files — default espree parser
727
+ // JS/JSX files — default espree parser with JSX support
565
728
  {
566
729
  files: ["**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"],
567
730
  plugins: { "ai-guard": plugin },
@@ -584,9 +747,9 @@ async function runEslint(options) {
584
747
  languageOptions: {
585
748
  parser: tsParser,
586
749
  parserOptions: {
587
- // project: true would be needed for type-aware rules — skip for speed
588
750
  ecmaVersion: "latest",
589
- sourceType: "module"
751
+ sourceType: "module",
752
+ ecmaFeatures: { jsx: true }
590
753
  }
591
754
  },
592
755
  rules
@@ -595,6 +758,13 @@ async function runEslint(options) {
595
758
  configBlocks.splice(1, 0, {
596
759
  files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
597
760
  plugins: { "ai-guard": plugin },
761
+ languageOptions: {
762
+ parserOptions: {
763
+ ecmaVersion: "latest",
764
+ sourceType: "module",
765
+ ecmaFeatures: { jsx: true }
766
+ }
767
+ },
598
768
  rules
599
769
  });
600
770
  }
@@ -604,67 +774,130 @@ async function runEslint(options) {
604
774
  overrideConfig: configBlocks
605
775
  });
606
776
  const patterns = isSingleFileTarget ? [import_path.default.basename(resolvedTargetPath)] : JS_TS_FILES;
607
- const perPatternResults = await Promise.all(
608
- patterns.map(async (pattern) => {
609
- try {
610
- return await eslint.lintFiles([pattern]);
611
- } catch (err) {
612
- const error = err instanceof Error ? err : new Error(String(err));
613
- if (isSkippablePatternError(error)) {
614
- log.debug(`Skipping pattern '${pattern}' (no lintable files)`);
615
- return [];
616
- }
617
- throw error;
618
- }
619
- })
620
- );
621
- const rawResults = perPatternResults.flat();
622
- const durationMs = Date.now() - startMs;
777
+ const lintStart = Date.now();
778
+ let rawResults;
779
+ try {
780
+ rawResults = await eslint.lintFiles(patterns);
781
+ } catch (err) {
782
+ const error = err instanceof Error ? err : new Error(String(err));
783
+ if (isSkippablePatternError(error)) {
784
+ log.debug("Single-call lintFiles failed with no-files, falling back to per-pattern");
785
+ const perPatternResults = await Promise.all(
786
+ patterns.map(async (pattern) => {
787
+ try {
788
+ return await eslint.lintFiles([pattern]);
789
+ } catch (patternErr) {
790
+ const patternError = patternErr instanceof Error ? patternErr : new Error(String(patternErr));
791
+ if (isSkippablePatternError(patternError)) {
792
+ log.debug(`Skipping pattern '${pattern}' (no lintable files)`);
793
+ return [];
794
+ }
795
+ throw patternError;
796
+ }
797
+ })
798
+ );
799
+ rawResults = perPatternResults.flat();
800
+ } else {
801
+ throw error;
802
+ }
803
+ }
804
+ timing.lintMs = Date.now() - lintStart;
805
+ const processStart = Date.now();
623
806
  const files = [];
807
+ const ecosystemIssues = [];
808
+ const parserErrors = [];
624
809
  const ruleBreakdown = /* @__PURE__ */ new Map();
625
810
  let totalErrors = 0;
626
811
  let totalWarnings = 0;
812
+ const filesScanned = rawResults.length;
627
813
  for (const result of rawResults) {
628
814
  if (result.messages.length === 0) continue;
629
- const issues = result.messages.map((m) => ({
630
- ruleId: m.ruleId ?? "unknown",
631
- severity: m.severity,
632
- message: m.message,
633
- line: m.line,
634
- column: m.column
635
- }));
636
- const errorCount = result.errorCount;
637
- const warningCount = result.warningCount;
638
- totalErrors += errorCount;
639
- totalWarnings += warningCount;
640
- for (const issue of issues) {
815
+ const relPath = import_path.default.relative(process.cwd(), result.filePath);
816
+ const aiGuardIssues = [];
817
+ let fileErrors = 0;
818
+ let fileWarnings = 0;
819
+ for (const m of result.messages) {
820
+ const source = classifyMessage(m);
821
+ if (source === "parser-error") {
822
+ parserErrors.push({
823
+ filePath: relPath,
824
+ message: m.message,
825
+ line: m.line ?? 0
826
+ });
827
+ continue;
828
+ }
829
+ if (source === "ecosystem") {
830
+ ecosystemIssues.push({
831
+ type: classifyEcosystemType(m),
832
+ ruleId: m.ruleId,
833
+ message: m.message,
834
+ filePath: relPath,
835
+ line: m.line ?? 0,
836
+ column: m.column ?? 0
837
+ });
838
+ continue;
839
+ }
840
+ const issue = {
841
+ ruleId: m.ruleId ?? "unknown",
842
+ severity: m.severity,
843
+ message: m.message,
844
+ line: m.line,
845
+ column: m.column
846
+ };
847
+ aiGuardIssues.push(issue);
848
+ if (m.severity === 2) {
849
+ fileErrors++;
850
+ totalErrors++;
851
+ } else {
852
+ fileWarnings++;
853
+ totalWarnings++;
854
+ }
641
855
  ruleBreakdown.set(
642
856
  issue.ruleId,
643
857
  (ruleBreakdown.get(issue.ruleId) ?? 0) + 1
644
858
  );
645
859
  }
646
- const relPath = import_path.default.relative(process.cwd(), result.filePath);
647
- files.push({ filePath: relPath, issues, errorCount, warningCount });
860
+ if (aiGuardIssues.length > 0) {
861
+ files.push({
862
+ filePath: relPath,
863
+ issues: aiGuardIssues,
864
+ errorCount: fileErrors,
865
+ warningCount: fileWarnings
866
+ });
867
+ }
648
868
  }
649
869
  const topFiles = [...files].sort((a, b) => b.issues.length - a.issues.length).slice(0, 10).map((f) => ({ path: f.filePath, count: f.issues.length }));
870
+ timing.processMs = Date.now() - processStart;
871
+ if (options.debugTiming) {
872
+ log.debug(`Plugin load: ${timing.pluginLoadMs}ms`);
873
+ log.debug(`Parser load: ${timing.parserLoadMs}ms`);
874
+ log.debug(`Lint: ${timing.lintMs}ms`);
875
+ log.debug(`Process: ${timing.processMs}ms`);
876
+ log.debug(`Total: ${Date.now() - overallStart}ms`);
877
+ }
650
878
  return {
651
879
  files,
652
880
  totalErrors,
653
881
  totalWarnings,
654
882
  totalIssues: totalErrors + totalWarnings,
883
+ filesScanned,
655
884
  ruleBreakdown,
656
885
  topFiles,
657
- durationMs
886
+ durationMs: Date.now() - overallStart,
887
+ timing,
888
+ ecosystemIssues,
889
+ parserErrors,
890
+ tsParserAvailable
658
891
  };
659
892
  }
660
893
 
661
894
  // cli/commands/run.ts
662
895
  function registerRunCommand(program2) {
663
- 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").option(
896
+ 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(
664
897
  "--max-warnings <n>",
665
- "Fail if warnings exceed this count",
898
+ "Fail with exit code 1 if warnings exceed this count",
666
899
  (value) => Number.parseInt(value, 10)
667
- ).action(async (opts) => {
900
+ ).option("--verbose", "Show all issues \u2014 disable grouping of repeated warnings").option("--debug-timing", "Print per-phase timing diagnostics").action(async (opts) => {
668
901
  if (opts.maxWarnings !== void 0 && (!Number.isInteger(opts.maxWarnings) || opts.maxWarnings < 0)) {
669
902
  log.blank();
670
903
  log.error("--max-warnings must be a non-negative integer.");
@@ -678,13 +911,17 @@ function registerRunCommand(program2) {
678
911
  log.blank();
679
912
  }
680
913
  if (!opts.json) {
681
- log.banner("AI GUARD RESULTS");
914
+ log.banner("AI GUARD");
682
915
  log.blank();
683
916
  }
684
917
  const spinner = opts.json ? null : (0, import_ora.default)({ text: "Scanning\u2026", color: "cyan" }).start();
685
918
  let result;
686
919
  try {
687
- result = await runEslint({ preset, targetPath: opts.path });
920
+ result = await runEslint({
921
+ preset,
922
+ targetPath: opts.path,
923
+ debugTiming: opts.debugTiming
924
+ });
688
925
  spinner?.stop();
689
926
  } catch (err) {
690
927
  spinner?.stop();
@@ -711,69 +948,149 @@ function registerRunCommand(program2) {
711
948
  const jsonOutput = {
712
949
  preset,
713
950
  scannedPath: opts.path,
951
+ filesScanned: result.filesScanned,
714
952
  totalErrors: result.totalErrors,
715
953
  totalWarnings: result.totalWarnings,
716
954
  totalIssues: result.totalIssues,
717
955
  durationMs: result.durationMs,
956
+ timing: result.timing,
718
957
  ruleBreakdown: Object.fromEntries(result.ruleBreakdown),
719
958
  topFiles: result.topFiles,
720
- files: result.files
959
+ files: result.files,
960
+ ecosystemIssues: result.ecosystemIssues,
961
+ parserErrors: result.parserErrors,
962
+ tsParserAvailable: result.tsParserAvailable
721
963
  };
722
964
  console.log(JSON.stringify(jsonOutput, null, 2));
723
965
  process.exit(getRunExitCode(result, opts.maxWarnings));
724
966
  return;
725
967
  }
726
- log.success(`Scanned: ${import_chalk2.default.white(opts.path)}`);
727
- log.success(`Duration: ${import_chalk2.default.white(result.durationMs + "ms")}`);
968
+ log.scanStats(result.filesScanned, result.files.length, result.durationMs, preset);
969
+ if (!result.tsParserAvailable) {
970
+ log.print(
971
+ import_chalk2.default.dim(` \u2139 TypeScript parser not found \u2014 .ts/.tsx files use espree fallback`) + import_chalk2.default.dim(` (install @typescript-eslint/parser for full TS support)`)
972
+ );
973
+ }
728
974
  log.blank();
729
- if (result.totalIssues === 0) {
975
+ if (result.totalIssues === 0 && result.ecosystemIssues.length === 0 && result.parserErrors.length === 0) {
730
976
  log.print(
731
- ` ${import_chalk2.default.green("\u2714")} ${import_chalk2.default.bold.green("No AI issues found \u2014 your code looks clean")}`
977
+ ` ${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`)}`
732
978
  );
733
979
  log.blank();
734
980
  process.exit(0);
735
981
  return;
736
982
  }
737
- log.print(
738
- ` ${import_chalk2.default.bold("Total Issues:")} ${formatIssueCount(result.totalErrors, result.totalWarnings)}`
739
- );
740
- log.blank();
741
- if (result.ruleBreakdown.size > 0) {
742
- log.section("By Rule");
743
- const sorted = [...result.ruleBreakdown.entries()].sort(
744
- (a, b) => b[1] - a[1]
983
+ if (result.totalIssues === 0) {
984
+ log.print(
985
+ ` ${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`)}`
745
986
  );
746
- for (const [rule, count] of sorted) {
747
- const shortRule = rule.replace(/^ai-guard\//, "");
748
- log.print(
749
- ` ${import_chalk2.default.gray("\u2022")} ${import_chalk2.default.yellow(shortRule)}${import_chalk2.default.gray(":")} ${import_chalk2.default.white(String(count))}`
987
+ log.blank();
988
+ }
989
+ if (result.totalIssues > 0) {
990
+ log.section("Summary by Category");
991
+ const categoryErrors = {};
992
+ const categoryWarnings = {};
993
+ for (const file of result.files) {
994
+ for (const issue of file.issues) {
995
+ const cat = RULE_CATEGORY[issue.ruleId] ?? "Other";
996
+ if (issue.severity === 2) {
997
+ categoryErrors[cat] = (categoryErrors[cat] ?? 0) + 1;
998
+ } else {
999
+ categoryWarnings[cat] = (categoryWarnings[cat] ?? 0) + 1;
1000
+ }
1001
+ }
1002
+ }
1003
+ const allCategories = /* @__PURE__ */ new Set([
1004
+ ...CATEGORY_ORDER,
1005
+ ...Object.keys(categoryErrors),
1006
+ ...Object.keys(categoryWarnings)
1007
+ ]);
1008
+ const sortedCategories = [...allCategories].filter(
1009
+ (cat) => (categoryErrors[cat] ?? 0) + (categoryWarnings[cat] ?? 0) > 0
1010
+ );
1011
+ sortedCategories.sort((a, b) => {
1012
+ const ai = CATEGORY_ORDER.indexOf(a);
1013
+ const bi = CATEGORY_ORDER.indexOf(b);
1014
+ if (ai !== -1 && bi !== -1) return ai - bi;
1015
+ if (ai !== -1) return -1;
1016
+ if (bi !== -1) return 1;
1017
+ return a.localeCompare(b);
1018
+ });
1019
+ for (const cat of sortedCategories) {
1020
+ const icon = CATEGORY_ICONS[cat] ?? "\u26AA";
1021
+ log.category(icon, cat, categoryErrors[cat] ?? 0, categoryWarnings[cat] ?? 0);
1022
+ }
1023
+ log.blank();
1024
+ log.print(
1025
+ ` ${import_chalk2.default.bold("Total:")} ${formatIssueCount(result.totalErrors, result.totalWarnings)}`
1026
+ );
1027
+ log.blank();
1028
+ if (result.ruleBreakdown.size > 0) {
1029
+ log.section("By Rule");
1030
+ const sorted = [...result.ruleBreakdown.entries()].sort(
1031
+ (a, b) => b[1] - a[1]
750
1032
  );
1033
+ for (const [rule, count] of sorted) {
1034
+ const shortRule = rule.replace(/^ai-guard\//, "");
1035
+ const tier = CONFIDENCE_TIER[rule];
1036
+ const tierLabel = tier === "high" ? import_chalk2.default.red.dim("[high]") : tier === "medium" ? import_chalk2.default.yellow.dim("[medium]") : import_chalk2.default.gray.dim("[low]");
1037
+ log.print(
1038
+ ` ${import_chalk2.default.gray("\u2022")} ${import_chalk2.default.yellow(shortRule)} ${tierLabel}${import_chalk2.default.gray(":")} ${import_chalk2.default.white(String(count))}`
1039
+ );
1040
+ }
1041
+ log.blank();
1042
+ }
1043
+ if (result.topFiles.length > 0) {
1044
+ log.section("Top Files");
1045
+ for (const { path: fp, count } of result.topFiles) {
1046
+ log.print(
1047
+ ` ${import_chalk2.default.gray("\u2022")} ${import_chalk2.default.white(fp)} ${import_chalk2.default.gray(`(${count})`)}`
1048
+ );
1049
+ }
1050
+ log.blank();
751
1051
  }
1052
+ log.section("Issues by File");
752
1053
  log.blank();
1054
+ const verbose = opts.verbose ?? false;
1055
+ renderIssuesByFile(result, verbose);
753
1056
  }
754
- if (result.topFiles.length > 0) {
755
- log.section("Top Files");
756
- for (const { path: fp, count } of result.topFiles) {
757
- log.print(
758
- ` ${import_chalk2.default.gray("\u2022")} ${import_chalk2.default.white(fp)} ${import_chalk2.default.gray(`(${count})`)}`
759
- );
1057
+ if (result.ecosystemIssues.length > 0) {
1058
+ log.ecosystemSection("ESLint Config Issues");
1059
+ log.print(
1060
+ import_chalk2.default.dim(` These issues come from your project's ESLint config, not from ai-guard.`)
1061
+ );
1062
+ log.blank();
1063
+ const seen = /* @__PURE__ */ new Map();
1064
+ for (const issue of result.ecosystemIssues) {
1065
+ const key = `${issue.ruleId ?? "null"}::${issue.message}`;
1066
+ const existing = seen.get(key);
1067
+ if (existing) {
1068
+ existing.count++;
1069
+ } else {
1070
+ seen.set(key, { issue, count: 1 });
1071
+ }
1072
+ }
1073
+ for (const { issue, count } of seen.values()) {
1074
+ log.ecosystemIssue(issue, count);
760
1075
  }
761
1076
  log.blank();
762
1077
  }
763
- log.section("Issues by File");
764
- log.blank();
765
- for (const file of result.files) {
1078
+ if (result.parserErrors.length > 0) {
1079
+ log.ecosystemSection("Parser Errors");
766
1080
  log.print(
767
- ` ${import_chalk2.default.bold.white(file.filePath)} ` + import_chalk2.default.gray(
768
- `(${file.errorCount} error${file.errorCount !== 1 ? "s" : ""}, ${file.warningCount} warning${file.warningCount !== 1 ? "s" : ""})`
769
- )
1081
+ import_chalk2.default.dim(` These files could not be parsed \u2014 not ai-guard findings.`)
770
1082
  );
771
- for (const issue of file.issues) {
772
- const sev = issue.severity === 2 ? import_chalk2.default.red("error") : import_chalk2.default.yellow(" warn");
773
- const loc = import_chalk2.default.gray(`${String(issue.line)}:${String(issue.column)}`).padEnd(12);
774
- const ruleShort = import_chalk2.default.gray(issue.ruleId.replace(/^ai-guard\//, ""));
1083
+ log.blank();
1084
+ for (const pe of result.parserErrors) {
1085
+ log.parserError(pe);
1086
+ }
1087
+ if (!result.tsParserAvailable) {
1088
+ log.blank();
775
1089
  log.print(
776
- ` ${loc} ${sev} ${import_chalk2.default.white(issue.message)} ${ruleShort}`
1090
+ ` ${import_chalk2.default.cyan("\u2192 Fix:")} ${import_chalk2.default.yellow("npm install --save-dev @typescript-eslint/parser")}`
1091
+ );
1092
+ log.print(
1093
+ import_chalk2.default.dim(` TypeScript parser resolves most .ts/.tsx parse failures.`)
777
1094
  );
778
1095
  }
779
1096
  log.blank();
@@ -781,15 +1098,83 @@ function registerRunCommand(program2) {
781
1098
  log.divider();
782
1099
  log.blank();
783
1100
  log.section("Next Steps");
784
- if (!result.topFiles.length) {
785
- log.info(`Run ${import_chalk2.default.cyan("ai-guard init")} to wire up ESLint for your editor`);
1101
+ if (result.totalIssues > 0) {
1102
+ log.info(`Run ${import_chalk2.default.cyan("ai-guard baseline")} to save these issues and track only new ones`);
1103
+ log.info(`Run ${import_chalk2.default.cyan("ai-guard report")} to generate a shareable HTML report`);
1104
+ log.info(`Run ${import_chalk2.default.cyan("ai-guard ignore")} to suppress dist/build noise`);
1105
+ }
1106
+ if (result.ecosystemIssues.length > 0) {
1107
+ log.info(`Run ${import_chalk2.default.cyan("ai-guard doctor")} to diagnose ESLint config issues`);
1108
+ }
1109
+ if (!opts.verbose && hasGroupedWarnings(result)) {
1110
+ log.info(`Run with ${import_chalk2.default.cyan("--verbose")} to expand grouped warnings`);
786
1111
  }
787
- log.info(`Run ${import_chalk2.default.cyan("ai-guard baseline")} to save these issues and track only new ones`);
788
- log.info(`Run ${import_chalk2.default.cyan("ai-guard ignore")} to suppress dist/build noise`);
789
1112
  log.blank();
790
1113
  process.exit(getRunExitCode(result, opts.maxWarnings));
791
1114
  });
792
1115
  }
1116
+ var GROUP_THRESHOLD = 4;
1117
+ function hasGroupedWarnings(result) {
1118
+ for (const file of result.files) {
1119
+ const ruleCounts = /* @__PURE__ */ new Map();
1120
+ for (const issue of file.issues) {
1121
+ ruleCounts.set(issue.ruleId, (ruleCounts.get(issue.ruleId) ?? 0) + 1);
1122
+ }
1123
+ for (const count of ruleCounts.values()) {
1124
+ if (count >= GROUP_THRESHOLD) return true;
1125
+ }
1126
+ }
1127
+ return false;
1128
+ }
1129
+ function renderIssuesByFile(result, verbose) {
1130
+ for (const file of result.files) {
1131
+ log.print(
1132
+ ` ${import_chalk2.default.bold.white(file.filePath)} ` + import_chalk2.default.gray(
1133
+ `(${file.errorCount} error${file.errorCount !== 1 ? "s" : ""}, ${file.warningCount} warning${file.warningCount !== 1 ? "s" : ""})`
1134
+ )
1135
+ );
1136
+ if (!verbose) {
1137
+ const ruleCounts = /* @__PURE__ */ new Map();
1138
+ for (const issue of file.issues) {
1139
+ const arr = ruleCounts.get(issue.ruleId) ?? [];
1140
+ arr.push(issue);
1141
+ ruleCounts.set(issue.ruleId, arr);
1142
+ }
1143
+ for (const issues of ruleCounts.values()) {
1144
+ const tier = CONFIDENCE_TIER[issues[0].ruleId] ?? "low";
1145
+ if (tier === "low" && issues.length >= GROUP_THRESHOLD) {
1146
+ const sev = issues[0].severity === 2 ? import_chalk2.default.red("error") : import_chalk2.default.yellow(" warn");
1147
+ const shortRule = import_chalk2.default.gray(issues[0].ruleId.replace(/^ai-guard\//, ""));
1148
+ log.print(
1149
+ ` ${sev} ${import_chalk2.default.white(issues[0].message)} ${shortRule}`
1150
+ );
1151
+ log.print(
1152
+ import_chalk2.default.dim(` \u21B3 ${issues.length} occurrences \u2014 run with --verbose to expand`)
1153
+ );
1154
+ } else {
1155
+ for (const issue of issues) {
1156
+ renderSingleIssue(issue);
1157
+ }
1158
+ }
1159
+ }
1160
+ } else {
1161
+ for (const issue of file.issues) {
1162
+ renderSingleIssue(issue);
1163
+ }
1164
+ }
1165
+ log.blank();
1166
+ }
1167
+ }
1168
+ function renderSingleIssue(issue) {
1169
+ const sev = issue.severity === 2 ? import_chalk2.default.red("error") : import_chalk2.default.yellow(" warn");
1170
+ const loc = import_chalk2.default.gray(`${String(issue.line)}:${String(issue.column)}`).padEnd(12);
1171
+ const ruleShort = import_chalk2.default.gray(issue.ruleId.replace(/^ai-guard\//, ""));
1172
+ const tier = CONFIDENCE_TIER[issue.ruleId];
1173
+ const tierTag = tier === "high" ? "" : tier === "medium" ? "" : import_chalk2.default.dim(" [low]");
1174
+ log.print(
1175
+ ` ${loc} ${sev} ${import_chalk2.default.white(issue.message)}${tierTag} ${ruleShort}`
1176
+ );
1177
+ }
793
1178
  function getRunExitCode(result, maxWarnings) {
794
1179
  if (result.totalErrors > 0) return 1;
795
1180
  if (maxWarnings !== void 0 && result.totalWarnings > maxWarnings) return 1;
@@ -1224,6 +1609,7 @@ function registerInitCommand(program2) {
1224
1609
  var import_chalk4 = __toESM(require("chalk"));
1225
1610
  var import_path5 = __toESM(require("path"));
1226
1611
  init_config_manager();
1612
+ var import_meta = {};
1227
1613
  async function probeConfigLoad(configPath, cwd) {
1228
1614
  try {
1229
1615
  const { ESLint } = await import("eslint");
@@ -1334,6 +1720,73 @@ ${env.allConfigPaths.map((p) => ` ${import_path5.default.relative(cwd, p)}
1334
1720
  note: probe.ok ? void 0 : "Run: ai-guard init --dry-run to preview a fresh config"
1335
1721
  });
1336
1722
  }
1723
+ {
1724
+ let utilsOk = false;
1725
+ let utilsVersion = null;
1726
+ try {
1727
+ const { createRequire: createRequire2 } = await import("module");
1728
+ const req = createRequire2(import_path5.default.join(cwd, "package.json"));
1729
+ const pkgPath = req.resolve("@typescript-eslint/utils/package.json");
1730
+ const pkg = JSON.parse(
1731
+ (await import("fs")).readFileSync(pkgPath, "utf-8")
1732
+ );
1733
+ utilsVersion = pkg.version ?? null;
1734
+ utilsOk = true;
1735
+ } catch {
1736
+ }
1737
+ if (!utilsOk) {
1738
+ try {
1739
+ const selfDir = import_path5.default.dirname(
1740
+ new URL(import_meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")
1741
+ );
1742
+ const pkgPath = import_path5.default.join(selfDir, "../../node_modules/@typescript-eslint/utils/package.json");
1743
+ if ((await import("fs")).existsSync(pkgPath)) {
1744
+ utilsOk = true;
1745
+ }
1746
+ } catch {
1747
+ }
1748
+ }
1749
+ checks.push({
1750
+ label: "@typescript-eslint/utils available at runtime",
1751
+ pass: utilsOk,
1752
+ detail: utilsOk ? `Found${utilsVersion ? ` v${utilsVersion}` : ""}` : "Not found \u2014 rule engine may crash on fresh installs",
1753
+ fix: utilsOk ? void 0 : "npm install @typescript-eslint/utils",
1754
+ note: utilsOk ? void 0 : "This is a runtime dependency of eslint-plugin-ai-guard rules."
1755
+ });
1756
+ }
1757
+ {
1758
+ let parserOk = false;
1759
+ let parserVersion = null;
1760
+ try {
1761
+ const { createRequire: createRequire2 } = await import("module");
1762
+ const req = createRequire2(import_path5.default.join(cwd, "package.json"));
1763
+ const pkgPath = req.resolve("@typescript-eslint/parser/package.json");
1764
+ const pkg = JSON.parse(
1765
+ (await import("fs")).readFileSync(pkgPath, "utf-8")
1766
+ );
1767
+ parserVersion = pkg.version ?? null;
1768
+ parserOk = true;
1769
+ } catch {
1770
+ }
1771
+ const hasTsFiles = (() => {
1772
+ try {
1773
+ const { readdirSync } = require("fs");
1774
+ const files = readdirSync(cwd);
1775
+ return files.some((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
1776
+ } catch {
1777
+ return false;
1778
+ }
1779
+ })();
1780
+ if (!parserOk && hasTsFiles) {
1781
+ checks.push({
1782
+ label: "@typescript-eslint/parser installed (TypeScript project)",
1783
+ pass: false,
1784
+ detail: ".ts/.tsx files detected but TypeScript parser not found",
1785
+ fix: "npm install --save-dev @typescript-eslint/parser",
1786
+ note: "TypeScript files will use espree fallback without this \u2014 some rules may not work correctly."
1787
+ });
1788
+ }
1789
+ }
1337
1790
  log.section("Diagnostics");
1338
1791
  let allPassed = true;
1339
1792
  for (const check of checks) {
@@ -2231,11 +2684,466 @@ function registerInitContextCommand(program2) {
2231
2684
  });
2232
2685
  }
2233
2686
 
2687
+ // cli/commands/report.ts
2688
+ var import_fs7 = __toESM(require("fs"));
2689
+ var import_path10 = __toESM(require("path"));
2690
+ var import_ora3 = __toESM(require("ora"));
2691
+ var RULE_META = {
2692
+ "ai-guard/no-empty-catch": { description: "Empty catch blocks silently swallow errors", category: "Reliability" },
2693
+ "ai-guard/no-broad-exception": { description: "Catching all exceptions hides bugs", category: "Reliability" },
2694
+ "ai-guard/no-catch-log-rethrow": { description: "Catch-log-rethrow adds noise without context", category: "Reliability" },
2695
+ "ai-guard/no-catch-without-use": { description: "Caught error is not used", category: "Reliability" },
2696
+ "ai-guard/no-floating-promise": { description: "Unhandled promise can crash silently", category: "Async Stability" },
2697
+ "ai-guard/no-await-in-loop": { description: "Sequential await in loops is slow; use Promise.all", category: "Async Stability" },
2698
+ "ai-guard/no-async-array-callback": { description: "Async callbacks in .map()/.filter() return Promises", category: "Async Stability" },
2699
+ "ai-guard/no-async-without-await": { description: "Async function never uses await", category: "Async Stability" },
2700
+ "ai-guard/no-redundant-await": { description: "Redundant await in return position", category: "Async Stability" },
2701
+ "ai-guard/no-hardcoded-secret": { description: "Secrets should come from environment variables", category: "Security" },
2702
+ "ai-guard/no-eval-dynamic": { description: "eval() with dynamic input is a security risk", category: "Security" },
2703
+ "ai-guard/no-sql-string-concat": { description: "SQL string concatenation enables injection", category: "Security" },
2704
+ "ai-guard/no-unsafe-deserialize": { description: "JSON.parse on untrusted input without validation", category: "Security" },
2705
+ "ai-guard/require-auth-middleware": { description: "Route handler missing authentication middleware", category: "Security" },
2706
+ "ai-guard/require-authz-check": { description: "Missing authorization check for resource access", category: "Security" },
2707
+ "ai-guard/no-console-in-handler": { description: "console.log in route handlers should be a logger", category: "AI Patterns" },
2708
+ "ai-guard/no-duplicate-logic-block": { description: "Duplicate consecutive code blocks should be extracted", category: "AI Patterns" },
2709
+ "ai-guard/no-dead-branch": { description: "Always-true/false condition creates dead or unreachable code", category: "AI Patterns" }
2710
+ };
2711
+ function getProjectName(cwd) {
2712
+ try {
2713
+ const pkgPath = import_path10.default.join(cwd, "package.json");
2714
+ if (import_fs7.default.existsSync(pkgPath)) {
2715
+ const pkg = JSON.parse(import_fs7.default.readFileSync(pkgPath, "utf-8"));
2716
+ if (pkg.name) return pkg.name;
2717
+ }
2718
+ } catch {
2719
+ }
2720
+ return import_path10.default.basename(cwd);
2721
+ }
2722
+ function getPluginVersion(cwd) {
2723
+ try {
2724
+ const pkgPath = import_path10.default.resolve(__dirname, "../../package.json");
2725
+ if (import_fs7.default.existsSync(pkgPath)) {
2726
+ const pkg = JSON.parse(import_fs7.default.readFileSync(pkgPath, "utf-8"));
2727
+ if (pkg.version) return pkg.version;
2728
+ }
2729
+ } catch {
2730
+ }
2731
+ try {
2732
+ const pkgPath = import_path10.default.join(cwd, "node_modules", "eslint-plugin-ai-guard", "package.json");
2733
+ if (import_fs7.default.existsSync(pkgPath)) {
2734
+ const pkg = JSON.parse(import_fs7.default.readFileSync(pkgPath, "utf-8"));
2735
+ if (pkg.version) return pkg.version;
2736
+ }
2737
+ } catch {
2738
+ }
2739
+ return "0.0.0";
2740
+ }
2741
+ function calculateScore(totalErrors, totalWarnings) {
2742
+ const penalty = totalErrors * 5 + totalWarnings * 2;
2743
+ return Math.max(0, Math.min(100, 100 - penalty));
2744
+ }
2745
+ function buildReportData(result, preset, scannedPath, cwd) {
2746
+ const projectName = getProjectName(cwd);
2747
+ const version = getPluginVersion(cwd);
2748
+ const score = calculateScore(result.totalErrors, result.totalWarnings);
2749
+ const topFiles = result.files.sort((a, b) => b.issues.length - a.issues.length).slice(0, 10).map((f) => ({
2750
+ path: f.filePath,
2751
+ count: f.issues.length,
2752
+ errors: f.errorCount,
2753
+ warnings: f.warningCount
2754
+ }));
2755
+ const categoryBreakdown = {};
2756
+ for (const [rule, count] of result.ruleBreakdown.entries()) {
2757
+ const cat = RULE_META[rule]?.category ?? "Other";
2758
+ categoryBreakdown[cat] = (categoryBreakdown[cat] ?? 0) + count;
2759
+ }
2760
+ const sortedRules = [...result.ruleBreakdown.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
2761
+ const topRules = sortedRules.map(([rule, count]) => {
2762
+ const meta = RULE_META[rule] ?? { description: "Rule violation", category: "Other" };
2763
+ let example = null;
2764
+ for (const file of result.files) {
2765
+ const issue = file.issues.find((i) => i.ruleId === rule);
2766
+ if (issue) {
2767
+ example = {
2768
+ file: file.filePath,
2769
+ line: issue.line,
2770
+ column: issue.column,
2771
+ message: issue.message
2772
+ };
2773
+ break;
2774
+ }
2775
+ }
2776
+ return {
2777
+ rule: rule.replace(/^ai-guard\//, ""),
2778
+ count,
2779
+ description: meta.description,
2780
+ category: meta.category,
2781
+ example
2782
+ };
2783
+ });
2784
+ const CONFIDENCE_ASSIGNMENT = {
2785
+ "ai-guard/no-hardcoded-secret": "high",
2786
+ "ai-guard/no-eval-dynamic": "high",
2787
+ "ai-guard/no-floating-promise": "high",
2788
+ "ai-guard/no-empty-catch": "high",
2789
+ "ai-guard/no-sql-string-concat": "medium",
2790
+ "ai-guard/no-await-in-loop": "medium",
2791
+ "ai-guard/require-auth-middleware": "medium",
2792
+ "ai-guard/require-authz-check": "medium",
2793
+ "ai-guard/no-catch-log-rethrow": "medium",
2794
+ "ai-guard/no-catch-without-use": "medium",
2795
+ "ai-guard/no-unsafe-deserialize": "medium",
2796
+ "ai-guard/no-async-without-await": "low",
2797
+ "ai-guard/no-async-array-callback": "low",
2798
+ "ai-guard/no-dead-branch": "low",
2799
+ "ai-guard/no-broad-exception": "low",
2800
+ "ai-guard/no-console-in-handler": "low",
2801
+ "ai-guard/no-duplicate-logic-block": "low",
2802
+ "ai-guard/no-redundant-await": "low"
2803
+ };
2804
+ let highCount = 0;
2805
+ let mediumCount = 0;
2806
+ let lowCount = 0;
2807
+ for (const [rule, count] of result.ruleBreakdown.entries()) {
2808
+ const tier = CONFIDENCE_ASSIGNMENT[rule] ?? "low";
2809
+ if (tier === "high") highCount += count;
2810
+ else if (tier === "medium") mediumCount += count;
2811
+ else lowCount += count;
2812
+ }
2813
+ const ecosystemCount = result.ecosystemIssues?.length ?? 0;
2814
+ const parserErrorCount = result.parserErrors?.length ?? 0;
2815
+ return {
2816
+ projectName,
2817
+ version,
2818
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2819
+ preset,
2820
+ scannedPath,
2821
+ durationMs: result.durationMs,
2822
+ totalIssues: result.totalIssues,
2823
+ totalErrors: result.totalErrors,
2824
+ totalWarnings: result.totalWarnings,
2825
+ score,
2826
+ ruleBreakdown: Object.fromEntries(result.ruleBreakdown),
2827
+ topFiles,
2828
+ topRules,
2829
+ categoryBreakdown,
2830
+ highCount,
2831
+ mediumCount,
2832
+ lowCount,
2833
+ ecosystemCount,
2834
+ parserErrorCount
2835
+ };
2836
+ }
2837
+ function generateHtml(data) {
2838
+ const scoreColor = data.score >= 80 ? "#10b981" : data.score >= 50 ? "#f59e0b" : "#ef4444";
2839
+ const scoreLabel = data.score >= 80 ? "Great" : data.score >= 50 ? "Needs Work" : "Critical";
2840
+ const formattedDate = new Date(data.generatedAt).toLocaleString();
2841
+ const topFilesRows = data.topFiles.length === 0 ? `<tr><td colspan="4" style="text-align:center;color:#6b7280;padding:2rem;">No issues found</td></tr>` : data.topFiles.map((f) => {
2842
+ const heatColor = f.errors > 0 ? `rgba(239,68,68,${Math.min(0.9, 0.2 + f.errors * 0.1)})` : `rgba(245,158,11,${Math.min(0.9, 0.2 + f.warnings * 0.05)})`;
2843
+ return `
2844
+ <tr style="border-bottom:1px solid #1f2937;">
2845
+ <td style="padding:0.75rem 1rem;font-family:monospace;font-size:0.8rem;color:#e5e7eb;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escHtml(f.path)}">${escHtml(f.path)}</td>
2846
+ <td style="padding:0.75rem 1rem;text-align:center;"><span style="background:${heatColor};border-radius:9999px;padding:0.2rem 0.7rem;font-weight:600;font-size:0.85rem;">${f.count}</span></td>
2847
+ <td style="padding:0.75rem 1rem;text-align:center;color:#f87171;">${f.errors}</td>
2848
+ <td style="padding:0.75rem 1rem;text-align:center;color:#fbbf24;">${f.warnings}</td>
2849
+ </tr>`;
2850
+ }).join("");
2851
+ const topRulesCards = data.topRules.length === 0 ? `<div style="text-align:center;color:#6b7280;padding:2rem;">No rules fired \u2014 clean codebase!</div>` : data.topRules.map((r, i) => {
2852
+ const catColor = {
2853
+ "Reliability": "#ec4899",
2854
+ "Async Stability": "#8b5cf6",
2855
+ "Security": "#ef4444",
2856
+ "AI Patterns": "#14b8a6",
2857
+ Other: "#6b7280"
2858
+ };
2859
+ const color = catColor[r.category] ?? "#6b7280";
2860
+ const exampleHtml = r.example ? `<div style="margin-top:0.75rem;background:#0f172a;border-radius:0.5rem;padding:0.75rem;border-left:3px solid ${color};">
2861
+ <div style="font-size:0.75rem;color:#94a3b8;margin-bottom:0.35rem;">${escHtml(r.example.file)}:${r.example.line}:${r.example.column}</div>
2862
+ <code style="font-size:0.8rem;color:#f1f5f9;white-space:pre-wrap;word-break:break-all;">${escHtml(r.example.message)}</code>
2863
+ </div>` : "";
2864
+ return `
2865
+ <div style="background:#111827;border:1px solid #1f2937;border-radius:0.75rem;padding:1.25rem;display:flex;flex-direction:column;gap:0.5rem;">
2866
+ <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:0.5rem;">
2867
+ <div style="display:flex;align-items:center;gap:0.75rem;">
2868
+ <span style="background:#1f2937;color:#9ca3af;font-size:0.75rem;padding:0.15rem 0.5rem;border-radius:9999px;">#${i + 1}</span>
2869
+ <code style="font-size:0.9rem;color:#f9fafb;font-weight:600;">${escHtml(r.rule)}</code>
2870
+ </div>
2871
+ <div style="display:flex;align-items:center;gap:0.5rem;">
2872
+ <span style="background:${color}22;color:${color};border:1px solid ${color}44;font-size:0.72rem;border-radius:9999px;padding:0.15rem 0.6rem;">${escHtml(r.category)}</span>
2873
+ <span style="background:#374151;color:#e5e7eb;font-size:0.8rem;border-radius:9999px;padding:0.2rem 0.7rem;font-weight:600;">${r.count}\xD7</span>
2874
+ </div>
2875
+ </div>
2876
+ <p style="color:#9ca3af;font-size:0.85rem;margin:0;">${escHtml(r.description)}</p>
2877
+ ${exampleHtml}
2878
+ </div>`;
2879
+ }).join("");
2880
+ const categoryCards = Object.entries(data.categoryBreakdown).map(([cat, count]) => {
2881
+ const catColor = {
2882
+ "Reliability": "#ec4899",
2883
+ "Async Stability": "#8b5cf6",
2884
+ "Security": "#ef4444",
2885
+ "AI Patterns": "#14b8a6",
2886
+ Other: "#6b7280"
2887
+ };
2888
+ const color = catColor[cat] ?? "#6b7280";
2889
+ return `
2890
+ <div style="background:#111827;border:1px solid #1f2937;border-radius:0.75rem;padding:1.25rem;display:flex;flex-direction:column;gap:0.5rem;">
2891
+ <div style="font-size:0.8rem;color:#9ca3af;">${escHtml(cat)}</div>
2892
+ <div style="font-size:2rem;font-weight:700;color:${color};">${count}</div>
2893
+ <div style="background:#1f2937;border-radius:9999px;height:4px;overflow:hidden;"><div style="background:${color};height:100%;width:${Math.min(100, count / Math.max(data.totalIssues, 1) * 100)}%;transition:width 0.8s ease;"></div></div>
2894
+ </div>`;
2895
+ }).join("");
2896
+ const cleanBanner = data.totalIssues === 0 ? `<div style="background:#064e3b;border:1px solid #10b981;border-radius:1rem;padding:2rem;text-align:center;margin-bottom:2rem;">
2897
+ <div style="font-size:2.5rem;">\u{1F389}</div>
2898
+ <div style="font-size:1.5rem;font-weight:700;color:#10b981;margin:0.5rem 0;">Clean Codebase!</div>
2899
+ <div style="color:#6ee7b7;">No AI guard issues detected. Your code is production-ready.</div>
2900
+ </div>` : "";
2901
+ return `<!DOCTYPE html>
2902
+ <html lang="en">
2903
+ <head>
2904
+ <meta charset="UTF-8">
2905
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2906
+ <title>AI Guard Report \u2014 ${escHtml(data.projectName)}</title>
2907
+ <script src="https://cdn.tailwindcss.com"></script>
2908
+ <style>
2909
+ * { box-sizing: border-box; margin: 0; padding: 0; }
2910
+ body { background: #030712; color: #f9fafb; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; min-height: 100vh; }
2911
+ @keyframes pulse-score { 0%, 100% { opacity: 1; } 50% { opacity: 0.8; } }
2912
+ @keyframes slide-in { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
2913
+ .card { animation: slide-in 0.4s ease both; }
2914
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
2915
+ ::-webkit-scrollbar-track { background: #111827; }
2916
+ ::-webkit-scrollbar-thumb { background: #374151; border-radius: 3px; }
2917
+ </style>
2918
+ </head>
2919
+ <body>
2920
+
2921
+ <!-- \u2500\u2500 Header \u2500\u2500 -->
2922
+ <header style="background:linear-gradient(135deg,#0f172a 0%,#1e1b4b 50%,#0f172a 100%);border-bottom:1px solid #1f2937;padding:2rem 2rem 1.5rem;">
2923
+ <div style="max-width:1100px;margin:0 auto;">
2924
+ <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem;">
2925
+ <div>
2926
+ <div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:0.4rem;">
2927
+ <span style="font-size:1.5rem;">\u{1F6E1}\uFE0F</span>
2928
+ <span style="font-size:0.85rem;color:#818cf8;font-weight:600;letter-spacing:0.05em;">AI GUARD REPORT</span>
2929
+ </div>
2930
+ <h1 style="font-size:1.8rem;font-weight:700;color:#f9fafb;">${escHtml(data.projectName)}</h1>
2931
+ <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>
2932
+ </div>
2933
+ <div style="text-align:center;">
2934
+ <div style="font-size:0.75rem;color:#6b7280;margin-bottom:0.75rem;letter-spacing:0.05em;">SIGNAL CONFIDENCE</div>
2935
+ <div style="display:flex;flex-direction:column;gap:0.4rem;">
2936
+ <div style="display:flex;align-items:center;gap:0.5rem;">
2937
+ <span style="width:8px;height:8px;border-radius:50%;background:#ef4444;display:inline-block;"></span>
2938
+ <span style="font-size:0.8rem;color:#f87171;font-weight:600;">${data.highCount} high-confidence</span>
2939
+ </div>
2940
+ <div style="display:flex;align-items:center;gap:0.5rem;">
2941
+ <span style="width:8px;height:8px;border-radius:50%;background:#f59e0b;display:inline-block;"></span>
2942
+ <span style="font-size:0.8rem;color:#fbbf24;font-weight:600;">${data.mediumCount} medium-confidence</span>
2943
+ </div>
2944
+ <div style="display:flex;align-items:center;gap:0.5rem;">
2945
+ <span style="width:8px;height:8px;border-radius:50%;background:#6b7280;display:inline-block;"></span>
2946
+ <span style="font-size:0.8rem;color:#9ca3af;font-weight:600;">${data.lowCount} suggestions</span>
2947
+ </div>
2948
+ ${data.ecosystemCount > 0 ? `<div style="display:flex;align-items:center;gap:0.5rem;margin-top:0.25rem;">
2949
+ <span style="font-size:0.75rem;color:#4b5563;">\u26A0 ${data.ecosystemCount} config issue${data.ecosystemCount !== 1 ? "s" : ""} (not ai-guard)</span>
2950
+ </div>` : ""}
2951
+ </div>
2952
+ </div>
2953
+ </div>
2954
+ <p style="color:#4b5563;font-size:0.75rem;margin-top:1rem;">Generated ${escHtml(formattedDate)}</p>
2955
+ </div>
2956
+ </header>
2957
+
2958
+ <!-- \u2500\u2500 Main \u2500\u2500 -->
2959
+ <main style="max-width:1100px;margin:0 auto;padding:2rem;">
2960
+
2961
+ ${cleanBanner}
2962
+
2963
+ <!-- Summary Cards -->
2964
+ <div class="card" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1rem;margin-bottom:2rem;">
2965
+ <div style="background:#111827;border:1px solid #1f2937;border-radius:0.75rem;padding:1.25rem;">
2966
+ <div style="font-size:0.75rem;color:#6b7280;margin-bottom:0.5rem;">TOTAL ISSUES</div>
2967
+ <div style="font-size:2.5rem;font-weight:800;color:#f9fafb;">${data.totalIssues}</div>
2968
+ </div>
2969
+ <div style="background:#111827;border:1px solid #1f2937;border-radius:0.75rem;padding:1.25rem;">
2970
+ <div style="font-size:0.75rem;color:#6b7280;margin-bottom:0.5rem;">ERRORS</div>
2971
+ <div style="font-size:2.5rem;font-weight:800;color:#f87171;">${data.totalErrors}</div>
2972
+ </div>
2973
+ <div style="background:#111827;border:1px solid #1f2937;border-radius:0.75rem;padding:1.25rem;">
2974
+ <div style="font-size:0.75rem;color:#6b7280;margin-bottom:0.5rem;">WARNINGS</div>
2975
+ <div style="font-size:2.5rem;font-weight:800;color:#fbbf24;">${data.totalWarnings}</div>
2976
+ </div>
2977
+ <div style="background:#111827;border:1px solid #1f2937;border-radius:0.75rem;padding:1.25rem;">
2978
+ <div style="font-size:0.75rem;color:#6b7280;margin-bottom:0.5rem;">FILES AFFECTED</div>
2979
+ <div style="font-size:2.5rem;font-weight:800;color:#818cf8;">${data.topFiles.length}</div>
2980
+ </div>
2981
+ <div style="background:#111827;border:1px solid #1f2937;border-radius:0.75rem;padding:1.25rem;">
2982
+ <div style="font-size:0.75rem;color:#6b7280;margin-bottom:0.5rem;">RULES FIRED</div>
2983
+ <div style="font-size:2.5rem;font-weight:800;color:#34d399;">${Object.keys(data.ruleBreakdown).length}</div>
2984
+ </div>
2985
+ </div>
2986
+
2987
+ <!-- Issues by Category -->
2988
+ ${Object.keys(data.categoryBreakdown).length > 0 ? `
2989
+ <section class="card" style="margin-bottom:2rem;">
2990
+ <h2 style="font-size:1rem;font-weight:600;color:#e5e7eb;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem;">
2991
+ <span>\u{1F4CA}</span> Issues by Category
2992
+ </h2>
2993
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1rem;">
2994
+ ${categoryCards}
2995
+ </div>
2996
+ </section>` : ""}
2997
+
2998
+ <!-- Severity Heatmap by File -->
2999
+ <section class="card" style="margin-bottom:2rem;">
3000
+ <h2 style="font-size:1rem;font-weight:600;color:#e5e7eb;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem;">
3001
+ <span>\u{1F525}</span> Severity Heatmap \u2014 Top Files
3002
+ </h2>
3003
+ <div style="background:#111827;border:1px solid #1f2937;border-radius:0.75rem;overflow:hidden;">
3004
+ <table style="width:100%;border-collapse:collapse;">
3005
+ <thead>
3006
+ <tr style="background:#0f172a;text-align:left;">
3007
+ <th style="padding:0.75rem 1rem;font-size:0.75rem;color:#6b7280;font-weight:500;letter-spacing:0.05em;">FILE</th>
3008
+ <th style="padding:0.75rem 1rem;font-size:0.75rem;color:#6b7280;font-weight:500;letter-spacing:0.05em;text-align:center;">ISSUES</th>
3009
+ <th style="padding:0.75rem 1rem;font-size:0.75rem;color:#6b7280;font-weight:500;letter-spacing:0.05em;text-align:center;">ERRORS</th>
3010
+ <th style="padding:0.75rem 1rem;font-size:0.75rem;color:#6b7280;font-weight:500;letter-spacing:0.05em;text-align:center;">WARNINGS</th>
3011
+ </tr>
3012
+ </thead>
3013
+ <tbody>
3014
+ ${topFilesRows}
3015
+ </tbody>
3016
+ </table>
3017
+ </div>
3018
+ </section>
3019
+
3020
+ <!-- Top 5 Rules -->
3021
+ <section class="card" style="margin-bottom:2rem;">
3022
+ <h2 style="font-size:1rem;font-weight:600;color:#e5e7eb;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem;">
3023
+ <span>\u26A1</span> Top Rules That Fired
3024
+ </h2>
3025
+ <div style="display:flex;flex-direction:column;gap:1rem;">
3026
+ ${topRulesCards}
3027
+ </div>
3028
+ </section>
3029
+
3030
+ <!-- Next Steps -->
3031
+ <section class="card" style="margin-bottom:2rem;">
3032
+ <h2 style="font-size:1rem;font-weight:600;color:#e5e7eb;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem;">
3033
+ <span>\u{1F680}</span> Suggested Next Steps
3034
+ </h2>
3035
+ <div style="display:flex;flex-direction:column;gap:0.75rem;">
3036
+ <div style="background:#111827;border:1px solid #1f2937;border-radius:0.75rem;padding:1rem;display:flex;align-items:flex-start;gap:0.75rem;">
3037
+ <span style="font-size:1.2rem;flex-shrink:0;">\u{1F527}</span>
3038
+ <div>
3039
+ <div style="color:#f9fafb;font-size:0.9rem;font-weight:500;margin-bottom:0.2rem;">Auto-fix safe issues</div>
3040
+ <code style="color:#818cf8;font-size:0.8rem;">npx ai-guard run --fix</code>
3041
+ </div>
3042
+ </div>
3043
+ <div style="background:#111827;border:1px solid #1f2937;border-radius:0.75rem;padding:1rem;display:flex;align-items:flex-start;gap:0.75rem;">
3044
+ <span style="font-size:1.2rem;flex-shrink:0;">\u{1F916}</span>
3045
+ <div>
3046
+ <div style="color:#f9fafb;font-size:0.9rem;font-weight:500;margin-bottom:0.2rem;">Prevent future AI-generated issues</div>
3047
+ <code style="color:#818cf8;font-size:0.8rem;">npx ai-guard init-context</code>
3048
+ </div>
3049
+ </div>
3050
+ <div style="background:#111827;border:1px solid #1f2937;border-radius:0.75rem;padding:1rem;display:flex;align-items:flex-start;gap:0.75rem;">
3051
+ <span style="font-size:1.2rem;flex-shrink:0;">\u{1F4CB}</span>
3052
+ <div>
3053
+ <div style="color:#f9fafb;font-size:0.9rem;font-weight:500;margin-bottom:0.2rem;">Save baseline &amp; track only new issues</div>
3054
+ <code style="color:#818cf8;font-size:0.8rem;">npx ai-guard baseline</code>
3055
+ </div>
3056
+ </div>
3057
+ <div style="background:#111827;border:1px solid #1f2937;border-radius:0.75rem;padding:1rem;display:flex;align-items:flex-start;gap:0.75rem;">
3058
+ <span style="font-size:1.2rem;flex-shrink:0;">\u{1F4E4}</span>
3059
+ <div>
3060
+ <div style="color:#f9fafb;font-size:0.9rem;font-weight:500;margin-bottom:0.2rem;">Share this report with your team</div>
3061
+ <div style="color:#6b7280;font-size:0.8rem;">Send the <code style="color:#94a3b8;">ai-guard-report.html</code> file \u2014 it's fully self-contained.</div>
3062
+ </div>
3063
+ </div>
3064
+ </div>
3065
+ </section>
3066
+
3067
+ </main>
3068
+
3069
+ <!-- \u2500\u2500 Footer \u2500\u2500 -->
3070
+ <footer style="border-top:1px solid #1f2937;padding:1.5rem 2rem;text-align:center;">
3071
+ <p style="color:#4b5563;font-size:0.8rem;">
3072
+ Generated by <a href="https://github.com/YashJadhav21/eslint-plugin-ai-guard" style="color:#818cf8;text-decoration:none;">eslint-plugin-ai-guard</a> v${escHtml(data.version)}
3073
+ &nbsp;\xB7&nbsp;
3074
+ <a href="https://github.com/YashJadhav21/eslint-plugin-ai-guard" style="color:#818cf8;text-decoration:none;">GitHub</a>
3075
+ </p>
3076
+ </footer>
3077
+
3078
+ </body>
3079
+ </html>`;
3080
+ }
3081
+ function escHtml(str) {
3082
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3083
+ }
3084
+ function registerReportCommand(program2) {
3085
+ program2.command("report").description(
3086
+ "Generate a beautiful HTML report of AI Guard scan results\nPerfect for sharing with your team or in Slack."
3087
+ ).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) => {
3088
+ const preset = ["recommended", "strict", "security"].includes(opts.preset) ? opts.preset : "recommended";
3089
+ const spinner = opts.json ? null : (0, import_ora3.default)({ text: "Scanning\u2026", color: "cyan" }).start();
3090
+ let result;
3091
+ try {
3092
+ result = await runEslint({ preset, targetPath: opts.path });
3093
+ spinner?.stop();
3094
+ } catch (err) {
3095
+ spinner?.stop();
3096
+ const msg = err instanceof Error ? err.message : String(err);
3097
+ log.error(msg);
3098
+ process.exit(1);
3099
+ return;
3100
+ }
3101
+ const cwd = process.cwd();
3102
+ const reportData = buildReportData(result, preset, opts.path, cwd);
3103
+ if (opts.json) {
3104
+ console.log(JSON.stringify(reportData, null, 2));
3105
+ process.exit(0);
3106
+ return;
3107
+ }
3108
+ log.banner("AI GUARD REPORT");
3109
+ log.blank();
3110
+ const html = generateHtml(reportData);
3111
+ const outputPath = import_path10.default.resolve(cwd, opts.output);
3112
+ import_fs7.default.writeFileSync(outputPath, html, "utf-8");
3113
+ log.success(`Report saved: ${opts.output}`);
3114
+ log.info(
3115
+ `Score: ${reportData.score}/100 \xB7 ${reportData.totalErrors} errors \xB7 ${reportData.totalWarnings} warnings`
3116
+ );
3117
+ log.blank();
3118
+ if (opts.open !== false) {
3119
+ try {
3120
+ const { default: open } = await import("open").catch(() => {
3121
+ return { default: null };
3122
+ });
3123
+ if (open) {
3124
+ await open(outputPath);
3125
+ log.info("Opened in your browser.");
3126
+ } else {
3127
+ const { execSync } = await import("child_process");
3128
+ const cmd = process.platform === "win32" ? `start "" "${outputPath}"` : process.platform === "darwin" ? `open "${outputPath}"` : `xdg-open "${outputPath}"`;
3129
+ execSync(cmd);
3130
+ log.info("Opened in your browser.");
3131
+ }
3132
+ } catch {
3133
+ log.info(`Open manually: file://${outputPath}`);
3134
+ }
3135
+ } else {
3136
+ log.info(`Open: file://${outputPath}`);
3137
+ }
3138
+ log.blank();
3139
+ });
3140
+ }
3141
+
2234
3142
  // cli/index.ts
2235
3143
  function getVersion() {
2236
3144
  try {
2237
- const pkgPath = import_path10.default.resolve(__dirname, "../../package.json");
2238
- const raw = import_fs7.default.readFileSync(pkgPath, "utf-8");
3145
+ const pkgPath = import_path11.default.resolve(__dirname, "../../package.json");
3146
+ const raw = import_fs8.default.readFileSync(pkgPath, "utf-8");
2239
3147
  const pkg = JSON.parse(raw);
2240
3148
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
2241
3149
  } catch {
@@ -2244,7 +3152,7 @@ function getVersion() {
2244
3152
  }
2245
3153
  var program = new import_commander.Command();
2246
3154
  program.name("ai-guard").description(
2247
- "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 doctor Check your setup\n npx ai-guard baseline Save baseline, track new issues only"
3155
+ "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"
2248
3156
  ).version(getVersion(), "-v, --version", "Print version number").helpOption("-h, --help", "Show help");
2249
3157
  registerRunCommand(program);
2250
3158
  registerInitCommand(program);
@@ -2253,6 +3161,7 @@ registerPresetCommand(program);
2253
3161
  registerIgnoreCommand(program);
2254
3162
  registerBaselineCommand(program);
2255
3163
  registerInitContextCommand(program);
3164
+ registerReportCommand(program);
2256
3165
  program.configureOutput({
2257
3166
  writeErr(str) {
2258
3167
  const cleaned = str.replace(/^error:\s*/i, "").trim();