@codexstar/bug-hunter 3.0.0 → 3.0.6
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/CHANGELOG.md +149 -83
- package/README.md +150 -15
- package/SKILL.md +94 -27
- package/agents/openai.yaml +4 -0
- package/bin/bug-hunter +9 -3
- package/docs/images/2026-03-12-fix-plan-rollout.png +0 -0
- package/docs/images/2026-03-12-hero-bug-hunter-overview.png +0 -0
- package/docs/images/2026-03-12-machine-readable-artifacts.png +0 -0
- package/docs/images/2026-03-12-pr-review-flow.png +0 -0
- package/docs/images/2026-03-12-security-pack.png +0 -0
- package/docs/images/adversarial-debate.png +0 -0
- package/docs/images/doc-verify-fix-plan.png +0 -0
- package/docs/images/hero.png +0 -0
- package/docs/images/pipeline-overview.png +0 -0
- package/docs/images/security-finding-card.png +0 -0
- package/docs/plans/2026-03-11-structured-output-migration-plan.md +288 -0
- package/docs/plans/2026-03-12-audit-bug-fixes-surgical-plan.md +193 -0
- package/docs/plans/2026-03-12-enterprise-security-pack-e2e-plan.md +59 -0
- package/docs/plans/2026-03-12-local-security-skills-integration-plan.md +39 -0
- package/docs/plans/2026-03-12-pr-review-strategic-fix-flow.md +78 -0
- package/evals/evals.json +366 -102
- package/modes/extended.md +2 -2
- package/modes/fix-loop.md +30 -30
- package/modes/fix-pipeline.md +32 -6
- package/modes/large-codebase.md +14 -15
- package/modes/local-sequential.md +44 -20
- package/modes/loop.md +56 -56
- package/modes/parallel.md +3 -3
- package/modes/scaled.md +2 -2
- package/modes/single-file.md +3 -3
- package/modes/small.md +11 -11
- package/package.json +11 -1
- package/prompts/fixer.md +37 -23
- package/prompts/hunter.md +39 -20
- package/prompts/referee.md +34 -20
- package/prompts/skeptic.md +25 -22
- package/schemas/coverage.schema.json +67 -0
- package/schemas/examples/findings.invalid.json +13 -0
- package/schemas/examples/findings.valid.json +17 -0
- package/schemas/findings.schema.json +76 -0
- package/schemas/fix-plan.schema.json +94 -0
- package/schemas/fix-report.schema.json +105 -0
- package/schemas/fix-strategy.schema.json +99 -0
- package/schemas/recon.schema.json +31 -0
- package/schemas/referee.schema.json +46 -0
- package/schemas/shared.schema.json +51 -0
- package/schemas/skeptic.schema.json +21 -0
- package/scripts/bug-hunter-state.cjs +35 -12
- package/scripts/code-index.cjs +11 -4
- package/scripts/fix-lock.cjs +95 -25
- package/scripts/payload-guard.cjs +24 -10
- package/scripts/pr-scope.cjs +181 -0
- package/scripts/prepublish-guard.cjs +82 -0
- package/scripts/render-report.cjs +346 -0
- package/scripts/run-bug-hunter.cjs +669 -33
- package/scripts/schema-runtime.cjs +273 -0
- package/scripts/schema-validate.cjs +40 -0
- package/scripts/tests/bug-hunter-state.test.cjs +68 -3
- package/scripts/tests/code-index.test.cjs +15 -0
- package/scripts/tests/fix-lock.test.cjs +60 -2
- package/scripts/tests/fixtures/flaky-worker.cjs +6 -1
- package/scripts/tests/fixtures/low-confidence-worker.cjs +8 -2
- package/scripts/tests/fixtures/success-worker.cjs +6 -1
- package/scripts/tests/payload-guard.test.cjs +154 -2
- package/scripts/tests/pr-scope.test.cjs +212 -0
- package/scripts/tests/render-report.test.cjs +180 -0
- package/scripts/tests/run-bug-hunter.test.cjs +686 -2
- package/scripts/tests/security-skills-integration.test.cjs +29 -0
- package/scripts/tests/skills-packaging.test.cjs +30 -0
- package/scripts/tests/worktree-harvest.test.cjs +67 -1
- package/scripts/worktree-harvest.cjs +62 -9
- package/skills/README.md +19 -0
- package/skills/commit-security-scan/SKILL.md +63 -0
- package/skills/security-review/SKILL.md +57 -0
- package/skills/threat-model-generation/SKILL.md +47 -0
- package/skills/vulnerability-validation/SKILL.md +59 -0
- package/templates/subagent-wrapper.md +12 -3
- package/modes/_dispatch.md +0 -121
package/scripts/fix-lock.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const fs = require('fs');
|
|
4
5
|
const os = require('os');
|
|
5
6
|
const path = require('path');
|
|
@@ -7,9 +8,10 @@ const path = require('path');
|
|
|
7
8
|
function usage() {
|
|
8
9
|
console.error('Usage:');
|
|
9
10
|
console.error(' fix-lock.cjs acquire <lockPath> [ttlSeconds]');
|
|
10
|
-
console.error(' fix-lock.cjs renew <lockPath>');
|
|
11
|
-
console.error(' fix-lock.cjs release <lockPath>');
|
|
11
|
+
console.error(' fix-lock.cjs renew <lockPath> <ownerToken>');
|
|
12
|
+
console.error(' fix-lock.cjs release <lockPath> <ownerToken>');
|
|
12
13
|
console.error(' fix-lock.cjs status <lockPath> [ttlSeconds]');
|
|
14
|
+
console.error(' Note: acquire returns lock.ownerToken; pass it to renew/release.');
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
function nowMs() {
|
|
@@ -24,7 +26,11 @@ function readLock(lockPath) {
|
|
|
24
26
|
if (!fs.existsSync(lockPath)) {
|
|
25
27
|
return null;
|
|
26
28
|
}
|
|
27
|
-
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
function pidAlive(pid) {
|
|
@@ -47,67 +53,131 @@ function lockIsStale(lockData, ttlSeconds) {
|
|
|
47
53
|
return expired;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
function writeLock(lockPath) {
|
|
56
|
+
function writeLock(lockPath, ownerTokenRaw, exclusive = true) {
|
|
51
57
|
ensureParent(lockPath);
|
|
52
58
|
const lockData = {
|
|
53
59
|
pid: process.pid,
|
|
54
60
|
host: os.hostname(),
|
|
55
61
|
cwd: process.cwd(),
|
|
62
|
+
ownerToken: ownerTokenRaw || crypto.randomUUID(),
|
|
56
63
|
createdAtMs: nowMs(),
|
|
57
64
|
createdAt: new Date().toISOString()
|
|
58
65
|
};
|
|
59
|
-
fs.
|
|
66
|
+
const fd = fs.openSync(lockPath, exclusive ? 'wx' : 'w');
|
|
67
|
+
try {
|
|
68
|
+
fs.writeFileSync(fd, `${JSON.stringify(lockData, null, 2)}\n`, 'utf8');
|
|
69
|
+
} finally {
|
|
70
|
+
fs.closeSync(fd);
|
|
71
|
+
}
|
|
60
72
|
return lockData;
|
|
61
73
|
}
|
|
62
74
|
|
|
63
|
-
function
|
|
75
|
+
function assertOwner(existing, ownerToken) {
|
|
76
|
+
if (!existing || !existing.ownerToken) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return ownerToken === existing.ownerToken;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function renew(lockPath, ownerToken) {
|
|
64
83
|
const existing = readLock(lockPath);
|
|
65
84
|
if (!existing) {
|
|
66
85
|
console.log(JSON.stringify({ ok: false, renewed: false, reason: 'no-lock' }, null, 2));
|
|
67
86
|
process.exit(1);
|
|
68
87
|
return;
|
|
69
88
|
}
|
|
89
|
+
if (!assertOwner(existing, ownerToken)) {
|
|
90
|
+
console.log(JSON.stringify({ ok: false, renewed: false, reason: 'lock-owner-mismatch' }, null, 2));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
70
94
|
existing.createdAtMs = nowMs();
|
|
71
95
|
existing.renewedAt = new Date().toISOString();
|
|
72
|
-
|
|
96
|
+
const tempPath = `${lockPath}.${process.pid}.tmp`;
|
|
97
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(existing, null, 2)}\n`, 'utf8');
|
|
98
|
+
fs.renameSync(tempPath, lockPath);
|
|
73
99
|
console.log(JSON.stringify({ ok: true, renewed: true, lock: existing }, null, 2));
|
|
74
100
|
}
|
|
75
101
|
|
|
76
102
|
function acquire(lockPath, ttlSeconds) {
|
|
77
103
|
const existing = readLock(lockPath);
|
|
78
104
|
if (!existing) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
105
|
+
if (fs.existsSync(lockPath)) {
|
|
106
|
+
fs.unlinkSync(lockPath);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const lockData = writeLock(lockPath);
|
|
110
|
+
console.log(JSON.stringify({ ok: true, acquired: true, lock: lockData }, null, 2));
|
|
111
|
+
return;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error && error.code === 'EEXIST') {
|
|
114
|
+
const current = readLock(lockPath);
|
|
115
|
+
console.log(JSON.stringify({
|
|
116
|
+
ok: false,
|
|
117
|
+
acquired: false,
|
|
118
|
+
reason: 'lock-held',
|
|
119
|
+
lock: current
|
|
120
|
+
}, null, 2));
|
|
121
|
+
process.exit(1);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
82
126
|
}
|
|
83
127
|
|
|
84
|
-
|
|
128
|
+
const stale = lockIsStale(existing, ttlSeconds);
|
|
129
|
+
const ownerAlive = typeof existing.pid === 'number' ? pidAlive(existing.pid) : false;
|
|
130
|
+
|
|
131
|
+
if (!stale || ownerAlive) {
|
|
85
132
|
console.log(JSON.stringify({
|
|
86
133
|
ok: false,
|
|
87
134
|
acquired: false,
|
|
88
|
-
reason: 'lock-held',
|
|
135
|
+
reason: ownerAlive ? 'lock-held-by-live-owner' : 'lock-held',
|
|
136
|
+
stale,
|
|
137
|
+
ownerAlive,
|
|
89
138
|
lock: existing
|
|
90
139
|
}, null, 2));
|
|
91
140
|
process.exit(1);
|
|
92
141
|
}
|
|
93
142
|
|
|
94
143
|
fs.unlinkSync(lockPath);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
144
|
+
try {
|
|
145
|
+
const lockData = writeLock(lockPath);
|
|
146
|
+
console.log(JSON.stringify({
|
|
147
|
+
ok: true,
|
|
148
|
+
acquired: true,
|
|
149
|
+
recoveredFromStaleLock: true,
|
|
150
|
+
previousLock: existing,
|
|
151
|
+
lock: lockData
|
|
152
|
+
}, null, 2));
|
|
153
|
+
return;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error && error.code === 'EEXIST') {
|
|
156
|
+
const current = readLock(lockPath);
|
|
157
|
+
console.log(JSON.stringify({
|
|
158
|
+
ok: false,
|
|
159
|
+
acquired: false,
|
|
160
|
+
reason: 'lock-held',
|
|
161
|
+
lock: current
|
|
162
|
+
}, null, 2));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
103
168
|
}
|
|
104
169
|
|
|
105
|
-
function release(lockPath) {
|
|
170
|
+
function release(lockPath, ownerToken) {
|
|
106
171
|
const existing = readLock(lockPath);
|
|
107
172
|
if (!existing) {
|
|
108
173
|
console.log(JSON.stringify({ ok: true, released: false, reason: 'no-lock' }, null, 2));
|
|
109
174
|
return;
|
|
110
175
|
}
|
|
176
|
+
if (!assertOwner(existing, ownerToken)) {
|
|
177
|
+
console.log(JSON.stringify({ ok: false, released: false, reason: 'lock-owner-mismatch' }, null, 2));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
111
181
|
fs.unlinkSync(lockPath);
|
|
112
182
|
console.log(JSON.stringify({ ok: true, released: true, previousLock: existing }, null, 2));
|
|
113
183
|
}
|
|
@@ -130,12 +200,12 @@ function status(lockPath, ttlSeconds) {
|
|
|
130
200
|
}
|
|
131
201
|
|
|
132
202
|
function main() {
|
|
133
|
-
const [command, lockPath,
|
|
203
|
+
const [command, lockPath, rawArg] = process.argv.slice(2);
|
|
134
204
|
if (!command || !lockPath) {
|
|
135
205
|
usage();
|
|
136
206
|
process.exit(1);
|
|
137
207
|
}
|
|
138
|
-
const ttlParsed = Number.parseInt(
|
|
208
|
+
const ttlParsed = Number.parseInt(rawArg || '', 10);
|
|
139
209
|
const ttlSeconds = Number.isInteger(ttlParsed) && ttlParsed > 0 ? ttlParsed : 1800;
|
|
140
210
|
|
|
141
211
|
if (command === 'acquire') {
|
|
@@ -143,11 +213,11 @@ function main() {
|
|
|
143
213
|
return;
|
|
144
214
|
}
|
|
145
215
|
if (command === 'renew') {
|
|
146
|
-
renew(lockPath);
|
|
216
|
+
renew(lockPath, rawArg);
|
|
147
217
|
return;
|
|
148
218
|
}
|
|
149
219
|
if (command === 'release') {
|
|
150
|
-
release(lockPath);
|
|
220
|
+
release(lockPath, rawArg);
|
|
151
221
|
return;
|
|
152
222
|
}
|
|
153
223
|
if (command === 'status') {
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const {
|
|
6
|
+
createSchemaRef,
|
|
7
|
+
validateSchemaRef
|
|
8
|
+
} = require('./schema-runtime.cjs');
|
|
5
9
|
|
|
6
10
|
const REQUIRED_BY_ROLE = {
|
|
7
11
|
recon: ['skillDir', 'targetFiles', 'outputSchema'],
|
|
@@ -16,20 +20,20 @@ const TEMPLATES = {
|
|
|
16
20
|
recon: {
|
|
17
21
|
skillDir: '/absolute/path/to/bug-hunter',
|
|
18
22
|
targetFiles: ['src/example.ts'],
|
|
19
|
-
outputSchema:
|
|
23
|
+
outputSchema: createSchemaRef('recon')
|
|
20
24
|
},
|
|
21
25
|
'triage-hunter': {
|
|
22
26
|
skillDir: '/absolute/path/to/bug-hunter',
|
|
23
27
|
targetFiles: ['src/example.ts'],
|
|
24
28
|
techStack: { framework: '', auth: '', database: '', dependencies: [] },
|
|
25
|
-
outputSchema:
|
|
29
|
+
outputSchema: createSchemaRef('findings')
|
|
26
30
|
},
|
|
27
31
|
hunter: {
|
|
28
32
|
skillDir: '/absolute/path/to/bug-hunter',
|
|
29
33
|
targetFiles: ['src/example.ts'],
|
|
30
34
|
riskMap: { critical: [], high: [], medium: [], contextOnly: [] },
|
|
31
35
|
techStack: { framework: '', auth: '', database: '', dependencies: [] },
|
|
32
|
-
outputSchema:
|
|
36
|
+
outputSchema: createSchemaRef('findings')
|
|
33
37
|
},
|
|
34
38
|
skeptic: {
|
|
35
39
|
skillDir: '/absolute/path/to/bug-hunter',
|
|
@@ -46,13 +50,24 @@ const TEMPLATES = {
|
|
|
46
50
|
}
|
|
47
51
|
],
|
|
48
52
|
techStack: { framework: '', auth: '', database: '', dependencies: [] },
|
|
49
|
-
outputSchema:
|
|
53
|
+
outputSchema: createSchemaRef('skeptic')
|
|
50
54
|
},
|
|
51
55
|
referee: {
|
|
52
56
|
skillDir: '/absolute/path/to/bug-hunter',
|
|
53
|
-
findings: [{
|
|
57
|
+
findings: [{
|
|
58
|
+
bugId: 'BUG-1',
|
|
59
|
+
severity: 'Critical',
|
|
60
|
+
category: 'security',
|
|
61
|
+
file: 'src/example.ts',
|
|
62
|
+
lines: '10-15',
|
|
63
|
+
claim: 'One-sentence description of the bug',
|
|
64
|
+
evidence: 'Exact code quote from the file',
|
|
65
|
+
runtimeTrigger: 'Specific scenario that triggers this bug',
|
|
66
|
+
crossReferences: ['Single file'],
|
|
67
|
+
confidenceScore: 92
|
|
68
|
+
}],
|
|
54
69
|
skepticResults: { accepted: ['BUG-1'], disproved: [], details: [] },
|
|
55
|
-
outputSchema:
|
|
70
|
+
outputSchema: createSchemaRef('referee')
|
|
56
71
|
},
|
|
57
72
|
fixer: {
|
|
58
73
|
skillDir: '/absolute/path/to/bug-hunter',
|
|
@@ -67,7 +82,7 @@ const TEMPLATES = {
|
|
|
67
82
|
}
|
|
68
83
|
],
|
|
69
84
|
techStack: { framework: '', auth: '', database: '', dependencies: [] },
|
|
70
|
-
outputSchema:
|
|
85
|
+
outputSchema: createSchemaRef('fix-report')
|
|
71
86
|
}
|
|
72
87
|
};
|
|
73
88
|
|
|
@@ -130,9 +145,8 @@ function validate(role, payload) {
|
|
|
130
145
|
}
|
|
131
146
|
|
|
132
147
|
if ('outputSchema' in payload) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
148
|
+
const schemaValidation = validateSchemaRef(payload.outputSchema);
|
|
149
|
+
errors.push(...schemaValidation.errors);
|
|
136
150
|
}
|
|
137
151
|
|
|
138
152
|
return {
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const childProcess = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function usage() {
|
|
7
|
+
console.error('Usage:');
|
|
8
|
+
console.error(' pr-scope.cjs resolve <current|recent|pr-number> [--repo-root <path>] [--base <branch>] [--gh-bin <path>] [--git-bin <path>]');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseOptions(argv) {
|
|
12
|
+
const options = {};
|
|
13
|
+
let index = 0;
|
|
14
|
+
while (index < argv.length) {
|
|
15
|
+
const token = argv[index];
|
|
16
|
+
if (!token.startsWith('--')) {
|
|
17
|
+
index += 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const key = token.slice(2);
|
|
21
|
+
const value = argv[index + 1];
|
|
22
|
+
if (!value || value.startsWith('--')) {
|
|
23
|
+
options[key] = 'true';
|
|
24
|
+
index += 1;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
options[key] = value;
|
|
28
|
+
index += 2;
|
|
29
|
+
}
|
|
30
|
+
return options;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function runJson(bin, args, cwd) {
|
|
34
|
+
const result = childProcess.spawnSync(bin, args, {
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
cwd
|
|
37
|
+
});
|
|
38
|
+
if (result.status !== 0) {
|
|
39
|
+
const stderr = (result.stderr || '').trim();
|
|
40
|
+
const stdout = (result.stdout || '').trim();
|
|
41
|
+
throw new Error(stderr || stdout || `${bin} ${args.join(' ')} failed`);
|
|
42
|
+
}
|
|
43
|
+
const output = (result.stdout || '').trim();
|
|
44
|
+
return output ? JSON.parse(output) : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function runLines(bin, args, cwd) {
|
|
48
|
+
const result = childProcess.spawnSync(bin, args, {
|
|
49
|
+
encoding: 'utf8',
|
|
50
|
+
cwd
|
|
51
|
+
});
|
|
52
|
+
if (result.status !== 0) {
|
|
53
|
+
const stderr = (result.stderr || '').trim();
|
|
54
|
+
const stdout = (result.stdout || '').trim();
|
|
55
|
+
throw new Error(stderr || stdout || `${bin} ${args.join(' ')} failed`);
|
|
56
|
+
}
|
|
57
|
+
return (result.stdout || '')
|
|
58
|
+
.split(/\r?\n/)
|
|
59
|
+
.map((line) => line.trim())
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ghMetadataSelector(selector) {
|
|
64
|
+
if (selector === 'current') {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
return [String(selector)];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolveWithGh({ selector, ghBin, cwd }) {
|
|
71
|
+
if (selector === 'recent') {
|
|
72
|
+
const list = runJson(ghBin, [
|
|
73
|
+
'pr',
|
|
74
|
+
'list',
|
|
75
|
+
'--limit',
|
|
76
|
+
'1',
|
|
77
|
+
'--state',
|
|
78
|
+
'open',
|
|
79
|
+
'--json',
|
|
80
|
+
'number,title,headRefName,baseRefName,url'
|
|
81
|
+
], cwd);
|
|
82
|
+
const pr = Array.isArray(list) ? list[0] : null;
|
|
83
|
+
if (!pr) {
|
|
84
|
+
throw new Error('No recent pull requests found');
|
|
85
|
+
}
|
|
86
|
+
const changedFiles = runLines(ghBin, ['pr', 'diff', String(pr.number), '--name-only'], cwd);
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
source: 'gh',
|
|
90
|
+
selector,
|
|
91
|
+
pr,
|
|
92
|
+
changedFiles
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const pr = runJson(ghBin, [
|
|
97
|
+
'pr',
|
|
98
|
+
'view',
|
|
99
|
+
...ghMetadataSelector(selector),
|
|
100
|
+
'--json',
|
|
101
|
+
'number,title,headRefName,baseRefName,url'
|
|
102
|
+
], cwd);
|
|
103
|
+
const changedFiles = runLines(ghBin, ['pr', 'diff', ...ghMetadataSelector(selector), '--name-only'], cwd);
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
source: 'gh',
|
|
107
|
+
selector,
|
|
108
|
+
pr,
|
|
109
|
+
changedFiles
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveDefaultBaseBranch({ gitBin, cwd, explicitBase }) {
|
|
114
|
+
if (explicitBase) {
|
|
115
|
+
return { baseRefName: explicitBase, diffBaseRef: explicitBase };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const symbolicRef = runLines(gitBin, ['symbolic-ref', 'refs/remotes/origin/HEAD'], cwd)[0];
|
|
119
|
+
const match = symbolicRef && symbolicRef.match(/^refs\/remotes\/origin\/(.+)$/);
|
|
120
|
+
if (match && match[1]) {
|
|
121
|
+
return { baseRefName: match[1], diffBaseRef: `origin/${match[1]}` };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
throw new Error('Unable to determine default base branch for git fallback');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function resolveWithGitFallback({ gitBin, cwd, base }) {
|
|
128
|
+
const headRefName = runLines(gitBin, ['rev-parse', '--abbrev-ref', 'HEAD'], cwd)[0];
|
|
129
|
+
const { baseRefName, diffBaseRef } = resolveDefaultBaseBranch({ gitBin, cwd, explicitBase: base });
|
|
130
|
+
const changedFiles = runLines(gitBin, ['diff', '--name-only', `${diffBaseRef}...${headRefName}`], cwd);
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
source: 'git',
|
|
134
|
+
selector: 'current',
|
|
135
|
+
pr: {
|
|
136
|
+
number: null,
|
|
137
|
+
title: `Current branch diff (${headRefName} vs ${baseRefName})`,
|
|
138
|
+
headRefName,
|
|
139
|
+
baseRefName,
|
|
140
|
+
url: null
|
|
141
|
+
},
|
|
142
|
+
changedFiles
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveScope({ selector, options }) {
|
|
147
|
+
const cwd = path.resolve(options['repo-root'] || process.cwd());
|
|
148
|
+
const ghBin = options['gh-bin'] || process.env.BUG_HUNTER_GH_BIN || 'gh';
|
|
149
|
+
const gitBin = options['git-bin'] || process.env.BUG_HUNTER_GIT_BIN || 'git';
|
|
150
|
+
const base = options.base || null;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
return resolveWithGh({ selector, ghBin, cwd });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (selector !== 'current') {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
const fallback = resolveWithGitFallback({ gitBin, cwd, base });
|
|
159
|
+
fallback.fallbackReason = error instanceof Error ? error.message : String(error);
|
|
160
|
+
return fallback;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function main() {
|
|
165
|
+
const [command, selector, ...rest] = process.argv.slice(2);
|
|
166
|
+
if (command !== 'resolve' || !selector) {
|
|
167
|
+
usage();
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
const options = parseOptions(rest);
|
|
171
|
+
const result = resolveScope({ selector, options });
|
|
172
|
+
console.log(JSON.stringify(result, null, 2));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
main();
|
|
177
|
+
} catch (error) {
|
|
178
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
179
|
+
console.error(message);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* prepublish-guard.cjs
|
|
6
|
+
*
|
|
7
|
+
* Blocks `npm publish` unless:
|
|
8
|
+
* 1. Git working tree is clean (no uncommitted changes)
|
|
9
|
+
* 2. Current HEAD is pushed to origin (no unpushed commits)
|
|
10
|
+
* 3. package.json version matches the git tag (if tag exists)
|
|
11
|
+
*
|
|
12
|
+
* This prevents the "published to npm but forgot to commit/push" problem.
|
|
13
|
+
* Bypass with: SKIP_PREPUBLISH_GUARD=1 npm publish
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
if (process.env.SKIP_PREPUBLISH_GUARD === '1') {
|
|
19
|
+
console.log('⚠️ prepublish-guard: SKIPPED (SKIP_PREPUBLISH_GUARD=1)');
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const run = (cmd) => execSync(cmd, { encoding: 'utf8' }).trim();
|
|
24
|
+
|
|
25
|
+
const errors = [];
|
|
26
|
+
|
|
27
|
+
// 1. Check for uncommitted changes
|
|
28
|
+
try {
|
|
29
|
+
const status = run('git status --porcelain');
|
|
30
|
+
if (status) {
|
|
31
|
+
errors.push(
|
|
32
|
+
'❌ Uncommitted changes detected. Commit or stash before publishing.\n' +
|
|
33
|
+
status.split('\n').map(l => ` ${l}`).join('\n')
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
errors.push('❌ Not a git repository or git not available.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. Check for unpushed commits
|
|
41
|
+
try {
|
|
42
|
+
const unpushed = run('git log --oneline origin/main..HEAD 2>/dev/null');
|
|
43
|
+
if (unpushed) {
|
|
44
|
+
errors.push(
|
|
45
|
+
'❌ Unpushed commits. Run `git push` before publishing.\n' +
|
|
46
|
+
unpushed.split('\n').map(l => ` ${l}`).join('\n')
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// If origin/main doesn't exist, skip this check
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. Version/tag consistency check
|
|
54
|
+
try {
|
|
55
|
+
const version = require('../package.json').version;
|
|
56
|
+
const tagExists = (() => {
|
|
57
|
+
try { run(`git rev-parse v${version} 2>/dev/null`); return true; } catch { return false; }
|
|
58
|
+
})();
|
|
59
|
+
if (tagExists) {
|
|
60
|
+
const tagCommit = run(`git rev-parse v${version}`);
|
|
61
|
+
const headCommit = run('git rev-parse HEAD');
|
|
62
|
+
if (tagCommit !== headCommit) {
|
|
63
|
+
errors.push(
|
|
64
|
+
`❌ Tag v${version} exists but points to a different commit.\n` +
|
|
65
|
+
` Tag: ${tagCommit.slice(0, 8)}\n` +
|
|
66
|
+
` HEAD: ${headCommit.slice(0, 8)}\n` +
|
|
67
|
+
` Bump the version or move the tag.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Non-fatal
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (errors.length > 0) {
|
|
76
|
+
console.error('\n🛑 prepublish-guard: publish blocked\n');
|
|
77
|
+
errors.forEach(e => console.error(e + '\n'));
|
|
78
|
+
console.error('Bypass with: SKIP_PREPUBLISH_GUARD=1 npm publish\n');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log('✅ prepublish-guard: clean tree, all pushed — safe to publish');
|