@ikunin/sprintpilot 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/_Sprintpilot/lib/runtime/args.js +0 -2
- package/_Sprintpilot/lib/runtime/git.js +0 -2
- package/_Sprintpilot/lib/runtime/http.js +12 -5
- package/_Sprintpilot/lib/runtime/log.js +0 -2
- package/_Sprintpilot/lib/runtime/secrets.js +14 -16
- package/_Sprintpilot/lib/runtime/spawn.js +21 -8
- package/_Sprintpilot/lib/runtime/text.js +0 -2
- package/_Sprintpilot/lib/runtime/yaml-lite.js +9 -5
- package/_Sprintpilot/manifest.yaml +1 -1
- package/_Sprintpilot/scripts/create-pr.js +76 -38
- package/_Sprintpilot/scripts/detect-platform.js +35 -10
- package/_Sprintpilot/scripts/health-check.js +17 -8
- package/_Sprintpilot/scripts/lint-changed.js +35 -16
- package/_Sprintpilot/scripts/lock.js +22 -6
- package/_Sprintpilot/scripts/sanitize-branch.js +4 -2
- package/_Sprintpilot/scripts/stage-and-commit.js +15 -7
- package/_Sprintpilot/scripts/sync-status.js +16 -6
- package/bin/sprintpilot.js +11 -4
- package/lib/commands/check-update.js +0 -2
- package/lib/commands/install.js +139 -49
- package/lib/commands/uninstall.js +21 -11
- package/lib/core/bmad-config.js +0 -2
- package/lib/core/file-ops.js +6 -6
- package/lib/core/gitignore.js +0 -2
- package/lib/core/markers.js +5 -3
- package/lib/core/tool-registry.js +19 -21
- package/lib/core/update-check.js +0 -2
- package/lib/core/v1-detect.js +0 -2
- package/lib/prompts.js +0 -2
- package/lib/substitute.js +1 -5
- package/package.json +1 -1
|
@@ -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);
|
|
@@ -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 = [
|
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');
|