eslint-plugin-ai-guard 1.2.0 → 1.2.2
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 +1676 -333
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +159 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +165 -21
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -30,6 +30,154 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
mod
|
|
31
31
|
));
|
|
32
32
|
|
|
33
|
+
// cli/utils/debug.ts
|
|
34
|
+
var debug_exports = {};
|
|
35
|
+
__export(debug_exports, {
|
|
36
|
+
debugLog: () => debugLog,
|
|
37
|
+
printCIDebug: () => printCIDebug,
|
|
38
|
+
printGitDebug: () => printGitDebug
|
|
39
|
+
});
|
|
40
|
+
function debugLog(message) {
|
|
41
|
+
process.stderr.write(`${DIM("[debug]")} ${message}
|
|
42
|
+
`);
|
|
43
|
+
}
|
|
44
|
+
function printGitDebug(result) {
|
|
45
|
+
const d = result.debugInfo;
|
|
46
|
+
process.stderr.write("\n");
|
|
47
|
+
process.stderr.write(`${LABEL(" Git Resolution Trace")}
|
|
48
|
+
`);
|
|
49
|
+
process.stderr.write(`${DIM(" \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}
|
|
50
|
+
`);
|
|
51
|
+
process.stderr.write(` ${DIM("Mode:")} ${import_chalk4.default.white(result.mode)}
|
|
52
|
+
`);
|
|
53
|
+
if (d.resolvedBase) {
|
|
54
|
+
process.stderr.write(` ${DIM("Base branch:")} ${import_chalk4.default.white(d.resolvedBase)}
|
|
55
|
+
`);
|
|
56
|
+
} else {
|
|
57
|
+
process.stderr.write(` ${DIM("Base branch:")} ${import_chalk4.default.dim("(none \u2014 uncommitted mode)")}
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
if (d.mergeBase) {
|
|
61
|
+
process.stderr.write(` ${DIM("Merge base:")} ${import_chalk4.default.white(d.mergeBase.slice(0, 12))}\u2026
|
|
62
|
+
`);
|
|
63
|
+
}
|
|
64
|
+
if (d.isShallowClone) {
|
|
65
|
+
process.stderr.write(` ${WARN} ${import_chalk4.default.yellow("Shallow clone detected")} \u2014 use fetch-depth: 0 in workflow
|
|
66
|
+
`);
|
|
67
|
+
}
|
|
68
|
+
if (d.isDetachedHead) {
|
|
69
|
+
process.stderr.write(` ${WARN} ${import_chalk4.default.yellow("Detached HEAD")} \u2014 branch context unavailable
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
process.stderr.write("\n");
|
|
73
|
+
process.stderr.write(` ${DIM("Strategies attempted:")}
|
|
74
|
+
`);
|
|
75
|
+
for (const strategy of d.strategiesAttempted) {
|
|
76
|
+
const succeeded = strategy === d.strategySucceeded;
|
|
77
|
+
const icon = succeeded ? OK : ERR;
|
|
78
|
+
const label = succeeded ? import_chalk4.default.green(strategy) : import_chalk4.default.dim(strategy);
|
|
79
|
+
process.stderr.write(` ${icon} ${label}
|
|
80
|
+
`);
|
|
81
|
+
}
|
|
82
|
+
if (d.strategySucceeded) {
|
|
83
|
+
process.stderr.write(`
|
|
84
|
+
${OK} ${import_chalk4.default.green("Succeeded:")} ${d.strategySucceeded}
|
|
85
|
+
`);
|
|
86
|
+
} else {
|
|
87
|
+
process.stderr.write(`
|
|
88
|
+
${ERR} ${import_chalk4.default.red("All strategies failed")}
|
|
89
|
+
`);
|
|
90
|
+
}
|
|
91
|
+
process.stderr.write("\n");
|
|
92
|
+
process.stderr.write(` ${DIM("Files:")}
|
|
93
|
+
`);
|
|
94
|
+
process.stderr.write(` ${ARROW} Detected raw: ${import_chalk4.default.white(d.rawFileCount)}
|
|
95
|
+
`);
|
|
96
|
+
process.stderr.write(` ${ARROW} Kept: ${import_chalk4.default.green(result.files.length)}
|
|
97
|
+
`);
|
|
98
|
+
if (d.droppedByExtension > 0) {
|
|
99
|
+
process.stderr.write(` ${ARROW} Dropped (ext): ${import_chalk4.default.dim(d.droppedByExtension)}
|
|
100
|
+
`);
|
|
101
|
+
}
|
|
102
|
+
if (d.droppedByIgnore > 0) {
|
|
103
|
+
process.stderr.write(` ${ARROW} Dropped (ignored): ${import_chalk4.default.dim(d.droppedByIgnore)}
|
|
104
|
+
`);
|
|
105
|
+
}
|
|
106
|
+
if (d.droppedByWorkingDir > 0) {
|
|
107
|
+
process.stderr.write(` ${ARROW} Dropped (dir): ${import_chalk4.default.dim(d.droppedByWorkingDir)}
|
|
108
|
+
`);
|
|
109
|
+
}
|
|
110
|
+
if (d.droppedMissing > 0) {
|
|
111
|
+
process.stderr.write(` ${ARROW} Dropped (missing): ${import_chalk4.default.dim(d.droppedMissing)}
|
|
112
|
+
`);
|
|
113
|
+
}
|
|
114
|
+
if (result.zeroFilesReason) {
|
|
115
|
+
process.stderr.write("\n");
|
|
116
|
+
process.stderr.write(` ${WARN} ${import_chalk4.default.yellow("Zero files reason:")}
|
|
117
|
+
`);
|
|
118
|
+
process.stderr.write(` ${import_chalk4.default.dim(result.zeroFilesReason)}
|
|
119
|
+
`);
|
|
120
|
+
}
|
|
121
|
+
if (result.files.length > 0) {
|
|
122
|
+
process.stderr.write("\n");
|
|
123
|
+
process.stderr.write(` ${DIM("Changed files (first 20):")}
|
|
124
|
+
`);
|
|
125
|
+
for (const f of result.files.slice(0, 20)) {
|
|
126
|
+
process.stderr.write(` ${import_chalk4.default.dim("\xB7")} ${import_chalk4.default.white(f)}
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
129
|
+
if (result.files.length > 20) {
|
|
130
|
+
process.stderr.write(` ${import_chalk4.default.dim(`\u2026 and ${result.files.length - 20} more`)}
|
|
131
|
+
`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
process.stderr.write("\n");
|
|
135
|
+
}
|
|
136
|
+
function printCIDebug(info) {
|
|
137
|
+
process.stderr.write("\n");
|
|
138
|
+
process.stderr.write(`${LABEL(" CI Environment")}
|
|
139
|
+
`);
|
|
140
|
+
process.stderr.write(`${DIM(" \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}
|
|
141
|
+
`);
|
|
142
|
+
const env = (name, value) => {
|
|
143
|
+
const val = value ? import_chalk4.default.white(value) : import_chalk4.default.dim("(not set)");
|
|
144
|
+
process.stderr.write(` ${DIM(name.padEnd(26))} ${val}
|
|
145
|
+
`);
|
|
146
|
+
};
|
|
147
|
+
env("GITHUB_ACTIONS:", info.isGitHubActions ? "true" : "false");
|
|
148
|
+
env("GITHUB_BASE_REF:", info.githubBaseRef);
|
|
149
|
+
env("GITHUB_SHA:", info.githubSha);
|
|
150
|
+
env("GITHUB_REF:", info.githubRef);
|
|
151
|
+
env("GITHUB_REPOSITORY:", info.githubRepository);
|
|
152
|
+
env("GITHUB_WORKFLOW:", info.githubWorkflow);
|
|
153
|
+
env("GITHUB_RUN_ID:", info.githubRunId);
|
|
154
|
+
env("GITHUB_EVENT_NAME:", info.githubEventName);
|
|
155
|
+
process.stderr.write("\n");
|
|
156
|
+
env("GITHUB_STEP_SUMMARY:", info.githubStepSummary);
|
|
157
|
+
env("GITHUB_OUTPUT:", info.githubOutput);
|
|
158
|
+
process.stderr.write("\n");
|
|
159
|
+
env("Scan preset:", info.preset);
|
|
160
|
+
env("Fail on:", info.failOn);
|
|
161
|
+
env("Scan mode:", info.scanMode);
|
|
162
|
+
if (info.sarifOutputPath) {
|
|
163
|
+
env("SARIF output:", info.sarifOutputPath);
|
|
164
|
+
}
|
|
165
|
+
process.stderr.write("\n");
|
|
166
|
+
}
|
|
167
|
+
var import_chalk4, DIM, LABEL, OK, WARN, ERR, ARROW;
|
|
168
|
+
var init_debug = __esm({
|
|
169
|
+
"cli/utils/debug.ts"() {
|
|
170
|
+
"use strict";
|
|
171
|
+
import_chalk4 = __toESM(require("chalk"));
|
|
172
|
+
DIM = import_chalk4.default.dim;
|
|
173
|
+
LABEL = import_chalk4.default.cyan.bold;
|
|
174
|
+
OK = import_chalk4.default.green("\u2714");
|
|
175
|
+
WARN = import_chalk4.default.yellow("\u26A0");
|
|
176
|
+
ERR = import_chalk4.default.red("\u2716");
|
|
177
|
+
ARROW = import_chalk4.default.dim("\u2192");
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
33
181
|
// cli/utils/config-manager.ts
|
|
34
182
|
var config_manager_exports = {};
|
|
35
183
|
__export(config_manager_exports, {
|
|
@@ -53,14 +201,14 @@ __export(config_manager_exports, {
|
|
|
53
201
|
});
|
|
54
202
|
function backupConfig(configPath) {
|
|
55
203
|
const backupPath = `${configPath}.bak`;
|
|
56
|
-
|
|
204
|
+
import_fs6.default.copyFileSync(configPath, backupPath);
|
|
57
205
|
return backupPath;
|
|
58
206
|
}
|
|
59
207
|
function readConfig(configPath) {
|
|
60
|
-
return
|
|
208
|
+
return import_fs6.default.readFileSync(configPath, "utf-8");
|
|
61
209
|
}
|
|
62
210
|
function writeConfig(configPath, content) {
|
|
63
|
-
|
|
211
|
+
import_fs6.default.writeFileSync(configPath, content, "utf-8");
|
|
64
212
|
}
|
|
65
213
|
function generateFlatConfig(preset) {
|
|
66
214
|
return `import aiGuard from 'eslint-plugin-ai-guard';
|
|
@@ -266,19 +414,19 @@ function addIgnoresToLegacyConfig(existing) {
|
|
|
266
414
|
function getConfigFilePath(configType, cwd = process.cwd()) {
|
|
267
415
|
switch (configType) {
|
|
268
416
|
case "flat-js":
|
|
269
|
-
return
|
|
417
|
+
return import_path6.default.join(cwd, "eslint.config.js");
|
|
270
418
|
case "flat-mjs":
|
|
271
|
-
return
|
|
419
|
+
return import_path6.default.join(cwd, "eslint.config.mjs");
|
|
272
420
|
case "flat-cjs":
|
|
273
|
-
return
|
|
421
|
+
return import_path6.default.join(cwd, "eslint.config.cjs");
|
|
274
422
|
case "eslintrc-js":
|
|
275
|
-
return
|
|
423
|
+
return import_path6.default.join(cwd, ".eslintrc.js");
|
|
276
424
|
case "eslintrc-cjs":
|
|
277
|
-
return
|
|
425
|
+
return import_path6.default.join(cwd, ".eslintrc.cjs");
|
|
278
426
|
case "eslintrc-json":
|
|
279
|
-
return
|
|
427
|
+
return import_path6.default.join(cwd, ".eslintrc.json");
|
|
280
428
|
default:
|
|
281
|
-
return
|
|
429
|
+
return import_path6.default.join(cwd, ".eslintrc.js");
|
|
282
430
|
}
|
|
283
431
|
}
|
|
284
432
|
function validateFlatConfigText(content) {
|
|
@@ -313,12 +461,12 @@ function validateLegacyConfigText(content) {
|
|
|
313
461
|
}
|
|
314
462
|
return problems;
|
|
315
463
|
}
|
|
316
|
-
var
|
|
464
|
+
var import_fs6, import_path6, AI_GUARD_MARKER, DEFAULT_FLAT_IGNORES, DEFAULT_LEGACY_IGNORES;
|
|
317
465
|
var init_config_manager = __esm({
|
|
318
466
|
"cli/utils/config-manager.ts"() {
|
|
319
467
|
"use strict";
|
|
320
|
-
|
|
321
|
-
|
|
468
|
+
import_fs6 = __toESM(require("fs"));
|
|
469
|
+
import_path6 = __toESM(require("path"));
|
|
322
470
|
AI_GUARD_MARKER = "eslint-plugin-ai-guard";
|
|
323
471
|
DEFAULT_FLAT_IGNORES = [
|
|
324
472
|
"node_modules/**",
|
|
@@ -341,17 +489,18 @@ var init_config_manager = __esm({
|
|
|
341
489
|
|
|
342
490
|
// cli/index.ts
|
|
343
491
|
var import_commander = require("commander");
|
|
344
|
-
var
|
|
345
|
-
var
|
|
492
|
+
var import_fs11 = __toESM(require("fs"));
|
|
493
|
+
var import_path14 = __toESM(require("path"));
|
|
346
494
|
|
|
347
495
|
// cli/commands/run.ts
|
|
348
496
|
var import_ora = __toESM(require("ora"));
|
|
349
|
-
var
|
|
497
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
350
498
|
|
|
351
499
|
// cli/utils/eslint-runner.ts
|
|
352
500
|
var import_fs = __toESM(require("fs"));
|
|
353
501
|
var import_path = __toESM(require("path"));
|
|
354
502
|
var import_url = require("url");
|
|
503
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
355
504
|
|
|
356
505
|
// cli/utils/logger.ts
|
|
357
506
|
var import_chalk = __toESM(require("chalk"));
|
|
@@ -396,14 +545,15 @@ var CONFIDENCE_TIER = {
|
|
|
396
545
|
"ai-guard/no-catch-log-rethrow": "medium",
|
|
397
546
|
"ai-guard/no-catch-without-use": "medium",
|
|
398
547
|
"ai-guard/no-unsafe-deserialize": "medium",
|
|
399
|
-
// Low confidence —
|
|
400
|
-
"ai-guard/no-async-without-await": "low",
|
|
548
|
+
// Low confidence — review case-by-case
|
|
401
549
|
"ai-guard/no-async-array-callback": "low",
|
|
402
550
|
"ai-guard/no-dead-branch": "low",
|
|
403
551
|
"ai-guard/no-broad-exception": "low",
|
|
404
552
|
"ai-guard/no-console-in-handler": "low",
|
|
405
553
|
"ai-guard/no-duplicate-logic-block": "low",
|
|
406
|
-
|
|
554
|
+
// Informational — mostly stylistic, high framework FP rate, collapsed by default
|
|
555
|
+
"ai-guard/no-async-without-await": "informational",
|
|
556
|
+
"ai-guard/no-redundant-await": "informational"
|
|
407
557
|
};
|
|
408
558
|
var ECOSYSTEM_FIX_HINTS = {
|
|
409
559
|
"react-hooks/exhaustive-deps": "npm install --save-dev eslint-plugin-react-hooks",
|
|
@@ -541,6 +691,96 @@ var log = {
|
|
|
541
691
|
};
|
|
542
692
|
|
|
543
693
|
// cli/utils/eslint-runner.ts
|
|
694
|
+
var ISSUE_CONFIDENCE = {
|
|
695
|
+
// HIGH — near-certain bugs: false positive rate <2% in real-world codebases
|
|
696
|
+
"ai-guard/no-hardcoded-secret": "high",
|
|
697
|
+
// Literal credential string in source: almost always a mistake
|
|
698
|
+
"ai-guard/no-eval-dynamic": "high",
|
|
699
|
+
// Dynamic eval: no legitimate modern use case
|
|
700
|
+
"ai-guard/no-floating-promise": "high",
|
|
701
|
+
// Unhandled promise: async failures silently swallowed
|
|
702
|
+
// MEDIUM — likely bugs, but legitimate patterns exist
|
|
703
|
+
"ai-guard/no-empty-catch": "medium",
|
|
704
|
+
// CHANGED from high: empty catch is valid for optional ops (JSON.parse, feature detection)
|
|
705
|
+
"ai-guard/no-sql-string-concat": "medium",
|
|
706
|
+
// SQL injection risk, but ORM template literals can FP
|
|
707
|
+
"ai-guard/no-await-in-loop": "medium",
|
|
708
|
+
// Sequential await is often intentional (rate limiting, ordering)
|
|
709
|
+
"ai-guard/require-auth-middleware": "medium",
|
|
710
|
+
// Context-blind: public routes are valid
|
|
711
|
+
"ai-guard/require-authz-check": "medium",
|
|
712
|
+
// Context-blind: some resources are public
|
|
713
|
+
"ai-guard/no-catch-log-rethrow": "medium",
|
|
714
|
+
// Log-then-rethrow is a known antipattern but sometimes intentional
|
|
715
|
+
"ai-guard/no-catch-without-use": "medium",
|
|
716
|
+
// Unused catch param common in TS
|
|
717
|
+
"ai-guard/no-unsafe-deserialize": "medium",
|
|
718
|
+
// JSON.parse is sometimes safe with trusted input
|
|
719
|
+
// LOW — possible improvement, high false-positive rate
|
|
720
|
+
"ai-guard/no-async-array-callback": "low",
|
|
721
|
+
// Promise.all is better, but forEach async is not always a bug
|
|
722
|
+
"ai-guard/no-dead-branch": "low",
|
|
723
|
+
// Static analysis of conditions is inherently imprecise
|
|
724
|
+
"ai-guard/no-broad-exception": "low",
|
|
725
|
+
// Catching Error is common and sometimes correct
|
|
726
|
+
"ai-guard/no-console-in-handler": "low",
|
|
727
|
+
// Stylistic: debug logs left in vs intentional logging
|
|
728
|
+
"ai-guard/no-duplicate-logic-block": "low",
|
|
729
|
+
// May be coincidental similarity, not true duplication
|
|
730
|
+
// INFORMATIONAL — hints only, never fail CI
|
|
731
|
+
"ai-guard/no-async-without-await": "informational",
|
|
732
|
+
// Framework patterns (Next.js RSC, etc.) make this very noisy
|
|
733
|
+
"ai-guard/no-redundant-await": "informational"
|
|
734
|
+
// Explicit await in return is idiomatic in many codebases
|
|
735
|
+
};
|
|
736
|
+
var ISSUE_CATEGORY = {
|
|
737
|
+
"ai-guard/no-floating-promise": "Async Reliability",
|
|
738
|
+
"ai-guard/no-await-in-loop": "Async Reliability",
|
|
739
|
+
"ai-guard/no-async-without-await": "Async Reliability",
|
|
740
|
+
"ai-guard/no-async-array-callback": "Async Reliability",
|
|
741
|
+
"ai-guard/no-redundant-await": "Async Reliability",
|
|
742
|
+
"ai-guard/no-empty-catch": "Reliability",
|
|
743
|
+
"ai-guard/no-broad-exception": "Reliability",
|
|
744
|
+
"ai-guard/no-catch-log-rethrow": "Reliability",
|
|
745
|
+
"ai-guard/no-catch-without-use": "Reliability",
|
|
746
|
+
"ai-guard/no-hardcoded-secret": "Security",
|
|
747
|
+
"ai-guard/no-eval-dynamic": "Security",
|
|
748
|
+
"ai-guard/no-sql-string-concat": "Security",
|
|
749
|
+
"ai-guard/no-unsafe-deserialize": "Security",
|
|
750
|
+
"ai-guard/require-auth-middleware": "Security",
|
|
751
|
+
"ai-guard/require-authz-check": "Security",
|
|
752
|
+
"ai-guard/no-console-in-handler": "AI Patterns",
|
|
753
|
+
"ai-guard/no-duplicate-logic-block": "AI Patterns",
|
|
754
|
+
"ai-guard/no-dead-branch": "AI Patterns"
|
|
755
|
+
};
|
|
756
|
+
var ISSUE_ASYNC_RISK_TYPE = {
|
|
757
|
+
"ai-guard/no-floating-promise": "floating-promise",
|
|
758
|
+
"ai-guard/no-await-in-loop": "sequential-await",
|
|
759
|
+
"ai-guard/no-async-without-await": "unnecessary-async",
|
|
760
|
+
"ai-guard/no-async-array-callback": "async-array-callback",
|
|
761
|
+
"ai-guard/no-redundant-await": "redundant-await"
|
|
762
|
+
};
|
|
763
|
+
var ISSUE_REMEDIATION = {
|
|
764
|
+
"ai-guard/no-floating-promise": "Add `await` before the call, assign to a variable, or add `.catch()` to handle rejection.",
|
|
765
|
+
"ai-guard/no-await-in-loop": "Collect promises in an array and use `await Promise.all(...)` outside the loop.",
|
|
766
|
+
"ai-guard/no-async-without-await": "Remove `async` if no async operations are needed, or add an `await` expression.",
|
|
767
|
+
"ai-guard/no-async-array-callback": "Use `Promise.all(array.map(async item => ...))` to ensure all async callbacks are awaited.",
|
|
768
|
+
"ai-guard/no-redundant-await": "Remove the `await` \u2014 the value is already resolved.",
|
|
769
|
+
"ai-guard/no-empty-catch": "Add error handling in the catch block, or at minimum log the error.",
|
|
770
|
+
"ai-guard/no-broad-exception": "Catch specific error types or re-throw after handling.",
|
|
771
|
+
"ai-guard/no-catch-log-rethrow": "Either log the error OR rethrow it \u2014 doing both duplicates the error in logs.",
|
|
772
|
+
"ai-guard/no-catch-without-use": "Use the caught error variable or rename to `_` to signal intentional suppression.",
|
|
773
|
+
"ai-guard/no-hardcoded-secret": "Move credentials to environment variables: `process.env.SECRET_KEY`.",
|
|
774
|
+
"ai-guard/no-eval-dynamic": "Avoid eval and Function constructors. Use safer alternatives like JSON.parse.",
|
|
775
|
+
"ai-guard/no-sql-string-concat": "Use parameterized queries or an ORM to prevent SQL injection.",
|
|
776
|
+
"ai-guard/no-unsafe-deserialize": "Validate input with a schema library (zod, joi, yup) before parsing.",
|
|
777
|
+
"ai-guard/require-auth-middleware": "Add an authentication middleware before the route handler.",
|
|
778
|
+
"ai-guard/require-authz-check": "Verify the requesting user has permission to access this resource.",
|
|
779
|
+
"ai-guard/no-console-in-handler": "Replace console.log with a structured logger (pino, winston, etc.).",
|
|
780
|
+
"ai-guard/no-duplicate-logic-block": "Extract the duplicated logic into a shared function.",
|
|
781
|
+
"ai-guard/no-dead-branch": "Remove the unreachable branch or fix the condition."
|
|
782
|
+
};
|
|
783
|
+
var _runnerCache = /* @__PURE__ */ new Map();
|
|
544
784
|
function isRecord(value) {
|
|
545
785
|
return typeof value === "object" && value !== null;
|
|
546
786
|
}
|
|
@@ -612,13 +852,40 @@ function getRules(preset) {
|
|
|
612
852
|
return RECOMMENDED_RULES;
|
|
613
853
|
}
|
|
614
854
|
var DEFAULT_IGNORE_PATTERNS = [
|
|
855
|
+
// Core — always skip
|
|
615
856
|
"**/node_modules/**",
|
|
616
857
|
"**/.next/**",
|
|
617
858
|
"**/dist/**",
|
|
618
859
|
"**/build/**",
|
|
619
860
|
"**/coverage/**",
|
|
620
861
|
"**/out/**",
|
|
621
|
-
"**/.git/**"
|
|
862
|
+
"**/.git/**",
|
|
863
|
+
// Build artifacts and caches
|
|
864
|
+
"**/.cache/**",
|
|
865
|
+
"**/.turbo/**",
|
|
866
|
+
"**/.nx/**",
|
|
867
|
+
"**/.parcel-cache/**",
|
|
868
|
+
"**/.webpack/**",
|
|
869
|
+
"**/tmp/**",
|
|
870
|
+
"**/temp/**",
|
|
871
|
+
// Package manager artifacts
|
|
872
|
+
"**/.yarn/**",
|
|
873
|
+
"**/.pnp.*",
|
|
874
|
+
// Framework generated
|
|
875
|
+
"**/.expo/**",
|
|
876
|
+
"**/.svelte-kit/**",
|
|
877
|
+
"**/storybook-static/**",
|
|
878
|
+
"**/.storybook/generated/**",
|
|
879
|
+
// Generated code
|
|
880
|
+
"**/generated/**",
|
|
881
|
+
"**/__generated__/**",
|
|
882
|
+
"**/vendor/**",
|
|
883
|
+
// Test outputs
|
|
884
|
+
"**/test-results/**",
|
|
885
|
+
"**/playwright-report/**",
|
|
886
|
+
// Misc
|
|
887
|
+
"**/.docusaurus/**",
|
|
888
|
+
"**/.vitepress/cache/**"
|
|
622
889
|
];
|
|
623
890
|
function isSkippablePatternError(error) {
|
|
624
891
|
const msg = error.message.toLowerCase();
|
|
@@ -645,8 +912,8 @@ function classifyEcosystemType(msg) {
|
|
|
645
912
|
return "foreign-rule";
|
|
646
913
|
}
|
|
647
914
|
async function loadPluginModuleFromCwd(cwd) {
|
|
648
|
-
const { createRequire:
|
|
649
|
-
const requireFromCwd =
|
|
915
|
+
const { createRequire: createRequire3 } = await import("module");
|
|
916
|
+
const requireFromCwd = createRequire3(import_path.default.join(cwd, "package.json"));
|
|
650
917
|
try {
|
|
651
918
|
const resolved = requireFromCwd.resolve("eslint-plugin-ai-guard");
|
|
652
919
|
return import((0, import_url.pathToFileURL)(resolved).href);
|
|
@@ -669,50 +936,44 @@ async function loadPluginModuleFromCwd(cwd) {
|
|
|
669
936
|
"eslint-plugin-ai-guard is not installed. Run: npm install --save-dev eslint-plugin-ai-guard"
|
|
670
937
|
);
|
|
671
938
|
}
|
|
672
|
-
async function
|
|
673
|
-
const
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
lintMs: 0,
|
|
678
|
-
processMs: 0
|
|
679
|
-
};
|
|
680
|
-
const { ESLint } = await import("eslint").catch(() => {
|
|
681
|
-
throw new Error(
|
|
682
|
-
"ESLint is not installed. Run: npm install --save-dev eslint"
|
|
683
|
-
);
|
|
684
|
-
});
|
|
685
|
-
const pluginStart = Date.now();
|
|
686
|
-
const rawPlugin = await loadPluginModuleFromCwd(process.cwd());
|
|
687
|
-
const plugin = normalizePlugin(rawPlugin);
|
|
688
|
-
timing.pluginLoadMs = Date.now() - pluginStart;
|
|
689
|
-
const rules = getRules(options.preset);
|
|
690
|
-
const resolvedTargetPath = import_path.default.resolve(options.targetPath);
|
|
691
|
-
if (!import_fs.default.existsSync(resolvedTargetPath)) {
|
|
692
|
-
throw new Error(`Path not found: ${options.targetPath}`);
|
|
939
|
+
async function getOrBuildRunner(cwd, preset, eslintCwd) {
|
|
940
|
+
const cacheKey = `${eslintCwd}::${preset}`;
|
|
941
|
+
const cached = _runnerCache.get(cacheKey);
|
|
942
|
+
if (cached) {
|
|
943
|
+
return { runner: cached, fromCache: true };
|
|
693
944
|
}
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
945
|
+
const [{ ESLint }, rawPlugin, tsParserResult] = await Promise.all([
|
|
946
|
+
// 1. Load ESLint
|
|
947
|
+
import("eslint").catch(() => {
|
|
948
|
+
throw new Error("ESLint is not installed. Run: npm install --save-dev eslint");
|
|
949
|
+
}),
|
|
950
|
+
// 2. Load plugin
|
|
951
|
+
loadPluginModuleFromCwd(cwd),
|
|
952
|
+
// 3. Load TS parser (optional — errors are caught)
|
|
953
|
+
Promise.resolve().then(() => {
|
|
954
|
+
const parserStart = Date.now();
|
|
955
|
+
try {
|
|
956
|
+
let tsParser2;
|
|
957
|
+
try {
|
|
958
|
+
tsParser2 = require(import_path.default.join(
|
|
959
|
+
cwd,
|
|
960
|
+
"node_modules",
|
|
961
|
+
"@typescript-eslint",
|
|
962
|
+
"parser"
|
|
963
|
+
));
|
|
964
|
+
} catch {
|
|
965
|
+
tsParser2 = require("@typescript-eslint/parser");
|
|
966
|
+
}
|
|
967
|
+
return { tsParser: tsParser2, available: true, loadMs: Date.now() - parserStart };
|
|
968
|
+
} catch {
|
|
969
|
+
log.debug("@typescript-eslint/parser not found \u2014 TypeScript files will use espree fallback");
|
|
970
|
+
return { tsParser: null, available: false, loadMs: Date.now() - parserStart };
|
|
971
|
+
}
|
|
972
|
+
})
|
|
973
|
+
]);
|
|
974
|
+
const plugin = normalizePlugin(rawPlugin);
|
|
975
|
+
const rules = getRules(preset);
|
|
976
|
+
const { tsParser, available: tsParserAvailable } = tsParserResult;
|
|
716
977
|
const JS_TS_FILES = [
|
|
717
978
|
"**/*.js",
|
|
718
979
|
"**/*.jsx",
|
|
@@ -724,7 +985,9 @@ async function runEslint(options) {
|
|
|
724
985
|
"**/*.cjs"
|
|
725
986
|
];
|
|
726
987
|
const configBlocks = [
|
|
727
|
-
//
|
|
988
|
+
// Ignore block first — ESLint can skip excluded files without processing
|
|
989
|
+
{ ignores: DEFAULT_IGNORE_PATTERNS },
|
|
990
|
+
// JS/JSX — espree parser with JSX support
|
|
728
991
|
{
|
|
729
992
|
files: ["**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"],
|
|
730
993
|
plugins: { "ai-guard": plugin },
|
|
@@ -736,12 +999,10 @@ async function runEslint(options) {
|
|
|
736
999
|
}
|
|
737
1000
|
},
|
|
738
1001
|
rules
|
|
739
|
-
}
|
|
740
|
-
// Ignore generated directories
|
|
741
|
-
{ ignores: DEFAULT_IGNORE_PATTERNS }
|
|
1002
|
+
}
|
|
742
1003
|
];
|
|
743
1004
|
if (tsParser) {
|
|
744
|
-
configBlocks.
|
|
1005
|
+
configBlocks.push({
|
|
745
1006
|
files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
|
|
746
1007
|
plugins: { "ai-guard": plugin },
|
|
747
1008
|
languageOptions: {
|
|
@@ -755,7 +1016,7 @@ async function runEslint(options) {
|
|
|
755
1016
|
rules
|
|
756
1017
|
});
|
|
757
1018
|
} else {
|
|
758
|
-
configBlocks.
|
|
1019
|
+
configBlocks.push({
|
|
759
1020
|
files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
|
|
760
1021
|
plugins: { "ai-guard": plugin },
|
|
761
1022
|
languageOptions: {
|
|
@@ -773,16 +1034,58 @@ async function runEslint(options) {
|
|
|
773
1034
|
overrideConfigFile: true,
|
|
774
1035
|
overrideConfig: configBlocks
|
|
775
1036
|
});
|
|
776
|
-
const
|
|
1037
|
+
const runner = {
|
|
1038
|
+
eslint,
|
|
1039
|
+
tsParserAvailable,
|
|
1040
|
+
pluginLoadMs: 0,
|
|
1041
|
+
// accounted in parallel Promise.all
|
|
1042
|
+
parserLoadMs: tsParserResult.loadMs
|
|
1043
|
+
};
|
|
1044
|
+
_runnerCache.set(cacheKey, runner);
|
|
1045
|
+
return { runner, fromCache: false };
|
|
1046
|
+
}
|
|
1047
|
+
var JS_TS_FILE_PATTERNS = [
|
|
1048
|
+
"**/*.js",
|
|
1049
|
+
"**/*.jsx",
|
|
1050
|
+
"**/*.ts",
|
|
1051
|
+
"**/*.tsx",
|
|
1052
|
+
"**/*.mts",
|
|
1053
|
+
"**/*.cts",
|
|
1054
|
+
"**/*.mjs",
|
|
1055
|
+
"**/*.cjs"
|
|
1056
|
+
];
|
|
1057
|
+
async function runEslint(options) {
|
|
1058
|
+
const overallStart = Date.now();
|
|
1059
|
+
const resolvedTargetPath = import_path.default.resolve(options.targetPath);
|
|
1060
|
+
if (!import_fs.default.existsSync(resolvedTargetPath)) {
|
|
1061
|
+
throw new Error(`Path not found: ${options.targetPath}`);
|
|
1062
|
+
}
|
|
1063
|
+
const targetStat = import_fs.default.statSync(resolvedTargetPath);
|
|
1064
|
+
const isSingleFileTarget = targetStat.isFile();
|
|
1065
|
+
const eslintCwd = isSingleFileTarget ? import_path.default.dirname(resolvedTargetPath) : resolvedTargetPath;
|
|
1066
|
+
const cwd = process.cwd();
|
|
1067
|
+
const initStart = Date.now();
|
|
1068
|
+
const { runner, fromCache } = await getOrBuildRunner(cwd, options.preset, eslintCwd);
|
|
1069
|
+
const initMs = Date.now() - initStart;
|
|
1070
|
+
const timing = {
|
|
1071
|
+
pluginLoadMs: fromCache ? 0 : runner.pluginLoadMs,
|
|
1072
|
+
parserLoadMs: fromCache ? 0 : runner.parserLoadMs,
|
|
1073
|
+
lintMs: 0,
|
|
1074
|
+
processMs: 0,
|
|
1075
|
+
fromCache
|
|
1076
|
+
};
|
|
1077
|
+
const hasExplicitFiles = options.files && options.files.length > 0;
|
|
1078
|
+
const patterns = hasExplicitFiles ? options.files : isSingleFileTarget ? [import_path.default.basename(resolvedTargetPath)] : JS_TS_FILE_PATTERNS;
|
|
777
1079
|
const lintStart = Date.now();
|
|
778
1080
|
let rawResults;
|
|
1081
|
+
const eslint = runner.eslint;
|
|
779
1082
|
try {
|
|
780
1083
|
rawResults = await eslint.lintFiles(patterns);
|
|
781
1084
|
} catch (err) {
|
|
782
1085
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
783
1086
|
if (isSkippablePatternError(error)) {
|
|
784
|
-
log.debug("Single-call lintFiles failed
|
|
785
|
-
const perPatternResults = await Promise.
|
|
1087
|
+
log.debug("Single-call lintFiles failed, falling back to per-pattern");
|
|
1088
|
+
const perPatternResults = await Promise.allSettled(
|
|
786
1089
|
patterns.map(async (pattern) => {
|
|
787
1090
|
try {
|
|
788
1091
|
return await eslint.lintFiles([pattern]);
|
|
@@ -796,8 +1099,10 @@ async function runEslint(options) {
|
|
|
796
1099
|
}
|
|
797
1100
|
})
|
|
798
1101
|
);
|
|
799
|
-
rawResults = perPatternResults.
|
|
1102
|
+
rawResults = perPatternResults.filter((r) => r.status === "fulfilled").flatMap((r) => r.value);
|
|
800
1103
|
} else {
|
|
1104
|
+
const cacheKey = `${eslintCwd}::${options.preset}`;
|
|
1105
|
+
_runnerCache.delete(cacheKey);
|
|
801
1106
|
throw error;
|
|
802
1107
|
}
|
|
803
1108
|
}
|
|
@@ -812,7 +1117,7 @@ async function runEslint(options) {
|
|
|
812
1117
|
const filesScanned = rawResults.length;
|
|
813
1118
|
for (const result of rawResults) {
|
|
814
1119
|
if (result.messages.length === 0) continue;
|
|
815
|
-
const relPath = import_path.default.relative(
|
|
1120
|
+
const relPath = import_path.default.relative(cwd, result.filePath);
|
|
816
1121
|
const aiGuardIssues = [];
|
|
817
1122
|
let fileErrors = 0;
|
|
818
1123
|
let fileWarnings = 0;
|
|
@@ -837,12 +1142,19 @@ async function runEslint(options) {
|
|
|
837
1142
|
});
|
|
838
1143
|
continue;
|
|
839
1144
|
}
|
|
1145
|
+
const ruleId = m.ruleId ?? "unknown";
|
|
840
1146
|
const issue = {
|
|
841
|
-
ruleId
|
|
1147
|
+
ruleId,
|
|
842
1148
|
severity: m.severity,
|
|
843
1149
|
message: m.message,
|
|
844
1150
|
line: m.line,
|
|
845
|
-
column: m.column
|
|
1151
|
+
column: m.column,
|
|
1152
|
+
endLine: m.endLine,
|
|
1153
|
+
endColumn: m.endColumn,
|
|
1154
|
+
confidence: ISSUE_CONFIDENCE[ruleId],
|
|
1155
|
+
category: ISSUE_CATEGORY[ruleId],
|
|
1156
|
+
asyncRiskType: ISSUE_ASYNC_RISK_TYPE[ruleId],
|
|
1157
|
+
remediation: ISSUE_REMEDIATION[ruleId]
|
|
846
1158
|
};
|
|
847
1159
|
aiGuardIssues.push(issue);
|
|
848
1160
|
if (m.severity === 2) {
|
|
@@ -869,11 +1181,12 @@ async function runEslint(options) {
|
|
|
869
1181
|
const topFiles = [...files].sort((a, b) => b.issues.length - a.issues.length).slice(0, 10).map((f) => ({ path: f.filePath, count: f.issues.length }));
|
|
870
1182
|
timing.processMs = Date.now() - processStart;
|
|
871
1183
|
if (options.debugTiming) {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
log.debug(`
|
|
1184
|
+
const totalMs = Date.now() - overallStart;
|
|
1185
|
+
const cacheLabel = fromCache ? import_chalk2.default.green("warm cache") : import_chalk2.default.yellow("cold start");
|
|
1186
|
+
log.debug(`Init: ${initMs}ms (${cacheLabel})`);
|
|
1187
|
+
log.debug(`Lint: ${timing.lintMs}ms`);
|
|
875
1188
|
log.debug(`Process: ${timing.processMs}ms`);
|
|
876
|
-
log.debug(`Total: ${
|
|
1189
|
+
log.debug(`Total: ${totalMs}ms | Files: ${filesScanned} | ${filesScanned > 0 ? Math.round(totalMs / filesScanned) : 0}ms/file`);
|
|
877
1190
|
}
|
|
878
1191
|
return {
|
|
879
1192
|
files,
|
|
@@ -887,17 +1200,698 @@ async function runEslint(options) {
|
|
|
887
1200
|
timing,
|
|
888
1201
|
ecosystemIssues,
|
|
889
1202
|
parserErrors,
|
|
890
|
-
tsParserAvailable
|
|
1203
|
+
tsParserAvailable: runner.tsParserAvailable
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// cli/utils/version.ts
|
|
1208
|
+
var import_module = require("module");
|
|
1209
|
+
var import_url2 = require("url");
|
|
1210
|
+
var import_path2 = __toESM(require("path"));
|
|
1211
|
+
var import_meta = {};
|
|
1212
|
+
var _version;
|
|
1213
|
+
function readVersion() {
|
|
1214
|
+
try {
|
|
1215
|
+
const require_ = (0, import_module.createRequire)(import_meta.url);
|
|
1216
|
+
const pkgPath = import_path2.default.resolve(
|
|
1217
|
+
import_path2.default.dirname((0, import_url2.fileURLToPath)(import_meta.url)),
|
|
1218
|
+
"../../package.json"
|
|
1219
|
+
);
|
|
1220
|
+
const pkg = require_(pkgPath);
|
|
1221
|
+
if (pkg.version) return pkg.version;
|
|
1222
|
+
} catch {
|
|
1223
|
+
}
|
|
1224
|
+
if (typeof __PKG_VERSION__ !== "undefined") {
|
|
1225
|
+
return __PKG_VERSION__;
|
|
1226
|
+
}
|
|
1227
|
+
return "0.0.0";
|
|
1228
|
+
}
|
|
1229
|
+
var PKG_VERSION = _version ?? (_version = readVersion());
|
|
1230
|
+
|
|
1231
|
+
// cli/utils/sarif.ts
|
|
1232
|
+
var RULE_DOCS = {
|
|
1233
|
+
"ai-guard/no-empty-catch": { shortDesc: "Empty catch block silently swallows errors", tags: ["reliability"] },
|
|
1234
|
+
"ai-guard/no-broad-exception": { shortDesc: "Catching broad Exception/Error masks specific failures", tags: ["reliability"] },
|
|
1235
|
+
"ai-guard/no-catch-log-rethrow": { shortDesc: "Catch block logs and rethrows \u2014 use one or the other", tags: ["reliability"] },
|
|
1236
|
+
"ai-guard/no-catch-without-use": { shortDesc: "Caught error variable is not used", tags: ["reliability"] },
|
|
1237
|
+
"ai-guard/no-floating-promise": { shortDesc: "Promise not awaited or error-handled \u2014 async failures will be silently swallowed", tags: ["async", "async-reliability"] },
|
|
1238
|
+
"ai-guard/no-await-in-loop": { shortDesc: "Sequential await in loop \u2014 consider Promise.all for parallel execution", tags: ["async", "async-reliability", "performance"] },
|
|
1239
|
+
"ai-guard/no-async-without-await": { shortDesc: "Async function contains no await expressions", tags: ["async", "async-reliability"] },
|
|
1240
|
+
"ai-guard/no-async-array-callback": { shortDesc: "Async callback in array method may not behave as expected", tags: ["async", "async-reliability"] },
|
|
1241
|
+
"ai-guard/no-redundant-await": { shortDesc: "Redundant await on already-resolved value", tags: ["async"] },
|
|
1242
|
+
"ai-guard/no-hardcoded-secret": { shortDesc: "Hardcoded secret or credential detected", tags: ["security", "secrets"] },
|
|
1243
|
+
"ai-guard/no-eval-dynamic": { shortDesc: "Dynamic eval or Function constructor is dangerous", tags: ["security"] },
|
|
1244
|
+
"ai-guard/no-sql-string-concat": { shortDesc: "SQL query built via string concatenation \u2014 SQL injection risk", tags: ["security", "injection"] },
|
|
1245
|
+
"ai-guard/no-unsafe-deserialize": { shortDesc: "Unsafe deserialization of untrusted data", tags: ["security"] },
|
|
1246
|
+
"ai-guard/require-auth-middleware": { shortDesc: "Route handler appears to lack authentication middleware", tags: ["security", "auth"] },
|
|
1247
|
+
"ai-guard/require-authz-check": { shortDesc: "Handler lacks authorization check", tags: ["security", "auth"] },
|
|
1248
|
+
"ai-guard/no-console-in-handler": { shortDesc: "console.log in request handler \u2014 use structured logging", tags: ["ai-patterns"] },
|
|
1249
|
+
"ai-guard/no-duplicate-logic-block": { shortDesc: "Duplicate logic block detected", tags: ["ai-patterns"] },
|
|
1250
|
+
"ai-guard/no-dead-branch": { shortDesc: "Dead code branch that can never execute", tags: ["ai-patterns"] }
|
|
1251
|
+
};
|
|
1252
|
+
var BASE_DOCS_URL = "https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules";
|
|
1253
|
+
function confidenceToSarifLevel(confidence) {
|
|
1254
|
+
switch (confidence) {
|
|
1255
|
+
case "high":
|
|
1256
|
+
return "error";
|
|
1257
|
+
case "medium":
|
|
1258
|
+
return "warning";
|
|
1259
|
+
case "low":
|
|
1260
|
+
return "warning";
|
|
1261
|
+
case "informational":
|
|
1262
|
+
return "note";
|
|
1263
|
+
default:
|
|
1264
|
+
return "warning";
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
function confidenceToRank(confidence) {
|
|
1268
|
+
switch (confidence) {
|
|
1269
|
+
case "high":
|
|
1270
|
+
return 90;
|
|
1271
|
+
case "medium":
|
|
1272
|
+
return 60;
|
|
1273
|
+
case "low":
|
|
1274
|
+
return 30;
|
|
1275
|
+
case "informational":
|
|
1276
|
+
return 10;
|
|
1277
|
+
default:
|
|
1278
|
+
return 30;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
function confidenceToGitHubSeverity(confidence) {
|
|
1282
|
+
switch (confidence) {
|
|
1283
|
+
case "high":
|
|
1284
|
+
return "high";
|
|
1285
|
+
case "medium":
|
|
1286
|
+
return "medium";
|
|
1287
|
+
case "low":
|
|
1288
|
+
return "low";
|
|
1289
|
+
case "informational":
|
|
1290
|
+
return "recommendation";
|
|
1291
|
+
default:
|
|
1292
|
+
return "low";
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
function filePathToUri(filePath) {
|
|
1296
|
+
return filePath.replace(/\\/g, "/");
|
|
1297
|
+
}
|
|
1298
|
+
function collectUsedRuleIds(result) {
|
|
1299
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1300
|
+
for (const file of result.files) {
|
|
1301
|
+
for (const issue of file.issues) {
|
|
1302
|
+
ids.add(issue.ruleId);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return [...ids].sort();
|
|
1306
|
+
}
|
|
1307
|
+
function buildRuleDescriptors(ruleIds) {
|
|
1308
|
+
return ruleIds.map((id) => {
|
|
1309
|
+
const meta = RULE_DOCS[id];
|
|
1310
|
+
const shortName = id.replace("ai-guard/", "");
|
|
1311
|
+
const confidence = ISSUE_CONFIDENCE[id];
|
|
1312
|
+
const category = ISSUE_CATEGORY[id] ?? "AI Patterns";
|
|
1313
|
+
const remediation = ISSUE_REMEDIATION[id];
|
|
1314
|
+
const helpMarkdown = [
|
|
1315
|
+
`**${meta?.shortDesc ?? shortName}**`,
|
|
1316
|
+
"",
|
|
1317
|
+
remediation ? `**Fix:** ${remediation}` : "",
|
|
1318
|
+
"",
|
|
1319
|
+
`[View rule documentation](${BASE_DOCS_URL}/${shortName}.md)`
|
|
1320
|
+
].filter(Boolean).join("\n");
|
|
1321
|
+
return {
|
|
1322
|
+
id,
|
|
1323
|
+
shortDescription: {
|
|
1324
|
+
text: meta?.shortDesc ?? `ai-guard rule: ${shortName}`
|
|
1325
|
+
},
|
|
1326
|
+
fullDescription: {
|
|
1327
|
+
text: meta?.shortDesc ?? `ai-guard rule: ${shortName}`
|
|
1328
|
+
},
|
|
1329
|
+
help: {
|
|
1330
|
+
text: remediation ?? meta?.shortDesc ?? shortName,
|
|
1331
|
+
markdown: helpMarkdown
|
|
1332
|
+
},
|
|
1333
|
+
helpUri: `${BASE_DOCS_URL}/${shortName}.md`,
|
|
1334
|
+
defaultConfiguration: {
|
|
1335
|
+
level: confidenceToSarifLevel(confidence)
|
|
1336
|
+
},
|
|
1337
|
+
properties: {
|
|
1338
|
+
tags: [...meta?.tags ?? ["ai-guard"], category.toLowerCase().replace(/ /g, "-")],
|
|
1339
|
+
confidence: confidence ?? "low",
|
|
1340
|
+
category,
|
|
1341
|
+
"problem.severity": confidenceToGitHubSeverity(confidence)
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
function buildSarifResult(issue, file) {
|
|
1347
|
+
const confidence = ISSUE_CONFIDENCE[issue.ruleId];
|
|
1348
|
+
const asyncRiskType = ISSUE_ASYNC_RISK_TYPE[issue.ruleId];
|
|
1349
|
+
const category = ISSUE_CATEGORY[issue.ruleId];
|
|
1350
|
+
return {
|
|
1351
|
+
ruleId: issue.ruleId,
|
|
1352
|
+
level: confidenceToSarifLevel(confidence),
|
|
1353
|
+
kind: confidence === "informational" ? "informational" : "fail",
|
|
1354
|
+
rank: confidenceToRank(confidence),
|
|
1355
|
+
message: { text: issue.message },
|
|
1356
|
+
locations: [
|
|
1357
|
+
{
|
|
1358
|
+
physicalLocation: {
|
|
1359
|
+
artifactLocation: {
|
|
1360
|
+
uri: filePathToUri(file.filePath),
|
|
1361
|
+
uriBaseId: "%SRCROOT%"
|
|
1362
|
+
},
|
|
1363
|
+
region: {
|
|
1364
|
+
startLine: Math.max(1, issue.line),
|
|
1365
|
+
startColumn: issue.column > 0 ? issue.column : 1,
|
|
1366
|
+
...issue.endLine !== void 0 && { endLine: issue.endLine },
|
|
1367
|
+
...issue.endColumn !== void 0 && { endColumn: issue.endColumn }
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1370
|
+
message: { text: issue.message }
|
|
1371
|
+
}
|
|
1372
|
+
],
|
|
1373
|
+
properties: {
|
|
1374
|
+
confidence: confidence ?? "low",
|
|
1375
|
+
...asyncRiskType && { asyncRiskType },
|
|
1376
|
+
...category && { category }
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
function buildSarifLog(result, version = PKG_VERSION) {
|
|
1381
|
+
const usedRuleIds = collectUsedRuleIds(result);
|
|
1382
|
+
const rules = buildRuleDescriptors(usedRuleIds);
|
|
1383
|
+
const sarifResults = [];
|
|
1384
|
+
for (const file of result.files) {
|
|
1385
|
+
for (const issue of file.issues) {
|
|
1386
|
+
sarifResults.push(buildSarifResult(issue, file));
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return {
|
|
1390
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
1391
|
+
version: "2.1.0",
|
|
1392
|
+
runs: [
|
|
1393
|
+
{
|
|
1394
|
+
tool: {
|
|
1395
|
+
driver: {
|
|
1396
|
+
name: "ai-guard",
|
|
1397
|
+
version,
|
|
1398
|
+
informationUri: "https://github.com/YashJadhav21/eslint-plugin-ai-guard",
|
|
1399
|
+
rules
|
|
1400
|
+
}
|
|
1401
|
+
},
|
|
1402
|
+
results: sarifResults,
|
|
1403
|
+
properties: {
|
|
1404
|
+
preset: "recommended",
|
|
1405
|
+
filesScanned: result.filesScanned,
|
|
1406
|
+
durationMs: result.durationMs,
|
|
1407
|
+
ecosystemIssuesCount: result.ecosystemIssues.length,
|
|
1408
|
+
parserErrorsCount: result.parserErrors.length
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
]
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
function sarifToJson(log2) {
|
|
1415
|
+
return JSON.stringify(log2, null, 2);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// cli/utils/github-summary.ts
|
|
1419
|
+
var import_fs2 = __toESM(require("fs"));
|
|
1420
|
+
function warnEnvIssue(name, filePath, err) {
|
|
1421
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1422
|
+
process.stderr.write(
|
|
1423
|
+
`[ai-guard] Warning: Could not write to ${name}=${filePath}: ${msg}
|
|
1424
|
+
`
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
function escapeMarkdown(text) {
|
|
1428
|
+
return text.replace(/[|`*_[\]<>]/g, "\\$&");
|
|
1429
|
+
}
|
|
1430
|
+
function tierEmoji(tier) {
|
|
1431
|
+
switch (tier) {
|
|
1432
|
+
case "high":
|
|
1433
|
+
return "\u{1F534}";
|
|
1434
|
+
case "medium":
|
|
1435
|
+
return "\u{1F7E1}";
|
|
1436
|
+
case "low":
|
|
1437
|
+
return "\u{1F535}";
|
|
1438
|
+
case "informational":
|
|
1439
|
+
return "\u2B1C";
|
|
1440
|
+
default:
|
|
1441
|
+
return "\u2B1C";
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
function tierLabel(tier) {
|
|
1445
|
+
switch (tier) {
|
|
1446
|
+
case "high":
|
|
1447
|
+
return "High";
|
|
1448
|
+
case "medium":
|
|
1449
|
+
return "Medium";
|
|
1450
|
+
case "low":
|
|
1451
|
+
return "Low";
|
|
1452
|
+
case "informational":
|
|
1453
|
+
return "Info";
|
|
1454
|
+
default:
|
|
1455
|
+
return "Low";
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
function emitGitHubAnnotations(result) {
|
|
1459
|
+
if (process.env.GITHUB_ACTIONS !== "true") return;
|
|
1460
|
+
for (const file of result.files) {
|
|
1461
|
+
for (const issue of file.issues) {
|
|
1462
|
+
const tier = CONFIDENCE_TIER[issue.ruleId];
|
|
1463
|
+
if (tier === "informational") continue;
|
|
1464
|
+
const level = issue.severity === 2 ? "error" : "warning";
|
|
1465
|
+
const filePath = file.filePath.replace(/\\/g, "/");
|
|
1466
|
+
const shortRule = issue.ruleId.replace("ai-guard/", "");
|
|
1467
|
+
const parts = [`file=${filePath}`, `line=${issue.line}`];
|
|
1468
|
+
if (issue.endLine) parts.push(`endLine=${issue.endLine}`);
|
|
1469
|
+
if (issue.column > 0) parts.push(`col=${issue.column}`);
|
|
1470
|
+
if (issue.endColumn) parts.push(`endColumn=${issue.endColumn}`);
|
|
1471
|
+
parts.push(`title=ai-guard/${shortRule}`);
|
|
1472
|
+
const msg = issue.message.replace(/\n/g, " ");
|
|
1473
|
+
process.stdout.write(`::${level} ${parts.join(",")}::${msg}
|
|
1474
|
+
`);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
function buildGitHubSummaryMarkdown(result, options) {
|
|
1479
|
+
const lines = [];
|
|
1480
|
+
lines.push("## AI Guard \u2014 Async Reliability Report");
|
|
1481
|
+
lines.push("");
|
|
1482
|
+
lines.push(`> **Preset:** \`${options.preset}\` | **Mode:** ${options.scanMode === "changed" ? "Changed files only" : options.scanMode === "staged" ? "Staged files" : "Full scan"}`);
|
|
1483
|
+
if (options.changedFilesCount !== void 0) {
|
|
1484
|
+
lines.push(`> **Files scanned:** ${result.filesScanned} (${options.changedFilesCount} changed)`);
|
|
1485
|
+
} else {
|
|
1486
|
+
lines.push(`> **Files scanned:** ${result.filesScanned}`);
|
|
1487
|
+
}
|
|
1488
|
+
lines.push(`> **Duration:** ${result.durationMs}ms`);
|
|
1489
|
+
lines.push("");
|
|
1490
|
+
if (result.totalIssues === 0) {
|
|
1491
|
+
lines.push("### \u2705 No issues found");
|
|
1492
|
+
lines.push("");
|
|
1493
|
+
lines.push("All scanned files passed AI Guard reliability checks.");
|
|
1494
|
+
lines.push("");
|
|
1495
|
+
return lines.join("\n");
|
|
1496
|
+
}
|
|
1497
|
+
const { high = 0, medium = 0, low = 0, informational = 0 } = buildSignalCounts(result);
|
|
1498
|
+
if (high > 0) {
|
|
1499
|
+
lines.push(`### \u26A0\uFE0F ${high} high-confidence issue${high !== 1 ? "s" : ""} found`);
|
|
1500
|
+
} else if (medium > 0) {
|
|
1501
|
+
lines.push(`### \u{1F535} ${medium} medium-confidence issue${medium !== 1 ? "s" : ""} found`);
|
|
1502
|
+
} else {
|
|
1503
|
+
lines.push("### \u2139\uFE0F Informational hints only");
|
|
1504
|
+
}
|
|
1505
|
+
lines.push("");
|
|
1506
|
+
lines.push("| Category | \u{1F534} High | \u{1F7E1} Medium | \u{1F535} Low | \u2B1C Info |");
|
|
1507
|
+
lines.push("|----------|---------|----------|-------|--------|");
|
|
1508
|
+
const categoryBreakdown = buildCategoryBreakdown(result);
|
|
1509
|
+
for (const [cat, counts] of Object.entries(categoryBreakdown)) {
|
|
1510
|
+
if (counts.high + counts.medium + counts.low + counts.informational === 0) continue;
|
|
1511
|
+
lines.push(`| ${cat} | ${counts.high} | ${counts.medium} | ${counts.low} | ${counts.informational} |`);
|
|
1512
|
+
}
|
|
1513
|
+
lines.push("");
|
|
1514
|
+
const allIssues = result.files.flatMap(
|
|
1515
|
+
(f) => f.issues.map((i) => ({ ...i, filePath: f.filePath }))
|
|
1516
|
+
);
|
|
1517
|
+
const actionableIssues = allIssues.filter((i) => {
|
|
1518
|
+
const tier = CONFIDENCE_TIER[i.ruleId];
|
|
1519
|
+
return tier !== "informational";
|
|
1520
|
+
}).sort((a, b) => {
|
|
1521
|
+
const tierOrder = { high: 0, medium: 1, low: 2, informational: 3 };
|
|
1522
|
+
const ta = CONFIDENCE_TIER[a.ruleId];
|
|
1523
|
+
const tb = CONFIDENCE_TIER[b.ruleId];
|
|
1524
|
+
return (tierOrder[ta ?? "low"] ?? 2) - (tierOrder[tb ?? "low"] ?? 2);
|
|
1525
|
+
}).slice(0, 10);
|
|
1526
|
+
if (actionableIssues.length > 0) {
|
|
1527
|
+
lines.push("### Top Findings");
|
|
1528
|
+
lines.push("");
|
|
1529
|
+
lines.push("| File | Line | Rule | Confidence |");
|
|
1530
|
+
lines.push("|------|------|------|------------|");
|
|
1531
|
+
for (const issue of actionableIssues) {
|
|
1532
|
+
const tier = CONFIDENCE_TIER[issue.ruleId];
|
|
1533
|
+
const shortFile = issue.filePath.replace(/\\/g, "/").split("/").slice(-2).join("/");
|
|
1534
|
+
const shortRule = issue.ruleId.replace("ai-guard/", "");
|
|
1535
|
+
lines.push(
|
|
1536
|
+
`| \`${escapeMarkdown(shortFile)}\` | ${issue.line} | \`${shortRule}\` | ${tierEmoji(tier)} ${tierLabel(tier)} |`
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
lines.push("");
|
|
1540
|
+
}
|
|
1541
|
+
if (informational > 0) {
|
|
1542
|
+
lines.push("<details>");
|
|
1543
|
+
lines.push(`<summary>\u{1F4A1} ${informational} informational hint${informational !== 1 ? "s" : ""} (click to expand)</summary>`);
|
|
1544
|
+
lines.push("");
|
|
1545
|
+
lines.push("These are stylistic hints with higher false-positive rates in frameworks like Next.js and React. Review manually before acting.");
|
|
1546
|
+
lines.push("");
|
|
1547
|
+
const infoIssues = allIssues.filter((i) => {
|
|
1548
|
+
const tier = CONFIDENCE_TIER[i.ruleId];
|
|
1549
|
+
return tier === "informational";
|
|
1550
|
+
}).slice(0, 20);
|
|
1551
|
+
lines.push("| File | Line | Rule |");
|
|
1552
|
+
lines.push("|------|------|------|");
|
|
1553
|
+
for (const issue of infoIssues) {
|
|
1554
|
+
const shortFile = issue.filePath.replace(/\\/g, "/").split("/").slice(-2).join("/");
|
|
1555
|
+
const shortRule = issue.ruleId.replace("ai-guard/", "");
|
|
1556
|
+
lines.push(`| \`${escapeMarkdown(shortFile)}\` | ${issue.line} | \`${shortRule}\` |`);
|
|
1557
|
+
}
|
|
1558
|
+
lines.push("</details>");
|
|
1559
|
+
lines.push("");
|
|
1560
|
+
}
|
|
1561
|
+
if (result.ecosystemIssues.length > 0) {
|
|
1562
|
+
lines.push("<details>");
|
|
1563
|
+
lines.push(`<summary>\u{1F527} ${result.ecosystemIssues.length} ESLint config issue${result.ecosystemIssues.length !== 1 ? "s" : ""} (not ai-guard findings)</summary>`);
|
|
1564
|
+
lines.push("");
|
|
1565
|
+
lines.push("These are from your project's ESLint configuration, not from ai-guard rules. Run `ai-guard doctor` to diagnose.");
|
|
1566
|
+
lines.push("</details>");
|
|
1567
|
+
lines.push("");
|
|
1568
|
+
}
|
|
1569
|
+
lines.push("---");
|
|
1570
|
+
lines.push("");
|
|
1571
|
+
lines.push("**Next steps:**");
|
|
1572
|
+
if (high > 0 || medium > 0) {
|
|
1573
|
+
lines.push("- Review high/medium confidence findings in the **Files changed** tab");
|
|
1574
|
+
lines.push("- Run `ai-guard run --verbose` locally to see full details");
|
|
1575
|
+
}
|
|
1576
|
+
if (informational > 0) {
|
|
1577
|
+
lines.push("- Run `ai-guard run --verbose` to expand informational hints");
|
|
1578
|
+
}
|
|
1579
|
+
lines.push("- Run `ai-guard baseline` to suppress known issues and track only new ones");
|
|
1580
|
+
lines.push("");
|
|
1581
|
+
lines.push("*Powered by [eslint-plugin-ai-guard](https://github.com/YashJadhav21/eslint-plugin-ai-guard)*");
|
|
1582
|
+
return lines.join("\n");
|
|
1583
|
+
}
|
|
1584
|
+
function buildSignalCounts(result) {
|
|
1585
|
+
let high = 0, medium = 0, low = 0, informational = 0;
|
|
1586
|
+
for (const file of result.files) {
|
|
1587
|
+
for (const issue of file.issues) {
|
|
1588
|
+
const tier = CONFIDENCE_TIER[issue.ruleId];
|
|
1589
|
+
if (tier === "high") high++;
|
|
1590
|
+
else if (tier === "medium") medium++;
|
|
1591
|
+
else if (tier === "informational") informational++;
|
|
1592
|
+
else low++;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
return { high, medium, low, informational };
|
|
1596
|
+
}
|
|
1597
|
+
function buildCategoryBreakdown(result) {
|
|
1598
|
+
const breakdown = {};
|
|
1599
|
+
for (const file of result.files) {
|
|
1600
|
+
for (const issue of file.issues) {
|
|
1601
|
+
const cat = RULE_CATEGORY[issue.ruleId] ?? "Other";
|
|
1602
|
+
const tier = CONFIDENCE_TIER[issue.ruleId];
|
|
1603
|
+
if (!breakdown[cat]) breakdown[cat] = { high: 0, medium: 0, low: 0, informational: 0 };
|
|
1604
|
+
if (tier === "high") breakdown[cat].high++;
|
|
1605
|
+
else if (tier === "medium") breakdown[cat].medium++;
|
|
1606
|
+
else if (tier === "informational") breakdown[cat].informational++;
|
|
1607
|
+
else breakdown[cat].low++;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return breakdown;
|
|
1611
|
+
}
|
|
1612
|
+
function writeGitHubSummary(result, options) {
|
|
1613
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
1614
|
+
if (!summaryPath) return;
|
|
1615
|
+
try {
|
|
1616
|
+
const markdown = buildGitHubSummaryMarkdown(result, options);
|
|
1617
|
+
import_fs2.default.appendFileSync(summaryPath, markdown + "\n", { encoding: "utf-8" });
|
|
1618
|
+
} catch (err) {
|
|
1619
|
+
warnEnvIssue("GITHUB_STEP_SUMMARY", summaryPath, err);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
function writeGitHubOutputs(result, sarifPath) {
|
|
1623
|
+
const outputPath = process.env.GITHUB_OUTPUT;
|
|
1624
|
+
if (!outputPath) return;
|
|
1625
|
+
const { high, medium, low, informational } = buildSignalCounts(result);
|
|
1626
|
+
const outputs = [
|
|
1627
|
+
`issues-found=${result.totalIssues}`,
|
|
1628
|
+
`high-confidence-count=${high}`,
|
|
1629
|
+
`medium-confidence-count=${medium}`,
|
|
1630
|
+
`low-confidence-count=${low}`,
|
|
1631
|
+
`informational-count=${informational}`,
|
|
1632
|
+
`files-scanned=${result.filesScanned}`,
|
|
1633
|
+
`duration-ms=${result.durationMs}`,
|
|
1634
|
+
...sarifPath ? [`sarif-file=${sarifPath}`] : []
|
|
1635
|
+
];
|
|
1636
|
+
try {
|
|
1637
|
+
import_fs2.default.appendFileSync(outputPath, outputs.join("\n") + "\n", { encoding: "utf-8" });
|
|
1638
|
+
} catch (err) {
|
|
1639
|
+
warnEnvIssue("GITHUB_OUTPUT", outputPath, err);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// cli/utils/git-diff.ts
|
|
1644
|
+
var import_child_process = require("child_process");
|
|
1645
|
+
var import_path3 = __toESM(require("path"));
|
|
1646
|
+
var import_fs3 = __toESM(require("fs"));
|
|
1647
|
+
var DEFAULT_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"];
|
|
1648
|
+
var IGNORE_PREFIXES = [
|
|
1649
|
+
"node_modules/",
|
|
1650
|
+
"dist/",
|
|
1651
|
+
"build/",
|
|
1652
|
+
".next/",
|
|
1653
|
+
"out/",
|
|
1654
|
+
"coverage/",
|
|
1655
|
+
".cache/",
|
|
1656
|
+
".turbo/",
|
|
1657
|
+
".nx/",
|
|
1658
|
+
"vendor/",
|
|
1659
|
+
"tmp/",
|
|
1660
|
+
"temp/",
|
|
1661
|
+
"__generated__/",
|
|
1662
|
+
"generated/",
|
|
1663
|
+
"storybook-static/",
|
|
1664
|
+
".expo/",
|
|
1665
|
+
".svelte-kit/"
|
|
1666
|
+
];
|
|
1667
|
+
function runGit(args, cwd) {
|
|
1668
|
+
try {
|
|
1669
|
+
return (0, import_child_process.execSync)(`git ${args}`, {
|
|
1670
|
+
cwd,
|
|
1671
|
+
encoding: "utf-8",
|
|
1672
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1673
|
+
timeout: 15e3
|
|
1674
|
+
}).trim();
|
|
1675
|
+
} catch {
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
function isGitAvailable(cwd) {
|
|
1680
|
+
return runGit("rev-parse --is-inside-work-tree", cwd) === "true";
|
|
1681
|
+
}
|
|
1682
|
+
function isShallowClone(cwd) {
|
|
1683
|
+
return runGit("rev-parse --is-shallow-repository", cwd) === "true";
|
|
1684
|
+
}
|
|
1685
|
+
function isDetachedHead(cwd) {
|
|
1686
|
+
const result = runGit("symbolic-ref --quiet HEAD", cwd);
|
|
1687
|
+
return result === null;
|
|
1688
|
+
}
|
|
1689
|
+
function normalizeBranchName(ref) {
|
|
1690
|
+
return ref.replace(/^origin\//, "").trim();
|
|
1691
|
+
}
|
|
1692
|
+
function tryPrDiff(branch, cwd, debugInfo) {
|
|
1693
|
+
const rawBranch = normalizeBranchName(branch);
|
|
1694
|
+
{
|
|
1695
|
+
const strategy = `merge-base HEAD origin/${rawBranch}`;
|
|
1696
|
+
debugInfo.strategiesAttempted.push(strategy);
|
|
1697
|
+
const mergeBase = runGit(`merge-base HEAD origin/${rawBranch}`, cwd);
|
|
1698
|
+
if (mergeBase) {
|
|
1699
|
+
debugInfo.mergeBase = mergeBase;
|
|
1700
|
+
const out = runGit(`diff --name-only --diff-filter=ACMRT ${mergeBase}`, cwd);
|
|
1701
|
+
if (out !== null) {
|
|
1702
|
+
debugInfo.strategySucceeded = strategy;
|
|
1703
|
+
return { output: out, strategy, mergeBase };
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
{
|
|
1708
|
+
const strategy = `merge-base HEAD ${rawBranch}`;
|
|
1709
|
+
debugInfo.strategiesAttempted.push(strategy);
|
|
1710
|
+
const mergeBase = runGit(`merge-base HEAD ${rawBranch}`, cwd);
|
|
1711
|
+
if (mergeBase) {
|
|
1712
|
+
if (!debugInfo.mergeBase) debugInfo.mergeBase = mergeBase;
|
|
1713
|
+
const out = runGit(`diff --name-only --diff-filter=ACMRT ${mergeBase}`, cwd);
|
|
1714
|
+
if (out !== null) {
|
|
1715
|
+
debugInfo.strategySucceeded = strategy;
|
|
1716
|
+
return { output: out, strategy, mergeBase };
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
{
|
|
1721
|
+
const strategy = `diff origin/${rawBranch}...HEAD`;
|
|
1722
|
+
debugInfo.strategiesAttempted.push(strategy);
|
|
1723
|
+
const out = runGit(`diff --name-only --diff-filter=ACMRT origin/${rawBranch}...HEAD`, cwd);
|
|
1724
|
+
if (out !== null) {
|
|
1725
|
+
debugInfo.strategySucceeded = strategy;
|
|
1726
|
+
return { output: out, strategy };
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
{
|
|
1730
|
+
const strategy = `diff ${rawBranch}...HEAD`;
|
|
1731
|
+
debugInfo.strategiesAttempted.push(strategy);
|
|
1732
|
+
const out = runGit(`diff --name-only --diff-filter=ACMRT ${rawBranch}...HEAD`, cwd);
|
|
1733
|
+
if (out !== null) {
|
|
1734
|
+
debugInfo.strategySucceeded = strategy;
|
|
1735
|
+
return { output: out, strategy };
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
{
|
|
1739
|
+
const strategy = "diff HEAD~1 (shallow fallback)";
|
|
1740
|
+
debugInfo.strategiesAttempted.push(strategy);
|
|
1741
|
+
const out = runGit("diff --name-only --diff-filter=ACMRT HEAD~1", cwd);
|
|
1742
|
+
if (out !== null) {
|
|
1743
|
+
debugInfo.strategySucceeded = strategy;
|
|
1744
|
+
return { output: out, strategy };
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
debugInfo.strategySucceeded = null;
|
|
1748
|
+
return null;
|
|
1749
|
+
}
|
|
1750
|
+
function filterFiles(rawFiles, extensions, workingDirectory, cwd, debugInfo) {
|
|
1751
|
+
const kept = [];
|
|
1752
|
+
for (const rel of rawFiles) {
|
|
1753
|
+
const normalized = rel.replace(/\\/g, "/").trim();
|
|
1754
|
+
if (!normalized) continue;
|
|
1755
|
+
const ext = import_path3.default.extname(normalized).toLowerCase();
|
|
1756
|
+
if (!extensions.includes(ext)) {
|
|
1757
|
+
debugInfo.droppedByExtension++;
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
if (IGNORE_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
|
|
1761
|
+
debugInfo.droppedByIgnore++;
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
if (workingDirectory) {
|
|
1765
|
+
const wdNorm = workingDirectory.replace(/\\/g, "/").replace(/\/$/, "");
|
|
1766
|
+
if (!normalized.startsWith(wdNorm + "/") && normalized !== wdNorm) {
|
|
1767
|
+
debugInfo.droppedByWorkingDir++;
|
|
1768
|
+
continue;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
const abs = import_path3.default.resolve(cwd, normalized);
|
|
1772
|
+
if (!import_fs3.default.existsSync(abs)) {
|
|
1773
|
+
debugInfo.droppedMissing++;
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
kept.push(abs);
|
|
1777
|
+
}
|
|
1778
|
+
return kept;
|
|
1779
|
+
}
|
|
1780
|
+
function parseFileList(output) {
|
|
1781
|
+
return output.split("\n").map((f) => f.trim()).filter(Boolean);
|
|
1782
|
+
}
|
|
1783
|
+
function getChangedFiles(options = {}) {
|
|
1784
|
+
const {
|
|
1785
|
+
staged = false,
|
|
1786
|
+
base,
|
|
1787
|
+
cwd = process.cwd(),
|
|
1788
|
+
extensions = DEFAULT_EXTENSIONS,
|
|
1789
|
+
workingDirectory,
|
|
1790
|
+
debug = false
|
|
1791
|
+
} = options;
|
|
1792
|
+
const debugInfo = {
|
|
1793
|
+
strategiesAttempted: [],
|
|
1794
|
+
strategySucceeded: null,
|
|
1795
|
+
isShallowClone: false,
|
|
1796
|
+
isDetachedHead: false,
|
|
1797
|
+
rawFileCount: 0,
|
|
1798
|
+
droppedByExtension: 0,
|
|
1799
|
+
droppedByIgnore: 0,
|
|
1800
|
+
droppedMissing: 0,
|
|
1801
|
+
droppedByWorkingDir: 0
|
|
1802
|
+
};
|
|
1803
|
+
if (!isGitAvailable(cwd)) {
|
|
1804
|
+
return {
|
|
1805
|
+
files: [],
|
|
1806
|
+
mode: "fallback",
|
|
1807
|
+
totalDetected: 0,
|
|
1808
|
+
filteredOut: 0,
|
|
1809
|
+
debugInfo,
|
|
1810
|
+
zeroFilesReason: "Git is not available in this directory. Run inside a git repository."
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
debugInfo.isShallowClone = isShallowClone(cwd);
|
|
1814
|
+
debugInfo.isDetachedHead = isDetachedHead(cwd);
|
|
1815
|
+
const envBase = process.env.GITHUB_BASE_REF ? normalizeBranchName(process.env.GITHUB_BASE_REF) : void 0;
|
|
1816
|
+
const resolvedBranch = base ? normalizeBranchName(base) : envBase;
|
|
1817
|
+
debugInfo.resolvedBase = resolvedBranch;
|
|
1818
|
+
let rawOutput = null;
|
|
1819
|
+
let mode;
|
|
1820
|
+
let usedBase;
|
|
1821
|
+
let zeroFilesReason;
|
|
1822
|
+
if (staged) {
|
|
1823
|
+
rawOutput = runGit("diff --name-only --cached --diff-filter=ACMRT", cwd);
|
|
1824
|
+
mode = "staged";
|
|
1825
|
+
debugInfo.strategiesAttempted.push("git diff --cached");
|
|
1826
|
+
if (rawOutput !== null) debugInfo.strategySucceeded = "git diff --cached";
|
|
1827
|
+
if (!rawOutput) {
|
|
1828
|
+
zeroFilesReason = "No staged files found. Run `git add <files>` before using --staged.";
|
|
1829
|
+
}
|
|
1830
|
+
} else if (resolvedBranch) {
|
|
1831
|
+
mode = "pr-diff";
|
|
1832
|
+
usedBase = resolvedBranch;
|
|
1833
|
+
const diffResult = tryPrDiff(resolvedBranch, cwd, debugInfo);
|
|
1834
|
+
if (diffResult) {
|
|
1835
|
+
rawOutput = diffResult.output;
|
|
1836
|
+
} else {
|
|
1837
|
+
const shallow = debugInfo.isShallowClone ? " Shallow clone detected \u2014 try fetch-depth: 0 in your workflow." : "";
|
|
1838
|
+
const detached = debugInfo.isDetachedHead ? " HEAD is detached \u2014 ensure checkout fetches the branch." : "";
|
|
1839
|
+
zeroFilesReason = `PR diff failed against "${resolvedBranch}". Tried: ${debugInfo.strategiesAttempted.join(", ")}.${shallow}${detached} Run with --debug-git for full diagnosis.`;
|
|
1840
|
+
}
|
|
1841
|
+
} else {
|
|
1842
|
+
mode = "uncommitted";
|
|
1843
|
+
const staged_ = runGit("diff --name-only --cached --diff-filter=ACMRT", cwd) ?? "";
|
|
1844
|
+
const unstaged = runGit("diff --name-only --diff-filter=ACMRT", cwd) ?? "";
|
|
1845
|
+
debugInfo.strategiesAttempted.push("git diff --cached", "git diff (unstaged)");
|
|
1846
|
+
const combined = /* @__PURE__ */ new Set([...parseFileList(staged_), ...parseFileList(unstaged)]);
|
|
1847
|
+
rawOutput = [...combined].join("\n");
|
|
1848
|
+
if (rawOutput) debugInfo.strategySucceeded = "uncommitted changes";
|
|
1849
|
+
if (!rawOutput) {
|
|
1850
|
+
zeroFilesReason = "No uncommitted changes found. Use --pr to scan PR changes, or --staged for staged files.";
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
if (!rawOutput) {
|
|
1854
|
+
return {
|
|
1855
|
+
files: [],
|
|
1856
|
+
mode,
|
|
1857
|
+
base: usedBase,
|
|
1858
|
+
totalDetected: 0,
|
|
1859
|
+
filteredOut: 0,
|
|
1860
|
+
debugInfo,
|
|
1861
|
+
zeroFilesReason
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
const rawFiles = parseFileList(rawOutput);
|
|
1865
|
+
debugInfo.rawFileCount = rawFiles.length;
|
|
1866
|
+
const kept = filterFiles(rawFiles, extensions, workingDirectory, cwd, debugInfo);
|
|
1867
|
+
const dropped = debugInfo.droppedByExtension + debugInfo.droppedByIgnore + debugInfo.droppedByWorkingDir + debugInfo.droppedMissing;
|
|
1868
|
+
if (kept.length === 0 && rawFiles.length > 0) {
|
|
1869
|
+
zeroFilesReason = `${rawFiles.length} changed file${rawFiles.length !== 1 ? "s" : ""} detected but all filtered out. Extension filter: ${debugInfo.droppedByExtension}, ignored paths: ${debugInfo.droppedByIgnore}, missing from disk: ${debugInfo.droppedMissing}.`;
|
|
1870
|
+
}
|
|
1871
|
+
return {
|
|
1872
|
+
files: kept,
|
|
1873
|
+
mode,
|
|
1874
|
+
base: usedBase,
|
|
1875
|
+
totalDetected: rawFiles.length,
|
|
1876
|
+
filteredOut: dropped,
|
|
1877
|
+
debugInfo,
|
|
1878
|
+
zeroFilesReason: kept.length === 0 ? zeroFilesReason : void 0
|
|
891
1879
|
};
|
|
892
1880
|
}
|
|
1881
|
+
function isGitHubActions() {
|
|
1882
|
+
return process.env.GITHUB_ACTIONS === "true";
|
|
1883
|
+
}
|
|
1884
|
+
function getGitHubBaseRef() {
|
|
1885
|
+
return process.env.GITHUB_BASE_REF || void 0;
|
|
1886
|
+
}
|
|
893
1887
|
|
|
894
1888
|
// cli/commands/run.ts
|
|
895
1889
|
function registerRunCommand(program2) {
|
|
896
|
-
program2.command("run").description("Run ai-guard rules on your project (zero ESLint config required)").option("--path <dir>", "Directory or file to scan", ".").option("--strict", "Use the strict rule preset (all rules at error)").option("--security", "Use the security-only rule preset").option("--json", "Output results as JSON (CI-friendly)").option(
|
|
1890
|
+
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(
|
|
897
1891
|
"--max-warnings <n>",
|
|
898
1892
|
"Fail with exit code 1 if warnings exceed this count",
|
|
899
1893
|
(value) => Number.parseInt(value, 10)
|
|
900
|
-
).option("--verbose", "Show all issues \u2014 disable grouping of repeated warnings").option("--debug-timing", "Print per-phase timing diagnostics").action(async (opts) => {
|
|
1894
|
+
).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) => {
|
|
901
1895
|
if (opts.maxWarnings !== void 0 && (!Number.isInteger(opts.maxWarnings) || opts.maxWarnings < 0)) {
|
|
902
1896
|
log.blank();
|
|
903
1897
|
log.error("--max-warnings must be a non-negative integer.");
|
|
@@ -930,21 +1924,33 @@ function registerRunCommand(program2) {
|
|
|
930
1924
|
log.blank();
|
|
931
1925
|
log.error("TypeScript detected but parser not found.");
|
|
932
1926
|
log.blank();
|
|
933
|
-
log.print(` ${
|
|
1927
|
+
log.print(` ${import_chalk3.default.bold("Install the TypeScript parser:")}`);
|
|
934
1928
|
log.blank();
|
|
935
|
-
log.print(` ${
|
|
1929
|
+
log.print(` ${import_chalk3.default.cyan("npm install --save-dev @typescript-eslint/parser")}`);
|
|
936
1930
|
log.blank();
|
|
937
1931
|
} else {
|
|
938
1932
|
log.blank();
|
|
939
1933
|
log.error(msg);
|
|
940
1934
|
log.blank();
|
|
941
|
-
log.print(` ${
|
|
1935
|
+
log.print(` ${import_chalk3.default.bold("Fix:")} Run ${import_chalk3.default.cyan("ai-guard doctor")} to diagnose your setup.`);
|
|
942
1936
|
log.blank();
|
|
943
1937
|
}
|
|
944
1938
|
process.exit(1);
|
|
945
1939
|
return;
|
|
946
1940
|
}
|
|
1941
|
+
if (isGitHubActions()) {
|
|
1942
|
+
emitGitHubAnnotations(result);
|
|
1943
|
+
writeGitHubSummary(result, { preset, scanMode: "full" });
|
|
1944
|
+
writeGitHubOutputs(result);
|
|
1945
|
+
}
|
|
1946
|
+
if (opts.sarif) {
|
|
1947
|
+
const sarifLog = buildSarifLog(result);
|
|
1948
|
+
console.log(sarifToJson(sarifLog));
|
|
1949
|
+
process.exit(resolveExitCode(result, opts.failOn ?? "errors", opts.maxWarnings));
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
947
1952
|
if (opts.json) {
|
|
1953
|
+
const signalSummary = buildSignalSummary(result);
|
|
948
1954
|
const jsonOutput = {
|
|
949
1955
|
preset,
|
|
950
1956
|
scannedPath: opts.path,
|
|
@@ -954,6 +1960,7 @@ function registerRunCommand(program2) {
|
|
|
954
1960
|
totalIssues: result.totalIssues,
|
|
955
1961
|
durationMs: result.durationMs,
|
|
956
1962
|
timing: result.timing,
|
|
1963
|
+
signalSummary,
|
|
957
1964
|
ruleBreakdown: Object.fromEntries(result.ruleBreakdown),
|
|
958
1965
|
topFiles: result.topFiles,
|
|
959
1966
|
files: result.files,
|
|
@@ -962,19 +1969,19 @@ function registerRunCommand(program2) {
|
|
|
962
1969
|
tsParserAvailable: result.tsParserAvailable
|
|
963
1970
|
};
|
|
964
1971
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
965
|
-
process.exit(
|
|
1972
|
+
process.exit(resolveExitCode(result, opts.failOn ?? "errors", opts.maxWarnings));
|
|
966
1973
|
return;
|
|
967
1974
|
}
|
|
968
1975
|
log.scanStats(result.filesScanned, result.files.length, result.durationMs, preset);
|
|
969
1976
|
if (!result.tsParserAvailable) {
|
|
970
1977
|
log.print(
|
|
971
|
-
|
|
1978
|
+
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)`)
|
|
972
1979
|
);
|
|
973
1980
|
}
|
|
974
1981
|
log.blank();
|
|
975
1982
|
if (result.totalIssues === 0 && result.ecosystemIssues.length === 0 && result.parserErrors.length === 0) {
|
|
976
1983
|
log.print(
|
|
977
|
-
` ${
|
|
1984
|
+
` ${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`)}`
|
|
978
1985
|
);
|
|
979
1986
|
log.blank();
|
|
980
1987
|
process.exit(0);
|
|
@@ -982,15 +1989,18 @@ function registerRunCommand(program2) {
|
|
|
982
1989
|
}
|
|
983
1990
|
if (result.totalIssues === 0) {
|
|
984
1991
|
log.print(
|
|
985
|
-
` ${
|
|
1992
|
+
` ${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`)}`
|
|
986
1993
|
);
|
|
987
1994
|
log.blank();
|
|
988
1995
|
}
|
|
989
1996
|
if (result.totalIssues > 0) {
|
|
1997
|
+
const verbose = opts.verbose ?? false;
|
|
1998
|
+
const quiet = opts.quiet ?? false;
|
|
1999
|
+
const { actionableFiles, informationalFiles } = partitionByTier(result, quiet);
|
|
990
2000
|
log.section("Summary by Category");
|
|
991
2001
|
const categoryErrors = {};
|
|
992
2002
|
const categoryWarnings = {};
|
|
993
|
-
for (const file of
|
|
2003
|
+
for (const file of actionableFiles) {
|
|
994
2004
|
for (const issue of file.issues) {
|
|
995
2005
|
const cat = RULE_CATEGORY[issue.ruleId] ?? "Other";
|
|
996
2006
|
if (issue.severity === 2) {
|
|
@@ -1022,7 +2032,7 @@ function registerRunCommand(program2) {
|
|
|
1022
2032
|
}
|
|
1023
2033
|
log.blank();
|
|
1024
2034
|
log.print(
|
|
1025
|
-
` ${
|
|
2035
|
+
` ${import_chalk3.default.bold("Total:")} ${formatIssueCount(result.totalErrors, result.totalWarnings)}`
|
|
1026
2036
|
);
|
|
1027
2037
|
log.blank();
|
|
1028
2038
|
if (result.ruleBreakdown.size > 0) {
|
|
@@ -1033,9 +2043,10 @@ function registerRunCommand(program2) {
|
|
|
1033
2043
|
for (const [rule, count] of sorted) {
|
|
1034
2044
|
const shortRule = rule.replace(/^ai-guard\//, "");
|
|
1035
2045
|
const tier = CONFIDENCE_TIER[rule];
|
|
1036
|
-
const
|
|
2046
|
+
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]");
|
|
2047
|
+
if (quiet && tier === "informational") continue;
|
|
1037
2048
|
log.print(
|
|
1038
|
-
` ${
|
|
2049
|
+
` ${import_chalk3.default.gray("\u2022")} ${import_chalk3.default.yellow(shortRule)} ${tierLabel2}${import_chalk3.default.gray(":")} ${import_chalk3.default.white(String(count))}`
|
|
1039
2050
|
);
|
|
1040
2051
|
}
|
|
1041
2052
|
log.blank();
|
|
@@ -1044,20 +2055,35 @@ function registerRunCommand(program2) {
|
|
|
1044
2055
|
log.section("Top Files");
|
|
1045
2056
|
for (const { path: fp, count } of result.topFiles) {
|
|
1046
2057
|
log.print(
|
|
1047
|
-
` ${
|
|
2058
|
+
` ${import_chalk3.default.gray("\u2022")} ${import_chalk3.default.white(fp)} ${import_chalk3.default.gray(`(${count})`)}`
|
|
1048
2059
|
);
|
|
1049
2060
|
}
|
|
1050
2061
|
log.blank();
|
|
1051
2062
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
2063
|
+
if (actionableFiles.length > 0) {
|
|
2064
|
+
log.section("Issues by File");
|
|
2065
|
+
log.blank();
|
|
2066
|
+
renderIssuesByFile({ ...result, files: actionableFiles }, verbose, quiet);
|
|
2067
|
+
}
|
|
2068
|
+
const totalInfoCount = informationalFiles.reduce((n, f) => n + f.issues.length, 0);
|
|
2069
|
+
if (totalInfoCount > 0 && !quiet) {
|
|
2070
|
+
if (verbose) {
|
|
2071
|
+
log.section("Informational Hints");
|
|
2072
|
+
log.print(import_chalk3.default.dim(` Stylistic hints with higher false-positive rate \u2014 review before acting.`));
|
|
2073
|
+
log.blank();
|
|
2074
|
+
renderIssuesByFile({ ...result, files: informationalFiles }, true, false);
|
|
2075
|
+
} else {
|
|
2076
|
+
log.blank();
|
|
2077
|
+
log.print(
|
|
2078
|
+
` ${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`)}`
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
1056
2082
|
}
|
|
1057
2083
|
if (result.ecosystemIssues.length > 0) {
|
|
1058
2084
|
log.ecosystemSection("ESLint Config Issues");
|
|
1059
2085
|
log.print(
|
|
1060
|
-
|
|
2086
|
+
import_chalk3.default.dim(` These issues come from your project's ESLint config, not from ai-guard.`)
|
|
1061
2087
|
);
|
|
1062
2088
|
log.blank();
|
|
1063
2089
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -1078,7 +2104,7 @@ function registerRunCommand(program2) {
|
|
|
1078
2104
|
if (result.parserErrors.length > 0) {
|
|
1079
2105
|
log.ecosystemSection("Parser Errors");
|
|
1080
2106
|
log.print(
|
|
1081
|
-
|
|
2107
|
+
import_chalk3.default.dim(` These files could not be parsed \u2014 not ai-guard findings.`)
|
|
1082
2108
|
);
|
|
1083
2109
|
log.blank();
|
|
1084
2110
|
for (const pe of result.parserErrors) {
|
|
@@ -1087,10 +2113,10 @@ function registerRunCommand(program2) {
|
|
|
1087
2113
|
if (!result.tsParserAvailable) {
|
|
1088
2114
|
log.blank();
|
|
1089
2115
|
log.print(
|
|
1090
|
-
` ${
|
|
2116
|
+
` ${import_chalk3.default.cyan("\u2192 Fix:")} ${import_chalk3.default.yellow("npm install --save-dev @typescript-eslint/parser")}`
|
|
1091
2117
|
);
|
|
1092
2118
|
log.print(
|
|
1093
|
-
|
|
2119
|
+
import_chalk3.default.dim(` TypeScript parser resolves most .ts/.tsx parse failures.`)
|
|
1094
2120
|
);
|
|
1095
2121
|
}
|
|
1096
2122
|
log.blank();
|
|
@@ -1099,21 +2125,63 @@ function registerRunCommand(program2) {
|
|
|
1099
2125
|
log.blank();
|
|
1100
2126
|
log.section("Next Steps");
|
|
1101
2127
|
if (result.totalIssues > 0) {
|
|
1102
|
-
log.info(`Run ${
|
|
1103
|
-
log.info(`Run ${
|
|
1104
|
-
log.info(`Run ${
|
|
2128
|
+
log.info(`Run ${import_chalk3.default.cyan("ai-guard baseline")} to save these issues and track only new ones`);
|
|
2129
|
+
log.info(`Run ${import_chalk3.default.cyan("ai-guard report")} to generate a shareable HTML report`);
|
|
2130
|
+
log.info(`Run ${import_chalk3.default.cyan("ai-guard ignore")} to suppress dist/build noise`);
|
|
1105
2131
|
}
|
|
1106
2132
|
if (result.ecosystemIssues.length > 0) {
|
|
1107
|
-
log.info(`Run ${
|
|
2133
|
+
log.info(`Run ${import_chalk3.default.cyan("ai-guard doctor")} to diagnose ESLint config issues`);
|
|
1108
2134
|
}
|
|
1109
2135
|
if (!opts.verbose && hasGroupedWarnings(result)) {
|
|
1110
|
-
log.info(`Run with ${
|
|
2136
|
+
log.info(`Run with ${import_chalk3.default.cyan("--verbose")} to expand grouped warnings`);
|
|
1111
2137
|
}
|
|
1112
2138
|
log.blank();
|
|
1113
2139
|
process.exit(getRunExitCode(result, opts.maxWarnings));
|
|
1114
2140
|
});
|
|
1115
2141
|
}
|
|
1116
2142
|
var GROUP_THRESHOLD = 4;
|
|
2143
|
+
function partitionByTier(result, quiet) {
|
|
2144
|
+
const actionableFiles = [];
|
|
2145
|
+
const informationalFiles = [];
|
|
2146
|
+
for (const file of result.files) {
|
|
2147
|
+
const actionableIssues = file.issues.filter((i) => {
|
|
2148
|
+
const tier = CONFIDENCE_TIER[i.ruleId];
|
|
2149
|
+
if (quiet && (tier === "informational" || i.severity === 1)) return false;
|
|
2150
|
+
return tier !== "informational";
|
|
2151
|
+
});
|
|
2152
|
+
const infoIssues = file.issues.filter((i) => {
|
|
2153
|
+
const tier = CONFIDENCE_TIER[i.ruleId];
|
|
2154
|
+
return tier === "informational";
|
|
2155
|
+
});
|
|
2156
|
+
if (actionableIssues.length > 0) {
|
|
2157
|
+
actionableFiles.push({ ...file, issues: actionableIssues });
|
|
2158
|
+
}
|
|
2159
|
+
if (infoIssues.length > 0) {
|
|
2160
|
+
informationalFiles.push({ ...file, issues: infoIssues });
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
return { actionableFiles, informationalFiles };
|
|
2164
|
+
}
|
|
2165
|
+
function buildSignalSummary(result) {
|
|
2166
|
+
let high = 0, medium = 0, low = 0, informational = 0;
|
|
2167
|
+
for (const file of result.files) {
|
|
2168
|
+
for (const issue of file.issues) {
|
|
2169
|
+
const tier = CONFIDENCE_TIER[issue.ruleId];
|
|
2170
|
+
if (tier === "high") high++;
|
|
2171
|
+
else if (tier === "medium") medium++;
|
|
2172
|
+
else if (tier === "informational") informational++;
|
|
2173
|
+
else low++;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
return {
|
|
2177
|
+
high,
|
|
2178
|
+
medium,
|
|
2179
|
+
low,
|
|
2180
|
+
informational,
|
|
2181
|
+
ecosystemIssues: result.ecosystemIssues.length,
|
|
2182
|
+
parserErrors: result.parserErrors.length
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
1117
2185
|
function hasGroupedWarnings(result) {
|
|
1118
2186
|
for (const file of result.files) {
|
|
1119
2187
|
const ruleCounts = /* @__PURE__ */ new Map();
|
|
@@ -1126,10 +2194,10 @@ function hasGroupedWarnings(result) {
|
|
|
1126
2194
|
}
|
|
1127
2195
|
return false;
|
|
1128
2196
|
}
|
|
1129
|
-
function renderIssuesByFile(result, verbose) {
|
|
2197
|
+
function renderIssuesByFile(result, verbose, quiet = false) {
|
|
1130
2198
|
for (const file of result.files) {
|
|
1131
2199
|
log.print(
|
|
1132
|
-
` ${
|
|
2200
|
+
` ${import_chalk3.default.bold.white(file.filePath)} ` + import_chalk3.default.gray(
|
|
1133
2201
|
`(${file.errorCount} error${file.errorCount !== 1 ? "s" : ""}, ${file.warningCount} warning${file.warningCount !== 1 ? "s" : ""})`
|
|
1134
2202
|
)
|
|
1135
2203
|
);
|
|
@@ -1143,13 +2211,13 @@ function renderIssuesByFile(result, verbose) {
|
|
|
1143
2211
|
for (const issues of ruleCounts.values()) {
|
|
1144
2212
|
const tier = CONFIDENCE_TIER[issues[0].ruleId] ?? "low";
|
|
1145
2213
|
if (tier === "low" && issues.length >= GROUP_THRESHOLD) {
|
|
1146
|
-
const sev = issues[0].severity === 2 ?
|
|
1147
|
-
const shortRule =
|
|
2214
|
+
const sev = issues[0].severity === 2 ? import_chalk3.default.red("error") : import_chalk3.default.yellow(" warn");
|
|
2215
|
+
const shortRule = import_chalk3.default.gray(issues[0].ruleId.replace(/^ai-guard\//, ""));
|
|
1148
2216
|
log.print(
|
|
1149
|
-
` ${sev} ${
|
|
2217
|
+
` ${sev} ${import_chalk3.default.white(issues[0].message)} ${shortRule}`
|
|
1150
2218
|
);
|
|
1151
2219
|
log.print(
|
|
1152
|
-
|
|
2220
|
+
import_chalk3.default.dim(` \u21B3 ${issues.length} occurrences \u2014 run with --verbose to expand`)
|
|
1153
2221
|
);
|
|
1154
2222
|
} else {
|
|
1155
2223
|
for (const issue of issues) {
|
|
@@ -1166,13 +2234,13 @@ function renderIssuesByFile(result, verbose) {
|
|
|
1166
2234
|
}
|
|
1167
2235
|
}
|
|
1168
2236
|
function renderSingleIssue(issue) {
|
|
1169
|
-
const sev = issue.severity === 2 ?
|
|
1170
|
-
const loc =
|
|
1171
|
-
const ruleShort =
|
|
2237
|
+
const sev = issue.severity === 2 ? import_chalk3.default.red("error") : import_chalk3.default.yellow(" warn");
|
|
2238
|
+
const loc = import_chalk3.default.gray(`${String(issue.line)}:${String(issue.column)}`).padEnd(12);
|
|
2239
|
+
const ruleShort = import_chalk3.default.gray(issue.ruleId.replace(/^ai-guard\//, ""));
|
|
1172
2240
|
const tier = CONFIDENCE_TIER[issue.ruleId];
|
|
1173
|
-
const tierTag = tier === "
|
|
2241
|
+
const tierTag = tier === "informational" ? import_chalk3.default.dim(" [info]") : "";
|
|
1174
2242
|
log.print(
|
|
1175
|
-
` ${loc} ${sev} ${
|
|
2243
|
+
` ${loc} ${sev} ${import_chalk3.default.white(issue.message)}${tierTag} ${ruleShort}`
|
|
1176
2244
|
);
|
|
1177
2245
|
}
|
|
1178
2246
|
function getRunExitCode(result, maxWarnings) {
|
|
@@ -1182,20 +2250,294 @@ function getRunExitCode(result, maxWarnings) {
|
|
|
1182
2250
|
}
|
|
1183
2251
|
function formatIssueCount(errors, warnings) {
|
|
1184
2252
|
const parts = [];
|
|
1185
|
-
if (errors > 0) parts.push(
|
|
1186
|
-
if (warnings > 0) parts.push(
|
|
1187
|
-
return parts.join(
|
|
2253
|
+
if (errors > 0) parts.push(import_chalk3.default.red.bold(`${errors} error${errors !== 1 ? "s" : ""}`));
|
|
2254
|
+
if (warnings > 0) parts.push(import_chalk3.default.yellow.bold(`${warnings} warning${warnings !== 1 ? "s" : ""}`));
|
|
2255
|
+
return parts.join(import_chalk3.default.gray(" \xB7 "));
|
|
2256
|
+
}
|
|
2257
|
+
function resolveExitCode(result, failOn, maxWarnings) {
|
|
2258
|
+
if (maxWarnings !== void 0 && result.totalWarnings > maxWarnings) return 1;
|
|
2259
|
+
switch (failOn) {
|
|
2260
|
+
case "none":
|
|
2261
|
+
return 0;
|
|
2262
|
+
case "errors":
|
|
2263
|
+
return result.totalErrors > 0 ? 1 : 0;
|
|
2264
|
+
case "high": {
|
|
2265
|
+
const high = result.files.flatMap((f) => f.issues).filter((i) => CONFIDENCE_TIER[i.ruleId] === "high").length;
|
|
2266
|
+
return high > 0 ? 1 : 0;
|
|
2267
|
+
}
|
|
2268
|
+
case "medium": {
|
|
2269
|
+
const highMed = result.files.flatMap((f) => f.issues).filter((i) => CONFIDENCE_TIER[i.ruleId] === "high" || CONFIDENCE_TIER[i.ruleId] === "medium").length;
|
|
2270
|
+
return highMed > 0 ? 1 : 0;
|
|
2271
|
+
}
|
|
2272
|
+
case "any": {
|
|
2273
|
+
const actionable = result.files.flatMap((f) => f.issues).filter((i) => CONFIDENCE_TIER[i.ruleId] !== "informational").length;
|
|
2274
|
+
return actionable > 0 ? 1 : 0;
|
|
2275
|
+
}
|
|
2276
|
+
default:
|
|
2277
|
+
return getRunExitCode(result, maxWarnings);
|
|
2278
|
+
}
|
|
1188
2279
|
}
|
|
1189
2280
|
|
|
1190
|
-
// cli/commands/
|
|
1191
|
-
var import_chalk3 = __toESM(require("chalk"));
|
|
2281
|
+
// cli/commands/changed.ts
|
|
1192
2282
|
var import_path4 = __toESM(require("path"));
|
|
1193
2283
|
var import_fs4 = __toESM(require("fs"));
|
|
2284
|
+
var import_ora2 = __toESM(require("ora"));
|
|
2285
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
2286
|
+
function registerChangedCommand(program2) {
|
|
2287
|
+
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 (raw name: main, develop \u2014 not 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 | errors | 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 this file path").option("--sarif-stdout", "Force SARIF to stdout even in CI (overrides auto-file behavior)").option("--github-summary", "Write GitHub step summary (auto-enabled in GitHub Actions)").option(
|
|
2288
|
+
"--max-warnings <n>",
|
|
2289
|
+
"Fail if warnings exceed this count",
|
|
2290
|
+
(v) => Number.parseInt(v, 10)
|
|
2291
|
+
).option("--verbose", "Expand all findings (disable grouping)").option("--quiet", "Show errors only").option("--debug-timing", "Print per-phase timing diagnostics").option("--debug-git", "Print full git resolution trace (useful for diagnosing zero-file issues)").option("--debug-ci", "Print CI environment state (GITHUB_* vars, SARIF path, summary path)").action(async (opts) => {
|
|
2292
|
+
const preset = opts.strict ? "strict" : opts.security ? "security" : "recommended";
|
|
2293
|
+
const inCI = isGitHubActions();
|
|
2294
|
+
const baseRef = opts.base ?? (opts.pr || inCI ? getGitHubBaseRef() : void 0);
|
|
2295
|
+
const sarifToFile = opts.sarif && !opts.sarifStdout && (opts.sarifOutput || inCI);
|
|
2296
|
+
const resolvedSarifPath = opts.sarifOutput ?? (sarifToFile ? "ai-guard-results.sarif" : void 0);
|
|
2297
|
+
if (!opts.json && !opts.sarif) {
|
|
2298
|
+
log.banner("AI GUARD");
|
|
2299
|
+
}
|
|
2300
|
+
if (opts.debugCi) {
|
|
2301
|
+
const { printCIDebug: printCIDebug2 } = await Promise.resolve().then(() => (init_debug(), debug_exports));
|
|
2302
|
+
printCIDebug2({
|
|
2303
|
+
isGitHubActions: inCI,
|
|
2304
|
+
githubBaseRef: process.env.GITHUB_BASE_REF,
|
|
2305
|
+
githubSha: process.env.GITHUB_SHA,
|
|
2306
|
+
githubRef: process.env.GITHUB_REF,
|
|
2307
|
+
githubRepository: process.env.GITHUB_REPOSITORY,
|
|
2308
|
+
githubWorkflow: process.env.GITHUB_WORKFLOW,
|
|
2309
|
+
githubRunId: process.env.GITHUB_RUN_ID,
|
|
2310
|
+
githubEventName: process.env.GITHUB_EVENT_NAME,
|
|
2311
|
+
githubStepSummary: process.env.GITHUB_STEP_SUMMARY,
|
|
2312
|
+
githubOutput: process.env.GITHUB_OUTPUT,
|
|
2313
|
+
sarifOutputPath: resolvedSarifPath,
|
|
2314
|
+
preset,
|
|
2315
|
+
failOn: opts.failOn,
|
|
2316
|
+
scanMode: opts.staged ? "staged" : baseRef ? "pr-diff" : "uncommitted"
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
const cwd = process.cwd();
|
|
2320
|
+
const resolvedPath = import_path4.default.resolve(opts.path);
|
|
2321
|
+
const workingDirectory = opts.path !== "." ? opts.path : void 0;
|
|
2322
|
+
const spinner = opts.json || opts.sarif ? null : (0, import_ora2.default)({ text: import_chalk5.default.dim("Detecting changed files\u2026"), color: "cyan" }).start();
|
|
2323
|
+
const changedResult = getChangedFiles({
|
|
2324
|
+
staged: opts.staged ?? false,
|
|
2325
|
+
base: baseRef,
|
|
2326
|
+
cwd,
|
|
2327
|
+
workingDirectory,
|
|
2328
|
+
debug: opts.debugGit
|
|
2329
|
+
});
|
|
2330
|
+
if (opts.debugGit) {
|
|
2331
|
+
const { printGitDebug: printGitDebug2 } = await Promise.resolve().then(() => (init_debug(), debug_exports));
|
|
2332
|
+
printGitDebug2(changedResult);
|
|
2333
|
+
}
|
|
2334
|
+
if (changedResult.files.length === 0) {
|
|
2335
|
+
spinner?.stop();
|
|
2336
|
+
if (changedResult.mode === "fallback") {
|
|
2337
|
+
if (!opts.json && !opts.sarif) {
|
|
2338
|
+
log.warn("Git not available. Falling back to full scan.");
|
|
2339
|
+
log.blank();
|
|
2340
|
+
}
|
|
2341
|
+
const result2 = await runEslint({
|
|
2342
|
+
preset,
|
|
2343
|
+
targetPath: resolvedPath.toString(),
|
|
2344
|
+
debugTiming: opts.debugTiming
|
|
2345
|
+
});
|
|
2346
|
+
handleResults(result2, opts, preset, "full", 0, inCI, resolvedSarifPath);
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
if (!opts.json && !opts.sarif) {
|
|
2350
|
+
log.blank();
|
|
2351
|
+
log.print(` ${import_chalk5.default.yellow("\u26A0")} No changed JS/TS files detected \u2014 nothing to scan`);
|
|
2352
|
+
if (changedResult.zeroFilesReason) {
|
|
2353
|
+
log.print(import_chalk5.default.dim(` ${changedResult.zeroFilesReason}`));
|
|
2354
|
+
}
|
|
2355
|
+
if (changedResult.filteredOut > 0) {
|
|
2356
|
+
log.print(import_chalk5.default.dim(` (${changedResult.filteredOut} file${changedResult.filteredOut !== 1 ? "s" : ""} filtered out)`));
|
|
2357
|
+
}
|
|
2358
|
+
if (!opts.debugGit) {
|
|
2359
|
+
log.print(import_chalk5.default.dim(" Run with --debug-git for full git resolution trace."));
|
|
2360
|
+
}
|
|
2361
|
+
log.blank();
|
|
2362
|
+
} else if (opts.json) {
|
|
2363
|
+
console.log(JSON.stringify({
|
|
2364
|
+
version: PKG_VERSION,
|
|
2365
|
+
preset,
|
|
2366
|
+
scanMode: changedResult.mode,
|
|
2367
|
+
changedFiles: 0,
|
|
2368
|
+
filesScanned: 0,
|
|
2369
|
+
totalIssues: 0,
|
|
2370
|
+
totalErrors: 0,
|
|
2371
|
+
totalWarnings: 0,
|
|
2372
|
+
zeroFilesReason: changedResult.zeroFilesReason,
|
|
2373
|
+
debugInfo: opts.debugGit ? changedResult.debugInfo : void 0,
|
|
2374
|
+
signalSummary: { high: 0, medium: 0, low: 0, informational: 0, ecosystemIssues: 0, parserErrors: 0 }
|
|
2375
|
+
}, null, 2));
|
|
2376
|
+
}
|
|
2377
|
+
process.exit(0);
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
if (spinner) {
|
|
2381
|
+
spinner.text = import_chalk5.default.dim(`Scanning ${changedResult.files.length} changed file${changedResult.files.length !== 1 ? "s" : ""}\u2026`);
|
|
2382
|
+
}
|
|
2383
|
+
const result = await runEslint({
|
|
2384
|
+
preset,
|
|
2385
|
+
targetPath: resolvedPath.toString(),
|
|
2386
|
+
files: changedResult.files,
|
|
2387
|
+
debugTiming: opts.debugTiming
|
|
2388
|
+
});
|
|
2389
|
+
spinner?.stop();
|
|
2390
|
+
const scanMode = changedResult.mode === "staged" ? "staged" : "changed";
|
|
2391
|
+
handleResults(result, opts, preset, scanMode, changedResult.files.length, inCI, resolvedSarifPath);
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
function handleResults(result, opts, preset, scanMode, changedFilesCount, inCI, resolvedSarifPath) {
|
|
2395
|
+
if (inCI || opts.githubSummary) {
|
|
2396
|
+
writeGitHubSummary(result, { preset, scanMode, changedFilesCount });
|
|
2397
|
+
writeGitHubOutputs(result, opts.sarifOutput);
|
|
2398
|
+
emitGitHubAnnotations(result);
|
|
2399
|
+
}
|
|
2400
|
+
if (opts.sarif) {
|
|
2401
|
+
const sarifLog = buildSarifLog(result);
|
|
2402
|
+
const sarifJson = sarifToJson(sarifLog);
|
|
2403
|
+
if (resolvedSarifPath) {
|
|
2404
|
+
import_fs4.default.writeFileSync(resolvedSarifPath, sarifJson, "utf-8");
|
|
2405
|
+
} else {
|
|
2406
|
+
console.log(sarifJson);
|
|
2407
|
+
}
|
|
2408
|
+
process.exit(getFailOnExitCode(result, opts.failOn, opts.maxWarnings));
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
if (opts.json) {
|
|
2412
|
+
const signalSummary = buildSignalSummary(result);
|
|
2413
|
+
const jsonOutput = {
|
|
2414
|
+
version: PKG_VERSION,
|
|
2415
|
+
preset,
|
|
2416
|
+
scanMode,
|
|
2417
|
+
scannedPath: opts.path,
|
|
2418
|
+
filesScanned: result.filesScanned,
|
|
2419
|
+
changedFilesCount,
|
|
2420
|
+
totalErrors: result.totalErrors,
|
|
2421
|
+
totalWarnings: result.totalWarnings,
|
|
2422
|
+
totalIssues: result.totalIssues,
|
|
2423
|
+
durationMs: result.durationMs,
|
|
2424
|
+
signalSummary,
|
|
2425
|
+
ruleBreakdown: Object.fromEntries(result.ruleBreakdown),
|
|
2426
|
+
topFiles: result.topFiles,
|
|
2427
|
+
files: result.files,
|
|
2428
|
+
ecosystemIssues: result.ecosystemIssues,
|
|
2429
|
+
parserErrors: result.parserErrors,
|
|
2430
|
+
tsParserAvailable: result.tsParserAvailable
|
|
2431
|
+
};
|
|
2432
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
2433
|
+
process.exit(getFailOnExitCode(result, opts.failOn, opts.maxWarnings));
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
const scanModeLabel = scanMode === "changed" ? "Changed files" : scanMode === "staged" ? "Staged files" : "Full scan";
|
|
2437
|
+
const modeTag = changedFilesCount > 0 ? `${changedFilesCount} changed` : "";
|
|
2438
|
+
log.summary(
|
|
2439
|
+
result.filesScanned,
|
|
2440
|
+
result.totalIssues,
|
|
2441
|
+
result.files.filter((f) => f.issues.length > 0).length,
|
|
2442
|
+
result.durationMs,
|
|
2443
|
+
`${preset} \xB7 ${scanModeLabel}${modeTag ? ` (${modeTag})` : ""}`
|
|
2444
|
+
);
|
|
2445
|
+
if (result.totalIssues === 0) {
|
|
2446
|
+
log.blank();
|
|
2447
|
+
log.success(`No AI issues found \u2014 ${result.filesScanned} file${result.filesScanned !== 1 ? "s" : ""} scanned, all clean`);
|
|
2448
|
+
log.blank();
|
|
2449
|
+
} else {
|
|
2450
|
+
const verbose = opts.verbose ?? false;
|
|
2451
|
+
const quiet = opts.quiet ?? false;
|
|
2452
|
+
const { actionableFiles, informationalFiles } = partitionByTier(result, quiet);
|
|
2453
|
+
log.section("Summary by Category");
|
|
2454
|
+
const categoryErrors = {};
|
|
2455
|
+
const categoryWarnings = {};
|
|
2456
|
+
for (const file of actionableFiles) {
|
|
2457
|
+
for (const issue of file.issues) {
|
|
2458
|
+
const cat = RULE_CATEGORY[issue.ruleId] ?? "Other";
|
|
2459
|
+
if (issue.severity === 2) categoryErrors[cat] = (categoryErrors[cat] ?? 0) + 1;
|
|
2460
|
+
else categoryWarnings[cat] = (categoryWarnings[cat] ?? 0) + 1;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
const allCategories = /* @__PURE__ */ new Set([...CATEGORY_ORDER, ...Object.keys(categoryErrors), ...Object.keys(categoryWarnings)]);
|
|
2464
|
+
const sortedCats = [...allCategories].filter((c) => (categoryErrors[c] ?? 0) + (categoryWarnings[c] ?? 0) > 0).sort((a, b) => {
|
|
2465
|
+
const ai = CATEGORY_ORDER.indexOf(a), bi = CATEGORY_ORDER.indexOf(b);
|
|
2466
|
+
if (ai !== -1 && bi !== -1) return ai - bi;
|
|
2467
|
+
if (ai !== -1) return -1;
|
|
2468
|
+
if (bi !== -1) return 1;
|
|
2469
|
+
return a.localeCompare(b);
|
|
2470
|
+
});
|
|
2471
|
+
for (const cat of sortedCats) {
|
|
2472
|
+
const icon = CATEGORY_ICONS[cat] ?? "\u26AA";
|
|
2473
|
+
log.category(icon, cat, categoryErrors[cat] ?? 0, categoryWarnings[cat] ?? 0);
|
|
2474
|
+
}
|
|
2475
|
+
log.blank();
|
|
2476
|
+
log.print(` ${import_chalk5.default.bold("Total:")} ${formatIssueCount(result.totalErrors, result.totalWarnings)}`);
|
|
2477
|
+
log.blank();
|
|
2478
|
+
if (actionableFiles.length > 0) {
|
|
2479
|
+
log.section("Issues by File");
|
|
2480
|
+
log.blank();
|
|
2481
|
+
renderIssuesByFile({ ...result, files: actionableFiles }, verbose, quiet);
|
|
2482
|
+
}
|
|
2483
|
+
const totalInfoCount = informationalFiles.reduce((n, f) => n + f.issues.length, 0);
|
|
2484
|
+
if (totalInfoCount > 0 && !quiet) {
|
|
2485
|
+
if (verbose) {
|
|
2486
|
+
log.section("Informational Hints");
|
|
2487
|
+
log.print(import_chalk5.default.dim(" Stylistic hints with higher false-positive rate \u2014 review before acting."));
|
|
2488
|
+
log.blank();
|
|
2489
|
+
renderIssuesByFile({ ...result, files: informationalFiles }, true, false);
|
|
2490
|
+
} else {
|
|
2491
|
+
log.blank();
|
|
2492
|
+
log.print(` ${import_chalk5.default.gray("\u25B8")} ${import_chalk5.default.gray(`${totalInfoCount} informational hint${totalInfoCount !== 1 ? "s" : ""} \u2014 run with ${import_chalk5.default.cyan("--verbose")} to expand`)}`);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
if (result.ecosystemIssues.length > 0) {
|
|
2497
|
+
log.ecosystemSection("ESLint Config Issues");
|
|
2498
|
+
log.print(import_chalk5.default.dim(" These issues come from your project's ESLint config, not from ai-guard."));
|
|
2499
|
+
log.blank();
|
|
2500
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2501
|
+
for (const issue of result.ecosystemIssues) {
|
|
2502
|
+
const key = `${issue.ruleId ?? "null"}::${issue.message}`;
|
|
2503
|
+
const existing = seen.get(key);
|
|
2504
|
+
if (existing) {
|
|
2505
|
+
existing.count++;
|
|
2506
|
+
} else {
|
|
2507
|
+
seen.set(key, { issue, count: 1 });
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
for (const { issue, count } of seen.values()) {
|
|
2511
|
+
log.ecosystemIssue(issue, count);
|
|
2512
|
+
}
|
|
2513
|
+
log.blank();
|
|
2514
|
+
}
|
|
2515
|
+
log.divider();
|
|
2516
|
+
log.blank();
|
|
2517
|
+
log.section("Next Steps");
|
|
2518
|
+
if (result.totalIssues > 0) {
|
|
2519
|
+
log.info(`Run ${import_chalk5.default.cyan("ai-guard baseline")} to save these issues and track only new ones`);
|
|
2520
|
+
log.info(`Run ${import_chalk5.default.cyan("ai-guard report")} to generate a shareable HTML report`);
|
|
2521
|
+
}
|
|
2522
|
+
if (result.ecosystemIssues.length > 0) {
|
|
2523
|
+
log.info(`Run ${import_chalk5.default.cyan("ai-guard doctor")} to diagnose ESLint config issues`);
|
|
2524
|
+
}
|
|
2525
|
+
log.blank();
|
|
2526
|
+
process.exit(getFailOnExitCode(result, opts.failOn, opts.maxWarnings));
|
|
2527
|
+
}
|
|
2528
|
+
function getFailOnExitCode(result, failOn, maxWarnings) {
|
|
2529
|
+
return resolveExitCode(result, failOn, maxWarnings);
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// cli/commands/init.ts
|
|
2533
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
2534
|
+
var import_path7 = __toESM(require("path"));
|
|
2535
|
+
var import_fs7 = __toESM(require("fs"));
|
|
1194
2536
|
|
|
1195
2537
|
// cli/utils/detector.ts
|
|
1196
|
-
var
|
|
1197
|
-
var
|
|
1198
|
-
var
|
|
2538
|
+
var import_fs5 = __toESM(require("fs"));
|
|
2539
|
+
var import_path5 = __toESM(require("path"));
|
|
2540
|
+
var import_module2 = require("module");
|
|
1199
2541
|
var FLAT_CONFIG_FILES = [
|
|
1200
2542
|
{ file: "eslint.config.js", type: "flat-js" },
|
|
1201
2543
|
{ file: "eslint.config.mjs", type: "flat-mjs" },
|
|
@@ -1211,11 +2553,11 @@ var LEGACY_CONFIG_FILES = [
|
|
|
1211
2553
|
var ALL_CONFIG_FILES = [...FLAT_CONFIG_FILES, ...LEGACY_CONFIG_FILES];
|
|
1212
2554
|
function isPackageInstalled(pkgName, cwd = process.cwd()) {
|
|
1213
2555
|
try {
|
|
1214
|
-
const pkgPath =
|
|
1215
|
-
if (
|
|
2556
|
+
const pkgPath = import_path5.default.join(cwd, "node_modules", pkgName, "package.json");
|
|
2557
|
+
if (import_fs5.default.existsSync(pkgPath)) {
|
|
1216
2558
|
return true;
|
|
1217
2559
|
}
|
|
1218
|
-
const req = (0,
|
|
2560
|
+
const req = (0, import_module2.createRequire)(import_path5.default.join(cwd, "package.json"));
|
|
1219
2561
|
req.resolve(`${pkgName}/package.json`);
|
|
1220
2562
|
return true;
|
|
1221
2563
|
} catch {
|
|
@@ -1224,9 +2566,9 @@ function isPackageInstalled(pkgName, cwd = process.cwd()) {
|
|
|
1224
2566
|
}
|
|
1225
2567
|
function getPackageVersion(pkgName, cwd = process.cwd()) {
|
|
1226
2568
|
try {
|
|
1227
|
-
const pkgPath =
|
|
1228
|
-
const resolvedPath =
|
|
1229
|
-
const raw =
|
|
2569
|
+
const pkgPath = import_path5.default.join(cwd, "node_modules", pkgName, "package.json");
|
|
2570
|
+
const resolvedPath = import_fs5.default.existsSync(pkgPath) ? pkgPath : (0, import_module2.createRequire)(import_path5.default.join(cwd, "package.json")).resolve(`${pkgName}/package.json`);
|
|
2571
|
+
const raw = import_fs5.default.readFileSync(resolvedPath, "utf-8");
|
|
1230
2572
|
const json = JSON.parse(raw);
|
|
1231
2573
|
return json.version ?? null;
|
|
1232
2574
|
} catch {
|
|
@@ -1242,8 +2584,8 @@ function detectConfigType(cwd = process.cwd()) {
|
|
|
1242
2584
|
const found = [];
|
|
1243
2585
|
let first = null;
|
|
1244
2586
|
for (const { file, type } of ALL_CONFIG_FILES) {
|
|
1245
|
-
const fullPath =
|
|
1246
|
-
if (
|
|
2587
|
+
const fullPath = import_path5.default.join(cwd, file);
|
|
2588
|
+
if (import_fs5.default.existsSync(fullPath)) {
|
|
1247
2589
|
found.push(fullPath);
|
|
1248
2590
|
if (!first) first = { type, path: fullPath };
|
|
1249
2591
|
}
|
|
@@ -1266,7 +2608,7 @@ function requiresFlatConfig(eslintMajor) {
|
|
|
1266
2608
|
function detectNukeIgnore(configPath) {
|
|
1267
2609
|
if (!configPath) return false;
|
|
1268
2610
|
try {
|
|
1269
|
-
const content =
|
|
2611
|
+
const content = import_fs5.default.readFileSync(configPath, "utf-8");
|
|
1270
2612
|
return content.includes('"**/*"') || content.includes("'**/*'");
|
|
1271
2613
|
} catch {
|
|
1272
2614
|
return false;
|
|
@@ -1322,7 +2664,7 @@ async function verifyConfigLoads(configPath, cwd) {
|
|
|
1322
2664
|
overrideConfigFile: configPath
|
|
1323
2665
|
});
|
|
1324
2666
|
await eslint.lintText("", {
|
|
1325
|
-
filePath:
|
|
2667
|
+
filePath: import_path7.default.join(cwd, "_ai_guard_probe_.js")
|
|
1326
2668
|
});
|
|
1327
2669
|
return null;
|
|
1328
2670
|
} catch (err) {
|
|
@@ -1340,7 +2682,7 @@ function registerInitCommand(program2) {
|
|
|
1340
2682
|
const isDryRun = opts.dryRun === true;
|
|
1341
2683
|
log.banner("AI GUARD INIT");
|
|
1342
2684
|
if (isDryRun) {
|
|
1343
|
-
log.print(` ${
|
|
2685
|
+
log.print(` ${import_chalk6.default.yellow("\u2691 DRY RUN \u2014 no files will be written")}`);
|
|
1344
2686
|
log.blank();
|
|
1345
2687
|
}
|
|
1346
2688
|
log.info("Detecting your project setup\u2026");
|
|
@@ -1358,7 +2700,7 @@ function registerInitCommand(program2) {
|
|
|
1358
2700
|
log.warn("eslint-plugin-ai-guard not found");
|
|
1359
2701
|
}
|
|
1360
2702
|
if (env.configType !== "none") {
|
|
1361
|
-
log.success(`ESLint config found: ${
|
|
2703
|
+
log.success(`ESLint config found: ${import_chalk6.default.white(import_path7.default.relative(cwd, env.configPath ?? env.configType))}`);
|
|
1362
2704
|
} else {
|
|
1363
2705
|
log.info("No ESLint config found \u2014 will generate one");
|
|
1364
2706
|
}
|
|
@@ -1367,9 +2709,9 @@ function registerInitCommand(program2) {
|
|
|
1367
2709
|
log.blank();
|
|
1368
2710
|
log.warn("Conflicting configs detected:");
|
|
1369
2711
|
for (const p of env.allConfigPaths) {
|
|
1370
|
-
log.print(` ${
|
|
2712
|
+
log.print(` ${import_chalk6.default.yellow("\u2192")} ${import_chalk6.default.white(import_path7.default.relative(cwd, p))}`);
|
|
1371
2713
|
}
|
|
1372
|
-
log.print(` ${
|
|
2714
|
+
log.print(` ${import_chalk6.default.gray("Multiple config formats found \u2014 this can cause unpredictable behavior.")}`);
|
|
1373
2715
|
}
|
|
1374
2716
|
log.blank();
|
|
1375
2717
|
const toInstall = [];
|
|
@@ -1380,22 +2722,22 @@ function registerInitCommand(program2) {
|
|
|
1380
2722
|
log.warn("The following packages are not installed:");
|
|
1381
2723
|
log.blank();
|
|
1382
2724
|
for (const pkg of toInstall) {
|
|
1383
|
-
log.print(` ${
|
|
2725
|
+
log.print(` ${import_chalk6.default.yellow("\u2192")} ${import_chalk6.default.white(pkg)}`);
|
|
1384
2726
|
}
|
|
1385
2727
|
log.blank();
|
|
1386
|
-
log.print(` ${
|
|
2728
|
+
log.print(` ${import_chalk6.default.bold("Run this first:")}`);
|
|
1387
2729
|
log.blank();
|
|
1388
|
-
log.print(` ${
|
|
2730
|
+
log.print(` ${import_chalk6.default.cyan(`npm install --save-dev ${toInstall.join(" ")}`)}`);
|
|
1389
2731
|
log.blank();
|
|
1390
|
-
log.print(` Then re-run ${
|
|
2732
|
+
log.print(` Then re-run ${import_chalk6.default.cyan("ai-guard init")} to complete setup.`);
|
|
1391
2733
|
log.blank();
|
|
1392
2734
|
process.exit(1);
|
|
1393
2735
|
return;
|
|
1394
2736
|
}
|
|
1395
2737
|
try {
|
|
1396
|
-
const { createRequire:
|
|
2738
|
+
const { createRequire: createRequire3 } = await import("module");
|
|
1397
2739
|
const { pathToFileURL: pathToFileURL2 } = await import("url");
|
|
1398
|
-
const req =
|
|
2740
|
+
const req = createRequire3(import_path7.default.join(cwd, "_probe.js"));
|
|
1399
2741
|
const resolvedPath = req.resolve("eslint-plugin-ai-guard");
|
|
1400
2742
|
const aiGuard = await import(pathToFileURL2(resolvedPath).href);
|
|
1401
2743
|
const plugin = aiGuard.default || aiGuard;
|
|
@@ -1414,9 +2756,9 @@ function registerInitCommand(program2) {
|
|
|
1414
2756
|
if (eslintNeedsFlat && existingIsLegacy) {
|
|
1415
2757
|
log.section("Config Migration Required");
|
|
1416
2758
|
log.warn(`ESLint v${env.eslintMajor} requires flat config, but a legacy config was found:`);
|
|
1417
|
-
log.print(` ${
|
|
2759
|
+
log.print(` ${import_chalk6.default.white(import_path7.default.relative(cwd, env.configPath))}`);
|
|
1418
2760
|
log.blank();
|
|
1419
|
-
log.print(` ${
|
|
2761
|
+
log.print(` ${import_chalk6.default.bold("ai-guard will create a new flat config.")}`);
|
|
1420
2762
|
log.blank();
|
|
1421
2763
|
}
|
|
1422
2764
|
log.section(`Configuring ESLint${isDryRun ? " (dry run)" : ""}`);
|
|
@@ -1426,34 +2768,34 @@ function registerInitCommand(program2) {
|
|
|
1426
2768
|
const raw = readConfig(env.configPath);
|
|
1427
2769
|
const { content: fixed, changed } = removeNukeIgnore(raw);
|
|
1428
2770
|
if (changed) {
|
|
1429
|
-
log.warn(`Removed "**/*" ignore pattern from ${
|
|
2771
|
+
log.warn(`Removed "**/*" ignore pattern from ${import_path7.default.relative(cwd, env.configPath)}`);
|
|
1430
2772
|
if (!isDryRun) {
|
|
1431
2773
|
backupConfig(env.configPath);
|
|
1432
2774
|
writeConfig(env.configPath, fixed);
|
|
1433
2775
|
log.success("Ignore pattern fixed");
|
|
1434
2776
|
} else {
|
|
1435
|
-
log.print(` ${
|
|
2777
|
+
log.print(` ${import_chalk6.default.gray("[dry-run] would remove nuke-ignore from existing config")}`);
|
|
1436
2778
|
}
|
|
1437
2779
|
}
|
|
1438
2780
|
}
|
|
1439
2781
|
if (env.configType === "none" || eslintNeedsFlat && existingIsLegacy) {
|
|
1440
2782
|
const targetName = useFlat ? "eslint.config.mjs" : ".eslintrc.js";
|
|
1441
|
-
finalConfigPath =
|
|
2783
|
+
finalConfigPath = import_path7.default.join(cwd, targetName);
|
|
1442
2784
|
const content2 = useFlat ? generateFlatConfig(preset) : generateLegacyConfig(preset);
|
|
1443
2785
|
if (isDryRun) {
|
|
1444
|
-
log.print(` ${
|
|
2786
|
+
log.print(` ${import_chalk6.default.gray(`[dry-run] would create: ${targetName}`)}`);
|
|
1445
2787
|
log.blank();
|
|
1446
|
-
log.print(
|
|
2788
|
+
log.print(import_chalk6.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"));
|
|
1447
2789
|
log.blank();
|
|
1448
2790
|
for (const line of content2.split("\n")) {
|
|
1449
|
-
log.print(
|
|
2791
|
+
log.print(import_chalk6.default.gray(` ${line}`));
|
|
1450
2792
|
}
|
|
1451
2793
|
log.blank();
|
|
1452
2794
|
} else {
|
|
1453
2795
|
writeConfig(finalConfigPath, content2);
|
|
1454
|
-
log.success(`Created ${
|
|
1455
|
-
log.info(` Preset: ${
|
|
1456
|
-
log.info(` Format: ${
|
|
2796
|
+
log.success(`Created ${import_chalk6.default.white(targetName)}`);
|
|
2797
|
+
log.info(` Preset: ${import_chalk6.default.cyan(preset)}`);
|
|
2798
|
+
log.info(` Format: ${import_chalk6.default.cyan(useFlat ? "ESLint v9 flat config (eslint.config.mjs)" : "ESLint v8 legacy config (.eslintrc.js)")}`);
|
|
1457
2799
|
configWritten = true;
|
|
1458
2800
|
}
|
|
1459
2801
|
} else if (useFlat && existingIsFlat) {
|
|
@@ -1466,15 +2808,15 @@ function registerInitCommand(program2) {
|
|
|
1466
2808
|
finalConfigPath = env.configPath;
|
|
1467
2809
|
} else if (isDryRun) {
|
|
1468
2810
|
finalConfigPath = env.configPath;
|
|
1469
|
-
log.print(` ${
|
|
2811
|
+
log.print(` ${import_chalk6.default.gray(`[dry-run] would patch: ${import_path7.default.relative(cwd, env.configPath)}`)}`);
|
|
1470
2812
|
} else {
|
|
1471
2813
|
const bak = backupConfig(env.configPath);
|
|
1472
|
-
log.info(`Backed up \u2192 ${
|
|
2814
|
+
log.info(`Backed up \u2192 ${import_chalk6.default.gray(import_path7.default.relative(cwd, bak))}`);
|
|
1473
2815
|
writeConfig(env.configPath, patched);
|
|
1474
2816
|
if (isInvalid) {
|
|
1475
|
-
log.success(`Detected invalid ai-guard config \u2014 repairing ${
|
|
2817
|
+
log.success(`Detected invalid ai-guard config \u2014 repairing ${import_chalk6.default.white(import_path7.default.relative(cwd, env.configPath))}`);
|
|
1476
2818
|
} else {
|
|
1477
|
-
log.success(`Patched ${
|
|
2819
|
+
log.success(`Patched ${import_chalk6.default.white(import_path7.default.relative(cwd, env.configPath))}`);
|
|
1478
2820
|
}
|
|
1479
2821
|
finalConfigPath = env.configPath;
|
|
1480
2822
|
configWritten = true;
|
|
@@ -1487,12 +2829,12 @@ function registerInitCommand(program2) {
|
|
|
1487
2829
|
finalConfigPath = env.configPath;
|
|
1488
2830
|
} else if (isDryRun) {
|
|
1489
2831
|
finalConfigPath = env.configPath;
|
|
1490
|
-
log.print(` ${
|
|
2832
|
+
log.print(` ${import_chalk6.default.gray(`[dry-run] would patch: ${import_path7.default.relative(cwd, env.configPath)}`)}`);
|
|
1491
2833
|
} else {
|
|
1492
2834
|
const bak = backupConfig(env.configPath);
|
|
1493
|
-
log.info(`Backed up \u2192 ${
|
|
2835
|
+
log.info(`Backed up \u2192 ${import_chalk6.default.gray(import_path7.default.relative(cwd, bak))}`);
|
|
1494
2836
|
writeConfig(env.configPath, patched);
|
|
1495
|
-
log.success(`Patched ${
|
|
2837
|
+
log.success(`Patched ${import_chalk6.default.white(import_path7.default.relative(cwd, env.configPath))}`);
|
|
1496
2838
|
finalConfigPath = env.configPath;
|
|
1497
2839
|
configWritten = true;
|
|
1498
2840
|
}
|
|
@@ -1501,18 +2843,18 @@ function registerInitCommand(program2) {
|
|
|
1501
2843
|
const legacyConfigs = env.allConfigPaths.filter((p) => !p.includes("eslint.config."));
|
|
1502
2844
|
let cleanedUp = false;
|
|
1503
2845
|
for (const p of legacyConfigs) {
|
|
1504
|
-
if (
|
|
2846
|
+
if (import_fs7.default.existsSync(p)) {
|
|
1505
2847
|
if (!isDryRun) {
|
|
1506
2848
|
try {
|
|
1507
|
-
|
|
2849
|
+
import_fs7.default.renameSync(p, `${p}.bak`);
|
|
1508
2850
|
cleanedUp = true;
|
|
1509
2851
|
} catch (err) {
|
|
1510
2852
|
const reason = err instanceof Error ? err.message : String(err);
|
|
1511
|
-
log.warn(`Could not back up ${
|
|
2853
|
+
log.warn(`Could not back up ${import_path7.default.relative(cwd, p)}: ${reason}`);
|
|
1512
2854
|
}
|
|
1513
2855
|
} else {
|
|
1514
2856
|
cleanedUp = true;
|
|
1515
|
-
log.print(` ${
|
|
2857
|
+
log.print(` ${import_chalk6.default.gray(`[dry-run] would rename ${import_path7.default.relative(cwd, p)} to ${import_path7.default.basename(p)}.bak`)}`);
|
|
1516
2858
|
}
|
|
1517
2859
|
}
|
|
1518
2860
|
}
|
|
@@ -1522,19 +2864,19 @@ function registerInitCommand(program2) {
|
|
|
1522
2864
|
}
|
|
1523
2865
|
if (isDryRun) {
|
|
1524
2866
|
log.blank();
|
|
1525
|
-
log.print(` ${
|
|
1526
|
-
log.print(` Re-run without ${
|
|
2867
|
+
log.print(` ${import_chalk6.default.yellow("Dry run complete \u2014 no files written.")}`);
|
|
2868
|
+
log.print(` Re-run without ${import_chalk6.default.cyan("--dry-run")} to apply.`);
|
|
1527
2869
|
log.blank();
|
|
1528
2870
|
process.exit(0);
|
|
1529
2871
|
return;
|
|
1530
2872
|
}
|
|
1531
2873
|
log.blank();
|
|
1532
2874
|
log.section("Validation");
|
|
1533
|
-
if (!
|
|
1534
|
-
log.error(`Config file not found after write: ${
|
|
2875
|
+
if (!import_fs7.default.existsSync(finalConfigPath)) {
|
|
2876
|
+
log.error(`Config file not found after write: ${import_path7.default.relative(cwd, finalConfigPath)}`);
|
|
1535
2877
|
log.blank();
|
|
1536
|
-
log.print(` ${
|
|
1537
|
-
log.print(` ${
|
|
2878
|
+
log.print(` ${import_chalk6.default.bold("Problem:")} File write may have failed.`);
|
|
2879
|
+
log.print(` ${import_chalk6.default.bold("Fix:")} Check disk space and file permissions, then re-run.`);
|
|
1538
2880
|
log.blank();
|
|
1539
2881
|
process.exit(1);
|
|
1540
2882
|
return;
|
|
@@ -1544,36 +2886,36 @@ function registerInitCommand(program2) {
|
|
|
1544
2886
|
if (validationErrors.length > 0) {
|
|
1545
2887
|
log.warn("Config validation found issues:");
|
|
1546
2888
|
for (const e of validationErrors) {
|
|
1547
|
-
log.print(` ${
|
|
2889
|
+
log.print(` ${import_chalk6.default.red("\u2716")} ${e}`);
|
|
1548
2890
|
}
|
|
1549
2891
|
} else {
|
|
1550
|
-
log.success(`Config structure valid: ${
|
|
2892
|
+
log.success(`Config structure valid: ${import_chalk6.default.white(import_path7.default.relative(cwd, finalConfigPath))}`);
|
|
1551
2893
|
}
|
|
1552
2894
|
log.info("Verifying ESLint can load the config\u2026");
|
|
1553
2895
|
const loadError = await verifyConfigLoads(finalConfigPath, cwd);
|
|
1554
2896
|
if (loadError !== null) {
|
|
1555
2897
|
log.blank();
|
|
1556
2898
|
if (loadError.kind === "module-not-found") {
|
|
1557
|
-
log.warn(`Could not verify: module not found: ${
|
|
2899
|
+
log.warn(`Could not verify: module not found: ${import_chalk6.default.yellow(loadError.pkg)}`);
|
|
1558
2900
|
log.blank();
|
|
1559
|
-
log.print(` ${
|
|
1560
|
-
log.print(` ${
|
|
1561
|
-
log.print(` Then run ${
|
|
2901
|
+
log.print(` ${import_chalk6.default.bold("Install the missing package:")}`);
|
|
2902
|
+
log.print(` ${import_chalk6.default.cyan(`npm install --save-dev ${loadError.pkg}`)}`);
|
|
2903
|
+
log.print(` Then run ${import_chalk6.default.cyan("ai-guard doctor")} to confirm.`);
|
|
1562
2904
|
log.blank();
|
|
1563
2905
|
} else {
|
|
1564
2906
|
log.error("ESLint failed to load the generated config.");
|
|
1565
2907
|
log.blank();
|
|
1566
|
-
log.print(` ${
|
|
1567
|
-
log.print(` ${
|
|
2908
|
+
log.print(` ${import_chalk6.default.bold("Problem:")}`);
|
|
2909
|
+
log.print(` ${import_chalk6.default.red(loadError.message)}`);
|
|
1568
2910
|
if (loadError.kind === "unknown") {
|
|
1569
2911
|
log.blank();
|
|
1570
|
-
log.print(` ${
|
|
2912
|
+
log.print(` ${import_chalk6.default.yellow("Generated config may be using incorrect plugin structure.")}`);
|
|
1571
2913
|
}
|
|
1572
2914
|
log.blank();
|
|
1573
|
-
log.print(` ${
|
|
1574
|
-
log.print(` 1. Check the syntax in ${
|
|
1575
|
-
log.print(` 2. Make sure ${
|
|
1576
|
-
log.print(` 3. Run ${
|
|
2915
|
+
log.print(` ${import_chalk6.default.bold("Fix:")}`);
|
|
2916
|
+
log.print(` 1. Check the syntax in ${import_chalk6.default.cyan(import_path7.default.relative(cwd, finalConfigPath))}`);
|
|
2917
|
+
log.print(` 2. Make sure ${import_chalk6.default.cyan("eslint-plugin-ai-guard")} is installed`);
|
|
2918
|
+
log.print(` 3. Run ${import_chalk6.default.cyan("ai-guard doctor")} for full diagnostics`);
|
|
1577
2919
|
log.blank();
|
|
1578
2920
|
process.exit(1);
|
|
1579
2921
|
return;
|
|
@@ -1587,34 +2929,34 @@ function registerInitCommand(program2) {
|
|
|
1587
2929
|
let cleanedUpInfo = false;
|
|
1588
2930
|
if (eslintNeedsFlat) {
|
|
1589
2931
|
const legacyConfigs = env.allConfigPaths.filter((p) => !p.includes("eslint.config."));
|
|
1590
|
-
cleanedUpInfo = legacyConfigs.some((p) =>
|
|
2932
|
+
cleanedUpInfo = legacyConfigs.some((p) => import_fs7.default.existsSync(`${p}.bak`) && !import_fs7.default.existsSync(p));
|
|
1591
2933
|
}
|
|
1592
2934
|
const lingeringConflicts = env.hasConflictingConfigs && !eslintNeedsFlat && !cleanedUpInfo;
|
|
1593
2935
|
if (loadError === null && !lingeringConflicts) {
|
|
1594
|
-
log.print(` ${
|
|
2936
|
+
log.print(` ${import_chalk6.default.green("\u2714")} ${import_chalk6.default.bold.green("Configuration validated successfully")}`);
|
|
1595
2937
|
}
|
|
1596
|
-
log.print(` ${
|
|
1597
|
-
log.print(` ${
|
|
1598
|
-
log.print(` ${
|
|
2938
|
+
log.print(` ${import_chalk6.default.green("\u2714")} ${import_chalk6.default.bold("ESLint config:")} ${import_chalk6.default.cyan(import_path7.default.relative(cwd, finalConfigPath))}`);
|
|
2939
|
+
log.print(` ${import_chalk6.default.green("\u2714")} ${import_chalk6.default.bold("Mode:")} ${import_chalk6.default.cyan(useFlat ? `Flat config (ESLint v${env.eslintMajor ?? 9})` : `Legacy config (ESLint v${env.eslintMajor ?? 8})`)}`);
|
|
2940
|
+
log.print(` ${import_chalk6.default.green("\u2714")} ${import_chalk6.default.bold("Preset:")} ${import_chalk6.default.cyan(preset)}`);
|
|
1599
2941
|
log.blank();
|
|
1600
|
-
log.info(`Run ${
|
|
1601
|
-
log.info(`Run ${
|
|
1602
|
-
log.info(`Run ${
|
|
1603
|
-
log.info(`Run ${
|
|
2942
|
+
log.info(`Run ${import_chalk6.default.cyan("npx eslint .")} \u2192 lint with your editor integration`);
|
|
2943
|
+
log.info(`Run ${import_chalk6.default.cyan("ai-guard run")} \u2192 zero-config scan`);
|
|
2944
|
+
log.info(`Run ${import_chalk6.default.cyan("ai-guard doctor")} \u2192 verify the full setup`);
|
|
2945
|
+
log.info(`Run ${import_chalk6.default.cyan("ai-guard baseline")} \u2192 save baseline, track only new issues`);
|
|
1604
2946
|
log.blank();
|
|
1605
2947
|
});
|
|
1606
2948
|
}
|
|
1607
2949
|
|
|
1608
2950
|
// cli/commands/doctor.ts
|
|
1609
|
-
var
|
|
1610
|
-
var
|
|
2951
|
+
var import_chalk7 = __toESM(require("chalk"));
|
|
2952
|
+
var import_path8 = __toESM(require("path"));
|
|
1611
2953
|
init_config_manager();
|
|
1612
|
-
var
|
|
2954
|
+
var import_meta2 = {};
|
|
1613
2955
|
async function probeConfigLoad(configPath, cwd) {
|
|
1614
2956
|
try {
|
|
1615
2957
|
const { ESLint } = await import("eslint");
|
|
1616
2958
|
const eslint = new ESLint({ cwd, overrideConfigFile: configPath });
|
|
1617
|
-
await eslint.lintText("", { filePath:
|
|
2959
|
+
await eslint.lintText("", { filePath: import_path8.default.join(cwd, "_probe_.js") });
|
|
1618
2960
|
return { ok: true, error: null };
|
|
1619
2961
|
} catch (err) {
|
|
1620
2962
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1649,7 +2991,7 @@ function registerDoctorCommand(program2) {
|
|
|
1649
2991
|
checks.push({
|
|
1650
2992
|
label: "ESLint config present",
|
|
1651
2993
|
pass: hasConfig,
|
|
1652
|
-
detail: hasConfig ? `${
|
|
2994
|
+
detail: hasConfig ? `${import_path8.default.relative(cwd, env.configPath ?? "")} (${env.configType})` : "No eslint.config.* or .eslintrc.* found",
|
|
1653
2995
|
fix: hasConfig ? void 0 : "ai-guard init",
|
|
1654
2996
|
note: hasConfig ? void 0 : "This will generate the correct config format for your ESLint version"
|
|
1655
2997
|
});
|
|
@@ -1681,7 +3023,7 @@ function registerDoctorCommand(program2) {
|
|
|
1681
3023
|
checks.push({
|
|
1682
3024
|
label: "Plugin wired in config",
|
|
1683
3025
|
pass: pluginWired && !isInvalidConfig,
|
|
1684
|
-
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 ${
|
|
3026
|
+
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_path8.default.relative(cwd, env.configPath)}`,
|
|
1685
3027
|
fix: isInvalidConfig || !pluginWired && !readError ? "ai-guard init (will patch/repair your existing config)" : void 0
|
|
1686
3028
|
});
|
|
1687
3029
|
}
|
|
@@ -1689,7 +3031,7 @@ function registerDoctorCommand(program2) {
|
|
|
1689
3031
|
label: "No conflicting config files",
|
|
1690
3032
|
pass: !env.hasConflictingConfigs,
|
|
1691
3033
|
detail: env.hasConflictingConfigs ? `Conflicting config detected:
|
|
1692
|
-
${env.allConfigPaths.map((p) => ` ${
|
|
3034
|
+
${env.allConfigPaths.map((p) => ` ${import_path8.default.relative(cwd, p)}`).join("\n")}` : "Only one config format detected",
|
|
1693
3035
|
fix: env.hasConflictingConfigs ? "Remove or backup legacy config" : void 0,
|
|
1694
3036
|
note: env.hasConflictingConfigs ? "ESLint v9 silently ignores .eslintrc.* when a flat config exists. This can cause confusion." : void 0
|
|
1695
3037
|
});
|
|
@@ -1716,7 +3058,7 @@ ${env.allConfigPaths.map((p) => ` ${import_path5.default.relative(cwd, p)}
|
|
|
1716
3058
|
label: "ESLint config loads without errors",
|
|
1717
3059
|
pass: probe.ok,
|
|
1718
3060
|
detail: probe.ok ? "ESLint loaded config successfully" : probe.error ?? "Unknown error",
|
|
1719
|
-
fix: probe.ok ? void 0 : `Check syntax in ${
|
|
3061
|
+
fix: probe.ok ? void 0 : `Check syntax in ${import_path8.default.relative(cwd, env.configPath)}`,
|
|
1720
3062
|
note: probe.ok ? void 0 : "Run: ai-guard init --dry-run to preview a fresh config"
|
|
1721
3063
|
});
|
|
1722
3064
|
}
|
|
@@ -1724,8 +3066,8 @@ ${env.allConfigPaths.map((p) => ` ${import_path5.default.relative(cwd, p)}
|
|
|
1724
3066
|
let utilsOk = false;
|
|
1725
3067
|
let utilsVersion = null;
|
|
1726
3068
|
try {
|
|
1727
|
-
const { createRequire:
|
|
1728
|
-
const req =
|
|
3069
|
+
const { createRequire: createRequire3 } = await import("module");
|
|
3070
|
+
const req = createRequire3(import_path8.default.join(cwd, "package.json"));
|
|
1729
3071
|
const pkgPath = req.resolve("@typescript-eslint/utils/package.json");
|
|
1730
3072
|
const pkg = JSON.parse(
|
|
1731
3073
|
(await import("fs")).readFileSync(pkgPath, "utf-8")
|
|
@@ -1736,10 +3078,10 @@ ${env.allConfigPaths.map((p) => ` ${import_path5.default.relative(cwd, p)}
|
|
|
1736
3078
|
}
|
|
1737
3079
|
if (!utilsOk) {
|
|
1738
3080
|
try {
|
|
1739
|
-
const selfDir =
|
|
1740
|
-
new URL(
|
|
3081
|
+
const selfDir = import_path8.default.dirname(
|
|
3082
|
+
new URL(import_meta2.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")
|
|
1741
3083
|
);
|
|
1742
|
-
const pkgPath =
|
|
3084
|
+
const pkgPath = import_path8.default.join(selfDir, "../../node_modules/@typescript-eslint/utils/package.json");
|
|
1743
3085
|
if ((await import("fs")).existsSync(pkgPath)) {
|
|
1744
3086
|
utilsOk = true;
|
|
1745
3087
|
}
|
|
@@ -1758,8 +3100,8 @@ ${env.allConfigPaths.map((p) => ` ${import_path5.default.relative(cwd, p)}
|
|
|
1758
3100
|
let parserOk = false;
|
|
1759
3101
|
let parserVersion = null;
|
|
1760
3102
|
try {
|
|
1761
|
-
const { createRequire:
|
|
1762
|
-
const req =
|
|
3103
|
+
const { createRequire: createRequire3 } = await import("module");
|
|
3104
|
+
const req = createRequire3(import_path8.default.join(cwd, "package.json"));
|
|
1763
3105
|
const pkgPath = req.resolve("@typescript-eslint/parser/package.json");
|
|
1764
3106
|
const pkg = JSON.parse(
|
|
1765
3107
|
(await import("fs")).readFileSync(pkgPath, "utf-8")
|
|
@@ -1790,17 +3132,17 @@ ${env.allConfigPaths.map((p) => ` ${import_path5.default.relative(cwd, p)}
|
|
|
1790
3132
|
log.section("Diagnostics");
|
|
1791
3133
|
let allPassed = true;
|
|
1792
3134
|
for (const check of checks) {
|
|
1793
|
-
const icon = check.pass ?
|
|
1794
|
-
const label = check.pass ?
|
|
3135
|
+
const icon = check.pass ? import_chalk7.default.green("\u2714") : import_chalk7.default.red("\u2716");
|
|
3136
|
+
const label = check.pass ? import_chalk7.default.white(check.label) : import_chalk7.default.red.bold(check.label);
|
|
1795
3137
|
log.print(` ${icon} ${label}`);
|
|
1796
|
-
log.print(` ${
|
|
3138
|
+
log.print(` ${import_chalk7.default.gray(check.detail)}`);
|
|
1797
3139
|
if (!check.pass) {
|
|
1798
3140
|
allPassed = false;
|
|
1799
3141
|
if (check.fix) {
|
|
1800
|
-
log.print(` ${
|
|
3142
|
+
log.print(` ${import_chalk7.default.cyan("\u2192 Fix:")} ${import_chalk7.default.yellow(check.fix)}`);
|
|
1801
3143
|
}
|
|
1802
3144
|
if (check.note) {
|
|
1803
|
-
log.print(` ${
|
|
3145
|
+
log.print(` ${import_chalk7.default.gray(check.note)}`);
|
|
1804
3146
|
}
|
|
1805
3147
|
}
|
|
1806
3148
|
log.blank();
|
|
@@ -1808,10 +3150,10 @@ ${env.allConfigPaths.map((p) => ` ${import_path5.default.relative(cwd, p)}
|
|
|
1808
3150
|
log.divider();
|
|
1809
3151
|
log.blank();
|
|
1810
3152
|
if (allPassed) {
|
|
1811
|
-
log.print(` ${
|
|
3153
|
+
log.print(` ${import_chalk7.default.green("\u2714")} ${import_chalk7.default.bold.green("All checks passed! Your setup is correct.")}`);
|
|
1812
3154
|
log.blank();
|
|
1813
|
-
log.info(`Run ${
|
|
1814
|
-
log.info(`Run ${
|
|
3155
|
+
log.info(`Run ${import_chalk7.default.cyan("ai-guard run")} to start scanning.`);
|
|
3156
|
+
log.info(`Run ${import_chalk7.default.cyan("npx eslint .")} to lint with ESLint directly.`);
|
|
1815
3157
|
} else {
|
|
1816
3158
|
log.error("Some checks failed. Follow the fixes above.");
|
|
1817
3159
|
log.blank();
|
|
@@ -1826,15 +3168,15 @@ ${env.allConfigPaths.map((p) => ` ${import_path5.default.relative(cwd, p)}
|
|
|
1826
3168
|
);
|
|
1827
3169
|
if (pluginMissing) {
|
|
1828
3170
|
log.print(
|
|
1829
|
-
` Install missing packages first, then re-run ${
|
|
3171
|
+
` Install missing packages first, then re-run ${import_chalk7.default.cyan("ai-guard doctor")}.`
|
|
1830
3172
|
);
|
|
1831
3173
|
} else if (configMissing || configFormatMismatch) {
|
|
1832
3174
|
log.print(
|
|
1833
|
-
` Run ${
|
|
3175
|
+
` Run ${import_chalk7.default.cyan("ai-guard init")} to generate the correct config for your ESLint version.`
|
|
1834
3176
|
);
|
|
1835
3177
|
} else {
|
|
1836
3178
|
log.print(
|
|
1837
|
-
` Run ${
|
|
3179
|
+
` Run ${import_chalk7.default.cyan("ai-guard init --dry-run")} to preview what would change.`
|
|
1838
3180
|
);
|
|
1839
3181
|
}
|
|
1840
3182
|
}
|
|
@@ -1845,9 +3187,9 @@ ${env.allConfigPaths.map((p) => ` ${import_path5.default.relative(cwd, p)}
|
|
|
1845
3187
|
|
|
1846
3188
|
// cli/commands/preset.ts
|
|
1847
3189
|
var import_prompts = require("@inquirer/prompts");
|
|
1848
|
-
var
|
|
3190
|
+
var import_chalk8 = __toESM(require("chalk"));
|
|
1849
3191
|
init_config_manager();
|
|
1850
|
-
var
|
|
3192
|
+
var import_path9 = __toESM(require("path"));
|
|
1851
3193
|
function registerPresetCommand(program2) {
|
|
1852
3194
|
program2.command("preset").description("Interactively select and apply an ai-guard preset to your ESLint config").action(async () => {
|
|
1853
3195
|
const cwd = process.cwd();
|
|
@@ -1857,32 +3199,32 @@ function registerPresetCommand(program2) {
|
|
|
1857
3199
|
message: "Choose a preset:",
|
|
1858
3200
|
choices: [
|
|
1859
3201
|
{
|
|
1860
|
-
name: `${
|
|
3202
|
+
name: `${import_chalk8.default.green("recommended")} \u2014 balanced defaults, low noise. Best starting point.`,
|
|
1861
3203
|
value: "recommended"
|
|
1862
3204
|
},
|
|
1863
3205
|
{
|
|
1864
|
-
name: `${
|
|
3206
|
+
name: `${import_chalk8.default.yellow("strict")} \u2014 all rules at error. For mature codebases.`,
|
|
1865
3207
|
value: "strict"
|
|
1866
3208
|
},
|
|
1867
3209
|
{
|
|
1868
|
-
name: `${
|
|
3210
|
+
name: `${import_chalk8.default.red("security")} \u2014 security rules only. For AppSec teams.`,
|
|
1869
3211
|
value: "security"
|
|
1870
3212
|
}
|
|
1871
3213
|
]
|
|
1872
3214
|
});
|
|
1873
3215
|
log.blank();
|
|
1874
|
-
log.info(`Applying preset: ${
|
|
3216
|
+
log.info(`Applying preset: ${import_chalk8.default.cyan(preset)}`);
|
|
1875
3217
|
const env = detect(cwd);
|
|
1876
3218
|
const useFlat = env.eslintMajor !== null && env.eslintMajor >= 9 || isFlat(env.configType);
|
|
1877
3219
|
if (env.configType === "none") {
|
|
1878
|
-
const configPath = useFlat ?
|
|
3220
|
+
const configPath = useFlat ? import_path9.default.join(cwd, "eslint.config.mjs") : import_path9.default.join(cwd, ".eslintrc.js");
|
|
1879
3221
|
const content = useFlat ? generateFlatConfig(preset) : generateLegacyConfig(preset);
|
|
1880
3222
|
writeConfig(configPath, content);
|
|
1881
|
-
log.success(`Created ${
|
|
3223
|
+
log.success(`Created ${import_chalk8.default.white(import_path9.default.relative(cwd, configPath))} with ${import_chalk8.default.cyan(preset)} preset`);
|
|
1882
3224
|
} else {
|
|
1883
3225
|
const configPath = env.configPath;
|
|
1884
3226
|
const backupPath = backupConfig(configPath);
|
|
1885
|
-
log.info(`Backed up \u2192 ${
|
|
3227
|
+
log.info(`Backed up \u2192 ${import_chalk8.default.gray(import_path9.default.relative(cwd, backupPath))}`);
|
|
1886
3228
|
const existing = readConfig(configPath);
|
|
1887
3229
|
const withPlugin = isFlat(env.configType) ? patchFlatConfig(existing, preset) : patchLegacyConfig(existing, preset);
|
|
1888
3230
|
const patched = isFlat(env.configType) ? switchFlatPreset(withPlugin, preset) : switchLegacyPreset(withPlugin, preset);
|
|
@@ -1890,47 +3232,47 @@ function registerPresetCommand(program2) {
|
|
|
1890
3232
|
log.warn("No preset changes needed. Config already matches selected preset.");
|
|
1891
3233
|
} else {
|
|
1892
3234
|
writeConfig(configPath, patched);
|
|
1893
|
-
log.success(`Updated ${
|
|
3235
|
+
log.success(`Updated ${import_chalk8.default.white(import_path9.default.relative(cwd, configPath))} to ${import_chalk8.default.cyan(preset)} preset`);
|
|
1894
3236
|
}
|
|
1895
3237
|
}
|
|
1896
3238
|
log.blank();
|
|
1897
3239
|
log.section("Preset Details");
|
|
1898
3240
|
const presetDetails = {
|
|
1899
3241
|
recommended: [
|
|
1900
|
-
{ rule: "no-empty-catch", level:
|
|
1901
|
-
{ rule: "no-floating-promise", level:
|
|
1902
|
-
{ rule: "no-hardcoded-secret", level:
|
|
1903
|
-
{ rule: "no-eval-dynamic", level:
|
|
1904
|
-
{ rule: "no-broad-exception", level:
|
|
1905
|
-
{ rule: "no-async-array-callback", level:
|
|
1906
|
-
{ rule: "no-await-in-loop", level:
|
|
1907
|
-
{ rule: "+ 5 more\u2026", level:
|
|
3242
|
+
{ rule: "no-empty-catch", level: import_chalk8.default.red("error") },
|
|
3243
|
+
{ rule: "no-floating-promise", level: import_chalk8.default.red("error") },
|
|
3244
|
+
{ rule: "no-hardcoded-secret", level: import_chalk8.default.red("error") },
|
|
3245
|
+
{ rule: "no-eval-dynamic", level: import_chalk8.default.red("error") },
|
|
3246
|
+
{ rule: "no-broad-exception", level: import_chalk8.default.yellow("warn") },
|
|
3247
|
+
{ rule: "no-async-array-callback", level: import_chalk8.default.yellow("warn") },
|
|
3248
|
+
{ rule: "no-await-in-loop", level: import_chalk8.default.yellow("warn") },
|
|
3249
|
+
{ rule: "+ 5 more\u2026", level: import_chalk8.default.gray("warn") }
|
|
1908
3250
|
],
|
|
1909
3251
|
strict: [
|
|
1910
|
-
{ rule: "All 17 rules", level:
|
|
3252
|
+
{ rule: "All 17 rules", level: import_chalk8.default.red("error") }
|
|
1911
3253
|
],
|
|
1912
3254
|
security: [
|
|
1913
|
-
{ rule: "no-hardcoded-secret", level:
|
|
1914
|
-
{ rule: "no-eval-dynamic", level:
|
|
1915
|
-
{ rule: "no-sql-string-concat", level:
|
|
1916
|
-
{ rule: "no-unsafe-deserialize", level:
|
|
1917
|
-
{ rule: "require-auth-middleware", level:
|
|
1918
|
-
{ rule: "require-authz-check", level:
|
|
3255
|
+
{ rule: "no-hardcoded-secret", level: import_chalk8.default.red("error") },
|
|
3256
|
+
{ rule: "no-eval-dynamic", level: import_chalk8.default.red("error") },
|
|
3257
|
+
{ rule: "no-sql-string-concat", level: import_chalk8.default.red("error") },
|
|
3258
|
+
{ rule: "no-unsafe-deserialize", level: import_chalk8.default.yellow("warn") },
|
|
3259
|
+
{ rule: "require-auth-middleware", level: import_chalk8.default.yellow("warn") },
|
|
3260
|
+
{ rule: "require-authz-check", level: import_chalk8.default.yellow("warn") }
|
|
1919
3261
|
]
|
|
1920
3262
|
};
|
|
1921
3263
|
for (const { rule, level } of presetDetails[preset]) {
|
|
1922
|
-
log.print(` ${
|
|
3264
|
+
log.print(` ${import_chalk8.default.gray("\u2022")} ${import_chalk8.default.white(rule).padEnd(32)}${level}`);
|
|
1923
3265
|
}
|
|
1924
3266
|
log.blank();
|
|
1925
|
-
log.info(`Run ${
|
|
3267
|
+
log.info(`Run ${import_chalk8.default.cyan("ai-guard run")} to see results with the new preset.`);
|
|
1926
3268
|
log.blank();
|
|
1927
3269
|
});
|
|
1928
3270
|
}
|
|
1929
3271
|
|
|
1930
3272
|
// cli/commands/ignore.ts
|
|
1931
|
-
var
|
|
3273
|
+
var import_chalk9 = __toESM(require("chalk"));
|
|
1932
3274
|
init_config_manager();
|
|
1933
|
-
var
|
|
3275
|
+
var import_path10 = __toESM(require("path"));
|
|
1934
3276
|
var DEFAULT_IGNORES = [
|
|
1935
3277
|
".next/",
|
|
1936
3278
|
"dist/",
|
|
@@ -1946,41 +3288,41 @@ function registerIgnoreCommand(program2) {
|
|
|
1946
3288
|
log.blank();
|
|
1947
3289
|
log.section("Default Ignores");
|
|
1948
3290
|
for (const p of DEFAULT_IGNORES) {
|
|
1949
|
-
log.print(` ${
|
|
3291
|
+
log.print(` ${import_chalk9.default.gray("\u2022")} ${import_chalk9.default.yellow(p)}`);
|
|
1950
3292
|
}
|
|
1951
3293
|
log.blank();
|
|
1952
3294
|
const env = detect(cwd);
|
|
1953
3295
|
if (env.configType === "none") {
|
|
1954
3296
|
const useFlat = env.eslintMajor !== null && env.eslintMajor >= 9;
|
|
1955
|
-
const configPath = useFlat ?
|
|
3297
|
+
const configPath = useFlat ? import_path10.default.join(cwd, "eslint.config.mjs") : import_path10.default.join(cwd, ".eslintrc.js");
|
|
1956
3298
|
const content = useFlat ? generateFlatConfig("recommended") : generateLegacyConfig("recommended");
|
|
1957
3299
|
writeConfig(configPath, content);
|
|
1958
|
-
log.success(`No config found \u2014 generated ${
|
|
3300
|
+
log.success(`No config found \u2014 generated ${import_chalk9.default.white(import_path10.default.relative(cwd, configPath))} with ignores included`);
|
|
1959
3301
|
} else {
|
|
1960
3302
|
const configPath = env.configPath;
|
|
1961
3303
|
const backupPath = backupConfig(configPath);
|
|
1962
|
-
log.info(`Backed up \u2192 ${
|
|
3304
|
+
log.info(`Backed up \u2192 ${import_chalk9.default.gray(import_path10.default.relative(cwd, backupPath))}`);
|
|
1963
3305
|
const existing = readConfig(configPath);
|
|
1964
3306
|
const patched = isFlat(env.configType) ? addIgnoresToFlatConfig(existing) : addIgnoresToLegacyConfig(existing);
|
|
1965
3307
|
if (patched === existing) {
|
|
1966
3308
|
log.warn("Ignore patterns already present in config \u2014 no changes made.");
|
|
1967
3309
|
} else {
|
|
1968
3310
|
writeConfig(configPath, patched);
|
|
1969
|
-
log.success(`Patched ${
|
|
3311
|
+
log.success(`Patched ${import_chalk9.default.white(import_path10.default.relative(cwd, configPath))} with ignore patterns`);
|
|
1970
3312
|
}
|
|
1971
3313
|
}
|
|
1972
3314
|
log.blank();
|
|
1973
3315
|
log.info("Noisy directories will now be skipped in all future ESLint runs.");
|
|
1974
|
-
log.info(`Run ${
|
|
3316
|
+
log.info(`Run ${import_chalk9.default.cyan("ai-guard run")} to verify cleaner results.`);
|
|
1975
3317
|
log.blank();
|
|
1976
3318
|
});
|
|
1977
3319
|
}
|
|
1978
3320
|
|
|
1979
3321
|
// cli/commands/baseline.ts
|
|
1980
|
-
var
|
|
1981
|
-
var
|
|
1982
|
-
var
|
|
1983
|
-
var
|
|
3322
|
+
var import_fs8 = __toESM(require("fs"));
|
|
3323
|
+
var import_path11 = __toESM(require("path"));
|
|
3324
|
+
var import_chalk10 = __toESM(require("chalk"));
|
|
3325
|
+
var import_ora3 = __toESM(require("ora"));
|
|
1984
3326
|
var BASELINE_FILE = ".ai-guard-baseline.json";
|
|
1985
3327
|
function saveBaseline(result, preset, mode, cwd) {
|
|
1986
3328
|
const entries = result.files.map((f) => ({
|
|
@@ -1999,15 +3341,15 @@ function saveBaseline(result, preset, mode, cwd) {
|
|
|
1999
3341
|
totalIssues: result.totalIssues,
|
|
2000
3342
|
entries
|
|
2001
3343
|
};
|
|
2002
|
-
const baselinePath =
|
|
2003
|
-
|
|
3344
|
+
const baselinePath = import_path11.default.join(cwd, BASELINE_FILE);
|
|
3345
|
+
import_fs8.default.writeFileSync(baselinePath, JSON.stringify(baseline, null, 2), "utf-8");
|
|
2004
3346
|
return baselinePath;
|
|
2005
3347
|
}
|
|
2006
3348
|
function loadBaseline(cwd) {
|
|
2007
|
-
const baselinePath =
|
|
2008
|
-
if (!
|
|
3349
|
+
const baselinePath = import_path11.default.join(cwd, BASELINE_FILE);
|
|
3350
|
+
if (!import_fs8.default.existsSync(baselinePath)) return null;
|
|
2009
3351
|
try {
|
|
2010
|
-
const raw =
|
|
3352
|
+
const raw = import_fs8.default.readFileSync(baselinePath, "utf-8");
|
|
2011
3353
|
return JSON.parse(raw);
|
|
2012
3354
|
} catch {
|
|
2013
3355
|
return null;
|
|
@@ -2079,7 +3421,7 @@ function registerBaselineCommand(program2) {
|
|
|
2079
3421
|
const doCheck = opts.check ?? (existingBaseline !== null && !opts.save);
|
|
2080
3422
|
if (doSave) {
|
|
2081
3423
|
log.info("Scanning project to save baseline\u2026");
|
|
2082
|
-
const spinner = (0,
|
|
3424
|
+
const spinner = (0, import_ora3.default)({ text: "Running analysis\u2026", color: "cyan" }).start();
|
|
2083
3425
|
let result;
|
|
2084
3426
|
try {
|
|
2085
3427
|
result = await runEslint({ preset, targetPath: opts.path });
|
|
@@ -2091,30 +3433,30 @@ function registerBaselineCommand(program2) {
|
|
|
2091
3433
|
}
|
|
2092
3434
|
const baselinePath = saveBaseline(result, preset, mode, cwd);
|
|
2093
3435
|
log.success(
|
|
2094
|
-
`Baseline saved \u2192 ${
|
|
3436
|
+
`Baseline saved \u2192 ${import_chalk10.default.white(import_path11.default.relative(cwd, baselinePath))}`
|
|
2095
3437
|
);
|
|
2096
3438
|
log.info(` ${result.totalIssues} issues recorded (${result.totalErrors} errors, ${result.totalWarnings} warnings)`);
|
|
2097
|
-
log.info(` Preset: ${
|
|
2098
|
-
log.info(` Mode: ${
|
|
3439
|
+
log.info(` Preset: ${import_chalk10.default.cyan(preset)}`);
|
|
3440
|
+
log.info(` Mode: ${import_chalk10.default.cyan(mode)}`);
|
|
2099
3441
|
log.blank();
|
|
2100
|
-
log.info("Future runs of " +
|
|
2101
|
-
log.info("Add " +
|
|
3442
|
+
log.info("Future runs of " + import_chalk10.default.cyan("ai-guard baseline --check") + " will show only NEW issues.");
|
|
3443
|
+
log.info("Add " + import_chalk10.default.white(BASELINE_FILE) + " to your git repository to share baseline with your team.");
|
|
2102
3444
|
log.blank();
|
|
2103
3445
|
return;
|
|
2104
3446
|
}
|
|
2105
3447
|
if (doCheck) {
|
|
2106
3448
|
if (!existingBaseline) {
|
|
2107
|
-
log.error(`No baseline found at ${
|
|
2108
|
-
log.info(`Run ${
|
|
3449
|
+
log.error(`No baseline found at ${import_chalk10.default.white(BASELINE_FILE)}`);
|
|
3450
|
+
log.info(`Run ${import_chalk10.default.cyan("ai-guard baseline --save")} first to create one.`);
|
|
2109
3451
|
process.exit(1);
|
|
2110
3452
|
}
|
|
2111
3453
|
const baselineDate = new Date(existingBaseline.createdAt).toLocaleString();
|
|
2112
|
-
log.info(`Baseline from: ${
|
|
2113
|
-
log.info(`Baseline preset: ${
|
|
2114
|
-
log.info(`Baseline mode: ${
|
|
2115
|
-
log.info(`Baseline issues: ${
|
|
3454
|
+
log.info(`Baseline from: ${import_chalk10.default.gray(baselineDate)}`);
|
|
3455
|
+
log.info(`Baseline preset: ${import_chalk10.default.cyan(existingBaseline.preset)}`);
|
|
3456
|
+
log.info(`Baseline mode: ${import_chalk10.default.cyan(existingBaseline.mode ?? "strict")}`);
|
|
3457
|
+
log.info(`Baseline issues: ${import_chalk10.default.gray(existingBaseline.totalIssues)}`);
|
|
2116
3458
|
log.blank();
|
|
2117
|
-
const spinner = (0,
|
|
3459
|
+
const spinner = (0, import_ora3.default)({ text: "Scanning for new issues\u2026", color: "cyan" }).start();
|
|
2118
3460
|
let result;
|
|
2119
3461
|
try {
|
|
2120
3462
|
result = await runEslint({ preset, targetPath: opts.path });
|
|
@@ -2137,7 +3479,7 @@ function registerBaselineCommand(program2) {
|
|
|
2137
3479
|
return;
|
|
2138
3480
|
}
|
|
2139
3481
|
log.print(
|
|
2140
|
-
|
|
3482
|
+
import_chalk10.default.bold.red(` \u2716 ${newResult.totalIssues} new issue${newResult.totalIssues !== 1 ? "s" : ""} since baseline`)
|
|
2141
3483
|
);
|
|
2142
3484
|
log.blank();
|
|
2143
3485
|
if (newResult.ruleBreakdown.size > 0) {
|
|
@@ -2153,7 +3495,7 @@ function registerBaselineCommand(program2) {
|
|
|
2153
3495
|
log.blank();
|
|
2154
3496
|
for (const file of newResult.files) {
|
|
2155
3497
|
log.print(
|
|
2156
|
-
` ${
|
|
3498
|
+
` ${import_chalk10.default.bold.white(file.filePath)} ` + import_chalk10.default.gray(`(${file.issues.length} new)`)
|
|
2157
3499
|
);
|
|
2158
3500
|
for (const issue of file.issues) {
|
|
2159
3501
|
log.issue(issue.message, issue.severity, issue.line, issue.column);
|
|
@@ -2166,7 +3508,7 @@ function registerBaselineCommand(program2) {
|
|
|
2166
3508
|
log.error(
|
|
2167
3509
|
`${newResult.totalIssues} new issue${newResult.totalIssues !== 1 ? "s" : ""} introduced since baseline.`
|
|
2168
3510
|
);
|
|
2169
|
-
log.info(`Run ${
|
|
3511
|
+
log.info(`Run ${import_chalk10.default.cyan("ai-guard baseline --save")} to update the baseline after fixing them.`);
|
|
2170
3512
|
log.blank();
|
|
2171
3513
|
process.exit(1);
|
|
2172
3514
|
}
|
|
@@ -2174,9 +3516,9 @@ function registerBaselineCommand(program2) {
|
|
|
2174
3516
|
}
|
|
2175
3517
|
|
|
2176
3518
|
// cli/commands/init-context.ts
|
|
2177
|
-
var
|
|
2178
|
-
var
|
|
2179
|
-
var
|
|
3519
|
+
var import_fs9 = __toESM(require("fs"));
|
|
3520
|
+
var import_path12 = __toESM(require("path"));
|
|
3521
|
+
var import_chalk11 = __toESM(require("chalk"));
|
|
2180
3522
|
var import_prompts2 = require("@inquirer/prompts");
|
|
2181
3523
|
var RULE_CATEGORIES = {
|
|
2182
3524
|
"error-handling": [
|
|
@@ -2539,23 +3881,23 @@ var AGENTS = [
|
|
|
2539
3881
|
{
|
|
2540
3882
|
id: "copilot",
|
|
2541
3883
|
label: "GitHub Copilot",
|
|
2542
|
-
filePath:
|
|
3884
|
+
filePath: import_path12.default.join(".github", "copilot-instructions.md"),
|
|
2543
3885
|
generate: generateCopilotFile
|
|
2544
3886
|
}
|
|
2545
3887
|
];
|
|
2546
3888
|
function getPackageVersion2(cwd) {
|
|
2547
3889
|
try {
|
|
2548
|
-
const pkgPath =
|
|
2549
|
-
if (
|
|
2550
|
-
const pkg = JSON.parse(
|
|
3890
|
+
const pkgPath = import_path12.default.join(cwd, "node_modules", "eslint-plugin-ai-guard", "package.json");
|
|
3891
|
+
if (import_fs9.default.existsSync(pkgPath)) {
|
|
3892
|
+
const pkg = JSON.parse(import_fs9.default.readFileSync(pkgPath, "utf-8"));
|
|
2551
3893
|
if (pkg.version) return pkg.version;
|
|
2552
3894
|
}
|
|
2553
3895
|
} catch {
|
|
2554
3896
|
}
|
|
2555
3897
|
try {
|
|
2556
|
-
const pkgPath =
|
|
2557
|
-
if (
|
|
2558
|
-
const pkg = JSON.parse(
|
|
3898
|
+
const pkgPath = import_path12.default.join(cwd, "package.json");
|
|
3899
|
+
if (import_fs9.default.existsSync(pkgPath)) {
|
|
3900
|
+
const pkg = JSON.parse(import_fs9.default.readFileSync(pkgPath, "utf-8"));
|
|
2559
3901
|
if (pkg.version) return pkg.version;
|
|
2560
3902
|
}
|
|
2561
3903
|
} catch {
|
|
@@ -2584,7 +3926,7 @@ function registerInitContextCommand(program2) {
|
|
|
2584
3926
|
const isForce = opts.force === true;
|
|
2585
3927
|
log.banner("AI GUARD INIT-CONTEXT");
|
|
2586
3928
|
if (isDryRun) {
|
|
2587
|
-
log.print(` ${
|
|
3929
|
+
log.print(` ${import_chalk11.default.yellow("\u2691 DRY RUN \u2014 no files will be written")}`);
|
|
2588
3930
|
log.blank();
|
|
2589
3931
|
}
|
|
2590
3932
|
let selectedAgents;
|
|
@@ -2617,13 +3959,13 @@ function registerInitContextCommand(program2) {
|
|
|
2617
3959
|
selectedAgents = AGENTS.filter((a) => selected.includes(a.id));
|
|
2618
3960
|
}
|
|
2619
3961
|
}
|
|
2620
|
-
log.info(`Categories: ${
|
|
2621
|
-
log.info(`Version: ${
|
|
3962
|
+
log.info(`Categories: ${import_chalk11.default.cyan(categories.join(", "))}`);
|
|
3963
|
+
log.info(`Version: ${import_chalk11.default.cyan(version)}`);
|
|
2622
3964
|
log.blank();
|
|
2623
3965
|
const results = [];
|
|
2624
3966
|
for (const agent of selectedAgents) {
|
|
2625
|
-
const fullPath =
|
|
2626
|
-
const exists =
|
|
3967
|
+
const fullPath = import_path12.default.join(cwd, agent.filePath);
|
|
3968
|
+
const exists = import_fs9.default.existsSync(fullPath);
|
|
2627
3969
|
if (exists && !isForce && !isDryRun) {
|
|
2628
3970
|
const overwrite = await (0, import_prompts2.confirm)({
|
|
2629
3971
|
message: `${agent.filePath} already exists. Overwrite?`,
|
|
@@ -2637,41 +3979,41 @@ function registerInitContextCommand(program2) {
|
|
|
2637
3979
|
const content = agent.generate(version, categories);
|
|
2638
3980
|
if (isDryRun) {
|
|
2639
3981
|
log.section(`Preview: ${agent.filePath}`);
|
|
2640
|
-
log.print(
|
|
3982
|
+
log.print(import_chalk11.default.gray("\u2500".repeat(60)));
|
|
2641
3983
|
for (const line of content.split("\n").slice(0, 20)) {
|
|
2642
|
-
log.print(
|
|
3984
|
+
log.print(import_chalk11.default.gray(` ${line}`));
|
|
2643
3985
|
}
|
|
2644
|
-
log.print(
|
|
2645
|
-
log.print(
|
|
3986
|
+
log.print(import_chalk11.default.gray(" ... (truncated)"));
|
|
3987
|
+
log.print(import_chalk11.default.gray("\u2500".repeat(60)));
|
|
2646
3988
|
log.blank();
|
|
2647
3989
|
results.push({ file: agent.filePath, status: "dry-run" });
|
|
2648
3990
|
continue;
|
|
2649
3991
|
}
|
|
2650
|
-
const parentDir =
|
|
2651
|
-
if (!
|
|
2652
|
-
|
|
3992
|
+
const parentDir = import_path12.default.dirname(fullPath);
|
|
3993
|
+
if (!import_fs9.default.existsSync(parentDir)) {
|
|
3994
|
+
import_fs9.default.mkdirSync(parentDir, { recursive: true });
|
|
2653
3995
|
}
|
|
2654
|
-
|
|
3996
|
+
import_fs9.default.writeFileSync(fullPath, content, "utf-8");
|
|
2655
3997
|
results.push({ file: agent.filePath, status: "created" });
|
|
2656
3998
|
}
|
|
2657
3999
|
log.section("Summary");
|
|
2658
4000
|
for (const result of results) {
|
|
2659
4001
|
switch (result.status) {
|
|
2660
4002
|
case "created":
|
|
2661
|
-
log.print(` ${
|
|
4003
|
+
log.print(` ${import_chalk11.default.green("\u2705")} Created ${import_chalk11.default.white(result.file)}`);
|
|
2662
4004
|
break;
|
|
2663
4005
|
case "skipped":
|
|
2664
|
-
log.print(` ${
|
|
4006
|
+
log.print(` ${import_chalk11.default.yellow("\u23ED")} Skipped ${import_chalk11.default.white(result.file)} ${import_chalk11.default.gray("(already exists \u2014 use --force to overwrite)")}`);
|
|
2665
4007
|
break;
|
|
2666
4008
|
case "dry-run":
|
|
2667
|
-
log.print(` ${
|
|
4009
|
+
log.print(` ${import_chalk11.default.cyan("\u{1F4DD}")} Would create ${import_chalk11.default.white(result.file)}`);
|
|
2668
4010
|
break;
|
|
2669
4011
|
}
|
|
2670
4012
|
}
|
|
2671
4013
|
log.blank();
|
|
2672
4014
|
if (isDryRun) {
|
|
2673
4015
|
log.info("Dry run complete \u2014 no files were written.");
|
|
2674
|
-
log.info(`Re-run without ${
|
|
4016
|
+
log.info(`Re-run without ${import_chalk11.default.cyan("--dry-run")} to apply.`);
|
|
2675
4017
|
} else {
|
|
2676
4018
|
const createdCount = results.filter((r) => r.status === "created").length;
|
|
2677
4019
|
if (createdCount > 0) {
|
|
@@ -2685,9 +4027,9 @@ function registerInitContextCommand(program2) {
|
|
|
2685
4027
|
}
|
|
2686
4028
|
|
|
2687
4029
|
// cli/commands/report.ts
|
|
2688
|
-
var
|
|
2689
|
-
var
|
|
2690
|
-
var
|
|
4030
|
+
var import_fs10 = __toESM(require("fs"));
|
|
4031
|
+
var import_path13 = __toESM(require("path"));
|
|
4032
|
+
var import_ora4 = __toESM(require("ora"));
|
|
2691
4033
|
var RULE_META = {
|
|
2692
4034
|
"ai-guard/no-empty-catch": { description: "Empty catch blocks silently swallow errors", category: "Reliability" },
|
|
2693
4035
|
"ai-guard/no-broad-exception": { description: "Catching all exceptions hides bugs", category: "Reliability" },
|
|
@@ -2710,28 +4052,28 @@ var RULE_META = {
|
|
|
2710
4052
|
};
|
|
2711
4053
|
function getProjectName(cwd) {
|
|
2712
4054
|
try {
|
|
2713
|
-
const pkgPath =
|
|
2714
|
-
if (
|
|
2715
|
-
const pkg = JSON.parse(
|
|
4055
|
+
const pkgPath = import_path13.default.join(cwd, "package.json");
|
|
4056
|
+
if (import_fs10.default.existsSync(pkgPath)) {
|
|
4057
|
+
const pkg = JSON.parse(import_fs10.default.readFileSync(pkgPath, "utf-8"));
|
|
2716
4058
|
if (pkg.name) return pkg.name;
|
|
2717
4059
|
}
|
|
2718
4060
|
} catch {
|
|
2719
4061
|
}
|
|
2720
|
-
return
|
|
4062
|
+
return import_path13.default.basename(cwd);
|
|
2721
4063
|
}
|
|
2722
4064
|
function getPluginVersion(cwd) {
|
|
2723
4065
|
try {
|
|
2724
|
-
const pkgPath =
|
|
2725
|
-
if (
|
|
2726
|
-
const pkg = JSON.parse(
|
|
4066
|
+
const pkgPath = import_path13.default.resolve(__dirname, "../../package.json");
|
|
4067
|
+
if (import_fs10.default.existsSync(pkgPath)) {
|
|
4068
|
+
const pkg = JSON.parse(import_fs10.default.readFileSync(pkgPath, "utf-8"));
|
|
2727
4069
|
if (pkg.version) return pkg.version;
|
|
2728
4070
|
}
|
|
2729
4071
|
} catch {
|
|
2730
4072
|
}
|
|
2731
4073
|
try {
|
|
2732
|
-
const pkgPath =
|
|
2733
|
-
if (
|
|
2734
|
-
const pkg = JSON.parse(
|
|
4074
|
+
const pkgPath = import_path13.default.join(cwd, "node_modules", "eslint-plugin-ai-guard", "package.json");
|
|
4075
|
+
if (import_fs10.default.existsSync(pkgPath)) {
|
|
4076
|
+
const pkg = JSON.parse(import_fs10.default.readFileSync(pkgPath, "utf-8"));
|
|
2735
4077
|
if (pkg.version) return pkg.version;
|
|
2736
4078
|
}
|
|
2737
4079
|
} catch {
|
|
@@ -3086,7 +4428,7 @@ function registerReportCommand(program2) {
|
|
|
3086
4428
|
"Generate a beautiful HTML report of AI Guard scan results\nPerfect for sharing with your team or in Slack."
|
|
3087
4429
|
).option("--path <dir>", "Directory or file to scan", ".").option("--preset <preset>", "Rule preset: recommended | strict | security", "recommended").option("--html", "Generate HTML report (default behavior)").option("--json", "Output raw JSON to stdout instead of writing HTML").option("--output <filename>", "Output file name", "ai-guard-report.html").option("--no-open", "Do not open the report in the browser after generating").action(async (opts) => {
|
|
3088
4430
|
const preset = ["recommended", "strict", "security"].includes(opts.preset) ? opts.preset : "recommended";
|
|
3089
|
-
const spinner = opts.json ? null : (0,
|
|
4431
|
+
const spinner = opts.json ? null : (0, import_ora4.default)({ text: "Scanning\u2026", color: "cyan" }).start();
|
|
3090
4432
|
let result;
|
|
3091
4433
|
try {
|
|
3092
4434
|
result = await runEslint({ preset, targetPath: opts.path });
|
|
@@ -3108,8 +4450,8 @@ function registerReportCommand(program2) {
|
|
|
3108
4450
|
log.banner("AI GUARD REPORT");
|
|
3109
4451
|
log.blank();
|
|
3110
4452
|
const html = generateHtml(reportData);
|
|
3111
|
-
const outputPath =
|
|
3112
|
-
|
|
4453
|
+
const outputPath = import_path13.default.resolve(cwd, opts.output);
|
|
4454
|
+
import_fs10.default.writeFileSync(outputPath, html, "utf-8");
|
|
3113
4455
|
log.success(`Report saved: ${opts.output}`);
|
|
3114
4456
|
log.info(
|
|
3115
4457
|
`Score: ${reportData.score}/100 \xB7 ${reportData.totalErrors} errors \xB7 ${reportData.totalWarnings} warnings`
|
|
@@ -3124,9 +4466,9 @@ function registerReportCommand(program2) {
|
|
|
3124
4466
|
await open(outputPath);
|
|
3125
4467
|
log.info("Opened in your browser.");
|
|
3126
4468
|
} else {
|
|
3127
|
-
const { execSync } = await import("child_process");
|
|
4469
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
3128
4470
|
const cmd = process.platform === "win32" ? `start "" "${outputPath}"` : process.platform === "darwin" ? `open "${outputPath}"` : `xdg-open "${outputPath}"`;
|
|
3129
|
-
|
|
4471
|
+
execSync2(cmd);
|
|
3130
4472
|
log.info("Opened in your browser.");
|
|
3131
4473
|
}
|
|
3132
4474
|
} catch {
|
|
@@ -3142,8 +4484,8 @@ function registerReportCommand(program2) {
|
|
|
3142
4484
|
// cli/index.ts
|
|
3143
4485
|
function getVersion() {
|
|
3144
4486
|
try {
|
|
3145
|
-
const pkgPath =
|
|
3146
|
-
const raw =
|
|
4487
|
+
const pkgPath = import_path14.default.resolve(__dirname, "../../package.json");
|
|
4488
|
+
const raw = import_fs11.default.readFileSync(pkgPath, "utf-8");
|
|
3147
4489
|
const pkg = JSON.parse(raw);
|
|
3148
4490
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
3149
4491
|
} catch {
|
|
@@ -3152,9 +4494,10 @@ function getVersion() {
|
|
|
3152
4494
|
}
|
|
3153
4495
|
var program = new import_commander.Command();
|
|
3154
4496
|
program.name("ai-guard").description(
|
|
3155
|
-
"Production-grade CLI for eslint-plugin-ai-guard\nCatch AI-generated code issues instantly \u2014 no ESLint config required.\n\nQuick start:\n npx ai-guard run Scan current project\n npx ai-guard init Auto-configure ESLint\n npx ai-guard init-context Generate AI agent rules\n npx ai-guard report Generate shareable HTML report\n npx ai-guard doctor Check your setup\n npx ai-guard baseline Save baseline, track new issues only"
|
|
4497
|
+
"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"
|
|
3156
4498
|
).version(getVersion(), "-v, --version", "Print version number").helpOption("-h, --help", "Show help");
|
|
3157
4499
|
registerRunCommand(program);
|
|
4500
|
+
registerChangedCommand(program);
|
|
3158
4501
|
registerInitCommand(program);
|
|
3159
4502
|
registerDoctorCommand(program);
|
|
3160
4503
|
registerPresetCommand(program);
|