prodlint 0.9.5 → 0.10.0

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/action.yml CHANGED
@@ -1,152 +1,152 @@
1
- name: 'Prodlint'
2
- description: 'Production readiness for vibe-coded apps — check your AI code before you ship'
3
- branding:
4
- icon: 'shield'
5
- color: 'green'
6
-
7
- inputs:
8
- path:
9
- description: 'Path to scan (default: current directory)'
10
- required: false
11
- default: '.'
12
- threshold:
13
- description: 'Minimum score to pass (0-100). Fails the check if score is below this.'
14
- required: false
15
- default: '0'
16
- ignore:
17
- description: 'Glob patterns to ignore (comma-separated)'
18
- required: false
19
- default: ''
20
- comment:
21
- description: 'Post a PR comment with results (true/false)'
22
- required: false
23
- default: 'true'
24
-
25
- outputs:
26
- score:
27
- description: 'Overall prodlint score (0-100)'
28
- value: ${{ steps.scan.outputs.score }}
29
- critical:
30
- description: 'Number of critical findings'
31
- value: ${{ steps.scan.outputs.critical }}
32
-
33
- runs:
34
- using: 'composite'
35
- steps:
36
- - name: Setup Node.js
37
- uses: actions/setup-node@v4
38
- with:
39
- node-version: '20'
40
-
41
- - name: Run prodlint
42
- id: scan
43
- shell: bash
44
- env:
45
- INPUT_PATH: ${{ inputs.path }}
46
- INPUT_IGNORE: ${{ inputs.ignore }}
47
- run: |
48
- CMD_ARGS=("$INPUT_PATH" "--json")
49
- if [ -n "$INPUT_IGNORE" ]; then
50
- IFS=',' read -ra PATTERNS <<< "$INPUT_IGNORE"
51
- for p in "${PATTERNS[@]}"; do
52
- trimmed=$(echo "$p" | xargs)
53
- CMD_ARGS+=("--ignore" "$trimmed")
54
- done
55
- fi
56
-
57
- OUTPUT=$(npx -y prodlint@latest "${CMD_ARGS[@]}" 2>&1) || true
58
-
59
- SCORE=$(echo "$OUTPUT" | node -e "
60
- const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
61
- console.log(Number(d.overallScore) || 0);
62
- " 2>/dev/null || echo "0")
63
- SCORE=$(echo "$SCORE" | tr -cd '0-9')
64
-
65
- CRITICAL=$(echo "$OUTPUT" | node -e "
66
- const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
67
- console.log(Number(d.summary.critical) || 0);
68
- " 2>/dev/null || echo "0")
69
- CRITICAL=$(echo "$CRITICAL" | tr -cd '0-9')
70
-
71
- echo "score=${SCORE:-0}" >> "$GITHUB_OUTPUT"
72
- echo "critical=${CRITICAL:-0}" >> "$GITHUB_OUTPUT"
73
- echo "$OUTPUT" > /tmp/prodlint-result.json
74
-
75
- - name: Generate comment body
76
- if: inputs.comment == 'true' && github.event_name == 'pull_request'
77
- id: comment
78
- shell: bash
79
- env:
80
- SCAN_SCORE: ${{ steps.scan.outputs.score }}
81
- INPUT_THRESHOLD: ${{ inputs.threshold }}
82
- run: |
83
- SCORE="$SCAN_SCORE"
84
- THRESHOLD="$INPUT_THRESHOLD"
85
-
86
- if [ "$SCORE" -ge 80 ]; then
87
- EMOJI="✅"
88
- COLOR="brightgreen"
89
- elif [ "$SCORE" -ge 60 ]; then
90
- EMOJI="⚠️"
91
- COLOR="yellow"
92
- else
93
- EMOJI="🚨"
94
- COLOR="red"
95
- fi
96
-
97
- # Build comment from JSON with markdown sanitization
98
- TMPFILE=$(mktemp "${RUNNER_TEMP:-/tmp}/prodlint-comment-XXXXXX.md")
99
- node -e "
100
- const fs = require('fs');
101
- const d = JSON.parse(fs.readFileSync('/tmp/prodlint-result.json', 'utf8'));
102
- const esc = s => String(s).replace(/[[\]()\\*_\`<>]/g, c => '\\\\' + c);
103
- const lines = [];
104
- lines.push('## ' + '$EMOJI' + ' Production Readiness: **' + d.overallScore + '/100**');
105
- lines.push('');
106
- lines.push('| Category | Score | Issues |');
107
- lines.push('|----------|-------|--------|');
108
- for (const c of d.categoryScores) {
109
- const icon = c.score >= 80 ? '🟢' : c.score >= 60 ? '🟡' : '🔴';
110
- lines.push('| ' + icon + ' ' + esc(c.category) + ' | ' + c.score + '/100 | ' + c.findingCount + ' |');
111
- }
112
- if (d.summary.critical > 0) {
113
- lines.push('');
114
- lines.push('### Critical Issues');
115
- lines.push('');
116
- const crits = d.findings.filter(f => f.severity === 'critical');
117
- for (const f of crits.slice(0, 10)) {
118
- lines.push('- \`' + esc(f.ruleId) + '\` \`' + esc(f.file) + ':' + f.line + '\` — ' + esc(f.message));
119
- }
120
- if (crits.length > 10) {
121
- lines.push('- ...and ' + (crits.length - 10) + ' more');
122
- }
123
- }
124
- lines.push('');
125
- lines.push('---');
126
- lines.push('*Scanned ' + d.filesScanned + ' files in ' + d.scanDurationMs + 'ms · [prodlint](https://prodlint.com)*');
127
- fs.writeFileSync('$TMPFILE', lines.join('\n'));
128
- "
129
-
130
- # Use the generated file for the comment step
131
- cp "$TMPFILE" /tmp/prodlint-comment.md
132
-
133
- - name: Post PR comment
134
- if: inputs.comment == 'true' && github.event_name == 'pull_request'
135
- uses: marocchino/sticky-pull-request-comment@v3
136
- with:
137
- header: prodlint
138
- path: /tmp/prodlint-comment.md
139
-
140
- - name: Check threshold
141
- if: inputs.threshold != '0'
142
- shell: bash
143
- env:
144
- SCAN_SCORE: ${{ steps.scan.outputs.score }}
145
- INPUT_THRESHOLD: ${{ inputs.threshold }}
146
- run: |
147
- SCORE="$SCAN_SCORE"
148
- THRESHOLD="$INPUT_THRESHOLD"
149
- if [ "$SCORE" -lt "$THRESHOLD" ]; then
150
- echo "::error::Prodlint score ($SCORE) is below threshold ($THRESHOLD)"
151
- exit 1
152
- fi
1
+ name: 'Prodlint'
2
+ description: 'Production readiness for vibe-coded apps — check your AI code before you ship'
3
+ branding:
4
+ icon: 'shield'
5
+ color: 'green'
6
+
7
+ inputs:
8
+ path:
9
+ description: 'Path to scan (default: current directory)'
10
+ required: false
11
+ default: '.'
12
+ threshold:
13
+ description: 'Minimum score to pass (0-100). Fails the check if score is below this.'
14
+ required: false
15
+ default: '0'
16
+ ignore:
17
+ description: 'Glob patterns to ignore (comma-separated)'
18
+ required: false
19
+ default: ''
20
+ comment:
21
+ description: 'Post a PR comment with results (true/false)'
22
+ required: false
23
+ default: 'true'
24
+
25
+ outputs:
26
+ score:
27
+ description: 'Overall prodlint score (0-100)'
28
+ value: ${{ steps.scan.outputs.score }}
29
+ critical:
30
+ description: 'Number of critical findings'
31
+ value: ${{ steps.scan.outputs.critical }}
32
+
33
+ runs:
34
+ using: 'composite'
35
+ steps:
36
+ - name: Setup Node.js
37
+ uses: actions/setup-node@v4
38
+ with:
39
+ node-version: '20'
40
+
41
+ - name: Run prodlint
42
+ id: scan
43
+ shell: bash
44
+ env:
45
+ INPUT_PATH: ${{ inputs.path }}
46
+ INPUT_IGNORE: ${{ inputs.ignore }}
47
+ run: |
48
+ CMD_ARGS=("$INPUT_PATH" "--json")
49
+ if [ -n "$INPUT_IGNORE" ]; then
50
+ IFS=',' read -ra PATTERNS <<< "$INPUT_IGNORE"
51
+ for p in "${PATTERNS[@]}"; do
52
+ trimmed=$(echo "$p" | xargs)
53
+ CMD_ARGS+=("--ignore" "$trimmed")
54
+ done
55
+ fi
56
+
57
+ OUTPUT=$(npx -y prodlint@latest "${CMD_ARGS[@]}" 2>&1) || true
58
+
59
+ SCORE=$(echo "$OUTPUT" | node -e "
60
+ const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
61
+ console.log(Number(d.overallScore) || 0);
62
+ " 2>/dev/null || echo "0")
63
+ SCORE=$(echo "$SCORE" | tr -cd '0-9')
64
+
65
+ CRITICAL=$(echo "$OUTPUT" | node -e "
66
+ const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
67
+ console.log(Number(d.summary.critical) || 0);
68
+ " 2>/dev/null || echo "0")
69
+ CRITICAL=$(echo "$CRITICAL" | tr -cd '0-9')
70
+
71
+ echo "score=${SCORE:-0}" >> "$GITHUB_OUTPUT"
72
+ echo "critical=${CRITICAL:-0}" >> "$GITHUB_OUTPUT"
73
+ echo "$OUTPUT" > /tmp/prodlint-result.json
74
+
75
+ - name: Generate comment body
76
+ if: inputs.comment == 'true' && github.event_name == 'pull_request'
77
+ id: comment
78
+ shell: bash
79
+ env:
80
+ SCAN_SCORE: ${{ steps.scan.outputs.score }}
81
+ INPUT_THRESHOLD: ${{ inputs.threshold }}
82
+ run: |
83
+ SCORE="$SCAN_SCORE"
84
+ THRESHOLD="$INPUT_THRESHOLD"
85
+
86
+ if [ "$SCORE" -ge 80 ]; then
87
+ EMOJI="✅"
88
+ COLOR="brightgreen"
89
+ elif [ "$SCORE" -ge 60 ]; then
90
+ EMOJI="⚠️"
91
+ COLOR="yellow"
92
+ else
93
+ EMOJI="🚨"
94
+ COLOR="red"
95
+ fi
96
+
97
+ # Build comment from JSON with markdown sanitization
98
+ TMPFILE=$(mktemp "${RUNNER_TEMP:-/tmp}/prodlint-comment-XXXXXX.md")
99
+ node -e "
100
+ const fs = require('fs');
101
+ const d = JSON.parse(fs.readFileSync('/tmp/prodlint-result.json', 'utf8'));
102
+ const esc = s => String(s).replace(/[[\]()\\*_\`<>]/g, c => '\\\\' + c);
103
+ const lines = [];
104
+ lines.push('## ' + '$EMOJI' + ' Production Readiness: **' + d.overallScore + '/100**');
105
+ lines.push('');
106
+ lines.push('| Category | Score | Issues |');
107
+ lines.push('|----------|-------|--------|');
108
+ for (const c of d.categoryScores) {
109
+ const icon = c.score >= 80 ? '🟢' : c.score >= 60 ? '🟡' : '🔴';
110
+ lines.push('| ' + icon + ' ' + esc(c.category) + ' | ' + c.score + '/100 | ' + c.findingCount + ' |');
111
+ }
112
+ if (d.summary.critical > 0) {
113
+ lines.push('');
114
+ lines.push('### Critical Issues');
115
+ lines.push('');
116
+ const crits = d.findings.filter(f => f.severity === 'critical');
117
+ for (const f of crits.slice(0, 10)) {
118
+ lines.push('- \`' + esc(f.ruleId) + '\` \`' + esc(f.file) + ':' + f.line + '\` — ' + esc(f.message));
119
+ }
120
+ if (crits.length > 10) {
121
+ lines.push('- ...and ' + (crits.length - 10) + ' more');
122
+ }
123
+ }
124
+ lines.push('');
125
+ lines.push('---');
126
+ lines.push('*Scanned ' + d.filesScanned + ' files in ' + d.scanDurationMs + 'ms · [prodlint](https://prodlint.com)*');
127
+ fs.writeFileSync('$TMPFILE', lines.join('\n'));
128
+ "
129
+
130
+ # Use the generated file for the comment step
131
+ cp "$TMPFILE" /tmp/prodlint-comment.md
132
+
133
+ - name: Post PR comment
134
+ if: inputs.comment == 'true' && github.event_name == 'pull_request'
135
+ uses: marocchino/sticky-pull-request-comment@v3
136
+ with:
137
+ header: prodlint
138
+ path: /tmp/prodlint-comment.md
139
+
140
+ - name: Check threshold
141
+ if: inputs.threshold != '0'
142
+ shell: bash
143
+ env:
144
+ SCAN_SCORE: ${{ steps.scan.outputs.score }}
145
+ INPUT_THRESHOLD: ${{ inputs.threshold }}
146
+ run: |
147
+ SCORE="$SCAN_SCORE"
148
+ THRESHOLD="$INPUT_THRESHOLD"
149
+ if [ "$SCORE" -lt "$THRESHOLD" ]; then
150
+ echo "::error::Prodlint score ($SCORE) is below threshold ($THRESHOLD)"
151
+ exit 1
152
+ fi
package/dist/cli.js CHANGED
@@ -522,6 +522,33 @@ async function collectWorkspaceDependencies(root, patterns) {
522
522
  }
523
523
  return deps;
524
524
  }
525
+ async function collectNestedPackageDependencies(root) {
526
+ const deps = /* @__PURE__ */ new Set();
527
+ try {
528
+ const pkgFiles = await fg(["**/package.json"], {
529
+ cwd: root,
530
+ absolute: false,
531
+ ignore: ["**/node_modules/**", "package.json"]
532
+ });
533
+ for (const pkgFile of pkgFiles) {
534
+ try {
535
+ const raw = await readFile(resolve(root, pkgFile), "utf-8");
536
+ const pkg = JSON.parse(raw);
537
+ if (pkg.name) deps.add(pkg.name);
538
+ for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
539
+ if (pkg[key] && typeof pkg[key] === "object") {
540
+ for (const dep of Object.keys(pkg[key])) {
541
+ deps.add(dep);
542
+ }
543
+ }
544
+ }
545
+ } catch {
546
+ }
547
+ }
548
+ } catch {
549
+ }
550
+ return deps;
551
+ }
525
552
  async function buildProjectContext(root, files) {
526
553
  let packageJson = null;
527
554
  let declaredDependencies = /* @__PURE__ */ new Set();
@@ -549,6 +576,10 @@ async function buildProjectContext(root, files) {
549
576
  declaredDependencies.add(dep);
550
577
  }
551
578
  }
579
+ const nestedDeps = await collectNestedPackageDependencies(root);
580
+ for (const dep of nestedDeps) {
581
+ declaredDependencies.add(dep);
582
+ }
552
583
  for (const dep of declaredDependencies) {
553
584
  const framework = DEPENDENCY_TO_FRAMEWORK[dep];
554
585
  if (framework) {
package/dist/index.js CHANGED
@@ -516,6 +516,33 @@ async function collectWorkspaceDependencies(root, patterns) {
516
516
  }
517
517
  return deps;
518
518
  }
519
+ async function collectNestedPackageDependencies(root) {
520
+ const deps = /* @__PURE__ */ new Set();
521
+ try {
522
+ const pkgFiles = await fg(["**/package.json"], {
523
+ cwd: root,
524
+ absolute: false,
525
+ ignore: ["**/node_modules/**", "package.json"]
526
+ });
527
+ for (const pkgFile of pkgFiles) {
528
+ try {
529
+ const raw = await readFile(resolve(root, pkgFile), "utf-8");
530
+ const pkg = JSON.parse(raw);
531
+ if (pkg.name) deps.add(pkg.name);
532
+ for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
533
+ if (pkg[key] && typeof pkg[key] === "object") {
534
+ for (const dep of Object.keys(pkg[key])) {
535
+ deps.add(dep);
536
+ }
537
+ }
538
+ }
539
+ } catch {
540
+ }
541
+ }
542
+ } catch {
543
+ }
544
+ return deps;
545
+ }
519
546
  async function buildProjectContext(root, files) {
520
547
  let packageJson = null;
521
548
  let declaredDependencies = /* @__PURE__ */ new Set();
@@ -543,6 +570,10 @@ async function buildProjectContext(root, files) {
543
570
  declaredDependencies.add(dep);
544
571
  }
545
572
  }
573
+ const nestedDeps = await collectNestedPackageDependencies(root);
574
+ for (const dep of nestedDeps) {
575
+ declaredDependencies.add(dep);
576
+ }
546
577
  for (const dep of declaredDependencies) {
547
578
  const framework = DEPENDENCY_TO_FRAMEWORK[dep];
548
579
  if (framework) {
package/dist/mcp.js CHANGED
@@ -525,6 +525,33 @@ async function collectWorkspaceDependencies(root, patterns) {
525
525
  }
526
526
  return deps;
527
527
  }
528
+ async function collectNestedPackageDependencies(root) {
529
+ const deps = /* @__PURE__ */ new Set();
530
+ try {
531
+ const pkgFiles = await fg(["**/package.json"], {
532
+ cwd: root,
533
+ absolute: false,
534
+ ignore: ["**/node_modules/**", "package.json"]
535
+ });
536
+ for (const pkgFile of pkgFiles) {
537
+ try {
538
+ const raw = await readFile(resolve(root, pkgFile), "utf-8");
539
+ const pkg = JSON.parse(raw);
540
+ if (pkg.name) deps.add(pkg.name);
541
+ for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
542
+ if (pkg[key] && typeof pkg[key] === "object") {
543
+ for (const dep of Object.keys(pkg[key])) {
544
+ deps.add(dep);
545
+ }
546
+ }
547
+ }
548
+ } catch {
549
+ }
550
+ }
551
+ } catch {
552
+ }
553
+ return deps;
554
+ }
528
555
  async function buildProjectContext(root, files) {
529
556
  let packageJson = null;
530
557
  let declaredDependencies = /* @__PURE__ */ new Set();
@@ -552,6 +579,10 @@ async function buildProjectContext(root, files) {
552
579
  declaredDependencies.add(dep);
553
580
  }
554
581
  }
582
+ const nestedDeps = await collectNestedPackageDependencies(root);
583
+ for (const dep of nestedDeps) {
584
+ declaredDependencies.add(dep);
585
+ }
555
586
  for (const dep of declaredDependencies) {
556
587
  const framework = DEPENDENCY_TO_FRAMEWORK[dep];
557
588
  if (framework) {
package/package.json CHANGED
@@ -1,97 +1,97 @@
1
- {
2
- "name": "prodlint",
3
- "version": "0.9.5",
4
- "description": "Production readiness for vibe-coded apps — know your AI code is ready to ship",
5
- "license": "MIT",
6
- "author": "prodlint contributors",
7
- "type": "module",
8
- "repository": {
9
- "type": "git",
10
- "url": "https://github.com/prodlint/prodlint"
11
- },
12
- "bugs": {
13
- "url": "https://github.com/prodlint/prodlint/issues"
14
- },
15
- "homepage": "https://prodlint.com",
16
- "mcpName": "io.github.prodlint/prodlint",
17
- "bin": {
18
- "prodlint": "./dist/cli.js",
19
- "prodlint-mcp": "./dist/mcp.js"
20
- },
21
- "main": "./dist/index.js",
22
- "types": "./dist/index.d.ts",
23
- "exports": {
24
- ".": {
25
- "import": "./dist/index.js",
26
- "types": "./dist/index.d.ts"
27
- },
28
- "./mcp": {
29
- "import": "./dist/mcp.js"
30
- }
31
- },
32
- "files": [
33
- "dist/**/*.js",
34
- "dist/**/*.d.ts",
35
- "action.yml"
36
- ],
37
- "scripts": {
38
- "build": "tsup",
39
- "dev": "tsup --watch",
40
- "test": "vitest run",
41
- "test:watch": "vitest",
42
- "lint": "node dist/cli.js .",
43
- "audit:supply-chain": "node scripts/supply-chain-audit.cjs",
44
- "prepublishOnly": "npm run build"
45
- },
46
- "dependencies": {
47
- "@babel/parser": "^7.29.7",
48
- "@babel/types": "^7.29.7",
49
- "@modelcontextprotocol/sdk": "^1.29.0",
50
- "fast-glob": "^3.3.3",
51
- "picocolors": "^1.1.1",
52
- "zod": "^4.4.3"
53
- },
54
- "devDependencies": {
55
- "@types/node": "^26.0.1",
56
- "tsup": "^8.4.0",
57
- "typescript": "^5.9.3",
58
- "vitest": "^3.2.6"
59
- },
60
- "optionalDependencies": {
61
- "@rollup/rollup-win32-x64-msvc": "^4.62.2"
62
- },
63
- "overrides": {
64
- "esbuild": "^0.28.1"
65
- },
66
- "engines": {
67
- "node": ">=18"
68
- },
69
- "keywords": [
70
- "lint",
71
- "linter",
72
- "security",
73
- "ai",
74
- "ai-code",
75
- "vibe-coding",
76
- "production-readiness",
77
- "code-quality",
78
- "code-scanner",
79
- "next.js",
80
- "react",
81
- "typescript",
82
- "static-analysis",
83
- "cli",
84
- "scanner",
85
- "mcp",
86
- "mcp-server",
87
- "cursor",
88
- "copilot",
89
- "v0",
90
- "bolt",
91
- "windsurf",
92
- "claude",
93
- "github-action",
94
- "eslint-alternative",
95
- "devtools"
96
- ]
97
- }
1
+ {
2
+ "name": "prodlint",
3
+ "version": "0.10.0",
4
+ "description": "Production readiness for vibe-coded apps — know your AI code is ready to ship",
5
+ "license": "MIT",
6
+ "author": "prodlint contributors",
7
+ "type": "module",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/prodlint/prodlint"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/prodlint/prodlint/issues"
14
+ },
15
+ "homepage": "https://prodlint.com",
16
+ "mcpName": "io.github.prodlint/prodlint",
17
+ "bin": {
18
+ "prodlint": "./dist/cli.js",
19
+ "prodlint-mcp": "./dist/mcp.js"
20
+ },
21
+ "main": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "import": "./dist/index.js",
26
+ "types": "./dist/index.d.ts"
27
+ },
28
+ "./mcp": {
29
+ "import": "./dist/mcp.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist/**/*.js",
34
+ "dist/**/*.d.ts",
35
+ "action.yml"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsup",
39
+ "dev": "tsup --watch",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest",
42
+ "lint": "node dist/cli.js .",
43
+ "audit:supply-chain": "node scripts/supply-chain-audit.cjs",
44
+ "verify:release": "node scripts/verify-release.cjs",
45
+ "sync:versions": "node scripts/sync-versions.cjs",
46
+ "version": "node scripts/sync-versions.cjs",
47
+ "prepublishOnly": "npm run build"
48
+ },
49
+ "dependencies": {
50
+ "@babel/parser": "^7.29.7",
51
+ "@babel/types": "^7.29.7",
52
+ "@modelcontextprotocol/sdk": "^1.30.0",
53
+ "fast-glob": "^3.3.3",
54
+ "picocolors": "^1.1.1",
55
+ "zod": "^4.4.3"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^26.1.2",
59
+ "tsup": "^8.4.0",
60
+ "typescript": "^5.9.3",
61
+ "vitest": "^3.2.7"
62
+ },
63
+ "overrides": {
64
+ "esbuild": "^0.28.1"
65
+ },
66
+ "engines": {
67
+ "node": ">=18"
68
+ },
69
+ "keywords": [
70
+ "lint",
71
+ "linter",
72
+ "security",
73
+ "ai",
74
+ "ai-code",
75
+ "vibe-coding",
76
+ "production-readiness",
77
+ "code-quality",
78
+ "code-scanner",
79
+ "next.js",
80
+ "react",
81
+ "typescript",
82
+ "static-analysis",
83
+ "cli",
84
+ "scanner",
85
+ "mcp",
86
+ "mcp-server",
87
+ "cursor",
88
+ "copilot",
89
+ "v0",
90
+ "bolt",
91
+ "windsurf",
92
+ "claude",
93
+ "github-action",
94
+ "eslint-alternative",
95
+ "devtools"
96
+ ]
97
+ }