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/README.md +337 -233
- package/dist/cli/index.js +1013 -104
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +23 -4
- package/dist/index.d.ts +23 -4
- package/dist/index.js +385 -55
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +375 -55
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -3
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
|
|
345
|
-
var
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
|
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
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
647
|
-
|
|
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
|
|
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({
|
|
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.
|
|
727
|
-
|
|
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
|
|
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
|
-
|
|
738
|
-
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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.
|
|
755
|
-
log.
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
for (const file of result.files) {
|
|
1078
|
+
if (result.parserErrors.length > 0) {
|
|
1079
|
+
log.ecosystemSection("Parser Errors");
|
|
766
1080
|
log.print(
|
|
767
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
`
|
|
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 (
|
|
785
|
-
log.info(`Run ${import_chalk2.default.cyan("ai-guard
|
|
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 & 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
|
+
\xB7
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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 =
|
|
2238
|
-
const raw =
|
|
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();
|