@younndai/lyt 0.13.5 → 0.20.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/README.md +27 -19
- package/dist/automator-run.d.ts +1 -0
- package/dist/automator-run.d.ts.map +1 -1
- package/dist/automator-run.js +5 -3
- package/dist/automator-run.js.map +1 -1
- package/dist/bench/pod-harness.d.ts.map +1 -1
- package/dist/bench/pod-harness.js +49 -1
- package/dist/bench/pod-harness.js.map +1 -1
- package/dist/build-program.d.ts +10 -1
- package/dist/build-program.d.ts.map +1 -1
- package/dist/build-program.js +23 -3
- package/dist/build-program.js.map +1 -1
- package/dist/cli.js +10 -4
- package/dist/cli.js.map +1 -1
- package/dist/commands/capture.d.ts.map +1 -1
- package/dist/commands/capture.js +1 -2
- package/dist/commands/capture.js.map +1 -1
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +404 -244
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/install.d.ts +4 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +107 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/outdated.d.ts.map +1 -1
- package/dist/commands/outdated.js +19 -5
- package/dist/commands/outdated.js.map +1 -1
- package/dist/commands/update.d.ts +90 -2
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +883 -48
- package/dist/commands/update.js.map +1 -1
- package/dist/flows/init-bootstrap.d.ts +7 -1
- package/dist/flows/init-bootstrap.d.ts.map +1 -1
- package/dist/flows/init-bootstrap.js +240 -69
- package/dist/flows/init-bootstrap.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/install/doctor-health.d.ts +16 -0
- package/dist/install/doctor-health.d.ts.map +1 -0
- package/dist/install/doctor-health.js +575 -0
- package/dist/install/doctor-health.js.map +1 -0
- package/dist/install/provider-inventory.d.ts +40 -0
- package/dist/install/provider-inventory.d.ts.map +1 -0
- package/dist/install/provider-inventory.js +73 -0
- package/dist/install/provider-inventory.js.map +1 -0
- package/dist/install/reconcile-engine.d.ts +87 -0
- package/dist/install/reconcile-engine.d.ts.map +1 -0
- package/dist/install/reconcile-engine.js +771 -0
- package/dist/install/reconcile-engine.js.map +1 -0
- package/dist/install/target-artifacts.d.ts +91 -0
- package/dist/install/target-artifacts.d.ts.map +1 -0
- package/dist/install/target-artifacts.js +597 -0
- package/dist/install/target-artifacts.js.map +1 -0
- package/dist/install/update-operation.d.ts +47 -0
- package/dist/install/update-operation.d.ts.map +1 -0
- package/dist/install/update-operation.js +318 -0
- package/dist/install/update-operation.js.map +1 -0
- package/dist/install/update-plan.d.ts +132 -0
- package/dist/install/update-plan.d.ts.map +1 -0
- package/dist/install/update-plan.js +418 -0
- package/dist/install/update-plan.js.map +1 -0
- package/package.json +16 -6
package/dist/commands/update.js
CHANGED
|
@@ -14,77 +14,912 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { spawnSync } from "node:child_process";
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
19
|
+
import { createRequire } from "node:module";
|
|
20
|
+
import { dirname, join, resolve } from "node:path";
|
|
17
21
|
import { createInterface } from "node:readline/promises";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
24
|
+
import { checkCurrency, formatCurrencyLine, isUpdateChannel, readUpdateChannel, resolveUpdateAction, writeUpdateChannel, } from "@younndai/lyt-vault";
|
|
25
|
+
import { canonicalJson, digestCanonical, applyExactUpdatePlanV1, parseUpdatePlanV1, prepareUpdatePlanV1, UPDATE_PLAN_PACKAGES, } from "../install/update-plan.js";
|
|
26
|
+
import { prepareInstallReconcilePlanV1, } from "../install/reconcile-engine.js";
|
|
27
|
+
import { beginUpdateOperationV1, defaultUpdateOperationRoot, readUpdateOperationPlanV1, } from "../install/update-operation.js";
|
|
28
|
+
import { inspectTargetTarballBytes, materializeTargetProviders, providersFromUpdatePlan, stageTargetArtifactsV1, verifyInstalledPayloadV1, writeInstalledStateAnchorV1, } from "../install/target-artifacts.js";
|
|
29
|
+
import { detectInstalledRuntimes } from "@younndai/lyt-vault";
|
|
30
|
+
export function configureUpdateChannel(channel, write = writeUpdateChannel) {
|
|
31
|
+
write(channel);
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
schema_id: "lyt.update-channel-configured",
|
|
34
|
+
schema_version: Object.freeze({ major: 1, minor: 0 }),
|
|
35
|
+
channel,
|
|
36
|
+
persisted: true,
|
|
37
|
+
next_action: "lyt update --check --json",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function parseChannel(value) {
|
|
41
|
+
if (isUpdateChannel(value))
|
|
42
|
+
return value;
|
|
43
|
+
throw new InvalidArgumentError("--channel must be alpha or latest");
|
|
44
|
+
}
|
|
45
|
+
function readMetaVersion() {
|
|
46
|
+
return createRequire(import.meta.url)("../package.json").version;
|
|
47
|
+
}
|
|
48
|
+
export function resolveConfiguredChannel(requested, persisted, opts) {
|
|
49
|
+
if (requested !== undefined) {
|
|
50
|
+
if (persisted !== null && requested !== persisted && opts.switchChannel !== true) {
|
|
51
|
+
return {
|
|
52
|
+
channel: null,
|
|
53
|
+
source: "explicit",
|
|
54
|
+
refusal: "channel-switch-requires-explicit",
|
|
55
|
+
nextAction: `lyt update --channel ${requested} --switch-channel`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return { channel: requested, source: "explicit" };
|
|
59
|
+
}
|
|
60
|
+
if (persisted !== null)
|
|
61
|
+
return { channel: persisted, source: "persisted" };
|
|
62
|
+
if (opts.interactive && opts.selectedInteractive !== undefined) {
|
|
63
|
+
if (opts.selectedInteractive !== null)
|
|
64
|
+
return { channel: opts.selectedInteractive, source: "interactive" };
|
|
65
|
+
return {
|
|
66
|
+
channel: null,
|
|
67
|
+
source: "unconfigured",
|
|
68
|
+
refusal: "channel-unconfigured",
|
|
69
|
+
nextAction: "lyt update --channel alpha",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
channel: null,
|
|
74
|
+
source: "unconfigured",
|
|
75
|
+
refusal: "channel-unconfigured",
|
|
76
|
+
nextAction: "lyt update --channel alpha",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function parseReconcileReceipt(raw, updatePlan) {
|
|
80
|
+
try {
|
|
81
|
+
const value = JSON.parse(raw);
|
|
82
|
+
const receipt = value.receipt;
|
|
83
|
+
if (value.schema_id !== "lyt.install-reconcile-result" ||
|
|
84
|
+
value.schema_version?.major !== 1 ||
|
|
85
|
+
value.schema_version?.minor !== 0 ||
|
|
86
|
+
typeof value.operation_id !== "string" ||
|
|
87
|
+
typeof value.attempt_id !== "string" ||
|
|
88
|
+
typeof value.status !== "string" ||
|
|
89
|
+
typeof value.success !== "boolean" ||
|
|
90
|
+
value.success !== true ||
|
|
91
|
+
(value.status !== "success" && value.status !== "no-op") ||
|
|
92
|
+
!Number.isInteger(value.mutations) ||
|
|
93
|
+
(value.mutations ?? -1) < 0 ||
|
|
94
|
+
!Array.isArray(value.completed) ||
|
|
95
|
+
!Array.isArray(value.pending) ||
|
|
96
|
+
!Array.isArray(value.refused) ||
|
|
97
|
+
value.pending.length !== 0 ||
|
|
98
|
+
value.refused.length !== 0 ||
|
|
99
|
+
value.next_action !== null ||
|
|
100
|
+
!("mutations" in value) ||
|
|
101
|
+
!("next_action" in value) ||
|
|
102
|
+
receipt?.schema_id !== "lyt.receipt" ||
|
|
103
|
+
receipt.schema_version?.major !== 1 ||
|
|
104
|
+
receipt.schema_version?.minor !== 0 ||
|
|
105
|
+
receipt.operation !== "install-reconcile" ||
|
|
106
|
+
receipt.operation_id !== value.operation_id ||
|
|
107
|
+
receipt.attempt_id !== value.attempt_id ||
|
|
108
|
+
receipt.status !== value.status ||
|
|
109
|
+
!receipt.mutations ||
|
|
110
|
+
receipt.mutations.local !== value.mutations ||
|
|
111
|
+
receipt.mutations.remote !== 0 ||
|
|
112
|
+
receipt.next_action !== null) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
if (updatePlan !== undefined) {
|
|
116
|
+
const completed = [...value.completed].sort();
|
|
117
|
+
const expected = [...updatePlan.target_provider_objects].sort((a, b) => a.object_id.localeCompare(b.object_id));
|
|
118
|
+
const actual = [...(value.plan?.objects ?? [])]
|
|
119
|
+
.filter((object) => expected.some((entry) => entry.object_id === object.object_id))
|
|
120
|
+
.sort((a, b) => a.object_id.localeCompare(b.object_id));
|
|
121
|
+
if (canonicalJson(completed) !== canonicalJson(expected.map((object) => object.object_id)) ||
|
|
122
|
+
canonicalJson(actual.map((object) => ({
|
|
123
|
+
object_id: object.object_id,
|
|
124
|
+
expected_digest: object.expected_digest,
|
|
125
|
+
expected_applied_digest: object.expected_applied_digest,
|
|
126
|
+
}))) !==
|
|
127
|
+
canonicalJson(expected.map((object) => ({
|
|
128
|
+
object_id: object.object_id,
|
|
129
|
+
expected_digest: object.expected_digest,
|
|
130
|
+
expected_applied_digest: object.expected_applied_digest,
|
|
131
|
+
}))))
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return receipt;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
20
140
|
export async function runUpdateFlow(result, opts, fx) {
|
|
21
|
-
const action = resolveUpdateAction(result, {
|
|
141
|
+
const action = resolveUpdateAction(result, {
|
|
142
|
+
yes: opts.yes,
|
|
143
|
+
interactive: opts.interactive,
|
|
144
|
+
allowDowngrade: opts.allowDowngrade,
|
|
145
|
+
});
|
|
22
146
|
switch (action.kind) {
|
|
23
|
-
case "
|
|
147
|
+
case "channel-unconfigured":
|
|
24
148
|
fx.error(action.message);
|
|
25
|
-
return {
|
|
149
|
+
return {
|
|
150
|
+
installed: false,
|
|
151
|
+
reconciled: false,
|
|
152
|
+
reconcileReceipt: null,
|
|
153
|
+
exitCode: 2,
|
|
154
|
+
nextAction: action.nextAction,
|
|
155
|
+
};
|
|
156
|
+
case "offline":
|
|
26
157
|
case "current":
|
|
27
|
-
|
|
28
|
-
|
|
158
|
+
case "ahead-of-channel":
|
|
159
|
+
(action.kind === "ahead-of-channel" ? fx.error : fx.log)(action.message);
|
|
160
|
+
return {
|
|
161
|
+
installed: false,
|
|
162
|
+
reconciled: false,
|
|
163
|
+
reconcileReceipt: null,
|
|
164
|
+
exitCode: action.kind === "ahead-of-channel" ? 2 : 0,
|
|
165
|
+
};
|
|
29
166
|
case "blocked-noninteractive":
|
|
30
167
|
fx.error(action.message);
|
|
31
|
-
return { installed: false, exitCode: 1 };
|
|
168
|
+
return { installed: false, reconciled: false, reconcileReceipt: null, exitCode: 1 };
|
|
32
169
|
case "needs-confirm": {
|
|
33
|
-
|
|
34
|
-
if (!ok) {
|
|
35
|
-
fx.log("Update cancelled.");
|
|
36
|
-
return { installed: false, exitCode: 0 };
|
|
37
|
-
}
|
|
38
|
-
return finishInstall(fx);
|
|
170
|
+
return prepareAndFinishInstall(result, fx, async (plan) => fx.confirm(`${action.message} Exact target ${plan.target_version}; sealed plan ${plan.plan_digest}. [y/N] `));
|
|
39
171
|
}
|
|
40
172
|
case "proceed":
|
|
41
173
|
fx.log(action.message);
|
|
42
|
-
return
|
|
174
|
+
return prepareAndFinishInstall(result, fx);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function prepareAndFinishInstall(currency, fx, confirmation) {
|
|
178
|
+
let plan;
|
|
179
|
+
try {
|
|
180
|
+
plan = parseUpdatePlanV1(await fx.preparePlan(currency));
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
fx.error(`lyt update: could not seal the exact update plan (${String(error)}).`);
|
|
184
|
+
return { installed: false, reconciled: false, reconcileReceipt: null, exitCode: 1 };
|
|
43
185
|
}
|
|
186
|
+
if (confirmation !== undefined && !(await confirmation(plan))) {
|
|
187
|
+
fx.log("Update cancelled.");
|
|
188
|
+
return {
|
|
189
|
+
installed: false,
|
|
190
|
+
reconciled: false,
|
|
191
|
+
reconcileReceipt: null,
|
|
192
|
+
exitCode: 0,
|
|
193
|
+
updatePlan: plan,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return finishInstall(plan, fx);
|
|
44
197
|
}
|
|
45
|
-
function finishInstall(fx) {
|
|
46
|
-
const
|
|
198
|
+
async function finishInstall(plan, fx) {
|
|
199
|
+
const alreadyInstalled = await fx.verifyInstalledPlan(plan);
|
|
200
|
+
if (!alreadyInstalled && !(await fx.revalidatePlan(plan))) {
|
|
201
|
+
fx.error("lyt update: sealed registry, package graph, or destination evidence changed; no install started.");
|
|
202
|
+
return {
|
|
203
|
+
installed: false,
|
|
204
|
+
reconciled: false,
|
|
205
|
+
reconcileReceipt: null,
|
|
206
|
+
exitCode: 1,
|
|
207
|
+
updatePlan: plan,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
let operation;
|
|
211
|
+
try {
|
|
212
|
+
operation = await fx.beginOperation(plan);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
fx.error(`lyt update: could not open the durable update operation (${String(error)}).`);
|
|
216
|
+
return {
|
|
217
|
+
installed: false,
|
|
218
|
+
reconciled: false,
|
|
219
|
+
reconcileReceipt: null,
|
|
220
|
+
exitCode: 1,
|
|
221
|
+
nextAction: "lyt doctor --json",
|
|
222
|
+
updatePlan: plan,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (operation === null) {
|
|
226
|
+
fx.error("lyt update: the Phase-A update receipt store is unavailable; no install started.");
|
|
227
|
+
return {
|
|
228
|
+
installed: false,
|
|
229
|
+
reconciled: false,
|
|
230
|
+
reconcileReceipt: null,
|
|
231
|
+
exitCode: 1,
|
|
232
|
+
updatePlan: plan,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (operation.alreadyCompleted) {
|
|
236
|
+
fx.log(`lyt update: sealed operation ${plan.operation_id} is already complete.`);
|
|
237
|
+
return {
|
|
238
|
+
installed: true,
|
|
239
|
+
reconciled: true,
|
|
240
|
+
reconcileReceipt: null,
|
|
241
|
+
exitCode: 0,
|
|
242
|
+
updatePlan: plan,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
let artifactPaths = [];
|
|
246
|
+
if (!alreadyInstalled) {
|
|
247
|
+
try {
|
|
248
|
+
artifactPaths = await fx.stageArtifacts(plan);
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
fx.error(`lyt update: exact target artifacts could not be staged (${String(error)}).`);
|
|
252
|
+
return {
|
|
253
|
+
installed: false,
|
|
254
|
+
reconciled: false,
|
|
255
|
+
reconcileReceipt: null,
|
|
256
|
+
exitCode: 1,
|
|
257
|
+
nextAction: `lyt update --resume ${plan.operation_id} --yes`,
|
|
258
|
+
updatePlan: plan,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const status = alreadyInstalled ? 0 : fx.install(plan, artifactPaths);
|
|
47
263
|
if (status !== 0) {
|
|
48
|
-
fx.error(`lyt update:
|
|
49
|
-
return {
|
|
264
|
+
fx.error(`lyt update: exact @younndai/lyt@${plan.target_version} install failed (exit ${status}).`);
|
|
265
|
+
return {
|
|
266
|
+
installed: false,
|
|
267
|
+
reconciled: false,
|
|
268
|
+
reconcileReceipt: null,
|
|
269
|
+
exitCode: 1,
|
|
270
|
+
nextAction: `lyt update --resume ${plan.operation_id} --yes`,
|
|
271
|
+
updatePlan: plan,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (!(await fx.verifyInstalledPlan(plan))) {
|
|
275
|
+
fx.error("lyt update: npm returned success, but the seven exact installed versions or sealed same-registry package graph did not verify.");
|
|
276
|
+
return {
|
|
277
|
+
installed: false,
|
|
278
|
+
reconciled: false,
|
|
279
|
+
reconcileReceipt: null,
|
|
280
|
+
exitCode: 1,
|
|
281
|
+
nextAction: `lyt update --resume ${plan.operation_id} --yes`,
|
|
282
|
+
updatePlan: plan,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (!(await operation.finalizeCompleted())) {
|
|
286
|
+
fx.error("lyt update: npm replacement verified, but the authoritative update receipt was not persisted.");
|
|
287
|
+
return {
|
|
288
|
+
installed: true,
|
|
289
|
+
reconciled: false,
|
|
290
|
+
reconcileReceipt: null,
|
|
291
|
+
exitCode: 1,
|
|
292
|
+
nextAction: `lyt update --resume ${plan.operation_id} --yes`,
|
|
293
|
+
updatePlan: plan,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const reconciliation = fx.reconcile(plan);
|
|
297
|
+
const receipt = reconciliation.exitCode === 0 ? parseReconcileReceipt(reconciliation.stdout, plan) : null;
|
|
298
|
+
if (receipt === null || (receipt.status !== "success" && receipt.status !== "no-op")) {
|
|
299
|
+
const detail = reconciliation.stderr.trim() || reconciliation.stdout.trim();
|
|
300
|
+
fx.error("lyt update: npm replacement completed, but the replacement Lyt did not return an authoritative reconciliation Receipt. " +
|
|
301
|
+
"Run `lyt install reconcile --apply --json` in a new process; start a fresh agent session before expecting managed skills or manuals to be loaded." +
|
|
302
|
+
(detail.length > 0 ? ` Detail: ${detail.slice(0, 500)}` : ""));
|
|
303
|
+
return {
|
|
304
|
+
installed: true,
|
|
305
|
+
reconciled: false,
|
|
306
|
+
reconcileReceipt: receipt,
|
|
307
|
+
exitCode: 1,
|
|
308
|
+
nextAction: "lyt install reconcile --apply --json",
|
|
309
|
+
updatePlan: plan,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const receiptDigest = digestCanonical(receipt);
|
|
313
|
+
if (!fx.writeInstalledAnchor(plan)) {
|
|
314
|
+
fx.error("lyt update: reconciliation succeeded, but installed-state evidence was not anchored.");
|
|
315
|
+
return {
|
|
316
|
+
installed: true,
|
|
317
|
+
reconciled: false,
|
|
318
|
+
reconcileReceipt: receipt,
|
|
319
|
+
exitCode: 1,
|
|
320
|
+
nextAction: `lyt update --resume ${plan.operation_id} --yes`,
|
|
321
|
+
updatePlan: plan,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (!(await operation.recordReconciled(receiptDigest))) {
|
|
325
|
+
fx.error("lyt update: reconciliation succeeded, but its boundary evidence was not journaled.");
|
|
326
|
+
return {
|
|
327
|
+
installed: true,
|
|
328
|
+
reconciled: false,
|
|
329
|
+
reconcileReceipt: receipt,
|
|
330
|
+
exitCode: 1,
|
|
331
|
+
nextAction: `lyt update --resume ${plan.operation_id} --yes`,
|
|
332
|
+
updatePlan: plan,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const applied = await applyExactUpdatePlanV1(plan, {
|
|
336
|
+
alreadyCompleted: async (boundary) => operation.isBoundaryCompleted(boundary.boundary_id),
|
|
337
|
+
revalidate: async () => false,
|
|
338
|
+
apply: async () => {
|
|
339
|
+
throw new Error("update-operation-boundary-not-journaled");
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
if (applied.pending.length > 0) {
|
|
343
|
+
fx.error("lyt update: the exact update plan still has unjournaled boundaries.");
|
|
344
|
+
return {
|
|
345
|
+
installed: true,
|
|
346
|
+
reconciled: false,
|
|
347
|
+
reconcileReceipt: receipt,
|
|
348
|
+
exitCode: 1,
|
|
349
|
+
nextAction: `lyt update --resume ${plan.operation_id} --yes`,
|
|
350
|
+
updatePlan: plan,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
fx.log(`lyt update: npm replacement completed; reconciliation Receipt ${receipt.operation_id}/${receipt.attempt_id} is authoritative. ` +
|
|
354
|
+
"Start a fresh agent session before treating injected manuals or skills as loaded.");
|
|
355
|
+
return {
|
|
356
|
+
installed: true,
|
|
357
|
+
reconciled: true,
|
|
358
|
+
reconcileReceipt: receipt,
|
|
359
|
+
exitCode: 0,
|
|
360
|
+
updatePlan: plan,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
export async function resumeUpdateFlow(operationId, fx) {
|
|
364
|
+
return finishInstall(readUpdateOperationPlanV1(operationId), fx);
|
|
365
|
+
}
|
|
366
|
+
function resolveNpmCliPath() {
|
|
367
|
+
const candidates = [
|
|
368
|
+
process.env.npm_execpath,
|
|
369
|
+
process.env.NPM_EXECPATH,
|
|
370
|
+
join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"),
|
|
371
|
+
join(dirname(dirname(process.execPath)), "node_modules", "npm", "bin", "npm-cli.js"),
|
|
372
|
+
].filter((candidate) => typeof candidate === "string" && candidate.length > 0);
|
|
373
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
374
|
+
return found === undefined ? null : resolve(found);
|
|
375
|
+
}
|
|
376
|
+
function packageRoot(packageName) {
|
|
377
|
+
try {
|
|
378
|
+
const require = createRequire(import.meta.url);
|
|
379
|
+
let cursor = dirname(require.resolve(packageName));
|
|
380
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
381
|
+
if (existsSync(join(cursor, "package.json")))
|
|
382
|
+
return cursor;
|
|
383
|
+
const parent = dirname(cursor);
|
|
384
|
+
if (parent === cursor)
|
|
385
|
+
break;
|
|
386
|
+
cursor = parent;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
async function fetchExactPackageGraph(plan) {
|
|
395
|
+
return Promise.all(UPDATE_PLAN_PACKAGES.map(async (name) => {
|
|
396
|
+
const url = new URL(`${encodeURIComponent(name)}/${encodeURIComponent(plan.version)}`, plan.registry);
|
|
397
|
+
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
398
|
+
if (!response.ok)
|
|
399
|
+
throw new Error(`update-plan-registry-${response.status}`);
|
|
400
|
+
const manifest = (await response.json());
|
|
401
|
+
if (manifest.version !== plan.version ||
|
|
402
|
+
typeof manifest.dist?.integrity !== "string" ||
|
|
403
|
+
typeof manifest.dist.tarball !== "string") {
|
|
404
|
+
throw new Error("update-plan-registry-manifest-invalid");
|
|
405
|
+
}
|
|
406
|
+
const dependencies = Object.keys(manifest.dependencies ?? {})
|
|
407
|
+
.filter((candidate) => UPDATE_PLAN_PACKAGES.includes(candidate))
|
|
408
|
+
.sort();
|
|
409
|
+
return Object.freeze({
|
|
410
|
+
name,
|
|
411
|
+
version: plan.version,
|
|
412
|
+
integrity: manifest.dist.integrity,
|
|
413
|
+
tarball_url: manifest.dist.tarball,
|
|
414
|
+
dependencies,
|
|
415
|
+
});
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
function observeInstalledState() {
|
|
419
|
+
const packages = UPDATE_PLAN_PACKAGES.map((name) => {
|
|
420
|
+
const root = packageRoot(name);
|
|
421
|
+
if (root === null)
|
|
422
|
+
return { name, version: null, integrity: null };
|
|
423
|
+
try {
|
|
424
|
+
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
425
|
+
return {
|
|
426
|
+
name,
|
|
427
|
+
version: typeof manifest.version === "string" ? manifest.version : null,
|
|
428
|
+
integrity: null,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
return { name, version: null, integrity: null };
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
const cli = typeof process.argv[1] === "string" ? resolve(process.argv[1]) : null;
|
|
436
|
+
const bins = cli === null
|
|
437
|
+
? []
|
|
438
|
+
: [
|
|
439
|
+
{
|
|
440
|
+
path: cli,
|
|
441
|
+
digest: existsSync(cli)
|
|
442
|
+
? createHash("sha256").update(readFileSync(cli)).digest("hex")
|
|
443
|
+
: null,
|
|
444
|
+
},
|
|
445
|
+
];
|
|
446
|
+
return { packages, bins };
|
|
447
|
+
}
|
|
448
|
+
export function providerDestinationsFromReconcilePlan(reconcilePlan) {
|
|
449
|
+
return reconcilePlan.objects.map((object) => ({
|
|
450
|
+
object_id: object.object_id,
|
|
451
|
+
kind: object.kind === "directory-link" ? "skill" : "manual",
|
|
452
|
+
path: object.target_path,
|
|
453
|
+
source_package: object.provider_package,
|
|
454
|
+
evidence_kind: "provider-content-digest",
|
|
455
|
+
expected_before_digest: object.observed_digest,
|
|
456
|
+
expected_after_digest: object.expected_applied_digest,
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
459
|
+
export function targetProviderObjectsFromTargetManifests(reconcilePlan, manifests) {
|
|
460
|
+
const declared = new Map();
|
|
461
|
+
for (const manifest of manifests) {
|
|
462
|
+
for (const object of manifest.objects) {
|
|
463
|
+
const id = object.kind === "directory-link"
|
|
464
|
+
? `skill:${object.runtime}:${object.name}`
|
|
465
|
+
: `manual:${object.runtime}`;
|
|
466
|
+
declared.set(id, object);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return reconcilePlan.objects.map((object) => {
|
|
470
|
+
const target = declared.get(object.object_id);
|
|
471
|
+
if (target === undefined)
|
|
472
|
+
throw new Error("update-target-provider-object-missing");
|
|
473
|
+
return {
|
|
474
|
+
object_id: object.object_id,
|
|
475
|
+
kind: object.kind,
|
|
476
|
+
provider_package: object.provider_package,
|
|
477
|
+
provider_version: object.provider_version,
|
|
478
|
+
target_path: object.target_path,
|
|
479
|
+
source_relative_path: target.kind === "directory-link" ? target.source_relative_path : null,
|
|
480
|
+
content: target.kind === "marker-file" ? target.content : null,
|
|
481
|
+
expected_digest: object.expected_digest,
|
|
482
|
+
expected_applied_digest: object.expected_applied_digest,
|
|
483
|
+
marker_begin: target.kind === "marker-file" ? target.marker_begin : null,
|
|
484
|
+
marker_end: target.kind === "marker-file" ? target.marker_end : null,
|
|
485
|
+
};
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
export async function prepareProductionUpdatePlan(currency) {
|
|
489
|
+
if (currency.latest === null || currency.integrity === null || currency.channel === null) {
|
|
490
|
+
throw new Error("update-plan-currency-incomplete");
|
|
491
|
+
}
|
|
492
|
+
const graph = await fetchExactPackageGraph({
|
|
493
|
+
registry: currency.registry,
|
|
494
|
+
version: currency.latest,
|
|
495
|
+
});
|
|
496
|
+
const meta = graph.find((entry) => entry.name === "@younndai/lyt");
|
|
497
|
+
if (meta?.integrity !== currency.integrity)
|
|
498
|
+
throw new Error("update-plan-currency-integrity-drift");
|
|
499
|
+
const inspected = await Promise.all(graph.map(async (fact) => {
|
|
500
|
+
const response = await fetch(fact.tarball_url);
|
|
501
|
+
if (!response.ok)
|
|
502
|
+
throw new Error(`update-target-fetch-${response.status}`);
|
|
503
|
+
return inspectTargetTarballBytes({
|
|
504
|
+
package: fact.name,
|
|
505
|
+
version: fact.version,
|
|
506
|
+
integrity: fact.integrity,
|
|
507
|
+
tarballUrl: fact.tarball_url,
|
|
508
|
+
bytes: new Uint8Array(await response.arrayBuffer()),
|
|
509
|
+
});
|
|
510
|
+
}));
|
|
511
|
+
const targetArtifacts = inspected.map((entry) => entry.artifact);
|
|
512
|
+
const manifests = inspected
|
|
513
|
+
.map((entry) => entry.provider_manifest)
|
|
514
|
+
.filter((entry) => entry !== null);
|
|
515
|
+
const extractedRoots = new Map(UPDATE_PLAN_PACKAGES.map((name) => [name, join(homedir(), "lyt", ".update-target", name)]));
|
|
516
|
+
const providers = materializeTargetProviders({
|
|
517
|
+
manifests,
|
|
518
|
+
extractedRoots,
|
|
519
|
+
homeDir: homedir(),
|
|
520
|
+
runtimes: detectInstalledRuntimes(homedir()),
|
|
521
|
+
});
|
|
522
|
+
const reconcilePlan = prepareInstallReconcilePlanV1(providers);
|
|
523
|
+
const targetProviderObjects = targetProviderObjectsFromTargetManifests(reconcilePlan, manifests);
|
|
524
|
+
const beforeState = observeInstalledState();
|
|
525
|
+
const packageDestinations = UPDATE_PLAN_PACKAGES.map((name) => {
|
|
526
|
+
const root = packageRoot(name);
|
|
527
|
+
const manifestPath = root === null ? `unresolved:${name}` : join(root, "package.json");
|
|
528
|
+
const before = root !== null && existsSync(manifestPath)
|
|
529
|
+
? createHash("sha256").update(readFileSync(manifestPath)).digest("hex")
|
|
530
|
+
: null;
|
|
531
|
+
const artifact = targetArtifacts.find((entry) => entry.package === name);
|
|
532
|
+
return {
|
|
533
|
+
object_id: `package:${name.slice("@younndai/".length)}`,
|
|
534
|
+
kind: "package",
|
|
535
|
+
path: root ?? manifestPath,
|
|
536
|
+
source_package: name,
|
|
537
|
+
evidence_kind: "registry-version-sri",
|
|
538
|
+
expected_before_digest: before,
|
|
539
|
+
expected_after_digest: artifact.payload_digest,
|
|
540
|
+
};
|
|
541
|
+
});
|
|
542
|
+
const binDestinations = beforeState.bins.map((bin, index) => ({
|
|
543
|
+
object_id: `bin:lyt:${index}`,
|
|
544
|
+
kind: "bin",
|
|
545
|
+
path: bin.path,
|
|
546
|
+
source_package: "@younndai/lyt",
|
|
547
|
+
evidence_kind: "bounded-target-identity",
|
|
548
|
+
expected_before_digest: bin.digest,
|
|
549
|
+
expected_after_digest: digestCanonical({ kind: "bin-target", path: resolve(bin.path) }),
|
|
550
|
+
}));
|
|
551
|
+
const providerDestinations = providerDestinationsFromReconcilePlan(reconcilePlan);
|
|
552
|
+
const destinations = [...packageDestinations, ...binDestinations, ...providerDestinations];
|
|
553
|
+
const npmObjectIds = [...packageDestinations, ...binDestinations].map((entry) => entry.object_id);
|
|
554
|
+
const skillObjectIds = providerDestinations
|
|
555
|
+
.filter((entry) => entry.kind === "skill")
|
|
556
|
+
.map((entry) => entry.object_id);
|
|
557
|
+
const manualObjectIds = providerDestinations
|
|
558
|
+
.filter((entry) => entry.kind === "manual")
|
|
559
|
+
.map((entry) => entry.object_id);
|
|
560
|
+
return prepareUpdatePlanV1({
|
|
561
|
+
registryUrl: currency.registry,
|
|
562
|
+
channel: currency.channel,
|
|
563
|
+
targetVersion: currency.latest,
|
|
564
|
+
distIntegrity: currency.integrity,
|
|
565
|
+
packageGraph: graph,
|
|
566
|
+
targetArtifacts,
|
|
567
|
+
targetProviderObjects,
|
|
568
|
+
beforeState,
|
|
569
|
+
destinations,
|
|
570
|
+
boundaries: [
|
|
571
|
+
{
|
|
572
|
+
boundary_id: "npm-self-replacement",
|
|
573
|
+
kind: "npm-self-replacement",
|
|
574
|
+
object_ids: npmObjectIds,
|
|
575
|
+
},
|
|
576
|
+
...(skillObjectIds.length > 0
|
|
577
|
+
? [
|
|
578
|
+
{
|
|
579
|
+
boundary_id: "skill-leaves",
|
|
580
|
+
kind: "skill-leaves",
|
|
581
|
+
object_ids: skillObjectIds,
|
|
582
|
+
},
|
|
583
|
+
]
|
|
584
|
+
: []),
|
|
585
|
+
...(manualObjectIds.length > 0
|
|
586
|
+
? [
|
|
587
|
+
{
|
|
588
|
+
boundary_id: "agent-manuals",
|
|
589
|
+
kind: "agent-manuals",
|
|
590
|
+
object_ids: manualObjectIds,
|
|
591
|
+
},
|
|
592
|
+
]
|
|
593
|
+
: []),
|
|
594
|
+
],
|
|
595
|
+
expectedEvidence: [
|
|
596
|
+
"registry-version-sri",
|
|
597
|
+
"package-graph",
|
|
598
|
+
"provider-content-digest",
|
|
599
|
+
"bin-target-identity",
|
|
600
|
+
"receipt-v1",
|
|
601
|
+
],
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
export async function revalidateProductionUpdatePlan(plan) {
|
|
605
|
+
try {
|
|
606
|
+
const parsed = parseUpdatePlanV1(plan);
|
|
607
|
+
const graph = await fetchExactPackageGraph({
|
|
608
|
+
registry: parsed.registry_url,
|
|
609
|
+
version: parsed.target_version,
|
|
610
|
+
});
|
|
611
|
+
if (canonicalJson(graph) !== canonicalJson(parsed.package_graph))
|
|
612
|
+
return false;
|
|
613
|
+
const observed = observeInstalledState();
|
|
614
|
+
if (canonicalJson(observed) !== canonicalJson(parsed.before_state))
|
|
615
|
+
return false;
|
|
616
|
+
const currentRoots = new Map(UPDATE_PLAN_PACKAGES.map((name) => [name, packageRoot(name) ?? "missing-target-package"]));
|
|
617
|
+
const currentProviders = providersFromUpdatePlan(parsed, currentRoots);
|
|
618
|
+
const currentProviderPlan = prepareInstallReconcilePlanV1(currentProviders);
|
|
619
|
+
const providerObserved = new Map(currentProviderPlan.objects.map((object) => [object.object_id, object.observed_digest]));
|
|
620
|
+
for (const destination of parsed.destinations) {
|
|
621
|
+
if (destination.expected_before_digest === null)
|
|
622
|
+
continue;
|
|
623
|
+
const digest = destination.evidence_kind === "registry-version-sri"
|
|
624
|
+
? existsSync(join(destination.path, "package.json"))
|
|
625
|
+
? createHash("sha256")
|
|
626
|
+
.update(readFileSync(join(destination.path, "package.json")))
|
|
627
|
+
.digest("hex")
|
|
628
|
+
: null
|
|
629
|
+
: destination.evidence_kind === "bounded-target-identity"
|
|
630
|
+
? existsSync(destination.path)
|
|
631
|
+
? createHash("sha256").update(readFileSync(destination.path)).digest("hex")
|
|
632
|
+
: null
|
|
633
|
+
: (providerObserved.get(destination.object_id) ?? null);
|
|
634
|
+
if (digest !== destination.expected_before_digest)
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
export async function verifyProductionInstalledPlan(plan) {
|
|
644
|
+
try {
|
|
645
|
+
const parsed = parseUpdatePlanV1(plan);
|
|
646
|
+
const installed = observeInstalledState();
|
|
647
|
+
const graph = await fetchExactPackageGraph({
|
|
648
|
+
registry: parsed.registry_url,
|
|
649
|
+
version: parsed.target_version,
|
|
650
|
+
});
|
|
651
|
+
if (!installedEvidenceMatchesPlan(parsed, installed, graph))
|
|
652
|
+
return false;
|
|
653
|
+
return parsed.target_artifacts.every((artifact) => {
|
|
654
|
+
const root = packageRoot(artifact.package);
|
|
655
|
+
return root !== null && verifyInstalledPayloadV1(root, artifact);
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
export function installedEvidenceMatchesPlan(plan, installed, graph) {
|
|
663
|
+
const versions = new Map(installed.packages.map((entry) => [entry.name, entry.version]));
|
|
664
|
+
return (versions.size === UPDATE_PLAN_PACKAGES.length &&
|
|
665
|
+
UPDATE_PLAN_PACKAGES.every((name) => versions.get(name) === plan.target_version) &&
|
|
666
|
+
canonicalJson(graph) === canonicalJson(plan.package_graph));
|
|
667
|
+
}
|
|
668
|
+
export function npmInstallArgsForUpdatePlan(plan, artifactPaths) {
|
|
669
|
+
const parsed = parseUpdatePlanV1(plan);
|
|
670
|
+
if (artifactPaths.length !== UPDATE_PLAN_PACKAGES.length) {
|
|
671
|
+
throw new Error("update-target-exact-seven-tarballs-required");
|
|
672
|
+
}
|
|
673
|
+
return Object.freeze(["i", "-g", ...artifactPaths, "--registry", parsed.registry_url]);
|
|
674
|
+
}
|
|
675
|
+
function runNpmInstall(plan, artifactPaths, quiet) {
|
|
676
|
+
const npmCli = resolveNpmCliPath();
|
|
677
|
+
if (npmCli === null)
|
|
678
|
+
return 1;
|
|
679
|
+
const res = spawnSync(process.execPath, [npmCli, ...npmInstallArgsForUpdatePlan(plan, artifactPaths)], {
|
|
680
|
+
stdio: quiet ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
681
|
+
shell: false,
|
|
682
|
+
});
|
|
683
|
+
return res.status ?? 1;
|
|
684
|
+
}
|
|
685
|
+
function runReplacementReconcile(plan) {
|
|
686
|
+
const cliPath = process.argv[1];
|
|
687
|
+
if (typeof cliPath !== "string" || cliPath.length === 0 || !existsSync(cliPath)) {
|
|
688
|
+
return {
|
|
689
|
+
exitCode: 1,
|
|
690
|
+
stdout: "",
|
|
691
|
+
stderr: "current Lyt CLI entrypoint is unavailable after npm replacement",
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const result = spawnSync(process.execPath, [
|
|
695
|
+
cliPath,
|
|
696
|
+
"install",
|
|
697
|
+
"reconcile",
|
|
698
|
+
"--apply",
|
|
699
|
+
"--json",
|
|
700
|
+
"--update-operation",
|
|
701
|
+
plan.operation_id,
|
|
702
|
+
"--plan-digest",
|
|
703
|
+
plan.plan_digest,
|
|
704
|
+
], {
|
|
705
|
+
encoding: "utf8",
|
|
706
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
707
|
+
shell: false,
|
|
708
|
+
});
|
|
709
|
+
return {
|
|
710
|
+
exitCode: result.status ?? 1,
|
|
711
|
+
stdout: result.stdout ?? "",
|
|
712
|
+
stderr: result.stderr ?? "",
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function printUpdateJson(result, currency) {
|
|
716
|
+
process.stdout.write(`${JSON.stringify({
|
|
717
|
+
schema_id: "lyt.update.v1",
|
|
718
|
+
schema_version: 1,
|
|
719
|
+
status: result.exitCode === 0 ? "completed" : "failed",
|
|
720
|
+
success: result.exitCode === 0,
|
|
721
|
+
npm_replacement_completed: result.installed,
|
|
722
|
+
reconciliation_completed: result.reconciled,
|
|
723
|
+
reconciliation_receipt: result.reconcileReceipt,
|
|
724
|
+
update_plan: result.updatePlan ?? null,
|
|
725
|
+
next_action: result.nextAction ?? null,
|
|
726
|
+
currency,
|
|
727
|
+
}, null, 2)}\n`);
|
|
728
|
+
}
|
|
729
|
+
async function selectChannelInteractively() {
|
|
730
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
731
|
+
try {
|
|
732
|
+
const answer = (await rl.question("Select Lyt update channel [alpha/latest] (default alpha): "))
|
|
733
|
+
.trim()
|
|
734
|
+
.toLowerCase();
|
|
735
|
+
if (answer.length === 0)
|
|
736
|
+
return "alpha";
|
|
737
|
+
return isUpdateChannel(answer) ? answer : null;
|
|
738
|
+
}
|
|
739
|
+
finally {
|
|
740
|
+
rl.close();
|
|
50
741
|
}
|
|
51
|
-
|
|
52
|
-
|
|
742
|
+
}
|
|
743
|
+
function productionUpdateEffects(json) {
|
|
744
|
+
return {
|
|
745
|
+
log: (message) => {
|
|
746
|
+
if (!json)
|
|
747
|
+
console.log(message);
|
|
748
|
+
},
|
|
749
|
+
error: (message) => {
|
|
750
|
+
if (!json)
|
|
751
|
+
console.error(message);
|
|
752
|
+
},
|
|
753
|
+
confirm: async (prompt) => {
|
|
754
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
755
|
+
try {
|
|
756
|
+
const answer = (await rl.question(prompt)).trim().toLowerCase();
|
|
757
|
+
return answer === "y" || answer === "yes";
|
|
758
|
+
}
|
|
759
|
+
finally {
|
|
760
|
+
rl.close();
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
preparePlan: prepareProductionUpdatePlan,
|
|
764
|
+
revalidatePlan: revalidateProductionUpdatePlan,
|
|
765
|
+
verifyInstalledPlan: verifyProductionInstalledPlan,
|
|
766
|
+
beginOperation: beginUpdateOperationV1,
|
|
767
|
+
stageArtifacts: (plan) => stageTargetArtifactsV1(plan, defaultUpdateOperationRoot()),
|
|
768
|
+
writeInstalledAnchor: (plan) => {
|
|
769
|
+
try {
|
|
770
|
+
writeInstalledStateAnchorV1(plan, defaultUpdateOperationRoot());
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
install: (plan, artifacts) => runNpmInstall(plan, artifacts, json),
|
|
778
|
+
reconcile: runReplacementReconcile,
|
|
779
|
+
};
|
|
53
780
|
}
|
|
54
781
|
export function buildUpdateCommand() {
|
|
55
782
|
return new Command("update")
|
|
56
|
-
.description(
|
|
57
|
-
.option("--check", "Only check for a newer version; do not install
|
|
783
|
+
.description("Update Lyt on one explicit alpha/latest channel, then launch the replacement binary's `lyt install reconcile --apply --json` boundary. Confirms before changing the global install.")
|
|
784
|
+
.option("--check", "Only check for a newer version; do not install")
|
|
58
785
|
.option("--yes", "Skip the confirmation prompt (required to update non-interactively)")
|
|
786
|
+
.option("--json", "Emit a deterministic update result")
|
|
787
|
+
.option("--channel <alpha|latest>", "Select and persist an update channel", parseChannel)
|
|
788
|
+
.option("--switch-channel", "Allow changing a previously selected channel (requires --channel)")
|
|
789
|
+
.option("--allow-downgrade", "Allow the explicit selected channel to replace an ahead install")
|
|
790
|
+
.option("--resume <operation-id>", "Resume one durable update operation after npm replacement")
|
|
791
|
+
.option("--configure", "Persist --channel without checking or installing")
|
|
59
792
|
.action(async (opts) => {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
793
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true && opts.json !== true;
|
|
794
|
+
const effects = productionUpdateEffects(opts.json === true);
|
|
795
|
+
if (opts.configure === true) {
|
|
796
|
+
if (opts.channel === undefined || opts.resume !== undefined || opts.check === true) {
|
|
797
|
+
const nextAction = "lyt update --channel <alpha|latest> --configure --json";
|
|
798
|
+
if (opts.json) {
|
|
799
|
+
process.stdout.write(`${JSON.stringify({
|
|
800
|
+
schema_id: "lyt.update-channel-configure-refusal",
|
|
801
|
+
schema_version: { major: 1, minor: 0 },
|
|
802
|
+
status: "refused",
|
|
803
|
+
error_code: "update-channel-configure-requires-channel-only",
|
|
804
|
+
next_action: nextAction,
|
|
805
|
+
})}\n`);
|
|
806
|
+
}
|
|
807
|
+
else
|
|
808
|
+
console.error(`lyt update: run \`${nextAction}\`.`);
|
|
809
|
+
process.exitCode = 2;
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const configured = configureUpdateChannel(opts.channel);
|
|
813
|
+
if (opts.json)
|
|
814
|
+
process.stdout.write(`${JSON.stringify(configured)}\n`);
|
|
815
|
+
else
|
|
816
|
+
console.log(`Lyt update channel saved: ${configured.channel}.`);
|
|
64
817
|
return;
|
|
65
818
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
819
|
+
if (opts.resume !== undefined) {
|
|
820
|
+
let result;
|
|
821
|
+
try {
|
|
822
|
+
result = await resumeUpdateFlow(opts.resume, effects);
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
effects.error(`lyt update: could not resume the sealed update operation (${String(error)}).`);
|
|
826
|
+
result = {
|
|
827
|
+
installed: false,
|
|
828
|
+
reconciled: false,
|
|
829
|
+
reconcileReceipt: null,
|
|
830
|
+
exitCode: 1,
|
|
831
|
+
nextAction: "lyt doctor --json",
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
if (opts.json)
|
|
835
|
+
printUpdateJson(result, null);
|
|
836
|
+
if (result.exitCode !== 0)
|
|
837
|
+
process.exitCode = result.exitCode;
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (opts.switchChannel === true && opts.channel === undefined) {
|
|
841
|
+
const result = {
|
|
842
|
+
installed: false,
|
|
843
|
+
reconciled: false,
|
|
844
|
+
reconcileReceipt: null,
|
|
845
|
+
exitCode: 2,
|
|
846
|
+
nextAction: "lyt update --channel alpha --switch-channel",
|
|
847
|
+
};
|
|
848
|
+
if (opts.json)
|
|
849
|
+
printUpdateJson(result, await checkCurrency({ installedVersion: readMetaVersion() }));
|
|
850
|
+
else
|
|
851
|
+
console.error("lyt update: --switch-channel requires --channel alpha|latest.");
|
|
852
|
+
process.exitCode = 2;
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if (opts.allowDowngrade === true && opts.channel === undefined) {
|
|
856
|
+
const result = {
|
|
857
|
+
installed: false,
|
|
858
|
+
reconciled: false,
|
|
859
|
+
reconcileReceipt: null,
|
|
860
|
+
exitCode: 2,
|
|
861
|
+
nextAction: "lyt update --channel alpha --allow-downgrade",
|
|
862
|
+
};
|
|
863
|
+
if (opts.json)
|
|
864
|
+
printUpdateJson(result, await checkCurrency({ installedVersion: readMetaVersion() }));
|
|
865
|
+
else
|
|
866
|
+
console.error("lyt update: --allow-downgrade requires --channel alpha|latest.");
|
|
867
|
+
process.exitCode = 2;
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const persisted = readUpdateChannel();
|
|
871
|
+
const initial = opts.check === true && opts.channel !== undefined
|
|
872
|
+
? { channel: opts.channel, source: "explicit" }
|
|
873
|
+
: resolveConfiguredChannel(opts.channel, persisted, {
|
|
874
|
+
switchChannel: opts.switchChannel,
|
|
875
|
+
interactive,
|
|
876
|
+
});
|
|
877
|
+
const resolved = initial.channel === null && initial.refusal === "channel-unconfigured" && interactive
|
|
878
|
+
? resolveConfiguredChannel(opts.channel, persisted, {
|
|
879
|
+
switchChannel: opts.switchChannel,
|
|
880
|
+
interactive,
|
|
881
|
+
selectedInteractive: await selectChannelInteractively(),
|
|
882
|
+
})
|
|
883
|
+
: initial;
|
|
884
|
+
if (resolved.channel === null) {
|
|
885
|
+
const currency = await checkCurrency({ installedVersion: readMetaVersion() });
|
|
886
|
+
const result = {
|
|
887
|
+
installed: false,
|
|
888
|
+
reconciled: false,
|
|
889
|
+
reconcileReceipt: null,
|
|
890
|
+
exitCode: 2,
|
|
891
|
+
nextAction: resolved.nextAction,
|
|
892
|
+
};
|
|
893
|
+
if (opts.json)
|
|
894
|
+
printUpdateJson(result, currency);
|
|
895
|
+
else
|
|
896
|
+
console.error(resolved.refusal === "channel-switch-requires-explicit"
|
|
897
|
+
? `lyt update: run \`${resolved.nextAction}\` to switch the saved channel.`
|
|
898
|
+
: formatCurrencyLine(currency));
|
|
899
|
+
process.exitCode = 2;
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (opts.check !== true &&
|
|
903
|
+
(resolved.source === "explicit" || resolved.source === "interactive")) {
|
|
904
|
+
writeUpdateChannel(resolved.channel);
|
|
905
|
+
}
|
|
906
|
+
const currency = await checkCurrency({
|
|
907
|
+
force: true,
|
|
908
|
+
channel: resolved.channel,
|
|
909
|
+
installedVersion: readMetaVersion(),
|
|
87
910
|
});
|
|
911
|
+
if (opts.check === true) {
|
|
912
|
+
if (opts.json)
|
|
913
|
+
process.stdout.write(`${JSON.stringify(currency, null, 2)}\n`);
|
|
914
|
+
else
|
|
915
|
+
console.log(formatCurrencyLine(currency));
|
|
916
|
+
process.exitCode = currency.stale ? 1 : currency.channelUnconfigured ? 2 : 0;
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const { exitCode, ...flowResult } = await runUpdateFlow(currency, { yes: opts.yes, interactive, allowDowngrade: opts.allowDowngrade }, effects);
|
|
920
|
+
const result = { ...flowResult, exitCode };
|
|
921
|
+
if (opts.json)
|
|
922
|
+
printUpdateJson(result, currency);
|
|
88
923
|
if (exitCode !== 0)
|
|
89
924
|
process.exitCode = exitCode;
|
|
90
925
|
});
|