@undercurrentai/eslint-plugin-ai-guard 2.0.0-beta.3
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/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/cli/index.js +2293 -0
- package/dist/index.d.mts +92 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.js +3679 -0
- package/dist/index.mjs +3652 -0
- package/package.json +104 -0
|
@@ -0,0 +1,2293 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
|
|
33
|
+
// cli/utils/constants.ts
|
|
34
|
+
var constants_exports = {};
|
|
35
|
+
__export(constants_exports, {
|
|
36
|
+
LEGACY_PLUGIN_NAME: () => LEGACY_PLUGIN_NAME,
|
|
37
|
+
PLUGIN_NAME: () => PLUGIN_NAME,
|
|
38
|
+
PLUGIN_NAMES: () => PLUGIN_NAMES,
|
|
39
|
+
contentReferencesPlugin: () => contentReferencesPlugin
|
|
40
|
+
});
|
|
41
|
+
function contentReferencesPlugin(content) {
|
|
42
|
+
return /['"`]@undercurrent\/eslint-plugin-ai-guard['"`]/.test(content) || /['"`]eslint-plugin-ai-guard['"`]/.test(content) || content.includes("'ai-guard'") || content.includes('"ai-guard"');
|
|
43
|
+
}
|
|
44
|
+
var PLUGIN_NAME, LEGACY_PLUGIN_NAME, PLUGIN_NAMES;
|
|
45
|
+
var init_constants = __esm({
|
|
46
|
+
"cli/utils/constants.ts"() {
|
|
47
|
+
"use strict";
|
|
48
|
+
PLUGIN_NAME = "@undercurrentai/eslint-plugin-ai-guard";
|
|
49
|
+
LEGACY_PLUGIN_NAME = "eslint-plugin-ai-guard";
|
|
50
|
+
PLUGIN_NAMES = [PLUGIN_NAME, LEGACY_PLUGIN_NAME];
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// cli/utils/config-manager.ts
|
|
55
|
+
var config_manager_exports = {};
|
|
56
|
+
__export(config_manager_exports, {
|
|
57
|
+
addIgnoresToFlatConfig: () => addIgnoresToFlatConfig,
|
|
58
|
+
addIgnoresToLegacyConfig: () => addIgnoresToLegacyConfig,
|
|
59
|
+
backupConfig: () => backupConfig,
|
|
60
|
+
generateFlatConfig: () => generateFlatConfig,
|
|
61
|
+
generateLegacyConfig: () => generateLegacyConfig,
|
|
62
|
+
getConfigFilePath: () => getConfigFilePath,
|
|
63
|
+
isInvalidAiGuardConfig: () => isInvalidAiGuardConfig,
|
|
64
|
+
patchFlatConfig: () => patchFlatConfig,
|
|
65
|
+
patchLegacyConfig: () => patchLegacyConfig,
|
|
66
|
+
readConfig: () => readConfig,
|
|
67
|
+
removeNukeIgnore: () => removeNukeIgnore,
|
|
68
|
+
repairInvalidFlatConfig: () => repairInvalidFlatConfig,
|
|
69
|
+
switchFlatPreset: () => switchFlatPreset,
|
|
70
|
+
switchLegacyPreset: () => switchLegacyPreset,
|
|
71
|
+
validateFlatConfigText: () => validateFlatConfigText,
|
|
72
|
+
validateLegacyConfigText: () => validateLegacyConfigText,
|
|
73
|
+
writeConfig: () => writeConfig
|
|
74
|
+
});
|
|
75
|
+
function backupConfig(configPath) {
|
|
76
|
+
const backupPath = `${configPath}.bak`;
|
|
77
|
+
import_fs3.default.copyFileSync(configPath, backupPath);
|
|
78
|
+
return backupPath;
|
|
79
|
+
}
|
|
80
|
+
function readConfig(configPath) {
|
|
81
|
+
return import_fs3.default.readFileSync(configPath, "utf-8");
|
|
82
|
+
}
|
|
83
|
+
function writeConfig(configPath, content) {
|
|
84
|
+
import_fs3.default.writeFileSync(configPath, content, "utf-8");
|
|
85
|
+
}
|
|
86
|
+
function generateFlatConfig(preset) {
|
|
87
|
+
return `import aiGuard from '${PLUGIN_NAME}';
|
|
88
|
+
|
|
89
|
+
export default [
|
|
90
|
+
// Ignore generated / dependency directories
|
|
91
|
+
{
|
|
92
|
+
ignores: [
|
|
93
|
+
'node_modules/**',
|
|
94
|
+
'.next/**',
|
|
95
|
+
'dist/**',
|
|
96
|
+
'build/**',
|
|
97
|
+
'coverage/**',
|
|
98
|
+
'out/**',
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// ai-guard: catch AI-generated code patterns
|
|
103
|
+
{
|
|
104
|
+
plugins: {
|
|
105
|
+
'ai-guard': aiGuard,
|
|
106
|
+
},
|
|
107
|
+
rules: {
|
|
108
|
+
...aiGuard.configs.${preset}.rules,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
function generateLegacyConfig(preset) {
|
|
115
|
+
return `module.exports = {
|
|
116
|
+
plugins: ['ai-guard'],
|
|
117
|
+
extends: ['plugin:ai-guard/${preset}'],
|
|
118
|
+
ignorePatterns: [
|
|
119
|
+
'node_modules/',
|
|
120
|
+
'.next/',
|
|
121
|
+
'dist/',
|
|
122
|
+
'build/',
|
|
123
|
+
'coverage/',
|
|
124
|
+
'out/',
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
function isInvalidAiGuardConfig(content) {
|
|
130
|
+
if (!contentReferencesPlugin(content) && !content.includes("ai-guard")) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (/['"]ai-guard['"]\s*:\s*aiGuard\.default/.test(content)) return true;
|
|
134
|
+
if (/\.\.\.aiGuard\.(recommended|strict|security)\.rules/.test(content)) return true;
|
|
135
|
+
if (/rules\s*:\s*aiGuard\.(recommended|strict|security)\.rules/.test(content)) return true;
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
function repairInvalidFlatConfig(content) {
|
|
139
|
+
let repaired = content;
|
|
140
|
+
repaired = repaired.replace(/(['"]ai-guard['"]\s*:\s*)aiGuard\.default/g, "$1aiGuard");
|
|
141
|
+
repaired = repaired.replace(
|
|
142
|
+
/rules\s*:\s*aiGuard\.([a-zA-Z0-9_]+)\.rules(,?)/g,
|
|
143
|
+
"rules: {\n ...aiGuard.configs.$1.rules,\n }$2"
|
|
144
|
+
);
|
|
145
|
+
repaired = repaired.replace(/\.\.\.aiGuard\.([a-zA-Z0-9_]+)\.rules/g, "...aiGuard.configs.$1.rules");
|
|
146
|
+
return repaired;
|
|
147
|
+
}
|
|
148
|
+
function patchFlatConfig(existing, preset) {
|
|
149
|
+
if (isInvalidAiGuardConfig(existing)) {
|
|
150
|
+
return repairInvalidFlatConfig(existing);
|
|
151
|
+
}
|
|
152
|
+
if (contentReferencesPlugin(existing)) {
|
|
153
|
+
return existing;
|
|
154
|
+
}
|
|
155
|
+
const importLine = `import aiGuard from '${PLUGIN_NAME}';
|
|
156
|
+
`;
|
|
157
|
+
const rulesBlock = `
|
|
158
|
+
// ai-guard injected by ai-guard CLI
|
|
159
|
+
{
|
|
160
|
+
plugins: {
|
|
161
|
+
'ai-guard': aiGuard,
|
|
162
|
+
},
|
|
163
|
+
rules: {
|
|
164
|
+
...aiGuard.configs.${preset}.rules,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
`;
|
|
168
|
+
let patched = existing;
|
|
169
|
+
const lastImportIdx = findLastImportEnd(patched);
|
|
170
|
+
if (lastImportIdx > -1) {
|
|
171
|
+
patched = patched.slice(0, lastImportIdx) + importLine + patched.slice(lastImportIdx);
|
|
172
|
+
} else {
|
|
173
|
+
patched = importLine + patched;
|
|
174
|
+
}
|
|
175
|
+
const arrayCloseMatch = findConfigArrayClose(patched);
|
|
176
|
+
if (arrayCloseMatch !== null) {
|
|
177
|
+
patched = patched.slice(0, arrayCloseMatch) + rulesBlock + patched.slice(arrayCloseMatch);
|
|
178
|
+
}
|
|
179
|
+
return patched;
|
|
180
|
+
}
|
|
181
|
+
function findConfigArrayClose(src) {
|
|
182
|
+
const matches = Array.from(src.matchAll(/\](\))?\s*;?/g));
|
|
183
|
+
if (matches.length === 0) return null;
|
|
184
|
+
const last = matches[matches.length - 1];
|
|
185
|
+
return last.index ?? null;
|
|
186
|
+
}
|
|
187
|
+
function findLastImportEnd(src) {
|
|
188
|
+
const lines = src.split("\n");
|
|
189
|
+
let lastImportLine = -1;
|
|
190
|
+
for (let i = 0; i < lines.length; i++) {
|
|
191
|
+
if (/^\s*import\s+/.test(lines[i])) lastImportLine = i;
|
|
192
|
+
}
|
|
193
|
+
if (lastImportLine === -1) return -1;
|
|
194
|
+
let offset = 0;
|
|
195
|
+
for (let i = 0; i <= lastImportLine; i++) {
|
|
196
|
+
offset += lines[i].length + 1;
|
|
197
|
+
}
|
|
198
|
+
return offset;
|
|
199
|
+
}
|
|
200
|
+
function patchLegacyConfig(existing, preset) {
|
|
201
|
+
if (existing.includes("ai-guard")) {
|
|
202
|
+
return existing;
|
|
203
|
+
}
|
|
204
|
+
let patched = existing;
|
|
205
|
+
if (patched.includes("plugins:")) {
|
|
206
|
+
patched = patched.replace(/plugins:\s*\[/, `plugins: ['ai-guard', `);
|
|
207
|
+
} else {
|
|
208
|
+
patched = patched.replace(
|
|
209
|
+
"module.exports = {",
|
|
210
|
+
`module.exports = {
|
|
211
|
+
plugins: ['ai-guard'],`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (patched.includes("extends:")) {
|
|
215
|
+
patched = patched.replace(
|
|
216
|
+
/extends:\s*\[/,
|
|
217
|
+
`extends: ['plugin:ai-guard/${preset}', `
|
|
218
|
+
);
|
|
219
|
+
} else {
|
|
220
|
+
patched = patched.replace(
|
|
221
|
+
"module.exports = {",
|
|
222
|
+
`module.exports = {
|
|
223
|
+
extends: ['plugin:ai-guard/${preset}'],`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return patched;
|
|
227
|
+
}
|
|
228
|
+
function switchFlatPreset(existing, preset) {
|
|
229
|
+
let patched = existing;
|
|
230
|
+
patched = patched.replace(
|
|
231
|
+
/\.\.\.aiGuard\.configs\.(recommended|strict|security)\.rules/g,
|
|
232
|
+
`...aiGuard.configs.${preset}.rules`
|
|
233
|
+
);
|
|
234
|
+
patched = patched.replace(
|
|
235
|
+
/['"]ai-guard\/(recommended|strict|security)['"]/g,
|
|
236
|
+
`'ai-guard/${preset}'`
|
|
237
|
+
);
|
|
238
|
+
return patched;
|
|
239
|
+
}
|
|
240
|
+
function switchLegacyPreset(existing, preset) {
|
|
241
|
+
let patched = existing;
|
|
242
|
+
patched = patched.replace(
|
|
243
|
+
/['"]plugin:ai-guard\/(recommended|strict|security)['"]/g,
|
|
244
|
+
`'plugin:ai-guard/${preset}'`
|
|
245
|
+
);
|
|
246
|
+
patched = patched.replace(
|
|
247
|
+
/['"]ai-guard\/(recommended|strict|security)['"]/g,
|
|
248
|
+
`'ai-guard/${preset}'`
|
|
249
|
+
);
|
|
250
|
+
return patched;
|
|
251
|
+
}
|
|
252
|
+
function removeNukeIgnore(existing) {
|
|
253
|
+
const nukePatterns = [/^\s*['"]\*\*\/\*['"]\s*,?\s*$/gm];
|
|
254
|
+
const hasNuke = existing.includes('"**/*"') || existing.includes("'**/*'");
|
|
255
|
+
if (!hasNuke) return { content: existing, changed: false };
|
|
256
|
+
let patched = existing.replace(
|
|
257
|
+
/ignorePatterns:\s*\[.*?\*\*\/\*.*?\]/gs,
|
|
258
|
+
`ignorePatterns: ['node_modules/', '.next/', 'dist/', 'build/', 'coverage/']`
|
|
259
|
+
).replace(
|
|
260
|
+
/ignores:\s*\[.*?\*\*\/\*.*?\]/gs,
|
|
261
|
+
`ignores: ['node_modules/**', '.next/**', 'dist/**', 'build/**', 'coverage/**']`
|
|
262
|
+
);
|
|
263
|
+
if (patched === existing) {
|
|
264
|
+
for (const re of nukePatterns) {
|
|
265
|
+
patched = patched.replace(re, "");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return { content: patched, changed: patched !== existing };
|
|
269
|
+
}
|
|
270
|
+
function addIgnoresToFlatConfig(existing) {
|
|
271
|
+
if (existing.includes("ignores:")) return existing;
|
|
272
|
+
const block = `
|
|
273
|
+
// Default ignores added by ai-guard CLI
|
|
274
|
+
{
|
|
275
|
+
ignores: ${JSON.stringify(DEFAULT_FLAT_IGNORES)},
|
|
276
|
+
},
|
|
277
|
+
`;
|
|
278
|
+
const lastBracket = existing.lastIndexOf("];");
|
|
279
|
+
if (lastBracket !== -1) {
|
|
280
|
+
return existing.slice(0, lastBracket) + block + existing.slice(lastBracket);
|
|
281
|
+
}
|
|
282
|
+
return existing;
|
|
283
|
+
}
|
|
284
|
+
function addIgnoresToLegacyConfig(existing) {
|
|
285
|
+
if (existing.includes("ignorePatterns")) return existing;
|
|
286
|
+
return existing.replace(
|
|
287
|
+
"module.exports = {",
|
|
288
|
+
`module.exports = {
|
|
289
|
+
ignorePatterns: ${JSON.stringify(DEFAULT_LEGACY_IGNORES)},`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
function getConfigFilePath(configType, cwd = process.cwd()) {
|
|
293
|
+
switch (configType) {
|
|
294
|
+
case "flat-js":
|
|
295
|
+
return import_path3.default.join(cwd, "eslint.config.js");
|
|
296
|
+
case "flat-mjs":
|
|
297
|
+
return import_path3.default.join(cwd, "eslint.config.mjs");
|
|
298
|
+
case "flat-cjs":
|
|
299
|
+
return import_path3.default.join(cwd, "eslint.config.cjs");
|
|
300
|
+
case "eslintrc-js":
|
|
301
|
+
return import_path3.default.join(cwd, ".eslintrc.js");
|
|
302
|
+
case "eslintrc-cjs":
|
|
303
|
+
return import_path3.default.join(cwd, ".eslintrc.cjs");
|
|
304
|
+
case "eslintrc-json":
|
|
305
|
+
return import_path3.default.join(cwd, ".eslintrc.json");
|
|
306
|
+
default:
|
|
307
|
+
return import_path3.default.join(cwd, ".eslintrc.js");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function validateFlatConfigText(content) {
|
|
311
|
+
const problems = [];
|
|
312
|
+
if (!content.includes("export default")) {
|
|
313
|
+
problems.push("Missing `export default` \u2014 file must be an ES module");
|
|
314
|
+
}
|
|
315
|
+
if (!contentReferencesPlugin(content) && !content.includes("aiGuard")) {
|
|
316
|
+
problems.push("Plugin import not found \u2014 ai-guard plugin may not be referenced");
|
|
317
|
+
}
|
|
318
|
+
if (content.includes('"**/*"') || content.includes("'**/*'")) {
|
|
319
|
+
problems.push(
|
|
320
|
+
'Config contains "**/*" ignore pattern \u2014 this will prevent all files from being linted'
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return problems;
|
|
324
|
+
}
|
|
325
|
+
function validateLegacyConfigText(content) {
|
|
326
|
+
const problems = [];
|
|
327
|
+
if (!content.includes("module.exports")) {
|
|
328
|
+
problems.push("Missing `module.exports` \u2014 legacy config must use CJS exports");
|
|
329
|
+
}
|
|
330
|
+
if (!content.includes("ai-guard")) {
|
|
331
|
+
problems.push("Plugin not referenced \u2014 add ai-guard to plugins and extends");
|
|
332
|
+
}
|
|
333
|
+
if (content.includes('"**/*"') || content.includes("'**/*'")) {
|
|
334
|
+
problems.push(
|
|
335
|
+
'Config contains "**/*" ignore pattern \u2014 this will prevent all files from being linted'
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return problems;
|
|
339
|
+
}
|
|
340
|
+
var import_fs3, import_path3, DEFAULT_FLAT_IGNORES, DEFAULT_LEGACY_IGNORES;
|
|
341
|
+
var init_config_manager = __esm({
|
|
342
|
+
"cli/utils/config-manager.ts"() {
|
|
343
|
+
"use strict";
|
|
344
|
+
import_fs3 = __toESM(require("fs"));
|
|
345
|
+
import_path3 = __toESM(require("path"));
|
|
346
|
+
init_constants();
|
|
347
|
+
DEFAULT_FLAT_IGNORES = [
|
|
348
|
+
"node_modules/**",
|
|
349
|
+
".next/**",
|
|
350
|
+
"dist/**",
|
|
351
|
+
"build/**",
|
|
352
|
+
"coverage/**",
|
|
353
|
+
"out/**"
|
|
354
|
+
];
|
|
355
|
+
DEFAULT_LEGACY_IGNORES = [
|
|
356
|
+
"node_modules/",
|
|
357
|
+
".next/",
|
|
358
|
+
"dist/",
|
|
359
|
+
"build/",
|
|
360
|
+
"coverage/",
|
|
361
|
+
"out/"
|
|
362
|
+
];
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// cli/index.ts
|
|
367
|
+
var import_commander = require("commander");
|
|
368
|
+
var import_fs7 = __toESM(require("fs"));
|
|
369
|
+
var import_path10 = __toESM(require("path"));
|
|
370
|
+
|
|
371
|
+
// cli/commands/run.ts
|
|
372
|
+
var import_ora = __toESM(require("ora"));
|
|
373
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
374
|
+
|
|
375
|
+
// cli/utils/eslint-runner.ts
|
|
376
|
+
var import_fs = __toESM(require("fs"));
|
|
377
|
+
var import_path = __toESM(require("path"));
|
|
378
|
+
var import_url = require("url");
|
|
379
|
+
|
|
380
|
+
// cli/utils/logger.ts
|
|
381
|
+
var import_chalk = __toESM(require("chalk"));
|
|
382
|
+
var PREFIX = {
|
|
383
|
+
success: import_chalk.default.green("\u2714"),
|
|
384
|
+
error: import_chalk.default.red("\u2716"),
|
|
385
|
+
warn: import_chalk.default.yellow("\u26A0"),
|
|
386
|
+
info: import_chalk.default.cyan("\u2139"),
|
|
387
|
+
section: import_chalk.default.bold.white,
|
|
388
|
+
bullet: import_chalk.default.gray("\u2022")
|
|
389
|
+
};
|
|
390
|
+
var log = {
|
|
391
|
+
info(msg) {
|
|
392
|
+
console.log(` ${PREFIX.info} ${import_chalk.default.white(msg)}`);
|
|
393
|
+
},
|
|
394
|
+
success(msg) {
|
|
395
|
+
console.log(` ${PREFIX.success} ${import_chalk.default.green(msg)}`);
|
|
396
|
+
},
|
|
397
|
+
warn(msg) {
|
|
398
|
+
console.log(` ${PREFIX.warn} ${import_chalk.default.yellow(msg)}`);
|
|
399
|
+
},
|
|
400
|
+
error(msg) {
|
|
401
|
+
console.error(` ${PREFIX.error} ${import_chalk.default.red(msg)}`);
|
|
402
|
+
},
|
|
403
|
+
section(title) {
|
|
404
|
+
console.log("");
|
|
405
|
+
console.log(import_chalk.default.bold.cyan(` \u2500\u2500 ${title} \u2500\u2500`));
|
|
406
|
+
console.log("");
|
|
407
|
+
},
|
|
408
|
+
rule(ruleName, count) {
|
|
409
|
+
console.log(
|
|
410
|
+
` ${PREFIX.bullet} ${import_chalk.default.yellow(ruleName)} ${import_chalk.default.gray(`(${count} issue${count !== 1 ? "s" : ""})`)}`
|
|
411
|
+
);
|
|
412
|
+
},
|
|
413
|
+
file(filePath, count) {
|
|
414
|
+
console.log(
|
|
415
|
+
` ${PREFIX.bullet} ${import_chalk.default.white(filePath)} ${import_chalk.default.gray(`\u2192 ${count} issue${count !== 1 ? "s" : ""}`)}`
|
|
416
|
+
);
|
|
417
|
+
},
|
|
418
|
+
issue(msg, severity, line, col) {
|
|
419
|
+
const icon = severity === 2 ? import_chalk.default.red("error") : import_chalk.default.yellow(" warn");
|
|
420
|
+
console.log(
|
|
421
|
+
` ${import_chalk.default.gray(`${line}:${col}`).padEnd(12)} ${icon} ${import_chalk.default.white(msg)}`
|
|
422
|
+
);
|
|
423
|
+
},
|
|
424
|
+
divider() {
|
|
425
|
+
console.log(import_chalk.default.gray(" " + "\u2500".repeat(60)));
|
|
426
|
+
},
|
|
427
|
+
blank() {
|
|
428
|
+
console.log("");
|
|
429
|
+
},
|
|
430
|
+
banner(title) {
|
|
431
|
+
console.log("");
|
|
432
|
+
console.log(import_chalk.default.bold.bgCyan.black(` ${title} `));
|
|
433
|
+
console.log("");
|
|
434
|
+
},
|
|
435
|
+
print(msg) {
|
|
436
|
+
console.log(msg);
|
|
437
|
+
},
|
|
438
|
+
debug(msg) {
|
|
439
|
+
if (process.env.AI_GUARD_DEBUG === "1") {
|
|
440
|
+
console.log(` ${import_chalk.default.gray("\xB7")} ${import_chalk.default.gray(msg)}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// cli/utils/eslint-runner.ts
|
|
446
|
+
function isRecord(value) {
|
|
447
|
+
return typeof value === "object" && value !== null;
|
|
448
|
+
}
|
|
449
|
+
function normalizePlugin(raw) {
|
|
450
|
+
if (isRecord(raw) && isRecord(raw.default) && isRecord(raw.default.rules)) {
|
|
451
|
+
return raw.default;
|
|
452
|
+
}
|
|
453
|
+
if (isRecord(raw) && isRecord(raw.rules)) {
|
|
454
|
+
return raw;
|
|
455
|
+
}
|
|
456
|
+
throw new Error(
|
|
457
|
+
"Could not load @undercurrentai/eslint-plugin-ai-guard. Run: npm install --save-dev @undercurrentai/eslint-plugin-ai-guard@next"
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
var RECOMMENDED_RULES = {
|
|
461
|
+
"ai-guard/no-empty-catch": "error",
|
|
462
|
+
"ai-guard/no-floating-promise": "error",
|
|
463
|
+
"ai-guard/no-hardcoded-secret": "error",
|
|
464
|
+
"ai-guard/no-eval-dynamic": "error",
|
|
465
|
+
"ai-guard/require-framework-auth": "warn",
|
|
466
|
+
"ai-guard/no-sql-string-concat": "warn",
|
|
467
|
+
"ai-guard/no-async-array-callback": "warn",
|
|
468
|
+
"ai-guard/no-unsafe-deserialize": "warn",
|
|
469
|
+
"ai-guard/require-framework-authz": "warn",
|
|
470
|
+
"ai-guard/require-webhook-signature": "warn"
|
|
471
|
+
};
|
|
472
|
+
var STRICT_RULES = {
|
|
473
|
+
"ai-guard/no-empty-catch": "error",
|
|
474
|
+
"ai-guard/no-catch-log-rethrow": "error",
|
|
475
|
+
"ai-guard/no-async-array-callback": "error",
|
|
476
|
+
"ai-guard/no-floating-promise": "error",
|
|
477
|
+
"ai-guard/no-hardcoded-secret": "error",
|
|
478
|
+
"ai-guard/no-eval-dynamic": "error",
|
|
479
|
+
"ai-guard/no-sql-string-concat": "error",
|
|
480
|
+
"ai-guard/no-unsafe-deserialize": "error",
|
|
481
|
+
"ai-guard/require-framework-auth": "error",
|
|
482
|
+
"ai-guard/require-framework-authz": "error",
|
|
483
|
+
"ai-guard/require-webhook-signature": "error",
|
|
484
|
+
"ai-guard/no-console-in-handler": "error",
|
|
485
|
+
"ai-guard/no-duplicate-logic-block": "error"
|
|
486
|
+
};
|
|
487
|
+
var SECURITY_RULES = {
|
|
488
|
+
"ai-guard/no-hardcoded-secret": "error",
|
|
489
|
+
"ai-guard/no-eval-dynamic": "error",
|
|
490
|
+
"ai-guard/no-sql-string-concat": "error",
|
|
491
|
+
"ai-guard/no-unsafe-deserialize": "warn",
|
|
492
|
+
"ai-guard/require-framework-auth": "warn",
|
|
493
|
+
"ai-guard/require-framework-authz": "warn",
|
|
494
|
+
"ai-guard/require-webhook-signature": "warn"
|
|
495
|
+
};
|
|
496
|
+
function getRules(preset) {
|
|
497
|
+
if (preset === "strict") return STRICT_RULES;
|
|
498
|
+
if (preset === "security") return SECURITY_RULES;
|
|
499
|
+
return RECOMMENDED_RULES;
|
|
500
|
+
}
|
|
501
|
+
var DEFAULT_IGNORE_PATTERNS = [
|
|
502
|
+
"**/node_modules/**",
|
|
503
|
+
"**/.next/**",
|
|
504
|
+
"**/dist/**",
|
|
505
|
+
"**/build/**",
|
|
506
|
+
"**/coverage/**",
|
|
507
|
+
"**/out/**",
|
|
508
|
+
"**/.git/**"
|
|
509
|
+
];
|
|
510
|
+
function isSkippablePatternError(error) {
|
|
511
|
+
const msg = error.message.toLowerCase();
|
|
512
|
+
const hasNoFilesSignal = msg.includes("no files") || msg.includes("no files matching");
|
|
513
|
+
const hasIgnoredSignal = msg.includes("ignored") || msg.includes("all files matched by") || msg.includes("are ignored") || msg.includes("was ignored") || msg.includes("file ignored");
|
|
514
|
+
return hasNoFilesSignal || hasIgnoredSignal;
|
|
515
|
+
}
|
|
516
|
+
async function loadPluginModuleFromCwd(cwd) {
|
|
517
|
+
const { createRequire: createRequire2 } = await import("module");
|
|
518
|
+
const { PLUGIN_NAMES: PLUGIN_NAMES2 } = await Promise.resolve().then(() => (init_constants(), constants_exports));
|
|
519
|
+
const cwdPkg = import_path.default.join(cwd, "package.json");
|
|
520
|
+
const anchor = import_fs.default.existsSync(cwdPkg) ? cwdPkg : import_path.default.join(cwd, "index.js");
|
|
521
|
+
const requireFromCwd = createRequire2(anchor);
|
|
522
|
+
let lastResolveError = null;
|
|
523
|
+
for (const pkgName of PLUGIN_NAMES2) {
|
|
524
|
+
let resolved;
|
|
525
|
+
try {
|
|
526
|
+
resolved = requireFromCwd.resolve(pkgName);
|
|
527
|
+
} catch (err) {
|
|
528
|
+
lastResolveError = err;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
return requireFromCwd(resolved);
|
|
532
|
+
}
|
|
533
|
+
const localDistEntry = import_path.default.join(cwd, "dist", "index.js");
|
|
534
|
+
if (import_fs.default.existsSync(localDistEntry)) {
|
|
535
|
+
return requireFromCwd(localDistEntry);
|
|
536
|
+
}
|
|
537
|
+
const localSrcEntry = import_path.default.join(cwd, "src", "index.ts");
|
|
538
|
+
if (import_fs.default.existsSync(localSrcEntry)) {
|
|
539
|
+
try {
|
|
540
|
+
return await import((0, import_url.pathToFileURL)(localSrcEntry).href);
|
|
541
|
+
} catch (err) {
|
|
542
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
543
|
+
log.debug(`Local src plugin import failed: ${reason}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const detail = lastResolveError instanceof Error ? ` (${lastResolveError.message})` : "";
|
|
547
|
+
throw new Error(
|
|
548
|
+
`@undercurrentai/eslint-plugin-ai-guard is not installed${detail}. Run: npm install --save-dev @undercurrentai/eslint-plugin-ai-guard@next`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
async function runEslint(options) {
|
|
552
|
+
const { ESLint } = await import("eslint").catch(() => {
|
|
553
|
+
throw new Error(
|
|
554
|
+
"ESLint is not installed. Run: npm install --save-dev eslint"
|
|
555
|
+
);
|
|
556
|
+
});
|
|
557
|
+
const rawPlugin = await loadPluginModuleFromCwd(process.cwd());
|
|
558
|
+
const plugin = normalizePlugin(rawPlugin);
|
|
559
|
+
const rules = getRules(options.preset);
|
|
560
|
+
const resolvedTargetPath = import_path.default.resolve(options.targetPath);
|
|
561
|
+
if (!import_fs.default.existsSync(resolvedTargetPath)) {
|
|
562
|
+
throw new Error(`Path not found: ${options.targetPath}`);
|
|
563
|
+
}
|
|
564
|
+
const targetStat = import_fs.default.statSync(resolvedTargetPath);
|
|
565
|
+
const isSingleFileTarget = targetStat.isFile();
|
|
566
|
+
const eslintCwd = isSingleFileTarget ? import_path.default.dirname(resolvedTargetPath) : resolvedTargetPath;
|
|
567
|
+
const startMs = Date.now();
|
|
568
|
+
const JS_TS_FILES = [
|
|
569
|
+
"**/*.js",
|
|
570
|
+
"**/*.jsx",
|
|
571
|
+
"**/*.ts",
|
|
572
|
+
"**/*.tsx",
|
|
573
|
+
"**/*.mts",
|
|
574
|
+
"**/*.cts",
|
|
575
|
+
"**/*.mjs",
|
|
576
|
+
"**/*.cjs"
|
|
577
|
+
];
|
|
578
|
+
let tsParser = null;
|
|
579
|
+
try {
|
|
580
|
+
try {
|
|
581
|
+
tsParser = require(import_path.default.join(
|
|
582
|
+
process.cwd(),
|
|
583
|
+
"node_modules",
|
|
584
|
+
"@typescript-eslint",
|
|
585
|
+
"parser"
|
|
586
|
+
));
|
|
587
|
+
} catch {
|
|
588
|
+
tsParser = require("@typescript-eslint/parser");
|
|
589
|
+
}
|
|
590
|
+
} catch {
|
|
591
|
+
}
|
|
592
|
+
const configBlocks = [
|
|
593
|
+
// JS/JSX files — default espree parser
|
|
594
|
+
{
|
|
595
|
+
files: ["**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"],
|
|
596
|
+
plugins: { "ai-guard": plugin },
|
|
597
|
+
languageOptions: {
|
|
598
|
+
parserOptions: {
|
|
599
|
+
ecmaVersion: "latest",
|
|
600
|
+
sourceType: "module",
|
|
601
|
+
ecmaFeatures: { jsx: true }
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
rules
|
|
605
|
+
},
|
|
606
|
+
// Ignore generated directories
|
|
607
|
+
{ ignores: DEFAULT_IGNORE_PATTERNS }
|
|
608
|
+
];
|
|
609
|
+
if (tsParser) {
|
|
610
|
+
configBlocks.splice(1, 0, {
|
|
611
|
+
files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
|
|
612
|
+
plugins: { "ai-guard": plugin },
|
|
613
|
+
languageOptions: {
|
|
614
|
+
parser: tsParser,
|
|
615
|
+
parserOptions: {
|
|
616
|
+
// project: true would be needed for type-aware rules — skip for speed
|
|
617
|
+
ecmaVersion: "latest",
|
|
618
|
+
sourceType: "module"
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
rules
|
|
622
|
+
});
|
|
623
|
+
} else {
|
|
624
|
+
configBlocks.splice(1, 0, {
|
|
625
|
+
files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
|
|
626
|
+
plugins: { "ai-guard": plugin },
|
|
627
|
+
rules
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
const eslint = new ESLint({
|
|
631
|
+
cwd: eslintCwd,
|
|
632
|
+
overrideConfigFile: true,
|
|
633
|
+
overrideConfig: configBlocks
|
|
634
|
+
});
|
|
635
|
+
const patterns = isSingleFileTarget ? [import_path.default.basename(resolvedTargetPath)] : JS_TS_FILES;
|
|
636
|
+
const perPatternResults = await Promise.all(
|
|
637
|
+
patterns.map(async (pattern) => {
|
|
638
|
+
try {
|
|
639
|
+
return await eslint.lintFiles([pattern]);
|
|
640
|
+
} catch (err) {
|
|
641
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
642
|
+
if (isSkippablePatternError(error)) {
|
|
643
|
+
log.debug(`Skipping pattern '${pattern}' (no lintable files)`);
|
|
644
|
+
return [];
|
|
645
|
+
}
|
|
646
|
+
throw error;
|
|
647
|
+
}
|
|
648
|
+
})
|
|
649
|
+
);
|
|
650
|
+
const rawResults = perPatternResults.flat();
|
|
651
|
+
const durationMs = Date.now() - startMs;
|
|
652
|
+
const files = [];
|
|
653
|
+
const ruleBreakdown = /* @__PURE__ */ new Map();
|
|
654
|
+
let totalErrors = 0;
|
|
655
|
+
let totalWarnings = 0;
|
|
656
|
+
for (const result of rawResults) {
|
|
657
|
+
if (result.messages.length === 0) continue;
|
|
658
|
+
const issues = result.messages.map((m) => ({
|
|
659
|
+
ruleId: m.ruleId ?? "unknown",
|
|
660
|
+
severity: m.severity,
|
|
661
|
+
message: m.message,
|
|
662
|
+
line: m.line,
|
|
663
|
+
column: m.column
|
|
664
|
+
}));
|
|
665
|
+
const errorCount = result.errorCount;
|
|
666
|
+
const warningCount = result.warningCount;
|
|
667
|
+
totalErrors += errorCount;
|
|
668
|
+
totalWarnings += warningCount;
|
|
669
|
+
for (const issue of issues) {
|
|
670
|
+
ruleBreakdown.set(
|
|
671
|
+
issue.ruleId,
|
|
672
|
+
(ruleBreakdown.get(issue.ruleId) ?? 0) + 1
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const relPath = import_path.default.relative(process.cwd(), result.filePath);
|
|
676
|
+
files.push({ filePath: relPath, issues, errorCount, warningCount });
|
|
677
|
+
}
|
|
678
|
+
const topFiles = [...files].sort((a, b) => b.issues.length - a.issues.length).slice(0, 10).map((f) => ({ path: f.filePath, count: f.issues.length }));
|
|
679
|
+
return {
|
|
680
|
+
files,
|
|
681
|
+
totalErrors,
|
|
682
|
+
totalWarnings,
|
|
683
|
+
totalIssues: totalErrors + totalWarnings,
|
|
684
|
+
ruleBreakdown,
|
|
685
|
+
topFiles,
|
|
686
|
+
durationMs
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// cli/commands/run.ts
|
|
691
|
+
function registerRunCommand(program2) {
|
|
692
|
+
program2.command("run").description("Run ai-guard rules on your project (zero ESLint config required)").option("--path <dir>", "Directory or file to scan", ".").option("--strict", "Use the strict rule preset (all rules at error)").option("--security", "Use the security-only rule preset").option("--json", "Output results as JSON").option(
|
|
693
|
+
"--max-warnings <n>",
|
|
694
|
+
"Fail if warnings exceed this count",
|
|
695
|
+
(value) => Number.parseInt(value, 10)
|
|
696
|
+
).action(async (opts) => {
|
|
697
|
+
if (opts.maxWarnings !== void 0 && (!Number.isInteger(opts.maxWarnings) || opts.maxWarnings < 0)) {
|
|
698
|
+
log.blank();
|
|
699
|
+
log.error("--max-warnings must be a non-negative integer.");
|
|
700
|
+
log.blank();
|
|
701
|
+
process.exit(1);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const preset = opts.strict ? "strict" : opts.security ? "security" : "recommended";
|
|
705
|
+
if (opts.strict && opts.security && !opts.json) {
|
|
706
|
+
log.warn("Both --strict and --security were provided. Using --strict.");
|
|
707
|
+
log.blank();
|
|
708
|
+
}
|
|
709
|
+
if (!opts.json) {
|
|
710
|
+
log.banner("AI GUARD RESULTS");
|
|
711
|
+
log.blank();
|
|
712
|
+
}
|
|
713
|
+
const spinner = opts.json ? null : (0, import_ora.default)({ text: "Scanning\u2026", color: "cyan" }).start();
|
|
714
|
+
let result;
|
|
715
|
+
try {
|
|
716
|
+
result = await runEslint({ preset, targetPath: opts.path });
|
|
717
|
+
spinner?.stop();
|
|
718
|
+
} catch (err) {
|
|
719
|
+
spinner?.stop();
|
|
720
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
721
|
+
if (msg.toLowerCase().includes("typescript") && msg.toLowerCase().includes("parser")) {
|
|
722
|
+
log.blank();
|
|
723
|
+
log.error("TypeScript detected but parser not found.");
|
|
724
|
+
log.blank();
|
|
725
|
+
log.print(` ${import_chalk2.default.bold("Install the TypeScript parser:")}`);
|
|
726
|
+
log.blank();
|
|
727
|
+
log.print(` ${import_chalk2.default.cyan("npm install --save-dev @typescript-eslint/parser")}`);
|
|
728
|
+
log.blank();
|
|
729
|
+
} else {
|
|
730
|
+
log.blank();
|
|
731
|
+
log.error(msg);
|
|
732
|
+
log.blank();
|
|
733
|
+
log.print(` ${import_chalk2.default.bold("Fix:")} Run ${import_chalk2.default.cyan("ai-guard doctor")} to diagnose your setup.`);
|
|
734
|
+
log.blank();
|
|
735
|
+
}
|
|
736
|
+
process.exit(1);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (opts.json) {
|
|
740
|
+
const jsonOutput = {
|
|
741
|
+
preset,
|
|
742
|
+
scannedPath: opts.path,
|
|
743
|
+
totalErrors: result.totalErrors,
|
|
744
|
+
totalWarnings: result.totalWarnings,
|
|
745
|
+
totalIssues: result.totalIssues,
|
|
746
|
+
durationMs: result.durationMs,
|
|
747
|
+
ruleBreakdown: Object.fromEntries(result.ruleBreakdown),
|
|
748
|
+
topFiles: result.topFiles,
|
|
749
|
+
files: result.files
|
|
750
|
+
};
|
|
751
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
752
|
+
process.exit(getRunExitCode(result, opts.maxWarnings));
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
log.success(`Scanned: ${import_chalk2.default.white(opts.path)}`);
|
|
756
|
+
log.success(`Duration: ${import_chalk2.default.white(result.durationMs + "ms")}`);
|
|
757
|
+
log.blank();
|
|
758
|
+
if (result.totalIssues === 0) {
|
|
759
|
+
log.print(
|
|
760
|
+
` ${import_chalk2.default.green("\u2714")} ${import_chalk2.default.bold.green("No AI issues found \u2014 your code looks clean")}`
|
|
761
|
+
);
|
|
762
|
+
log.blank();
|
|
763
|
+
process.exit(0);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
log.print(
|
|
767
|
+
` ${import_chalk2.default.bold("Total Issues:")} ${formatIssueCount(result.totalErrors, result.totalWarnings)}`
|
|
768
|
+
);
|
|
769
|
+
log.blank();
|
|
770
|
+
if (result.ruleBreakdown.size > 0) {
|
|
771
|
+
log.section("By Rule");
|
|
772
|
+
const sorted = [...result.ruleBreakdown.entries()].sort(
|
|
773
|
+
(a, b) => b[1] - a[1]
|
|
774
|
+
);
|
|
775
|
+
for (const [rule, count] of sorted) {
|
|
776
|
+
const shortRule = rule.replace(/^ai-guard\//, "");
|
|
777
|
+
log.print(
|
|
778
|
+
` ${import_chalk2.default.gray("\u2022")} ${import_chalk2.default.yellow(shortRule)}${import_chalk2.default.gray(":")} ${import_chalk2.default.white(String(count))}`
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
log.blank();
|
|
782
|
+
}
|
|
783
|
+
if (result.topFiles.length > 0) {
|
|
784
|
+
log.section("Top Files");
|
|
785
|
+
for (const { path: fp, count } of result.topFiles) {
|
|
786
|
+
log.print(
|
|
787
|
+
` ${import_chalk2.default.gray("\u2022")} ${import_chalk2.default.white(fp)} ${import_chalk2.default.gray(`(${count})`)}`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
log.blank();
|
|
791
|
+
}
|
|
792
|
+
log.section("Issues by File");
|
|
793
|
+
log.blank();
|
|
794
|
+
for (const file of result.files) {
|
|
795
|
+
log.print(
|
|
796
|
+
` ${import_chalk2.default.bold.white(file.filePath)} ` + import_chalk2.default.gray(
|
|
797
|
+
`(${file.errorCount} error${file.errorCount !== 1 ? "s" : ""}, ${file.warningCount} warning${file.warningCount !== 1 ? "s" : ""})`
|
|
798
|
+
)
|
|
799
|
+
);
|
|
800
|
+
for (const issue of file.issues) {
|
|
801
|
+
const sev = issue.severity === 2 ? import_chalk2.default.red("error") : import_chalk2.default.yellow(" warn");
|
|
802
|
+
const loc = import_chalk2.default.gray(`${String(issue.line)}:${String(issue.column)}`).padEnd(12);
|
|
803
|
+
const ruleShort = import_chalk2.default.gray(issue.ruleId.replace(/^ai-guard\//, ""));
|
|
804
|
+
log.print(
|
|
805
|
+
` ${loc} ${sev} ${import_chalk2.default.white(issue.message)} ${ruleShort}`
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
log.blank();
|
|
809
|
+
}
|
|
810
|
+
log.divider();
|
|
811
|
+
log.blank();
|
|
812
|
+
log.section("Next Steps");
|
|
813
|
+
if (!result.topFiles.length) {
|
|
814
|
+
log.info(`Run ${import_chalk2.default.cyan("ai-guard init")} to wire up ESLint for your editor`);
|
|
815
|
+
}
|
|
816
|
+
log.info(`Run ${import_chalk2.default.cyan("ai-guard baseline")} to save these issues and track only new ones`);
|
|
817
|
+
log.info(`Run ${import_chalk2.default.cyan("ai-guard ignore")} to suppress dist/build noise`);
|
|
818
|
+
log.blank();
|
|
819
|
+
process.exit(getRunExitCode(result, opts.maxWarnings));
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
function getRunExitCode(result, maxWarnings) {
|
|
823
|
+
if (result.totalErrors > 0) return 1;
|
|
824
|
+
if (maxWarnings !== void 0 && result.totalWarnings > maxWarnings) return 1;
|
|
825
|
+
return 0;
|
|
826
|
+
}
|
|
827
|
+
function formatIssueCount(errors, warnings) {
|
|
828
|
+
const parts = [];
|
|
829
|
+
if (errors > 0) parts.push(import_chalk2.default.red.bold(`${errors} error${errors !== 1 ? "s" : ""}`));
|
|
830
|
+
if (warnings > 0) parts.push(import_chalk2.default.yellow.bold(`${warnings} warning${warnings !== 1 ? "s" : ""}`));
|
|
831
|
+
return parts.join(import_chalk2.default.gray(" \xB7 "));
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// cli/commands/init.ts
|
|
835
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
836
|
+
var import_path4 = __toESM(require("path"));
|
|
837
|
+
var import_fs4 = __toESM(require("fs"));
|
|
838
|
+
|
|
839
|
+
// cli/utils/detector.ts
|
|
840
|
+
var import_fs2 = __toESM(require("fs"));
|
|
841
|
+
var import_path2 = __toESM(require("path"));
|
|
842
|
+
var import_module = require("module");
|
|
843
|
+
init_constants();
|
|
844
|
+
var FLAT_CONFIG_FILES = [
|
|
845
|
+
{ file: "eslint.config.js", type: "flat-js" },
|
|
846
|
+
{ file: "eslint.config.mjs", type: "flat-mjs" },
|
|
847
|
+
{ file: "eslint.config.cjs", type: "flat-cjs" }
|
|
848
|
+
];
|
|
849
|
+
var LEGACY_CONFIG_FILES = [
|
|
850
|
+
{ file: ".eslintrc.js", type: "eslintrc-js" },
|
|
851
|
+
{ file: ".eslintrc.cjs", type: "eslintrc-cjs" },
|
|
852
|
+
{ file: ".eslintrc.json", type: "eslintrc-json" },
|
|
853
|
+
{ file: ".eslintrc.yaml", type: "eslintrc-yaml" },
|
|
854
|
+
{ file: ".eslintrc.yml", type: "eslintrc-yaml" }
|
|
855
|
+
];
|
|
856
|
+
var ALL_CONFIG_FILES = [...FLAT_CONFIG_FILES, ...LEGACY_CONFIG_FILES];
|
|
857
|
+
function isPackageInstalled(pkgName, cwd = process.cwd()) {
|
|
858
|
+
try {
|
|
859
|
+
const pkgPath = import_path2.default.join(cwd, "node_modules", pkgName, "package.json");
|
|
860
|
+
if (import_fs2.default.existsSync(pkgPath)) {
|
|
861
|
+
return true;
|
|
862
|
+
}
|
|
863
|
+
const req = (0, import_module.createRequire)(import_path2.default.join(cwd, "package.json"));
|
|
864
|
+
req.resolve(`${pkgName}/package.json`);
|
|
865
|
+
return true;
|
|
866
|
+
} catch {
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function getPackageVersion(pkgName, cwd = process.cwd()) {
|
|
871
|
+
try {
|
|
872
|
+
const pkgPath = import_path2.default.join(cwd, "node_modules", pkgName, "package.json");
|
|
873
|
+
const resolvedPath = import_fs2.default.existsSync(pkgPath) ? pkgPath : (0, import_module.createRequire)(import_path2.default.join(cwd, "package.json")).resolve(`${pkgName}/package.json`);
|
|
874
|
+
const raw = import_fs2.default.readFileSync(resolvedPath, "utf-8");
|
|
875
|
+
const json = JSON.parse(raw);
|
|
876
|
+
return json.version ?? null;
|
|
877
|
+
} catch {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
function parseMajor(version) {
|
|
882
|
+
if (!version) return null;
|
|
883
|
+
const n = parseInt(version.split(".")[0], 10);
|
|
884
|
+
return isNaN(n) ? null : n;
|
|
885
|
+
}
|
|
886
|
+
function detectConfigType(cwd = process.cwd()) {
|
|
887
|
+
const found = [];
|
|
888
|
+
let first = null;
|
|
889
|
+
for (const { file, type } of ALL_CONFIG_FILES) {
|
|
890
|
+
const fullPath = import_path2.default.join(cwd, file);
|
|
891
|
+
if (import_fs2.default.existsSync(fullPath)) {
|
|
892
|
+
found.push(fullPath);
|
|
893
|
+
if (!first) first = { type, path: fullPath };
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return {
|
|
897
|
+
type: first?.type ?? "none",
|
|
898
|
+
path: first?.path ?? null,
|
|
899
|
+
allPaths: found
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
function isFlat(type) {
|
|
903
|
+
return type === "flat-js" || type === "flat-mjs" || type === "flat-cjs";
|
|
904
|
+
}
|
|
905
|
+
function isLegacy(type) {
|
|
906
|
+
return type === "eslintrc-js" || type === "eslintrc-cjs" || type === "eslintrc-json" || type === "eslintrc-yaml";
|
|
907
|
+
}
|
|
908
|
+
function requiresFlatConfig(eslintMajor) {
|
|
909
|
+
return eslintMajor !== null && eslintMajor >= 9;
|
|
910
|
+
}
|
|
911
|
+
function detectNukeIgnore(configPath) {
|
|
912
|
+
if (!configPath) return false;
|
|
913
|
+
try {
|
|
914
|
+
const content = import_fs2.default.readFileSync(configPath, "utf-8");
|
|
915
|
+
return content.includes('"**/*"') || content.includes("'**/*'");
|
|
916
|
+
} catch {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function detect(cwd = process.cwd()) {
|
|
921
|
+
const eslintVersion = getPackageVersion("eslint", cwd);
|
|
922
|
+
const eslintMajor = parseMajor(eslintVersion);
|
|
923
|
+
const pluginInstalled = PLUGIN_NAMES.some((name) => isPackageInstalled(name, cwd));
|
|
924
|
+
const { type: configType, path: configPath, allPaths } = detectConfigType(cwd);
|
|
925
|
+
const hasFlatConfig = allPaths.some(
|
|
926
|
+
(p) => FLAT_CONFIG_FILES.some((c) => p.endsWith(c.file))
|
|
927
|
+
);
|
|
928
|
+
const hasLegacyConfig = allPaths.some(
|
|
929
|
+
(p) => LEGACY_CONFIG_FILES.some((c) => p.endsWith(c.file))
|
|
930
|
+
);
|
|
931
|
+
const hasConflictingConfigs = hasFlatConfig && hasLegacyConfig;
|
|
932
|
+
const hasNukeIgnore = detectNukeIgnore(configPath);
|
|
933
|
+
return {
|
|
934
|
+
eslintVersion,
|
|
935
|
+
eslintMajor,
|
|
936
|
+
pluginInstalled,
|
|
937
|
+
configType,
|
|
938
|
+
configPath,
|
|
939
|
+
allConfigPaths: allPaths,
|
|
940
|
+
hasConflictingConfigs,
|
|
941
|
+
hasNukeIgnore
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// cli/commands/init.ts
|
|
946
|
+
init_config_manager();
|
|
947
|
+
function classifyVerifyError(err) {
|
|
948
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
949
|
+
const clean = msg.replace(/\s+at\s+.+/gm, "").trim();
|
|
950
|
+
const modNotFound = clean.match(
|
|
951
|
+
/Cannot find module '([^']+)'|Cannot find package '([^']+)'/
|
|
952
|
+
);
|
|
953
|
+
if (modNotFound) {
|
|
954
|
+
const pkg = modNotFound[1] ?? modNotFound[2] ?? "unknown";
|
|
955
|
+
return { kind: "module-not-found", pkg, message: clean };
|
|
956
|
+
}
|
|
957
|
+
if (clean.includes("SyntaxError") || clean.includes("Unexpected token") || clean.includes("Invalid or unexpected token")) {
|
|
958
|
+
return { kind: "syntax", message: clean };
|
|
959
|
+
}
|
|
960
|
+
return { kind: "unknown", message: clean };
|
|
961
|
+
}
|
|
962
|
+
async function verifyConfigLoads(configPath, cwd) {
|
|
963
|
+
try {
|
|
964
|
+
const { ESLint } = await import("eslint");
|
|
965
|
+
const eslint = new ESLint({
|
|
966
|
+
cwd,
|
|
967
|
+
overrideConfigFile: configPath
|
|
968
|
+
});
|
|
969
|
+
await eslint.lintText("", {
|
|
970
|
+
filePath: import_path4.default.join(cwd, "_ai_guard_probe_.js")
|
|
971
|
+
});
|
|
972
|
+
return null;
|
|
973
|
+
} catch (err) {
|
|
974
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
975
|
+
if (msg.includes("No files matching") || msg.includes("_ai_guard_probe_")) {
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
return classifyVerifyError(err);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function registerInitCommand(program2) {
|
|
982
|
+
program2.command("init").description("Configure @undercurrentai/eslint-plugin-ai-guard in this project").option("--preset <name>", "Preset to use: recommended | strict | security", "recommended").option("--flat", "Force flat config format (eslint.config.mjs) regardless of ESLint version").option("--dry-run", "Preview what would change without writing any files").action(async (opts) => {
|
|
983
|
+
const preset = opts.preset ?? "recommended";
|
|
984
|
+
const cwd = process.cwd();
|
|
985
|
+
const isDryRun = opts.dryRun === true;
|
|
986
|
+
log.banner("AI GUARD INIT");
|
|
987
|
+
if (isDryRun) {
|
|
988
|
+
log.print(` ${import_chalk3.default.yellow("\u2691 DRY RUN \u2014 no files will be written")}`);
|
|
989
|
+
log.blank();
|
|
990
|
+
}
|
|
991
|
+
log.info("Detecting your project setup\u2026");
|
|
992
|
+
log.blank();
|
|
993
|
+
const env = detect(cwd);
|
|
994
|
+
log.section("Environment");
|
|
995
|
+
if (env.eslintVersion) {
|
|
996
|
+
log.success(`ESLint ${env.eslintVersion} detected (v${env.eslintMajor})`);
|
|
997
|
+
} else {
|
|
998
|
+
log.warn("ESLint not found in node_modules");
|
|
999
|
+
}
|
|
1000
|
+
if (env.pluginInstalled) {
|
|
1001
|
+
log.success("@undercurrentai/eslint-plugin-ai-guard is installed");
|
|
1002
|
+
} else {
|
|
1003
|
+
log.warn("@undercurrentai/eslint-plugin-ai-guard not found");
|
|
1004
|
+
}
|
|
1005
|
+
if (env.configType !== "none") {
|
|
1006
|
+
log.success(`ESLint config found: ${import_chalk3.default.white(import_path4.default.relative(cwd, env.configPath ?? env.configType))}`);
|
|
1007
|
+
} else {
|
|
1008
|
+
log.info("No ESLint config found \u2014 will generate one");
|
|
1009
|
+
}
|
|
1010
|
+
const eslintNeedsFlat = requiresFlatConfig(env.eslintMajor);
|
|
1011
|
+
if (env.hasConflictingConfigs && !eslintNeedsFlat) {
|
|
1012
|
+
log.blank();
|
|
1013
|
+
log.warn("Conflicting configs detected:");
|
|
1014
|
+
for (const p of env.allConfigPaths) {
|
|
1015
|
+
log.print(` ${import_chalk3.default.yellow("\u2192")} ${import_chalk3.default.white(import_path4.default.relative(cwd, p))}`);
|
|
1016
|
+
}
|
|
1017
|
+
log.print(` ${import_chalk3.default.gray("Multiple config formats found \u2014 this can cause unpredictable behavior.")}`);
|
|
1018
|
+
}
|
|
1019
|
+
log.blank();
|
|
1020
|
+
const toInstall = [];
|
|
1021
|
+
if (!env.eslintVersion) toInstall.push("eslint");
|
|
1022
|
+
if (!env.pluginInstalled) toInstall.push("@undercurrentai/eslint-plugin-ai-guard@next");
|
|
1023
|
+
if (toInstall.length > 0) {
|
|
1024
|
+
log.section("Missing Dependencies");
|
|
1025
|
+
log.warn("The following packages are not installed:");
|
|
1026
|
+
log.blank();
|
|
1027
|
+
for (const pkg of toInstall) {
|
|
1028
|
+
log.print(` ${import_chalk3.default.yellow("\u2192")} ${import_chalk3.default.white(pkg)}`);
|
|
1029
|
+
}
|
|
1030
|
+
log.blank();
|
|
1031
|
+
log.print(` ${import_chalk3.default.bold("Run this first:")}`);
|
|
1032
|
+
log.blank();
|
|
1033
|
+
log.print(` ${import_chalk3.default.cyan(`npm install --save-dev ${toInstall.join(" ")}`)}`);
|
|
1034
|
+
log.blank();
|
|
1035
|
+
log.print(` Then re-run ${import_chalk3.default.cyan("ai-guard init")} to complete setup.`);
|
|
1036
|
+
log.blank();
|
|
1037
|
+
process.exit(1);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
try {
|
|
1041
|
+
const { createRequire: createRequire2 } = await import("module");
|
|
1042
|
+
const { pathToFileURL: pathToFileURL2 } = await import("url");
|
|
1043
|
+
const req = createRequire2(import_path4.default.join(cwd, "_probe.js"));
|
|
1044
|
+
const resolvedPath = req.resolve("@undercurrentai/eslint-plugin-ai-guard");
|
|
1045
|
+
const aiGuard = await import(pathToFileURL2(resolvedPath).href);
|
|
1046
|
+
const plugin = aiGuard.default || aiGuard;
|
|
1047
|
+
const hasPresetRules = !!plugin.configs && !!plugin.configs[preset] && !!plugin.configs[preset].rules;
|
|
1048
|
+
if (!hasPresetRules) {
|
|
1049
|
+
log.debug("Plugin precheck could not verify configs shape; continuing to validation step.");
|
|
1050
|
+
}
|
|
1051
|
+
} catch (e) {
|
|
1052
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
1053
|
+
log.debug(`Skipped plugin precheck: ${reason}`);
|
|
1054
|
+
}
|
|
1055
|
+
const forceFlat = opts.flat === true;
|
|
1056
|
+
const existingIsFlat = isFlat(env.configType);
|
|
1057
|
+
const existingIsLegacy = isLegacy(env.configType);
|
|
1058
|
+
const useFlat = forceFlat || eslintNeedsFlat || existingIsFlat || env.configType === "none";
|
|
1059
|
+
if (eslintNeedsFlat && existingIsLegacy) {
|
|
1060
|
+
log.section("Config Migration Required");
|
|
1061
|
+
log.warn(`ESLint v${env.eslintMajor} requires flat config, but a legacy config was found:`);
|
|
1062
|
+
log.print(` ${import_chalk3.default.white(import_path4.default.relative(cwd, env.configPath))}`);
|
|
1063
|
+
log.blank();
|
|
1064
|
+
log.print(` ${import_chalk3.default.bold("ai-guard will create a new flat config.")}`);
|
|
1065
|
+
log.blank();
|
|
1066
|
+
}
|
|
1067
|
+
log.section(`Configuring ESLint${isDryRun ? " (dry run)" : ""}`);
|
|
1068
|
+
let finalConfigPath;
|
|
1069
|
+
let configWritten = false;
|
|
1070
|
+
if (env.configPath && env.hasNukeIgnore) {
|
|
1071
|
+
const raw = readConfig(env.configPath);
|
|
1072
|
+
const { content: fixed, changed } = removeNukeIgnore(raw);
|
|
1073
|
+
if (changed) {
|
|
1074
|
+
log.warn(`Removed "**/*" ignore pattern from ${import_path4.default.relative(cwd, env.configPath)}`);
|
|
1075
|
+
if (!isDryRun) {
|
|
1076
|
+
backupConfig(env.configPath);
|
|
1077
|
+
writeConfig(env.configPath, fixed);
|
|
1078
|
+
log.success("Ignore pattern fixed");
|
|
1079
|
+
} else {
|
|
1080
|
+
log.print(` ${import_chalk3.default.gray("[dry-run] would remove nuke-ignore from existing config")}`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
if (env.configType === "none" || eslintNeedsFlat && existingIsLegacy) {
|
|
1085
|
+
const targetName = useFlat ? "eslint.config.mjs" : ".eslintrc.js";
|
|
1086
|
+
finalConfigPath = import_path4.default.join(cwd, targetName);
|
|
1087
|
+
const content2 = useFlat ? generateFlatConfig(preset) : generateLegacyConfig(preset);
|
|
1088
|
+
if (isDryRun) {
|
|
1089
|
+
log.print(` ${import_chalk3.default.gray(`[dry-run] would create: ${targetName}`)}`);
|
|
1090
|
+
log.blank();
|
|
1091
|
+
log.print(import_chalk3.default.gray("\u2500\u2500\u2500 Preview \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1092
|
+
log.blank();
|
|
1093
|
+
for (const line of content2.split("\n")) {
|
|
1094
|
+
log.print(import_chalk3.default.gray(` ${line}`));
|
|
1095
|
+
}
|
|
1096
|
+
log.blank();
|
|
1097
|
+
} else {
|
|
1098
|
+
writeConfig(finalConfigPath, content2);
|
|
1099
|
+
log.success(`Created ${import_chalk3.default.white(targetName)}`);
|
|
1100
|
+
log.info(` Preset: ${import_chalk3.default.cyan(preset)}`);
|
|
1101
|
+
log.info(` Format: ${import_chalk3.default.cyan(useFlat ? "ESLint v9 flat config (eslint.config.mjs)" : "ESLint v8 legacy config (.eslintrc.js)")}`);
|
|
1102
|
+
configWritten = true;
|
|
1103
|
+
}
|
|
1104
|
+
} else if (useFlat && existingIsFlat) {
|
|
1105
|
+
const { isInvalidAiGuardConfig: isInvalidAiGuardConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
|
|
1106
|
+
const existing = readConfig(env.configPath);
|
|
1107
|
+
const isInvalid = isInvalidAiGuardConfig2(existing);
|
|
1108
|
+
const patched = patchFlatConfig(existing, preset);
|
|
1109
|
+
if (patched === existing && !isInvalid) {
|
|
1110
|
+
log.warn("ai-guard is already present in your config \u2014 no changes made");
|
|
1111
|
+
finalConfigPath = env.configPath;
|
|
1112
|
+
} else if (isDryRun) {
|
|
1113
|
+
finalConfigPath = env.configPath;
|
|
1114
|
+
log.print(` ${import_chalk3.default.gray(`[dry-run] would patch: ${import_path4.default.relative(cwd, env.configPath)}`)}`);
|
|
1115
|
+
} else {
|
|
1116
|
+
const bak = backupConfig(env.configPath);
|
|
1117
|
+
log.info(`Backed up \u2192 ${import_chalk3.default.gray(import_path4.default.relative(cwd, bak))}`);
|
|
1118
|
+
writeConfig(env.configPath, patched);
|
|
1119
|
+
if (isInvalid) {
|
|
1120
|
+
log.success(`Detected invalid ai-guard config \u2014 repairing ${import_chalk3.default.white(import_path4.default.relative(cwd, env.configPath))}`);
|
|
1121
|
+
} else {
|
|
1122
|
+
log.success(`Patched ${import_chalk3.default.white(import_path4.default.relative(cwd, env.configPath))}`);
|
|
1123
|
+
}
|
|
1124
|
+
finalConfigPath = env.configPath;
|
|
1125
|
+
configWritten = true;
|
|
1126
|
+
}
|
|
1127
|
+
} else {
|
|
1128
|
+
const existing = readConfig(env.configPath);
|
|
1129
|
+
const patched = patchLegacyConfig(existing, preset);
|
|
1130
|
+
if (patched === existing) {
|
|
1131
|
+
log.warn("ai-guard is already present in your config \u2014 no changes made");
|
|
1132
|
+
finalConfigPath = env.configPath;
|
|
1133
|
+
} else if (isDryRun) {
|
|
1134
|
+
finalConfigPath = env.configPath;
|
|
1135
|
+
log.print(` ${import_chalk3.default.gray(`[dry-run] would patch: ${import_path4.default.relative(cwd, env.configPath)}`)}`);
|
|
1136
|
+
} else {
|
|
1137
|
+
const bak = backupConfig(env.configPath);
|
|
1138
|
+
log.info(`Backed up \u2192 ${import_chalk3.default.gray(import_path4.default.relative(cwd, bak))}`);
|
|
1139
|
+
writeConfig(env.configPath, patched);
|
|
1140
|
+
log.success(`Patched ${import_chalk3.default.white(import_path4.default.relative(cwd, env.configPath))}`);
|
|
1141
|
+
finalConfigPath = env.configPath;
|
|
1142
|
+
configWritten = true;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (eslintNeedsFlat) {
|
|
1146
|
+
const legacyConfigs = env.allConfigPaths.filter((p) => !p.includes("eslint.config."));
|
|
1147
|
+
let cleanedUp = false;
|
|
1148
|
+
for (const p of legacyConfigs) {
|
|
1149
|
+
if (import_fs4.default.existsSync(p)) {
|
|
1150
|
+
if (!isDryRun) {
|
|
1151
|
+
try {
|
|
1152
|
+
import_fs4.default.renameSync(p, `${p}.bak`);
|
|
1153
|
+
cleanedUp = true;
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1156
|
+
log.warn(`Could not back up ${import_path4.default.relative(cwd, p)}: ${reason}`);
|
|
1157
|
+
}
|
|
1158
|
+
} else {
|
|
1159
|
+
cleanedUp = true;
|
|
1160
|
+
log.print(` ${import_chalk3.default.gray(`[dry-run] would rename ${import_path4.default.relative(cwd, p)} to ${import_path4.default.basename(p)}.bak`)}`);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
if (cleanedUp && !isDryRun) {
|
|
1165
|
+
log.success("Legacy config backed up to avoid ESLint conflict");
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (isDryRun) {
|
|
1169
|
+
log.blank();
|
|
1170
|
+
log.print(` ${import_chalk3.default.yellow("Dry run complete \u2014 no files written.")}`);
|
|
1171
|
+
log.print(` Re-run without ${import_chalk3.default.cyan("--dry-run")} to apply.`);
|
|
1172
|
+
log.blank();
|
|
1173
|
+
process.exit(0);
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
log.blank();
|
|
1177
|
+
log.section("Validation");
|
|
1178
|
+
if (!import_fs4.default.existsSync(finalConfigPath)) {
|
|
1179
|
+
log.error(`Config file not found after write: ${import_path4.default.relative(cwd, finalConfigPath)}`);
|
|
1180
|
+
log.blank();
|
|
1181
|
+
log.print(` ${import_chalk3.default.bold("Problem:")} File write may have failed.`);
|
|
1182
|
+
log.print(` ${import_chalk3.default.bold("Fix:")} Check disk space and file permissions, then re-run.`);
|
|
1183
|
+
log.blank();
|
|
1184
|
+
process.exit(1);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const content = readConfig(finalConfigPath);
|
|
1188
|
+
const validationErrors = useFlat ? validateFlatConfigText(content) : validateLegacyConfigText(content);
|
|
1189
|
+
if (validationErrors.length > 0) {
|
|
1190
|
+
log.warn("Config validation found issues:");
|
|
1191
|
+
for (const e of validationErrors) {
|
|
1192
|
+
log.print(` ${import_chalk3.default.red("\u2716")} ${e}`);
|
|
1193
|
+
}
|
|
1194
|
+
} else {
|
|
1195
|
+
log.success(`Config structure valid: ${import_chalk3.default.white(import_path4.default.relative(cwd, finalConfigPath))}`);
|
|
1196
|
+
}
|
|
1197
|
+
log.info("Verifying ESLint can load the config\u2026");
|
|
1198
|
+
const loadError = await verifyConfigLoads(finalConfigPath, cwd);
|
|
1199
|
+
if (loadError !== null) {
|
|
1200
|
+
log.blank();
|
|
1201
|
+
if (loadError.kind === "module-not-found") {
|
|
1202
|
+
log.warn(`Could not verify: module not found: ${import_chalk3.default.yellow(loadError.pkg)}`);
|
|
1203
|
+
log.blank();
|
|
1204
|
+
log.print(` ${import_chalk3.default.bold("Install the missing package:")}`);
|
|
1205
|
+
log.print(` ${import_chalk3.default.cyan(`npm install --save-dev ${loadError.pkg}`)}`);
|
|
1206
|
+
log.print(` Then run ${import_chalk3.default.cyan("ai-guard doctor")} to confirm.`);
|
|
1207
|
+
log.blank();
|
|
1208
|
+
} else {
|
|
1209
|
+
log.error("ESLint failed to load the generated config.");
|
|
1210
|
+
log.blank();
|
|
1211
|
+
log.print(` ${import_chalk3.default.bold("Problem:")}`);
|
|
1212
|
+
log.print(` ${import_chalk3.default.red(loadError.message)}`);
|
|
1213
|
+
if (loadError.kind === "unknown") {
|
|
1214
|
+
log.blank();
|
|
1215
|
+
log.print(` ${import_chalk3.default.yellow("Generated config may be using incorrect plugin structure.")}`);
|
|
1216
|
+
}
|
|
1217
|
+
log.blank();
|
|
1218
|
+
log.print(` ${import_chalk3.default.bold("Fix:")}`);
|
|
1219
|
+
log.print(` 1. Check the syntax in ${import_chalk3.default.cyan(import_path4.default.relative(cwd, finalConfigPath))}`);
|
|
1220
|
+
log.print(` 2. Make sure ${import_chalk3.default.cyan("@undercurrentai/eslint-plugin-ai-guard")} is installed`);
|
|
1221
|
+
log.print(` 3. Run ${import_chalk3.default.cyan("ai-guard doctor")} for full diagnostics`);
|
|
1222
|
+
log.blank();
|
|
1223
|
+
process.exit(1);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
} else {
|
|
1227
|
+
log.success("ESLint loaded config successfully");
|
|
1228
|
+
}
|
|
1229
|
+
log.blank();
|
|
1230
|
+
log.section("Setup Complete");
|
|
1231
|
+
log.blank();
|
|
1232
|
+
let cleanedUpInfo = false;
|
|
1233
|
+
if (eslintNeedsFlat) {
|
|
1234
|
+
const legacyConfigs = env.allConfigPaths.filter((p) => !p.includes("eslint.config."));
|
|
1235
|
+
cleanedUpInfo = legacyConfigs.some((p) => import_fs4.default.existsSync(`${p}.bak`) && !import_fs4.default.existsSync(p));
|
|
1236
|
+
}
|
|
1237
|
+
const lingeringConflicts = env.hasConflictingConfigs && !eslintNeedsFlat && !cleanedUpInfo;
|
|
1238
|
+
if (loadError === null && !lingeringConflicts) {
|
|
1239
|
+
log.print(` ${import_chalk3.default.green("\u2714")} ${import_chalk3.default.bold.green("Configuration validated successfully")}`);
|
|
1240
|
+
}
|
|
1241
|
+
log.print(` ${import_chalk3.default.green("\u2714")} ${import_chalk3.default.bold("ESLint config:")} ${import_chalk3.default.cyan(import_path4.default.relative(cwd, finalConfigPath))}`);
|
|
1242
|
+
log.print(` ${import_chalk3.default.green("\u2714")} ${import_chalk3.default.bold("Mode:")} ${import_chalk3.default.cyan(useFlat ? `Flat config (ESLint v${env.eslintMajor ?? 9})` : `Legacy config (ESLint v${env.eslintMajor ?? 8})`)}`);
|
|
1243
|
+
log.print(` ${import_chalk3.default.green("\u2714")} ${import_chalk3.default.bold("Preset:")} ${import_chalk3.default.cyan(preset)}`);
|
|
1244
|
+
log.blank();
|
|
1245
|
+
log.info(`Run ${import_chalk3.default.cyan("npx eslint .")} \u2192 lint with your editor integration`);
|
|
1246
|
+
log.info(`Run ${import_chalk3.default.cyan("ai-guard run")} \u2192 zero-config scan`);
|
|
1247
|
+
log.info(`Run ${import_chalk3.default.cyan("ai-guard doctor")} \u2192 verify the full setup`);
|
|
1248
|
+
log.info(`Run ${import_chalk3.default.cyan("ai-guard baseline")} \u2192 save baseline, track only new issues`);
|
|
1249
|
+
log.blank();
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// cli/commands/doctor.ts
|
|
1254
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
1255
|
+
var import_path5 = __toESM(require("path"));
|
|
1256
|
+
init_config_manager();
|
|
1257
|
+
async function probeConfigLoad(configPath, cwd) {
|
|
1258
|
+
try {
|
|
1259
|
+
const { ESLint } = await import("eslint");
|
|
1260
|
+
const eslint = new ESLint({ cwd, overrideConfigFile: configPath });
|
|
1261
|
+
await eslint.lintText("", { filePath: import_path5.default.join(cwd, "_probe_.js") });
|
|
1262
|
+
return { ok: true, error: null };
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1265
|
+
if (msg.includes("No files") || msg.includes("no files") || msg.includes("_probe_")) {
|
|
1266
|
+
return { ok: true, error: null };
|
|
1267
|
+
}
|
|
1268
|
+
const clean = msg.replace(/\s+at\s+.+/gm, "").trim();
|
|
1269
|
+
return { ok: false, error: clean };
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
function registerDoctorCommand(program2) {
|
|
1273
|
+
program2.command("doctor").description("Diagnose your ai-guard setup and print actionable fixes").action(async () => {
|
|
1274
|
+
const cwd = process.cwd();
|
|
1275
|
+
log.banner("AI GUARD DOCTOR");
|
|
1276
|
+
log.info("Checking your environment\u2026");
|
|
1277
|
+
log.blank();
|
|
1278
|
+
const env = detect(cwd);
|
|
1279
|
+
const checks = [];
|
|
1280
|
+
checks.push({
|
|
1281
|
+
label: "ESLint installed",
|
|
1282
|
+
pass: env.eslintVersion !== null,
|
|
1283
|
+
detail: env.eslintVersion ? `ESLint ${env.eslintVersion}` : "Not found in node_modules",
|
|
1284
|
+
fix: env.eslintVersion ? void 0 : "npm install --save-dev eslint"
|
|
1285
|
+
});
|
|
1286
|
+
checks.push({
|
|
1287
|
+
label: "ai-guard plugin installed",
|
|
1288
|
+
pass: env.pluginInstalled,
|
|
1289
|
+
detail: env.pluginInstalled ? "Found in node_modules (scoped or legacy package)" : "Not found in node_modules",
|
|
1290
|
+
fix: env.pluginInstalled ? void 0 : "npm install --save-dev @undercurrentai/eslint-plugin-ai-guard@next"
|
|
1291
|
+
});
|
|
1292
|
+
const hasConfig = env.configType !== "none";
|
|
1293
|
+
checks.push({
|
|
1294
|
+
label: "ESLint config present",
|
|
1295
|
+
pass: hasConfig,
|
|
1296
|
+
detail: hasConfig ? `${import_path5.default.relative(cwd, env.configPath ?? "")} (${env.configType})` : "No eslint.config.* or .eslintrc.* found",
|
|
1297
|
+
fix: hasConfig ? void 0 : "ai-guard init",
|
|
1298
|
+
note: hasConfig ? void 0 : "This will generate the correct config format for your ESLint version"
|
|
1299
|
+
});
|
|
1300
|
+
if (env.eslintMajor !== null && hasConfig) {
|
|
1301
|
+
const needsFlat = requiresFlatConfig(env.eslintMajor);
|
|
1302
|
+
const actuallyFlat = isFlat(env.configType);
|
|
1303
|
+
const actuallyLegacy = isLegacy(env.configType);
|
|
1304
|
+
const aligned = needsFlat && actuallyFlat || !needsFlat && actuallyLegacy;
|
|
1305
|
+
checks.push({
|
|
1306
|
+
label: "Config format matches ESLint version",
|
|
1307
|
+
pass: aligned,
|
|
1308
|
+
detail: aligned ? `${actuallyFlat ? "Flat config" : "Legacy config"} \u2714 matches ESLint v${env.eslintMajor}` : `ESLint v${env.eslintMajor} requires ${needsFlat ? "flat config (eslint.config.*)" : "legacy config (.eslintrc.*)"} but found ${actuallyFlat ? "flat" : "legacy"} config`,
|
|
1309
|
+
fix: aligned ? void 0 : needsFlat ? "ai-guard init (will create eslint.config.mjs)" : "npm install --save-dev eslint@8 (or upgrade to v9 and run ai-guard init)",
|
|
1310
|
+
note: aligned ? void 0 : needsFlat ? "ESLint v9 ignores .eslintrc.* files. The flat config must be used." : void 0
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
if (hasConfig && env.configPath) {
|
|
1314
|
+
let pluginWired = false;
|
|
1315
|
+
let isInvalidConfig = false;
|
|
1316
|
+
let readError = null;
|
|
1317
|
+
try {
|
|
1318
|
+
const { isInvalidAiGuardConfig: isInvalidAiGuardConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
|
|
1319
|
+
const content = readConfig(env.configPath);
|
|
1320
|
+
pluginWired = content.includes("ai-guard") || content.includes("@undercurrentai/eslint-plugin-ai-guard");
|
|
1321
|
+
isInvalidConfig = isInvalidAiGuardConfig2(content);
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
readError = err instanceof Error ? err.message : String(err);
|
|
1324
|
+
}
|
|
1325
|
+
checks.push({
|
|
1326
|
+
label: "Plugin wired in config",
|
|
1327
|
+
pass: pluginWired && !isInvalidConfig,
|
|
1328
|
+
detail: readError ? `Could not read config: ${readError}` : isInvalidConfig ? "Invalid ai-guard config detected (wrong export usage)" : pluginWired ? "@undercurrentai/eslint-plugin-ai-guard referenced in config" : `Config exists but ai-guard plugin not found in ${import_path5.default.relative(cwd, env.configPath)}`,
|
|
1329
|
+
fix: isInvalidConfig || !pluginWired && !readError ? "ai-guard init (will patch/repair your existing config)" : void 0
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
checks.push({
|
|
1333
|
+
label: "No conflicting config files",
|
|
1334
|
+
pass: !env.hasConflictingConfigs,
|
|
1335
|
+
detail: env.hasConflictingConfigs ? `Conflicting config detected:
|
|
1336
|
+
${env.allConfigPaths.map((p) => ` ${import_path5.default.relative(cwd, p)}`).join("\n")}` : "Only one config format detected",
|
|
1337
|
+
fix: env.hasConflictingConfigs ? "Remove or backup legacy config" : void 0,
|
|
1338
|
+
note: env.hasConflictingConfigs ? "ESLint v9 silently ignores .eslintrc.* when a flat config exists. This can cause confusion." : void 0
|
|
1339
|
+
});
|
|
1340
|
+
if (hasConfig && env.hasNukeIgnore) {
|
|
1341
|
+
checks.push({
|
|
1342
|
+
label: 'No "ignore everything" pattern',
|
|
1343
|
+
pass: false,
|
|
1344
|
+
detail: `Config contains "**/*" ignore pattern \u2014 this silently prevents all files from being linted`,
|
|
1345
|
+
fix: "ai-guard init (will remove the pattern and replace with safe ignores)"
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
const versionOk = env.eslintMajor !== null && env.eslintMajor >= 8;
|
|
1349
|
+
checks.push({
|
|
1350
|
+
label: "ESLint version compatible",
|
|
1351
|
+
pass: versionOk,
|
|
1352
|
+
detail: versionOk ? `v${env.eslintMajor} \u2714 (requires \u2265 v8)` : env.eslintMajor !== null ? `v${env.eslintMajor} is too old \u2014 requires ESLint \u2265 8` : "ESLint not installed",
|
|
1353
|
+
fix: versionOk ? void 0 : "npm install --save-dev eslint@latest"
|
|
1354
|
+
});
|
|
1355
|
+
const configChecksPass = checks.every((c) => c.pass);
|
|
1356
|
+
if (hasConfig && env.configPath && env.pluginInstalled && configChecksPass) {
|
|
1357
|
+
log.info("Probing ESLint config load\u2026");
|
|
1358
|
+
const probe = await probeConfigLoad(env.configPath, cwd);
|
|
1359
|
+
checks.push({
|
|
1360
|
+
label: "ESLint config loads without errors",
|
|
1361
|
+
pass: probe.ok,
|
|
1362
|
+
detail: probe.ok ? "ESLint loaded config successfully" : probe.error ?? "Unknown error",
|
|
1363
|
+
fix: probe.ok ? void 0 : `Check syntax in ${import_path5.default.relative(cwd, env.configPath)}`,
|
|
1364
|
+
note: probe.ok ? void 0 : "Run: ai-guard init --dry-run to preview a fresh config"
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
log.section("Diagnostics");
|
|
1368
|
+
let allPassed = true;
|
|
1369
|
+
for (const check of checks) {
|
|
1370
|
+
const icon = check.pass ? import_chalk4.default.green("\u2714") : import_chalk4.default.red("\u2716");
|
|
1371
|
+
const label = check.pass ? import_chalk4.default.white(check.label) : import_chalk4.default.red.bold(check.label);
|
|
1372
|
+
log.print(` ${icon} ${label}`);
|
|
1373
|
+
log.print(` ${import_chalk4.default.gray(check.detail)}`);
|
|
1374
|
+
if (!check.pass) {
|
|
1375
|
+
allPassed = false;
|
|
1376
|
+
if (check.fix) {
|
|
1377
|
+
log.print(` ${import_chalk4.default.cyan("\u2192 Fix:")} ${import_chalk4.default.yellow(check.fix)}`);
|
|
1378
|
+
}
|
|
1379
|
+
if (check.note) {
|
|
1380
|
+
log.print(` ${import_chalk4.default.gray(check.note)}`);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
log.blank();
|
|
1384
|
+
}
|
|
1385
|
+
log.divider();
|
|
1386
|
+
log.blank();
|
|
1387
|
+
if (allPassed) {
|
|
1388
|
+
log.print(` ${import_chalk4.default.green("\u2714")} ${import_chalk4.default.bold.green("All checks passed! Your setup is correct.")}`);
|
|
1389
|
+
log.blank();
|
|
1390
|
+
log.info(`Run ${import_chalk4.default.cyan("ai-guard run")} to start scanning.`);
|
|
1391
|
+
log.info(`Run ${import_chalk4.default.cyan("npx eslint .")} to lint with ESLint directly.`);
|
|
1392
|
+
} else {
|
|
1393
|
+
log.error("Some checks failed. Follow the fixes above.");
|
|
1394
|
+
log.blank();
|
|
1395
|
+
const configFormatMismatch = checks.some(
|
|
1396
|
+
(c) => c.label === "Config format matches ESLint version" && !c.pass
|
|
1397
|
+
);
|
|
1398
|
+
const configMissing = checks.some(
|
|
1399
|
+
(c) => c.label === "ESLint config present" && !c.pass
|
|
1400
|
+
);
|
|
1401
|
+
const pluginMissing = checks.some(
|
|
1402
|
+
(c) => c.label === "ai-guard plugin installed" && !c.pass
|
|
1403
|
+
);
|
|
1404
|
+
if (pluginMissing) {
|
|
1405
|
+
log.print(
|
|
1406
|
+
` Install missing packages first, then re-run ${import_chalk4.default.cyan("ai-guard doctor")}.`
|
|
1407
|
+
);
|
|
1408
|
+
} else if (configMissing || configFormatMismatch) {
|
|
1409
|
+
log.print(
|
|
1410
|
+
` Run ${import_chalk4.default.cyan("ai-guard init")} to generate the correct config for your ESLint version.`
|
|
1411
|
+
);
|
|
1412
|
+
} else {
|
|
1413
|
+
log.print(
|
|
1414
|
+
` Run ${import_chalk4.default.cyan("ai-guard init --dry-run")} to preview what would change.`
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
log.blank();
|
|
1419
|
+
process.exit(allPassed ? 0 : 1);
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// cli/commands/preset.ts
|
|
1424
|
+
var import_prompts = require("@inquirer/prompts");
|
|
1425
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
1426
|
+
init_config_manager();
|
|
1427
|
+
var import_path6 = __toESM(require("path"));
|
|
1428
|
+
function registerPresetCommand(program2) {
|
|
1429
|
+
program2.command("preset").description("Interactively select and apply an ai-guard preset to your ESLint config").action(async () => {
|
|
1430
|
+
const cwd = process.cwd();
|
|
1431
|
+
log.banner("AI GUARD PRESET");
|
|
1432
|
+
log.blank();
|
|
1433
|
+
const preset = await (0, import_prompts.select)({
|
|
1434
|
+
message: "Choose a preset:",
|
|
1435
|
+
choices: [
|
|
1436
|
+
{
|
|
1437
|
+
name: `${import_chalk5.default.green("recommended")} \u2014 balanced defaults, low noise. Best starting point.`,
|
|
1438
|
+
value: "recommended"
|
|
1439
|
+
},
|
|
1440
|
+
{
|
|
1441
|
+
name: `${import_chalk5.default.yellow("strict")} \u2014 all rules at error. For mature codebases.`,
|
|
1442
|
+
value: "strict"
|
|
1443
|
+
},
|
|
1444
|
+
{
|
|
1445
|
+
name: `${import_chalk5.default.red("security")} \u2014 security rules only. For AppSec teams.`,
|
|
1446
|
+
value: "security"
|
|
1447
|
+
}
|
|
1448
|
+
]
|
|
1449
|
+
});
|
|
1450
|
+
log.blank();
|
|
1451
|
+
log.info(`Applying preset: ${import_chalk5.default.cyan(preset)}`);
|
|
1452
|
+
const env = detect(cwd);
|
|
1453
|
+
const useFlat = env.eslintMajor !== null && env.eslintMajor >= 9 || isFlat(env.configType);
|
|
1454
|
+
if (env.configType === "none") {
|
|
1455
|
+
const configPath = useFlat ? import_path6.default.join(cwd, "eslint.config.mjs") : import_path6.default.join(cwd, ".eslintrc.js");
|
|
1456
|
+
const content = useFlat ? generateFlatConfig(preset) : generateLegacyConfig(preset);
|
|
1457
|
+
writeConfig(configPath, content);
|
|
1458
|
+
log.success(`Created ${import_chalk5.default.white(import_path6.default.relative(cwd, configPath))} with ${import_chalk5.default.cyan(preset)} preset`);
|
|
1459
|
+
} else {
|
|
1460
|
+
const configPath = env.configPath;
|
|
1461
|
+
const backupPath = backupConfig(configPath);
|
|
1462
|
+
log.info(`Backed up \u2192 ${import_chalk5.default.gray(import_path6.default.relative(cwd, backupPath))}`);
|
|
1463
|
+
const existing = readConfig(configPath);
|
|
1464
|
+
const withPlugin = isFlat(env.configType) ? patchFlatConfig(existing, preset) : patchLegacyConfig(existing, preset);
|
|
1465
|
+
const patched = isFlat(env.configType) ? switchFlatPreset(withPlugin, preset) : switchLegacyPreset(withPlugin, preset);
|
|
1466
|
+
if (patched === existing) {
|
|
1467
|
+
log.warn("No preset changes needed. Config already matches selected preset.");
|
|
1468
|
+
} else {
|
|
1469
|
+
writeConfig(configPath, patched);
|
|
1470
|
+
log.success(`Updated ${import_chalk5.default.white(import_path6.default.relative(cwd, configPath))} to ${import_chalk5.default.cyan(preset)} preset`);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
log.blank();
|
|
1474
|
+
log.section("Preset Details");
|
|
1475
|
+
const ruleMap = preset === "strict" ? STRICT_RULES : preset === "security" ? SECURITY_RULES : RECOMMENDED_RULES;
|
|
1476
|
+
const entries = Object.entries(ruleMap).map(([ruleId, level]) => ({
|
|
1477
|
+
rule: ruleId.replace(/^ai-guard\//, ""),
|
|
1478
|
+
level: level === "error" ? import_chalk5.default.red("error") : import_chalk5.default.yellow("warn")
|
|
1479
|
+
}));
|
|
1480
|
+
for (const { rule, level } of entries) {
|
|
1481
|
+
log.print(` ${import_chalk5.default.gray("\u2022")} ${import_chalk5.default.white(rule).padEnd(32)}${level}`);
|
|
1482
|
+
}
|
|
1483
|
+
log.blank();
|
|
1484
|
+
log.info(`Run ${import_chalk5.default.cyan("ai-guard run")} to see results with the new preset.`);
|
|
1485
|
+
log.blank();
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// cli/commands/ignore.ts
|
|
1490
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
1491
|
+
init_config_manager();
|
|
1492
|
+
var import_path7 = __toESM(require("path"));
|
|
1493
|
+
var DEFAULT_IGNORES = [
|
|
1494
|
+
".next/",
|
|
1495
|
+
"dist/",
|
|
1496
|
+
"build/",
|
|
1497
|
+
"coverage/",
|
|
1498
|
+
"out/",
|
|
1499
|
+
"node_modules/"
|
|
1500
|
+
];
|
|
1501
|
+
function registerIgnoreCommand(program2) {
|
|
1502
|
+
program2.command("ignore").description("Add default ignore patterns (.next, dist, build, coverage) to ESLint config").action(() => {
|
|
1503
|
+
const cwd = process.cwd();
|
|
1504
|
+
log.banner("AI GUARD IGNORE");
|
|
1505
|
+
log.blank();
|
|
1506
|
+
log.section("Default Ignores");
|
|
1507
|
+
for (const p of DEFAULT_IGNORES) {
|
|
1508
|
+
log.print(` ${import_chalk6.default.gray("\u2022")} ${import_chalk6.default.yellow(p)}`);
|
|
1509
|
+
}
|
|
1510
|
+
log.blank();
|
|
1511
|
+
const env = detect(cwd);
|
|
1512
|
+
if (env.configType === "none") {
|
|
1513
|
+
const useFlat = env.eslintMajor !== null && env.eslintMajor >= 9;
|
|
1514
|
+
const configPath = useFlat ? import_path7.default.join(cwd, "eslint.config.mjs") : import_path7.default.join(cwd, ".eslintrc.js");
|
|
1515
|
+
const content = useFlat ? generateFlatConfig("recommended") : generateLegacyConfig("recommended");
|
|
1516
|
+
writeConfig(configPath, content);
|
|
1517
|
+
log.success(`No config found \u2014 generated ${import_chalk6.default.white(import_path7.default.relative(cwd, configPath))} with ignores included`);
|
|
1518
|
+
} else {
|
|
1519
|
+
const configPath = env.configPath;
|
|
1520
|
+
const backupPath = backupConfig(configPath);
|
|
1521
|
+
log.info(`Backed up \u2192 ${import_chalk6.default.gray(import_path7.default.relative(cwd, backupPath))}`);
|
|
1522
|
+
const existing = readConfig(configPath);
|
|
1523
|
+
const patched = isFlat(env.configType) ? addIgnoresToFlatConfig(existing) : addIgnoresToLegacyConfig(existing);
|
|
1524
|
+
if (patched === existing) {
|
|
1525
|
+
log.warn("Ignore patterns already present in config \u2014 no changes made.");
|
|
1526
|
+
} else {
|
|
1527
|
+
writeConfig(configPath, patched);
|
|
1528
|
+
log.success(`Patched ${import_chalk6.default.white(import_path7.default.relative(cwd, configPath))} with ignore patterns`);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
log.blank();
|
|
1532
|
+
log.info("Noisy directories will now be skipped in all future ESLint runs.");
|
|
1533
|
+
log.info(`Run ${import_chalk6.default.cyan("ai-guard run")} to verify cleaner results.`);
|
|
1534
|
+
log.blank();
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// cli/commands/baseline.ts
|
|
1539
|
+
var import_fs5 = __toESM(require("fs"));
|
|
1540
|
+
var import_path8 = __toESM(require("path"));
|
|
1541
|
+
var import_chalk7 = __toESM(require("chalk"));
|
|
1542
|
+
var import_ora2 = __toESM(require("ora"));
|
|
1543
|
+
var BASELINE_FILE = ".ai-guard-baseline.json";
|
|
1544
|
+
function saveBaseline(result, preset, mode, cwd) {
|
|
1545
|
+
const entries = result.files.map((f) => ({
|
|
1546
|
+
filePath: f.filePath,
|
|
1547
|
+
issues: f.issues.map((i) => ({
|
|
1548
|
+
ruleId: i.ruleId,
|
|
1549
|
+
message: i.message,
|
|
1550
|
+
line: i.line,
|
|
1551
|
+
column: i.column
|
|
1552
|
+
}))
|
|
1553
|
+
}));
|
|
1554
|
+
const baseline = {
|
|
1555
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1556
|
+
preset,
|
|
1557
|
+
mode,
|
|
1558
|
+
totalIssues: result.totalIssues,
|
|
1559
|
+
entries
|
|
1560
|
+
};
|
|
1561
|
+
const baselinePath = import_path8.default.join(cwd, BASELINE_FILE);
|
|
1562
|
+
import_fs5.default.writeFileSync(baselinePath, JSON.stringify(baseline, null, 2), "utf-8");
|
|
1563
|
+
return baselinePath;
|
|
1564
|
+
}
|
|
1565
|
+
function loadBaseline(cwd) {
|
|
1566
|
+
const baselinePath = import_path8.default.join(cwd, BASELINE_FILE);
|
|
1567
|
+
if (!import_fs5.default.existsSync(baselinePath)) return null;
|
|
1568
|
+
try {
|
|
1569
|
+
const raw = import_fs5.default.readFileSync(baselinePath, "utf-8");
|
|
1570
|
+
const parsed = JSON.parse(raw);
|
|
1571
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.preset !== "string" || !Array.isArray(parsed.entries)) {
|
|
1572
|
+
log.warn(
|
|
1573
|
+
`Baseline file ${BASELINE_FILE} is malformed \u2014 ignoring. Run 'ai-guard baseline --save' to regenerate.`
|
|
1574
|
+
);
|
|
1575
|
+
return null;
|
|
1576
|
+
}
|
|
1577
|
+
return parsed;
|
|
1578
|
+
} catch {
|
|
1579
|
+
log.warn(
|
|
1580
|
+
`Baseline file ${BASELINE_FILE} could not be parsed \u2014 ignoring. Run 'ai-guard baseline --save' to regenerate.`
|
|
1581
|
+
);
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
function issueKey(filePath, issue) {
|
|
1586
|
+
return `${filePath}::${issue.ruleId}::${issue.line}::${issue.column}::${issue.message}`;
|
|
1587
|
+
}
|
|
1588
|
+
function stableIssueKey(filePath, issue) {
|
|
1589
|
+
return `${filePath}::${issue.ruleId}::${issue.message}`;
|
|
1590
|
+
}
|
|
1591
|
+
function buildBaselineSet(baseline, mode) {
|
|
1592
|
+
const toKey = mode === "stable" ? stableIssueKey : issueKey;
|
|
1593
|
+
const set = /* @__PURE__ */ new Set();
|
|
1594
|
+
for (const entry of baseline.entries) {
|
|
1595
|
+
for (const issue of entry.issues) {
|
|
1596
|
+
set.add(toKey(entry.filePath, issue));
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
return set;
|
|
1600
|
+
}
|
|
1601
|
+
function computeNewIssues(result, baselineSet, mode) {
|
|
1602
|
+
const toKey = mode === "stable" ? stableIssueKey : issueKey;
|
|
1603
|
+
const newFiles = [];
|
|
1604
|
+
let totalErrors = 0;
|
|
1605
|
+
let totalWarnings = 0;
|
|
1606
|
+
const ruleBreakdown = /* @__PURE__ */ new Map();
|
|
1607
|
+
for (const file of result.files) {
|
|
1608
|
+
const newIssues = file.issues.filter(
|
|
1609
|
+
(i) => !baselineSet.has(toKey(file.filePath, i))
|
|
1610
|
+
);
|
|
1611
|
+
if (newIssues.length === 0) continue;
|
|
1612
|
+
const errorCount = newIssues.filter((i) => i.severity === 2).length;
|
|
1613
|
+
const warningCount = newIssues.filter((i) => i.severity === 1).length;
|
|
1614
|
+
totalErrors += errorCount;
|
|
1615
|
+
totalWarnings += warningCount;
|
|
1616
|
+
for (const issue of newIssues) {
|
|
1617
|
+
ruleBreakdown.set(
|
|
1618
|
+
issue.ruleId,
|
|
1619
|
+
(ruleBreakdown.get(issue.ruleId) ?? 0) + 1
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
newFiles.push({ ...file, issues: newIssues, errorCount, warningCount });
|
|
1623
|
+
}
|
|
1624
|
+
const topFiles = [...newFiles].sort((a, b) => b.issues.length - a.issues.length).slice(0, 10).map((f) => ({ path: f.filePath, count: f.issues.length }));
|
|
1625
|
+
return {
|
|
1626
|
+
files: newFiles,
|
|
1627
|
+
totalErrors,
|
|
1628
|
+
totalWarnings,
|
|
1629
|
+
totalIssues: totalErrors + totalWarnings,
|
|
1630
|
+
ruleBreakdown,
|
|
1631
|
+
topFiles,
|
|
1632
|
+
durationMs: result.durationMs
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
function registerBaselineCommand(program2) {
|
|
1636
|
+
program2.command("baseline").description("Save current issues as baseline; future runs show only new issues").option("--save", "Save current state as the new baseline").option("--check", "Show only issues introduced since the last baseline").option("--mode <name>", "Baseline match mode: strict | stable", "stable").option("--path <dir>", "Directory to scan", ".").option("--preset <name>", "Preset: recommended | strict | security", "recommended").action(async (opts) => {
|
|
1637
|
+
const cwd = process.cwd();
|
|
1638
|
+
const preset = opts.preset ?? "recommended";
|
|
1639
|
+
const mode = opts.mode ?? "stable";
|
|
1640
|
+
if (mode !== "strict" && mode !== "stable") {
|
|
1641
|
+
log.error("Invalid --mode. Use strict or stable.");
|
|
1642
|
+
process.exit(1);
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
log.banner("AI GUARD BASELINE");
|
|
1646
|
+
log.blank();
|
|
1647
|
+
const existingBaseline = loadBaseline(cwd);
|
|
1648
|
+
const doSave = opts.save ?? (!existingBaseline && !opts.check);
|
|
1649
|
+
const doCheck = opts.check ?? (existingBaseline !== null && !opts.save);
|
|
1650
|
+
if (doSave) {
|
|
1651
|
+
log.info("Scanning project to save baseline\u2026");
|
|
1652
|
+
const spinner = (0, import_ora2.default)({ text: "Running analysis\u2026", color: "cyan" }).start();
|
|
1653
|
+
let result;
|
|
1654
|
+
try {
|
|
1655
|
+
result = await runEslint({ preset, targetPath: opts.path });
|
|
1656
|
+
spinner.stop();
|
|
1657
|
+
} catch (err) {
|
|
1658
|
+
spinner.stop();
|
|
1659
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
1660
|
+
process.exit(1);
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const baselinePath = saveBaseline(result, preset, mode, cwd);
|
|
1664
|
+
log.success(
|
|
1665
|
+
`Baseline saved \u2192 ${import_chalk7.default.white(import_path8.default.relative(cwd, baselinePath))}`
|
|
1666
|
+
);
|
|
1667
|
+
log.info(` ${result.totalIssues} issues recorded (${result.totalErrors} errors, ${result.totalWarnings} warnings)`);
|
|
1668
|
+
log.info(` Preset: ${import_chalk7.default.cyan(preset)}`);
|
|
1669
|
+
log.info(` Mode: ${import_chalk7.default.cyan(mode)}`);
|
|
1670
|
+
log.blank();
|
|
1671
|
+
log.info("Future runs of " + import_chalk7.default.cyan("ai-guard baseline --check") + " will show only NEW issues.");
|
|
1672
|
+
log.info("Add " + import_chalk7.default.white(BASELINE_FILE) + " to your git repository to share baseline with your team.");
|
|
1673
|
+
log.blank();
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
if (doCheck) {
|
|
1677
|
+
if (!existingBaseline) {
|
|
1678
|
+
log.error(`No baseline found at ${import_chalk7.default.white(BASELINE_FILE)}`);
|
|
1679
|
+
log.info(`Run ${import_chalk7.default.cyan("ai-guard baseline --save")} first to create one.`);
|
|
1680
|
+
process.exit(1);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
const baselineDate = new Date(existingBaseline.createdAt).toLocaleString();
|
|
1684
|
+
log.info(`Baseline from: ${import_chalk7.default.gray(baselineDate)}`);
|
|
1685
|
+
log.info(`Baseline preset: ${import_chalk7.default.cyan(existingBaseline.preset)}`);
|
|
1686
|
+
log.info(`Baseline mode: ${import_chalk7.default.cyan(existingBaseline.mode ?? "strict")}`);
|
|
1687
|
+
log.info(`Baseline issues: ${import_chalk7.default.gray(existingBaseline.totalIssues)}`);
|
|
1688
|
+
const scanPreset = existingBaseline.preset;
|
|
1689
|
+
if (opts.preset && opts.preset !== scanPreset) {
|
|
1690
|
+
log.warn(
|
|
1691
|
+
`--preset ${import_chalk7.default.cyan(opts.preset)} ignored: scanning with baseline's preset ${import_chalk7.default.cyan(scanPreset)} to keep the comparison apples-to-apples.`
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
log.blank();
|
|
1695
|
+
const spinner = (0, import_ora2.default)({ text: "Scanning for new issues\u2026", color: "cyan" }).start();
|
|
1696
|
+
let result;
|
|
1697
|
+
try {
|
|
1698
|
+
result = await runEslint({ preset: scanPreset, targetPath: opts.path });
|
|
1699
|
+
spinner.stop();
|
|
1700
|
+
} catch (err) {
|
|
1701
|
+
spinner.stop();
|
|
1702
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
1703
|
+
process.exit(1);
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
const compareMode = existingBaseline.mode ?? "strict";
|
|
1707
|
+
const baselineSet = buildBaselineSet(existingBaseline, compareMode);
|
|
1708
|
+
const newResult = computeNewIssues(result, baselineSet, compareMode);
|
|
1709
|
+
if (newResult.totalIssues === 0) {
|
|
1710
|
+
log.success("No new issues since baseline! \u2728");
|
|
1711
|
+
log.info(
|
|
1712
|
+
`Total in codebase: ${result.totalIssues} (all existing, none new)`
|
|
1713
|
+
);
|
|
1714
|
+
log.blank();
|
|
1715
|
+
process.exit(0);
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
log.print(
|
|
1719
|
+
import_chalk7.default.bold.red(` \u2716 ${newResult.totalIssues} new issue${newResult.totalIssues !== 1 ? "s" : ""} since baseline`)
|
|
1720
|
+
);
|
|
1721
|
+
log.blank();
|
|
1722
|
+
if (newResult.ruleBreakdown.size > 0) {
|
|
1723
|
+
log.section("New Issues by Rule");
|
|
1724
|
+
for (const [rule, count] of [...newResult.ruleBreakdown.entries()].sort(
|
|
1725
|
+
(a, b) => b[1] - a[1]
|
|
1726
|
+
)) {
|
|
1727
|
+
log.rule(rule, count);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
if (newResult.files.length > 0) {
|
|
1731
|
+
log.section("New Issues by File");
|
|
1732
|
+
log.blank();
|
|
1733
|
+
for (const file of newResult.files) {
|
|
1734
|
+
log.print(
|
|
1735
|
+
` ${import_chalk7.default.bold.white(file.filePath)} ` + import_chalk7.default.gray(`(${file.issues.length} new)`)
|
|
1736
|
+
);
|
|
1737
|
+
for (const issue of file.issues) {
|
|
1738
|
+
log.issue(issue.message, issue.severity, issue.line, issue.column);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
log.blank();
|
|
1743
|
+
log.divider();
|
|
1744
|
+
log.blank();
|
|
1745
|
+
log.error(
|
|
1746
|
+
`${newResult.totalIssues} new issue${newResult.totalIssues !== 1 ? "s" : ""} introduced since baseline.`
|
|
1747
|
+
);
|
|
1748
|
+
log.info(`Run ${import_chalk7.default.cyan("ai-guard baseline --save")} to update the baseline after fixing them.`);
|
|
1749
|
+
log.blank();
|
|
1750
|
+
process.exit(1);
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// cli/commands/init-context.ts
|
|
1756
|
+
var import_fs6 = __toESM(require("fs"));
|
|
1757
|
+
var import_path9 = __toESM(require("path"));
|
|
1758
|
+
var import_chalk8 = __toESM(require("chalk"));
|
|
1759
|
+
var import_prompts2 = require("@inquirer/prompts");
|
|
1760
|
+
var RULE_CATEGORIES = {
|
|
1761
|
+
"error-handling": [
|
|
1762
|
+
"no-empty-catch",
|
|
1763
|
+
"no-catch-log-rethrow"
|
|
1764
|
+
],
|
|
1765
|
+
async: [
|
|
1766
|
+
"no-async-array-callback",
|
|
1767
|
+
"no-floating-promise"
|
|
1768
|
+
],
|
|
1769
|
+
security: [
|
|
1770
|
+
"no-hardcoded-secret",
|
|
1771
|
+
"no-eval-dynamic",
|
|
1772
|
+
"no-sql-string-concat",
|
|
1773
|
+
"no-unsafe-deserialize",
|
|
1774
|
+
"require-framework-auth",
|
|
1775
|
+
"require-framework-authz",
|
|
1776
|
+
"require-webhook-signature"
|
|
1777
|
+
],
|
|
1778
|
+
quality: [
|
|
1779
|
+
"no-console-in-handler",
|
|
1780
|
+
"no-duplicate-logic-block"
|
|
1781
|
+
]
|
|
1782
|
+
};
|
|
1783
|
+
var ALL_CATEGORIES = ["error-handling", "async", "security", "quality"];
|
|
1784
|
+
var ACTIVE_RULE_COUNT = Object.values(RULE_CATEGORIES).flat().length;
|
|
1785
|
+
function errorHandlingSection() {
|
|
1786
|
+
return `## Error Handling
|
|
1787
|
+
**Never** generate empty catch blocks:
|
|
1788
|
+
\u274C try { await fetchUser() } catch (e) {}
|
|
1789
|
+
\u2705 try { await fetchUser() } catch (e) { logger.error(e); throw e; }
|
|
1790
|
+
|
|
1791
|
+
**Never** catch, log, and immediately rethrow without adding context:
|
|
1792
|
+
\u274C catch (e) { console.error(e); throw e; }
|
|
1793
|
+
\u2705 catch (e) { throw new Error('fetchUser failed', { cause: e }); }`;
|
|
1794
|
+
}
|
|
1795
|
+
function asyncSection() {
|
|
1796
|
+
return `## Async Correctness
|
|
1797
|
+
**Never** pass async callbacks to array.map(), filter(), forEach(), or reduce():
|
|
1798
|
+
\u274C const results = ids.map(async (id) => await fetchUser(id));
|
|
1799
|
+
\u2705 const results = await Promise.all(ids.map((id) => fetchUser(id)));
|
|
1800
|
+
|
|
1801
|
+
**Never** call an async function without await or .catch():
|
|
1802
|
+
\u274C sendEmail(user);
|
|
1803
|
+
\u2705 await sendEmail(user);`;
|
|
1804
|
+
}
|
|
1805
|
+
function securitySection() {
|
|
1806
|
+
return `## Security
|
|
1807
|
+
**Never** hardcode secrets, API keys, tokens, or passwords inline:
|
|
1808
|
+
\u274C const apiKey = 'sk-prod-abc123';
|
|
1809
|
+
\u2705 const apiKey = process.env.API_KEY;
|
|
1810
|
+
|
|
1811
|
+
**Never** concatenate user input into SQL strings:
|
|
1812
|
+
\u274C db.query('SELECT * FROM users WHERE id = ' + req.params.id);
|
|
1813
|
+
\u2705 db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
|
|
1814
|
+
|
|
1815
|
+
**Never** use eval() or new Function() with dynamic/user-supplied strings.
|
|
1816
|
+
|
|
1817
|
+
**Never** call JSON.parse() on req.body, req.query, or req.params directly
|
|
1818
|
+
without schema validation.
|
|
1819
|
+
|
|
1820
|
+
**Always** add authentication middleware before route handlers:
|
|
1821
|
+
\u274C router.get('/admin/users', getUsers);
|
|
1822
|
+
\u2705 router.get('/admin/users', requireAuth, getUsers);
|
|
1823
|
+
|
|
1824
|
+
**Always** verify that the authenticated user owns or has access to the
|
|
1825
|
+
resource being accessed when using req.params.id or similar identifiers.
|
|
1826
|
+
|
|
1827
|
+
**Always** verify webhook signatures before parsing or trusting webhook
|
|
1828
|
+
payloads.`;
|
|
1829
|
+
}
|
|
1830
|
+
function qualitySection() {
|
|
1831
|
+
return `## Code Quality
|
|
1832
|
+
**Never** leave console.log or console.debug statements inside HTTP route
|
|
1833
|
+
handlers or event listeners in production code.
|
|
1834
|
+
|
|
1835
|
+
**Never** copy-paste the same logic block consecutively \u2014 extract shared
|
|
1836
|
+
logic into a helper function or consolidate duplicated code.`;
|
|
1837
|
+
}
|
|
1838
|
+
var SECTION_MAP = {
|
|
1839
|
+
"error-handling": errorHandlingSection,
|
|
1840
|
+
async: asyncSection,
|
|
1841
|
+
security: securitySection,
|
|
1842
|
+
quality: qualitySection
|
|
1843
|
+
};
|
|
1844
|
+
function getSections(categories) {
|
|
1845
|
+
return categories.map((cat) => SECTION_MAP[cat]()).join("\n\n");
|
|
1846
|
+
}
|
|
1847
|
+
function workflowGuidanceMarkdown() {
|
|
1848
|
+
return `## Workflow Guardrails
|
|
1849
|
+
- Follow user and repository instructions first; apply these rules on top.
|
|
1850
|
+
- Reuse existing project patterns and libraries before introducing new abstractions.
|
|
1851
|
+
- If a rule must be bypassed intentionally, add a suppression comment with reason:
|
|
1852
|
+
\`ai-guard-disable <rule-name> -- reason: <why>\`.
|
|
1853
|
+
- Before finalizing changes, run tests/lint and \`ai-guard run\` when available.`;
|
|
1854
|
+
}
|
|
1855
|
+
function workflowGuidanceCursor() {
|
|
1856
|
+
return `## Workflow Guardrails
|
|
1857
|
+
|
|
1858
|
+
- Follow user and repository instructions first.
|
|
1859
|
+
- Prefer existing project patterns and dependencies.
|
|
1860
|
+
- If bypassing a rule intentionally, add: ai-guard-disable <rule-name> -- reason: <why>.
|
|
1861
|
+
- Validate generated code with tests/lint and ai-guard run when available.`;
|
|
1862
|
+
}
|
|
1863
|
+
function workflowGuidanceCopilot() {
|
|
1864
|
+
return `## Required Workflow
|
|
1865
|
+
|
|
1866
|
+
- Follow user and repository instructions first.
|
|
1867
|
+
- Prefer edits that match existing architecture and dependencies.
|
|
1868
|
+
- If intentionally violating an ai-guard rule, add an inline suppression with a reason.
|
|
1869
|
+
- Run tests/lint and \`ai-guard run\` before finalizing changes when possible.`;
|
|
1870
|
+
}
|
|
1871
|
+
function generateClaudeFile(version, categories) {
|
|
1872
|
+
return `<!-- DO NOT EDIT MANUALLY \u2014 generated by @undercurrentai/eslint-plugin-ai-guard -->
|
|
1873
|
+
<!-- Regenerate: npx ai-guard init-context --force -->
|
|
1874
|
+
|
|
1875
|
+
# AI Guard Rules \u2014 Claude Code Instructions
|
|
1876
|
+
|
|
1877
|
+
You are coding in a project that uses @undercurrentai/eslint-plugin-ai-guard.
|
|
1878
|
+
Avoid generating the following patterns. They will be flagged as lint errors.
|
|
1879
|
+
|
|
1880
|
+
${workflowGuidanceMarkdown()}
|
|
1881
|
+
|
|
1882
|
+
${getSections(categories)}
|
|
1883
|
+
|
|
1884
|
+
---
|
|
1885
|
+
Generated by @undercurrentai/eslint-plugin-ai-guard v${version}.
|
|
1886
|
+
Run \`npx ai-guard init-context --force\` to regenerate.
|
|
1887
|
+
`;
|
|
1888
|
+
}
|
|
1889
|
+
function cursorErrorHandling() {
|
|
1890
|
+
return `## Error Handling
|
|
1891
|
+
|
|
1892
|
+
Rule: no-empty-catch
|
|
1893
|
+
Bad: try { await fetchUser() } catch (e) {}
|
|
1894
|
+
Good: try { await fetchUser() } catch (e) { logger.error(e); throw e; }
|
|
1895
|
+
|
|
1896
|
+
Rule: no-catch-log-rethrow
|
|
1897
|
+
Bad: catch (e) { console.error(e); throw e; }
|
|
1898
|
+
Good: catch (e) { throw new Error('fetchUser failed', { cause: e }); }`;
|
|
1899
|
+
}
|
|
1900
|
+
function cursorAsyncSection() {
|
|
1901
|
+
return `## Async Correctness
|
|
1902
|
+
|
|
1903
|
+
Rule: no-async-array-callback
|
|
1904
|
+
Bad: const results = ids.map(async (id) => await fetchUser(id));
|
|
1905
|
+
Good: const results = await Promise.all(ids.map((id) => fetchUser(id)));
|
|
1906
|
+
|
|
1907
|
+
Rule: no-floating-promise
|
|
1908
|
+
Bad: sendEmail(user);
|
|
1909
|
+
Good: await sendEmail(user);`;
|
|
1910
|
+
}
|
|
1911
|
+
function cursorSecuritySection() {
|
|
1912
|
+
return `## Security
|
|
1913
|
+
|
|
1914
|
+
Rule: no-hardcoded-secret
|
|
1915
|
+
Bad: const apiKey = 'sk-prod-abc123';
|
|
1916
|
+
Good: const apiKey = process.env.API_KEY;
|
|
1917
|
+
|
|
1918
|
+
Rule: no-sql-string-concat
|
|
1919
|
+
Bad: db.query('SELECT * FROM users WHERE id = ' + req.params.id);
|
|
1920
|
+
Good: db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
|
|
1921
|
+
|
|
1922
|
+
Rule: no-eval-dynamic
|
|
1923
|
+
Bad: eval(userInput);
|
|
1924
|
+
Good: Use a safe parser or allowlist approach instead.
|
|
1925
|
+
|
|
1926
|
+
Rule: no-unsafe-deserialize
|
|
1927
|
+
Bad: const data = JSON.parse(req.body);
|
|
1928
|
+
Good: const data = schema.parse(req.body);
|
|
1929
|
+
|
|
1930
|
+
Rule: require-framework-auth
|
|
1931
|
+
Bad: router.get('/admin/users', getUsers);
|
|
1932
|
+
Good: router.get('/admin/users', requireAuth, getUsers);
|
|
1933
|
+
|
|
1934
|
+
Rule: require-framework-authz
|
|
1935
|
+
Bad: const user = await db.findById(req.params.id); res.json(user);
|
|
1936
|
+
Good: const user = await db.findById(req.params.id); if (user.ownerId !== req.user.id) throw new ForbiddenError(); res.json(user);
|
|
1937
|
+
|
|
1938
|
+
Rule: require-webhook-signature
|
|
1939
|
+
Bad: app.post('/webhooks/stripe', async (req, res) => handleStripe(req.body));
|
|
1940
|
+
Good: app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), verifyStripeSignature, handleStripe);`;
|
|
1941
|
+
}
|
|
1942
|
+
function cursorQualitySection() {
|
|
1943
|
+
return `## Code Quality
|
|
1944
|
+
|
|
1945
|
+
Rule: no-console-in-handler
|
|
1946
|
+
Bad: router.get('/users', (req, res) => { console.log('hit'); ... });
|
|
1947
|
+
Good: router.get('/users', (req, res) => { logger.info('hit'); ... });
|
|
1948
|
+
|
|
1949
|
+
Rule: no-duplicate-logic-block
|
|
1950
|
+
Bad: Two identical consecutive code blocks copy-pasted.
|
|
1951
|
+
Good: Extract the duplicated logic into a shared helper function.`;
|
|
1952
|
+
}
|
|
1953
|
+
var CURSOR_SECTION_MAP = {
|
|
1954
|
+
"error-handling": cursorErrorHandling,
|
|
1955
|
+
async: cursorAsyncSection,
|
|
1956
|
+
security: cursorSecuritySection,
|
|
1957
|
+
quality: cursorQualitySection
|
|
1958
|
+
};
|
|
1959
|
+
function generateCursorFile(version, categories) {
|
|
1960
|
+
const sections = categories.map((cat) => CURSOR_SECTION_MAP[cat]()).join("\n\n");
|
|
1961
|
+
return `# AI Guard Rules \u2014 Cursor Instructions
|
|
1962
|
+
# DO NOT EDIT MANUALLY \u2014 generated by @undercurrentai/eslint-plugin-ai-guard
|
|
1963
|
+
# Regenerate: npx ai-guard init-context --force
|
|
1964
|
+
|
|
1965
|
+
This project uses @undercurrentai/eslint-plugin-ai-guard to catch ${ACTIVE_RULE_COUNT} active
|
|
1966
|
+
AI-risk rules. Follow these rules when
|
|
1967
|
+
generating or editing code in this project. Violations will be flagged
|
|
1968
|
+
as lint errors.
|
|
1969
|
+
|
|
1970
|
+
${workflowGuidanceCursor()}
|
|
1971
|
+
|
|
1972
|
+
${sections}
|
|
1973
|
+
|
|
1974
|
+
---
|
|
1975
|
+
These rules mirror @undercurrentai/eslint-plugin-ai-guard v${version}.
|
|
1976
|
+
Run \`npx ai-guard init-context --force\` to regenerate.
|
|
1977
|
+
`;
|
|
1978
|
+
}
|
|
1979
|
+
function copilotQuickRef(categories) {
|
|
1980
|
+
const lines = [];
|
|
1981
|
+
if (categories.includes("error-handling")) {
|
|
1982
|
+
lines.push(
|
|
1983
|
+
"Never: generate empty catch blocks",
|
|
1984
|
+
"Never: catch, log, and rethrow without adding context"
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
if (categories.includes("async")) {
|
|
1988
|
+
lines.push(
|
|
1989
|
+
"Never: pass async callbacks to .map(), .filter(), .forEach()",
|
|
1990
|
+
"Never: call async functions without await or .catch()"
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
if (categories.includes("security")) {
|
|
1994
|
+
lines.push(
|
|
1995
|
+
"Never: hardcode API keys, secrets, or passwords",
|
|
1996
|
+
"Never: concatenate user input into SQL strings",
|
|
1997
|
+
"Never: use eval() or new Function() with dynamic input",
|
|
1998
|
+
"Never: JSON.parse() untrusted input without validation",
|
|
1999
|
+
"Always: add auth middleware before route handlers",
|
|
2000
|
+
"Always: verify resource ownership in handlers",
|
|
2001
|
+
"Always: verify webhook signatures before trusting payloads"
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
if (categories.includes("quality")) {
|
|
2005
|
+
lines.push(
|
|
2006
|
+
"Never: leave console.log in route handlers",
|
|
2007
|
+
"Never: copy-paste identical consecutive code blocks"
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
return lines.map((l) => `- ${l}`).join("\n");
|
|
2011
|
+
}
|
|
2012
|
+
function copilotDetails(categories) {
|
|
2013
|
+
const details = [];
|
|
2014
|
+
if (categories.includes("async")) {
|
|
2015
|
+
details.push(`### no-floating-promise
|
|
2016
|
+
\`\`\`typescript
|
|
2017
|
+
// \u274C Bad \u2014 promise rejected silently
|
|
2018
|
+
sendEmail(user);
|
|
2019
|
+
|
|
2020
|
+
// \u2705 Good
|
|
2021
|
+
await sendEmail(user);
|
|
2022
|
+
\`\`\``);
|
|
2023
|
+
details.push(`### no-async-array-callback
|
|
2024
|
+
\`\`\`typescript
|
|
2025
|
+
// \u274C Bad \u2014 returns Promise[], not values
|
|
2026
|
+
ids.map(async (id) => await fetchUser(id));
|
|
2027
|
+
|
|
2028
|
+
// \u2705 Good
|
|
2029
|
+
await Promise.all(ids.map((id) => fetchUser(id)));
|
|
2030
|
+
\`\`\``);
|
|
2031
|
+
}
|
|
2032
|
+
if (categories.includes("error-handling")) {
|
|
2033
|
+
details.push(`### no-empty-catch
|
|
2034
|
+
\`\`\`typescript
|
|
2035
|
+
// \u274C Bad \u2014 error swallowed silently
|
|
2036
|
+
try { await fetchUser() } catch (e) {}
|
|
2037
|
+
|
|
2038
|
+
// \u2705 Good
|
|
2039
|
+
try { await fetchUser() } catch (e) { logger.error(e); throw e; }
|
|
2040
|
+
\`\`\``);
|
|
2041
|
+
}
|
|
2042
|
+
if (categories.includes("security")) {
|
|
2043
|
+
details.push(`### no-hardcoded-secret
|
|
2044
|
+
\`\`\`typescript
|
|
2045
|
+
// \u274C Bad
|
|
2046
|
+
const apiKey = 'sk-prod-abc123';
|
|
2047
|
+
|
|
2048
|
+
// \u2705 Good
|
|
2049
|
+
const apiKey = process.env.API_KEY;
|
|
2050
|
+
\`\`\``);
|
|
2051
|
+
details.push(`### no-sql-string-concat
|
|
2052
|
+
\`\`\`typescript
|
|
2053
|
+
// \u274C Bad \u2014 SQL injection
|
|
2054
|
+
db.query('SELECT * FROM users WHERE id = ' + req.params.id);
|
|
2055
|
+
|
|
2056
|
+
// \u2705 Good
|
|
2057
|
+
db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
|
|
2058
|
+
\`\`\``);
|
|
2059
|
+
}
|
|
2060
|
+
return details.join("\n\n");
|
|
2061
|
+
}
|
|
2062
|
+
function generateCopilotFile(version, categories) {
|
|
2063
|
+
return `<!-- DO NOT EDIT MANUALLY \u2014 generated by @undercurrentai/eslint-plugin-ai-guard -->
|
|
2064
|
+
<!-- Regenerate: npx ai-guard init-context --force -->
|
|
2065
|
+
|
|
2066
|
+
# AI Guard \u2014 GitHub Copilot Instructions
|
|
2067
|
+
|
|
2068
|
+
This project uses \`@undercurrentai/eslint-plugin-ai-guard\`. Follow these rules to avoid
|
|
2069
|
+
generating patterns that will be flagged as lint errors.
|
|
2070
|
+
|
|
2071
|
+
${workflowGuidanceCopilot()}
|
|
2072
|
+
|
|
2073
|
+
## Quick Reference
|
|
2074
|
+
|
|
2075
|
+
${copilotQuickRef(categories)}
|
|
2076
|
+
|
|
2077
|
+
## Details
|
|
2078
|
+
|
|
2079
|
+
${copilotDetails(categories)}
|
|
2080
|
+
|
|
2081
|
+
---
|
|
2082
|
+
Generated by @undercurrentai/eslint-plugin-ai-guard v${version}.
|
|
2083
|
+
Run \`npx ai-guard init-context --force\` to regenerate.
|
|
2084
|
+
`;
|
|
2085
|
+
}
|
|
2086
|
+
var AGENTS = [
|
|
2087
|
+
{
|
|
2088
|
+
id: "claude",
|
|
2089
|
+
label: "Claude Code",
|
|
2090
|
+
filePath: "CLAUDE.md",
|
|
2091
|
+
generate: generateClaudeFile
|
|
2092
|
+
},
|
|
2093
|
+
{
|
|
2094
|
+
id: "cursor",
|
|
2095
|
+
label: "Cursor",
|
|
2096
|
+
filePath: ".cursorrules",
|
|
2097
|
+
generate: generateCursorFile
|
|
2098
|
+
},
|
|
2099
|
+
{
|
|
2100
|
+
id: "copilot",
|
|
2101
|
+
label: "GitHub Copilot",
|
|
2102
|
+
filePath: import_path9.default.join(".github", "copilot-instructions.md"),
|
|
2103
|
+
generate: generateCopilotFile
|
|
2104
|
+
}
|
|
2105
|
+
];
|
|
2106
|
+
function getPackageVersion2(cwd) {
|
|
2107
|
+
try {
|
|
2108
|
+
const pkgPath = import_path9.default.join(cwd, "node_modules", "@undercurrentai/eslint-plugin-ai-guard", "package.json");
|
|
2109
|
+
if (import_fs6.default.existsSync(pkgPath)) {
|
|
2110
|
+
const pkg = JSON.parse(import_fs6.default.readFileSync(pkgPath, "utf-8"));
|
|
2111
|
+
if (pkg.version) return pkg.version;
|
|
2112
|
+
}
|
|
2113
|
+
} catch {
|
|
2114
|
+
}
|
|
2115
|
+
try {
|
|
2116
|
+
const pkgPath = import_path9.default.join(cwd, "package.json");
|
|
2117
|
+
if (import_fs6.default.existsSync(pkgPath)) {
|
|
2118
|
+
const pkg = JSON.parse(import_fs6.default.readFileSync(pkgPath, "utf-8"));
|
|
2119
|
+
if (pkg.version) return pkg.version;
|
|
2120
|
+
}
|
|
2121
|
+
} catch {
|
|
2122
|
+
}
|
|
2123
|
+
return "0.0.0";
|
|
2124
|
+
}
|
|
2125
|
+
function parseCategories(raw) {
|
|
2126
|
+
if (!raw) return ALL_CATEGORIES;
|
|
2127
|
+
const parts = raw.split(",").map((s) => s.trim().toLowerCase());
|
|
2128
|
+
const valid = [];
|
|
2129
|
+
for (const part of parts) {
|
|
2130
|
+
if (part in RULE_CATEGORIES) {
|
|
2131
|
+
valid.push(part);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
return valid.length > 0 ? valid : ALL_CATEGORIES;
|
|
2135
|
+
}
|
|
2136
|
+
function registerInitContextCommand(program2) {
|
|
2137
|
+
program2.command("init-context").description(
|
|
2138
|
+
`Generate AI agent instruction files (CLAUDE.md, .cursorrules, copilot-instructions.md)
|
|
2139
|
+
so Claude Code, Cursor, and GitHub Copilot align with ${ACTIVE_RULE_COUNT} active ai-guard rules.`
|
|
2140
|
+
).option("-a, --all", "Generate files for all agents (skip prompt)").option("--force", "Overwrite existing files without asking").option("--dry-run", "Print what would be generated without writing files").option("--rules <categories>", "Comma-separated categories: async,security,error-handling,quality").action(async (opts) => {
|
|
2141
|
+
const cwd = process.cwd();
|
|
2142
|
+
const version = getPackageVersion2(cwd);
|
|
2143
|
+
const categories = parseCategories(opts.rules);
|
|
2144
|
+
const isDryRun = opts.dryRun === true;
|
|
2145
|
+
const isForce = opts.force === true;
|
|
2146
|
+
log.banner("AI GUARD INIT-CONTEXT");
|
|
2147
|
+
if (isDryRun) {
|
|
2148
|
+
log.print(` ${import_chalk8.default.yellow("\u2691 DRY RUN \u2014 no files will be written")}`);
|
|
2149
|
+
log.blank();
|
|
2150
|
+
}
|
|
2151
|
+
let selectedAgents;
|
|
2152
|
+
if (opts.all) {
|
|
2153
|
+
selectedAgents = [...AGENTS];
|
|
2154
|
+
} else {
|
|
2155
|
+
let selected;
|
|
2156
|
+
try {
|
|
2157
|
+
selected = await (0, import_prompts2.checkbox)({
|
|
2158
|
+
message: "Which AI agents do you use?",
|
|
2159
|
+
choices: [
|
|
2160
|
+
{ name: "Claude Code -> writes CLAUDE.md", value: "claude" },
|
|
2161
|
+
{ name: "Cursor -> writes .cursorrules", value: "cursor" },
|
|
2162
|
+
{ name: "GitHub Copilot -> writes .github/copilot-instructions.md", value: "copilot" },
|
|
2163
|
+
{ name: "All three (recommended) -> writes all files", value: "all" }
|
|
2164
|
+
],
|
|
2165
|
+
required: true
|
|
2166
|
+
});
|
|
2167
|
+
} catch {
|
|
2168
|
+
log.info("Selection cancelled. Nothing to do.");
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
if (selected.length === 0) {
|
|
2172
|
+
log.info("No valid agents selected. Use --all to generate all files.");
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
if (selected.includes("all")) {
|
|
2176
|
+
selectedAgents = [...AGENTS];
|
|
2177
|
+
} else {
|
|
2178
|
+
selectedAgents = AGENTS.filter((a) => selected.includes(a.id));
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
log.info(`Categories: ${import_chalk8.default.cyan(categories.join(", "))}`);
|
|
2182
|
+
log.info(`Version: ${import_chalk8.default.cyan(version)}`);
|
|
2183
|
+
log.blank();
|
|
2184
|
+
const results = [];
|
|
2185
|
+
for (const agent of selectedAgents) {
|
|
2186
|
+
const fullPath = import_path9.default.join(cwd, agent.filePath);
|
|
2187
|
+
const exists = import_fs6.default.existsSync(fullPath);
|
|
2188
|
+
if (exists && !isForce && !isDryRun) {
|
|
2189
|
+
const overwrite = await (0, import_prompts2.confirm)({
|
|
2190
|
+
message: `${agent.filePath} already exists. Overwrite?`,
|
|
2191
|
+
default: false
|
|
2192
|
+
});
|
|
2193
|
+
if (!overwrite) {
|
|
2194
|
+
results.push({ file: agent.filePath, status: "skipped" });
|
|
2195
|
+
continue;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
const content = agent.generate(version, categories);
|
|
2199
|
+
if (isDryRun) {
|
|
2200
|
+
log.section(`Preview: ${agent.filePath}`);
|
|
2201
|
+
log.print(import_chalk8.default.gray("\u2500".repeat(60)));
|
|
2202
|
+
for (const line of content.split("\n").slice(0, 20)) {
|
|
2203
|
+
log.print(import_chalk8.default.gray(` ${line}`));
|
|
2204
|
+
}
|
|
2205
|
+
log.print(import_chalk8.default.gray(" ... (truncated)"));
|
|
2206
|
+
log.print(import_chalk8.default.gray("\u2500".repeat(60)));
|
|
2207
|
+
log.blank();
|
|
2208
|
+
results.push({ file: agent.filePath, status: "dry-run" });
|
|
2209
|
+
continue;
|
|
2210
|
+
}
|
|
2211
|
+
const parentDir = import_path9.default.dirname(fullPath);
|
|
2212
|
+
if (!import_fs6.default.existsSync(parentDir)) {
|
|
2213
|
+
import_fs6.default.mkdirSync(parentDir, { recursive: true });
|
|
2214
|
+
}
|
|
2215
|
+
import_fs6.default.writeFileSync(fullPath, content, "utf-8");
|
|
2216
|
+
results.push({ file: agent.filePath, status: "created" });
|
|
2217
|
+
}
|
|
2218
|
+
log.section("Summary");
|
|
2219
|
+
for (const result of results) {
|
|
2220
|
+
switch (result.status) {
|
|
2221
|
+
case "created":
|
|
2222
|
+
log.print(` ${import_chalk8.default.green("\u2705")} Created ${import_chalk8.default.white(result.file)}`);
|
|
2223
|
+
break;
|
|
2224
|
+
case "skipped":
|
|
2225
|
+
log.print(` ${import_chalk8.default.yellow("\u23ED")} Skipped ${import_chalk8.default.white(result.file)} ${import_chalk8.default.gray("(already exists \u2014 use --force to overwrite)")}`);
|
|
2226
|
+
break;
|
|
2227
|
+
case "dry-run":
|
|
2228
|
+
log.print(` ${import_chalk8.default.cyan("\u{1F4DD}")} Would create ${import_chalk8.default.white(result.file)}`);
|
|
2229
|
+
break;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
log.blank();
|
|
2233
|
+
if (isDryRun) {
|
|
2234
|
+
log.info("Dry run complete \u2014 no files were written.");
|
|
2235
|
+
log.info(`Re-run without ${import_chalk8.default.cyan("--dry-run")} to apply.`);
|
|
2236
|
+
} else {
|
|
2237
|
+
const createdCount = results.filter((r) => r.status === "created").length;
|
|
2238
|
+
if (createdCount > 0) {
|
|
2239
|
+
log.success(
|
|
2240
|
+
`Done. Your AI agents will now avoid ${ACTIVE_RULE_COUNT} active ai-guard rules.`
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
log.blank();
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// cli/index.ts
|
|
2249
|
+
function getVersion() {
|
|
2250
|
+
try {
|
|
2251
|
+
const pkgPath = import_path10.default.resolve(__dirname, "../../package.json");
|
|
2252
|
+
const raw = import_fs7.default.readFileSync(pkgPath, "utf-8");
|
|
2253
|
+
const pkg = JSON.parse(raw);
|
|
2254
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
2255
|
+
} catch {
|
|
2256
|
+
return "0.0.0";
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
var program = new import_commander.Command();
|
|
2260
|
+
program.name("ai-guard").description(
|
|
2261
|
+
"Production-grade CLI for @undercurrentai/eslint-plugin-ai-guard\nCatch AI-generated code issues instantly \u2014 no ESLint config required.\n\nQuick start:\n npx ai-guard run Scan current project\n npx ai-guard init Auto-configure ESLint\n npx ai-guard init-context Generate AI agent rules\n npx ai-guard doctor Check your setup\n npx ai-guard baseline Save baseline, track new issues only"
|
|
2262
|
+
).version(getVersion(), "-v, --version", "Print version number").helpOption("-h, --help", "Show help");
|
|
2263
|
+
registerRunCommand(program);
|
|
2264
|
+
registerInitCommand(program);
|
|
2265
|
+
registerDoctorCommand(program);
|
|
2266
|
+
registerPresetCommand(program);
|
|
2267
|
+
registerIgnoreCommand(program);
|
|
2268
|
+
registerBaselineCommand(program);
|
|
2269
|
+
registerInitContextCommand(program);
|
|
2270
|
+
program.configureOutput({
|
|
2271
|
+
writeErr(str) {
|
|
2272
|
+
const cleaned = str.replace(/^error:\s*/i, "").trim();
|
|
2273
|
+
log.error(cleaned);
|
|
2274
|
+
}
|
|
2275
|
+
});
|
|
2276
|
+
process.on("unhandledRejection", (reason) => {
|
|
2277
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
2278
|
+
log.error(`Unexpected error: ${msg}`);
|
|
2279
|
+
log.info("If this looks like a bug, please report it at:");
|
|
2280
|
+
log.info(" https://github.com/undercurrentai/eslint-plugin-ai-guard/issues");
|
|
2281
|
+
process.exit(1);
|
|
2282
|
+
});
|
|
2283
|
+
process.on("SIGINT", () => {
|
|
2284
|
+
log.blank();
|
|
2285
|
+
log.info("Cancelled.");
|
|
2286
|
+
process.exit(130);
|
|
2287
|
+
});
|
|
2288
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
2289
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2290
|
+
log.error(msg);
|
|
2291
|
+
process.exit(1);
|
|
2292
|
+
});
|
|
2293
|
+
//# sourceMappingURL=index.js.map
|