@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.
Files changed (40) hide show
  1. package/README.md +2 -2
  2. package/_Sprintpilot/lib/runtime/args.js +0 -2
  3. package/_Sprintpilot/lib/runtime/git.js +0 -2
  4. package/_Sprintpilot/lib/runtime/http.js +12 -5
  5. package/_Sprintpilot/lib/runtime/log.js +0 -2
  6. package/_Sprintpilot/lib/runtime/secrets.js +14 -16
  7. package/_Sprintpilot/lib/runtime/spawn.js +21 -8
  8. package/_Sprintpilot/lib/runtime/text.js +0 -2
  9. package/_Sprintpilot/lib/runtime/yaml-lite.js +9 -5
  10. package/_Sprintpilot/manifest.yaml +1 -1
  11. package/_Sprintpilot/scripts/create-pr.js +76 -38
  12. package/_Sprintpilot/scripts/detect-platform.js +35 -10
  13. package/_Sprintpilot/scripts/health-check.js +17 -8
  14. package/_Sprintpilot/scripts/lint-changed.js +35 -16
  15. package/_Sprintpilot/scripts/lock.js +22 -6
  16. package/_Sprintpilot/scripts/sanitize-branch.js +4 -2
  17. package/_Sprintpilot/scripts/scan.js +457 -0
  18. package/_Sprintpilot/scripts/stage-and-commit.js +15 -7
  19. package/_Sprintpilot/scripts/sync-status.js +16 -6
  20. package/_Sprintpilot/skills/sprint-autopilot-on/workflow.md +62 -31
  21. package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/architecture-mapper.md +22 -15
  22. package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/concerns-hunter.md +47 -24
  23. package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/integration-mapper.md +21 -21
  24. package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/quality-assessor.md +34 -22
  25. package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/stack-analyzer.md +41 -43
  26. package/_Sprintpilot/skills/sprintpilot-codebase-map/workflow.md +1 -1
  27. package/bin/sprintpilot.js +11 -4
  28. package/lib/commands/check-update.js +0 -2
  29. package/lib/commands/install.js +139 -49
  30. package/lib/commands/uninstall.js +21 -11
  31. package/lib/core/bmad-config.js +0 -2
  32. package/lib/core/file-ops.js +6 -6
  33. package/lib/core/gitignore.js +0 -2
  34. package/lib/core/markers.js +5 -3
  35. package/lib/core/tool-registry.js +19 -21
  36. package/lib/core/update-check.js +0 -2
  37. package/lib/core/v1-detect.js +0 -2
  38. package/lib/prompts.js +0 -2
  39. package/lib/substitute.js +1 -5
  40. 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 { fs.closeSync(fd); } catch { /* ignore */ }
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 { fs.unlinkSync(lockFile); } catch { /* ignore */ }
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 { fs.unlinkSync(lockFile); } catch { /* ignore */ }
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 { fs.unlinkSync(lockFile); } catch { /* ignore */ }
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') log.out(`Lock: ACTIVE — session ${info.id}, age ${info.ageMin}m`);
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) { help(); process.exit(0); }
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("Usage: stage-and-commit.js --message 'msg' [--allowlist path] [--max-size-mb 1] [--file-list path] [--dry-run]");
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
- .filter((f) => !deletedSet.has(f));
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) { help(); process.exit(0); }
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(`secret scan skipped for ${file} (size ${Math.floor(lstat.size / 1024)} KB > ${MAX_SCAN_BYTES / 1024} KB limit)`);
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.readFileSync('.gitignore', 'utf8')
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 { fs.unlinkSync(tmp); } catch { /* best effort */ }
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 { fs.unlinkSync(tmp); } catch { /* best effort */ }
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) { help(); process.exit(0); }
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.prototype.hasOwnProperty.call(opts, 'worktree-cleaned');
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 = (v === true || String(v).toLowerCase() === 'true') ? 'true' : 'false';
120
+ worktreeCleaned = v === true || String(v).toLowerCase() === 'true' ? 'true' : 'false';
111
121
  }
112
122
 
113
123
  const fields = [