eslint-plugin-ai-guard 1.1.10 โ 1.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/dist/cli/index.js +525 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +358 -113
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +358 -113
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -28,11 +28,35 @@ npx ai-guard run # recommended preset (lowest noise)
|
|
|
28
28
|
npx ai-guard run --strict
|
|
29
29
|
npx ai-guard run --security
|
|
30
30
|
npx ai-guard init # auto-creates ESLint config for you
|
|
31
|
+
npx ai-guard init --dry-run
|
|
31
32
|
npx ai-guard doctor # diagnoses setup issues
|
|
32
33
|
npx ai-guard baseline # track only *new* issues going forward
|
|
33
34
|
```
|
|
34
35
|
That's it. **Zero configuration required.**
|
|
35
36
|
|
|
37
|
+
## ๐ค Set Up AI Agent Rules
|
|
38
|
+
|
|
39
|
+
Generate instruction files so Claude Code, Cursor, and GitHub Copilot
|
|
40
|
+
automatically avoid the 17 most common AI-generated anti-patterns:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx ai-guard init-context
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Follow the prompts to select your agent(s). Or generate all at once:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx ai-guard init-context --all
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This writes:
|
|
53
|
+
- `CLAUDE.md` โ read automatically by Claude Code
|
|
54
|
+
- `.cursorrules` โ read automatically by Cursor
|
|
55
|
+
- `.github/copilot-instructions.md` โ read automatically by GitHub Copilot
|
|
56
|
+
|
|
57
|
+
Your AI tools will now avoid these patterns before you even run the linter.
|
|
58
|
+
Use `--force` to regenerate after upgrading to a new version with new rules.
|
|
59
|
+
|
|
36
60
|
## ๐งช Real-World Usage Philosophy
|
|
37
61
|
|
|
38
62
|
`ai-guard` is designed for production adoption in existing codebases:
|
|
@@ -62,7 +86,8 @@ const users = await Promise.all(userIds.map(async (id) => {
|
|
|
62
86
|
|
|
63
87
|
### Terminal Output
|
|
64
88
|
|
|
65
|
-

|
|
90
|
+

|
|
66
91
|
|
|
67
92
|
*The terminal output above shows `ai-guard` catching multiple AI-generated anti-patterns in a single run.*
|
|
68
93
|
|
|
@@ -102,7 +127,7 @@ const users = await Promise.all(userIds.map(async (id) => {
|
|
|
102
127
|
Disallow dynamic `eval()` or `new Function()`.
|
|
103
128
|
- **`ai-guard/no-sql-string-concat`** (Warn in `recommended`, Error in `security`/`strict`)
|
|
104
129
|
Disallow variable concatenation/interpolation in SQL queries. AI tools frequently generate dangerous code enabling SQL injection.
|
|
105
|
-
- **`ai-guard/no-unsafe-deserialize`** (Warn)
|
|
130
|
+
- **`ai-guard/no-unsafe-deserialize`** (Warn in `recommended`/`security`, Error in `strict`)
|
|
106
131
|
Disallow `JSON.parse()` on likely untrusted inputs (like `req.body`) without visible validation.
|
|
107
132
|
- **`ai-guard/require-auth-middleware`** (Warn)
|
|
108
133
|
Enforce authentication middleware on Express/Fastify routes. AI tools frequently generate unprotected endpoints exposing sensitive data.
|
package/dist/cli/index.js
CHANGED
|
@@ -315,8 +315,8 @@ var init_config_manager = __esm({
|
|
|
315
315
|
|
|
316
316
|
// cli/index.ts
|
|
317
317
|
var import_commander = require("commander");
|
|
318
|
-
var
|
|
319
|
-
var
|
|
318
|
+
var import_fs7 = __toESM(require("fs"));
|
|
319
|
+
var import_path10 = __toESM(require("path"));
|
|
320
320
|
|
|
321
321
|
// cli/commands/run.ts
|
|
322
322
|
var import_ora = __toESM(require("ora"));
|
|
@@ -473,12 +473,21 @@ async function loadPluginModuleFromCwd(cwd) {
|
|
|
473
473
|
const requireFromCwd = createRequire(import_path.default.join(cwd, "package.json"));
|
|
474
474
|
try {
|
|
475
475
|
const resolved = requireFromCwd.resolve("eslint-plugin-ai-guard");
|
|
476
|
-
return
|
|
476
|
+
return import((0, import_url.pathToFileURL)(resolved).href);
|
|
477
477
|
} catch {
|
|
478
478
|
}
|
|
479
479
|
const localDistEntry = import_path.default.join(cwd, "dist", "index.js");
|
|
480
480
|
if (import_fs.default.existsSync(localDistEntry)) {
|
|
481
|
-
return
|
|
481
|
+
return import((0, import_url.pathToFileURL)(localDistEntry).href);
|
|
482
|
+
}
|
|
483
|
+
const localSrcEntry = import_path.default.join(cwd, "src", "index.ts");
|
|
484
|
+
if (import_fs.default.existsSync(localSrcEntry)) {
|
|
485
|
+
try {
|
|
486
|
+
return import((0, import_url.pathToFileURL)(localSrcEntry).href);
|
|
487
|
+
} catch (err) {
|
|
488
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
489
|
+
log.debug(`Local src plugin import failed: ${reason}`);
|
|
490
|
+
}
|
|
482
491
|
}
|
|
483
492
|
throw new Error(
|
|
484
493
|
"eslint-plugin-ai-guard is not installed. Run: npm install --save-dev eslint-plugin-ai-guard"
|
|
@@ -493,7 +502,13 @@ async function runEslint(options) {
|
|
|
493
502
|
const rawPlugin = await loadPluginModuleFromCwd(process.cwd());
|
|
494
503
|
const plugin = normalizePlugin(rawPlugin);
|
|
495
504
|
const rules = getRules(options.preset);
|
|
496
|
-
const
|
|
505
|
+
const resolvedTargetPath = import_path.default.resolve(options.targetPath);
|
|
506
|
+
if (!import_fs.default.existsSync(resolvedTargetPath)) {
|
|
507
|
+
throw new Error(`Path not found: ${options.targetPath}`);
|
|
508
|
+
}
|
|
509
|
+
const targetStat = import_fs.default.statSync(resolvedTargetPath);
|
|
510
|
+
const isSingleFileTarget = targetStat.isFile();
|
|
511
|
+
const eslintCwd = isSingleFileTarget ? import_path.default.dirname(resolvedTargetPath) : resolvedTargetPath;
|
|
497
512
|
const startMs = Date.now();
|
|
498
513
|
const JS_TS_FILES = [
|
|
499
514
|
"**/*.js",
|
|
@@ -556,11 +571,11 @@ async function runEslint(options) {
|
|
|
556
571
|
});
|
|
557
572
|
}
|
|
558
573
|
const eslint = new ESLint({
|
|
559
|
-
cwd:
|
|
574
|
+
cwd: eslintCwd,
|
|
560
575
|
overrideConfigFile: true,
|
|
561
576
|
overrideConfig: configBlocks
|
|
562
577
|
});
|
|
563
|
-
const patterns = JS_TS_FILES;
|
|
578
|
+
const patterns = isSingleFileTarget ? [import_path.default.basename(resolvedTargetPath)] : JS_TS_FILES;
|
|
564
579
|
const perPatternResults = await Promise.all(
|
|
565
580
|
patterns.map(async (pattern) => {
|
|
566
581
|
try {
|
|
@@ -617,8 +632,23 @@ async function runEslint(options) {
|
|
|
617
632
|
|
|
618
633
|
// cli/commands/run.ts
|
|
619
634
|
function registerRunCommand(program2) {
|
|
620
|
-
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(
|
|
635
|
+
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(
|
|
636
|
+
"--max-warnings <n>",
|
|
637
|
+
"Fail if warnings exceed this count",
|
|
638
|
+
(value) => Number.parseInt(value, 10)
|
|
639
|
+
).action(async (opts) => {
|
|
640
|
+
if (opts.maxWarnings !== void 0 && (!Number.isInteger(opts.maxWarnings) || opts.maxWarnings < 0)) {
|
|
641
|
+
log.blank();
|
|
642
|
+
log.error("--max-warnings must be a non-negative integer.");
|
|
643
|
+
log.blank();
|
|
644
|
+
process.exit(1);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
621
647
|
const preset = opts.strict ? "strict" : opts.security ? "security" : "recommended";
|
|
648
|
+
if (opts.strict && opts.security && !opts.json) {
|
|
649
|
+
log.warn("Both --strict and --security were provided. Using --strict.");
|
|
650
|
+
log.blank();
|
|
651
|
+
}
|
|
622
652
|
if (!opts.json) {
|
|
623
653
|
log.banner("AI GUARD RESULTS");
|
|
624
654
|
log.blank();
|
|
@@ -662,7 +692,7 @@ function registerRunCommand(program2) {
|
|
|
662
692
|
files: result.files
|
|
663
693
|
};
|
|
664
694
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
665
|
-
process.exit(
|
|
695
|
+
process.exit(getRunExitCode(result, opts.maxWarnings));
|
|
666
696
|
return;
|
|
667
697
|
}
|
|
668
698
|
log.success(`Scanned: ${import_chalk2.default.white(opts.path)}`);
|
|
@@ -729,10 +759,10 @@ function registerRunCommand(program2) {
|
|
|
729
759
|
log.info(`Run ${import_chalk2.default.cyan("ai-guard baseline")} to save these issues and track only new ones`);
|
|
730
760
|
log.info(`Run ${import_chalk2.default.cyan("ai-guard ignore")} to suppress dist/build noise`);
|
|
731
761
|
log.blank();
|
|
732
|
-
process.exit(
|
|
762
|
+
process.exit(getRunExitCode(result, opts.maxWarnings));
|
|
733
763
|
});
|
|
734
764
|
}
|
|
735
|
-
function
|
|
765
|
+
function getRunExitCode(result, maxWarnings) {
|
|
736
766
|
if (result.totalErrors > 0) return 1;
|
|
737
767
|
if (maxWarnings !== void 0 && result.totalWarnings > maxWarnings) return 1;
|
|
738
768
|
return 0;
|
|
@@ -1641,11 +1671,491 @@ function registerBaselineCommand(program2) {
|
|
|
1641
1671
|
});
|
|
1642
1672
|
}
|
|
1643
1673
|
|
|
1674
|
+
// cli/commands/init-context.ts
|
|
1675
|
+
var import_fs6 = __toESM(require("fs"));
|
|
1676
|
+
var import_path9 = __toESM(require("path"));
|
|
1677
|
+
var import_chalk8 = __toESM(require("chalk"));
|
|
1678
|
+
var import_prompts2 = __toESM(require("prompts"));
|
|
1679
|
+
var RULE_CATEGORIES = {
|
|
1680
|
+
"error-handling": [
|
|
1681
|
+
"no-empty-catch",
|
|
1682
|
+
"no-broad-exception",
|
|
1683
|
+
"no-catch-log-rethrow",
|
|
1684
|
+
"no-catch-without-use"
|
|
1685
|
+
],
|
|
1686
|
+
async: [
|
|
1687
|
+
"no-async-array-callback",
|
|
1688
|
+
"no-floating-promise",
|
|
1689
|
+
"no-await-in-loop",
|
|
1690
|
+
"no-async-without-await",
|
|
1691
|
+
"no-redundant-await"
|
|
1692
|
+
],
|
|
1693
|
+
security: [
|
|
1694
|
+
"no-hardcoded-secret",
|
|
1695
|
+
"no-eval-dynamic",
|
|
1696
|
+
"no-sql-string-concat",
|
|
1697
|
+
"no-unsafe-deserialize",
|
|
1698
|
+
"require-auth-middleware",
|
|
1699
|
+
"require-authz-check"
|
|
1700
|
+
],
|
|
1701
|
+
quality: [
|
|
1702
|
+
"no-console-in-handler",
|
|
1703
|
+
"no-duplicate-logic-block"
|
|
1704
|
+
]
|
|
1705
|
+
};
|
|
1706
|
+
var ALL_CATEGORIES = ["error-handling", "async", "security", "quality"];
|
|
1707
|
+
function errorHandlingSection() {
|
|
1708
|
+
return `## Error Handling
|
|
1709
|
+
**Never** generate empty catch blocks:
|
|
1710
|
+
\u274C try { await fetchUser() } catch (e) {}
|
|
1711
|
+
\u2705 try { await fetchUser() } catch (e) { logger.error(e); throw e; }
|
|
1712
|
+
|
|
1713
|
+
**Never** catch (e: any) \u2014 use specific types or narrow with instanceof.
|
|
1714
|
+
|
|
1715
|
+
**Never** catch, log, and immediately rethrow without adding context:
|
|
1716
|
+
\u274C catch (e) { console.error(e); throw e; }
|
|
1717
|
+
\u2705 catch (e) { throw new Error('fetchUser failed', { cause: e }); }`;
|
|
1718
|
+
}
|
|
1719
|
+
function asyncSection() {
|
|
1720
|
+
return `## Async Correctness
|
|
1721
|
+
**Never** pass async callbacks to array.map(), filter(), forEach(), or reduce():
|
|
1722
|
+
\u274C const results = ids.map(async (id) => await fetchUser(id));
|
|
1723
|
+
\u2705 const results = await Promise.all(ids.map((id) => fetchUser(id)));
|
|
1724
|
+
|
|
1725
|
+
**Never** use await inside a for...of or while loop when parallel execution
|
|
1726
|
+
is possible:
|
|
1727
|
+
\u274C for (const id of ids) { const user = await fetchUser(id); }
|
|
1728
|
+
\u2705 const users = await Promise.all(ids.map(fetchUser));
|
|
1729
|
+
|
|
1730
|
+
**Never** call an async function without await or .catch():
|
|
1731
|
+
\u274C sendEmail(user);
|
|
1732
|
+
\u2705 await sendEmail(user);
|
|
1733
|
+
|
|
1734
|
+
**Never** declare a function async if it never uses await.`;
|
|
1735
|
+
}
|
|
1736
|
+
function securitySection() {
|
|
1737
|
+
return `## Security
|
|
1738
|
+
**Never** hardcode secrets, API keys, tokens, or passwords inline:
|
|
1739
|
+
\u274C const apiKey = 'sk-prod-abc123';
|
|
1740
|
+
\u2705 const apiKey = process.env.API_KEY;
|
|
1741
|
+
|
|
1742
|
+
**Never** concatenate user input into SQL strings:
|
|
1743
|
+
\u274C db.query('SELECT * FROM users WHERE id = ' + req.params.id);
|
|
1744
|
+
\u2705 db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
|
|
1745
|
+
|
|
1746
|
+
**Never** use eval() or new Function() with dynamic/user-supplied strings.
|
|
1747
|
+
|
|
1748
|
+
**Never** call JSON.parse() on req.body, req.query, or req.params directly
|
|
1749
|
+
without schema validation.
|
|
1750
|
+
|
|
1751
|
+
**Always** add authentication middleware before route handlers:
|
|
1752
|
+
\u274C router.get('/admin/users', getUsers);
|
|
1753
|
+
\u2705 router.get('/admin/users', requireAuth, getUsers);
|
|
1754
|
+
|
|
1755
|
+
**Always** verify that the authenticated user owns or has access to the
|
|
1756
|
+
resource being accessed when using req.params.id or similar identifiers.`;
|
|
1757
|
+
}
|
|
1758
|
+
function qualitySection() {
|
|
1759
|
+
return `## Code Quality
|
|
1760
|
+
**Never** leave console.log or console.debug statements inside HTTP route
|
|
1761
|
+
handlers or event listeners in production code.
|
|
1762
|
+
|
|
1763
|
+
**Never** copy-paste the same logic block consecutively \u2014 extract shared
|
|
1764
|
+
logic into a helper function or consolidate duplicated code.`;
|
|
1765
|
+
}
|
|
1766
|
+
var SECTION_MAP = {
|
|
1767
|
+
"error-handling": errorHandlingSection,
|
|
1768
|
+
async: asyncSection,
|
|
1769
|
+
security: securitySection,
|
|
1770
|
+
quality: qualitySection
|
|
1771
|
+
};
|
|
1772
|
+
function getSections(categories) {
|
|
1773
|
+
return categories.map((cat) => SECTION_MAP[cat]()).join("\n\n");
|
|
1774
|
+
}
|
|
1775
|
+
function generateClaudeFile(version, categories) {
|
|
1776
|
+
return `<!-- DO NOT EDIT MANUALLY \u2014 generated by eslint-plugin-ai-guard -->
|
|
1777
|
+
<!-- Regenerate: npx ai-guard init-context --force -->
|
|
1778
|
+
|
|
1779
|
+
# AI Guard Rules \u2014 Claude Code Instructions
|
|
1780
|
+
|
|
1781
|
+
You are coding in a project that uses eslint-plugin-ai-guard.
|
|
1782
|
+
Avoid generating the following patterns. They will be flagged as lint errors.
|
|
1783
|
+
|
|
1784
|
+
${getSections(categories)}
|
|
1785
|
+
|
|
1786
|
+
---
|
|
1787
|
+
Generated by eslint-plugin-ai-guard v${version}.
|
|
1788
|
+
Run \`npx ai-guard init-context --force\` to regenerate.
|
|
1789
|
+
`;
|
|
1790
|
+
}
|
|
1791
|
+
function cursorErrorHandling() {
|
|
1792
|
+
return `## Error Handling
|
|
1793
|
+
|
|
1794
|
+
Rule: no-empty-catch
|
|
1795
|
+
Bad: try { await fetchUser() } catch (e) {}
|
|
1796
|
+
Good: try { await fetchUser() } catch (e) { logger.error(e); throw e; }
|
|
1797
|
+
|
|
1798
|
+
Rule: no-broad-exception
|
|
1799
|
+
Bad: catch (e: any) { handle(e); }
|
|
1800
|
+
Good: catch (e) { if (e instanceof NetworkError) { retry(); } throw e; }
|
|
1801
|
+
|
|
1802
|
+
Rule: no-catch-log-rethrow
|
|
1803
|
+
Bad: catch (e) { console.error(e); throw e; }
|
|
1804
|
+
Good: catch (e) { throw new Error('fetchUser failed', { cause: e }); }`;
|
|
1805
|
+
}
|
|
1806
|
+
function cursorAsyncSection() {
|
|
1807
|
+
return `## Async Correctness
|
|
1808
|
+
|
|
1809
|
+
Rule: no-async-array-callback
|
|
1810
|
+
Bad: const results = ids.map(async (id) => await fetchUser(id));
|
|
1811
|
+
Good: const results = await Promise.all(ids.map((id) => fetchUser(id)));
|
|
1812
|
+
|
|
1813
|
+
Rule: no-await-in-loop
|
|
1814
|
+
Bad: for (const id of ids) { const user = await fetchUser(id); }
|
|
1815
|
+
Good: const users = await Promise.all(ids.map(fetchUser));
|
|
1816
|
+
|
|
1817
|
+
Rule: no-floating-promise
|
|
1818
|
+
Bad: sendEmail(user);
|
|
1819
|
+
Good: await sendEmail(user);
|
|
1820
|
+
|
|
1821
|
+
Rule: no-async-without-await
|
|
1822
|
+
Bad: async function getUser() { return db.findOne(id); }
|
|
1823
|
+
Good: function getUser() { return db.findOne(id); }`;
|
|
1824
|
+
}
|
|
1825
|
+
function cursorSecuritySection() {
|
|
1826
|
+
return `## Security
|
|
1827
|
+
|
|
1828
|
+
Rule: no-hardcoded-secret
|
|
1829
|
+
Bad: const apiKey = 'sk-prod-abc123';
|
|
1830
|
+
Good: const apiKey = process.env.API_KEY;
|
|
1831
|
+
|
|
1832
|
+
Rule: no-sql-string-concat
|
|
1833
|
+
Bad: db.query('SELECT * FROM users WHERE id = ' + req.params.id);
|
|
1834
|
+
Good: db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
|
|
1835
|
+
|
|
1836
|
+
Rule: no-eval-dynamic
|
|
1837
|
+
Bad: eval(userInput);
|
|
1838
|
+
Good: Use a safe parser or allowlist approach instead.
|
|
1839
|
+
|
|
1840
|
+
Rule: no-unsafe-deserialize
|
|
1841
|
+
Bad: const data = JSON.parse(req.body);
|
|
1842
|
+
Good: const data = schema.parse(req.body);
|
|
1843
|
+
|
|
1844
|
+
Rule: require-auth-middleware
|
|
1845
|
+
Bad: router.get('/admin/users', getUsers);
|
|
1846
|
+
Good: router.get('/admin/users', requireAuth, getUsers);
|
|
1847
|
+
|
|
1848
|
+
Rule: require-authz-check
|
|
1849
|
+
Bad: const user = await db.findById(req.params.id); res.json(user);
|
|
1850
|
+
Good: const user = await db.findById(req.params.id); if (user.ownerId !== req.user.id) throw new ForbiddenError(); res.json(user);`;
|
|
1851
|
+
}
|
|
1852
|
+
function cursorQualitySection() {
|
|
1853
|
+
return `## Code Quality
|
|
1854
|
+
|
|
1855
|
+
Rule: no-console-in-handler
|
|
1856
|
+
Bad: router.get('/users', (req, res) => { console.log('hit'); ... });
|
|
1857
|
+
Good: router.get('/users', (req, res) => { logger.info('hit'); ... });
|
|
1858
|
+
|
|
1859
|
+
Rule: no-duplicate-logic-block
|
|
1860
|
+
Bad: Two identical consecutive code blocks copy-pasted.
|
|
1861
|
+
Good: Extract the duplicated logic into a shared helper function.`;
|
|
1862
|
+
}
|
|
1863
|
+
var CURSOR_SECTION_MAP = {
|
|
1864
|
+
"error-handling": cursorErrorHandling,
|
|
1865
|
+
async: cursorAsyncSection,
|
|
1866
|
+
security: cursorSecuritySection,
|
|
1867
|
+
quality: cursorQualitySection
|
|
1868
|
+
};
|
|
1869
|
+
function generateCursorFile(version, categories) {
|
|
1870
|
+
const sections = categories.map((cat) => CURSOR_SECTION_MAP[cat]()).join("\n\n");
|
|
1871
|
+
return `# AI Guard Rules \u2014 Cursor Instructions
|
|
1872
|
+
# DO NOT EDIT MANUALLY \u2014 generated by eslint-plugin-ai-guard
|
|
1873
|
+
# Regenerate: npx ai-guard init-context --force
|
|
1874
|
+
|
|
1875
|
+
This project uses eslint-plugin-ai-guard to catch the 17 most common
|
|
1876
|
+
anti-patterns generated by AI coding tools. Follow these rules when
|
|
1877
|
+
generating or editing code in this project. Violations will be flagged
|
|
1878
|
+
as lint errors.
|
|
1879
|
+
|
|
1880
|
+
${sections}
|
|
1881
|
+
|
|
1882
|
+
---
|
|
1883
|
+
These rules mirror eslint-plugin-ai-guard v${version}.
|
|
1884
|
+
Run \`npx ai-guard init-context --force\` to regenerate.
|
|
1885
|
+
`;
|
|
1886
|
+
}
|
|
1887
|
+
function copilotQuickRef(categories) {
|
|
1888
|
+
const lines = [];
|
|
1889
|
+
if (categories.includes("error-handling")) {
|
|
1890
|
+
lines.push(
|
|
1891
|
+
"Never: generate empty catch blocks",
|
|
1892
|
+
"Never: catch (e: any) without narrowing",
|
|
1893
|
+
"Never: catch, log, and rethrow without adding context"
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
if (categories.includes("async")) {
|
|
1897
|
+
lines.push(
|
|
1898
|
+
"Never: pass async callbacks to .map(), .filter(), .forEach()",
|
|
1899
|
+
"Never: use await inside loops when Promise.all() works",
|
|
1900
|
+
"Never: call async functions without await or .catch()",
|
|
1901
|
+
"Never: mark functions async if they never await"
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
if (categories.includes("security")) {
|
|
1905
|
+
lines.push(
|
|
1906
|
+
"Never: hardcode API keys, secrets, or passwords",
|
|
1907
|
+
"Never: concatenate user input into SQL strings",
|
|
1908
|
+
"Never: use eval() or new Function() with dynamic input",
|
|
1909
|
+
"Never: JSON.parse() untrusted input without validation",
|
|
1910
|
+
"Always: add auth middleware before route handlers",
|
|
1911
|
+
"Always: verify resource ownership in handlers"
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
if (categories.includes("quality")) {
|
|
1915
|
+
lines.push(
|
|
1916
|
+
"Never: leave console.log in route handlers",
|
|
1917
|
+
"Never: copy-paste identical consecutive code blocks"
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
return lines.map((l) => `- ${l}`).join("\n");
|
|
1921
|
+
}
|
|
1922
|
+
function copilotDetails(categories) {
|
|
1923
|
+
const details = [];
|
|
1924
|
+
if (categories.includes("async")) {
|
|
1925
|
+
details.push(`### no-floating-promise
|
|
1926
|
+
\`\`\`typescript
|
|
1927
|
+
// \u274C Bad \u2014 promise rejected silently
|
|
1928
|
+
sendEmail(user);
|
|
1929
|
+
|
|
1930
|
+
// \u2705 Good
|
|
1931
|
+
await sendEmail(user);
|
|
1932
|
+
\`\`\``);
|
|
1933
|
+
details.push(`### no-async-array-callback
|
|
1934
|
+
\`\`\`typescript
|
|
1935
|
+
// \u274C Bad \u2014 returns Promise[], not values
|
|
1936
|
+
ids.map(async (id) => await fetchUser(id));
|
|
1937
|
+
|
|
1938
|
+
// \u2705 Good
|
|
1939
|
+
await Promise.all(ids.map((id) => fetchUser(id)));
|
|
1940
|
+
\`\`\``);
|
|
1941
|
+
}
|
|
1942
|
+
if (categories.includes("error-handling")) {
|
|
1943
|
+
details.push(`### no-empty-catch
|
|
1944
|
+
\`\`\`typescript
|
|
1945
|
+
// \u274C Bad \u2014 error swallowed silently
|
|
1946
|
+
try { await fetchUser() } catch (e) {}
|
|
1947
|
+
|
|
1948
|
+
// \u2705 Good
|
|
1949
|
+
try { await fetchUser() } catch (e) { logger.error(e); throw e; }
|
|
1950
|
+
\`\`\``);
|
|
1951
|
+
}
|
|
1952
|
+
if (categories.includes("security")) {
|
|
1953
|
+
details.push(`### no-hardcoded-secret
|
|
1954
|
+
\`\`\`typescript
|
|
1955
|
+
// \u274C Bad
|
|
1956
|
+
const apiKey = 'sk-prod-abc123';
|
|
1957
|
+
|
|
1958
|
+
// \u2705 Good
|
|
1959
|
+
const apiKey = process.env.API_KEY;
|
|
1960
|
+
\`\`\``);
|
|
1961
|
+
details.push(`### no-sql-string-concat
|
|
1962
|
+
\`\`\`typescript
|
|
1963
|
+
// \u274C Bad \u2014 SQL injection
|
|
1964
|
+
db.query('SELECT * FROM users WHERE id = ' + req.params.id);
|
|
1965
|
+
|
|
1966
|
+
// \u2705 Good
|
|
1967
|
+
db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
|
|
1968
|
+
\`\`\``);
|
|
1969
|
+
}
|
|
1970
|
+
return details.join("\n\n");
|
|
1971
|
+
}
|
|
1972
|
+
function generateCopilotFile(version, categories) {
|
|
1973
|
+
return `<!-- DO NOT EDIT MANUALLY \u2014 generated by eslint-plugin-ai-guard -->
|
|
1974
|
+
<!-- Regenerate: npx ai-guard init-context --force -->
|
|
1975
|
+
|
|
1976
|
+
# AI Guard \u2014 GitHub Copilot Instructions
|
|
1977
|
+
|
|
1978
|
+
This project uses \`eslint-plugin-ai-guard\`. Follow these rules to avoid
|
|
1979
|
+
generating patterns that will be flagged as lint errors.
|
|
1980
|
+
|
|
1981
|
+
## Quick Reference
|
|
1982
|
+
|
|
1983
|
+
${copilotQuickRef(categories)}
|
|
1984
|
+
|
|
1985
|
+
## Details
|
|
1986
|
+
|
|
1987
|
+
${copilotDetails(categories)}
|
|
1988
|
+
|
|
1989
|
+
---
|
|
1990
|
+
Generated by eslint-plugin-ai-guard v${version}.
|
|
1991
|
+
Run \`npx ai-guard init-context --force\` to regenerate.
|
|
1992
|
+
`;
|
|
1993
|
+
}
|
|
1994
|
+
var AGENTS = [
|
|
1995
|
+
{
|
|
1996
|
+
id: "claude",
|
|
1997
|
+
label: "Claude Code",
|
|
1998
|
+
filePath: "CLAUDE.md",
|
|
1999
|
+
generate: generateClaudeFile
|
|
2000
|
+
},
|
|
2001
|
+
{
|
|
2002
|
+
id: "cursor",
|
|
2003
|
+
label: "Cursor",
|
|
2004
|
+
filePath: ".cursorrules",
|
|
2005
|
+
generate: generateCursorFile
|
|
2006
|
+
},
|
|
2007
|
+
{
|
|
2008
|
+
id: "copilot",
|
|
2009
|
+
label: "GitHub Copilot",
|
|
2010
|
+
filePath: import_path9.default.join(".github", "copilot-instructions.md"),
|
|
2011
|
+
generate: generateCopilotFile
|
|
2012
|
+
}
|
|
2013
|
+
];
|
|
2014
|
+
function getPackageVersion2(cwd) {
|
|
2015
|
+
try {
|
|
2016
|
+
const pkgPath = import_path9.default.join(cwd, "node_modules", "eslint-plugin-ai-guard", "package.json");
|
|
2017
|
+
if (import_fs6.default.existsSync(pkgPath)) {
|
|
2018
|
+
const pkg = JSON.parse(import_fs6.default.readFileSync(pkgPath, "utf-8"));
|
|
2019
|
+
if (pkg.version) return pkg.version;
|
|
2020
|
+
}
|
|
2021
|
+
} catch {
|
|
2022
|
+
}
|
|
2023
|
+
try {
|
|
2024
|
+
const pkgPath = import_path9.default.join(cwd, "package.json");
|
|
2025
|
+
if (import_fs6.default.existsSync(pkgPath)) {
|
|
2026
|
+
const pkg = JSON.parse(import_fs6.default.readFileSync(pkgPath, "utf-8"));
|
|
2027
|
+
if (pkg.version) return pkg.version;
|
|
2028
|
+
}
|
|
2029
|
+
} catch {
|
|
2030
|
+
}
|
|
2031
|
+
return "0.0.0";
|
|
2032
|
+
}
|
|
2033
|
+
function parseCategories(raw) {
|
|
2034
|
+
if (!raw) return ALL_CATEGORIES;
|
|
2035
|
+
const parts = raw.split(",").map((s) => s.trim().toLowerCase());
|
|
2036
|
+
const valid = [];
|
|
2037
|
+
for (const part of parts) {
|
|
2038
|
+
if (part in RULE_CATEGORIES) {
|
|
2039
|
+
valid.push(part);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
return valid.length > 0 ? valid : ALL_CATEGORIES;
|
|
2043
|
+
}
|
|
2044
|
+
function registerInitContextCommand(program2) {
|
|
2045
|
+
program2.command("init-context").description(
|
|
2046
|
+
"Generate AI agent instruction files (CLAUDE.md, .cursorrules, copilot-instructions.md)\nso Claude Code, Cursor, and GitHub Copilot avoid the 17 most common AI anti-patterns."
|
|
2047
|
+
).option("--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) => {
|
|
2048
|
+
const cwd = process.cwd();
|
|
2049
|
+
const version = getPackageVersion2(cwd);
|
|
2050
|
+
const categories = parseCategories(opts.rules);
|
|
2051
|
+
const isDryRun = opts.dryRun === true;
|
|
2052
|
+
const isForce = opts.force === true;
|
|
2053
|
+
log.banner("AI GUARD INIT-CONTEXT");
|
|
2054
|
+
if (isDryRun) {
|
|
2055
|
+
log.print(` ${import_chalk8.default.yellow("\u2691 DRY RUN \u2014 no files will be written")}`);
|
|
2056
|
+
log.blank();
|
|
2057
|
+
}
|
|
2058
|
+
let selectedAgents;
|
|
2059
|
+
if (opts.all) {
|
|
2060
|
+
selectedAgents = [...AGENTS];
|
|
2061
|
+
} else {
|
|
2062
|
+
const response = await (0, import_prompts2.default)({
|
|
2063
|
+
type: "multiselect",
|
|
2064
|
+
name: "agents",
|
|
2065
|
+
message: "Which AI agents do you use?",
|
|
2066
|
+
choices: [
|
|
2067
|
+
{ title: "Claude Code", value: "claude", description: "\u2192 writes CLAUDE.md" },
|
|
2068
|
+
{ title: "Cursor", value: "cursor", description: "\u2192 writes .cursorrules" },
|
|
2069
|
+
{ title: "GitHub Copilot", value: "copilot", description: "\u2192 writes .github/copilot-instructions.md" },
|
|
2070
|
+
{ title: "All three (recommended)", value: "all", description: "\u2192 writes all files" }
|
|
2071
|
+
],
|
|
2072
|
+
hint: "- space to select, enter to confirm"
|
|
2073
|
+
});
|
|
2074
|
+
if (!response.agents || response.agents.length === 0) {
|
|
2075
|
+
log.info("No agents selected. Nothing to do.");
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
const selected = response.agents;
|
|
2079
|
+
if (selected.includes("all")) {
|
|
2080
|
+
selectedAgents = [...AGENTS];
|
|
2081
|
+
} else {
|
|
2082
|
+
selectedAgents = AGENTS.filter((a) => selected.includes(a.id));
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
log.info(`Categories: ${import_chalk8.default.cyan(categories.join(", "))}`);
|
|
2086
|
+
log.info(`Version: ${import_chalk8.default.cyan(version)}`);
|
|
2087
|
+
log.blank();
|
|
2088
|
+
const results = [];
|
|
2089
|
+
for (const agent of selectedAgents) {
|
|
2090
|
+
const fullPath = import_path9.default.join(cwd, agent.filePath);
|
|
2091
|
+
const exists = import_fs6.default.existsSync(fullPath);
|
|
2092
|
+
if (exists && !isForce && !isDryRun) {
|
|
2093
|
+
const confirm = await (0, import_prompts2.default)({
|
|
2094
|
+
type: "confirm",
|
|
2095
|
+
name: "overwrite",
|
|
2096
|
+
message: `${agent.filePath} already exists. Overwrite?`,
|
|
2097
|
+
initial: false
|
|
2098
|
+
});
|
|
2099
|
+
if (!confirm.overwrite) {
|
|
2100
|
+
results.push({ file: agent.filePath, status: "skipped" });
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
const content = agent.generate(version, categories);
|
|
2105
|
+
if (isDryRun) {
|
|
2106
|
+
log.section(`Preview: ${agent.filePath}`);
|
|
2107
|
+
log.print(import_chalk8.default.gray("\u2500".repeat(60)));
|
|
2108
|
+
for (const line of content.split("\n").slice(0, 20)) {
|
|
2109
|
+
log.print(import_chalk8.default.gray(` ${line}`));
|
|
2110
|
+
}
|
|
2111
|
+
log.print(import_chalk8.default.gray(" ... (truncated)"));
|
|
2112
|
+
log.print(import_chalk8.default.gray("\u2500".repeat(60)));
|
|
2113
|
+
log.blank();
|
|
2114
|
+
results.push({ file: agent.filePath, status: "dry-run" });
|
|
2115
|
+
continue;
|
|
2116
|
+
}
|
|
2117
|
+
const parentDir = import_path9.default.dirname(fullPath);
|
|
2118
|
+
if (!import_fs6.default.existsSync(parentDir)) {
|
|
2119
|
+
import_fs6.default.mkdirSync(parentDir, { recursive: true });
|
|
2120
|
+
}
|
|
2121
|
+
import_fs6.default.writeFileSync(fullPath, content, "utf-8");
|
|
2122
|
+
results.push({ file: agent.filePath, status: "created" });
|
|
2123
|
+
}
|
|
2124
|
+
log.section("Summary");
|
|
2125
|
+
for (const result of results) {
|
|
2126
|
+
switch (result.status) {
|
|
2127
|
+
case "created":
|
|
2128
|
+
log.print(` ${import_chalk8.default.green("\u2705")} Created ${import_chalk8.default.white(result.file)}`);
|
|
2129
|
+
break;
|
|
2130
|
+
case "skipped":
|
|
2131
|
+
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)")}`);
|
|
2132
|
+
break;
|
|
2133
|
+
case "dry-run":
|
|
2134
|
+
log.print(` ${import_chalk8.default.cyan("\u{1F4DD}")} Would create ${import_chalk8.default.white(result.file)}`);
|
|
2135
|
+
break;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
log.blank();
|
|
2139
|
+
if (isDryRun) {
|
|
2140
|
+
log.info("Dry run complete \u2014 no files were written.");
|
|
2141
|
+
log.info(`Re-run without ${import_chalk8.default.cyan("--dry-run")} to apply.`);
|
|
2142
|
+
} else {
|
|
2143
|
+
const createdCount = results.filter((r) => r.status === "created").length;
|
|
2144
|
+
if (createdCount > 0) {
|
|
2145
|
+
log.success(
|
|
2146
|
+
"Done. Your AI agents will now automatically avoid the 17 most common AI-generated code anti-patterns."
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
log.blank();
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
|
|
1644
2154
|
// cli/index.ts
|
|
1645
2155
|
function getVersion() {
|
|
1646
2156
|
try {
|
|
1647
|
-
const pkgPath =
|
|
1648
|
-
const raw =
|
|
2157
|
+
const pkgPath = import_path10.default.resolve(__dirname, "../../package.json");
|
|
2158
|
+
const raw = import_fs7.default.readFileSync(pkgPath, "utf-8");
|
|
1649
2159
|
const pkg = JSON.parse(raw);
|
|
1650
2160
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
1651
2161
|
} catch {
|
|
@@ -1654,7 +2164,7 @@ function getVersion() {
|
|
|
1654
2164
|
}
|
|
1655
2165
|
var program = new import_commander.Command();
|
|
1656
2166
|
program.name("ai-guard").description(
|
|
1657
|
-
"Production-grade CLI for eslint-plugin-ai-guard\nCatch AI-generated code issues instantly \u2014 no ESLint config required.\n\nQuick start:\n npx ai-guard run Scan current project\n npx ai-guard init Auto-configure ESLint\n npx ai-guard doctor Check your setup\n npx ai-guard baseline Save baseline, track new issues only"
|
|
2167
|
+
"Production-grade CLI for eslint-plugin-ai-guard\nCatch AI-generated code issues instantly \u2014 no ESLint config required.\n\nQuick start:\n npx ai-guard run Scan current project\n npx ai-guard init Auto-configure ESLint\n npx ai-guard init-context Generate AI agent rules\n npx ai-guard doctor Check your setup\n npx ai-guard baseline Save baseline, track new issues only"
|
|
1658
2168
|
).version(getVersion(), "-v, --version", "Print version number").helpOption("-h, --help", "Show help");
|
|
1659
2169
|
registerRunCommand(program);
|
|
1660
2170
|
registerInitCommand(program);
|
|
@@ -1662,6 +2172,7 @@ registerDoctorCommand(program);
|
|
|
1662
2172
|
registerPresetCommand(program);
|
|
1663
2173
|
registerIgnoreCommand(program);
|
|
1664
2174
|
registerBaselineCommand(program);
|
|
2175
|
+
registerInitContextCommand(program);
|
|
1665
2176
|
program.configureOutput({
|
|
1666
2177
|
writeErr(str) {
|
|
1667
2178
|
const cleaned = str.replace(/^error:\s*/i, "").trim();
|