@ikunin/sprintpilot 1.0.1 → 1.0.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 +2 -2
- package/_Sprintpilot/lib/runtime/args.js +0 -2
- package/_Sprintpilot/lib/runtime/git.js +0 -2
- package/_Sprintpilot/lib/runtime/http.js +12 -5
- package/_Sprintpilot/lib/runtime/log.js +0 -2
- package/_Sprintpilot/lib/runtime/secrets.js +14 -16
- package/_Sprintpilot/lib/runtime/spawn.js +21 -8
- package/_Sprintpilot/lib/runtime/text.js +0 -2
- package/_Sprintpilot/lib/runtime/yaml-lite.js +9 -5
- package/_Sprintpilot/manifest.yaml +1 -1
- package/_Sprintpilot/scripts/create-pr.js +76 -38
- package/_Sprintpilot/scripts/detect-platform.js +35 -10
- package/_Sprintpilot/scripts/health-check.js +17 -8
- package/_Sprintpilot/scripts/lint-changed.js +35 -16
- package/_Sprintpilot/scripts/lock.js +22 -6
- package/_Sprintpilot/scripts/sanitize-branch.js +4 -2
- package/_Sprintpilot/scripts/stage-and-commit.js +15 -7
- package/_Sprintpilot/scripts/sync-status.js +16 -6
- package/bin/sprintpilot.js +11 -4
- package/lib/commands/check-update.js +0 -2
- package/lib/commands/install.js +139 -49
- package/lib/commands/uninstall.js +21 -11
- package/lib/core/bmad-config.js +0 -2
- package/lib/core/file-ops.js +6 -6
- package/lib/core/gitignore.js +0 -2
- package/lib/core/markers.js +5 -3
- package/lib/core/tool-registry.js +19 -21
- package/lib/core/update-check.js +0 -2
- package/lib/core/v1-detect.js +0 -2
- package/lib/prompts.js +0 -2
- package/lib/substitute.js +1 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Sprintpilot is an autonomous delivery addon **compatible with [BMad Method](http
|
|
|
33
33
|
|
|
34
34
|
BMad Method provides a structured development workflow with 50+ skills and agent personas. But using it manually means invoking each skill one at a time, navigating menus, making routine decisions, and handling git operations yourself. For a sprint with 10 stories across 3 epics, that's dozens of manual steps, context switches, and session restarts.
|
|
35
35
|
|
|
36
|
-
## The Solution:
|
|
36
|
+
## The Solution: Sprint Autopilot
|
|
37
37
|
|
|
38
38
|
```
|
|
39
39
|
/sprint-autopilot-on
|
|
@@ -148,7 +148,7 @@ Output files (`_bmad-output/codebase-analysis/`):
|
|
|
148
148
|
| `concerns-analysis.md` | TODOs/FIXMEs, security issues, dead code, deprecated patterns, error handling gaps |
|
|
149
149
|
| `integrations-analysis.md` | External APIs, databases, message queues, cloud services, env vars |
|
|
150
150
|
|
|
151
|
-
Scanned file types: TypeScript, JavaScript, Python, Java, Go, Rust, Ruby, C#, SQL, PL/SQL (`.sps`, `.spb`), XML, Shell.
|
|
151
|
+
Scanned file types: TypeScript, JavaScript, Python, Java, Go, Rust, Ruby, C, C++, C#, SQL, PL/SQL (`.sps`, `.spb`), XML, Shell.
|
|
152
152
|
|
|
153
153
|
**`/sprintpilot-assess`** — 3 parallel agents produce actionable findings:
|
|
154
154
|
- Dependency Auditor (CVEs, outdated packages, upgrade paths)
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
1
|
const https = require('node:https');
|
|
4
2
|
const http = require('node:http');
|
|
5
3
|
const { URL } = require('node:url');
|
|
@@ -22,7 +20,12 @@ function postJson(urlStr, body, { headers = {}, timeoutMs = 15_000 } = {}) {
|
|
|
22
20
|
// can fire on the size-cap abort path (we call req.destroy(err)). Without
|
|
23
21
|
// this, the observed error message becomes non-deterministic.
|
|
24
22
|
let settled = false;
|
|
25
|
-
const done = (fn, val) => {
|
|
23
|
+
const done = (fn, val) => {
|
|
24
|
+
if (!settled) {
|
|
25
|
+
settled = true;
|
|
26
|
+
fn(val);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
26
29
|
const ok = (val) => done(resolve, val);
|
|
27
30
|
const fail = (err) => done(reject, err);
|
|
28
31
|
|
|
@@ -78,11 +81,15 @@ function postJson(urlStr, body, { headers = {}, timeoutMs = 15_000 } = {}) {
|
|
|
78
81
|
if (aborted) return; // error path handles it
|
|
79
82
|
const text = Buffer.concat(chunks).toString('utf8');
|
|
80
83
|
let json = null;
|
|
81
|
-
try {
|
|
84
|
+
try {
|
|
85
|
+
json = JSON.parse(text);
|
|
86
|
+
} catch {
|
|
87
|
+
/* non-json */
|
|
88
|
+
}
|
|
82
89
|
ok({ statusCode: res.statusCode, body: text, json });
|
|
83
90
|
});
|
|
84
91
|
res.on('error', fail);
|
|
85
|
-
}
|
|
92
|
+
},
|
|
86
93
|
);
|
|
87
94
|
req.on('timeout', () => {
|
|
88
95
|
req.destroy(new Error(`HTTP timeout after ${timeoutMs}ms`));
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
1
|
const fs = require('node:fs');
|
|
4
2
|
|
|
5
3
|
// Keyword-based fuzzy matches (high false-positive rate, but useful as a
|
|
@@ -10,20 +8,20 @@ const SECRET_KEYWORD = /API_KEY|SECRET|TOKEN|PASSWORD|aws_access|private_key/i;
|
|
|
10
8
|
// secrets actually look like. Matching here is much less noisy than the
|
|
11
9
|
// keyword list above.
|
|
12
10
|
const SECRET_FORMATS = [
|
|
13
|
-
/\bAKIA[0-9A-Z]{16}\b/,
|
|
14
|
-
/\bASIA[0-9A-Z]{16}\b/,
|
|
15
|
-
/\bghp_[A-Za-z0-9]{30,}\b/,
|
|
16
|
-
/\bgho_[A-Za-z0-9]{30,}\b/,
|
|
17
|
-
/\bghu_[A-Za-z0-9]{30,}\b/,
|
|
18
|
-
/\bghs_[A-Za-z0-9]{30,}\b/,
|
|
19
|
-
/\bghr_[A-Za-z0-9]{30,}\b/,
|
|
20
|
-
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/,
|
|
21
|
-
/\bsk-[A-Za-z0-9_-]{20,}\b/,
|
|
22
|
-
/\bsk_live_[A-Za-z0-9]{20,}\b/,
|
|
23
|
-
/\bsk_test_[A-Za-z0-9]{20,}\b/,
|
|
24
|
-
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/,
|
|
25
|
-
/\bAIza[0-9A-Za-z_-]{35,99}\b/,
|
|
26
|
-
/-----BEGIN [A-Z ]*PRIVATE KEY-----/,
|
|
11
|
+
/\bAKIA[0-9A-Z]{16}\b/, // AWS Access Key ID
|
|
12
|
+
/\bASIA[0-9A-Z]{16}\b/, // AWS temporary Access Key ID
|
|
13
|
+
/\bghp_[A-Za-z0-9]{30,}\b/, // GitHub personal access token
|
|
14
|
+
/\bgho_[A-Za-z0-9]{30,}\b/, // GitHub OAuth token
|
|
15
|
+
/\bghu_[A-Za-z0-9]{30,}\b/, // GitHub user-to-server token
|
|
16
|
+
/\bghs_[A-Za-z0-9]{30,}\b/, // GitHub server-to-server token
|
|
17
|
+
/\bghr_[A-Za-z0-9]{30,}\b/, // GitHub refresh token
|
|
18
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/, // GitHub fine-grained PAT
|
|
19
|
+
/\bsk-[A-Za-z0-9_-]{20,}\b/, // OpenAI / Anthropic-like
|
|
20
|
+
/\bsk_live_[A-Za-z0-9]{20,}\b/, // Stripe live secret
|
|
21
|
+
/\bsk_test_[A-Za-z0-9]{20,}\b/, // Stripe test secret
|
|
22
|
+
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, // Slack tokens
|
|
23
|
+
/\bAIza[0-9A-Za-z_-]{35,99}\b/, // Google API key (standard is 39 chars = AIza + 35)
|
|
24
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM private key header
|
|
27
25
|
];
|
|
28
26
|
|
|
29
27
|
// Deprecated: callers should use `matchesSecret(line)` which applies both
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
1
|
const child = require('node:child_process');
|
|
4
2
|
|
|
5
3
|
function run(file, args, { cwd, timeoutMs = 30_000, input, env } = {}) {
|
|
@@ -17,7 +15,7 @@ function run(file, args, { cwd, timeoutMs = 30_000, input, env } = {}) {
|
|
|
17
15
|
(err, stdout, stderr) => {
|
|
18
16
|
if (err) {
|
|
19
17
|
const e = new Error(
|
|
20
|
-
`${file} ${args.join(' ')} failed with code ${err.code ?? err.signal ?? 'unknown'}
|
|
18
|
+
`${file} ${args.join(' ')} failed with code ${err.code ?? err.signal ?? 'unknown'}`,
|
|
21
19
|
);
|
|
22
20
|
e.exitCode = typeof err.code === 'number' ? err.code : 1;
|
|
23
21
|
e.signal = err.signal || null;
|
|
@@ -27,16 +25,26 @@ function run(file, args, { cwd, timeoutMs = 30_000, input, env } = {}) {
|
|
|
27
25
|
return reject(e);
|
|
28
26
|
}
|
|
29
27
|
resolve({ stdout: String(stdout || ''), stderr: String(stderr || ''), exitCode: 0 });
|
|
30
|
-
}
|
|
28
|
+
},
|
|
31
29
|
);
|
|
32
30
|
// Rejecting on spawn-time errors (ENOENT etc) ensures the caller sees a
|
|
33
31
|
// rejected Promise rather than hanging, even if `proc.stdin` was never
|
|
34
32
|
// attached. Without this, a missing binary can crash via `proc.stdin.write`.
|
|
35
33
|
proc.on('error', reject);
|
|
36
34
|
if (input !== undefined && proc.stdin) {
|
|
37
|
-
proc.stdin.on('error', () => {
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
proc.stdin.on('error', () => {
|
|
36
|
+
/* ignore EPIPE etc */
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
proc.stdin.write(input);
|
|
40
|
+
} catch {
|
|
41
|
+
/* ignore */
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
proc.stdin.end();
|
|
45
|
+
} catch {
|
|
46
|
+
/* ignore */
|
|
47
|
+
}
|
|
40
48
|
}
|
|
41
49
|
});
|
|
42
50
|
}
|
|
@@ -60,7 +68,12 @@ function runInherit(file, args, { cwd, env, input } = {}) {
|
|
|
60
68
|
proc.on('error', reject);
|
|
61
69
|
proc.on('close', (code) => resolve({ exitCode: code ?? 0 }));
|
|
62
70
|
if (input !== undefined && proc.stdin) {
|
|
63
|
-
try {
|
|
71
|
+
try {
|
|
72
|
+
proc.stdin.write(input);
|
|
73
|
+
proc.stdin.end();
|
|
74
|
+
} catch {
|
|
75
|
+
/* ignore */
|
|
76
|
+
}
|
|
64
77
|
}
|
|
65
78
|
});
|
|
66
79
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
1
|
// Narrow YAML helper covering the addon-owned shape:
|
|
4
2
|
// key: value
|
|
5
3
|
// stories:
|
|
@@ -8,7 +6,7 @@
|
|
|
8
6
|
// field2: value
|
|
9
7
|
// No deep nesting, no anchors, no flow sequences beyond [a,b,c] literal we pass through.
|
|
10
8
|
|
|
11
|
-
const SPECIAL_CHARS = /[:{}
|
|
9
|
+
const SPECIAL_CHARS = /[:{}[\],&*#?|<>=!%@`\n]|^-|^\s|\s$/;
|
|
12
10
|
|
|
13
11
|
// YAML 1.1 reserved literals that parsers interpret as booleans/null when
|
|
14
12
|
// unquoted (e.g. "no" -> false). Must be quoted to round-trip as strings.
|
|
@@ -81,8 +79,14 @@ function replaceStoryBlock(existing, storyKey, newBlock) {
|
|
|
81
79
|
// header is part of this block. Blank lines inside the block count
|
|
82
80
|
// as continuations and are also consumed.
|
|
83
81
|
while (i < lines.length) {
|
|
84
|
-
if (lines[i].length === 0) {
|
|
85
|
-
|
|
82
|
+
if (lines[i].length === 0) {
|
|
83
|
+
i++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (indentOf(lines[i]) > headerIndent) {
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
86
90
|
break;
|
|
87
91
|
}
|
|
88
92
|
// Strip any trailing blanks we may have consumed past the block.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
2
|
|
|
4
3
|
const { parseArgs } = require('../lib/runtime/args');
|
|
5
4
|
const { tryRun, run } = require('../lib/runtime/spawn');
|
|
@@ -9,7 +8,9 @@ const { postJson } = require('../lib/runtime/http');
|
|
|
9
8
|
const log = require('../lib/runtime/log');
|
|
10
9
|
|
|
11
10
|
function help() {
|
|
12
|
-
log.out(
|
|
11
|
+
log.out(
|
|
12
|
+
"Usage: create-pr.js --platform <github|gitlab|bitbucket|gitea|git_only> --branch <name> --base <branch> --title 'title' --body 'body' [--base-url <url>]",
|
|
13
|
+
);
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
async function hasCli(name) {
|
|
@@ -80,12 +81,18 @@ function redactAuth(text) {
|
|
|
80
81
|
return String(text)
|
|
81
82
|
.replace(/("?authorization"?\s*:\s*")[^"]*(")/gi, '$1[REDACTED]$2')
|
|
82
83
|
.replace(/(bearer\s+)\S+/gi, '$1[REDACTED]')
|
|
83
|
-
.replace(
|
|
84
|
+
.replace(
|
|
85
|
+
/("?(?:token|access_token|api_key|private_token)"?\s*[:=]\s*")[^"]*(")/gi,
|
|
86
|
+
'$1[REDACTED]$2',
|
|
87
|
+
);
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
async function main() {
|
|
87
91
|
const { opts } = parseArgs(process.argv.slice(2), { booleanFlags: ['dry-run'] });
|
|
88
|
-
if (opts.help) {
|
|
92
|
+
if (opts.help) {
|
|
93
|
+
help();
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
89
96
|
|
|
90
97
|
const platform = opts.platform;
|
|
91
98
|
const branch = opts.branch;
|
|
@@ -130,13 +137,11 @@ async function main() {
|
|
|
130
137
|
log.out('SKIPPED');
|
|
131
138
|
process.exit(2);
|
|
132
139
|
}
|
|
133
|
-
const r = await tryRun(
|
|
134
|
-
'
|
|
135
|
-
'--base', baseBranch,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
'--body', body,
|
|
139
|
-
], { timeoutMs: 60_000 });
|
|
140
|
+
const r = await tryRun(
|
|
141
|
+
'gh',
|
|
142
|
+
['pr', 'create', '--base', baseBranch, '--head', branch, '--title', title, '--body', body],
|
|
143
|
+
{ timeoutMs: 60_000 },
|
|
144
|
+
);
|
|
140
145
|
const combined = `${r.stdout}${r.stderr}`;
|
|
141
146
|
if (r.exitCode !== 0) {
|
|
142
147
|
log.error(`gh pr create failed: ${combined.trim()}`);
|
|
@@ -152,15 +157,24 @@ async function main() {
|
|
|
152
157
|
log.out('SKIPPED');
|
|
153
158
|
process.exit(2);
|
|
154
159
|
}
|
|
155
|
-
const r = await tryRun(
|
|
156
|
-
'
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
const r = await tryRun(
|
|
161
|
+
'glab',
|
|
162
|
+
[
|
|
163
|
+
'mr',
|
|
164
|
+
'create',
|
|
165
|
+
'--source-branch',
|
|
166
|
+
branch,
|
|
167
|
+
'--target-branch',
|
|
168
|
+
baseBranch,
|
|
169
|
+
'--title',
|
|
170
|
+
title,
|
|
171
|
+
'--description',
|
|
172
|
+
body,
|
|
173
|
+
'--remove-source-branch',
|
|
174
|
+
'--yes',
|
|
175
|
+
],
|
|
176
|
+
{ timeoutMs: 60_000 },
|
|
177
|
+
);
|
|
164
178
|
const combined = `${r.stdout}${r.stderr}`;
|
|
165
179
|
if (r.exitCode !== 0) {
|
|
166
180
|
log.error(`glab mr create failed: ${combined.trim()}`);
|
|
@@ -173,13 +187,22 @@ async function main() {
|
|
|
173
187
|
|
|
174
188
|
if (platform === 'bitbucket') {
|
|
175
189
|
if (await hasCli('bb')) {
|
|
176
|
-
const r = await tryRun(
|
|
177
|
-
'
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
190
|
+
const r = await tryRun(
|
|
191
|
+
'bb',
|
|
192
|
+
[
|
|
193
|
+
'pr',
|
|
194
|
+
'create',
|
|
195
|
+
'--source',
|
|
196
|
+
branch,
|
|
197
|
+
'--destination',
|
|
198
|
+
baseBranch,
|
|
199
|
+
'--title',
|
|
200
|
+
title,
|
|
201
|
+
'--description',
|
|
202
|
+
body,
|
|
203
|
+
],
|
|
204
|
+
{ timeoutMs: 60_000 },
|
|
205
|
+
);
|
|
183
206
|
const combined = `${r.stdout}${r.stderr}`;
|
|
184
207
|
if (r.exitCode !== 0) {
|
|
185
208
|
log.error(`bb pr create failed: ${combined.trim()}`);
|
|
@@ -200,11 +223,14 @@ async function main() {
|
|
|
200
223
|
destination: { branch: { name: baseBranch } },
|
|
201
224
|
description: body,
|
|
202
225
|
},
|
|
203
|
-
{ headers: { Authorization: `Bearer ${process.env.BITBUCKET_TOKEN}` } }
|
|
226
|
+
{ headers: { Authorization: `Bearer ${process.env.BITBUCKET_TOKEN}` } },
|
|
204
227
|
);
|
|
205
228
|
if (res.statusCode === 201) {
|
|
206
229
|
const href = res.json?.links?.html?.href;
|
|
207
|
-
if (href) {
|
|
230
|
+
if (href) {
|
|
231
|
+
log.out(href);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
208
234
|
log.out('CREATED (URL not extracted from response)');
|
|
209
235
|
return;
|
|
210
236
|
}
|
|
@@ -223,13 +249,22 @@ async function main() {
|
|
|
223
249
|
|
|
224
250
|
if (platform === 'gitea') {
|
|
225
251
|
if (await hasCli('tea')) {
|
|
226
|
-
const r = await tryRun(
|
|
227
|
-
'
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
252
|
+
const r = await tryRun(
|
|
253
|
+
'tea',
|
|
254
|
+
[
|
|
255
|
+
'pr',
|
|
256
|
+
'create',
|
|
257
|
+
'--base',
|
|
258
|
+
baseBranch,
|
|
259
|
+
'--head',
|
|
260
|
+
branch,
|
|
261
|
+
'--title',
|
|
262
|
+
title,
|
|
263
|
+
'--description',
|
|
264
|
+
body,
|
|
265
|
+
],
|
|
266
|
+
{ timeoutMs: 60_000 },
|
|
267
|
+
);
|
|
233
268
|
const combined = `${r.stdout}${r.stderr}`;
|
|
234
269
|
if (r.exitCode !== 0) {
|
|
235
270
|
log.error(`tea pr create failed: ${combined.trim()}`);
|
|
@@ -245,11 +280,14 @@ async function main() {
|
|
|
245
280
|
const res = await postJson(
|
|
246
281
|
`${baseUrl.replace(/\/+$/, '')}/api/v1/repos/${ownerRepo}/pulls`,
|
|
247
282
|
{ base: baseBranch, head: branch, title, body },
|
|
248
|
-
{ headers: { Authorization: `token ${process.env.GITEA_TOKEN}` } }
|
|
283
|
+
{ headers: { Authorization: `token ${process.env.GITEA_TOKEN}` } },
|
|
249
284
|
);
|
|
250
285
|
if (res.statusCode === 201) {
|
|
251
286
|
const href = res.json?.html_url;
|
|
252
|
-
if (href) {
|
|
287
|
+
if (href) {
|
|
288
|
+
log.out(href);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
253
291
|
log.out('CREATED (URL not extracted from response)');
|
|
254
292
|
return;
|
|
255
293
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
2
|
|
|
4
3
|
const { parseArgs } = require('../lib/runtime/args');
|
|
5
4
|
const { tryRun } = require('../lib/runtime/spawn');
|
|
@@ -17,7 +16,10 @@ async function hasCli(name) {
|
|
|
17
16
|
|
|
18
17
|
async function main() {
|
|
19
18
|
const { opts } = parseArgs(process.argv.slice(2));
|
|
20
|
-
if (opts.help) {
|
|
19
|
+
if (opts.help) {
|
|
20
|
+
help();
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
21
23
|
const provider = opts.provider || 'auto';
|
|
22
24
|
|
|
23
25
|
if (provider !== 'auto') {
|
|
@@ -45,16 +47,39 @@ async function main() {
|
|
|
45
47
|
|
|
46
48
|
const remote = (await tryGitStdout(['remote', 'get-url', 'origin'])) || '';
|
|
47
49
|
|
|
48
|
-
if (/github\.com[:/]/i.test(remote)) {
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
if (/github\.com[:/]/i.test(remote)) {
|
|
51
|
+
log.out('github');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (/gitlab\./i.test(remote)) {
|
|
55
|
+
log.out('gitlab');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (/bitbucket\.org[:/]/i.test(remote)) {
|
|
59
|
+
log.out('bitbucket');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
51
62
|
|
|
52
|
-
if (hasTea) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
if (hasTea) {
|
|
64
|
+
log.out('gitea');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (hasGh) {
|
|
68
|
+
log.out('github');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (hasGlab) {
|
|
72
|
+
log.out('gitlab');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (hasBb) {
|
|
76
|
+
log.out('bitbucket');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
56
79
|
|
|
57
|
-
log.err(
|
|
80
|
+
log.err(
|
|
81
|
+
"WARN: no platform CLI found (gh, glab, bb, tea) and remote URL didn't match known platforms",
|
|
82
|
+
);
|
|
58
83
|
log.out('git_only');
|
|
59
84
|
}
|
|
60
85
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
2
|
|
|
4
3
|
const fs = require('node:fs');
|
|
5
4
|
const path = require('node:path');
|
|
@@ -10,12 +9,17 @@ const { readStoryField } = require('../lib/runtime/yaml-lite');
|
|
|
10
9
|
const log = require('../lib/runtime/log');
|
|
11
10
|
|
|
12
11
|
function help() {
|
|
13
|
-
log.out(
|
|
12
|
+
log.out(
|
|
13
|
+
'Usage: health-check.js [--worktrees-dir path] [--base-branch main] [--status-file path]',
|
|
14
|
+
);
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
async function main() {
|
|
17
18
|
const { opts } = parseArgs(process.argv.slice(2));
|
|
18
|
-
if (opts.help) {
|
|
19
|
+
if (opts.help) {
|
|
20
|
+
help();
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
19
23
|
const worktreesDir = opts['worktrees-dir'] || '.worktrees';
|
|
20
24
|
const baseBranch = opts['base-branch'] || 'main';
|
|
21
25
|
const statusFile = opts['status-file'] || '';
|
|
@@ -34,13 +38,18 @@ async function main() {
|
|
|
34
38
|
log.err("WARN: no 'origin' remote configured — commit comparison may be inaccurate");
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
let total = 0,
|
|
41
|
+
let total = 0,
|
|
42
|
+
cleanDone = 0,
|
|
43
|
+
committed = 0,
|
|
44
|
+
stale = 0,
|
|
45
|
+
dirty = 0,
|
|
46
|
+
orphan = 0;
|
|
38
47
|
|
|
39
|
-
const statusText =
|
|
40
|
-
? fs.readFileSync(statusFile, 'utf8')
|
|
41
|
-
: null;
|
|
48
|
+
const statusText =
|
|
49
|
+
statusFile && fs.existsSync(statusFile) ? fs.readFileSync(statusFile, 'utf8') : null;
|
|
42
50
|
|
|
43
|
-
const entries = fs
|
|
51
|
+
const entries = fs
|
|
52
|
+
.readdirSync(worktreesDir, { withFileTypes: true })
|
|
44
53
|
.filter((e) => e.isDirectory());
|
|
45
54
|
|
|
46
55
|
for (const entry of entries) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
2
|
|
|
4
3
|
const fs = require('node:fs');
|
|
5
4
|
const path = require('node:path');
|
|
@@ -54,7 +53,9 @@ function localBin(name) {
|
|
|
54
53
|
try {
|
|
55
54
|
const stat = fs.statSync(p);
|
|
56
55
|
if (stat.isFile()) return p;
|
|
57
|
-
} catch {
|
|
56
|
+
} catch {
|
|
57
|
+
/* ignore */
|
|
58
|
+
}
|
|
58
59
|
return null;
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -72,7 +73,8 @@ async function lintLanguage(lang, files) {
|
|
|
72
73
|
if (lang === 'python') {
|
|
73
74
|
if (await hasCli('ruff')) return runLinter('ruff', 'ruff', ['check'], files);
|
|
74
75
|
if (await hasCli('flake8')) return runLinter('flake8', 'flake8', [], files);
|
|
75
|
-
if (await hasCli('pylint'))
|
|
76
|
+
if (await hasCli('pylint'))
|
|
77
|
+
return runLinter('pylint', 'pylint', ['--output-format=text'], files);
|
|
76
78
|
return null;
|
|
77
79
|
}
|
|
78
80
|
if (lang === 'js-ts') {
|
|
@@ -83,15 +85,18 @@ async function lintLanguage(lang, files) {
|
|
|
83
85
|
return null;
|
|
84
86
|
}
|
|
85
87
|
if (lang === 'rust') {
|
|
86
|
-
if (await hasCli('cargo'))
|
|
88
|
+
if (await hasCli('cargo'))
|
|
89
|
+
return runLinter('cargo-clippy', 'cargo', ['clippy', '--message-format=short'], []);
|
|
87
90
|
return null;
|
|
88
91
|
}
|
|
89
92
|
if (lang === 'go') {
|
|
90
|
-
if (await hasCli('golangci-lint'))
|
|
93
|
+
if (await hasCli('golangci-lint'))
|
|
94
|
+
return runLinter('golangci-lint', 'golangci-lint', ['run'], []);
|
|
91
95
|
return null;
|
|
92
96
|
}
|
|
93
97
|
if (lang === 'ruby') {
|
|
94
|
-
if (await hasCli('rubocop'))
|
|
98
|
+
if (await hasCli('rubocop'))
|
|
99
|
+
return runLinter('rubocop', 'rubocop', ['--format', 'simple'], files);
|
|
95
100
|
return null;
|
|
96
101
|
}
|
|
97
102
|
if (lang === 'java') {
|
|
@@ -108,25 +113,35 @@ async function lintLanguage(lang, files) {
|
|
|
108
113
|
return null;
|
|
109
114
|
}
|
|
110
115
|
if (lang === 'c') {
|
|
111
|
-
if (await hasCli('cppcheck'))
|
|
116
|
+
if (await hasCli('cppcheck'))
|
|
117
|
+
return runLinter('cppcheck', 'cppcheck', ['--enable=warning,style'], files);
|
|
112
118
|
if (await hasCli('clang-tidy')) return runLinter('clang-tidy', 'clang-tidy', [], files);
|
|
113
119
|
return null;
|
|
114
120
|
}
|
|
115
121
|
if (lang === 'cpp') {
|
|
116
|
-
if (await hasCli('cppcheck'))
|
|
122
|
+
if (await hasCli('cppcheck'))
|
|
123
|
+
return runLinter('cppcheck', 'cppcheck', ['--enable=warning,style', '--language=c++'], files);
|
|
117
124
|
if (await hasCli('clang-tidy')) return runLinter('clang-tidy', 'clang-tidy', [], files);
|
|
118
125
|
return null;
|
|
119
126
|
}
|
|
120
127
|
if (lang === 'csharp') {
|
|
121
|
-
if (await hasCli('dotnet'))
|
|
128
|
+
if (await hasCli('dotnet'))
|
|
129
|
+
return runLinter(
|
|
130
|
+
'dotnet-format',
|
|
131
|
+
'dotnet',
|
|
132
|
+
['format', '--verify-no-changes', '--diagnostics'],
|
|
133
|
+
[],
|
|
134
|
+
);
|
|
122
135
|
return null;
|
|
123
136
|
}
|
|
124
137
|
if (lang === 'swift') {
|
|
125
|
-
if (await hasCli('swiftlint'))
|
|
138
|
+
if (await hasCli('swiftlint'))
|
|
139
|
+
return runLinter('swiftlint', 'swiftlint', ['lint', '--quiet'], files);
|
|
126
140
|
return null;
|
|
127
141
|
}
|
|
128
142
|
if (lang === 'sql') {
|
|
129
|
-
if (await hasCli('sqlfluff'))
|
|
143
|
+
if (await hasCli('sqlfluff'))
|
|
144
|
+
return runLinter('sqlfluff', 'sqlfluff', ['lint', '--dialect', 'oracle'], files);
|
|
130
145
|
return null;
|
|
131
146
|
}
|
|
132
147
|
if (lang === 'kotlin') {
|
|
@@ -135,7 +150,8 @@ async function lintLanguage(lang, files) {
|
|
|
135
150
|
return null;
|
|
136
151
|
}
|
|
137
152
|
if (lang === 'php') {
|
|
138
|
-
if (await hasCli('phpstan'))
|
|
153
|
+
if (await hasCli('phpstan'))
|
|
154
|
+
return runLinter('phpstan', 'phpstan', ['analyse', '--no-progress'], files);
|
|
139
155
|
if (await hasCli('phpcs')) return runLinter('phpcs', 'phpcs', [], files);
|
|
140
156
|
return null;
|
|
141
157
|
}
|
|
@@ -172,7 +188,10 @@ async function runOverride(name, files) {
|
|
|
172
188
|
|
|
173
189
|
async function main() {
|
|
174
190
|
const { opts } = parseArgs(process.argv.slice(2));
|
|
175
|
-
if (opts.help) {
|
|
191
|
+
if (opts.help) {
|
|
192
|
+
help();
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
176
195
|
|
|
177
196
|
const limit = parseInt(opts.limit || '100', 10);
|
|
178
197
|
const outputFile = opts['output-file'] || '';
|
|
@@ -180,9 +199,9 @@ async function main() {
|
|
|
180
199
|
|
|
181
200
|
const modified = await tryGitStdout(['diff', '--name-only', 'HEAD']);
|
|
182
201
|
const untracked = await tryGitStdout(['ls-files', '--others', '--exclude-standard']);
|
|
183
|
-
const all = Array.from(
|
|
184
|
-
|
|
185
|
-
|
|
202
|
+
const all = Array.from(new Set([...splitLines(modified || ''), ...splitLines(untracked || '')]))
|
|
203
|
+
.filter(Boolean)
|
|
204
|
+
.sort();
|
|
186
205
|
|
|
187
206
|
if (all.length === 0) {
|
|
188
207
|
log.out('No changed files to lint');
|