@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 path = require('node:path');
5
4
  const { Command } = require('commander');
@@ -42,16 +41,24 @@ async function main() {
42
41
 
43
42
  program
44
43
  .name('sprintpilot')
45
- .description('Sprintpilot — autopilot and multi-agent addon for BMad Method: autonomous story execution, parallel agents, git workflow')
44
+ .description(
45
+ 'Sprintpilot — autopilot and multi-agent addon for BMad Method: autonomous story execution, parallel agents, git workflow',
46
+ )
46
47
  .version(await resolveVersion(), '-v, --version', 'Show version');
47
48
 
48
49
  program
49
50
  .command('install', { isDefault: true })
50
51
  .description('Install Sprintpilot into the current BMad Method project')
51
- .option('--tools <list>', 'Comma-separated tools (claude-code,cursor,windsurf,cline,roo,trae,kiro,gemini-cli,github-copilot,all)')
52
+ .option(
53
+ '--tools <list>',
54
+ 'Comma-separated tools (claude-code,cursor,windsurf,cline,roo,trae,kiro,gemini-cli,github-copilot,all)',
55
+ )
52
56
  .option('--dry-run', 'Preview without making changes')
53
57
  .option('--force', 'Skip backup of existing skills')
54
- .option('--migrate-v1', 'Migrate from bmad-autopilot-addon v1 (auto-detected; this flag is for non-interactive CI)')
58
+ .option(
59
+ '--migrate-v1',
60
+ 'Migrate from bmad-autopilot-addon v1 (auto-detected; this flag is for non-interactive CI)',
61
+ )
55
62
  .option('-y, --yes', 'Non-interactive mode')
56
63
  .action(async (options) => {
57
64
  try {
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  const path = require('node:path');
4
2
  const fs = require('fs-extra');
5
3
 
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  const path = require('node:path');
4
2
  const { execFile } = require('node:child_process');
5
3
  const { promisify } = require('node:util');
@@ -22,7 +20,14 @@ const {
22
20
  const { resolveIgnoreFile, addIgnoreEntry } = require('../core/gitignore');
23
21
  const { copyDirWithSubstitution, backupSkill, pruneBackups } = require('../core/file-ops');
24
22
  const {
25
- BEGIN, END, stripBlock, stripLegacyBlock, upsertBlock, writeAtomic, hasBlock, hasLegacyBlock,
23
+ BEGIN,
24
+ END,
25
+ stripBlock,
26
+ stripLegacyBlock,
27
+ upsertBlock,
28
+ writeAtomic,
29
+ hasBlock,
30
+ hasLegacyBlock,
26
31
  } = require('../core/markers');
27
32
  const { renderString, buildContext, isTextFile } = require('../substitute');
28
33
  const { fetchLatestVersion, compareVersions } = require('../core/update-check');
@@ -55,15 +60,19 @@ function renderBanner(version) {
55
60
  return lines.join('\n');
56
61
  }
57
62
 
58
- const {
59
- V1_ADDON_DIR_NAME,
60
- V1_SKILL_NAMES,
61
- detectV1Installation,
62
- } = require('../core/v1-detect');
63
+ const { V1_ADDON_DIR_NAME, V1_SKILL_NAMES, detectV1Installation } = require('../core/v1-detect');
63
64
 
64
65
  const ADDON_DIR = path.resolve(__dirname, '..', '..', '_Sprintpilot');
65
66
  const PROJECT_ADDON_DIR_NAME = '_Sprintpilot';
66
- const RUNTIME_RESOURCES = ['Sprintpilot.md', 'manifest.yaml', '.secrets-allowlist', 'lib', 'modules', 'scripts', 'templates'];
67
+ const RUNTIME_RESOURCES = [
68
+ 'Sprintpilot.md',
69
+ 'manifest.yaml',
70
+ '.secrets-allowlist',
71
+ 'lib',
72
+ 'modules',
73
+ 'scripts',
74
+ 'templates',
75
+ ];
67
76
  const V1_MODULE_NAMES = ['git', 'ma', 'autopilot'];
68
77
 
69
78
  // Sentinel thrown by evictV1Installation when the user declines migration.
@@ -81,7 +90,10 @@ function parseToolsArg(value) {
81
90
  const trimmed = String(value).trim();
82
91
  if (!trimmed) return null;
83
92
  if (trimmed === 'all') return ALL_TOOLS.slice();
84
- return trimmed.split(',').map((t) => t.trim()).filter(Boolean);
93
+ return trimmed
94
+ .split(',')
95
+ .map((t) => t.trim())
96
+ .filter(Boolean);
85
97
  }
86
98
 
87
99
  async function detectInstalledTools(projectRoot) {
@@ -100,7 +112,10 @@ async function detectInstalledTools(projectRoot) {
100
112
  async function listSkills() {
101
113
  const skillsDir = path.join(ADDON_DIR, 'skills');
102
114
  const entries = await fs.readdir(skillsDir, { withFileTypes: true });
103
- return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
115
+ return entries
116
+ .filter((e) => e.isDirectory())
117
+ .map((e) => e.name)
118
+ .sort();
104
119
  }
105
120
 
106
121
  function timestamp() {
@@ -135,8 +150,8 @@ async function installSystemPrompt(tool, projectRoot, addonDir, ctx, { dryRun =
135
150
 
136
151
  if (mode === 'claude-code') {
137
152
  const agentsFile = path.join(projectRoot, 'AGENTS.md');
138
- let existed = await fs.pathExists(agentsFile);
139
- let existing = existed ? await fs.readFile(agentsFile, 'utf8') : '';
153
+ const existed = await fs.pathExists(agentsFile);
154
+ const existing = existed ? await fs.readFile(agentsFile, 'utf8') : '';
140
155
  const updated = upsertBlock(existing, rulesContent);
141
156
  await writeAtomic(agentsFile, updated);
142
157
  if (!existed) {
@@ -154,7 +169,11 @@ async function installSystemPrompt(tool, projectRoot, addonDir, ctx, { dryRun =
154
169
  console.log(` System prompt: CLAUDE.md (already has @AGENTS.md)`);
155
170
  } else if (claudeExists) {
156
171
  const needsNewline = claudeContent.length && !claudeContent.endsWith('\n');
157
- await fs.writeFile(claudeFile, `${claudeContent}${needsNewline ? '\n' : ''}@AGENTS.md\n`, 'utf8');
172
+ await fs.writeFile(
173
+ claudeFile,
174
+ `${claudeContent}${needsNewline ? '\n' : ''}@AGENTS.md\n`,
175
+ 'utf8',
176
+ );
158
177
  console.log(` System prompt: CLAUDE.md (appended @AGENTS.md)`);
159
178
  } else {
160
179
  await fs.writeFile(claudeFile, '@AGENTS.md\n', 'utf8');
@@ -275,11 +294,15 @@ async function persistSnapshotForRecovery(projectRoot, snapshot) {
275
294
  contentBase64: f.buffer.toString('base64'),
276
295
  }));
277
296
  }
278
- const body = JSON.stringify({
279
- note: 'v1 module-config snapshot — restore manually under _Sprintpilot/modules/ and delete this file. Each file.contentBase64 is base64-encoded.',
280
- capturedAt: new Date().toISOString(),
281
- modules: serialized,
282
- }, null, 2);
297
+ const body = JSON.stringify(
298
+ {
299
+ note: 'v1 module-config snapshot — restore manually under _Sprintpilot/modules/ and delete this file. Each file.contentBase64 is base64-encoded.',
300
+ capturedAt: new Date().toISOString(),
301
+ modules: serialized,
302
+ },
303
+ null,
304
+ 2,
305
+ );
283
306
  await writeAtomic(recoveryFile, body);
284
307
  return recoveryFile;
285
308
  }
@@ -321,10 +344,14 @@ async function stripLegacyMarkers(projectRoot) {
321
344
  await writeAtomic(backup, content);
322
345
  if (!stripped.trim()) {
323
346
  await writeAtomic(file, '');
324
- touched.push(`emptied ${path.relative(projectRoot, file)} (was legacy-only) — backup at ${path.relative(projectRoot, backup)}`);
347
+ touched.push(
348
+ `emptied ${path.relative(projectRoot, file)} (was legacy-only) — backup at ${path.relative(projectRoot, backup)}`,
349
+ );
325
350
  } else {
326
351
  await writeAtomic(file, stripped.endsWith('\n') ? stripped : `${stripped}\n`);
327
- touched.push(`stripped legacy block from ${path.relative(projectRoot, file)} — backup at ${path.relative(projectRoot, backup)}`);
352
+ touched.push(
353
+ `stripped legacy block from ${path.relative(projectRoot, file)} — backup at ${path.relative(projectRoot, backup)}`,
354
+ );
328
355
  }
329
356
  }
330
357
  // Dedicated-file tools (cursor, roo, kiro, trae): the whole file is ours;
@@ -367,14 +394,16 @@ async function detectOldGlobalNpmPackage() {
367
394
  try {
368
395
  const data = JSON.parse(out);
369
396
  const deps = (data && data.dependencies) || {};
370
- return Object.prototype.hasOwnProperty.call(deps, 'bmad-autopilot-addon');
397
+ return Object.hasOwn(deps, 'bmad-autopilot-addon');
371
398
  } catch {
372
399
  return null;
373
400
  }
374
401
  };
375
402
 
376
403
  try {
377
- const { stdout } = await execFileAsync('npm', ['ls', '-g', '--depth=0', '--json'], { timeout: 10_000 });
404
+ const { stdout } = await execFileAsync('npm', ['ls', '-g', '--depth=0', '--json'], {
405
+ timeout: 10_000,
406
+ });
378
407
  const result = parseOutput(stdout);
379
408
  return result === null ? false : result;
380
409
  } catch (err) {
@@ -405,15 +434,23 @@ async function evictV1Installation(projectRoot, { dryRun, migrateV1, yes }) {
405
434
  console.log(pc.yellow(' (v1 manifest missing — detected via v1-named skill directories)'));
406
435
  break;
407
436
  case 'skills-unreadable-manifest':
408
- console.log(pc.yellow(' (v1 manifest unreadable — detected via v1-named skill directories)'));
437
+ console.log(
438
+ pc.yellow(' (v1 manifest unreadable — detected via v1-named skill directories)'),
439
+ );
409
440
  break;
410
441
  case 'skills-other-addon':
411
- console.log(pc.yellow(` (manifest names addon "${v1.manifestAddonName}" — NOT bmad-ma-git — but v1-named skill directories are present)`));
442
+ console.log(
443
+ pc.yellow(
444
+ ` (manifest names addon "${v1.manifestAddonName}" — NOT bmad-ma-git — but v1-named skill directories are present)`,
445
+ ),
446
+ );
412
447
  break;
413
448
  default:
414
449
  // Fail-closed: an unknown detection reason means we don't fully
415
450
  // understand what we're about to migrate. Require explicit opt-in.
416
- console.error(pc.red(`ERROR: unknown v1 detection reason "${v1.detectedVia}". Refusing to auto-migrate.`));
451
+ console.error(
452
+ pc.red(`ERROR: unknown v1 detection reason "${v1.detectedVia}". Refusing to auto-migrate.`),
453
+ );
417
454
  console.error(pc.red(' Pass --migrate-v1 explicitly if you want to proceed anyway.'));
418
455
  if (!migrateV1) throw new V1MigrationDeclinedError();
419
456
  break;
@@ -425,9 +462,17 @@ async function evictV1Installation(projectRoot, { dryRun, migrateV1, yes }) {
425
462
  // confirmation or an explicit --migrate-v1.
426
463
  if (v1.detectedVia === 'skills-other-addon' && !migrateV1) {
427
464
  console.error(pc.red('ERROR: ambiguous v1 signature.'));
428
- console.error(pc.red(` The manifest at ${path.relative(projectRoot, v1.v1Manifest)} names "${v1.manifestAddonName}", not "bmad-ma-git",`));
429
- console.error(pc.red(' but v1-named skill directories are present. This might be a custom install.'));
430
- console.error(pc.red(' If you want Sprintpilot to migrate it anyway, pass --migrate-v1 explicitly:'));
465
+ console.error(
466
+ pc.red(
467
+ ` The manifest at ${path.relative(projectRoot, v1.v1Manifest)} names "${v1.manifestAddonName}", not "bmad-ma-git",`,
468
+ ),
469
+ );
470
+ console.error(
471
+ pc.red(' but v1-named skill directories are present. This might be a custom install.'),
472
+ );
473
+ console.error(
474
+ pc.red(' If you want Sprintpilot to migrate it anyway, pass --migrate-v1 explicitly:'),
475
+ );
431
476
  console.error(' sprintpilot install --migrate-v1' + (yes ? ' --yes' : ''));
432
477
  throw new V1MigrationDeclinedError();
433
478
  }
@@ -440,9 +485,13 @@ async function evictV1Installation(projectRoot, { dryRun, migrateV1, yes }) {
440
485
  let isTTY = false;
441
486
  try {
442
487
  isTTY = process.stdin && process.stdin.isTTY === true && !process.stdin.destroyed;
443
- } catch { /* treat as non-TTY */ }
488
+ } catch {
489
+ /* treat as non-TTY */
490
+ }
444
491
  if (!migrateV1 && !yes && !isTTY) {
445
- console.error(pc.red('ERROR: v1 install detected but stdin is not a TTY — cannot prompt for confirmation.'));
492
+ console.error(
493
+ pc.red('ERROR: v1 install detected but stdin is not a TTY — cannot prompt for confirmation.'),
494
+ );
446
495
  console.error(pc.red('Re-run with --migrate-v1 --yes to migrate non-interactively:'));
447
496
  console.error(' sprintpilot install --migrate-v1 --yes');
448
497
  throw new V1MigrationDeclinedError();
@@ -462,7 +511,8 @@ async function evictV1Installation(projectRoot, { dryRun, migrateV1, yes }) {
462
511
 
463
512
  if (!migrateV1 && !yes) {
464
513
  const proceed = await prompts.confirm({
465
- message: 'Migrate this project from bmad-autopilot-addon to Sprintpilot? (preserves module configs, removes legacy artifacts, backs up rule files)',
514
+ message:
515
+ 'Migrate this project from bmad-autopilot-addon to Sprintpilot? (preserves module configs, removes legacy artifacts, backs up rule files)',
466
516
  initialValue: false,
467
517
  });
468
518
  if (!proceed) {
@@ -472,7 +522,11 @@ async function evictV1Installation(projectRoot, { dryRun, migrateV1, yes }) {
472
522
  }
473
523
 
474
524
  if (dryRun) {
475
- console.log(pc.dim('[DRY RUN] Would snapshot legacy module configs, strip legacy markers (with backups), evict legacy skills from project tool dirs, remove _bmad-addons/, then re-apply snapshot after Sprintpilot install.'));
525
+ console.log(
526
+ pc.dim(
527
+ '[DRY RUN] Would snapshot legacy module configs, strip legacy markers (with backups), evict legacy skills from project tool dirs, remove _bmad-addons/, then re-apply snapshot after Sprintpilot install.',
528
+ ),
529
+ );
476
530
  return { migrated: true, moduleConfigSnapshot: {} };
477
531
  }
478
532
 
@@ -487,7 +541,9 @@ async function evictV1Installation(projectRoot, { dryRun, migrateV1, yes }) {
487
541
  const ignore = await resolveIgnoreFile(projectRoot);
488
542
  await addIgnoreEntry(ignore.path, '*.bak-sprintpilot-migration', { dryRun: false });
489
543
  await addIgnoreEntry(ignore.path, '.sprintpilot-v1-snapshot*.json', { dryRun: false });
490
- } catch { /* non-blocking: the migration must not fail if .gitignore is unwritable */ }
544
+ } catch {
545
+ /* non-blocking: the migration must not fail if .gitignore is unwritable */
546
+ }
491
547
 
492
548
  // 1. Snapshot the full v1 modules/ tree into memory BEFORE any
493
549
  // destructive operation. Templates (commit-story.txt, pr-body.md,
@@ -495,14 +551,18 @@ async function evictV1Installation(projectRoot, { dryRun, migrateV1, yes }) {
495
551
  // survive. If any read fails, abort before removing the original.
496
552
  const { snapshot, failures } = await snapshotV1ModuleConfigs(projectRoot);
497
553
  if (failures.length) {
498
- console.error(pc.red('ERROR: failed to read v1 module files — aborting migration to avoid data loss:'));
554
+ console.error(
555
+ pc.red('ERROR: failed to read v1 module files — aborting migration to avoid data loss:'),
556
+ );
499
557
  for (const { mod, err } of failures) {
500
558
  console.error(` modules/${mod}/ — ${err.message || err}`);
501
559
  }
502
560
  throw new Error('v1 config snapshot failed');
503
561
  }
504
562
  for (const mod of Object.keys(snapshot)) {
505
- console.log(` Captured v1 modules/${mod}/ (${snapshot[mod].length} file${snapshot[mod].length === 1 ? '' : 's'})`);
563
+ console.log(
564
+ ` Captured v1 modules/${mod}/ (${snapshot[mod].length} file${snapshot[mod].length === 1 ? '' : 's'})`,
565
+ );
506
566
  }
507
567
 
508
568
  // 2. Strip legacy marker blocks from user rule files (with backups).
@@ -520,7 +580,9 @@ async function evictV1Installation(projectRoot, { dryRun, migrateV1, yes }) {
520
580
  await fs.remove(v1.v1Dir);
521
581
  console.log(' Removed ' + V1_ADDON_DIR_NAME + '/');
522
582
  } catch (err) {
523
- console.warn(pc.yellow(` WARNING: failed to remove ${V1_ADDON_DIR_NAME}/ — ${err.message || err}`));
583
+ console.warn(
584
+ pc.yellow(` WARNING: failed to remove ${V1_ADDON_DIR_NAME}/ — ${err.message || err}`),
585
+ );
524
586
  console.warn(pc.yellow(' Remove it manually after install: rm -rf ' + V1_ADDON_DIR_NAME));
525
587
  }
526
588
 
@@ -528,7 +590,9 @@ async function evictV1Installation(projectRoot, { dryRun, migrateV1, yes }) {
528
590
  const hasOldGlobal = await detectOldGlobalNpmPackage();
529
591
  if (hasOldGlobal) {
530
592
  console.log('');
531
- console.log(pc.yellow('Legacy npm package detected: bmad-autopilot-addon is installed globally.'));
593
+ console.log(
594
+ pc.yellow('Legacy npm package detected: bmad-autopilot-addon is installed globally.'),
595
+ );
532
596
  console.log(pc.yellow('Run this to remove it:'));
533
597
  console.log(' npm uninstall -g bmad-autopilot-addon');
534
598
  }
@@ -568,7 +632,7 @@ async function runInstall(options = {}) {
568
632
  const addonVersion = await readAddonManifestVersion(path.join(ADDON_DIR, 'manifest.yaml'));
569
633
 
570
634
  // Non-blocking update check
571
- let latestVersionPromise = fetchLatestVersion().catch(() => null);
635
+ const latestVersionPromise = fetchLatestVersion().catch(() => null);
572
636
 
573
637
  process.stdout.write(pc.cyan(renderBanner(addonVersion)));
574
638
  console.log('');
@@ -610,7 +674,7 @@ async function runInstall(options = {}) {
610
674
  // 3. Detect + select tools
611
675
  const detected = await detectInstalledTools(projectRoot);
612
676
 
613
- let parsedTools = parseToolsArg(options.tools);
677
+ const parsedTools = parseToolsArg(options.tools);
614
678
  let selectedTools;
615
679
 
616
680
  if (parsedTools) {
@@ -641,12 +705,16 @@ async function runInstall(options = {}) {
641
705
  console.log('');
642
706
 
643
707
  // 4. .gitignore maintenance
644
- let ignore = await resolveIgnoreFile(projectRoot);
708
+ const ignore = await resolveIgnoreFile(projectRoot);
645
709
  const lockResult = await addIgnoreEntry(ignore.path, '.autopilot.lock', { dryRun });
646
710
  if (lockResult.added) {
647
711
  const name = path.basename(ignore.path);
648
712
  if (dryRun) {
649
- console.log(pc.dim(`[DRY RUN] Would ${lockResult.created ? 'create' : 'add'} '.autopilot.lock' in ${name}`));
713
+ console.log(
714
+ pc.dim(
715
+ `[DRY RUN] Would ${lockResult.created ? 'create' : 'add'} '.autopilot.lock' in ${name}`,
716
+ ),
717
+ );
650
718
  } else if (lockResult.created) {
651
719
  console.log(`Created ${name} with '.autopilot.lock'`);
652
720
  } else {
@@ -728,7 +796,11 @@ async function runInstall(options = {}) {
728
796
  // Swap failed — put the old target back so the tool still has
729
797
  // a skill present, then re-raise.
730
798
  if (targetExistsNow) {
731
- try { await fs.rename(oldTarget, target); } catch { /* best effort */ }
799
+ try {
800
+ await fs.rename(oldTarget, target);
801
+ } catch {
802
+ /* best effort */
803
+ }
732
804
  }
733
805
  throw e;
734
806
  }
@@ -736,7 +808,11 @@ async function runInstall(options = {}) {
736
808
  await fs.remove(oldTarget);
737
809
  }
738
810
  } catch (e) {
739
- try { await fs.remove(stagingTarget); } catch { /* best effort */ }
811
+ try {
812
+ await fs.remove(stagingTarget);
813
+ } catch {
814
+ /* best effort */
815
+ }
740
816
  throw e;
741
817
  }
742
818
  toolInstalled++;
@@ -787,13 +863,23 @@ async function runInstall(options = {}) {
787
863
  const reapplied = await applyV1ModuleConfigs(projectRoot, v1ConfigSnapshot);
788
864
  for (const r of reapplied) console.log(` Preserved v1 ${r}`);
789
865
  } catch (err) {
790
- console.error(pc.red('ERROR: failed to re-apply legacy module snapshot after Sprintpilot install.'));
866
+ console.error(
867
+ pc.red('ERROR: failed to re-apply legacy module snapshot after Sprintpilot install.'),
868
+ );
791
869
  console.error(pc.red(` ${err.message || err}`));
792
870
  try {
793
871
  const recoveryFile = await persistSnapshotForRecovery(projectRoot, v1ConfigSnapshot);
794
- console.error(pc.yellow(` Snapshot persisted to ${path.relative(projectRoot, recoveryFile)} — restore manually.`));
872
+ console.error(
873
+ pc.yellow(
874
+ ` Snapshot persisted to ${path.relative(projectRoot, recoveryFile)} — restore manually.`,
875
+ ),
876
+ );
795
877
  } catch (persistErr) {
796
- console.error(pc.red(` Additionally failed to persist snapshot: ${persistErr.message || persistErr}`));
878
+ console.error(
879
+ pc.red(
880
+ ` Additionally failed to persist snapshot: ${persistErr.message || persistErr}`,
881
+ ),
882
+ );
797
883
  }
798
884
  throw err;
799
885
  }
@@ -819,7 +905,9 @@ async function runInstall(options = {}) {
819
905
  console.log(pc.green(`=== Sprintpilot v${addonVersion || 'unknown'} installed ===`));
820
906
  console.log('');
821
907
  console.log(`Tools configured: ${selectedTools.join(' ')}`);
822
- console.log(`Total skills installed: ${totalInstalled} (${skillCount} skills x ${selectedTools.length} tools)`);
908
+ console.log(
909
+ `Total skills installed: ${totalInstalled} (${skillCount} skills x ${selectedTools.length} tools)`,
910
+ );
823
911
  console.log('');
824
912
  console.log('Skills:');
825
913
  for (const skill of allSkills) console.log(` - ${skill}`);
@@ -848,7 +936,9 @@ async function runInstall(options = {}) {
848
936
  console.log(' multi_agent.max_parallel_analysis 5 Codebase analysis agents');
849
937
  console.log('');
850
938
  console.log(' _Sprintpilot/modules/autopilot/config.yaml');
851
- console.log(' autopilot.session_story_limit 3 Stories to fully implement per run (0 = unlimited)');
939
+ console.log(
940
+ ' autopilot.session_story_limit 3 Stories to fully implement per run (0 = unlimited)',
941
+ );
852
942
  console.log('');
853
943
  console.log('Multi-agent skills — run parallel subagents for faster analysis:');
854
944
  console.log(' /sprintpilot-code-review Parallel 3-layer adversarial review');
@@ -1,18 +1,17 @@
1
- 'use strict';
2
-
3
1
  const path = require('node:path');
4
2
  const { execFile } = require('node:child_process');
5
3
  const { promisify } = require('node:util');
6
4
  const fs = require('fs-extra');
7
5
  const pc = require('picocolors');
8
6
 
9
- const { ALL_TOOLS, getToolDir, getSystemPromptFile, getSystemPromptMode } = require('../core/tool-registry');
10
- const { stripBlock, hasBlock, writeAtomic } = require('../core/markers');
11
7
  const {
12
- V1_ADDON_DIR_NAME,
13
- V1_SKILL_NAMES,
14
- detectV1Installation,
15
- } = require('../core/v1-detect');
8
+ ALL_TOOLS,
9
+ getToolDir,
10
+ getSystemPromptFile,
11
+ getSystemPromptMode,
12
+ } = require('../core/tool-registry');
13
+ const { stripBlock, hasBlock, writeAtomic } = require('../core/markers');
14
+ const { V1_ADDON_DIR_NAME, V1_SKILL_NAMES, detectV1Installation } = require('../core/v1-detect');
16
15
 
17
16
  const execFileAsync = promisify(execFile);
18
17
  const ADDON_DIR = path.resolve(__dirname, '..', '..', '_Sprintpilot');
@@ -46,7 +45,10 @@ async function removeSystemPrompt(tool, projectRoot) {
46
45
  if (await fs.pathExists(claudeFile)) {
47
46
  const content = await fs.readFile(claudeFile, 'utf8');
48
47
  if (content.includes('@AGENTS.md')) {
49
- const newContent = content.split(/\r?\n/).filter((l) => !l.includes('@AGENTS.md')).join('\n');
48
+ const newContent = content
49
+ .split(/\r?\n/)
50
+ .filter((l) => !l.includes('@AGENTS.md'))
51
+ .join('\n');
50
52
  if (!newContent.trim()) {
51
53
  await fs.remove(claudeFile);
52
54
  console.log(`${tool}: removed CLAUDE.md (was Sprintpilot-only)`);
@@ -200,7 +202,11 @@ async function runUninstall(options = {}) {
200
202
  } else {
201
203
  const legacyAddonDir = path.join(projectRoot, V1_ADDON_DIR_NAME);
202
204
  if (await fs.pathExists(legacyAddonDir)) {
203
- console.log(pc.yellow(`Skipped ${V1_ADDON_DIR_NAME}/ — no v1 signature found (not a Sprintpilot v1 artifact).`));
205
+ console.log(
206
+ pc.yellow(
207
+ `Skipped ${V1_ADDON_DIR_NAME}/ — no v1 signature found (not a Sprintpilot v1 artifact).`,
208
+ ),
209
+ );
204
210
  }
205
211
  }
206
212
 
@@ -212,7 +218,11 @@ async function runUninstall(options = {}) {
212
218
  }
213
219
 
214
220
  console.log('');
215
- console.log(pc.green(`Sprintpilot uninstalled (${totalRemoved} skills removed). BMad Method skills are unaffected.`));
221
+ console.log(
222
+ pc.green(
223
+ `Sprintpilot uninstalled (${totalRemoved} skills removed). BMad Method skills are unaffected.`,
224
+ ),
225
+ );
216
226
  }
217
227
 
218
228
  module.exports = { runUninstall };
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  const path = require('node:path');
4
2
  const fs = require('fs-extra');
5
3
  const yaml = require('js-yaml');
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  const path = require('node:path');
4
2
  const fs = require('fs-extra');
5
3
  const { isTextFile, renderString } = require('../substitute');
@@ -43,7 +41,11 @@ async function copyDirWithSubstitution(src, dest, ctx, { dryRun = false } = {})
43
41
  // chmod best-effort
44
42
  }
45
43
  } else {
46
- await fs.copy(file, target, { overwrite: true, dereference: false, preserveTimestamps: false });
44
+ await fs.copy(file, target, {
45
+ overwrite: true,
46
+ dereference: false,
47
+ preserveTimestamps: false,
48
+ });
47
49
  }
48
50
  }
49
51
  }
@@ -71,9 +73,7 @@ async function pruneBackups(backupDir, skillName, max = 3) {
71
73
  if (!(await fs.pathExists(backupDir))) return;
72
74
  const prefix = `${skillName}.`;
73
75
  const entries = await fs.readdir(backupDir);
74
- const matches = entries
75
- .filter((e) => e.startsWith(prefix))
76
- .sort();
76
+ const matches = entries.filter((e) => e.startsWith(prefix)).sort();
77
77
  if (matches.length <= max) return;
78
78
  const toRemove = matches.slice(0, matches.length - max);
79
79
  for (const name of toRemove) {
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  const path = require('node:path');
4
2
  const fs = require('fs-extra');
5
3
 
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  const path = require('node:path');
4
2
  const crypto = require('node:crypto');
5
3
  const fs = require('fs-extra');
@@ -105,7 +103,11 @@ async function writeAtomic(filePath, content) {
105
103
  await fs.writeFile(tmp, content, 'utf8');
106
104
  await fs.move(tmp, filePath, { overwrite: true });
107
105
  } catch (e) {
108
- try { await fs.remove(tmp); } catch { /* best effort */ }
106
+ try {
107
+ await fs.remove(tmp);
108
+ } catch {
109
+ /* best effort */
110
+ }
109
111
  throw e;
110
112
  }
111
113
  }
@@ -1,37 +1,35 @@
1
- 'use strict';
2
-
3
1
  const TOOL_DIRS = {
4
2
  'claude-code': '.claude',
5
- 'cursor': '.cursor',
6
- 'windsurf': '.windsurf',
7
- 'cline': '.cline',
8
- 'roo': '.roo',
9
- 'trae': '.trae',
10
- 'kiro': '.kiro',
3
+ cursor: '.cursor',
4
+ windsurf: '.windsurf',
5
+ cline: '.cline',
6
+ roo: '.roo',
7
+ trae: '.trae',
8
+ kiro: '.kiro',
11
9
  'github-copilot': '.github/copilot',
12
10
  'gemini-cli': '.gemini',
13
11
  };
14
12
 
15
13
  const SYSTEM_PROMPT_FILES = {
16
14
  'claude-code': 'AGENTS.md',
17
- 'cursor': '.cursor/rules/bmad.md',
18
- 'windsurf': '.windsurfrules',
19
- 'cline': '.clinerules',
20
- 'roo': '.roo/rules/bmad.md',
15
+ cursor: '.cursor/rules/bmad.md',
16
+ windsurf: '.windsurfrules',
17
+ cline: '.clinerules',
18
+ roo: '.roo/rules/bmad.md',
21
19
  'gemini-cli': 'GEMINI.md',
22
20
  'github-copilot': '.github/copilot-instructions.md',
23
- 'kiro': '.kiro/rules/bmad.md',
24
- 'trae': '.trae/rules/bmad.md',
21
+ kiro: '.kiro/rules/bmad.md',
22
+ trae: '.trae/rules/bmad.md',
25
23
  };
26
24
 
27
25
  const SYSTEM_PROMPT_MODES = {
28
26
  'claude-code': 'claude-code',
29
- 'cursor': 'own-file',
30
- 'roo': 'own-file',
31
- 'kiro': 'own-file',
32
- 'trae': 'own-file',
33
- 'windsurf': 'append',
34
- 'cline': 'append',
27
+ cursor: 'own-file',
28
+ roo: 'own-file',
29
+ kiro: 'own-file',
30
+ trae: 'own-file',
31
+ windsurf: 'append',
32
+ cline: 'append',
35
33
  'gemini-cli': 'append',
36
34
  'github-copilot': 'append',
37
35
  };
@@ -61,7 +59,7 @@ function getSystemPromptMode(tool) {
61
59
  }
62
60
 
63
61
  function isKnownTool(tool) {
64
- return Object.prototype.hasOwnProperty.call(TOOL_DIRS, tool);
62
+ return Object.hasOwn(TOOL_DIRS, tool);
65
63
  }
66
64
 
67
65
  module.exports = {
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  const { execFile } = require('node:child_process');
4
2
  const semver = require('semver');
5
3
 
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  const path = require('node:path');
4
2
  const fs = require('fs-extra');
5
3
 
package/lib/prompts.js CHANGED
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  let clackPromise;
4
2
 
5
3
  function loadClack() {
package/lib/substitute.js CHANGED
@@ -1,10 +1,6 @@
1
- 'use strict';
2
-
3
1
  const path = require('node:path');
4
2
 
5
- const TEXT_EXTENSIONS = new Set([
6
- '.md', '.yaml', '.yml', '.json', '.sh', '.txt',
7
- ]);
3
+ const TEXT_EXTENSIONS = new Set(['.md', '.yaml', '.yml', '.json', '.sh', '.txt']);
8
4
 
9
5
  function isTextFile(filePath) {
10
6
  return TEXT_EXTENSIONS.has(path.extname(filePath).toLowerCase());