@younndai/lyt 0.13.4 → 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.
Files changed (65) hide show
  1. package/README.md +27 -19
  2. package/dist/automator-run.d.ts +1 -0
  3. package/dist/automator-run.d.ts.map +1 -1
  4. package/dist/automator-run.js +5 -3
  5. package/dist/automator-run.js.map +1 -1
  6. package/dist/bench/pod-harness.d.ts.map +1 -1
  7. package/dist/bench/pod-harness.js +49 -1
  8. package/dist/bench/pod-harness.js.map +1 -1
  9. package/dist/build-program.d.ts +10 -1
  10. package/dist/build-program.d.ts.map +1 -1
  11. package/dist/build-program.js +23 -3
  12. package/dist/build-program.js.map +1 -1
  13. package/dist/cli.js +10 -4
  14. package/dist/cli.js.map +1 -1
  15. package/dist/commands/capture.d.ts.map +1 -1
  16. package/dist/commands/capture.js +1 -2
  17. package/dist/commands/capture.js.map +1 -1
  18. package/dist/commands/init.d.ts +2 -0
  19. package/dist/commands/init.d.ts.map +1 -1
  20. package/dist/commands/init.js +406 -224
  21. package/dist/commands/init.js.map +1 -1
  22. package/dist/commands/install.d.ts +4 -0
  23. package/dist/commands/install.d.ts.map +1 -0
  24. package/dist/commands/install.js +107 -0
  25. package/dist/commands/install.js.map +1 -0
  26. package/dist/commands/outdated.d.ts.map +1 -1
  27. package/dist/commands/outdated.js +19 -5
  28. package/dist/commands/outdated.js.map +1 -1
  29. package/dist/commands/update.d.ts +90 -2
  30. package/dist/commands/update.d.ts.map +1 -1
  31. package/dist/commands/update.js +883 -48
  32. package/dist/commands/update.js.map +1 -1
  33. package/dist/flows/init-bootstrap.d.ts +8 -1
  34. package/dist/flows/init-bootstrap.d.ts.map +1 -1
  35. package/dist/flows/init-bootstrap.js +244 -74
  36. package/dist/flows/init-bootstrap.js.map +1 -1
  37. package/dist/index.d.ts +6 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +3 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/install/doctor-health.d.ts +16 -0
  42. package/dist/install/doctor-health.d.ts.map +1 -0
  43. package/dist/install/doctor-health.js +575 -0
  44. package/dist/install/doctor-health.js.map +1 -0
  45. package/dist/install/provider-inventory.d.ts +40 -0
  46. package/dist/install/provider-inventory.d.ts.map +1 -0
  47. package/dist/install/provider-inventory.js +73 -0
  48. package/dist/install/provider-inventory.js.map +1 -0
  49. package/dist/install/reconcile-engine.d.ts +87 -0
  50. package/dist/install/reconcile-engine.d.ts.map +1 -0
  51. package/dist/install/reconcile-engine.js +771 -0
  52. package/dist/install/reconcile-engine.js.map +1 -0
  53. package/dist/install/target-artifacts.d.ts +91 -0
  54. package/dist/install/target-artifacts.d.ts.map +1 -0
  55. package/dist/install/target-artifacts.js +597 -0
  56. package/dist/install/target-artifacts.js.map +1 -0
  57. package/dist/install/update-operation.d.ts +47 -0
  58. package/dist/install/update-operation.d.ts.map +1 -0
  59. package/dist/install/update-operation.js +318 -0
  60. package/dist/install/update-operation.js.map +1 -0
  61. package/dist/install/update-plan.d.ts +132 -0
  62. package/dist/install/update-plan.d.ts.map +1 -0
  63. package/dist/install/update-plan.js +418 -0
  64. package/dist/install/update-plan.js.map +1 -0
  65. package/package.json +16 -6
@@ -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 { Command } from "commander";
19
- import { checkCurrency, CURRENCY_DIST_TAG, CURRENCY_PACKAGE, formatCurrencyLine, resolveUpdateAction, } from "@younndai/lyt-vault";
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, { yes: opts.yes, interactive: opts.interactive });
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 "offline":
147
+ case "channel-unconfigured":
24
148
  fx.error(action.message);
25
- return { installed: false, exitCode: 0 };
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
- fx.log(action.message);
28
- return { installed: false, exitCode: 0 };
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
- const ok = await fx.confirm(`${action.message} [y/N] `);
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 finishInstall(fx);
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 status = fx.install();
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: \`npm i -g ${CURRENCY_PACKAGE}@${CURRENCY_DIST_TAG}\` failed (exit ${status}).`);
49
- return { installed: false, exitCode: 1 };
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
- fx.log("lyt update: done. Run `lyt --version` to confirm.");
52
- return { installed: true, exitCode: 0 };
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(`Update Lyt to the latest published version (runs \`npm i -g ${CURRENCY_PACKAGE}@${CURRENCY_DIST_TAG}\`). Confirms before changing your global install.`)
57
- .option("--check", "Only check for a newer version; do not install (alias for `lyt outdated`)")
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 result = await checkCurrency({ force: true });
61
- if (opts.check === true) {
62
- console.log(formatCurrencyLine(result));
63
- process.exitCode = result.stale ? 1 : 0;
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
- const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
67
- const { exitCode } = await runUpdateFlow(result, { yes: opts.yes, interactive }, {
68
- log: (m) => console.log(m),
69
- error: (m) => console.error(m),
70
- confirm: async (prompt) => {
71
- const rl = createInterface({ input: process.stdin, output: process.stdout });
72
- try {
73
- const answer = (await rl.question(prompt)).trim().toLowerCase();
74
- return answer === "y" || answer === "yes";
75
- }
76
- finally {
77
- rl.close();
78
- }
79
- },
80
- install: () => {
81
- const res = spawnSync("npm", ["i", "-g", `${CURRENCY_PACKAGE}@${CURRENCY_DIST_TAG}`], {
82
- stdio: "inherit",
83
- shell: process.platform === "win32",
84
- });
85
- return res.status ?? 1;
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
  });