@ikunin/sprintpilot 1.0.1 → 1.0.3
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/scan.js +457 -0
- package/_Sprintpilot/scripts/stage-and-commit.js +15 -7
- package/_Sprintpilot/scripts/sync-status.js +16 -6
- package/_Sprintpilot/skills/sprint-autopilot-on/workflow.md +62 -31
- package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/architecture-mapper.md +22 -15
- package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/concerns-hunter.md +47 -24
- package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/integration-mapper.md +21 -21
- package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/quality-assessor.md +34 -22
- package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/stack-analyzer.md +41 -43
- package/_Sprintpilot/skills/sprintpilot-codebase-map/workflow.md +1 -1
- 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
|
@@ -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');
|
|
@@ -88,12 +87,20 @@ function writeLockExclusive(lockFile, id) {
|
|
|
88
87
|
fs.writeSync(fd, content, 0, 'utf8');
|
|
89
88
|
wrote = true;
|
|
90
89
|
} finally {
|
|
91
|
-
try {
|
|
90
|
+
try {
|
|
91
|
+
fs.closeSync(fd);
|
|
92
|
+
} catch {
|
|
93
|
+
/* ignore */
|
|
94
|
+
}
|
|
92
95
|
if (!wrote) {
|
|
93
96
|
// writeSync failed (ENOSPC, EIO): leaving an empty lockfile behind
|
|
94
97
|
// would look "corrupt" to the next acquirer and permanently wedge
|
|
95
98
|
// the autopilot. Unlink so the next try can re-create cleanly.
|
|
96
|
-
try {
|
|
99
|
+
try {
|
|
100
|
+
fs.unlinkSync(lockFile);
|
|
101
|
+
} catch {
|
|
102
|
+
/* ignore */
|
|
103
|
+
}
|
|
97
104
|
}
|
|
98
105
|
}
|
|
99
106
|
}
|
|
@@ -143,7 +150,11 @@ function main() {
|
|
|
143
150
|
// of them gets EEXIST.
|
|
144
151
|
const info = readLockInfo(lockFile, staleSeconds);
|
|
145
152
|
if (info.state === 'STALE') {
|
|
146
|
-
try {
|
|
153
|
+
try {
|
|
154
|
+
fs.unlinkSync(lockFile);
|
|
155
|
+
} catch {
|
|
156
|
+
/* ignore */
|
|
157
|
+
}
|
|
147
158
|
try {
|
|
148
159
|
writeLockExclusive(lockFile, id);
|
|
149
160
|
log.out(`ACQUIRED_STALE:${id}`);
|
|
@@ -176,7 +187,11 @@ function main() {
|
|
|
176
187
|
|
|
177
188
|
if (action === 'release') {
|
|
178
189
|
if (fs.existsSync(lockFile)) {
|
|
179
|
-
try {
|
|
190
|
+
try {
|
|
191
|
+
fs.unlinkSync(lockFile);
|
|
192
|
+
} catch {
|
|
193
|
+
/* ignore */
|
|
194
|
+
}
|
|
180
195
|
log.out('RELEASED');
|
|
181
196
|
} else {
|
|
182
197
|
log.out('NO_LOCK');
|
|
@@ -187,7 +202,8 @@ function main() {
|
|
|
187
202
|
if (action === 'status') {
|
|
188
203
|
const info = readLockInfo(lockFile, staleSeconds);
|
|
189
204
|
if (info.state === 'FREE') log.out('Lock: free (no active session)');
|
|
190
|
-
else if (info.state === 'LOCKED')
|
|
205
|
+
else if (info.state === 'LOCKED')
|
|
206
|
+
log.out(`Lock: ACTIVE — session ${info.id}, age ${info.ageMin}m`);
|
|
191
207
|
else log.out(`Lock: STALE — session ${info.id}, age ${info.ageMin}m (will auto-remove)`);
|
|
192
208
|
}
|
|
193
209
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
2
|
|
|
4
3
|
const crypto = require('node:crypto');
|
|
5
4
|
|
|
@@ -54,7 +53,10 @@ async function validateRefFormat(fullName) {
|
|
|
54
53
|
|
|
55
54
|
async function main() {
|
|
56
55
|
const { opts, positional } = parseArgs(process.argv.slice(2));
|
|
57
|
-
if (opts.help) {
|
|
56
|
+
if (opts.help) {
|
|
57
|
+
help();
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
58
60
|
const storyKey = positional[0];
|
|
59
61
|
const prefix = opts.prefix ?? 'story/';
|
|
60
62
|
const maxLength = parseInt(opts['max-length'] || '60', 10);
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cross-platform codebase scanner.
|
|
5
|
+
*
|
|
6
|
+
* Replaces bash pipelines like `find ... -exec wc -l {} + | sort -rn | head -N`
|
|
7
|
+
* so sprintpilot skills work on Windows PowerShell / cmd / Gemini CLI, not just bash.
|
|
8
|
+
*
|
|
9
|
+
* Subcommands:
|
|
10
|
+
* files List files matching include globs, excluding ignore globs.
|
|
11
|
+
* Flags: --include, --exclude, --root, --limit, --count
|
|
12
|
+
* largest Top N files by line count.
|
|
13
|
+
* Flags: --include, --exclude, --root, --limit (default 10)
|
|
14
|
+
* loc Total line count across matched files.
|
|
15
|
+
* Flags: --include, --exclude, --root
|
|
16
|
+
* extensions Extension frequency histogram, descending.
|
|
17
|
+
* Flags: --exclude, --root, --limit (default 20)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { parseArgs } = require('../lib/runtime/args');
|
|
23
|
+
const log = require('../lib/runtime/log');
|
|
24
|
+
|
|
25
|
+
const DEFAULT_EXCLUDES = [
|
|
26
|
+
'node_modules',
|
|
27
|
+
'.git',
|
|
28
|
+
'vendor',
|
|
29
|
+
'target',
|
|
30
|
+
'dist',
|
|
31
|
+
'build',
|
|
32
|
+
'.next',
|
|
33
|
+
'.nuxt',
|
|
34
|
+
'.svelte-kit',
|
|
35
|
+
'coverage',
|
|
36
|
+
'.turbo',
|
|
37
|
+
'.cache',
|
|
38
|
+
'__pycache__',
|
|
39
|
+
'.venv',
|
|
40
|
+
'venv',
|
|
41
|
+
'.worktrees',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function help() {
|
|
45
|
+
log.out(
|
|
46
|
+
'Usage: scan.js <files|largest|loc|extensions> [--include <globs>] [--exclude <globs>] [--root <path>] [--limit <N>] [--count]',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Split a comma-delimited list, but keep commas inside {a,b} brace groups intact.
|
|
51
|
+
function splitList(value) {
|
|
52
|
+
if (value === undefined || value === null || value === true) return [];
|
|
53
|
+
const s = String(value);
|
|
54
|
+
const out = [];
|
|
55
|
+
let buf = '';
|
|
56
|
+
let depth = 0;
|
|
57
|
+
for (let i = 0; i < s.length; i++) {
|
|
58
|
+
const c = s[i];
|
|
59
|
+
if (c === '{') depth++;
|
|
60
|
+
else if (c === '}') depth = Math.max(0, depth - 1);
|
|
61
|
+
if (c === ',' && depth === 0) {
|
|
62
|
+
if (buf.trim()) out.push(buf.trim());
|
|
63
|
+
buf = '';
|
|
64
|
+
} else {
|
|
65
|
+
buf += c;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (buf.trim()) out.push(buf.trim());
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Find matching closing brace, respecting nesting. Returns -1 if unterminated.
|
|
73
|
+
function findBraceClose(glob, start) {
|
|
74
|
+
let depth = 1;
|
|
75
|
+
for (let i = start + 1; i < glob.length; i++) {
|
|
76
|
+
const c = glob[i];
|
|
77
|
+
if (c === '\\' && i + 1 < glob.length) {
|
|
78
|
+
i++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (c === '{') depth++;
|
|
82
|
+
else if (c === '}') {
|
|
83
|
+
depth--;
|
|
84
|
+
if (depth === 0) return i;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return -1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Split a brace group's body on commas at depth 0 (so nested braces stay intact).
|
|
91
|
+
function splitBraceAlts(body) {
|
|
92
|
+
const parts = [];
|
|
93
|
+
let buf = '';
|
|
94
|
+
let depth = 0;
|
|
95
|
+
for (let i = 0; i < body.length; i++) {
|
|
96
|
+
const c = body[i];
|
|
97
|
+
if (c === '\\' && i + 1 < body.length) {
|
|
98
|
+
buf += c + body[i + 1];
|
|
99
|
+
i++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (c === '{') depth++;
|
|
103
|
+
else if (c === '}') depth = Math.max(0, depth - 1);
|
|
104
|
+
if (c === ',' && depth === 0) {
|
|
105
|
+
parts.push(buf);
|
|
106
|
+
buf = '';
|
|
107
|
+
} else {
|
|
108
|
+
buf += c;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
parts.push(buf);
|
|
112
|
+
return parts;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Convert a glob pattern into a RegExp.
|
|
116
|
+
// Supports: * (any chars except /), ** (any chars incl. /), ? (single non-/),
|
|
117
|
+
// {a,b} alternation (nestable), and literal path segments.
|
|
118
|
+
// Matching is against forward-slash paths.
|
|
119
|
+
function globToRegex(glob) {
|
|
120
|
+
let re = '';
|
|
121
|
+
let i = 0;
|
|
122
|
+
while (i < glob.length) {
|
|
123
|
+
const c = glob[i];
|
|
124
|
+
if (c === '\\') {
|
|
125
|
+
if (i + 1 >= glob.length) {
|
|
126
|
+
// Trailing lone backslash — emit as a literal backslash.
|
|
127
|
+
re += '\\\\';
|
|
128
|
+
i++;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// Literal escape: pass the next char through verbatim.
|
|
132
|
+
const next = glob[i + 1];
|
|
133
|
+
re += '.+^$()|[]{}?*\\'.includes(next) ? '\\' + next : next;
|
|
134
|
+
i += 2;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (c === '*') {
|
|
138
|
+
if (glob[i + 1] === '*') {
|
|
139
|
+
re += '.*';
|
|
140
|
+
i += 2;
|
|
141
|
+
if (glob[i] === '/') i++; // consume trailing slash of ** segment
|
|
142
|
+
} else {
|
|
143
|
+
re += '[^/]*';
|
|
144
|
+
i++;
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (c === '?') {
|
|
149
|
+
re += '[^/]';
|
|
150
|
+
i++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (c === '{') {
|
|
154
|
+
const end = findBraceClose(glob, i);
|
|
155
|
+
if (end === -1) {
|
|
156
|
+
// Unterminated brace — treat as literal.
|
|
157
|
+
re += '\\{';
|
|
158
|
+
i++;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const alts = splitBraceAlts(glob.slice(i + 1, end));
|
|
162
|
+
const altRegexes = alts.map((p) => globToRegex(p).source.slice(1, -1));
|
|
163
|
+
re += `(?:${altRegexes.join('|')})`;
|
|
164
|
+
i = end + 1;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if ('.+^$()|[]'.includes(c)) {
|
|
168
|
+
re += '\\' + c;
|
|
169
|
+
i++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
re += c;
|
|
173
|
+
i++;
|
|
174
|
+
}
|
|
175
|
+
return new RegExp('^' + re + '$');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function toPosix(p) {
|
|
179
|
+
return p.split(path.sep).join('/');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Compile a pattern into { raw, re, pathAnchored }.
|
|
183
|
+
// pathAnchored = true if the pattern contains a path separator; such patterns
|
|
184
|
+
// only match the full relative path. Basename-only patterns (no '/') match
|
|
185
|
+
// both the full path and the basename, so "*.ts" works at any depth.
|
|
186
|
+
function compilePatterns(patterns) {
|
|
187
|
+
return patterns.map((p) => ({
|
|
188
|
+
raw: p,
|
|
189
|
+
re: globToRegex(p),
|
|
190
|
+
pathAnchored: p.includes('/'),
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function matchesAny(relPath, compiled) {
|
|
195
|
+
if (compiled.length === 0) return false;
|
|
196
|
+
const basename = relPath.slice(relPath.lastIndexOf('/') + 1);
|
|
197
|
+
for (const { re, pathAnchored } of compiled) {
|
|
198
|
+
if (re.test(relPath)) return true;
|
|
199
|
+
if (!pathAnchored && re.test(basename)) return true;
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isExcludedDir(name, excludeBasenames) {
|
|
205
|
+
return excludeBasenames.has(name);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function matchesExcludePath(relPath, compiled) {
|
|
209
|
+
return matchesAny(relPath, compiled);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Resolve a dirent to { kind: 'file' | 'dir' | 'other' }, following symlinks
|
|
213
|
+
// through stat(). Returns 'other' on broken links or errors.
|
|
214
|
+
function classifyEntry(fullPath, entry) {
|
|
215
|
+
if (entry.isFile()) return 'file';
|
|
216
|
+
if (entry.isDirectory()) return 'dir';
|
|
217
|
+
if (entry.isSymbolicLink()) {
|
|
218
|
+
try {
|
|
219
|
+
const st = fs.statSync(fullPath);
|
|
220
|
+
if (st.isFile()) return 'file';
|
|
221
|
+
if (st.isDirectory()) return 'dir';
|
|
222
|
+
} catch {
|
|
223
|
+
return 'other';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return 'other';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isWithinRoot(real, rootReal) {
|
|
230
|
+
if (real === rootReal) return true;
|
|
231
|
+
const prefix = rootReal.endsWith(path.sep) ? rootReal : rootReal + path.sep;
|
|
232
|
+
return real.startsWith(prefix);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Walk directory tree, yielding files that match includes and not excludes.
|
|
236
|
+
// Follows symlinks (like GNU find's default) but:
|
|
237
|
+
// - breaks cycles by tracking the realpath of every directory visited
|
|
238
|
+
// - refuses to traverse symlinks that escape the --root boundary
|
|
239
|
+
function* walk(root, includes, excludes, excludeBasenames) {
|
|
240
|
+
const visited = new Set();
|
|
241
|
+
let rootReal;
|
|
242
|
+
try {
|
|
243
|
+
rootReal = fs.realpathSync(root);
|
|
244
|
+
} catch {
|
|
245
|
+
rootReal = path.resolve(root);
|
|
246
|
+
}
|
|
247
|
+
visited.add(rootReal);
|
|
248
|
+
|
|
249
|
+
const stack = [root];
|
|
250
|
+
while (stack.length) {
|
|
251
|
+
const dir = stack.pop();
|
|
252
|
+
let entries;
|
|
253
|
+
try {
|
|
254
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
255
|
+
} catch {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
for (const entry of entries) {
|
|
259
|
+
const full = path.join(dir, entry.name);
|
|
260
|
+
const rel = toPosix(path.relative(root, full));
|
|
261
|
+
const kind = classifyEntry(full, entry);
|
|
262
|
+
if (kind === 'dir') {
|
|
263
|
+
if (isExcludedDir(entry.name, excludeBasenames)) continue;
|
|
264
|
+
if (matchesExcludePath(rel, excludes)) continue;
|
|
265
|
+
let real;
|
|
266
|
+
try {
|
|
267
|
+
real = fs.realpathSync(full);
|
|
268
|
+
} catch {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (visited.has(real)) continue;
|
|
272
|
+
if (!isWithinRoot(real, rootReal)) continue; // refuse symlinks that escape root
|
|
273
|
+
visited.add(real);
|
|
274
|
+
stack.push(full);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (kind !== 'file') continue;
|
|
278
|
+
if (matchesExcludePath(rel, excludes)) continue;
|
|
279
|
+
if (includes.length > 0 && !matchesAny(rel, includes)) continue;
|
|
280
|
+
// For symlinked files, verify the target is within root.
|
|
281
|
+
if (entry.isSymbolicLink()) {
|
|
282
|
+
try {
|
|
283
|
+
const fileReal = fs.realpathSync(full);
|
|
284
|
+
if (!isWithinRoot(fileReal, rootReal)) continue;
|
|
285
|
+
} catch {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
yield rel;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function readLineCount(fullPath) {
|
|
295
|
+
let fd;
|
|
296
|
+
try {
|
|
297
|
+
fd = fs.openSync(fullPath, 'r');
|
|
298
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
299
|
+
let count = 0;
|
|
300
|
+
let bytesRead;
|
|
301
|
+
let lastByte = null;
|
|
302
|
+
let total = 0;
|
|
303
|
+
while ((bytesRead = fs.readSync(fd, buf, 0, buf.length, null)) > 0) {
|
|
304
|
+
total += bytesRead;
|
|
305
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
306
|
+
if (buf[i] === 0x0a) count++;
|
|
307
|
+
}
|
|
308
|
+
lastByte = buf[bytesRead - 1];
|
|
309
|
+
}
|
|
310
|
+
// Count the final line if the file is non-empty and doesn't end with \n
|
|
311
|
+
if (total > 0 && lastByte !== 0x0a) count++;
|
|
312
|
+
return count;
|
|
313
|
+
} catch {
|
|
314
|
+
return 0;
|
|
315
|
+
} finally {
|
|
316
|
+
if (fd !== undefined) {
|
|
317
|
+
try {
|
|
318
|
+
fs.closeSync(fd);
|
|
319
|
+
} catch {
|
|
320
|
+
/* ignore */
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function resolveRoot(opts) {
|
|
327
|
+
const root = opts.root ? path.resolve(opts.root) : process.cwd();
|
|
328
|
+
try {
|
|
329
|
+
if (!fs.statSync(root).isDirectory()) {
|
|
330
|
+
log.fail(`root is not a directory: ${root}`);
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
log.fail(`root does not exist: ${root}`);
|
|
334
|
+
}
|
|
335
|
+
return root;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function buildExcludes(extra) {
|
|
339
|
+
const list = [...DEFAULT_EXCLUDES, ...extra];
|
|
340
|
+
// Patterns: match the basename of a directory OR any path containing it.
|
|
341
|
+
const patterns = [];
|
|
342
|
+
const basenames = new Set();
|
|
343
|
+
for (const item of list) {
|
|
344
|
+
if (item.includes('/') || item.includes('*')) {
|
|
345
|
+
patterns.push(item);
|
|
346
|
+
} else {
|
|
347
|
+
basenames.add(item);
|
|
348
|
+
patterns.push(`**/${item}/**`);
|
|
349
|
+
patterns.push(item);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return { compiled: compilePatterns(patterns), basenames };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function cmdFiles(opts) {
|
|
356
|
+
const root = resolveRoot(opts);
|
|
357
|
+
const includes = compilePatterns(splitList(opts.include));
|
|
358
|
+
const { compiled: excludes, basenames } = buildExcludes(splitList(opts.exclude));
|
|
359
|
+
const limit = opts.limit ? Number(opts.limit) : 0;
|
|
360
|
+
const count = opts.count === true || opts.count === 'true';
|
|
361
|
+
|
|
362
|
+
let n = 0;
|
|
363
|
+
const out = [];
|
|
364
|
+
for (const rel of walk(root, includes, excludes, basenames)) {
|
|
365
|
+
n++;
|
|
366
|
+
if (!count) {
|
|
367
|
+
out.push(rel);
|
|
368
|
+
if (limit > 0 && out.length >= limit) break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (count) {
|
|
372
|
+
log.out(String(n));
|
|
373
|
+
} else {
|
|
374
|
+
for (const p of out) log.out(p);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function cmdLargest(opts) {
|
|
379
|
+
const root = resolveRoot(opts);
|
|
380
|
+
const includes = compilePatterns(splitList(opts.include));
|
|
381
|
+
const { compiled: excludes, basenames } = buildExcludes(splitList(opts.exclude));
|
|
382
|
+
const limit = opts.limit ? Number(opts.limit) : 10;
|
|
383
|
+
|
|
384
|
+
const heap = []; // simple array; N is small so O(files * log N) is fine
|
|
385
|
+
for (const rel of walk(root, includes, excludes, basenames)) {
|
|
386
|
+
const full = path.join(root, rel);
|
|
387
|
+
const lines = readLineCount(full);
|
|
388
|
+
heap.push({ lines, path: rel });
|
|
389
|
+
}
|
|
390
|
+
heap.sort((a, b) => b.lines - a.lines);
|
|
391
|
+
for (const item of heap.slice(0, limit)) {
|
|
392
|
+
log.out(`${item.lines}\t${item.path}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function cmdLoc(opts) {
|
|
397
|
+
const root = resolveRoot(opts);
|
|
398
|
+
const includes = compilePatterns(splitList(opts.include));
|
|
399
|
+
const { compiled: excludes, basenames } = buildExcludes(splitList(opts.exclude));
|
|
400
|
+
|
|
401
|
+
let total = 0;
|
|
402
|
+
let fileCount = 0;
|
|
403
|
+
for (const rel of walk(root, includes, excludes, basenames)) {
|
|
404
|
+
total += readLineCount(path.join(root, rel));
|
|
405
|
+
fileCount++;
|
|
406
|
+
}
|
|
407
|
+
log.out(`${total}\t${fileCount}`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function cmdExtensions(opts) {
|
|
411
|
+
const root = resolveRoot(opts);
|
|
412
|
+
const { compiled: excludes, basenames } = buildExcludes(splitList(opts.exclude));
|
|
413
|
+
const limit = opts.limit ? Number(opts.limit) : 20;
|
|
414
|
+
|
|
415
|
+
const counts = new Map();
|
|
416
|
+
for (const rel of walk(root, [], excludes, basenames)) {
|
|
417
|
+
const base = rel.split('/').pop();
|
|
418
|
+
const dot = base.lastIndexOf('.');
|
|
419
|
+
const ext = dot > 0 ? base.slice(dot + 1) : '(no-ext)';
|
|
420
|
+
counts.set(ext, (counts.get(ext) || 0) + 1);
|
|
421
|
+
}
|
|
422
|
+
const rows = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
|
|
423
|
+
for (const [ext, n] of rows) {
|
|
424
|
+
log.out(`${n}\t${ext}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function main() {
|
|
429
|
+
const { opts, positional } = parseArgs(process.argv.slice(2), {
|
|
430
|
+
booleanFlags: ['count'],
|
|
431
|
+
});
|
|
432
|
+
if (opts.help || positional.length === 0) {
|
|
433
|
+
help();
|
|
434
|
+
process.exit(opts.help ? 0 : 1);
|
|
435
|
+
}
|
|
436
|
+
const cmd = positional[0];
|
|
437
|
+
switch (cmd) {
|
|
438
|
+
case 'files':
|
|
439
|
+
cmdFiles(opts);
|
|
440
|
+
break;
|
|
441
|
+
case 'largest':
|
|
442
|
+
cmdLargest(opts);
|
|
443
|
+
break;
|
|
444
|
+
case 'loc':
|
|
445
|
+
cmdLoc(opts);
|
|
446
|
+
break;
|
|
447
|
+
case 'extensions':
|
|
448
|
+
cmdExtensions(opts);
|
|
449
|
+
break;
|
|
450
|
+
default:
|
|
451
|
+
log.error(`Unknown subcommand: ${cmd}`);
|
|
452
|
+
help();
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
main();
|
|
@@ -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');
|
|
@@ -15,7 +14,9 @@ const {
|
|
|
15
14
|
const log = require('../lib/runtime/log');
|
|
16
15
|
|
|
17
16
|
function help() {
|
|
18
|
-
log.out(
|
|
17
|
+
log.out(
|
|
18
|
+
"Usage: stage-and-commit.js --message 'msg' [--allowlist path] [--max-size-mb 1] [--file-list path] [--dry-run]",
|
|
19
|
+
);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
function splitOut(out) {
|
|
@@ -34,8 +35,9 @@ async function collectChanges() {
|
|
|
34
35
|
// add-side list so we don't `git add` a path that no longer exists and
|
|
35
36
|
// emit a spurious warning; the dedicated `git rm` loop handles them.
|
|
36
37
|
const deletedSet = new Set(deleted);
|
|
37
|
-
const all = dedupeSorted([...splitOut(modified), ...splitOut(untracked)])
|
|
38
|
-
|
|
38
|
+
const all = dedupeSorted([...splitOut(modified), ...splitOut(untracked)]).filter(
|
|
39
|
+
(f) => !deletedSet.has(f),
|
|
40
|
+
);
|
|
39
41
|
return { all, deleted };
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -52,7 +54,10 @@ function parseFileListMarkdown(filePath) {
|
|
|
52
54
|
|
|
53
55
|
async function main() {
|
|
54
56
|
const { opts } = parseArgs(process.argv.slice(2), { booleanFlags: ['dry-run'] });
|
|
55
|
-
if (opts.help) {
|
|
57
|
+
if (opts.help) {
|
|
58
|
+
help();
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
56
61
|
|
|
57
62
|
const message = opts.message ?? opts.m;
|
|
58
63
|
const allowlist = opts.allowlist;
|
|
@@ -95,7 +100,9 @@ async function main() {
|
|
|
95
100
|
|
|
96
101
|
if (!isAllowlisted(file, allowPatterns)) {
|
|
97
102
|
if (lstat.size > MAX_SCAN_BYTES) {
|
|
98
|
-
warnings.push(
|
|
103
|
+
warnings.push(
|
|
104
|
+
`secret scan skipped for ${file} (size ${Math.floor(lstat.size / 1024)} KB > ${MAX_SCAN_BYTES / 1024} KB limit)`,
|
|
105
|
+
);
|
|
99
106
|
} else if (!isBinary) {
|
|
100
107
|
try {
|
|
101
108
|
const raw = fs.readFileSync(file, 'utf8');
|
|
@@ -126,7 +133,8 @@ async function main() {
|
|
|
126
133
|
if (fs.existsSync('.gitignore')) {
|
|
127
134
|
// Exact line match — substring tests were fooled by the entry appearing
|
|
128
135
|
// inside a comment (e.g. "# .autopilot.lock is auto-created").
|
|
129
|
-
const entries = fs
|
|
136
|
+
const entries = fs
|
|
137
|
+
.readFileSync('.gitignore', 'utf8')
|
|
130
138
|
.split(/\r?\n/)
|
|
131
139
|
.map((l) => l.trim())
|
|
132
140
|
.filter((l) => l && !l.startsWith('#'));
|
|
@@ -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');
|
|
@@ -40,12 +39,20 @@ function atomicWrite(targetPath, content) {
|
|
|
40
39
|
try {
|
|
41
40
|
fs.writeFileSync(targetPath, content, 'utf8');
|
|
42
41
|
} finally {
|
|
43
|
-
try {
|
|
42
|
+
try {
|
|
43
|
+
fs.unlinkSync(tmp);
|
|
44
|
+
} catch {
|
|
45
|
+
/* best effort */
|
|
46
|
+
}
|
|
44
47
|
}
|
|
45
48
|
return;
|
|
46
49
|
}
|
|
47
50
|
// Any other error: clean up tmp so we don't leak cruft.
|
|
48
|
-
try {
|
|
51
|
+
try {
|
|
52
|
+
fs.unlinkSync(tmp);
|
|
53
|
+
} catch {
|
|
54
|
+
/* best effort */
|
|
55
|
+
}
|
|
49
56
|
throw e;
|
|
50
57
|
}
|
|
51
58
|
}
|
|
@@ -78,7 +85,10 @@ function buildHeader(baseBranch, platform) {
|
|
|
78
85
|
|
|
79
86
|
function main() {
|
|
80
87
|
const { opts } = parseArgs(process.argv.slice(2));
|
|
81
|
-
if (opts.help) {
|
|
88
|
+
if (opts.help) {
|
|
89
|
+
help();
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
82
92
|
|
|
83
93
|
const story = opts.story;
|
|
84
94
|
const statusFile = opts['git-status-file'];
|
|
@@ -102,12 +112,12 @@ function main() {
|
|
|
102
112
|
// value", so we must NOT emit the field when the flag is absent. The
|
|
103
113
|
// previous logic defaulted to 'false' and overwrote a prior 'true' every
|
|
104
114
|
// call.
|
|
105
|
-
const hasWorktreeCleaned = Object.
|
|
115
|
+
const hasWorktreeCleaned = Object.hasOwn(opts, 'worktree-cleaned');
|
|
106
116
|
let worktreeCleaned;
|
|
107
117
|
if (hasWorktreeCleaned) {
|
|
108
118
|
const v = opts['worktree-cleaned'];
|
|
109
119
|
// Accept 'true'/'false' strings (any case) and boolean true.
|
|
110
|
-
worktreeCleaned =
|
|
120
|
+
worktreeCleaned = v === true || String(v).toLowerCase() === 'true' ? 'true' : 'false';
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
const fields = [
|