mandrel 1.60.0 → 1.62.0
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/.agents/README.md +74 -32
- package/.agents/docs/SDLC.md +18 -12
- package/.agents/docs/configuration.md +61 -4
- package/.agents/docs/quality-gates.md +796 -0
- package/.agents/docs/workflows.md +3 -4
- package/.agents/runtime-deps.json +2 -2
- package/.agents/scripts/README.md +1 -1
- package/.agents/scripts/agents-bootstrap-github.js +23 -119
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +46 -0
- package/.agents/scripts/lib/bootstrap/gh-preflight.js +7 -9
- package/.agents/scripts/lib/bootstrap/manifest.js +21 -1
- package/.agents/scripts/lib/bootstrap/merge-methods.js +31 -16
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +32 -11
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/detect-package-manager.js +72 -0
- package/.agents/scripts/lib/errors/index.js +4 -4
- package/.agents/scripts/lib/label-taxonomy.js +2 -2
- package/.agents/scripts/lib/onboard/detect-stack.js +10 -10
- package/.agents/scripts/lib/onboard/init-tail.js +218 -0
- package/.agents/scripts/lib/onboard/scaffold-docs.js +18 -3
- package/.agents/scripts/lib/runtime-deps/preflight.js +6 -6
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +5 -2
- package/.agents/workflows/agents-update.md +14 -29
- package/.agents/workflows/deliver.md +87 -26
- package/.agents/workflows/helpers/agents-sync-config.md +3 -2
- package/.agents/workflows/helpers/deliver-epic.md +12 -5
- package/.agents/workflows/helpers/deliver-stories.md +13 -7
- package/.agents/workflows/plan.md +48 -4
- package/README.md +18 -30
- package/bin/mandrel.js +235 -16
- package/docs/CHANGELOG.md +36 -0
- package/lib/cli/doctor.js +45 -3
- package/lib/cli/init.js +66 -7
- package/lib/cli/registry.js +42 -146
- package/lib/cli/sync.js +122 -23
- package/lib/cli/uninstall.js +42 -7
- package/lib/cli/update.js +257 -198
- package/lib/cli/version-helpers.js +59 -0
- package/package.json +6 -6
- package/.agents/workflows/onboard.md +0 -208
- package/lib/cli/__tests__/migrate.test.js +0 -268
- package/lib/cli/__tests__/sync-local-zone.test.js +0 -247
- package/lib/cli/__tests__/sync.test.js +0 -372
- package/lib/cli/__tests__/update-changelog-surface.test.js +0 -357
- package/lib/cli/__tests__/update-major.test.js +0 -217
- package/lib/cli/__tests__/update-reexec.test.js +0 -513
- package/lib/cli/__tests__/update.test.js +0 -696
- package/lib/cli/__tests__/version-check.test.js +0 -398
- package/lib/migrations/__tests__/index.test.js +0 -216
package/bin/mandrel.js
CHANGED
|
@@ -2,35 +2,252 @@
|
|
|
2
2
|
// bin/mandrel.js — mandrel CLI entry point
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Allowlist-based subcommand dispatcher.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Only modules listed in SUBCOMMANDS are dispatchable. Each entry declares the
|
|
8
|
+
* name, a one-line description for help output, and the set of known flags so
|
|
9
|
+
* the dispatcher can reject unknown flags before loading the subcommand.
|
|
10
|
+
*
|
|
11
|
+
* Supported top-level flags:
|
|
12
|
+
* --help / -h Print subcommand list and exit 0.
|
|
13
|
+
* --version Print installed version and exit 0.
|
|
14
|
+
*
|
|
15
|
+
* Each subcommand module must export a default function `run(argv)`.
|
|
10
16
|
*/
|
|
11
17
|
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
12
19
|
import path from 'node:path';
|
|
13
20
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
14
21
|
|
|
15
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Subcommand registry — the ONLY allowed dispatch targets
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {{ description: string, knownFlags: Set<string> }} SubcommandMeta
|
|
30
|
+
* @type {Map<string, SubcommandMeta>}
|
|
31
|
+
*/
|
|
32
|
+
const SUBCOMMANDS = new Map([
|
|
33
|
+
[
|
|
34
|
+
'init',
|
|
35
|
+
{
|
|
36
|
+
description: 'install and configure mandrel in the current project',
|
|
37
|
+
knownFlags: new Set([
|
|
38
|
+
'--assume-yes',
|
|
39
|
+
'--skip-github',
|
|
40
|
+
'--owner',
|
|
41
|
+
'--repo',
|
|
42
|
+
'--base-branch',
|
|
43
|
+
'--project-number',
|
|
44
|
+
'--operator-handle',
|
|
45
|
+
'--dry-run',
|
|
46
|
+
]),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
[
|
|
50
|
+
'sync',
|
|
51
|
+
{
|
|
52
|
+
description: 'materialize .agents/ payload from installed package',
|
|
53
|
+
knownFlags: new Set(['--dry-run']),
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
'sync-commands',
|
|
58
|
+
{
|
|
59
|
+
description: 'regenerate .claude/commands/ from .agents/workflows/',
|
|
60
|
+
knownFlags: new Set(['--dry-run']),
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
'doctor',
|
|
65
|
+
{
|
|
66
|
+
description: 'run readiness checks and report remedies',
|
|
67
|
+
knownFlags: new Set([]),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
[
|
|
71
|
+
'update',
|
|
72
|
+
{
|
|
73
|
+
description: 'upgrade mandrel to the newest published version',
|
|
74
|
+
knownFlags: new Set(['--dry-run', '--install-cmd']),
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
'migrate',
|
|
79
|
+
{
|
|
80
|
+
description: 'apply version-keyed migrations for a version range',
|
|
81
|
+
knownFlags: new Set(['--from', '--to', '--dry-run']),
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
'explain',
|
|
86
|
+
{
|
|
87
|
+
description: 'print resolved config values and their sources',
|
|
88
|
+
knownFlags: new Set(['--json']),
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
'uninstall',
|
|
93
|
+
{
|
|
94
|
+
description: 'reverse a recorded install using the install ledger',
|
|
95
|
+
knownFlags: new Set(['--include-github', '--dry-run']),
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Helpers
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Read the installed mandrel version from the root package.json.
|
|
106
|
+
*
|
|
107
|
+
* @returns {string}
|
|
108
|
+
*/
|
|
109
|
+
function installedVersion() {
|
|
110
|
+
const req = createRequire(import.meta.url);
|
|
111
|
+
const manifest = req('../package.json');
|
|
112
|
+
return String(manifest.version);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Print the help screen listing all subcommands with descriptions.
|
|
117
|
+
*
|
|
118
|
+
* @param {(s: string) => void} [write]
|
|
119
|
+
*/
|
|
120
|
+
function printHelp(write = (s) => process.stdout.write(s)) {
|
|
121
|
+
const lines = ['Usage: mandrel <subcommand> [args]\n', '\nSubcommands:\n'];
|
|
122
|
+
for (const [name, meta] of SUBCOMMANDS) {
|
|
123
|
+
const pad = ' '.repeat(Math.max(1, 16 - name.length));
|
|
124
|
+
lines.push(` ${name}${pad}${meta.description}\n`);
|
|
125
|
+
}
|
|
126
|
+
lines.push(
|
|
127
|
+
'\nFlags:\n',
|
|
128
|
+
' --help, -h Print this help message\n',
|
|
129
|
+
' --version Print the installed version\n',
|
|
130
|
+
'\nRun `mandrel <subcommand> --help` for subcommand-specific flags.\n',
|
|
131
|
+
);
|
|
132
|
+
write(lines.join(''));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Suggest the closest known subcommand name for a typo (Levenshtein-1).
|
|
137
|
+
*
|
|
138
|
+
* @param {string} input
|
|
139
|
+
* @returns {string | undefined}
|
|
140
|
+
*/
|
|
141
|
+
function suggest(input) {
|
|
142
|
+
for (const name of SUBCOMMANDS.keys()) {
|
|
143
|
+
if (levenshtein(input, name) <= 2) return name;
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Compute Levenshtein edit distance between two strings (capped at 3 for
|
|
150
|
+
* performance — we only care about small distances).
|
|
151
|
+
*
|
|
152
|
+
* @param {string} a
|
|
153
|
+
* @param {string} b
|
|
154
|
+
* @returns {number}
|
|
155
|
+
*/
|
|
156
|
+
function levenshtein(a, b) {
|
|
157
|
+
if (a === b) return 0;
|
|
158
|
+
if (Math.abs(a.length - b.length) > 3) return 4;
|
|
159
|
+
const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
160
|
+
for (let i = 0; i < a.length; i++) {
|
|
161
|
+
const curr = [i + 1];
|
|
162
|
+
for (let j = 0; j < b.length; j++) {
|
|
163
|
+
curr[j + 1] = Math.min(
|
|
164
|
+
curr[j] + 1,
|
|
165
|
+
prev[j + 1] + 1,
|
|
166
|
+
prev[j] + (a[i] === b[j] ? 0 : 1),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
prev.splice(0, prev.length, ...curr);
|
|
170
|
+
}
|
|
171
|
+
return prev[b.length];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Universal flags that all subcommands accept and that bypass per-subcommand
|
|
176
|
+
* flag validation. Subcommands may handle these themselves internally.
|
|
177
|
+
*/
|
|
178
|
+
const UNIVERSAL_FLAGS = new Set(['--help', '-h']);
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Validate the argv array against the known flags for a subcommand.
|
|
182
|
+
* Returns null when all flags are known; an error message string when an
|
|
183
|
+
* unknown flag is detected. Values following `=` or the next positional are
|
|
184
|
+
* allowed — only the flag name prefix is checked.
|
|
185
|
+
*
|
|
186
|
+
* Universal flags (--help, -h) are always allowed and bypass this check.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} subName
|
|
189
|
+
* @param {Set<string>} knownFlags
|
|
190
|
+
* @param {string[]} argv
|
|
191
|
+
* @returns {string | null}
|
|
192
|
+
*/
|
|
193
|
+
function findUnknownFlag(subName, knownFlags, argv) {
|
|
194
|
+
for (const arg of argv) {
|
|
195
|
+
if (!arg.startsWith('-')) continue;
|
|
196
|
+
// Strip value portion for `--flag=value` form.
|
|
197
|
+
const flagName = arg.includes('=') ? arg.slice(0, arg.indexOf('=')) : arg;
|
|
198
|
+
if (UNIVERSAL_FLAGS.has(flagName)) continue;
|
|
199
|
+
if (!knownFlags.has(flagName)) {
|
|
200
|
+
const names = [...knownFlags].sort().join(', ');
|
|
201
|
+
const hint = names.length > 0 ? ` Known flags: ${names}\n` : '';
|
|
202
|
+
return (
|
|
203
|
+
`mandrel: unknown flag '${flagName}' for subcommand '${subName}'\n` +
|
|
204
|
+
hint
|
|
205
|
+
);
|
|
206
|
+
}
|
|
24
207
|
}
|
|
208
|
+
return null;
|
|
25
209
|
}
|
|
26
210
|
|
|
27
|
-
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Dispatch
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
const args = process.argv.slice(2);
|
|
216
|
+
const sub = args[0];
|
|
217
|
+
|
|
218
|
+
// --- Top-level flags (no subcommand) ---
|
|
219
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
220
|
+
printHelp();
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
28
223
|
|
|
29
|
-
if (
|
|
30
|
-
|
|
224
|
+
if (sub === '--version') {
|
|
225
|
+
process.stdout.write(`${installedVersion()}\n`);
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// --- Subcommand lookup ---
|
|
230
|
+
const meta = SUBCOMMANDS.get(sub);
|
|
231
|
+
if (!meta) {
|
|
232
|
+
const subcommandList = [...SUBCOMMANDS.keys()].join(', ');
|
|
233
|
+
const hint = suggest(sub);
|
|
234
|
+
const didYouMean = hint ? `\n Did you mean '${hint}'?` : '';
|
|
235
|
+
process.stderr.write(
|
|
236
|
+
`mandrel: unknown subcommand '${sub}'${didYouMean}\n` +
|
|
237
|
+
` Available subcommands: ${subcommandList}\n`,
|
|
238
|
+
);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// --- Unknown-flag rejection ---
|
|
243
|
+
const subArgv = args.slice(1);
|
|
244
|
+
const unknownFlagError = findUnknownFlag(sub, meta.knownFlags, subArgv);
|
|
245
|
+
if (unknownFlagError) {
|
|
246
|
+
process.stderr.write(unknownFlagError);
|
|
31
247
|
process.exit(1);
|
|
32
248
|
}
|
|
33
249
|
|
|
250
|
+
// --- Load and dispatch ---
|
|
34
251
|
const subFile = path.resolve(__dirname, '..', 'lib', 'cli', `${sub}.js`);
|
|
35
252
|
const subFileUrl = pathToFileURL(subFile).href;
|
|
36
253
|
|
|
@@ -39,7 +256,9 @@ try {
|
|
|
39
256
|
mod = await import(subFileUrl);
|
|
40
257
|
} catch (err) {
|
|
41
258
|
if (err.code === 'ERR_MODULE_NOT_FOUND' && err.message.includes(subFile)) {
|
|
42
|
-
|
|
259
|
+
process.stderr.write(
|
|
260
|
+
`mandrel: subcommand '${sub}' module not found — this is a bug\n`,
|
|
261
|
+
);
|
|
43
262
|
process.exit(1);
|
|
44
263
|
}
|
|
45
264
|
// Re-throw broken-module errors so they are visible rather than masked.
|
|
@@ -53,4 +272,4 @@ if (typeof mod.default !== 'function') {
|
|
|
53
272
|
process.exit(1);
|
|
54
273
|
}
|
|
55
274
|
|
|
56
|
-
await mod.default(
|
|
275
|
+
await mod.default(subArgv);
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.62.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.61.0...mandrel-v1.62.0) (2026-06-12)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
* /deliver composes a sequential segment plan over mixed Epic and standalone-Story ID sets (refs [#4062](https://github.com/dsj1984/mandrel/issues/4062)) ([#4063](https://github.com/dsj1984/mandrel/issues/4063)) ([a83d29e](https://github.com/dsj1984/mandrel/commit/a83d29e032b7f6b6846d7988f071d6b6416df2f9))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
* make mandrel update no-op short-circuit drift-aware (refs [#4065](https://github.com/dsj1984/mandrel/issues/4065)) ([#4066](https://github.com/dsj1984/mandrel/issues/4066)) ([693f1cf](https://github.com/dsj1984/mandrel/commit/693f1cf5bf8fd7d29fed910659708ffceb7a54cb))
|
|
16
|
+
|
|
17
|
+
## [1.61.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.60.0...mandrel-v1.61.0) (2026-06-12)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### ⚠ BREAKING CHANGES
|
|
21
|
+
|
|
22
|
+
* mandrel update no longer refuses major version bumps and the --major flag is removed; major upgrades apply like any other update.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
* **init:** fold onboarding into mandrel init, retire /onboard (refs [#4045](https://github.com/dsj1984/mandrel/issues/4045)) ([#4050](https://github.com/dsj1984/mandrel/issues/4050)) ([2b2c115](https://github.com/dsj1984/mandrel/commit/2b2c115ad4f5394cefb2ecfcc72ddbd0dae6683a))
|
|
27
|
+
* make the CLI behave like a CLI: dispatcher, help, flag rejection, TS peer, engines floor ([#4047](https://github.com/dsj1984/mandrel/issues/4047)) ([#4053](https://github.com/dsj1984/mandrel/issues/4053)) ([c625573](https://github.com/dsj1984/mandrel/commit/c625573c9feb0908d897bb0f2778c2b22ee28b04))
|
|
28
|
+
* one implementation per concept: install-surface dead-code deletion and helper unification ([#4048](https://github.com/dsj1984/mandrel/issues/4048)) ([#4054](https://github.com/dsj1984/mandrel/issues/4054)) ([ffb5dc7](https://github.com/dsj1984/mandrel/commit/ffb5dc72f25fedc70829dd70c64e14e3dfa18c72))
|
|
29
|
+
* remediate documentation audit, remove the major-upgrade gate, and retire stranded consumer runbooks ([#4061](https://github.com/dsj1984/mandrel/issues/4061)) ([fa429e4](https://github.com/dsj1984/mandrel/commit/fa429e490b7d410938eeec24ef44ffc7857cf470))
|
|
30
|
+
* **update:** make upgrade loop truthful — cache bypass, sync prune, cwd anchoring (refs [#4046](https://github.com/dsj1984/mandrel/issues/4046)) ([#4051](https://github.com/dsj1984/mandrel/issues/4051)) ([1a89a7c](https://github.com/dsj1984/mandrel/commit/1a89a7cc1150f26d87a93f58e2fd940670604381))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
* close the 9 post-merge review findings on the install/bootstrap batch ([#4060](https://github.com/dsj1984/mandrel/issues/4060)) ([4e76441](https://github.com/dsj1984/mandrel/commit/4e76441c1620481e49ac33e4884e604dc8366f48))
|
|
36
|
+
* packaging and CI prove the install contract; retire the install-bootstrap review doc ([#4049](https://github.com/dsj1984/mandrel/issues/4049)) ([#4057](https://github.com/dsj1984/mandrel/issues/4057)) ([1051ed1](https://github.com/dsj1984/mandrel/commit/1051ed130f5720edcb3ca8e4cfe898ec00495eb5))
|
|
37
|
+
* **tests:** add shell:true to spawnSync for Windows npm.cmd compat (refs [#4049](https://github.com/dsj1984/mandrel/issues/4049)) ([#4059](https://github.com/dsj1984/mandrel/issues/4059)) ([f5abcd5](https://github.com/dsj1984/mandrel/commit/f5abcd550e7e4110372b35c56abe55931ce3058a))
|
|
38
|
+
* **transpile:** add typescript to devDependencies so Windows CI finds it ([#4055](https://github.com/dsj1984/mandrel/issues/4055)) ([8f0a6bd](https://github.com/dsj1984/mandrel/commit/8f0a6bdf6681286d8b691e1b9163639613ce8f9f))
|
|
39
|
+
* **transpile:** use pathToFileURL for ESM import on Windows ([#4056](https://github.com/dsj1984/mandrel/issues/4056)) ([2e3d210](https://github.com/dsj1984/mandrel/commit/2e3d210b8e351e8b23ae4e5272ff7d950792b29b))
|
|
40
|
+
|
|
5
41
|
## [1.60.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.59.0...mandrel-v1.60.0) (2026-06-11)
|
|
6
42
|
|
|
7
43
|
|
package/lib/cli/doctor.js
CHANGED
|
@@ -17,11 +17,15 @@
|
|
|
17
17
|
* Exit code 0 = all pass, 1 = any fail.
|
|
18
18
|
*
|
|
19
19
|
* Injectable seams (used by tests):
|
|
20
|
-
* - `checks`
|
|
21
|
-
* - `write`
|
|
22
|
-
* - `exit`
|
|
20
|
+
* - `checks` — replaces the default registry array
|
|
21
|
+
* - `write` — replaces process.stdout.write
|
|
22
|
+
* - `exit` — replaces process.exit
|
|
23
|
+
* - `writeResultCache` — replaces the temp/doctor-result.json writer
|
|
23
24
|
*/
|
|
24
25
|
|
|
26
|
+
import nodeFs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
|
|
25
29
|
import { registry } from './registry.js';
|
|
26
30
|
|
|
27
31
|
// ---------------------------------------------------------------------------
|
|
@@ -74,6 +78,40 @@ function formatSummary(passed, total) {
|
|
|
74
78
|
return `❌ Not ready (${failed}/${total} checks failed)\n`;
|
|
75
79
|
}
|
|
76
80
|
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Result cache
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Best-effort write of the doctor verdict to `temp/doctor-result.json` under
|
|
87
|
+
* the consumer root so downstream workflows (e.g. the `/plan` first-run
|
|
88
|
+
* preflight) can read the last recorded verdict without re-running doctor.
|
|
89
|
+
* `temp/` is the gitignored scratch root, so neither git nor the sync prune
|
|
90
|
+
* pass ever sees the cache. Any write failure is swallowed — the cache is
|
|
91
|
+
* advisory, never a gate.
|
|
92
|
+
*
|
|
93
|
+
* Exported for testing.
|
|
94
|
+
*
|
|
95
|
+
* @param {'ready'|'unready'} verdict
|
|
96
|
+
* @param {{ fs?: typeof nodeFs, cwd?: () => string }} [opts]
|
|
97
|
+
* @returns {void}
|
|
98
|
+
*/
|
|
99
|
+
export function writeDoctorResultCache(
|
|
100
|
+
verdict,
|
|
101
|
+
{ fs = nodeFs, cwd = process.cwd } = {},
|
|
102
|
+
) {
|
|
103
|
+
try {
|
|
104
|
+
const dir = path.join(cwd(), 'temp');
|
|
105
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
106
|
+
fs.writeFileSync(
|
|
107
|
+
path.join(dir, 'doctor-result.json'),
|
|
108
|
+
`${JSON.stringify({ verdict, checkedAt: new Date().toISOString() }, null, 2)}\n`,
|
|
109
|
+
);
|
|
110
|
+
} catch {
|
|
111
|
+
// Best-effort only — an unwritable temp/ must never fail the doctor run.
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
77
115
|
// ---------------------------------------------------------------------------
|
|
78
116
|
// Doctor runner (exported for testing)
|
|
79
117
|
// ---------------------------------------------------------------------------
|
|
@@ -85,6 +123,7 @@ function formatSummary(passed, total) {
|
|
|
85
123
|
* checks?: Array<{ name: string, run(opts?: unknown): { ok: boolean, detail: string, remedy?: string } }>,
|
|
86
124
|
* write?: (s: string) => void,
|
|
87
125
|
* exit?: (code: number) => void,
|
|
126
|
+
* writeResultCache?: (verdict: 'ready'|'unready') => void,
|
|
88
127
|
* }} [opts]
|
|
89
128
|
* @returns {void}
|
|
90
129
|
*/
|
|
@@ -92,6 +131,7 @@ export async function runDoctor({
|
|
|
92
131
|
checks = registry,
|
|
93
132
|
write = (s) => process.stdout.write(s),
|
|
94
133
|
exit = (code) => process.exit(code),
|
|
134
|
+
writeResultCache = writeDoctorResultCache,
|
|
95
135
|
} = {}) {
|
|
96
136
|
let passed = 0;
|
|
97
137
|
|
|
@@ -104,6 +144,8 @@ export async function runDoctor({
|
|
|
104
144
|
const total = checks.length;
|
|
105
145
|
write(formatSummary(passed, total));
|
|
106
146
|
|
|
147
|
+
writeResultCache(passed === total ? 'ready' : 'unready');
|
|
148
|
+
|
|
107
149
|
if (passed < total) {
|
|
108
150
|
exit(1);
|
|
109
151
|
}
|
package/lib/cli/init.js
CHANGED
|
@@ -66,6 +66,30 @@
|
|
|
66
66
|
import { spawnSync } from 'node:child_process';
|
|
67
67
|
import fs from 'node:fs';
|
|
68
68
|
import path from 'node:path';
|
|
69
|
+
import { pathToFileURL } from 'node:url';
|
|
70
|
+
|
|
71
|
+
// Lazily resolved at runtime so cold-start `npx mandrel init` (where
|
|
72
|
+
// `.agents/` is absent) does not try to import from a directory that does not
|
|
73
|
+
// exist yet. The import is performed after bootstrap succeeds and `.agents/`
|
|
74
|
+
// is guaranteed to be materialised.
|
|
75
|
+
let _runInitTail = null;
|
|
76
|
+
async function getRunInitTail(projectRoot) {
|
|
77
|
+
if (_runInitTail) return _runInitTail;
|
|
78
|
+
const tailPath = path.join(
|
|
79
|
+
projectRoot,
|
|
80
|
+
'.agents',
|
|
81
|
+
'scripts',
|
|
82
|
+
'lib',
|
|
83
|
+
'onboard',
|
|
84
|
+
'init-tail.js',
|
|
85
|
+
);
|
|
86
|
+
// pathToFileURL handles Windows drive letters correctly (a raw
|
|
87
|
+
// `file://C:\…` template treats the drive letter as a URL host — same
|
|
88
|
+
// fix as commit 2e3d210b in lib/transpile.js).
|
|
89
|
+
const mod = await import(pathToFileURL(tailPath).href);
|
|
90
|
+
_runInitTail = mod.runInitTail;
|
|
91
|
+
return _runInitTail;
|
|
92
|
+
}
|
|
69
93
|
|
|
70
94
|
/**
|
|
71
95
|
* Hardcoded build-time package name. NEVER read from argv or env — cold-start
|
|
@@ -154,21 +178,23 @@ function buildBootstrapArgs(argv, assumeYes) {
|
|
|
154
178
|
* confirm?: () => boolean,
|
|
155
179
|
* stdout?: (s: string) => void,
|
|
156
180
|
* isTTY?: boolean,
|
|
181
|
+
* afterBootstrap?: (root: string) => Promise<{ ok?: boolean } | void> | { ok?: boolean } | void,
|
|
157
182
|
* }} [opts]
|
|
158
|
-
* @returns {{
|
|
183
|
+
* @returns {Promise<{
|
|
159
184
|
* installed: boolean,
|
|
160
185
|
* ranBootstrap: boolean,
|
|
161
186
|
* steps: Array<{ cmd: string, args: string[] }>,
|
|
162
187
|
* exitCode: number,
|
|
163
|
-
* }}
|
|
188
|
+
* }>}
|
|
164
189
|
*/
|
|
165
|
-
export function planInit({
|
|
190
|
+
export async function planInit({
|
|
166
191
|
argv = [],
|
|
167
192
|
exists,
|
|
168
193
|
runStep,
|
|
169
194
|
confirm,
|
|
170
195
|
stdout = (s) => process.stdout.write(s),
|
|
171
196
|
isTTY,
|
|
197
|
+
afterBootstrap,
|
|
172
198
|
} = {}) {
|
|
173
199
|
const steps = [];
|
|
174
200
|
|
|
@@ -243,11 +269,37 @@ export function planInit({
|
|
|
243
269
|
BOOTSTRAP_SCRIPT,
|
|
244
270
|
...bootstrapArgs,
|
|
245
271
|
]);
|
|
272
|
+
if (bootstrapStatus !== 0) {
|
|
273
|
+
return {
|
|
274
|
+
installed,
|
|
275
|
+
ranBootstrap: true,
|
|
276
|
+
steps,
|
|
277
|
+
exitCode: bootstrapStatus,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Bootstrap succeeded — run the onboarding tail: stack detection, docs
|
|
282
|
+
// scaffolding offer, doctor gate, and /plan handoff. A tail that reports
|
|
283
|
+
// `ok: false` (the doctor gate failed) makes the whole init exit
|
|
284
|
+
// non-zero; the tail already printed its own remediation message, and the
|
|
285
|
+
// earlier install/sync/bootstrap phases' results stand as completed.
|
|
286
|
+
if (afterBootstrap) {
|
|
287
|
+
const tail = await afterBootstrap(process.cwd());
|
|
288
|
+
if (tail && tail.ok === false) {
|
|
289
|
+
return {
|
|
290
|
+
installed,
|
|
291
|
+
ranBootstrap: true,
|
|
292
|
+
steps,
|
|
293
|
+
exitCode: 1,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
246
298
|
return {
|
|
247
299
|
installed,
|
|
248
300
|
ranBootstrap: true,
|
|
249
301
|
steps,
|
|
250
|
-
exitCode:
|
|
302
|
+
exitCode: 0,
|
|
251
303
|
};
|
|
252
304
|
}
|
|
253
305
|
|
|
@@ -309,9 +361,9 @@ function defaultConfirm() {
|
|
|
309
361
|
* Default export consumed by `bin/mandrel.js`.
|
|
310
362
|
*
|
|
311
363
|
* @param {string[]} [argv] - Subcommand arguments (after `mandrel init`).
|
|
312
|
-
* @returns {void}
|
|
364
|
+
* @returns {Promise<void>}
|
|
313
365
|
*/
|
|
314
|
-
export default function run(argv = []) {
|
|
366
|
+
export default async function run(argv = []) {
|
|
315
367
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
316
368
|
process.stdout.write(
|
|
317
369
|
'Usage: mandrel init [bootstrap flags]\n\n' +
|
|
@@ -324,12 +376,19 @@ export default function run(argv = []) {
|
|
|
324
376
|
return;
|
|
325
377
|
}
|
|
326
378
|
|
|
327
|
-
const result = planInit({
|
|
379
|
+
const result = await planInit({
|
|
328
380
|
argv,
|
|
329
381
|
exists: defaultExists,
|
|
330
382
|
runStep: defaultRunStep,
|
|
331
383
|
confirm: defaultConfirm,
|
|
332
384
|
isTTY: Boolean(process.stdin.isTTY),
|
|
385
|
+
afterBootstrap: async (projectRoot) => {
|
|
386
|
+
const runInitTail = await getRunInitTail(projectRoot);
|
|
387
|
+
return runInitTail({
|
|
388
|
+
root: projectRoot,
|
|
389
|
+
isTTY: Boolean(process.stdin.isTTY),
|
|
390
|
+
});
|
|
391
|
+
},
|
|
333
392
|
});
|
|
334
393
|
|
|
335
394
|
if (result.exitCode !== 0) {
|