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
|
@@ -52,10 +52,10 @@ export class GhVersionError extends Error {
|
|
|
52
52
|
/**
|
|
53
53
|
* Raised when a framework runtime dependency (e.g. `ajv`) cannot be
|
|
54
54
|
* resolved from the consumer's `node_modules/`. Surfaces during the
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
55
|
+
* `agents-bootstrap-github` preflight to redirect operators to the
|
|
56
|
+
* correct remediation (`mandrel init` for new projects, or
|
|
57
|
+
* `npm install mandrel` for existing ones). `missing` carries the
|
|
58
|
+
* package specifiers that failed to resolve so the CLI can render an
|
|
59
59
|
* actionable hint.
|
|
60
60
|
*/
|
|
61
61
|
export class MissingRuntimeDepsError extends Error {
|
|
@@ -164,8 +164,8 @@ export const STATUS_FIELD_OPTIONS = ['Todo', 'In Progress', 'Done'];
|
|
|
164
164
|
*
|
|
165
165
|
* GitHub's GraphQL surface does not expose a public `createProjectV2View`
|
|
166
166
|
* mutation, so bootstrap creates these via the REST Projects V2 views
|
|
167
|
-
* endpoint best-effort
|
|
168
|
-
*
|
|
167
|
+
* endpoint best-effort; when the endpoint is unavailable the views must be
|
|
168
|
+
* configured manually in the GitHub Projects UI.
|
|
169
169
|
*
|
|
170
170
|
* @type {Array<{ name: string, filter: string, groupBy: string,
|
|
171
171
|
* layout?: 'table'|'board'|'roadmap' }>}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* detect-stack.js — Consumer stack detection for
|
|
2
|
+
* detect-stack.js — Consumer stack detection for `mandrel init`
|
|
3
3
|
*
|
|
4
4
|
* Inspects a consumer repository root and reports the package manager,
|
|
5
5
|
* test runner, and primary language it can infer from on-disk signals
|
|
6
6
|
* (lockfiles, `package.json` contents, and source-file extensions). The
|
|
7
|
-
*
|
|
8
|
-
* what it found before scaffolding missing
|
|
7
|
+
* `mandrel init` configure-path tail (Feature #3514, Story #4045) uses
|
|
8
|
+
* this to tell the operator what it found before scaffolding missing
|
|
9
|
+
* `docsContextFiles`.
|
|
9
10
|
*
|
|
10
11
|
* The detection functions are seam-injectable: each takes an injected
|
|
11
12
|
* filesystem facade (`exists` / `readFile` / `listExtensions`) so they
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
|
|
20
21
|
import fs from 'node:fs';
|
|
21
22
|
import path from 'node:path';
|
|
23
|
+
import { detectPackageManager as detectPm } from '../detect-package-manager.js';
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* Filesystem facade. Pure detection logic talks to disk only through
|
|
@@ -163,18 +165,16 @@ export const defaultFsFacade = {
|
|
|
163
165
|
* when no lockfile is found but a `package.json` exists, and `null` when
|
|
164
166
|
* the repo has no Node manifest at all.
|
|
165
167
|
*
|
|
168
|
+
* Delegates to the shared `detectPackageManager` helper
|
|
169
|
+
* (Story #4048 B3 — one implementation per concept). The `fsFacade.exists`
|
|
170
|
+
* seam is forwarded directly.
|
|
171
|
+
*
|
|
166
172
|
* @param {string} root - Repository root.
|
|
167
173
|
* @param {FsFacade} [fsFacade=defaultFsFacade]
|
|
168
174
|
* @returns {'pnpm'|'yarn'|'bun'|'npm'|null}
|
|
169
175
|
*/
|
|
170
176
|
export function detectPackageManager(root, fsFacade = defaultFsFacade) {
|
|
171
|
-
|
|
172
|
-
if (exists(path.join(root, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
173
|
-
if (exists(path.join(root, 'yarn.lock'))) return 'yarn';
|
|
174
|
-
if (exists(path.join(root, 'bun.lockb'))) return 'bun';
|
|
175
|
-
if (exists(path.join(root, 'package-lock.json'))) return 'npm';
|
|
176
|
-
if (exists(path.join(root, 'package.json'))) return 'npm';
|
|
177
|
-
return null;
|
|
177
|
+
return detectPm(root, fsFacade.exists);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
/**
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init-tail.js — post-bootstrap onboarding tail for `mandrel init`.
|
|
3
|
+
*
|
|
4
|
+
* Called by `mandrel init` after `bootstrap.js` completes successfully on
|
|
5
|
+
* the "configure now" path. Sequences the four phases that walk an operator
|
|
6
|
+
* from a freshly bootstrapped project to a ready-to-plan workspace:
|
|
7
|
+
*
|
|
8
|
+
* Phase 1 — Detect the consumer stack (lib/onboard/detect-stack.js).
|
|
9
|
+
* Phase 2 — Offer to scaffold missing docsContextFiles (scaffold-docs.js).
|
|
10
|
+
* Phase 3 — Run `mandrel doctor` as a readiness gate.
|
|
11
|
+
* Phase 4 — Print the /plan handoff next-step text.
|
|
12
|
+
*
|
|
13
|
+
* The whole tail is idempotent: re-running after an already-onboarded project
|
|
14
|
+
* re-detects, re-checks, and re-offers scaffolding without duplicating stubs
|
|
15
|
+
* (the scaffolder only writes genuinely absent files) and without modifying
|
|
16
|
+
* anything (doctor is read-only).
|
|
17
|
+
*
|
|
18
|
+
* Injectable seams: `runDoctor`, `stdout`, `confirmScaffold`, and `isTTY`
|
|
19
|
+
* allow the unit suite to drive every branch without real I/O.
|
|
20
|
+
*
|
|
21
|
+
* Story #4045 (refs #4045).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { spawnSync as defaultSpawnSync } from 'node:child_process';
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
|
|
28
|
+
import { detectStack } from './detect-stack.js';
|
|
29
|
+
import { STUB_MARKER, scaffoldDocs } from './scaffold-docs.js';
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Phase text constants
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Text printed at the end of the init tail to hand the operator off to /plan.
|
|
37
|
+
*
|
|
38
|
+
* @type {string}
|
|
39
|
+
*/
|
|
40
|
+
export const PLAN_HANDOFF_TEXT =
|
|
41
|
+
'\n✅ Mandrel is ready. Start your first Epic:\n\n' +
|
|
42
|
+
' /plan --idea "<one-line description of what you want to build>"\n\n' +
|
|
43
|
+
'Or, if you already have a `type::epic` Issue open:\n\n' +
|
|
44
|
+
' /plan <epicId>\n';
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Internal helpers
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format a stack-detection result as a human-readable report line.
|
|
52
|
+
*
|
|
53
|
+
* @param {{ packageManager: string|null, testRunner: string|null, primaryLanguage: string|null }} stack
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
function formatStackReport(stack) {
|
|
57
|
+
const pm = stack.packageManager ?? '(unknown)';
|
|
58
|
+
const runner = stack.testRunner ?? '(unknown)';
|
|
59
|
+
const lang = stack.primaryLanguage ?? '(unknown)';
|
|
60
|
+
return (
|
|
61
|
+
'\n[init] Stack detection:\n' +
|
|
62
|
+
` Package manager : ${pm}\n` +
|
|
63
|
+
` Test runner : ${runner}\n` +
|
|
64
|
+
` Primary language: ${lang}\n`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Format a list of missing docs as a human-readable report (no prompt).
|
|
70
|
+
*
|
|
71
|
+
* @param {string[]} missing
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
function formatMissingList(missing) {
|
|
75
|
+
if (missing.length === 0) return '';
|
|
76
|
+
const list = missing.map((f) => ` • ${f}`).join('\n');
|
|
77
|
+
return (
|
|
78
|
+
'\n[init] The following docsContextFiles are missing — agents load\n' +
|
|
79
|
+
'these before every task:\n' +
|
|
80
|
+
`${list}\n`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Prompt text shown only on a TTY when asking to scaffold. */
|
|
85
|
+
const SCAFFOLD_PROMPT = '\nScaffold stubs now? [y/N]: ';
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Synchronous y/N read from stdin (fd 0). Returns `false` on any read error
|
|
89
|
+
* or when the user enters something other than `y` / `yes`.
|
|
90
|
+
*
|
|
91
|
+
* @returns {boolean}
|
|
92
|
+
*/
|
|
93
|
+
function syncConfirm() {
|
|
94
|
+
let answer = '';
|
|
95
|
+
try {
|
|
96
|
+
const buf = fs.readFileSync(0, 'utf8');
|
|
97
|
+
answer = buf.split('\n', 1)[0].trim().toLowerCase();
|
|
98
|
+
} catch {
|
|
99
|
+
answer = '';
|
|
100
|
+
}
|
|
101
|
+
return answer === 'y' || answer === 'yes';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Public API
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Run the post-bootstrap init tail.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} [opts]
|
|
112
|
+
* @param {string} [opts.root] - Project root (defaults to `process.cwd()`).
|
|
113
|
+
* @param {(msg: string) => void} [opts.stdout] - Output sink (defaults to
|
|
114
|
+
* `process.stdout.write` bound to `process.stdout`).
|
|
115
|
+
* @param {() => boolean} [opts.confirmScaffold] - Read the operator's y/N
|
|
116
|
+
* answer for the scaffold offer. Returns `true` to scaffold. In non-TTY
|
|
117
|
+
* contexts defaults to `false` (no write without operator confirmation).
|
|
118
|
+
* @param {(extraArgs?: string[]) => { status: number|null }} [opts.runDoctor]
|
|
119
|
+
* - Run `mandrel doctor`; injectable for tests.
|
|
120
|
+
* @param {boolean} [opts.isTTY] - Whether stdin is a TTY (defaults to
|
|
121
|
+
* `Boolean(process.stdin.isTTY)`).
|
|
122
|
+
* @returns {{
|
|
123
|
+
* stack: { packageManager: string|null, testRunner: string|null, primaryLanguage: string|null },
|
|
124
|
+
* scaffoldResult: object,
|
|
125
|
+
* doctorStatus: number,
|
|
126
|
+
* ok: boolean,
|
|
127
|
+
* }}
|
|
128
|
+
*/
|
|
129
|
+
export function runInitTail({
|
|
130
|
+
root,
|
|
131
|
+
stdout = (s) => process.stdout.write(s),
|
|
132
|
+
confirmScaffold,
|
|
133
|
+
runDoctor,
|
|
134
|
+
isTTY,
|
|
135
|
+
} = {}) {
|
|
136
|
+
const projectRoot = root ?? process.cwd();
|
|
137
|
+
const tty = isTTY ?? Boolean(process.stdin.isTTY);
|
|
138
|
+
|
|
139
|
+
// When confirmScaffold is explicitly injected (e.g. from tests), always use
|
|
140
|
+
// it. When using the default, auto-decline on non-TTY so the scaffolder
|
|
141
|
+
// never writes unattended.
|
|
142
|
+
const usingDefaultConfirm = confirmScaffold == null;
|
|
143
|
+
const confirmFn = confirmScaffold ?? (() => syncConfirm());
|
|
144
|
+
|
|
145
|
+
// Default doctor runner — spawns `mandrel doctor` via the locally installed
|
|
146
|
+
// bin; inherits stdio so the report streams to the terminal.
|
|
147
|
+
const mandrelBin = path.join(
|
|
148
|
+
projectRoot,
|
|
149
|
+
'node_modules',
|
|
150
|
+
'mandrel',
|
|
151
|
+
'bin',
|
|
152
|
+
'mandrel.js',
|
|
153
|
+
);
|
|
154
|
+
const defaultRunDoctor = (extraArgs = []) =>
|
|
155
|
+
defaultSpawnSync(process.execPath, [mandrelBin, 'doctor', ...extraArgs], {
|
|
156
|
+
cwd: projectRoot,
|
|
157
|
+
stdio: 'inherit',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const doctorFn = runDoctor ?? defaultRunDoctor;
|
|
161
|
+
|
|
162
|
+
// --- Phase 1: Detect the stack -------------------------------------------
|
|
163
|
+
let stack;
|
|
164
|
+
try {
|
|
165
|
+
stack = detectStack(projectRoot);
|
|
166
|
+
} catch {
|
|
167
|
+
stack = { packageManager: null, testRunner: null, primaryLanguage: null };
|
|
168
|
+
}
|
|
169
|
+
stdout(formatStackReport(stack));
|
|
170
|
+
|
|
171
|
+
// --- Phase 2: Offer to scaffold missing docsContextFiles -----------------
|
|
172
|
+
const preview = scaffoldDocs({ root: projectRoot, write: false });
|
|
173
|
+
let scaffoldResult = preview;
|
|
174
|
+
|
|
175
|
+
if (preview.missing.length === 0) {
|
|
176
|
+
stdout('\n[init] All docsContextFiles are present.\n');
|
|
177
|
+
} else {
|
|
178
|
+
stdout(formatMissingList(preview.missing));
|
|
179
|
+
// On non-TTY without an injected confirm, auto-decline so the scaffolder
|
|
180
|
+
// never writes unattended. On TTY (or with an injected confirm seam), show
|
|
181
|
+
// the prompt and consult the confirm function.
|
|
182
|
+
const canPrompt = tty || !usingDefaultConfirm;
|
|
183
|
+
if (canPrompt) stdout(SCAFFOLD_PROMPT);
|
|
184
|
+
const accepted = canPrompt ? confirmFn() : false;
|
|
185
|
+
if (accepted) {
|
|
186
|
+
scaffoldResult = scaffoldDocs({ root: projectRoot, write: true });
|
|
187
|
+
if (scaffoldResult.created.length > 0) {
|
|
188
|
+
stdout(
|
|
189
|
+
`[init] Scaffolded ${scaffoldResult.created.length} stub(s). ` +
|
|
190
|
+
`Each carries a \`${STUB_MARKER}\` marker — replace placeholder ` +
|
|
191
|
+
'content before planning.\n',
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
stdout(
|
|
196
|
+
'[init] Scaffolding declined. docsContextFiles are still missing — ' +
|
|
197
|
+
'agents will load degraded context until you create them.\n',
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// --- Phase 3: Readiness gate (mandrel doctor) ----------------------------
|
|
203
|
+
stdout('\n[init] Running mandrel doctor…\n');
|
|
204
|
+
const doctorResult = doctorFn();
|
|
205
|
+
const doctorStatus = doctorResult?.status ?? 1;
|
|
206
|
+
|
|
207
|
+
if (doctorStatus !== 0) {
|
|
208
|
+
stdout(
|
|
209
|
+
'\n[init] ❌ Doctor check failed. Resolve the remedies above and\n' +
|
|
210
|
+
'then re-run: mandrel init\n',
|
|
211
|
+
);
|
|
212
|
+
return { stack, scaffoldResult, doctorStatus, ok: false };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- Phase 4: Handoff to /plan -------------------------------------------
|
|
216
|
+
stdout(PLAN_HANDOFF_TEXT);
|
|
217
|
+
return { stack, scaffoldResult, doctorStatus, ok: true };
|
|
218
|
+
}
|
|
@@ -24,6 +24,15 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
24
24
|
const AGENT_ROOT = path.resolve(__dirname, '../../..');
|
|
25
25
|
const DOCS_TEMPLATE_DIR = path.join(AGENT_ROOT, 'templates', 'docs');
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Deterministic marker written into every scaffolded stub. The `/plan`
|
|
29
|
+
* first-run preflight (and any tooling that wants to detect unedited stubs)
|
|
30
|
+
* keys off this exact string — do not change it without a hard cutover.
|
|
31
|
+
*
|
|
32
|
+
* @type {string}
|
|
33
|
+
*/
|
|
34
|
+
export const STUB_MARKER = '<!-- MANDREL:STUB -->';
|
|
35
|
+
|
|
27
36
|
/**
|
|
28
37
|
* Build the generic placeholder stub for a docsContextFile that has no
|
|
29
38
|
* dedicated template. Derives a human-readable title from the filename.
|
|
@@ -39,8 +48,9 @@ function genericStub(fileName) {
|
|
|
39
48
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
40
49
|
.join(' ');
|
|
41
50
|
return (
|
|
51
|
+
`${STUB_MARKER}\n` +
|
|
42
52
|
`# ${title}\n\n` +
|
|
43
|
-
'> **Stub generated by
|
|
53
|
+
'> **Stub generated by `mandrel init`.** This file is one of the\n' +
|
|
44
54
|
'> `project.docsContextFiles` mandatory-reads — agents load it before\n' +
|
|
45
55
|
'> every task. Replace this placeholder with real content.\n'
|
|
46
56
|
);
|
|
@@ -48,7 +58,9 @@ function genericStub(fileName) {
|
|
|
48
58
|
|
|
49
59
|
/**
|
|
50
60
|
* Read the dedicated template body for a docsContextFile, or fall back to the
|
|
51
|
-
* generic stub when no template ships for that name.
|
|
61
|
+
* generic stub when no template ships for that name. Either path prepends the
|
|
62
|
+
* {@link STUB_MARKER} so the `/plan` first-run preflight can detect un-edited
|
|
63
|
+
* stubs regardless of whether a dedicated template was used.
|
|
52
64
|
*
|
|
53
65
|
* @param {string} fileName
|
|
54
66
|
* @param {import('node:fs')} fsImpl
|
|
@@ -57,7 +69,10 @@ function genericStub(fileName) {
|
|
|
57
69
|
function stubContentFor(fileName, fsImpl) {
|
|
58
70
|
const templatePath = path.join(DOCS_TEMPLATE_DIR, fileName);
|
|
59
71
|
if (fsImpl.existsSync(templatePath)) {
|
|
60
|
-
|
|
72
|
+
const body = fsImpl.readFileSync(templatePath, 'utf8');
|
|
73
|
+
// Prepend the marker only when absent (idempotent; the template files
|
|
74
|
+
// themselves are kept marker-free so they read cleanly as documentation).
|
|
75
|
+
return body.startsWith(STUB_MARKER) ? body : `${STUB_MARKER}\n${body}`;
|
|
61
76
|
}
|
|
62
77
|
return genericStub(fileName);
|
|
63
78
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import fs from 'node:fs';
|
|
15
|
-
import
|
|
15
|
+
import { detectPackageManager as detectPm } from '../detect-package-manager.js';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Resolve each required package via the injected `resolve` seam and collect
|
|
@@ -38,17 +38,17 @@ export function checkRuntimeDeps({ required, resolve }) {
|
|
|
38
38
|
/**
|
|
39
39
|
* Detect the consumer's package manager from lockfile presence so the
|
|
40
40
|
* remediation message names the right install command. Defaults to `npm`.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
*
|
|
42
|
+
* Delegates to the shared `detectPackageManager` helper
|
|
43
|
+
* (Story #4048 B3 — one implementation per concept). The `exists` seam
|
|
44
|
+
* is forwarded directly; `null` (no manifest) coerces to `'npm'`.
|
|
43
45
|
*
|
|
44
46
|
* @param {string} root
|
|
45
47
|
* @param {(p: string) => boolean} [exists=fs.existsSync]
|
|
46
48
|
* @returns {'pnpm'|'yarn'|'npm'}
|
|
47
49
|
*/
|
|
48
50
|
export function detectPackageManager(root, exists = fs.existsSync) {
|
|
49
|
-
|
|
50
|
-
if (exists(path.join(root, 'yarn.lock'))) return 'yarn';
|
|
51
|
-
return 'npm';
|
|
51
|
+
return detectPm(root, exists) ?? 'npm';
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/** Map a detected package manager to its install command. */
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { spawnSync } from 'node:child_process';
|
|
21
21
|
import fs from 'node:fs';
|
|
22
22
|
import path from 'node:path';
|
|
23
|
+
import { detectPackageManager } from '../detect-package-manager.js';
|
|
23
24
|
|
|
24
25
|
function sleepSync(ms) {
|
|
25
26
|
if (!Number.isFinite(ms) || ms <= 0) return;
|
|
@@ -110,10 +111,12 @@ export function selectInstallCommand(strategy, wtPath, fsLike = fs) {
|
|
|
110
111
|
if (strategy === 'pnpm-store') {
|
|
111
112
|
return { cmd: 'pnpm', args: ['install', '--frozen-lockfile'] };
|
|
112
113
|
}
|
|
113
|
-
|
|
114
|
+
// Shared lockfile probe (Story #4048 B3 — one implementation per concept).
|
|
115
|
+
const pm = detectPackageManager(wtPath, (p) => fsLike.existsSync(p)) ?? 'npm';
|
|
116
|
+
if (pm === 'pnpm') {
|
|
114
117
|
return { cmd: 'pnpm', args: ['install', '--frozen-lockfile'] };
|
|
115
118
|
}
|
|
116
|
-
if (
|
|
119
|
+
if (pm === 'yarn') {
|
|
117
120
|
return { cmd: 'yarn', args: ['install', '--frozen-lockfile'] };
|
|
118
121
|
}
|
|
119
122
|
return { cmd: 'npm', args: ['ci'] };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: >-
|
|
3
3
|
npm-era upgrade wraparound for a Mandrel consumer. Runs `mandrel update`
|
|
4
|
-
(resolve newest
|
|
4
|
+
(resolve newest published version → install → re-materialize `.agents/` →
|
|
5
5
|
migrate → doctor → surface changelog) as the single mechanical step, then
|
|
6
6
|
walks the operator through the judgment wraparound the CLI deliberately
|
|
7
7
|
leaves unowned: reconcile `.agentrc.json`, install the Epic #1386
|
|
@@ -23,7 +23,7 @@ description: >-
|
|
|
23
23
|
|
|
24
24
|
## Overview
|
|
25
25
|
|
|
26
|
-
`/agents-update` advances the consumer repo to the newest
|
|
26
|
+
`/agents-update` advances the consumer repo to the newest published
|
|
27
27
|
`mandrel` release, re-materializes `.agents/`, and regenerates the
|
|
28
28
|
flat `.claude/commands/` tree (invoked as `/<name>`) against the new workflow
|
|
29
29
|
set — then reconciles the consumer's own config, harness allowlist, and
|
|
@@ -40,11 +40,10 @@ The upgrade contract:
|
|
|
40
40
|
- **CI honours the committed lockfile.** Consumer CI runs `npm ci` against
|
|
41
41
|
the committed `package-lock.json`, so it installs exactly the version the
|
|
42
42
|
lockfile pins — never "whatever the registry's newest is today."
|
|
43
|
-
- **
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
gated.
|
|
43
|
+
- **Majors apply like any other bump.** Mandrel ships hard cutovers
|
|
44
|
+
(`.agents/rules/git-conventions.md` § Contract Cutovers), so a major
|
|
45
|
+
crossing is applied directly — the surfaced changelog is the migration
|
|
46
|
+
guide.
|
|
48
47
|
- **The CLI never commits.** The npm bump rewrites `package.json` /
|
|
49
48
|
`package-lock.json` and leaves them **staged on disk** for operator review;
|
|
50
49
|
`mandrel update` performs no `git add` / `git commit`. Staging and
|
|
@@ -55,8 +54,7 @@ The upgrade contract:
|
|
|
55
54
|
authoritative writer of the generated flat command tree
|
|
56
55
|
(`.claude/commands/`) is
|
|
57
56
|
[`sync-claude-commands.js`](../scripts/sync-claude-commands.js), which
|
|
58
|
-
prepends the `<!-- AUTO-GENERATED -->` header
|
|
59
|
-
`/agents-bootstrap-project` parity-checks. Nothing else copies workflow
|
|
57
|
+
prepends the `<!-- AUTO-GENERATED -->` header. Nothing else copies workflow
|
|
60
58
|
files.
|
|
61
59
|
|
|
62
60
|
> **Persona**: `devops-engineer` · **Skills**:
|
|
@@ -71,7 +69,7 @@ mandrel update --dry-run
|
|
|
71
69
|
mandrel update
|
|
72
70
|
```
|
|
73
71
|
|
|
74
|
-
`mandrel update --dry-run` resolves the newest
|
|
72
|
+
`mandrel update --dry-run` resolves the newest published version and prints
|
|
75
73
|
the ordered step plan (`npm-update → runSync → runMigrations → doctor →
|
|
76
74
|
surface changelog`) without invoking any effectful seam — no dependency bump,
|
|
77
75
|
no sync, no migrations, no doctor, nothing written. Read the planned target
|
|
@@ -82,23 +80,19 @@ version before applying.
|
|
|
82
80
|
1. **Resolve target** — the newest published `mandrel` version (via
|
|
83
81
|
the daily freshness cache in `temp/version-check.json`) and the currently
|
|
84
82
|
installed version.
|
|
85
|
-
2. **
|
|
86
|
-
declines, prints the `docs/upgrade-major.md` pointer, and exits non-zero
|
|
87
|
-
without touching anything. Re-run with `--major` only after reviewing that
|
|
88
|
-
runbook.
|
|
89
|
-
3. **No-op short-circuit** — already on the newest version ⇒ prints
|
|
83
|
+
2. **No-op short-circuit** — already on the newest version ⇒ prints
|
|
90
84
|
`Already up to date` and exits 0.
|
|
91
|
-
|
|
85
|
+
3. **Install** — bumps the dependency (default
|
|
92
86
|
`npm install mandrel@<target>`; pass
|
|
93
87
|
`--install-cmd "<pm> <args>"` for a pnpm/yarn workspace). The lockfile
|
|
94
88
|
change is left **staged** for review; the CLI never commits.
|
|
95
|
-
|
|
89
|
+
4. **runSync** — re-materializes `.agents/` from the freshly installed
|
|
96
90
|
payload, which also regenerates the flat `.claude/commands/` tree via
|
|
97
91
|
`sync-claude-commands.js`.
|
|
98
|
-
|
|
92
|
+
5. **runMigrations** — applies any version-keyed migration steps for the
|
|
99
93
|
crossed range.
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
6. **doctor** — runs the check registry to verify the resulting install.
|
|
95
|
+
7. **Surface changelog** — prints the `docs/CHANGELOG.md` section(s) covering
|
|
102
96
|
the applied range `(current, target]`. Capture this output — Step 4
|
|
103
97
|
reconciles the consumer's own instructions against it.
|
|
104
98
|
|
|
@@ -371,12 +365,6 @@ no-op.
|
|
|
371
365
|
|
|
372
366
|
## Troubleshooting
|
|
373
367
|
|
|
374
|
-
- **`a newer MAJOR version (X.0.0) is available`** — `mandrel update`
|
|
375
|
-
hit the major gate and exited non-zero without touching anything. A
|
|
376
|
-
major crossing is a breaking upgrade. Read `docs/upgrade-major.md`,
|
|
377
|
-
then re-run `mandrel update --major` only after you have absorbed the
|
|
378
|
-
migration steps that runbook describes.
|
|
379
|
-
|
|
380
368
|
- **`doctor reported failures: …`** — the dependency bumped and `.agents/`
|
|
381
369
|
re-materialized, but a doctor check failed (and the run exited
|
|
382
370
|
non-zero). Run `mandrel doctor` for the per-check remedies. The lockfile
|
|
@@ -402,9 +390,6 @@ no-op.
|
|
|
402
390
|
- **Idempotent.** A second `mandrel update` immediately after a successful
|
|
403
391
|
run resolves the same newest version, hits the no-op short-circuit, and
|
|
404
392
|
prints `Already up to date` — exit 0, nothing bumped.
|
|
405
|
-
- **Non-major only by default.** The major axis is gated behind an explicit
|
|
406
|
-
`--major`; routine minor/patch bumps within the current major apply
|
|
407
|
-
without a gate.
|
|
408
393
|
- **No auto-commit.** `mandrel update` leaves the lockfile bump staged on
|
|
409
394
|
disk and never runs git. The operator reviews the surfaced changelog and
|
|
410
395
|
writes the commit message (Step 5) — the CLI does not know whether the
|