prodlint 0.6.0 → 0.7.1
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 -21
- package/README.md +253 -252
- package/action.yml +152 -152
- package/dist/cli.js +718 -23
- package/dist/index.js +718 -23
- package/dist/mcp.js +718 -23
- package/package.json +83 -83
package/action.yml
CHANGED
|
@@ -1,152 +1,152 @@
|
|
|
1
|
-
name: 'Prodlint'
|
|
2
|
-
description: 'The linter for vibe-coded apps — catch what AI coding tools miss'
|
|
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' + ' Prodlint Score: **' + 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@v2
|
|
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: 'The linter for vibe-coded apps — catch what AI coding tools miss'
|
|
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' + ' Prodlint Score: **' + 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@v2
|
|
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
|