@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
package/bin/sprintpilot.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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 {
|
package/lib/commands/install.js
CHANGED
|
@@ -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,
|
|
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 = [
|
|
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
|
|
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
|
|
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
|
-
|
|
139
|
-
|
|
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(
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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'], {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
429
|
-
|
|
430
|
-
|
|
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 {
|
|
488
|
+
} catch {
|
|
489
|
+
/* treat as non-TTY */
|
|
490
|
+
}
|
|
444
491
|
if (!migrateV1 && !yes && !isTTY) {
|
|
445
|
-
console.error(
|
|
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:
|
|
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(
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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 };
|
package/lib/core/bmad-config.js
CHANGED
package/lib/core/file-ops.js
CHANGED
|
@@ -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, {
|
|
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) {
|
package/lib/core/gitignore.js
CHANGED
package/lib/core/markers.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
62
|
+
return Object.hasOwn(TOOL_DIRS, tool);
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
module.exports = {
|
package/lib/core/update-check.js
CHANGED
package/lib/core/v1-detect.js
CHANGED
package/lib/prompts.js
CHANGED
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());
|