prodlint 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,14 +6,14 @@
6
6
 
7
7
  The linter for vibe-coded apps.
8
8
 
9
- Cursor, v0, Bolt, and Copilot build fast. prodlint catches what they miss — hallucinated imports, missing auth, hardcoded secrets, unvalidated server actions, and the other production bugs that compile just fine. Zero config, no LLM, fast static analysis.
9
+ Static analysis for vibe-coded apps. Catches the production bugs that Cursor, v0, Bolt, and Copilot write — hallucinated imports, missing auth, hardcoded secrets, unvalidated server actions, and more. Zero config, no LLM, 52 rules, under 100ms.
10
10
 
11
11
  ```bash
12
12
  npx prodlint
13
13
  ```
14
14
 
15
15
  ```
16
- prodlint v0.6.0
16
+ prodlint v0.7.1
17
17
  Scanned 148 files · 3 critical · 5 warnings
18
18
 
19
19
  src/app/api/checkout/route.ts
@@ -140,7 +140,8 @@ npm i -g prodlint # Global install
140
140
 
141
141
  prodlint avoids common false positives:
142
142
 
143
- - **AST parsing** — Babel-based analysis for loops, imports, and SQL templates with regex fallback
143
+ - **AST parsing** — Babel-based analysis for 12 rules (imports, catch blocks, redirects, SSRF, path traversal, JWT, HTML injection, hydration, transactions, env leaks, loops, SQL) with regex fallback
144
+ - **Monorepo support** — npm/yarn/pnpm workspace dependencies resolved automatically
144
145
  - **Framework awareness** — Prisma, Drizzle, Supabase, Knex, and Sequelize whitelists prevent false SQL injection flags
145
146
  - **Middleware detection** — Clerk, NextAuth, Supabase middleware detected — auth findings downgraded
146
147
  - **Block comment awareness** — patterns inside `/* */` are ignored
@@ -218,6 +219,14 @@ claude mcp add prodlint npx prodlint-mcp
218
219
 
219
220
  Ask your AI: *"Run prodlint on this project"* and it calls the `scan` tool directly.
220
221
 
222
+ ## For AI Tools
223
+
224
+ - **LLM-friendly docs**: [prodlint.com/llms.txt](https://prodlint.com/llms.txt) — concise project summary for LLMs
225
+ - **Full reference**: [prodlint.com/llms-full.txt](https://prodlint.com/llms-full.txt) — all 52 rules with details
226
+ - **MCP setup guide**: [prodlint.com/mcp](https://prodlint.com/mcp) — detailed editor setup for Claude Code, Cursor, Windsurf
227
+
228
+ prodlint is designed specifically for AI-generated code patterns. Every rule targets bugs that AI coding tools consistently produce — not style nits.
229
+
221
230
  ## Suppression
222
231
 
223
232
  Suppress a single line:
package/dist/cli.js CHANGED
@@ -76,7 +76,10 @@ function isTestFile(relativePath) {
76
76
  return /\.(test|spec)\.[jt]sx?$/.test(relativePath) || /(?:^|\/)__tests__\//.test(relativePath) || /(?:^|\/)tests?\//.test(relativePath) || /(?:^|\/)fixtures?\//.test(relativePath) || /(?:^|\/)mocks?\//.test(relativePath);
77
77
  }
78
78
  function isScriptFile(relativePath) {
79
- return /(?:^|\/)scripts?\//.test(relativePath);
79
+ if (/(?:^|\/)scripts?\//.test(relativePath)) return true;
80
+ const name = relativePath.split("/").pop() ?? "";
81
+ const base = name.replace(/\.[^.]+$/, "");
82
+ return /^(seed|migrate|setup|bootstrap|generate|codegen|sync|deploy|cleanup|reset)$/.test(base);
80
83
  }
81
84
  function isConfigFile(relativePath) {
82
85
  const name = relativePath.split("/").pop() ?? "";
@@ -245,6 +248,25 @@ function findLoopsAST(ast) {
245
248
  });
246
249
  return loops;
247
250
  }
251
+ function getImportSources(ast) {
252
+ const sources = [];
253
+ walkAST(ast.program, (node) => {
254
+ if (node.type === "ImportDeclaration") {
255
+ const decl = node;
256
+ sources.push({ source: decl.source.value, line: decl.loc?.start.line ?? 1 });
257
+ }
258
+ if (node.type === "CallExpression") {
259
+ const call = node;
260
+ if (call.callee.type === "Identifier" && call.callee.name === "require" && call.arguments.length === 1 && call.arguments[0].type === "StringLiteral") {
261
+ sources.push({ source: call.arguments[0].value, line: node.loc?.start.line ?? 1 });
262
+ }
263
+ if (call.callee.type === "Import" && call.arguments.length >= 1 && call.arguments[0].type === "StringLiteral") {
264
+ sources.push({ source: call.arguments[0].value, line: node.loc?.start.line ?? 1 });
265
+ }
266
+ }
267
+ });
268
+ return sources;
269
+ }
248
270
  function isUserInputNode(node) {
249
271
  if (node.type === "MemberExpression") {
250
272
  const mem = node;
@@ -439,6 +461,66 @@ async function readFileContext(root, relativePath) {
439
461
  return null;
440
462
  }
441
463
  }
464
+ async function getWorkspacePatterns(root, packageJson) {
465
+ const patterns = [];
466
+ if (packageJson) {
467
+ const workspaces = packageJson.workspaces;
468
+ if (Array.isArray(workspaces)) {
469
+ patterns.push(...workspaces);
470
+ } else if (workspaces && typeof workspaces === "object" && Array.isArray(workspaces.packages)) {
471
+ patterns.push(...workspaces.packages);
472
+ }
473
+ }
474
+ try {
475
+ const raw = await readFile(resolve(root, "pnpm-workspace.yaml"), "utf-8");
476
+ let inPackages = false;
477
+ for (const line of raw.split(/\r?\n/)) {
478
+ const trimmed = line.trim();
479
+ if (/^packages\s*:/.test(trimmed)) {
480
+ inPackages = true;
481
+ continue;
482
+ }
483
+ if (inPackages) {
484
+ if (/^-\s+/.test(trimmed)) {
485
+ const glob = trimmed.replace(/^-\s+/, "").replace(/^['"]|['"]$/g, "");
486
+ if (glob) patterns.push(glob);
487
+ } else if (trimmed && !trimmed.startsWith("#")) {
488
+ break;
489
+ }
490
+ }
491
+ }
492
+ } catch {
493
+ }
494
+ return patterns;
495
+ }
496
+ async function collectWorkspaceDependencies(root, patterns) {
497
+ const deps = /* @__PURE__ */ new Set();
498
+ const globPatterns = patterns.map((p) => `${p}/package.json`);
499
+ try {
500
+ const pkgFiles = await fg(globPatterns, {
501
+ cwd: root,
502
+ absolute: false,
503
+ ignore: ["**/node_modules/**"]
504
+ });
505
+ for (const pkgFile of pkgFiles) {
506
+ try {
507
+ const raw = await readFile(resolve(root, pkgFile), "utf-8");
508
+ const pkg = JSON.parse(raw);
509
+ if (pkg.name) deps.add(pkg.name);
510
+ for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
511
+ if (pkg[key] && typeof pkg[key] === "object") {
512
+ for (const dep of Object.keys(pkg[key])) {
513
+ deps.add(dep);
514
+ }
515
+ }
516
+ }
517
+ } catch {
518
+ }
519
+ }
520
+ } catch {
521
+ }
522
+ return deps;
523
+ }
442
524
  async function buildProjectContext(root, files) {
443
525
  let packageJson = null;
444
526
  let declaredDependencies = /* @__PURE__ */ new Set();
@@ -457,19 +539,26 @@ async function buildProjectContext(root, files) {
457
539
  ...packageJson?.peerDependencies ?? {}
458
540
  };
459
541
  declaredDependencies = new Set(Object.keys(deps));
460
- for (const dep of declaredDependencies) {
461
- const framework = DEPENDENCY_TO_FRAMEWORK[dep];
462
- if (framework) {
463
- detectedFrameworks.add(framework);
464
- }
542
+ } catch {
543
+ }
544
+ const workspacePatterns = await getWorkspacePatterns(root, packageJson);
545
+ if (workspacePatterns.length > 0) {
546
+ const workspaceDeps = await collectWorkspaceDependencies(root, workspacePatterns);
547
+ for (const dep of workspaceDeps) {
548
+ declaredDependencies.add(dep);
465
549
  }
466
- for (const framework of detectedFrameworks) {
467
- if (RATE_LIMIT_FRAMEWORKS.has(framework)) {
468
- hasRateLimiting = true;
469
- break;
470
- }
550
+ }
551
+ for (const dep of declaredDependencies) {
552
+ const framework = DEPENDENCY_TO_FRAMEWORK[dep];
553
+ if (framework) {
554
+ detectedFrameworks.add(framework);
555
+ }
556
+ }
557
+ for (const framework of detectedFrameworks) {
558
+ if (RATE_LIMIT_FRAMEWORKS.has(framework)) {
559
+ hasRateLimiting = true;
560
+ break;
471
561
  }
472
- } catch {
473
562
  }
474
563
  try {
475
564
  const raw = await readFile(resolve(root, "tsconfig.json"), "utf-8");
@@ -724,6 +813,32 @@ var hallucinatedImportsRule = {
724
813
  const findings = [];
725
814
  const seen = /* @__PURE__ */ new Set();
726
815
  const isNonProd = isTestFile(file.relativePath) || isScriptFile(file.relativePath);
816
+ if (file.ast) {
817
+ try {
818
+ const imports = getImportSources(file.ast);
819
+ for (const { source: importPath, line } of imports) {
820
+ if (importPath.startsWith(".") || importPath.startsWith("/")) continue;
821
+ const pkgName = getPackageName(importPath);
822
+ if (seen.has(pkgName)) continue;
823
+ seen.add(pkgName);
824
+ if (isPathAlias(importPath, project.tsconfigPaths)) continue;
825
+ if (isNodeBuiltin(pkgName)) continue;
826
+ if (IMPLICIT_PACKAGES.has(importPath) || IMPLICIT_PACKAGES.has(pkgName)) continue;
827
+ if (project.declaredDependencies.has(pkgName)) continue;
828
+ findings.push({
829
+ ruleId: "hallucinated-imports",
830
+ file: file.relativePath,
831
+ line,
832
+ column: 1,
833
+ message: `Package "${pkgName}" is imported but not in package.json`,
834
+ severity: isNonProd ? "warning" : "critical",
835
+ category: "reliability"
836
+ });
837
+ }
838
+ return findings;
839
+ } catch {
840
+ }
841
+ }
727
842
  for (let i = 0; i < file.lines.length; i++) {
728
843
  if (isCommentLine(file.lines, i, file.commentMap)) continue;
729
844
  const line = file.lines[i];
@@ -2255,8 +2370,14 @@ function scoreCatchBody(bodyLines) {
2255
2370
  const body = bodyLines.join("\n");
2256
2371
  let score = 0;
2257
2372
  if (/console\.(log|warn|error|info)\s*\(/.test(body)) score = 1;
2258
- if (/console\.(error|warn)\s*\(/.test(body) && /\b(err|error|e)\b/.test(body)) score = 2;
2259
- if (/\bthrow\b/.test(body) || /\breturn\b.*(?:error|err|status|Response|NextResponse|json)/.test(body) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body)) {
2373
+ if ((/console\.(error|warn)\s*\(/.test(body) || /\b(logger|log)\.(error|warn)\s*\(/.test(body)) && /\b(err|error|e)\b/.test(body)) score = 2;
2374
+ if (/\bthrow\b/.test(body) || /\breturn\b.*(?:error|err|status|Response|NextResponse|json)/.test(body) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body) || // Toast notifications (react-toastify, sonner, shadcn)
2375
+ /\btoast\.(error|warn|warning)\s*\(/.test(body) || /\btoast\s*\(\s*\{/.test(body) || // Error monitoring (Sentry, etc.)
2376
+ /\b(Sentry\.)?captureException\s*\(/.test(body) || /\b(Sentry\.)?captureMessage\s*\(/.test(body) || // Structured loggers
2377
+ /\b(logger|log)\.(error|fatal)\s*\(/.test(body) || // Error handler utilities
2378
+ /\b(handleError|reportError|logError|notifyError|showError|onError|trackError)\s*\(/.test(body) || // Express middleware: next(err)
2379
+ /\bnext\s*\(\s*(err|error|e)\s*\)/.test(body) || // Next.js notFound()
2380
+ /\bnotFound\s*\(/.test(body)) {
2260
2381
  score = 3;
2261
2382
  }
2262
2383
  const labels = {
@@ -2288,7 +2409,7 @@ var shallowCatchRule = {
2288
2409
  if (!body || !body.loc) return;
2289
2410
  const bodyStart = body.loc.start.line - 1;
2290
2411
  const bodyEnd = body.loc.end.line - 1;
2291
- const bodyLines = file.lines.slice(bodyStart + 1, bodyEnd);
2412
+ const bodyLines = bodyStart === bodyEnd ? [file.lines[bodyStart]] : file.lines.slice(bodyStart + 1, bodyEnd);
2292
2413
  const { score, label } = scoreCatchBody(bodyLines);
2293
2414
  if (score <= 1) {
2294
2415
  findings.push({
package/dist/index.js CHANGED
@@ -71,7 +71,10 @@ function isTestFile(relativePath) {
71
71
  return /\.(test|spec)\.[jt]sx?$/.test(relativePath) || /(?:^|\/)__tests__\//.test(relativePath) || /(?:^|\/)tests?\//.test(relativePath) || /(?:^|\/)fixtures?\//.test(relativePath) || /(?:^|\/)mocks?\//.test(relativePath);
72
72
  }
73
73
  function isScriptFile(relativePath) {
74
- return /(?:^|\/)scripts?\//.test(relativePath);
74
+ if (/(?:^|\/)scripts?\//.test(relativePath)) return true;
75
+ const name = relativePath.split("/").pop() ?? "";
76
+ const base = name.replace(/\.[^.]+$/, "");
77
+ return /^(seed|migrate|setup|bootstrap|generate|codegen|sync|deploy|cleanup|reset)$/.test(base);
75
78
  }
76
79
  function isConfigFile(relativePath) {
77
80
  const name = relativePath.split("/").pop() ?? "";
@@ -240,6 +243,25 @@ function findLoopsAST(ast) {
240
243
  });
241
244
  return loops;
242
245
  }
246
+ function getImportSources(ast) {
247
+ const sources = [];
248
+ walkAST(ast.program, (node) => {
249
+ if (node.type === "ImportDeclaration") {
250
+ const decl = node;
251
+ sources.push({ source: decl.source.value, line: decl.loc?.start.line ?? 1 });
252
+ }
253
+ if (node.type === "CallExpression") {
254
+ const call = node;
255
+ if (call.callee.type === "Identifier" && call.callee.name === "require" && call.arguments.length === 1 && call.arguments[0].type === "StringLiteral") {
256
+ sources.push({ source: call.arguments[0].value, line: node.loc?.start.line ?? 1 });
257
+ }
258
+ if (call.callee.type === "Import" && call.arguments.length >= 1 && call.arguments[0].type === "StringLiteral") {
259
+ sources.push({ source: call.arguments[0].value, line: node.loc?.start.line ?? 1 });
260
+ }
261
+ }
262
+ });
263
+ return sources;
264
+ }
243
265
  function isUserInputNode(node) {
244
266
  if (node.type === "MemberExpression") {
245
267
  const mem = node;
@@ -434,6 +456,66 @@ async function readFileContext(root, relativePath) {
434
456
  return null;
435
457
  }
436
458
  }
459
+ async function getWorkspacePatterns(root, packageJson) {
460
+ const patterns = [];
461
+ if (packageJson) {
462
+ const workspaces = packageJson.workspaces;
463
+ if (Array.isArray(workspaces)) {
464
+ patterns.push(...workspaces);
465
+ } else if (workspaces && typeof workspaces === "object" && Array.isArray(workspaces.packages)) {
466
+ patterns.push(...workspaces.packages);
467
+ }
468
+ }
469
+ try {
470
+ const raw = await readFile(resolve(root, "pnpm-workspace.yaml"), "utf-8");
471
+ let inPackages = false;
472
+ for (const line of raw.split(/\r?\n/)) {
473
+ const trimmed = line.trim();
474
+ if (/^packages\s*:/.test(trimmed)) {
475
+ inPackages = true;
476
+ continue;
477
+ }
478
+ if (inPackages) {
479
+ if (/^-\s+/.test(trimmed)) {
480
+ const glob = trimmed.replace(/^-\s+/, "").replace(/^['"]|['"]$/g, "");
481
+ if (glob) patterns.push(glob);
482
+ } else if (trimmed && !trimmed.startsWith("#")) {
483
+ break;
484
+ }
485
+ }
486
+ }
487
+ } catch {
488
+ }
489
+ return patterns;
490
+ }
491
+ async function collectWorkspaceDependencies(root, patterns) {
492
+ const deps = /* @__PURE__ */ new Set();
493
+ const globPatterns = patterns.map((p) => `${p}/package.json`);
494
+ try {
495
+ const pkgFiles = await fg(globPatterns, {
496
+ cwd: root,
497
+ absolute: false,
498
+ ignore: ["**/node_modules/**"]
499
+ });
500
+ for (const pkgFile of pkgFiles) {
501
+ try {
502
+ const raw = await readFile(resolve(root, pkgFile), "utf-8");
503
+ const pkg = JSON.parse(raw);
504
+ if (pkg.name) deps.add(pkg.name);
505
+ for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
506
+ if (pkg[key] && typeof pkg[key] === "object") {
507
+ for (const dep of Object.keys(pkg[key])) {
508
+ deps.add(dep);
509
+ }
510
+ }
511
+ }
512
+ } catch {
513
+ }
514
+ }
515
+ } catch {
516
+ }
517
+ return deps;
518
+ }
437
519
  async function buildProjectContext(root, files) {
438
520
  let packageJson = null;
439
521
  let declaredDependencies = /* @__PURE__ */ new Set();
@@ -452,19 +534,26 @@ async function buildProjectContext(root, files) {
452
534
  ...packageJson?.peerDependencies ?? {}
453
535
  };
454
536
  declaredDependencies = new Set(Object.keys(deps));
455
- for (const dep of declaredDependencies) {
456
- const framework = DEPENDENCY_TO_FRAMEWORK[dep];
457
- if (framework) {
458
- detectedFrameworks.add(framework);
459
- }
537
+ } catch {
538
+ }
539
+ const workspacePatterns = await getWorkspacePatterns(root, packageJson);
540
+ if (workspacePatterns.length > 0) {
541
+ const workspaceDeps = await collectWorkspaceDependencies(root, workspacePatterns);
542
+ for (const dep of workspaceDeps) {
543
+ declaredDependencies.add(dep);
460
544
  }
461
- for (const framework of detectedFrameworks) {
462
- if (RATE_LIMIT_FRAMEWORKS.has(framework)) {
463
- hasRateLimiting = true;
464
- break;
465
- }
545
+ }
546
+ for (const dep of declaredDependencies) {
547
+ const framework = DEPENDENCY_TO_FRAMEWORK[dep];
548
+ if (framework) {
549
+ detectedFrameworks.add(framework);
550
+ }
551
+ }
552
+ for (const framework of detectedFrameworks) {
553
+ if (RATE_LIMIT_FRAMEWORKS.has(framework)) {
554
+ hasRateLimiting = true;
555
+ break;
466
556
  }
467
- } catch {
468
557
  }
469
558
  try {
470
559
  const raw = await readFile(resolve(root, "tsconfig.json"), "utf-8");
@@ -719,6 +808,32 @@ var hallucinatedImportsRule = {
719
808
  const findings = [];
720
809
  const seen = /* @__PURE__ */ new Set();
721
810
  const isNonProd = isTestFile(file.relativePath) || isScriptFile(file.relativePath);
811
+ if (file.ast) {
812
+ try {
813
+ const imports = getImportSources(file.ast);
814
+ for (const { source: importPath, line } of imports) {
815
+ if (importPath.startsWith(".") || importPath.startsWith("/")) continue;
816
+ const pkgName = getPackageName(importPath);
817
+ if (seen.has(pkgName)) continue;
818
+ seen.add(pkgName);
819
+ if (isPathAlias(importPath, project.tsconfigPaths)) continue;
820
+ if (isNodeBuiltin(pkgName)) continue;
821
+ if (IMPLICIT_PACKAGES.has(importPath) || IMPLICIT_PACKAGES.has(pkgName)) continue;
822
+ if (project.declaredDependencies.has(pkgName)) continue;
823
+ findings.push({
824
+ ruleId: "hallucinated-imports",
825
+ file: file.relativePath,
826
+ line,
827
+ column: 1,
828
+ message: `Package "${pkgName}" is imported but not in package.json`,
829
+ severity: isNonProd ? "warning" : "critical",
830
+ category: "reliability"
831
+ });
832
+ }
833
+ return findings;
834
+ } catch {
835
+ }
836
+ }
722
837
  for (let i = 0; i < file.lines.length; i++) {
723
838
  if (isCommentLine(file.lines, i, file.commentMap)) continue;
724
839
  const line = file.lines[i];
@@ -2250,8 +2365,14 @@ function scoreCatchBody(bodyLines) {
2250
2365
  const body = bodyLines.join("\n");
2251
2366
  let score = 0;
2252
2367
  if (/console\.(log|warn|error|info)\s*\(/.test(body)) score = 1;
2253
- if (/console\.(error|warn)\s*\(/.test(body) && /\b(err|error|e)\b/.test(body)) score = 2;
2254
- if (/\bthrow\b/.test(body) || /\breturn\b.*(?:error|err|status|Response|NextResponse|json)/.test(body) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body)) {
2368
+ if ((/console\.(error|warn)\s*\(/.test(body) || /\b(logger|log)\.(error|warn)\s*\(/.test(body)) && /\b(err|error|e)\b/.test(body)) score = 2;
2369
+ if (/\bthrow\b/.test(body) || /\breturn\b.*(?:error|err|status|Response|NextResponse|json)/.test(body) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body) || // Toast notifications (react-toastify, sonner, shadcn)
2370
+ /\btoast\.(error|warn|warning)\s*\(/.test(body) || /\btoast\s*\(\s*\{/.test(body) || // Error monitoring (Sentry, etc.)
2371
+ /\b(Sentry\.)?captureException\s*\(/.test(body) || /\b(Sentry\.)?captureMessage\s*\(/.test(body) || // Structured loggers
2372
+ /\b(logger|log)\.(error|fatal)\s*\(/.test(body) || // Error handler utilities
2373
+ /\b(handleError|reportError|logError|notifyError|showError|onError|trackError)\s*\(/.test(body) || // Express middleware: next(err)
2374
+ /\bnext\s*\(\s*(err|error|e)\s*\)/.test(body) || // Next.js notFound()
2375
+ /\bnotFound\s*\(/.test(body)) {
2255
2376
  score = 3;
2256
2377
  }
2257
2378
  const labels = {
@@ -2283,7 +2404,7 @@ var shallowCatchRule = {
2283
2404
  if (!body || !body.loc) return;
2284
2405
  const bodyStart = body.loc.start.line - 1;
2285
2406
  const bodyEnd = body.loc.end.line - 1;
2286
- const bodyLines = file.lines.slice(bodyStart + 1, bodyEnd);
2407
+ const bodyLines = bodyStart === bodyEnd ? [file.lines[bodyStart]] : file.lines.slice(bodyStart + 1, bodyEnd);
2287
2408
  const { score, label } = scoreCatchBody(bodyLines);
2288
2409
  if (score <= 1) {
2289
2410
  findings.push({
package/dist/mcp.js CHANGED
@@ -80,7 +80,10 @@ function isTestFile(relativePath) {
80
80
  return /\.(test|spec)\.[jt]sx?$/.test(relativePath) || /(?:^|\/)__tests__\//.test(relativePath) || /(?:^|\/)tests?\//.test(relativePath) || /(?:^|\/)fixtures?\//.test(relativePath) || /(?:^|\/)mocks?\//.test(relativePath);
81
81
  }
82
82
  function isScriptFile(relativePath) {
83
- return /(?:^|\/)scripts?\//.test(relativePath);
83
+ if (/(?:^|\/)scripts?\//.test(relativePath)) return true;
84
+ const name = relativePath.split("/").pop() ?? "";
85
+ const base = name.replace(/\.[^.]+$/, "");
86
+ return /^(seed|migrate|setup|bootstrap|generate|codegen|sync|deploy|cleanup|reset)$/.test(base);
84
87
  }
85
88
  function isConfigFile(relativePath) {
86
89
  const name = relativePath.split("/").pop() ?? "";
@@ -249,6 +252,25 @@ function findLoopsAST(ast) {
249
252
  });
250
253
  return loops;
251
254
  }
255
+ function getImportSources(ast) {
256
+ const sources = [];
257
+ walkAST(ast.program, (node) => {
258
+ if (node.type === "ImportDeclaration") {
259
+ const decl = node;
260
+ sources.push({ source: decl.source.value, line: decl.loc?.start.line ?? 1 });
261
+ }
262
+ if (node.type === "CallExpression") {
263
+ const call = node;
264
+ if (call.callee.type === "Identifier" && call.callee.name === "require" && call.arguments.length === 1 && call.arguments[0].type === "StringLiteral") {
265
+ sources.push({ source: call.arguments[0].value, line: node.loc?.start.line ?? 1 });
266
+ }
267
+ if (call.callee.type === "Import" && call.arguments.length >= 1 && call.arguments[0].type === "StringLiteral") {
268
+ sources.push({ source: call.arguments[0].value, line: node.loc?.start.line ?? 1 });
269
+ }
270
+ }
271
+ });
272
+ return sources;
273
+ }
252
274
  function isUserInputNode(node) {
253
275
  if (node.type === "MemberExpression") {
254
276
  const mem = node;
@@ -443,6 +465,66 @@ async function readFileContext(root, relativePath) {
443
465
  return null;
444
466
  }
445
467
  }
468
+ async function getWorkspacePatterns(root, packageJson) {
469
+ const patterns = [];
470
+ if (packageJson) {
471
+ const workspaces = packageJson.workspaces;
472
+ if (Array.isArray(workspaces)) {
473
+ patterns.push(...workspaces);
474
+ } else if (workspaces && typeof workspaces === "object" && Array.isArray(workspaces.packages)) {
475
+ patterns.push(...workspaces.packages);
476
+ }
477
+ }
478
+ try {
479
+ const raw = await readFile(resolve(root, "pnpm-workspace.yaml"), "utf-8");
480
+ let inPackages = false;
481
+ for (const line of raw.split(/\r?\n/)) {
482
+ const trimmed = line.trim();
483
+ if (/^packages\s*:/.test(trimmed)) {
484
+ inPackages = true;
485
+ continue;
486
+ }
487
+ if (inPackages) {
488
+ if (/^-\s+/.test(trimmed)) {
489
+ const glob = trimmed.replace(/^-\s+/, "").replace(/^['"]|['"]$/g, "");
490
+ if (glob) patterns.push(glob);
491
+ } else if (trimmed && !trimmed.startsWith("#")) {
492
+ break;
493
+ }
494
+ }
495
+ }
496
+ } catch {
497
+ }
498
+ return patterns;
499
+ }
500
+ async function collectWorkspaceDependencies(root, patterns) {
501
+ const deps = /* @__PURE__ */ new Set();
502
+ const globPatterns = patterns.map((p) => `${p}/package.json`);
503
+ try {
504
+ const pkgFiles = await fg(globPatterns, {
505
+ cwd: root,
506
+ absolute: false,
507
+ ignore: ["**/node_modules/**"]
508
+ });
509
+ for (const pkgFile of pkgFiles) {
510
+ try {
511
+ const raw = await readFile(resolve(root, pkgFile), "utf-8");
512
+ const pkg = JSON.parse(raw);
513
+ if (pkg.name) deps.add(pkg.name);
514
+ for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
515
+ if (pkg[key] && typeof pkg[key] === "object") {
516
+ for (const dep of Object.keys(pkg[key])) {
517
+ deps.add(dep);
518
+ }
519
+ }
520
+ }
521
+ } catch {
522
+ }
523
+ }
524
+ } catch {
525
+ }
526
+ return deps;
527
+ }
446
528
  async function buildProjectContext(root, files) {
447
529
  let packageJson = null;
448
530
  let declaredDependencies = /* @__PURE__ */ new Set();
@@ -461,19 +543,26 @@ async function buildProjectContext(root, files) {
461
543
  ...packageJson?.peerDependencies ?? {}
462
544
  };
463
545
  declaredDependencies = new Set(Object.keys(deps));
464
- for (const dep of declaredDependencies) {
465
- const framework = DEPENDENCY_TO_FRAMEWORK[dep];
466
- if (framework) {
467
- detectedFrameworks.add(framework);
468
- }
546
+ } catch {
547
+ }
548
+ const workspacePatterns = await getWorkspacePatterns(root, packageJson);
549
+ if (workspacePatterns.length > 0) {
550
+ const workspaceDeps = await collectWorkspaceDependencies(root, workspacePatterns);
551
+ for (const dep of workspaceDeps) {
552
+ declaredDependencies.add(dep);
469
553
  }
470
- for (const framework of detectedFrameworks) {
471
- if (RATE_LIMIT_FRAMEWORKS.has(framework)) {
472
- hasRateLimiting = true;
473
- break;
474
- }
554
+ }
555
+ for (const dep of declaredDependencies) {
556
+ const framework = DEPENDENCY_TO_FRAMEWORK[dep];
557
+ if (framework) {
558
+ detectedFrameworks.add(framework);
559
+ }
560
+ }
561
+ for (const framework of detectedFrameworks) {
562
+ if (RATE_LIMIT_FRAMEWORKS.has(framework)) {
563
+ hasRateLimiting = true;
564
+ break;
475
565
  }
476
- } catch {
477
566
  }
478
567
  try {
479
568
  const raw = await readFile(resolve(root, "tsconfig.json"), "utf-8");
@@ -728,6 +817,32 @@ var hallucinatedImportsRule = {
728
817
  const findings = [];
729
818
  const seen = /* @__PURE__ */ new Set();
730
819
  const isNonProd = isTestFile(file.relativePath) || isScriptFile(file.relativePath);
820
+ if (file.ast) {
821
+ try {
822
+ const imports = getImportSources(file.ast);
823
+ for (const { source: importPath, line } of imports) {
824
+ if (importPath.startsWith(".") || importPath.startsWith("/")) continue;
825
+ const pkgName = getPackageName(importPath);
826
+ if (seen.has(pkgName)) continue;
827
+ seen.add(pkgName);
828
+ if (isPathAlias(importPath, project.tsconfigPaths)) continue;
829
+ if (isNodeBuiltin(pkgName)) continue;
830
+ if (IMPLICIT_PACKAGES.has(importPath) || IMPLICIT_PACKAGES.has(pkgName)) continue;
831
+ if (project.declaredDependencies.has(pkgName)) continue;
832
+ findings.push({
833
+ ruleId: "hallucinated-imports",
834
+ file: file.relativePath,
835
+ line,
836
+ column: 1,
837
+ message: `Package "${pkgName}" is imported but not in package.json`,
838
+ severity: isNonProd ? "warning" : "critical",
839
+ category: "reliability"
840
+ });
841
+ }
842
+ return findings;
843
+ } catch {
844
+ }
845
+ }
731
846
  for (let i = 0; i < file.lines.length; i++) {
732
847
  if (isCommentLine(file.lines, i, file.commentMap)) continue;
733
848
  const line = file.lines[i];
@@ -2259,8 +2374,14 @@ function scoreCatchBody(bodyLines) {
2259
2374
  const body = bodyLines.join("\n");
2260
2375
  let score = 0;
2261
2376
  if (/console\.(log|warn|error|info)\s*\(/.test(body)) score = 1;
2262
- if (/console\.(error|warn)\s*\(/.test(body) && /\b(err|error|e)\b/.test(body)) score = 2;
2263
- if (/\bthrow\b/.test(body) || /\breturn\b.*(?:error|err|status|Response|NextResponse|json)/.test(body) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body)) {
2377
+ if ((/console\.(error|warn)\s*\(/.test(body) || /\b(logger|log)\.(error|warn)\s*\(/.test(body)) && /\b(err|error|e)\b/.test(body)) score = 2;
2378
+ if (/\bthrow\b/.test(body) || /\breturn\b.*(?:error|err|status|Response|NextResponse|json)/.test(body) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body) || // Toast notifications (react-toastify, sonner, shadcn)
2379
+ /\btoast\.(error|warn|warning)\s*\(/.test(body) || /\btoast\s*\(\s*\{/.test(body) || // Error monitoring (Sentry, etc.)
2380
+ /\b(Sentry\.)?captureException\s*\(/.test(body) || /\b(Sentry\.)?captureMessage\s*\(/.test(body) || // Structured loggers
2381
+ /\b(logger|log)\.(error|fatal)\s*\(/.test(body) || // Error handler utilities
2382
+ /\b(handleError|reportError|logError|notifyError|showError|onError|trackError)\s*\(/.test(body) || // Express middleware: next(err)
2383
+ /\bnext\s*\(\s*(err|error|e)\s*\)/.test(body) || // Next.js notFound()
2384
+ /\bnotFound\s*\(/.test(body)) {
2264
2385
  score = 3;
2265
2386
  }
2266
2387
  const labels = {
@@ -2292,7 +2413,7 @@ var shallowCatchRule = {
2292
2413
  if (!body || !body.loc) return;
2293
2414
  const bodyStart = body.loc.start.line - 1;
2294
2415
  const bodyEnd = body.loc.end.line - 1;
2295
- const bodyLines = file.lines.slice(bodyStart + 1, bodyEnd);
2416
+ const bodyLines = bodyStart === bodyEnd ? [file.lines[bodyStart]] : file.lines.slice(bodyStart + 1, bodyEnd);
2296
2417
  const { score, label } = scoreCatchBody(bodyLines);
2297
2418
  if (score <= 1) {
2298
2419
  findings.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prodlint",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "The linter for vibe-coded apps — catch what AI coding tools miss",
5
5
  "license": "MIT",
6
6
  "author": "prodlint contributors",
@@ -13,6 +13,7 @@
13
13
  "url": "https://github.com/prodlint/prodlint/issues"
14
14
  },
15
15
  "homepage": "https://prodlint.com",
16
+ "mcpName": "io.github.Anthony-Marcovecchio/prodlint",
16
17
  "bin": {
17
18
  "prodlint": "./dist/cli.js",
18
19
  "prodlint-mcp": "./dist/mcp.js"
@@ -78,6 +79,12 @@
78
79
  "mcp-server",
79
80
  "cursor",
80
81
  "copilot",
82
+ "v0",
83
+ "bolt",
84
+ "windsurf",
85
+ "claude",
86
+ "github-action",
87
+ "eslint-alternative",
81
88
  "devtools"
82
89
  ]
83
90
  }