mandrel 1.59.0 → 1.61.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 +86 -44
- package/.agents/docs/SDLC.md +135 -141
- package/.agents/docs/configuration.md +77 -20
- package/.agents/docs/quality-gates.md +796 -0
- package/.agents/docs/workflows.md +6 -9
- package/.agents/instructions.md +12 -11
- package/.agents/personas/architect.md +1 -1
- package/.agents/personas/product.md +1 -1
- package/.agents/personas/project-manager.md +14 -14
- package/.agents/personas/technical-writer.md +1 -1
- package/.agents/rules/changelog-style.md +5 -5
- package/.agents/rules/git-conventions.md +3 -3
- package/.agents/runtime-deps.json +2 -2
- package/.agents/schemas/agentrc.schema.json +3 -3
- package/.agents/schemas/dispatch-manifest.json +4 -4
- package/.agents/schemas/epic-spec.schema.json +15 -45
- package/.agents/schemas/lifecycle/README.md +1 -1
- package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +1 -1
- package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +1 -1
- package/.agents/schemas/lifecycle/story.heartbeat.schema.json +1 -1
- package/.agents/schemas/validation-evidence.schema.json +1 -1
- package/.agents/scripts/README.md +2 -2
- package/.agents/scripts/acceptance-eval.js +1 -1
- package/.agents/scripts/acceptance-spec-reconciler.js +2 -2
- package/.agents/scripts/agents-bootstrap-github.js +23 -119
- package/.agents/scripts/analyze-execution.js +2 -2
- package/.agents/scripts/audit-to-stories.js +1 -1
- package/.agents/scripts/check-doc-links.js +2 -3
- package/.agents/scripts/diagnose-friction.js +1 -1
- package/.agents/scripts/dispatcher.js +2 -2
- package/.agents/scripts/drain-pending-cleanup.js +1 -1
- package/.agents/scripts/epic-audit-prepare.js +3 -3
- package/.agents/scripts/epic-deliver-note-intervention.js +2 -2
- package/.agents/scripts/epic-deliver-preflight.js +6 -6
- package/.agents/scripts/epic-deliver-prepare.js +1 -1
- package/.agents/scripts/epic-execute-record-wave.js +4 -4
- package/.agents/scripts/epic-plan-healthcheck.js +6 -10
- package/.agents/scripts/epic-plan-spec-validate.js +1 -1
- package/.agents/scripts/epic-reconcile.js +11 -29
- package/.agents/scripts/evidence-gate.js +1 -1
- package/.agents/scripts/generate-workflows-doc.js +1 -1
- package/.agents/scripts/hierarchy-gate.js +7 -11
- package/.agents/scripts/lib/ITicketingProvider.js +1 -1
- package/.agents/scripts/lib/audit-suite/selector.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/seed-epic-from-findings.js +2 -2
- package/.agents/scripts/lib/baseline-snapshot.js +7 -7
- package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
- package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
- package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +1 -1
- package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +47 -1
- package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
- 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/codebase-snapshot.js +1 -1
- package/.agents/scripts/lib/config/explain.js +1 -1
- package/.agents/scripts/lib/config/runners.js +2 -2
- package/.agents/scripts/lib/config/runtime.js +1 -1
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/config/temp-paths.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-quality.js +1 -1
- package/.agents/scripts/lib/config-settings-schema.js +3 -3
- package/.agents/scripts/lib/detect-package-manager.js +72 -0
- package/.agents/scripts/lib/duplicate-search.js +1 -1
- package/.agents/scripts/lib/dynamic-workflow/capability.js +1 -1
- package/.agents/scripts/lib/epic-plan-clarity.js +1 -1
- package/.agents/scripts/lib/epic-plan-ideation.js +1 -1
- package/.agents/scripts/lib/errors/index.js +4 -4
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +1 -1
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
- package/.agents/scripts/lib/findings/classify-finding.js +1 -1
- package/.agents/scripts/lib/findings/promote-finding.js +10 -10
- package/.agents/scripts/lib/label-constants.js +3 -4
- package/.agents/scripts/lib/label-taxonomy.js +5 -10
- 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/orchestration/acceptance-eval-decision.js +1 -1
- package/.agents/scripts/lib/orchestration/code-review.js +5 -5
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +8 -9
- package/.agents/scripts/lib/orchestration/dependency-analyzer.js +3 -3
- package/.agents/scripts/lib/orchestration/detectors-phase.js +2 -2
- package/.agents/scripts/lib/orchestration/dispatch-engine.js +30 -38
- package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +9 -25
- package/.agents/scripts/lib/orchestration/epic-cleanup.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +8 -8
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +7 -21
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +3 -3
- package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +26 -13
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-run-state-store.js +3 -3
- package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +3 -3
- package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +6 -21
- package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +7 -7
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +8 -8
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +7 -15
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +72 -41
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +2 -4
- package/.agents/scripts/lib/orchestration/file-assumptions.js +2 -2
- package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +1 -1
- package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +2 -2
- package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +1 -1
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -3
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +1 -1
- package/.agents/scripts/lib/orchestration/manifest-builder.js +5 -5
- package/.agents/scripts/lib/orchestration/parked-follow-ons.js +2 -2
- package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +5 -5
- package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +3 -3
- package/.agents/scripts/lib/orchestration/preflight-cache.js +1 -1
- package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +1 -1
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +1 -1
- package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +2 -2
- package/.agents/scripts/lib/orchestration/retro-runner.js +3 -3
- package/.agents/scripts/lib/orchestration/review-depth.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +1 -1
- package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
- package/.agents/scripts/lib/orchestration/spec-renderer.js +36 -73
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +2 -2
- package/.agents/scripts/lib/orchestration/task-body-validator.js +6 -6
- package/.agents/scripts/lib/orchestration/ticket-lease.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -2
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -10
- package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -70
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +5 -12
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +8 -8
- package/.agents/scripts/lib/orchestration/ticketing/state.js +3 -3
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +2 -2
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -1
- package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
- package/.agents/scripts/lib/preflight-runner.js +1 -1
- package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +4 -5
- package/.agents/scripts/lib/presentation/manifest-builder.js +28 -34
- package/.agents/scripts/lib/presentation/manifest-formatter.js +3 -4
- package/.agents/scripts/lib/presentation/manifest-helpers.js +1 -1
- package/.agents/scripts/lib/presentation/manifest-procedures.js +4 -4
- package/.agents/scripts/lib/presentation/manifest-render-waves.js +4 -23
- package/.agents/scripts/lib/presentation/manifest-renderer.js +1 -1
- package/.agents/scripts/lib/presentation/manifest-story-views.js +2 -11
- package/.agents/scripts/lib/runtime-deps/preflight.js +6 -6
- package/.agents/scripts/lib/signals/schema.js +1 -1
- package/.agents/scripts/lib/spec/index.js +1 -1
- package/.agents/scripts/lib/spec/loader.js +2 -2
- package/.agents/scripts/lib/spec/state.js +7 -16
- package/.agents/scripts/lib/story-init/context-resolver.js +3 -3
- package/.agents/scripts/lib/story-init/state-transitioner.js +2 -2
- package/.agents/scripts/lib/story-init/task-graph-builder.js +7 -7
- package/.agents/scripts/lib/story-lifecycle.js +8 -8
- package/.agents/scripts/lib/story-plan.js +1 -1
- package/.agents/scripts/lib/templates/decomposer-prompts.js +59 -52
- package/.agents/scripts/lib/wave-runner/tick.js +1 -1
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +5 -2
- package/.agents/scripts/lifecycle-emit-story-dispatch.js +1 -1
- package/.agents/scripts/lifecycle-emit.js +1 -1
- package/.agents/scripts/providers/github/board-add.js +1 -1
- package/.agents/scripts/providers/github/errors.js +1 -1
- package/.agents/scripts/providers/github/mappers.js +2 -2
- package/.agents/scripts/providers/github/tickets.js +4 -4
- package/.agents/scripts/resync-status-column.js +1 -1
- package/.agents/scripts/retro-run.js +2 -2
- package/.agents/scripts/run-lint.js +1 -1
- package/.agents/scripts/single-story-init.js +1 -1
- package/.agents/scripts/stories-wave-tick.js +5 -5
- package/.agents/scripts/story-close.js +1 -1
- package/.agents/scripts/story-init.js +13 -16
- package/.agents/scripts/story-phase.js +5 -5
- package/.agents/scripts/story-plan.js +3 -3
- package/.agents/scripts/sync-branch-from-base.js +1 -1
- package/.agents/scripts/validate-docs-freshness.js +1 -1
- package/.agents/scripts/wave-tick.js +1 -1
- package/.agents/skills/core/analyze-execution/SKILL.md +2 -2
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +21 -26
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -56
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +4 -4
- package/.agents/skills/core/hydrate-context/SKILL.md +2 -2
- package/.agents/skills/core/idea-refinement/SKILL.md +4 -4
- package/.agents/skills/core/knowledge-transfer/SKILL.md +2 -2
- package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +1 -1
- package/.agents/skills/core/scope-triage/SKILL.md +9 -10
- package/.agents/skills/core/using-agent-skills/SKILL.md +1 -1
- package/.agents/skills/skills.index.json +7 -7
- package/.agents/templates/agent-protocol.md +2 -2
- package/.agents/workflows/agents-update.md +16 -31
- package/.agents/workflows/audit-architecture.md +2 -2
- package/.agents/workflows/audit-clean-code.md +2 -2
- package/.agents/workflows/audit-dependencies.md +1 -1
- package/.agents/workflows/audit-devops.md +1 -1
- package/.agents/workflows/audit-documentation.md +2 -2
- package/.agents/workflows/audit-lighthouse.md +1 -1
- package/.agents/workflows/audit-performance.md +2 -2
- package/.agents/workflows/audit-privacy.md +1 -1
- package/.agents/workflows/audit-quality.md +2 -2
- package/.agents/workflows/audit-security.md +2 -2
- package/.agents/workflows/audit-seo.md +1 -1
- package/.agents/workflows/audit-sre.md +1 -1
- package/.agents/workflows/audit-to-stories.md +10 -10
- package/.agents/workflows/audit-ux-ui.md +1 -1
- package/.agents/workflows/deliver.md +85 -0
- package/.agents/workflows/explain.md +3 -3
- package/.agents/workflows/git-merge-pr.md +1 -1
- package/.agents/workflows/git-pr-all.md +13 -10
- package/.agents/workflows/git-push.md +6 -3
- package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
- package/.agents/workflows/helpers/acceptance-self-eval.md +1 -1
- package/.agents/workflows/helpers/agents-sync-config.md +3 -2
- package/.agents/workflows/helpers/code-review.md +5 -5
- package/.agents/workflows/{epic-deliver.md → helpers/deliver-epic.md} +43 -43
- package/.agents/workflows/{story-deliver.md → helpers/deliver-stories.md} +25 -25
- package/.agents/workflows/helpers/diagnose.md +1 -1
- package/.agents/workflows/helpers/epic-audit.md +6 -6
- package/.agents/workflows/helpers/epic-deliver-story.md +13 -13
- package/.agents/workflows/helpers/epic-plan-decompose.md +23 -23
- package/.agents/workflows/helpers/epic-plan-spec.md +6 -6
- package/.agents/workflows/helpers/epic-testing.md +3 -3
- package/.agents/workflows/helpers/parallel-tooling.md +1 -1
- package/.agents/workflows/{epic-plan.md → helpers/plan-epic.md} +84 -84
- package/.agents/workflows/{story-plan.md → helpers/plan-story.md} +43 -43
- package/.agents/workflows/helpers/signals.md +1 -1
- package/.agents/workflows/helpers/single-story-deliver.md +11 -11
- package/.agents/workflows/helpers/worktree-lifecycle.md +18 -18
- package/.agents/workflows/plan.md +131 -0
- package/.agents/workflows/qa-explore.md +1 -1
- package/.agents/workflows/qa-run-harness.md +1 -1
- package/README.md +19 -39
- package/bin/mandrel.js +235 -16
- package/docs/CHANGELOG.md +1173 -0
- package/lib/cli/doctor.js +45 -3
- package/lib/cli/init.js +97 -36
- package/lib/cli/registry.js +41 -145
- package/lib/cli/sync.js +122 -23
- package/lib/cli/uninstall.js +42 -7
- package/lib/cli/update.js +524 -210
- package/lib/cli/version-helpers.js +59 -0
- package/package.json +7 -6
- package/.agents/scripts/lib/orchestration/reconciler.js +0 -137
- 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-major.test.js +0 -217
- 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/lib/cli/update.js
CHANGED
|
@@ -3,20 +3,17 @@
|
|
|
3
3
|
* `mandrel update` subcommand — the auto-update orchestrator (f-update-command,
|
|
4
4
|
* Story #3503, Epic #3437 — Auto-Update & Version Lifecycle).
|
|
5
5
|
*
|
|
6
|
-
* Advances `mandrel` to the newest
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `docs/upgrade-major.md`, and exits non-zero without touching anything.
|
|
6
|
+
* Advances `mandrel` to the newest published version, re-materializes
|
|
7
|
+
* `.agents/`, runs applicable version-keyed migrations, surfaces the target
|
|
8
|
+
* changelog, and verifies the result via the doctor registry. Major crossings
|
|
9
|
+
* are applied like any other bump (hard-cutover doctrine —
|
|
10
|
+
* `.agents/rules/git-conventions.md` § Contract Cutovers).
|
|
12
11
|
*
|
|
13
|
-
* ## Ordered cycle (happy path
|
|
12
|
+
* ## Ordered cycle (happy path)
|
|
14
13
|
*
|
|
15
14
|
* 1. resolve target version (newest published) and the current version
|
|
16
|
-
* 2.
|
|
17
|
-
*
|
|
18
|
-
* 3. no-op short-circuit — already on the newest version ⇒ nothing to do
|
|
19
|
-
* 4. install — bump the dependency (lockfile bump left STAGED).
|
|
15
|
+
* 2. no-op short-circuit — already on the newest version ⇒ nothing to do
|
|
16
|
+
* 3. install — bump the dependency (lockfile bump left STAGED).
|
|
20
17
|
* The package manager is auto-detected from the lockfile in the project
|
|
21
18
|
* root: `pnpm-lock.yaml` ⇒ `pnpm add -D …` (with `-w` at a
|
|
22
19
|
* `pnpm-workspace.yaml` root), `yarn.lock` ⇒ `yarn add -D …`, otherwise
|
|
@@ -25,10 +22,36 @@
|
|
|
25
22
|
* the resolved version so an override can still consume the auto-probed
|
|
26
23
|
* newest. The registry probe in step 1 always stays on `npm view` (a
|
|
27
24
|
* PM-agnostic registry query).
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
25
|
+
* 4. runSync — re-materialize ./.agents/ **from the newly-installed
|
|
26
|
+
* binary** so the materialized payload is always the target version's.
|
|
27
|
+
* 5. runMigrations — apply version-keyed steps for the crossed range,
|
|
28
|
+
* **from the newly-installed binary**.
|
|
29
|
+
* 6. doctor — run the check registry **from the newly-installed
|
|
30
|
+
* binary** so `agents-drift` is never a false-green against stale payload.
|
|
31
|
+
* 7. surface the changelog for the target version
|
|
32
|
+
*
|
|
33
|
+
* ## Re-exec of post-install phases (Story #4034)
|
|
34
|
+
*
|
|
35
|
+
* Steps 4–6 execute as **child processes spawned from the newly-installed
|
|
36
|
+
* binary** (`<cwd>/node_modules/.bin/mandrel`) rather than in the running
|
|
37
|
+
* process. Node cannot hot-swap a `require`d module mid-process, so without
|
|
38
|
+
* re-exec, the still-running old binary's `runSync`/`runMigrations`/`runDoctor`
|
|
39
|
+
* code would materialise the old payload even though the package on disk has
|
|
40
|
+
* already been updated. This produced the silent stale-`.agents/`
|
|
41
|
+
* materialization and `doctor` false-green observed in the v1.58.0 → v1.59.0
|
|
42
|
+
* consumer upgrade.
|
|
43
|
+
*
|
|
44
|
+
* The orchestration (progress messages, step tracking, changelog surface, exit
|
|
45
|
+
* code) stays in the parent process; only the version-sensitive phases run from
|
|
46
|
+
* the new bin. The `spawnPhase` seam makes the child-process boundary fully
|
|
47
|
+
* injectable so tests can verify the re-exec path without a real npm install.
|
|
48
|
+
*
|
|
49
|
+
* Backward compatibility: when `runSync`, `runMigrations`, or `runDoctor`
|
|
50
|
+
* are explicitly injected (the historical test-seam pattern) and `spawnPhase`
|
|
51
|
+
* is NOT injected, the in-process seams are used unchanged (old tests stay
|
|
52
|
+
* green). When `spawnPhase` IS injected, it takes priority over the in-process
|
|
53
|
+
* seams for the live phases — so new tests targeting the re-exec boundary can
|
|
54
|
+
* inject `spawnPhase` without touching the old seam interface.
|
|
32
55
|
*
|
|
33
56
|
* ## No git mutation
|
|
34
57
|
*
|
|
@@ -43,17 +66,19 @@
|
|
|
43
66
|
* without invoking any effectful seam (no npm update, no sync, no migrations,
|
|
44
67
|
* no doctor) and writing nothing.
|
|
45
68
|
*
|
|
46
|
-
* ##
|
|
69
|
+
* ## Changelog surface
|
|
70
|
+
*
|
|
71
|
+
* `defaultSurfaceChangelog` prints the `docs/CHANGELOG.md` section(s) for the
|
|
72
|
+
* applied range `(current, target]`. It resolves the file against the target
|
|
73
|
+
* version's install directory (the freshly bumped `node_modules/mandrel/`),
|
|
74
|
+
* where the changelog is now included in the published tarball
|
|
75
|
+
* (`docs/CHANGELOG.md` in the `files` allowlist — Story #4035).
|
|
47
76
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* `docs/upgrade-major.md` runbook pointer, exit non-zero, and invoke
|
|
54
|
-
* **no** npm-update / sync / migration / doctor seam.
|
|
55
|
-
* - **with `--major`**: apply the major target and print the runbook inline.
|
|
56
|
-
* Routine minor/patch bumps within the 1.x line are never gated.
|
|
77
|
+
* When the packaged file is absent (e.g. an older installed version predating
|
|
78
|
+
* Story #4035), the seam attempts a one-shot HTTP GET of the raw file from
|
|
79
|
+
* GitHub via the injectable `fetchChangelog` seam. If that fetch also fails,
|
|
80
|
+
* the seam degrades gracefully — never throwing — and emits an actionable
|
|
81
|
+
* message directing the operator to the GitHub Releases page.
|
|
57
82
|
*
|
|
58
83
|
* ## Injectable seams (used by lib/cli/__tests__/update*.test.js)
|
|
59
84
|
*
|
|
@@ -62,15 +87,27 @@
|
|
|
62
87
|
* - `resolveTargetVersion`— async, returns the newest published version
|
|
63
88
|
* - `npmUpdate` — async, performs the dependency bump (no git);
|
|
64
89
|
* receives `(target, { installCmd })`
|
|
65
|
-
* - `
|
|
66
|
-
*
|
|
67
|
-
*
|
|
90
|
+
* - `spawnPhase` — async, spawns a post-install phase from the new
|
|
91
|
+
* binary; receives `(phase, args, { binPath, cwd })`
|
|
92
|
+
* and returns `{ ok, stdout, stderr }`. When
|
|
93
|
+
* injected, it takes priority over `runSync`,
|
|
94
|
+
* `runMigrations`, and `runDoctor` for the live
|
|
95
|
+
* phases. See § Re-exec of post-install phases.
|
|
96
|
+
* - `runSync` — re-materializes ./.agents/ (lib/cli/sync.js).
|
|
97
|
+
* Used when `spawnPhase` is NOT injected (backward
|
|
98
|
+
* compat for tests that pre-date Story #4034).
|
|
99
|
+
* - `runMigrations` — version-keyed migration runner (lib/migrations).
|
|
100
|
+
* Used when `spawnPhase` is NOT injected.
|
|
101
|
+
* - `runDoctor` — async, returns { ok, results } from the registry.
|
|
102
|
+
* Used when `spawnPhase` is NOT injected.
|
|
68
103
|
* - `surfaceChangelog` — emits the target changelog section
|
|
69
104
|
* - `write` / `writeErr` — stdout / stderr sinks
|
|
70
105
|
* - `exit` — process.exit replacement
|
|
106
|
+
* - `cwd` — process.cwd() replacement (used to resolve the
|
|
107
|
+
* new binary path when `spawnPhase` is absent)
|
|
71
108
|
*
|
|
72
109
|
* Security (security-baseline § 5 — Data Leakage & Logging): logs only version
|
|
73
|
-
* strings
|
|
110
|
+
* strings and step names. No tokens, credentials, or env
|
|
74
111
|
* values are read or logged; no shell-string interpolation occurs here (the
|
|
75
112
|
* npm bump is delegated to the injected `npmUpdate` seam, which owns transport).
|
|
76
113
|
*
|
|
@@ -86,72 +123,46 @@
|
|
|
86
123
|
* the install argv is a tokenized list whose only variable segment is a
|
|
87
124
|
* resolved semver string — see `lib/install-cmd-parser.js` for the shared
|
|
88
125
|
* tokenize-and-spawn rationale this module reuses (no duplicated workaround).
|
|
126
|
+
*
|
|
127
|
+
* The `spawnPhase` default (Story #4034) similarly uses `shell: true` only on
|
|
128
|
+
* Windows: the new binary resolves from `node_modules/.bin/mandrel` (a fixed,
|
|
129
|
+
* non-operator-supplied path) and the per-phase argv vector is a constant
|
|
130
|
+
* fixed list (e.g. `['sync']`, `['migrate', '--from', v, '--to', v]`,
|
|
131
|
+
* `['doctor']`) with no injection risk regardless of the shell flag.
|
|
89
132
|
*/
|
|
90
133
|
|
|
91
134
|
import { spawnSync } from 'node:child_process';
|
|
92
135
|
import nodeFs from 'node:fs';
|
|
136
|
+
import nodeHttps from 'node:https';
|
|
93
137
|
import path from 'node:path';
|
|
94
138
|
import { fileURLToPath } from 'node:url';
|
|
95
139
|
|
|
140
|
+
import { detectPackageManagerWithWorkspace } from '../../.agents/scripts/lib/detect-package-manager.js';
|
|
96
141
|
import { runInstallCommand } from '../../.agents/scripts/lib/install-cmd-parser.js';
|
|
97
142
|
import { runMigrations as defaultRunMigrations } from '../migrations/index.js';
|
|
98
143
|
import { registry } from './registry.js';
|
|
99
144
|
import { runSync as defaultRunSync } from './sync.js';
|
|
100
145
|
import { isStale } from './version-check.js';
|
|
101
|
-
|
|
102
|
-
/** Path (relative to project root) of the major-upgrade runbook. */
|
|
103
|
-
const RUNBOOK_PATH = 'docs/upgrade-major.md';
|
|
146
|
+
import { compareVersions } from './version-helpers.js';
|
|
104
147
|
|
|
105
148
|
/** The published package whose newest version `mandrel update` advances to. */
|
|
106
149
|
const PACKAGE_NAME = 'mandrel';
|
|
107
150
|
|
|
108
|
-
/** Default freshness-cache filename — mirrors version-check.js. */
|
|
109
|
-
const DEFAULT_CACHE_FILENAME = 'version-check.json';
|
|
110
|
-
|
|
111
151
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* @param {string} version
|
|
116
|
-
* @returns {[number, number, number]}
|
|
152
|
+
* GitHub raw-file base URL for fetching `docs/CHANGELOG.md` when the packaged
|
|
153
|
+
* file is absent (Story #4035 — GitHub fallback). Resolves to the tagged
|
|
154
|
+
* release, e.g. `.../mandrel-v1.59.0/docs/CHANGELOG.md`.
|
|
117
155
|
*/
|
|
118
|
-
|
|
119
|
-
const [major, minor, patch] = String(version).split('.');
|
|
120
|
-
return [
|
|
121
|
-
Number.parseInt(major, 10) || 0,
|
|
122
|
-
Number.parseInt(minor, 10) || 0,
|
|
123
|
-
Number.parseInt(patch, 10) || 0,
|
|
124
|
-
];
|
|
125
|
-
}
|
|
156
|
+
const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/dsj1984/mandrel/';
|
|
126
157
|
|
|
127
158
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* @param {string} a
|
|
132
|
-
* @param {string} b
|
|
133
|
-
* @returns {number}
|
|
159
|
+
* Human-readable GitHub Releases page — surfaced in the actionable fallback
|
|
160
|
+
* message when neither the packaged file nor the GitHub fetch succeeds.
|
|
134
161
|
*/
|
|
135
|
-
|
|
136
|
-
const pa = parseVersion(a);
|
|
137
|
-
const pb = parseVersion(b);
|
|
138
|
-
for (let i = 0; i < 3; i += 1) {
|
|
139
|
-
if (pa[i] !== pb[i]) return pa[i] - pb[i];
|
|
140
|
-
}
|
|
141
|
-
return 0;
|
|
142
|
-
}
|
|
162
|
+
const GITHUB_RELEASES_URL = 'https://github.com/dsj1984/mandrel/releases';
|
|
143
163
|
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
* gated "crosses a major boundary" condition.
|
|
147
|
-
*
|
|
148
|
-
* @param {string} current
|
|
149
|
-
* @param {string} target
|
|
150
|
-
* @returns {boolean}
|
|
151
|
-
*/
|
|
152
|
-
function crossesMajor(current, target) {
|
|
153
|
-
return parseVersion(target)[0] > parseVersion(current)[0];
|
|
154
|
-
}
|
|
164
|
+
/** Default freshness-cache filename — mirrors version-check.js. */
|
|
165
|
+
const DEFAULT_CACHE_FILENAME = 'version-check.json';
|
|
155
166
|
|
|
156
167
|
/**
|
|
157
168
|
* Resolve the installed `mandrel` version from this package's own
|
|
@@ -183,12 +194,15 @@ function resolveProjectRoot() {
|
|
|
183
194
|
* Default `resolveTargetVersion` seam: determine the newest published
|
|
184
195
|
* `mandrel` version via the daily freshness cache (`version-check.js`).
|
|
185
196
|
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
197
|
+
* When `bypassCache` is `true` (the default for an explicit `mandrel update`
|
|
198
|
+
* call — Story #4046 A1b), the cache freshness window is effectively zeroed by
|
|
199
|
+
* passing `now` as far in the future, which makes `isStale` treat any existing
|
|
200
|
+
* cache as stale and always issue exactly one network probe. The cache is still
|
|
201
|
+
* written so the post-update `version-current` advisory has a fresh baseline.
|
|
202
|
+
*
|
|
203
|
+
* When `bypassCache` is `false` (passive staleness checks only), the normal
|
|
204
|
+
* 24h-cache semantics apply: a fresh cache returns the cached version with
|
|
205
|
+
* zero network I/O.
|
|
192
206
|
*
|
|
193
207
|
* The network probe shells `npm view` through `spawnSync` with a fixed argument
|
|
194
208
|
* vector (no shell-string interpolation; the package name is a constant). On
|
|
@@ -202,6 +216,7 @@ function resolveProjectRoot() {
|
|
|
202
216
|
* fs?: typeof nodeFs,
|
|
203
217
|
* runner?: () => string,
|
|
204
218
|
* now?: Date,
|
|
219
|
+
* bypassCache?: boolean,
|
|
205
220
|
* log?: (msg: string) => void,
|
|
206
221
|
* }} [opts]
|
|
207
222
|
* @returns {Promise<string>} The newest published version string.
|
|
@@ -211,9 +226,22 @@ async function defaultResolveTargetVersion({
|
|
|
211
226
|
fs = nodeFs,
|
|
212
227
|
runner = defaultVersionRunner,
|
|
213
228
|
now = new Date(),
|
|
229
|
+
bypassCache = false,
|
|
214
230
|
log = () => {},
|
|
215
231
|
} = {}) {
|
|
216
|
-
|
|
232
|
+
// When bypassCache is true, push `now` far enough into the future that any
|
|
233
|
+
// cached checkedAt value is guaranteed to be older than the STALE_AFTER_MS
|
|
234
|
+
// window, forcing a fresh network probe (Story #4046 A1b).
|
|
235
|
+
const effectiveNow = bypassCache
|
|
236
|
+
? new Date(now.getTime() + 48 * 60 * 60 * 1000)
|
|
237
|
+
: now;
|
|
238
|
+
const result = await isStale({
|
|
239
|
+
cachePath,
|
|
240
|
+
now: effectiveNow,
|
|
241
|
+
runner,
|
|
242
|
+
fs,
|
|
243
|
+
log,
|
|
244
|
+
});
|
|
217
245
|
return String(result.latestVersion);
|
|
218
246
|
}
|
|
219
247
|
|
|
@@ -280,28 +308,30 @@ function repairInstallCommand(packageManager) {
|
|
|
280
308
|
* Detecting the lockfile keeps the bump on the operator's real package manager
|
|
281
309
|
* so the change lands in the matching lockfile.
|
|
282
310
|
*
|
|
311
|
+
* Delegates to the shared `detectPackageManagerWithWorkspace` helper
|
|
312
|
+
* (Story #4048 B3 — one implementation per concept). The `fs` seam is adapted
|
|
313
|
+
* to the shared module's `exists` contract; the shared module's `bun` return
|
|
314
|
+
* value coerces to `npm` here because `bun add` is not yet a first-class update
|
|
315
|
+
* path for this orchestrator.
|
|
316
|
+
*
|
|
283
317
|
* @param {string} [cwd] - Project root to probe (default `process.cwd()`).
|
|
284
318
|
* @param {typeof nodeFs} [fs]
|
|
285
319
|
* @returns {{ packageManager: 'pnpm' | 'yarn' | 'npm', workspaceRoot: boolean }}
|
|
286
320
|
*/
|
|
287
321
|
export function detectPackageManager(cwd = process.cwd(), fs = nodeFs) {
|
|
288
|
-
const
|
|
322
|
+
const exists = (p) => {
|
|
289
323
|
try {
|
|
290
|
-
return fs.existsSync(
|
|
324
|
+
return fs.existsSync(p);
|
|
291
325
|
} catch {
|
|
292
326
|
return false;
|
|
293
327
|
}
|
|
294
328
|
};
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
if (has('yarn.lock')) {
|
|
302
|
-
return { packageManager: 'yarn', workspaceRoot: false };
|
|
303
|
-
}
|
|
304
|
-
return { packageManager: 'npm', workspaceRoot: false };
|
|
329
|
+
const result = detectPackageManagerWithWorkspace(cwd, exists);
|
|
330
|
+
// Coerce `bun` → `npm` because this orchestrator's install-command builder
|
|
331
|
+
// only handles pnpm / yarn / npm today.
|
|
332
|
+
const packageManager =
|
|
333
|
+
result.packageManager === 'bun' ? 'npm' : result.packageManager;
|
|
334
|
+
return { packageManager, workspaceRoot: result.workspaceRoot };
|
|
305
335
|
}
|
|
306
336
|
|
|
307
337
|
/**
|
|
@@ -408,6 +438,72 @@ export function defaultNpmUpdate(
|
|
|
408
438
|
}
|
|
409
439
|
}
|
|
410
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Fetch `docs/CHANGELOG.md` for a specific mandrel tag from GitHub's raw
|
|
443
|
+
* content endpoint. This is the fallback when the packaged file is absent
|
|
444
|
+
* (e.g. an older install predating Story #4035 which added the file to the
|
|
445
|
+
* npm `files` allowlist).
|
|
446
|
+
*
|
|
447
|
+
* Injectable via the `fetchChangelog` seam so tests can verify the fallback
|
|
448
|
+
* path without issuing real network calls.
|
|
449
|
+
*
|
|
450
|
+
* The tag shape follows the `mandrel-vX.Y.Z` namespace (namespaced at
|
|
451
|
+
* `mandrel-v1.44.0`; bare `vX.Y.Z` for earlier releases). This function
|
|
452
|
+
* tries the namespaced tag first, then the bare-tag form, so it covers both
|
|
453
|
+
* tag series without forcing callers to know the boundary.
|
|
454
|
+
*
|
|
455
|
+
* Security (security-baseline § Transport & Headers): the URL is constructed
|
|
456
|
+
* from a constant base and a semver string — no user input, no shell
|
|
457
|
+
* interpolation. The GET is a read-only fetch with no credentials.
|
|
458
|
+
*
|
|
459
|
+
* @param {string} version - The target semver string (e.g. `"1.59.0"`).
|
|
460
|
+
* @param {{
|
|
461
|
+
* https?: typeof nodeHttps,
|
|
462
|
+
* }} [deps]
|
|
463
|
+
* @returns {Promise<string>} The raw changelog text.
|
|
464
|
+
* @throws {Error} When both tag forms return a non-2xx response or the request
|
|
465
|
+
* errors out — the caller handles this gracefully.
|
|
466
|
+
*/
|
|
467
|
+
export async function fetchChangelogFromGitHub(
|
|
468
|
+
version,
|
|
469
|
+
{ https: httpsImpl = nodeHttps } = {},
|
|
470
|
+
) {
|
|
471
|
+
const tags = [`mandrel-v${version}`, `v${version}`];
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* @param {string} url
|
|
475
|
+
* @returns {Promise<{ status: number, body: string }>}
|
|
476
|
+
*/
|
|
477
|
+
const httpGet = (url) =>
|
|
478
|
+
new Promise((resolve, reject) => {
|
|
479
|
+
httpsImpl
|
|
480
|
+
.get(url, (res) => {
|
|
481
|
+
const chunks = [];
|
|
482
|
+
res.on('data', (d) => chunks.push(d));
|
|
483
|
+
res.on('end', () =>
|
|
484
|
+
resolve({
|
|
485
|
+
status: res.statusCode ?? 0,
|
|
486
|
+
body: Buffer.concat(chunks).toString('utf8'),
|
|
487
|
+
}),
|
|
488
|
+
);
|
|
489
|
+
})
|
|
490
|
+
.on('error', reject);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
for (const tag of tags) {
|
|
494
|
+
const url = `${GITHUB_RAW_BASE}${tag}/docs/CHANGELOG.md`;
|
|
495
|
+
// eslint-disable-next-line no-await-in-loop
|
|
496
|
+
const { status, body } = await httpGet(url);
|
|
497
|
+
if (status >= 200 && status < 300) {
|
|
498
|
+
return body;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
throw new Error(
|
|
503
|
+
`mandrel update: GitHub fetch for mandrel v${version} docs/CHANGELOG.md returned non-2xx for all tag forms (tried: ${tags.join(', ')})`,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
411
507
|
/**
|
|
412
508
|
* Default `surfaceChangelog` seam: print the `docs/CHANGELOG.md` section(s)
|
|
413
509
|
* covering the applied version range `(current, target]`. The changelog is
|
|
@@ -415,38 +511,61 @@ export function defaultNpmUpdate(
|
|
|
415
511
|
* prints every section whose version is newer than `current` and no newer than
|
|
416
512
|
* `target`.
|
|
417
513
|
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
514
|
+
* Resolution order (Story #4035):
|
|
515
|
+
* 1. Read `docs/CHANGELOG.md` from the target version's install directory
|
|
516
|
+
* (`node_modules/mandrel/docs/CHANGELOG.md` — now in the published
|
|
517
|
+
* tarball since `package.json` lists `docs/CHANGELOG.md` in `files`).
|
|
518
|
+
* 2. When the packaged file is absent (older install), fetch it from GitHub
|
|
519
|
+
* via the injectable `fetchChangelog` seam.
|
|
520
|
+
* 3. When both sources fail, emit an actionable warning with a link to the
|
|
521
|
+
* GitHub Releases page — never a bare "not found … skipping".
|
|
522
|
+
*
|
|
523
|
+
* Degrades gracefully (warns, never throws) — surfacing the changelog is
|
|
524
|
+
* best-effort and must never fail an otherwise-successful upgrade.
|
|
421
525
|
*
|
|
422
526
|
* @param {string} target - The applied target version.
|
|
423
527
|
* @param {{
|
|
424
528
|
* current?: string,
|
|
425
529
|
* changelogPath?: string,
|
|
426
530
|
* fs?: typeof nodeFs,
|
|
531
|
+
* fetchChangelog?: (version: string) => Promise<string>,
|
|
427
532
|
* write?: (s: string) => void,
|
|
428
533
|
* writeErr?: (s: string) => void,
|
|
429
534
|
* }} [opts]
|
|
430
|
-
* @returns {void}
|
|
535
|
+
* @returns {Promise<void>}
|
|
431
536
|
*/
|
|
432
|
-
function defaultSurfaceChangelog(
|
|
537
|
+
async function defaultSurfaceChangelog(
|
|
433
538
|
target,
|
|
434
539
|
{
|
|
435
540
|
current,
|
|
436
541
|
changelogPath = path.join(resolveProjectRoot(), 'docs', 'CHANGELOG.md'),
|
|
437
542
|
fs = nodeFs,
|
|
543
|
+
fetchChangelog = fetchChangelogFromGitHub,
|
|
438
544
|
write = (s) => process.stdout.write(s),
|
|
439
545
|
writeErr = (s) => process.stderr.write(s),
|
|
440
546
|
} = {},
|
|
441
547
|
) {
|
|
442
548
|
let raw;
|
|
549
|
+
|
|
550
|
+
// 1. Try the packaged file (present in installs since Story #4035).
|
|
443
551
|
try {
|
|
444
552
|
raw = fs.readFileSync(changelogPath, 'utf8');
|
|
445
553
|
} catch {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
554
|
+
// File absent — fall through to GitHub fetch.
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// 2. Packaged file absent: attempt a GitHub fetch for the target tag.
|
|
558
|
+
if (raw === undefined) {
|
|
559
|
+
try {
|
|
560
|
+
raw = await fetchChangelog(target);
|
|
561
|
+
} catch {
|
|
562
|
+
// Both sources unavailable — emit an actionable message and return.
|
|
563
|
+
writeErr(
|
|
564
|
+
`mandrel update: changelog not available for v${target} — ` +
|
|
565
|
+
`view the release notes at ${GITHUB_RELEASES_URL}\n`,
|
|
566
|
+
);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
450
569
|
}
|
|
451
570
|
|
|
452
571
|
const sections = parseChangelogSections(raw);
|
|
@@ -458,7 +577,8 @@ function defaultSurfaceChangelog(
|
|
|
458
577
|
|
|
459
578
|
if (relevant.length === 0) {
|
|
460
579
|
writeErr(
|
|
461
|
-
`mandrel update: no CHANGELOG section found for v${target} —
|
|
580
|
+
`mandrel update: no CHANGELOG section found for v${target} — ` +
|
|
581
|
+
`view the release notes at ${GITHUB_RELEASES_URL}\n`,
|
|
462
582
|
);
|
|
463
583
|
return;
|
|
464
584
|
}
|
|
@@ -509,6 +629,8 @@ function parseChangelogSections(raw) {
|
|
|
509
629
|
* report whether all passed. Mirrors lib/cli/doctor.js's pass accounting
|
|
510
630
|
* without the formatted report (the orchestrator owns its own output).
|
|
511
631
|
*
|
|
632
|
+
* This is used only when `spawnPhase` is NOT injected (backward-compat path).
|
|
633
|
+
*
|
|
512
634
|
* @param {{ checks?: typeof registry }} [opts]
|
|
513
635
|
* @returns {Promise<{ ok: boolean, results: Array<{ name: string, ok: boolean }> }>}
|
|
514
636
|
*/
|
|
@@ -522,10 +644,88 @@ async function defaultRunDoctor({ checks = registry } = {}) {
|
|
|
522
644
|
}
|
|
523
645
|
|
|
524
646
|
/**
|
|
525
|
-
*
|
|
647
|
+
* Resolve the path to the `mandrel` binary inside `node_modules/.bin/` for the
|
|
648
|
+
* given project root. On Windows the binary is a `.cmd` shim; on POSIX it is a
|
|
649
|
+
* plain executable. The resolved path is used as the target for the post-install
|
|
650
|
+
* phase re-exec (Story #4034).
|
|
651
|
+
*
|
|
652
|
+
* @param {string} projectRoot - Absolute path to the consumer project.
|
|
653
|
+
* @returns {string} Absolute path to the new binary.
|
|
654
|
+
*/
|
|
655
|
+
export function resolveNewBinPath(projectRoot) {
|
|
656
|
+
const binName = process.platform === 'win32' ? 'mandrel.cmd' : 'mandrel';
|
|
657
|
+
return path.join(projectRoot, 'node_modules', '.bin', binName);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Default `spawnPhase` seam (Story #4034): spawn a post-install phase from the
|
|
662
|
+
* newly-installed `mandrel` binary and stream its stdout/stderr through the
|
|
663
|
+
* parent's write sinks. Each phase runs as an isolated child process so the
|
|
664
|
+
* newly-installed module code (not the currently-loaded old module) executes.
|
|
665
|
+
*
|
|
666
|
+
* The spawn uses `shell: true` only on Windows where the binary is a `.cmd`
|
|
667
|
+
* shim (CVE-2024-27980 parity). The argv vector is a fixed constant list
|
|
668
|
+
* per phase — no operator-supplied data enters the vector, so the shell flag
|
|
669
|
+
* carries no injection risk (security-baseline § Output & Rendering).
|
|
670
|
+
*
|
|
671
|
+
* Throws when the child exits non-zero so the orchestrator can surface the
|
|
672
|
+
* failure to the operator.
|
|
673
|
+
*
|
|
674
|
+
* @param {string} phase - The mandrel sub-command to run (e.g. `'sync'`).
|
|
675
|
+
* @param {string[]} args - Additional arguments for the sub-command.
|
|
676
|
+
* @param {{
|
|
677
|
+
* binPath: string,
|
|
678
|
+
* cwd: string,
|
|
679
|
+
* write: (s: string) => void,
|
|
680
|
+
* writeErr: (s: string) => void,
|
|
681
|
+
* spawnFn?: typeof spawnSync,
|
|
682
|
+
* }} opts
|
|
683
|
+
* @returns {{ ok: boolean, stdout: string, stderr: string }}
|
|
684
|
+
*/
|
|
685
|
+
export function defaultSpawnPhase(
|
|
686
|
+
phase,
|
|
687
|
+
args,
|
|
688
|
+
{ binPath, cwd, write, writeErr, spawnFn = spawnSync },
|
|
689
|
+
) {
|
|
690
|
+
const argv = [phase, ...args];
|
|
691
|
+
const r = spawnFn(binPath, argv, {
|
|
692
|
+
cwd,
|
|
693
|
+
encoding: 'utf8',
|
|
694
|
+
shell: process.platform === 'win32',
|
|
695
|
+
});
|
|
696
|
+
const stdout = typeof r.stdout === 'string' ? r.stdout : '';
|
|
697
|
+
const stderr = typeof r.stderr === 'string' ? r.stderr : '';
|
|
698
|
+
if (stdout) write(stdout);
|
|
699
|
+
if (stderr) writeErr(stderr);
|
|
700
|
+
if (r.error) {
|
|
701
|
+
throw new Error(
|
|
702
|
+
`mandrel update: failed to spawn \`mandrel ${phase}\` from new binary: ${r.error.message}`,
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
const ok = r.status === 0;
|
|
706
|
+
return { ok, stdout, stderr };
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* The ordered step names the orchestrator drives on an update. Shared
|
|
526
711
|
* by the live path and the `--dry-run` plan printout so the two never drift.
|
|
712
|
+
*
|
|
713
|
+
* Step ordering (Story #4046 A1c):
|
|
714
|
+
* 1. npm-update — install the new version
|
|
715
|
+
* 2. runSync — re-materialize .agents/ from the new payload
|
|
716
|
+
* 3. sync-commands — regenerate .claude/commands/ from the new payload
|
|
717
|
+
* 4. runMigrations — apply version-keyed migrations
|
|
718
|
+
* 5. doctor — validate the post-upgrade state
|
|
719
|
+
* 6. surface changelog — print the changelog (always last, best-effort)
|
|
527
720
|
*/
|
|
528
|
-
const STEP_PLAN = [
|
|
721
|
+
const STEP_PLAN = [
|
|
722
|
+
'npm-update',
|
|
723
|
+
'runSync',
|
|
724
|
+
'sync-commands',
|
|
725
|
+
'runMigrations',
|
|
726
|
+
'doctor',
|
|
727
|
+
'surface changelog',
|
|
728
|
+
];
|
|
529
729
|
|
|
530
730
|
/**
|
|
531
731
|
* Extract the `--install-cmd "<cmd>"` value from the subcommand argv. Accepts
|
|
@@ -553,21 +753,6 @@ function parseInstallCmdFlag(argv) {
|
|
|
553
753
|
return undefined;
|
|
554
754
|
}
|
|
555
755
|
|
|
556
|
-
/**
|
|
557
|
-
* Print the major-gate refusal: the available version, the runbook pointer,
|
|
558
|
-
* and the re-run hint. No effectful seam runs after this.
|
|
559
|
-
*
|
|
560
|
-
* @param {string} target
|
|
561
|
-
* @param {(s: string) => void} writeErr
|
|
562
|
-
*/
|
|
563
|
-
function emitMajorRefusal(target, writeErr) {
|
|
564
|
-
writeErr(
|
|
565
|
-
`mandrel update: a newer MAJOR version (${target}) is available; ` +
|
|
566
|
-
'this is a breaking upgrade.\n' +
|
|
567
|
-
` → Review ${RUNBOOK_PATH}, then re-run with --major to apply it.\n`,
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
756
|
/**
|
|
572
757
|
* Run the `mandrel update` orchestration cycle.
|
|
573
758
|
*
|
|
@@ -576,6 +761,7 @@ function emitMajorRefusal(target, writeErr) {
|
|
|
576
761
|
* currentVersion?: string | (() => string),
|
|
577
762
|
* resolveTargetVersion?: () => (string | Promise<string>),
|
|
578
763
|
* npmUpdate?: (version: string, opts: { installCmd?: string }) => unknown | Promise<unknown>,
|
|
764
|
+
* spawnPhase?: (phase: string, args: string[], opts: { binPath: string, cwd: string, write: (s: string) => void, writeErr: (s: string) => void }) => Promise<{ ok: boolean, stdout: string, stderr: string }> | { ok: boolean, stdout: string, stderr: string },
|
|
579
765
|
* runSync?: typeof defaultRunSync,
|
|
580
766
|
* runMigrations?: typeof defaultRunMigrations,
|
|
581
767
|
* runDoctor?: typeof defaultRunDoctor,
|
|
@@ -583,13 +769,13 @@ function emitMajorRefusal(target, writeErr) {
|
|
|
583
769
|
* write?: (s: string) => void,
|
|
584
770
|
* writeErr?: (s: string) => void,
|
|
585
771
|
* exit?: (code: number) => void,
|
|
772
|
+
* cwd?: () => string,
|
|
586
773
|
* }} [opts]
|
|
587
774
|
* @returns {Promise<{
|
|
588
775
|
* ok: boolean,
|
|
589
|
-
* action: 'updated' | '
|
|
776
|
+
* action: 'updated' | 'dry-run' | 'up-to-date' | 'doctor-failed',
|
|
590
777
|
* currentVersion: string,
|
|
591
778
|
* targetVersion: string | null,
|
|
592
|
-
* major: boolean,
|
|
593
779
|
* stepsRun: string[],
|
|
594
780
|
* dryRun: boolean,
|
|
595
781
|
* }>}
|
|
@@ -599,6 +785,7 @@ export async function runUpdate({
|
|
|
599
785
|
currentVersion,
|
|
600
786
|
resolveTargetVersion,
|
|
601
787
|
npmUpdate,
|
|
788
|
+
spawnPhase,
|
|
602
789
|
runSync = defaultRunSync,
|
|
603
790
|
runMigrations = defaultRunMigrations,
|
|
604
791
|
runDoctor = defaultRunDoctor,
|
|
@@ -606,9 +793,9 @@ export async function runUpdate({
|
|
|
606
793
|
write = (s) => process.stdout.write(s),
|
|
607
794
|
writeErr = (s) => process.stderr.write(s),
|
|
608
795
|
exit = (code) => process.exit(code),
|
|
796
|
+
cwd = () => process.cwd(),
|
|
609
797
|
} = {}) {
|
|
610
798
|
const dryRun = argv.includes('--dry-run');
|
|
611
|
-
const allowMajor = argv.includes('--major');
|
|
612
799
|
const installCmd = parseInstallCmdFlag(argv);
|
|
613
800
|
|
|
614
801
|
const current =
|
|
@@ -623,25 +810,6 @@ export async function runUpdate({
|
|
|
623
810
|
}
|
|
624
811
|
const target = String(await resolveTargetVersion());
|
|
625
812
|
|
|
626
|
-
const major = crossesMajor(current, target);
|
|
627
|
-
|
|
628
|
-
// --- Major gate -----------------------------------------------------------
|
|
629
|
-
// A major crossing without --major is refused outright: no npm-update, no
|
|
630
|
-
// sync, no migration, no doctor — print the runbook pointer and exit non-zero.
|
|
631
|
-
if (major && !allowMajor) {
|
|
632
|
-
emitMajorRefusal(target, writeErr);
|
|
633
|
-
exit(1);
|
|
634
|
-
return {
|
|
635
|
-
ok: false,
|
|
636
|
-
action: 'declined-major',
|
|
637
|
-
currentVersion: current,
|
|
638
|
-
targetVersion: target,
|
|
639
|
-
major: true,
|
|
640
|
-
stepsRun: [],
|
|
641
|
-
dryRun,
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
|
|
645
813
|
// --- No-op short-circuit --------------------------------------------------
|
|
646
814
|
// Already on (or ahead of) the newest version: nothing to apply.
|
|
647
815
|
if (compareVersions(target, current) <= 0) {
|
|
@@ -651,7 +819,6 @@ export async function runUpdate({
|
|
|
651
819
|
action: 'up-to-date',
|
|
652
820
|
currentVersion: current,
|
|
653
821
|
targetVersion: target,
|
|
654
|
-
major,
|
|
655
822
|
stepsRun: [],
|
|
656
823
|
dryRun,
|
|
657
824
|
};
|
|
@@ -662,34 +829,21 @@ export async function runUpdate({
|
|
|
662
829
|
// write nothing to disk.
|
|
663
830
|
if (dryRun) {
|
|
664
831
|
write(`mandrel update — planned upgrade v${current} → v${target}\n`);
|
|
665
|
-
if (major) {
|
|
666
|
-
write(' (major upgrade — --major supplied)\n');
|
|
667
|
-
}
|
|
668
832
|
STEP_PLAN.forEach((step, i) => {
|
|
669
833
|
write(` ${i + 1}. ${step}\n`);
|
|
670
834
|
});
|
|
671
|
-
write(' 5. surface changelog\n');
|
|
672
835
|
write('Dry run: no files written, no dependency bumped.\n');
|
|
673
836
|
return {
|
|
674
837
|
ok: true,
|
|
675
838
|
action: 'dry-run',
|
|
676
839
|
currentVersion: current,
|
|
677
840
|
targetVersion: target,
|
|
678
|
-
major,
|
|
679
841
|
stepsRun: [],
|
|
680
842
|
dryRun: true,
|
|
681
843
|
};
|
|
682
844
|
}
|
|
683
845
|
|
|
684
|
-
|
|
685
|
-
if (major) {
|
|
686
|
-
write(
|
|
687
|
-
`Applying MAJOR upgrade v${current} → v${target} (--major). ` +
|
|
688
|
-
`Review the runbook: ${RUNBOOK_PATH}\n`,
|
|
689
|
-
);
|
|
690
|
-
} else {
|
|
691
|
-
write(`Updating v${current} → v${target}…\n`);
|
|
692
|
-
}
|
|
846
|
+
write(`Updating v${current} → v${target}…\n`);
|
|
693
847
|
|
|
694
848
|
const stepsRun = [];
|
|
695
849
|
|
|
@@ -703,40 +857,159 @@ export async function runUpdate({
|
|
|
703
857
|
await npmUpdate(target, { installCmd });
|
|
704
858
|
stepsRun.push('npm-update');
|
|
705
859
|
|
|
706
|
-
//
|
|
707
|
-
runSync
|
|
708
|
-
|
|
860
|
+
// Decide whether to use the re-exec path (spawnPhase) or the in-process
|
|
861
|
+
// backward-compat seams (runSync / runMigrations / runDoctor).
|
|
862
|
+
//
|
|
863
|
+
// spawnPhase injected → re-exec path: all post-install phases run from the
|
|
864
|
+
// newly-installed binary. This is the production path and is what fixes
|
|
865
|
+
// the stale-materialization bug (Story #4034).
|
|
866
|
+
//
|
|
867
|
+
// spawnPhase NOT injected → in-process path: the original pre-Story-#4034
|
|
868
|
+
// behaviour. Tests that pre-date this change inject runSync/runMigrations/
|
|
869
|
+
// runDoctor and rely on the in-process path; they stay green without any
|
|
870
|
+
// modification.
|
|
871
|
+
const useReExec = typeof spawnPhase === 'function';
|
|
709
872
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
873
|
+
if (useReExec) {
|
|
874
|
+
// Re-exec path: post-install phases run from the new binary.
|
|
875
|
+
const projectRoot = cwd();
|
|
876
|
+
const binPath = resolveNewBinPath(projectRoot);
|
|
713
877
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
878
|
+
// 2. runSync from new bin — re-materialize ./.agents/ from the freshly
|
|
879
|
+
// installed payload. Running from the new bin ensures the copied files
|
|
880
|
+
// come from the new package's .agents/ tree, not the old loaded module.
|
|
881
|
+
const syncResult = await spawnPhase('sync', [], {
|
|
882
|
+
binPath,
|
|
883
|
+
cwd: projectRoot,
|
|
884
|
+
write,
|
|
885
|
+
writeErr,
|
|
886
|
+
});
|
|
887
|
+
if (!syncResult.ok) {
|
|
888
|
+
throw new Error(
|
|
889
|
+
`mandrel update: \`mandrel sync\` from new binary exited non-zero — ` +
|
|
890
|
+
'the .agents/ materialization may be incomplete. ' +
|
|
891
|
+
'Run `mandrel sync` manually to restore.',
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
stepsRun.push('runSync');
|
|
717
895
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
896
|
+
// 3. sync-commands from new bin — regenerate .claude/commands/ from the
|
|
897
|
+
// freshly-materialized .agents/workflows/. Running from the new bin
|
|
898
|
+
// ensures the command tree is consistent with the new payload; an
|
|
899
|
+
// upstream-renamed workflow will be projected correctly and the old
|
|
900
|
+
// command file will be reaped. This step must follow runSync so the
|
|
901
|
+
// workflow sources are up to date before the command tree is rebuilt
|
|
902
|
+
// (Story #4046 A1c — `commands-in-sync` validates the post-sync state).
|
|
903
|
+
const syncCommandsResult = await spawnPhase('sync-commands', [], {
|
|
904
|
+
binPath,
|
|
905
|
+
cwd: projectRoot,
|
|
906
|
+
write,
|
|
907
|
+
writeErr,
|
|
908
|
+
});
|
|
909
|
+
if (!syncCommandsResult.ok) {
|
|
910
|
+
throw new Error(
|
|
911
|
+
`mandrel update: \`mandrel sync-commands\` from new binary exited non-zero — ` +
|
|
912
|
+
'the .claude/commands/ tree may be out of sync. ' +
|
|
913
|
+
'Run `npm run sync:commands` manually to restore.',
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
stepsRun.push('sync-commands');
|
|
722
917
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
918
|
+
// 4. runMigrations from new bin — apply version-keyed steps for the
|
|
919
|
+
// crossed range. The new binary's migration registry contains any steps
|
|
920
|
+
// added in the target version; the old process's registry does not.
|
|
921
|
+
const migrateResult = await spawnPhase(
|
|
922
|
+
'migrate',
|
|
923
|
+
['--from', current, '--to', target],
|
|
924
|
+
{ binPath, cwd: projectRoot, write, writeErr },
|
|
729
925
|
);
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
926
|
+
if (!migrateResult.ok) {
|
|
927
|
+
throw new Error(
|
|
928
|
+
`mandrel update: \`mandrel migrate\` from new binary exited non-zero — ` +
|
|
929
|
+
`some migrations for v${current} → v${target} may not have applied. ` +
|
|
930
|
+
`Run \`mandrel migrate --from ${current} --to ${target}\` manually to retry.`,
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
stepsRun.push('runMigrations');
|
|
934
|
+
|
|
935
|
+
// 5. doctor from new bin — verify the resulting install. Running from the
|
|
936
|
+
// new bin is critical: the agents-drift check compares the materialized
|
|
937
|
+
// .agents/ against the installed package payload. When the old process
|
|
938
|
+
// runs this check, it resolves the package root to its own (old) install
|
|
939
|
+
// dir, so drift against the new payload is invisible. The new binary
|
|
940
|
+
// resolves the package root to the now-installed new version, producing
|
|
941
|
+
// an accurate result.
|
|
942
|
+
const doctorResult = await spawnPhase('doctor', [], {
|
|
943
|
+
binPath,
|
|
944
|
+
cwd: projectRoot,
|
|
945
|
+
write,
|
|
946
|
+
writeErr,
|
|
947
|
+
});
|
|
948
|
+
stepsRun.push('doctor');
|
|
949
|
+
|
|
950
|
+
// 6. surface the target changelog (best-effort; optional seam).
|
|
951
|
+
if (typeof surfaceChangelog === 'function') {
|
|
952
|
+
await surfaceChangelog(target);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (!doctorResult.ok) {
|
|
956
|
+
writeErr(
|
|
957
|
+
`mandrel update: upgraded to v${target} but doctor reported failures.\n` +
|
|
958
|
+
' → Run `mandrel doctor` for remedies.\n',
|
|
959
|
+
);
|
|
960
|
+
exit(1);
|
|
961
|
+
return {
|
|
962
|
+
ok: false,
|
|
963
|
+
action: 'doctor-failed',
|
|
964
|
+
currentVersion: current,
|
|
965
|
+
targetVersion: target,
|
|
966
|
+
stepsRun,
|
|
967
|
+
dryRun: false,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
} else {
|
|
971
|
+
// In-process backward-compat path (pre-Story-#4034 behaviour).
|
|
972
|
+
// Used when no `spawnPhase` seam is injected — preserves full backward
|
|
973
|
+
// compatibility with existing tests that inject runSync/runMigrations/
|
|
974
|
+
// runDoctor directly.
|
|
975
|
+
|
|
976
|
+
// 2. runSync — re-materialize ./.agents/ from the new payload.
|
|
977
|
+
runSync({ argv: [] });
|
|
978
|
+
stepsRun.push('runSync');
|
|
979
|
+
|
|
980
|
+
// 3. runMigrations — apply version-keyed steps for the crossed range.
|
|
981
|
+
// Note: the in-process path (pre-Story-#4034) does not run sync-commands
|
|
982
|
+
// here because sync-commands runs as a child process and there is no
|
|
983
|
+
// in-process seam for it. The re-exec path (spawnPhase) handles it.
|
|
984
|
+
runMigrations({ fromVersion: current, toVersion: target, ctx: {} });
|
|
985
|
+
stepsRun.push('runMigrations');
|
|
986
|
+
|
|
987
|
+
// 4. doctor — verify the resulting install.
|
|
988
|
+
const doctor = await runDoctor();
|
|
989
|
+
stepsRun.push('doctor');
|
|
990
|
+
|
|
991
|
+
// 5. surface the target changelog (best-effort; optional seam).
|
|
992
|
+
if (typeof surfaceChangelog === 'function') {
|
|
993
|
+
await surfaceChangelog(target);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (!doctor.ok) {
|
|
997
|
+
const failed = doctor.results.filter((r) => !r.ok).map((r) => r.name);
|
|
998
|
+
writeErr(
|
|
999
|
+
`mandrel update: upgraded to v${target} but doctor reported failures: ` +
|
|
1000
|
+
`${failed.join(', ')}\n` +
|
|
1001
|
+
' → Run `mandrel doctor` for remedies.\n',
|
|
1002
|
+
);
|
|
1003
|
+
exit(1);
|
|
1004
|
+
return {
|
|
1005
|
+
ok: false,
|
|
1006
|
+
action: 'doctor-failed',
|
|
1007
|
+
currentVersion: current,
|
|
1008
|
+
targetVersion: target,
|
|
1009
|
+
stepsRun,
|
|
1010
|
+
dryRun: false,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
740
1013
|
}
|
|
741
1014
|
|
|
742
1015
|
write(`✅ Updated to v${target}. The lockfile bump is staged for review.\n`);
|
|
@@ -745,7 +1018,6 @@ export async function runUpdate({
|
|
|
745
1018
|
action: 'updated',
|
|
746
1019
|
currentVersion: current,
|
|
747
1020
|
targetVersion: target,
|
|
748
|
-
major,
|
|
749
1021
|
stepsRun,
|
|
750
1022
|
dryRun: false,
|
|
751
1023
|
};
|
|
@@ -755,26 +1027,37 @@ export async function runUpdate({
|
|
|
755
1027
|
* Default export consumed by `bin/mandrel.js`.
|
|
756
1028
|
*
|
|
757
1029
|
* Wires the production-default seams that `runUpdate` leaves injectable:
|
|
758
|
-
* - `resolveTargetVersion` probes the
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
* `version-current` doctor advisory reads
|
|
1030
|
+
* - `resolveTargetVersion` always probes the registry via `isStale` with
|
|
1031
|
+
* `bypassCache: true` — the 24h cache is overridden for explicit update
|
|
1032
|
+
* calls so the resolved version is always fresh (Story #4046 A1b). The
|
|
1033
|
+
* cache is still written so the `version-current` doctor advisory reads
|
|
1034
|
+
* a current baseline after the upgrade.
|
|
762
1035
|
* - `npmUpdate` runs the install command — auto-detected from the project
|
|
763
1036
|
* lockfile (`pnpm`/`yarn`/`npm`), or the `--install-cmd` override —
|
|
764
1037
|
* through the shared `runInstallCommand` helper — no git mutation;
|
|
765
1038
|
* lockfile left staged.
|
|
1039
|
+
* - `spawnPhase` is wired to `defaultSpawnPhase`, which spawns each
|
|
1040
|
+
* post-install phase (sync, sync-commands, migrate, doctor) from the
|
|
1041
|
+
* newly-installed binary (`node_modules/.bin/mandrel`). This is the
|
|
1042
|
+
* Story #4034 fix: the new bin loads the new package's module code and
|
|
1043
|
+
* resolves paths against the new install dir, so these phases can never
|
|
1044
|
+
* observe the old payload.
|
|
766
1045
|
* - `surfaceChangelog` prints the relevant `docs/CHANGELOG.md` section(s)
|
|
767
|
-
* for the applied range
|
|
1046
|
+
* for the applied range. Reads from the packaged file first; falls back to
|
|
1047
|
+
* a GitHub raw-content fetch via the injectable `fetchChangelog` seam when
|
|
1048
|
+
* the packaged file is absent; emits an actionable link to the GitHub
|
|
1049
|
+
* Releases page when both sources fail (Story #4035).
|
|
768
1050
|
*
|
|
769
1051
|
* Every seam stays injectable on `runUpdate`; these are merely the
|
|
770
1052
|
* no-seam-provided fallbacks, so the existing seam-driven tests stay green.
|
|
771
|
-
* `--
|
|
1053
|
+
* `--dry-run` / `--install-cmd` are parsed from `argv` by
|
|
772
1054
|
* `runUpdate` itself.
|
|
773
1055
|
*
|
|
774
1056
|
* The second `deps` argument exposes the **process boundaries** the production
|
|
775
1057
|
* defaults shell out across (`versionRunner` = `npm view`, `runInstall` =
|
|
776
|
-
* the install spawn
|
|
777
|
-
* driven end-to-end with the
|
|
1058
|
+
* the install spawn, `spawnFn` = the phase-spawn boundary) plus `fs` /
|
|
1059
|
+
* `cachePath` / `now`, so the entrypoint can be driven end-to-end with the
|
|
1060
|
+
* network/npm boundary stubbed and no real I/O.
|
|
778
1061
|
* `bin/mandrel.js` calls `run(argv)` with no `deps`, getting the production
|
|
779
1062
|
* wiring; tests pass fakes. The `deps` surface is NOT part of the public
|
|
780
1063
|
* subcommand contract — `bin/mandrel.js` only ever supplies `argv`.
|
|
@@ -787,7 +1070,9 @@ export async function runUpdate({
|
|
|
787
1070
|
* now?: Date,
|
|
788
1071
|
* versionRunner?: () => string,
|
|
789
1072
|
* runInstall?: (installCmd: string, cwd: string) => { status: number, stderr: string },
|
|
1073
|
+
* spawnFn?: typeof spawnSync,
|
|
790
1074
|
* changelogPath?: string,
|
|
1075
|
+
* fetchChangelog?: (version: string) => Promise<string>,
|
|
791
1076
|
* runUpdate?: typeof runUpdate,
|
|
792
1077
|
* runSync?: typeof defaultRunSync,
|
|
793
1078
|
* runMigrations?: typeof defaultRunMigrations,
|
|
@@ -806,49 +1091,78 @@ export default async function run(argv = [], deps = {}) {
|
|
|
806
1091
|
now,
|
|
807
1092
|
versionRunner,
|
|
808
1093
|
runInstall,
|
|
1094
|
+
spawnFn,
|
|
809
1095
|
changelogPath,
|
|
1096
|
+
fetchChangelog,
|
|
810
1097
|
runUpdate: runUpdateImpl = runUpdate,
|
|
811
1098
|
runSync,
|
|
812
1099
|
runMigrations,
|
|
813
1100
|
runDoctor,
|
|
814
|
-
write,
|
|
815
|
-
writeErr,
|
|
816
|
-
exit,
|
|
1101
|
+
write = (s) => process.stdout.write(s),
|
|
1102
|
+
writeErr = (s) => process.stderr.write(s),
|
|
1103
|
+
exit = (code) => process.exit(code),
|
|
817
1104
|
log,
|
|
818
1105
|
} = deps;
|
|
819
1106
|
|
|
820
1107
|
const current = deps.currentVersion ?? defaultCurrentVersion(fs);
|
|
821
1108
|
|
|
1109
|
+
// The production spawnPhase default: spawn each post-install phase from
|
|
1110
|
+
// node_modules/.bin/mandrel (the newly-installed binary). spawnFn is
|
|
1111
|
+
// injectable so tests can stub the spawn boundary without running a real
|
|
1112
|
+
// child process.
|
|
1113
|
+
const productionSpawnPhase = (phase, args, opts) =>
|
|
1114
|
+
defaultSpawnPhase(phase, args, {
|
|
1115
|
+
...opts,
|
|
1116
|
+
...(spawnFn ? { spawnFn } : {}),
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
// Resolve which seam set to use for post-install phases. If any old-style
|
|
1120
|
+
// in-process seam (runSync/runMigrations/runDoctor) is injected, fall back
|
|
1121
|
+
// to the pre-Story-#4034 in-process path so the entrypoint test stays green.
|
|
1122
|
+
// Otherwise use the re-exec path (spawnPhase). This is a single ternary
|
|
1123
|
+
// rather than stacked optional spreads (tidy, Story #4046).
|
|
1124
|
+
const phaseSeams =
|
|
1125
|
+
runSync || runMigrations || runDoctor
|
|
1126
|
+
? {
|
|
1127
|
+
...(runSync ? { runSync } : {}),
|
|
1128
|
+
...(runMigrations ? { runMigrations } : {}),
|
|
1129
|
+
...(runDoctor ? { runDoctor } : {}),
|
|
1130
|
+
}
|
|
1131
|
+
: { spawnPhase: productionSpawnPhase };
|
|
1132
|
+
|
|
822
1133
|
await runUpdateImpl({
|
|
823
1134
|
argv,
|
|
824
1135
|
currentVersion: current,
|
|
1136
|
+
// Always bypass the 24h cache on an explicit `mandrel update` so the
|
|
1137
|
+
// resolved target is fresh from the registry (Story #4046 A1b).
|
|
825
1138
|
resolveTargetVersion: () =>
|
|
826
1139
|
defaultResolveTargetVersion({
|
|
827
|
-
|
|
1140
|
+
cachePath:
|
|
1141
|
+
cachePath ?? path.join(process.cwd(), 'temp', DEFAULT_CACHE_FILENAME),
|
|
828
1142
|
fs,
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1143
|
+
runner: versionRunner ?? defaultVersionRunner,
|
|
1144
|
+
now: now ?? new Date(),
|
|
1145
|
+
bypassCache: true,
|
|
1146
|
+
log: log ?? (() => {}),
|
|
832
1147
|
}),
|
|
833
1148
|
npmUpdate: (target, { installCmd } = {}) =>
|
|
834
1149
|
defaultNpmUpdate(target, {
|
|
835
1150
|
...(installCmd ? { installCmd } : {}),
|
|
836
|
-
|
|
1151
|
+
runInstall: runInstall ?? runInstallCommand,
|
|
837
1152
|
fs,
|
|
838
1153
|
}),
|
|
1154
|
+
...phaseSeams,
|
|
839
1155
|
surfaceChangelog: (target) =>
|
|
840
1156
|
defaultSurfaceChangelog(target, {
|
|
841
1157
|
current,
|
|
842
1158
|
fs,
|
|
843
1159
|
...(changelogPath ? { changelogPath } : {}),
|
|
844
|
-
|
|
845
|
-
|
|
1160
|
+
fetchChangelog: fetchChangelog ?? fetchChangelogFromGitHub,
|
|
1161
|
+
write,
|
|
1162
|
+
writeErr,
|
|
846
1163
|
}),
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
...(write ? { write } : {}),
|
|
851
|
-
...(writeErr ? { writeErr } : {}),
|
|
852
|
-
...(exit ? { exit } : {}),
|
|
1164
|
+
write,
|
|
1165
|
+
writeErr,
|
|
1166
|
+
exit,
|
|
853
1167
|
});
|
|
854
1168
|
}
|