@zq-silk/yui 0.14.2 → 0.15.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/ARCHITECTURE.md +27 -12
- package/README.md +52 -52
- package/dist/cli/commandCatalog.js +1 -1
- package/dist/cli/updateCommand.js +17 -9
- package/dist/cli/updateOrchestrator.js +81 -15
- package/dist/cli/updatePorts.js +72 -10
- package/dist/cli/upgradeCommand.js +104 -19
- package/dist/commands/controllerCommands.js +1 -1
- package/dist/core/controllerServer.js +5 -5
- package/dist/doctor/doctor.js +37 -14
- package/dist/observability/runtimeIdentity.js +48 -50
- package/dist/release/runtimeRelease.js +9 -1
- package/dist/runtime/exactControlPlane.js +20 -29
- package/dist/storage/currentTaskStore.js +6 -4
- package/dist/storage/sqliteSchema.js +134 -59
- package/dist/storage/sqliteStore.js +7 -5
- package/dist/storage/storageSchema.js +92 -223
- package/dist/storage/storageVersions.js +12 -16
- package/dist/storage/upgrade/upgradeOrchestrator.js +224 -62
- package/dist/version.js +3 -3
- package/docs/task-local-identity.md +9 -9
- package/i18n/README.zh-CN.md +26 -17
- package/package.json +1 -1
- package/dist/storage/upgrade/recordVersions.js +0 -82
package/ARCHITECTURE.md
CHANGED
|
@@ -366,18 +366,33 @@ cleanup revalidates ownership and fails safely when concurrent state changes;
|
|
|
366
366
|
manual retry is the recovery boundary rather than another durable state
|
|
367
367
|
machine.
|
|
368
368
|
|
|
369
|
-
Storage
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
369
|
+
Storage has one compatibility authority: the highest contiguous, checksummed
|
|
370
|
+
version in SQLite's append-only migration ledger. The CLI exposes that release's
|
|
371
|
+
current version and minimum supported migration version. Ordinary opening and
|
|
372
|
+
Controller startup accept only the exact current version; historical decoding
|
|
373
|
+
and rewriting exist only inside `yui upgrade` and the migration phase of
|
|
374
|
+
`yui update`.
|
|
375
|
+
|
|
376
|
+
Yui 0.15.0 establishes storage version 1 as the clean single-version baseline:
|
|
377
|
+
`schema.json` has no authority, and layout/aggregate/record-version axes are
|
|
378
|
+
absent from Home metadata. Pre-0.15.0 Homes are outside the migration floor.
|
|
379
|
+
Future releases append one immutable migration per storage version and retain
|
|
380
|
+
the complete chain from version 1. A fresh Home replays that same chain, so
|
|
381
|
+
fresh and upgraded Homes converge on one current schema.
|
|
382
|
+
|
|
383
|
+
The staged `upgrade --update-preflight` / `--update-apply` machine handshake is
|
|
384
|
+
also a compatibility contract: target releases keep its established outcomes
|
|
385
|
+
and required fields, plus the parent-owned handover-lock proof, readable by
|
|
386
|
+
every updater released since version 1. Additive detail is allowed; renaming or
|
|
387
|
+
removing consumed fields would strand an otherwise valid cross-version
|
|
388
|
+
migration path.
|
|
389
|
+
|
|
390
|
+
An upgrade runs only while the Controller is quiesced, creates a consistent
|
|
391
|
+
database backup, applies all missing migrations in one transaction, and
|
|
392
|
+
validates the current record model before writes resume. A missing or changed
|
|
393
|
+
ledger row, a migration gap, a Home below the migration floor, a future Home,
|
|
394
|
+
or malformed data fails closed. Runtime code contains no dual readers,
|
|
395
|
+
historical normalizers, or independently writable compatibility state.
|
|
381
396
|
|
|
382
397
|
The Web control room is loopback-only and never receives Controller socket
|
|
383
398
|
credentials. It presents durable records and native terminal access without
|
package/README.md
CHANGED
|
@@ -72,12 +72,12 @@ export YUI_HOME=/absolute/path/to/yui-home
|
|
|
72
72
|
yui setup
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
The home contains
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
The home contains the authoritative SQLite database `yui.db`, Project Catalog
|
|
76
|
+
and knowledge, and Controller discovery files. Stable Project checkouts and
|
|
77
|
+
managed worktrees live under the configured workspace, outside Yui home.
|
|
78
|
+
Legacy `schema.json` and `state.json` files are evidence only. Runtime storage
|
|
79
|
+
accepts only the exact current contract; supported earlier storage versions
|
|
80
|
+
enter only through the explicit upgrade boundary.
|
|
81
81
|
|
|
82
82
|
Every Task-owned record family allocates a monotonically increasing local ID
|
|
83
83
|
inside its Task. Different Tasks may therefore both contain `work-item-1`,
|
|
@@ -89,43 +89,41 @@ and `task integration start`, keep their subordinate IDs local to that Task.
|
|
|
89
89
|
Candidate IDs are local to their WorkItem and carry both Task and WorkItem
|
|
90
90
|
provenance.
|
|
91
91
|
|
|
92
|
-
Yui
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
Yui has one Home storage version, recorded by the append-only SQLite migration
|
|
93
|
+
ledger. Every CLI reports both its current storage version and its minimum
|
|
94
|
+
supported migration version. Runtime admission still has only two outcomes:
|
|
95
|
+
exact current, or rejected; a Controller never migrates storage while serving
|
|
96
|
+
work. `yui upgrade --dry-run` is read-only. `yui upgrade` quiesces a running
|
|
97
|
+
Controller, creates a consistent backup, applies every missing migration in
|
|
98
|
+
one transaction, validates the current contract, and then restarts the
|
|
99
|
+
Controller when it was running before the upgrade.
|
|
97
100
|
|
|
98
101
|
`yui update` stages and pins one exact package, runs that staged binary's
|
|
99
102
|
storage preflight, stops the exact old Controller, activates the same artifact,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
103
|
+
applies the staged release's migration chain when required, verifies the
|
|
104
|
+
installed binary and current Home, and starts the replacement Controller. A
|
|
105
|
+
Home inside the staged release's supported range can upgrade directly across
|
|
106
|
+
multiple versions without installing intermediate releases. A newer Home, a
|
|
107
|
+
Home below the migration floor, an incomplete migration ledger, or malformed
|
|
108
|
+
data fails closed without a guessed repair.
|
|
109
|
+
|
|
110
|
+
Yui 0.15.0 establishes storage version 1 and the migration floor. Homes created
|
|
111
|
+
by earlier releases, including 0.14.2, are not on this compatibility line:
|
|
112
|
+
preserve them for inspection with their matching Yui release or initialize a
|
|
113
|
+
new Home. From 0.15.0 onward, each release retains the complete chain, so later
|
|
114
|
+
`yui update` invocations can cross versions directly.
|
|
112
115
|
|
|
113
116
|
See [Task-local identity](docs/task-local-identity.md) for the current reference
|
|
114
117
|
contract.
|
|
115
118
|
|
|
116
|
-
Schema work across Tasks is not serialized: any Task may
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
current manifest descriptor map is re-derived against the newest head, while the
|
|
125
|
-
post-baseline descriptor snapshot remains frozen. If another Task later lands a
|
|
126
|
-
record-schema change, the integrating branch must rebase, allocate the next
|
|
127
|
-
current contract without collision, and re-test to convergence. It does not
|
|
128
|
-
preserve an upgrade path for the displaced intermediate contract.
|
|
119
|
+
Schema work across Tasks is not serialized: any Task may propose the next
|
|
120
|
+
storage migration on its own isolated branch without waiting for another Task's
|
|
121
|
+
schema change to land. The later-integrating branch owns the reconciliation:
|
|
122
|
+
rebase onto the latest project head, preserve every already released migration
|
|
123
|
+
unchanged, allocate the next contiguous storage version, resolve schema and code
|
|
124
|
+
conflicts, and re-run the bounded validation. A migration may update physical
|
|
125
|
+
tables and current record payloads together; it must not introduce another
|
|
126
|
+
writable compatibility axis.
|
|
129
127
|
|
|
130
128
|
Yui provides four reusable Worker Profile definitions through
|
|
131
129
|
`yui config profile reset`; minimum setup makes each one inherit the current
|
|
@@ -917,10 +915,9 @@ Global Context entry:
|
|
|
917
915
|
yui session enter <global-role>
|
|
918
916
|
```
|
|
919
917
|
|
|
920
|
-
`yui update` accepts
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
Home.
|
|
918
|
+
`yui update` accepts either the current Home contract or any valid historical
|
|
919
|
+
contract at or above the staged CLI's minimum supported migration version.
|
|
920
|
+
Unsupported newer Homes and Homes below that floor remain untouched.
|
|
924
921
|
|
|
925
922
|
tmux fixes a pane's history capacity when that pane is created. Existing panes
|
|
926
923
|
retain their configured capacity; managed runtime output remains observable in
|
|
@@ -1003,9 +1000,10 @@ hiding the resources that remain. Use `--all` to include discovered Yui homes.
|
|
|
1003
1000
|
`controller restart` replaces the Controller process and its scheduler/socket services with the currently installed Yui version. It can recover a lost discovery record only when the old process still matches the current UID, Controller entrypoint, physical Home, PID, and process-start identity. It does not stop or restart managed tmux/Agent sessions.
|
|
1004
1001
|
|
|
1005
1002
|
Successful `setup` and `update` commands ensure that the current Home has a
|
|
1006
|
-
running Controller, starting one when the Home was previously idle.
|
|
1007
|
-
|
|
1008
|
-
|
|
1003
|
+
running Controller, starting one when the Home was previously idle. A
|
|
1004
|
+
successful `upgrade` restores a Controller only when it stopped one for the
|
|
1005
|
+
migration. `update` starts the replacement only after migration and health
|
|
1006
|
+
checks pass.
|
|
1009
1007
|
|
|
1010
1008
|
Its recovery reconciliation runs every 120 seconds by default. Normal durable state changes enqueue a Task, Role, or Operator key and return immediately; keys received in the same fixed 100 ms window trigger one non-overlapping targeted pass. Operator presentation has an independent lane, so a blocked Task workspace operation cannot delay a user question. Periodic Git/worktree work is limited to Tasks with durable Task-mailbox work, while active Role liveness uses one tmux inventory. Structured Agent Driver observations, whether received from native provider events or supported Hooks, are exact-fenced before they reach the durable runtime inbox. A terminal Turn observation atomically records the exact Turn result. Durable mailboxes freeze the current batch while new signals merge into the next batch. Task-orchestration failures retain the exact Controller-owned processing batch for two bounded fast retries and later periodic recovery; a successful retry completes that batch before newer pending work is claimed. Recommended InputRequest and pending Turn deadlines share one nearest-deadline selector and therefore do not wait for the recovery interval. Explicit `task reconcile` still requests an immediate recovery pass. The retained loop is:
|
|
1011
1009
|
|
|
@@ -1097,15 +1095,17 @@ yui project reset|replace|retire|delete
|
|
|
1097
1095
|
```
|
|
1098
1096
|
|
|
1099
1097
|
`yui update` stages the newly published package side by side and asks that exact
|
|
1100
|
-
binary to
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
Homes block preflight and
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
`yui upgrade`
|
|
1107
|
-
|
|
1108
|
-
|
|
1098
|
+
binary to classify the Home. Only then does it stop the exact old Controller,
|
|
1099
|
+
activate the same concrete package version, apply the complete missing
|
|
1100
|
+
migration chain when needed, validate the actually installed binary and Home,
|
|
1101
|
+
and start the replacement Controller. Unsupported Homes block preflight and
|
|
1102
|
+
remain untouched.
|
|
1103
|
+
|
|
1104
|
+
`yui upgrade --dry-run` prints the ordered migration plan without writing.
|
|
1105
|
+
`yui upgrade` creates a timestamped SQLite backup and upgrades any valid Home
|
|
1106
|
+
from the CLI's minimum supported storage version to its current version. Yui
|
|
1107
|
+
0.15.0 is storage version 1; pre-0.15.0 Homes remain outside that migration
|
|
1108
|
+
line and are never rewritten.
|
|
1109
1109
|
|
|
1110
1110
|
Agent environment bindings store process-environment variable names, never secret values. Adapter-owned lifecycle arguments cannot be overridden through raw arguments.
|
|
1111
1111
|
|
|
@@ -995,7 +995,7 @@ export const ROOT_COMMAND = buildNode({
|
|
|
995
995
|
{ name: "update", summary: "Install the latest published Yui package globally." },
|
|
996
996
|
{
|
|
997
997
|
name: "upgrade",
|
|
998
|
-
summary: "
|
|
998
|
+
summary: "Plan or apply supported storage migrations for this Home.",
|
|
999
999
|
usage: "yui upgrade [--dry-run]",
|
|
1000
1000
|
options: ["--dry-run"]
|
|
1001
1001
|
},
|
|
@@ -5,9 +5,9 @@ import { runUpdate } from "./updateOrchestrator.js";
|
|
|
5
5
|
* Run `yui update` as a side-by-side, recoverable orchestration.
|
|
6
6
|
*
|
|
7
7
|
* The new package is staged beside the live install and used to run a read-only
|
|
8
|
-
* preflight against the target Home.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* preflight against the target Home. A current Home proceeds directly; a Home
|
|
9
|
+
* inside the staged release's supported range is migrated after the exact
|
|
10
|
+
* Controller handoff and before post-update verification.
|
|
11
11
|
*
|
|
12
12
|
* Returns a process exit code: 0 on success or already-current, 5 on abort.
|
|
13
13
|
*/
|
|
@@ -27,15 +27,23 @@ export function renderUpdateResult(result) {
|
|
|
27
27
|
case "already-current":
|
|
28
28
|
return "Yui is already up to date; nothing to install.";
|
|
29
29
|
case "updated":
|
|
30
|
-
return
|
|
30
|
+
return result.backupPath === undefined
|
|
31
|
+
? `Updated Yui to ${result.version}; storage was already current.`
|
|
32
|
+
: `Updated Yui to ${result.version} and migrated storage. Backup: ${result.backupPath}`;
|
|
31
33
|
case "aborted":
|
|
32
34
|
return [
|
|
33
35
|
`Update aborted during ${result.phase}: ${result.message}`,
|
|
34
|
-
result.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
result.phase === "migrate-storage"
|
|
37
|
+
|| (result.phase === "post-verify" && result.recoverable)
|
|
38
|
+
? "The target binary is installed; the Home remains quiesced pending successful verification."
|
|
39
|
+
: result.recoverable
|
|
40
|
+
? "The current install and Home remain usable."
|
|
41
|
+
: result.phase === "activate-binary"
|
|
42
|
+
? "The Home was unchanged, but binary health is unknown; do not assume the current install is usable."
|
|
43
|
+
: "Manual recovery is required (see below).",
|
|
44
|
+
...(result.backupPath === undefined
|
|
45
|
+
? []
|
|
46
|
+
: [`Storage backup: ${result.backupPath}`]),
|
|
39
47
|
`Action: ${result.action}`
|
|
40
48
|
].join("\n");
|
|
41
49
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Side-by-side `yui update` orchestration for the
|
|
2
|
+
* Side-by-side `yui update` orchestration for the supported storage range.
|
|
3
3
|
*
|
|
4
|
-
* The staged binary proves an exact-current Home
|
|
5
|
-
*
|
|
4
|
+
* The staged binary proves either an exact-current Home or a complete
|
|
5
|
+
* migration path before the exact Controller is stopped. The replacement is
|
|
6
|
+
* then activated, any required storage migration is applied, and the result is
|
|
7
|
+
* verified before the Controller is restarted.
|
|
6
8
|
*/
|
|
7
9
|
export function runUpdate(ports, options) {
|
|
8
10
|
let staged;
|
|
@@ -61,6 +63,26 @@ function runStagedUpdate(ports, staged, home) {
|
|
|
61
63
|
...(preflight.sceneUnchanged === true ? { sceneUnchanged: true } : {})
|
|
62
64
|
};
|
|
63
65
|
}
|
|
66
|
+
if (preflight.status === "migration-ready" && ports.migrateStorage === undefined) {
|
|
67
|
+
return {
|
|
68
|
+
outcome: "aborted",
|
|
69
|
+
phase: "preflight",
|
|
70
|
+
message: "The staged binary requires a storage migration, but no migration operation is available.",
|
|
71
|
+
action: "Use the complete Yui updater or run the staged release's `yui upgrade` explicitly.",
|
|
72
|
+
recoverable: true,
|
|
73
|
+
version: staged.version
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (preflight.status === "migration-ready" && !hasCompleteControllerLifecycle(ports)) {
|
|
77
|
+
return {
|
|
78
|
+
outcome: "aborted",
|
|
79
|
+
phase: "preflight",
|
|
80
|
+
message: "Storage migration requires a complete, fenced Controller handoff.",
|
|
81
|
+
action: "Provide status, exact stop, replacement start, and exact restore operations.",
|
|
82
|
+
recoverable: true,
|
|
83
|
+
version: staged.version
|
|
84
|
+
};
|
|
85
|
+
}
|
|
64
86
|
let releaseHandover;
|
|
65
87
|
try {
|
|
66
88
|
releaseHandover = ports.beginControllerHandover?.(home);
|
|
@@ -79,13 +101,13 @@ function runStagedUpdate(ports, staged, home) {
|
|
|
79
101
|
const captured = captureControllerLifecycle(ports, staged.version, home);
|
|
80
102
|
if ("outcome" in captured)
|
|
81
103
|
return captured;
|
|
82
|
-
return activateAndVerify(ports, staged, home, captured.lifecycle);
|
|
104
|
+
return activateAndVerify(ports, staged, home, captured.lifecycle, preflight);
|
|
83
105
|
}
|
|
84
106
|
finally {
|
|
85
107
|
releaseHandover?.();
|
|
86
108
|
}
|
|
87
109
|
}
|
|
88
|
-
function activateAndVerify(ports, staged, home, lifecycle) {
|
|
110
|
+
function activateAndVerify(ports, staged, home, lifecycle, preflight) {
|
|
89
111
|
try {
|
|
90
112
|
ports.activateBinary(staged);
|
|
91
113
|
}
|
|
@@ -99,19 +121,46 @@ function activateAndVerify(ports, staged, home, lifecycle) {
|
|
|
99
121
|
version: staged.version
|
|
100
122
|
});
|
|
101
123
|
}
|
|
124
|
+
let migration;
|
|
125
|
+
if (preflight.status === "migration-ready") {
|
|
126
|
+
try {
|
|
127
|
+
migration = ports.migrateStorage(staged, home);
|
|
128
|
+
if (!isStorageMigrationResult(migration)) {
|
|
129
|
+
throw new Error("Storage migration returned a malformed result.");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return {
|
|
134
|
+
outcome: "aborted",
|
|
135
|
+
phase: "migrate-storage",
|
|
136
|
+
message: `Storage migration failed: ${messageOf(error)}`,
|
|
137
|
+
action: "The target Yui binary is installed and the Home remains quiesced. "
|
|
138
|
+
+ "Resolve the reported problem, rerun `yui upgrade`, then verify "
|
|
139
|
+
+ "`yui doctor` before starting the Controller.",
|
|
140
|
+
recoverable: true,
|
|
141
|
+
version: staged.version
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
102
145
|
try {
|
|
103
146
|
ports.verify(staged, home);
|
|
104
147
|
}
|
|
105
148
|
catch (error) {
|
|
149
|
+
const storageMigrated = preflight.status === "migration-ready";
|
|
106
150
|
const failure = {
|
|
107
151
|
outcome: "aborted",
|
|
108
152
|
phase: "post-verify",
|
|
109
153
|
message: `Post-update health check failed: ${messageOf(error)}`,
|
|
110
154
|
action: binaryHealthUncertainAction(),
|
|
111
|
-
recoverable:
|
|
112
|
-
version: staged.version
|
|
155
|
+
recoverable: storageMigrated,
|
|
156
|
+
version: staged.version,
|
|
157
|
+
...(migration?.backupPath === undefined
|
|
158
|
+
? {}
|
|
159
|
+
: { backupPath: migration.backupPath })
|
|
113
160
|
};
|
|
114
|
-
return
|
|
161
|
+
return storageMigrated
|
|
162
|
+
? failure
|
|
163
|
+
: restoreControllerOrReport(ports, home, lifecycle, failure);
|
|
115
164
|
}
|
|
116
165
|
if (lifecycle.ensureRunning) {
|
|
117
166
|
try {
|
|
@@ -119,6 +168,7 @@ function activateAndVerify(ports, staged, home, lifecycle) {
|
|
|
119
168
|
}
|
|
120
169
|
catch (error) {
|
|
121
170
|
const unknownActive = isUnknownActiveControllerFailure(error);
|
|
171
|
+
const storageMigrated = preflight.status === "migration-ready";
|
|
122
172
|
const failure = {
|
|
123
173
|
outcome: "aborted",
|
|
124
174
|
phase: "post-verify",
|
|
@@ -127,21 +177,27 @@ function activateAndVerify(ports, staged, home, lifecycle) {
|
|
|
127
177
|
: "The replacement Controller could not start after activation and verification"}: ${messageOf(error)}.`,
|
|
128
178
|
action: unknownActive
|
|
129
179
|
? unknownActiveControllerAction(home)
|
|
130
|
-
:
|
|
131
|
-
? "Keep the Home quiesced
|
|
132
|
-
:
|
|
133
|
-
|
|
180
|
+
: storageMigrated
|
|
181
|
+
? "Keep the Home quiesced, verify the activated binary, then start the current Controller explicitly."
|
|
182
|
+
: lifecycle.wasRunning
|
|
183
|
+
? "Keep the Home quiesced and restore the captured Controller identity before retrying."
|
|
184
|
+
: "Verify the activated binary, then start the Controller explicitly.",
|
|
185
|
+
recoverable: storageMigrated && !unknownActive,
|
|
134
186
|
version: staged.version,
|
|
135
|
-
|
|
187
|
+
...(migration?.backupPath === undefined
|
|
188
|
+
? {}
|
|
189
|
+
: { backupPath: migration.backupPath }),
|
|
190
|
+
...(unknownActive ? { controllerOwnershipUnknown: true } : {})
|
|
136
191
|
};
|
|
137
|
-
return unknownActive
|
|
192
|
+
return unknownActive || storageMigrated
|
|
138
193
|
? failure
|
|
139
194
|
: restoreControllerOrReport(ports, home, lifecycle, failure);
|
|
140
195
|
}
|
|
141
196
|
}
|
|
142
197
|
return {
|
|
143
198
|
outcome: "updated",
|
|
144
|
-
version: staged.version
|
|
199
|
+
version: staged.version,
|
|
200
|
+
...(migration?.backupPath === undefined ? {} : { backupPath: migration.backupPath })
|
|
145
201
|
};
|
|
146
202
|
}
|
|
147
203
|
function captureControllerLifecycle(ports, version, home) {
|
|
@@ -259,6 +315,16 @@ function isControllerLifecycleStatus(value) {
|
|
|
259
315
|
function isControllerStopResult(value) {
|
|
260
316
|
return isRecord(value) && typeof value.stopped === "boolean";
|
|
261
317
|
}
|
|
318
|
+
function hasCompleteControllerLifecycle(ports) {
|
|
319
|
+
return ports.controllerStatus !== undefined
|
|
320
|
+
&& ports.stopController !== undefined
|
|
321
|
+
&& ports.startController !== undefined
|
|
322
|
+
&& ports.restoreController !== undefined;
|
|
323
|
+
}
|
|
324
|
+
function isStorageMigrationResult(value) {
|
|
325
|
+
return isRecord(value)
|
|
326
|
+
&& (value.backupPath === undefined || typeof value.backupPath === "string");
|
|
327
|
+
}
|
|
262
328
|
function isPositivePid(value) {
|
|
263
329
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
264
330
|
}
|
package/dist/cli/updatePorts.js
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* `npm install --global --prefix <tmp>`, so the live global install is never
|
|
8
8
|
* touched until the binary-activation step. Preflight invokes the STAGED binary's
|
|
9
9
|
* internal `yui upgrade --update-preflight` contract so the target version
|
|
10
|
-
* proves that the Home
|
|
11
|
-
* parent stops the exact old Controller, the binary is promoted
|
|
12
|
-
*
|
|
10
|
+
* proves that the Home is current or has a complete supported migration path.
|
|
11
|
+
* After the parent stops the exact old Controller, the binary is promoted,
|
|
12
|
+
* required migrations run through that same staged artifact, and the activated
|
|
13
|
+
* binary verifies the resulting current Home.
|
|
13
14
|
*
|
|
14
15
|
* Two hardening guarantees this module enforces:
|
|
15
16
|
*
|
|
@@ -146,13 +147,26 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
|
|
|
146
147
|
const result = run("npm", ["install", "--global", spec], { cwd: process.cwd(), env: environment, shell: false, stdio: "inherit" });
|
|
147
148
|
assertSpawnOk(result, "activate the new binary");
|
|
148
149
|
},
|
|
150
|
+
migrateStorage(staged, home) {
|
|
151
|
+
const result = run(staged.binaryPath, ["--json", "upgrade", "--update-apply"], {
|
|
152
|
+
cwd: process.cwd(),
|
|
153
|
+
env: {
|
|
154
|
+
...environment,
|
|
155
|
+
YUI_HOME: home,
|
|
156
|
+
YUI_UPDATE_HANDOVER_OWNER_PID: String(process.pid)
|
|
157
|
+
},
|
|
158
|
+
shell: false
|
|
159
|
+
});
|
|
160
|
+
return interpretStorageMigration(result);
|
|
161
|
+
},
|
|
149
162
|
verify(staged, home) {
|
|
150
163
|
// Verify the ACTUALLY-ACTIVATED global binary, not the staging path (P1-3).
|
|
151
164
|
const activeBinary = resolveGlobalBinary(environment, run);
|
|
152
165
|
if (activeBinary === null || !existsSync(activeBinary)) {
|
|
153
166
|
throw runtimeError("Post-update health check failed: could not locate the activated global `yui` binary.");
|
|
154
167
|
}
|
|
155
|
-
// 1) Health check the
|
|
168
|
+
// 1) Health check the current (possibly just migrated) Home through the
|
|
169
|
+
// activated binary.
|
|
156
170
|
// POST-VERIFY PARSES THE MACHINE-READABLE RESULT FIRST, THEN THE EXIT STATUS
|
|
157
171
|
// (R2-F2). `yui --json doctor` deliberately sets a non-zero exit when storage
|
|
158
172
|
// is unhealthy, so interpreting the exit status before the envelope would
|
|
@@ -784,21 +798,69 @@ function interpretPreflight(result) {
|
|
|
784
798
|
...(data.sceneUnchanged === true ? { sceneUnchanged: true } : {})
|
|
785
799
|
};
|
|
786
800
|
}
|
|
787
|
-
/** Accept
|
|
801
|
+
/** Accept either an exact current Home or a complete supported migration path. */
|
|
788
802
|
function parseUpdatePreflightResult(data) {
|
|
789
|
-
if (data.status !== "already-current"
|
|
803
|
+
if ((data.status !== "already-current" && data.status !== "migration-ready")
|
|
790
804
|
|| !Number.isSafeInteger(data.stepCount)
|
|
791
|
-
|| data.stepCount !== 0
|
|
792
805
|
|| !Array.isArray(data.steps)
|
|
793
|
-
|| data.steps.length !==
|
|
806
|
+
|| data.steps.length !== data.stepCount)
|
|
794
807
|
return null;
|
|
795
808
|
const homeClassification = data.classification;
|
|
796
809
|
if (!isRecord(homeClassification) || !isRecord(homeClassification.classification))
|
|
797
810
|
return null;
|
|
798
811
|
const classification = homeClassification.classification;
|
|
799
|
-
if (
|
|
812
|
+
if (data.status === "already-current") {
|
|
813
|
+
if (data.stepCount !== 0
|
|
814
|
+
|| classification.verdict !== "USABLE"
|
|
815
|
+
|| classification.status !== "current")
|
|
816
|
+
return null;
|
|
817
|
+
return { status: "already-current", stepCount: 0 };
|
|
818
|
+
}
|
|
819
|
+
if (data.stepCount <= 0
|
|
820
|
+
|| classification.verdict !== "MIGRATABLE"
|
|
821
|
+
|| classification.status !== "migration-ready"
|
|
822
|
+
|| !data.steps.every(isStorageMigrationStep))
|
|
800
823
|
return null;
|
|
801
|
-
return { status: "
|
|
824
|
+
return { status: "migration-ready", stepCount: data.stepCount };
|
|
825
|
+
}
|
|
826
|
+
function interpretStorageMigration(result) {
|
|
827
|
+
const data = parseSuccessEnvelopeData(result);
|
|
828
|
+
if (data === null) {
|
|
829
|
+
throw runtimeError("The staged binary did not return a successful storage migration result "
|
|
830
|
+
+ `(exit ${result.status ?? "null"}${result.signal === null ? "" : `, signal ${result.signal}`}).`);
|
|
831
|
+
}
|
|
832
|
+
if (data.outcome === "blocked" || data.outcome === "failed") {
|
|
833
|
+
const message = typeof data.message === "string"
|
|
834
|
+
? data.message
|
|
835
|
+
: "The staged binary refused the storage migration.";
|
|
836
|
+
const action = typeof data.action === "string" ? ` Action: ${data.action}` : "";
|
|
837
|
+
const backup = typeof data.backupPath === "string"
|
|
838
|
+
? ` Backup: ${data.backupPath}`
|
|
839
|
+
: "";
|
|
840
|
+
throw runtimeError(`${message}${action}${backup}`);
|
|
841
|
+
}
|
|
842
|
+
if (result.status !== 0) {
|
|
843
|
+
throw runtimeError(`The staged binary returned outcome=${String(data.outcome)} but exited `
|
|
844
|
+
+ `with status ${result.status ?? "null"}.`);
|
|
845
|
+
}
|
|
846
|
+
if (data.outcome === "already-current")
|
|
847
|
+
return {};
|
|
848
|
+
if (data.outcome !== "upgraded" || !isRecord(data.report)) {
|
|
849
|
+
throw runtimeError(`The staged binary returned unexpected storage migration outcome=${String(data.outcome)}.`);
|
|
850
|
+
}
|
|
851
|
+
const backupPath = data.report.backupPath;
|
|
852
|
+
if (typeof backupPath !== "string" || backupPath.length === 0) {
|
|
853
|
+
throw runtimeError("The staged binary did not report the required storage backup path.");
|
|
854
|
+
}
|
|
855
|
+
return { backupPath };
|
|
856
|
+
}
|
|
857
|
+
function isStorageMigrationStep(value) {
|
|
858
|
+
return isRecord(value)
|
|
859
|
+
&& Number.isSafeInteger(value.fromVersion)
|
|
860
|
+
&& Number.isSafeInteger(value.toVersion)
|
|
861
|
+
&& value.toVersion === value.fromVersion + 1
|
|
862
|
+
&& typeof value.name === "string"
|
|
863
|
+
&& value.name.length > 0;
|
|
802
864
|
}
|
|
803
865
|
function parseUpdateBlockers(value) {
|
|
804
866
|
if (value === undefined)
|