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