owlrunkit 0.14.0 → 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.
@@ -8,13 +8,20 @@ import {
8
8
  mkdtemp,
9
9
  readFile,
10
10
  readdir,
11
+ realpath,
11
12
  rename,
12
13
  rm,
13
14
  writeFile,
14
15
  } from "node:fs/promises";
15
16
  import path from "node:path";
17
+ import { pathToFileURL } from "node:url";
16
18
 
17
- import { isDirectExecution } from "./core-contract.mjs";
19
+ import {
20
+ initializeProjectRunKit,
21
+ inspectProjectUpgradeSafety,
22
+ isDirectExecution,
23
+ validateProjectConfigV2,
24
+ } from "./core-contract.mjs";
18
25
 
19
26
  const MANIFEST_FILE = ".owlcoda-install-manifest.json";
20
27
 
@@ -22,6 +29,18 @@ function sha256(value) {
22
29
  return createHash("sha256").update(value).digest("hex");
23
30
  }
24
31
 
32
+ function sameCoreIdentity(left, right) {
33
+ return [
34
+ "contractVersion",
35
+ "coreVersion",
36
+ "coreManifestSha256",
37
+ "coreSourceRef",
38
+ ].every((field) => (
39
+ typeof left?.[field] === "string"
40
+ && left[field] === right?.[field]
41
+ ));
42
+ }
43
+
25
44
  async function exists(filePath) {
26
45
  try {
27
46
  await lstat(filePath);
@@ -32,6 +51,43 @@ async function exists(filePath) {
32
51
  }
33
52
  }
34
53
 
54
+ async function atomicWriteBytes(filePath, bytes) {
55
+ const temporaryPath = `${filePath}.tmp-${process.pid}`;
56
+ await rm(temporaryPath, { force: true });
57
+ await writeFile(temporaryPath, bytes, { flag: "wx" });
58
+ await rename(temporaryPath, filePath);
59
+ }
60
+
61
+ async function nextAvailablePath(parentRoot, baseName) {
62
+ for (let attempt = 1; ; attempt += 1) {
63
+ const suffix = attempt === 1
64
+ ? ""
65
+ : `-attempt-${String(attempt).padStart(3, "0")}`;
66
+ const candidate = path.join(parentRoot, `${baseName}${suffix}`);
67
+ if (!await exists(candidate)) return candidate;
68
+ }
69
+ }
70
+
71
+ async function writeAppendOnlyReceipt(receiptsRoot, baseName, receipt) {
72
+ await mkdir(receiptsRoot, { recursive: true });
73
+ for (let attempt = 1; ; attempt += 1) {
74
+ const suffix = attempt === 1
75
+ ? ""
76
+ : `-attempt-${String(attempt).padStart(3, "0")}`;
77
+ const receiptPath = path.join(receiptsRoot, `${baseName}${suffix}.json`);
78
+ try {
79
+ await writeFile(
80
+ receiptPath,
81
+ `${JSON.stringify(receipt, null, 2)}\n`,
82
+ { flag: "wx" },
83
+ );
84
+ return receiptPath;
85
+ } catch (error) {
86
+ if (error?.code !== "EEXIST") throw error;
87
+ }
88
+ }
89
+ }
90
+
35
91
  async function fileMap(root, { excludeManifest = true } = {}) {
36
92
  const files = {};
37
93
  async function walk(directory, prefix = "") {
@@ -137,11 +193,79 @@ export async function inspectInstalledSkill({ targetRoot } = {}) {
137
193
  };
138
194
  }
139
195
 
196
+ async function inspectFleetForSkillChange(workspaceRoots) {
197
+ if (!Array.isArray(workspaceRoots) || workspaceRoots.length === 0) {
198
+ throw new Error("skill_update_fleet_preflight_required");
199
+ }
200
+ const roots = [];
201
+ for (const workspaceRoot of workspaceRoots) {
202
+ if (typeof workspaceRoot !== "string" || workspaceRoot.length === 0) {
203
+ throw new Error("skill_update_fleet_workspace_invalid");
204
+ }
205
+ roots.push(await realpath(workspaceRoot));
206
+ }
207
+ const fleet = [...new Set(roots)].sort();
208
+ const projects = [];
209
+ for (const workspaceRoot of fleet) {
210
+ const safety = inspectProjectUpgradeSafety({ workspaceRoot });
211
+ if (safety.status !== "safe") {
212
+ const blockers = [
213
+ ...safety.activeRunIds,
214
+ ...safety.activeLeaseIds,
215
+ ...safety.issues,
216
+ ].join(",");
217
+ throw new Error(
218
+ `skill_update_blocked_active_execution:${workspaceRoot}:${blockers}`,
219
+ );
220
+ }
221
+ const configPath = path.join(
222
+ workspaceRoot,
223
+ ".owlcoda/runkit/config.json",
224
+ );
225
+ const configBytes = await readFile(configPath);
226
+ let config;
227
+ try {
228
+ config = JSON.parse(configBytes.toString("utf8"));
229
+ } catch {
230
+ throw new Error(`skill_update_fleet_config_invalid:${workspaceRoot}:invalid_json`);
231
+ }
232
+ const configV2 = config?.schemaVersion === "OwlCodaRunKitConfigV2"
233
+ ? validateProjectConfigV2(config)
234
+ : null;
235
+ const supportedV1 = config?.schemaVersion === "OwlCodaRunKitConfigV1"
236
+ && config?.core?.contractVersion === "0.1"
237
+ && config?.authorizationPolicy === "external_explicit_authority_required";
238
+ if (
239
+ (configV2 !== null && !configV2.valid)
240
+ || (configV2 === null && !supportedV1)
241
+ ) {
242
+ const issues = configV2?.issues?.join(",") ?? "unsupported_config";
243
+ throw new Error(
244
+ `skill_update_fleet_config_invalid:${workspaceRoot}:${issues}`,
245
+ );
246
+ }
247
+ projects.push({
248
+ workspaceRoot,
249
+ configPath,
250
+ configBytes,
251
+ configSha256: sha256(configBytes),
252
+ });
253
+ }
254
+ return projects;
255
+ }
256
+
257
+ async function restoreProjectConfigs(projects) {
258
+ for (const project of projects) {
259
+ await atomicWriteBytes(project.configPath, project.configBytes);
260
+ }
261
+ }
262
+
140
263
  export async function installCodexSkill({
141
264
  repositoryRoot,
142
265
  targetRoot,
143
266
  archiveRoot,
144
267
  expectedUnmanagedManifestSha256 = null,
268
+ workspaceRoots = null,
145
269
  } = {}) {
146
270
  if (typeof repositoryRoot !== "string" || typeof targetRoot !== "string" || typeof archiveRoot !== "string") {
147
271
  throw new Error("repositoryRoot, targetRoot, and archiveRoot are required");
@@ -176,35 +300,377 @@ export async function installCodexSkill({
176
300
  }
177
301
  return { status: "unchanged", manifestSha256: desired.manifestSha256, previousInstallation: null };
178
302
  }
179
-
303
+ const projects = await inspectFleetForSkillChange(workspaceRoots);
180
304
  await mkdir(archiveRoot, { recursive: true });
181
- const archivePath = path.join(archiveRoot, `owlcoda-runkit-${prior.manifestSha256.slice(7)}`);
182
- if (await exists(archivePath)) throw new Error(`archive_already_exists:${archivePath}`);
305
+ const archivePath = await nextAvailablePath(
306
+ archiveRoot,
307
+ `owlcoda-runkit-${prior.manifestSha256.slice(7)}`,
308
+ );
183
309
  await rename(targetRoot, archivePath);
184
310
  try {
185
311
  await rename(stagingRoot, targetRoot);
186
312
  stagingPresent = false;
313
+ const projectMigrations = [];
314
+ for (const project of projects) {
315
+ const initialized = await initializeProjectRunKit({
316
+ workspaceRoot: project.workspaceRoot,
317
+ });
318
+ if (initialized.exitCode !== 0) {
319
+ throw new Error(
320
+ `skill_update_project_migration_blocked:${project.workspaceRoot}:${initialized.status}`,
321
+ );
322
+ }
323
+ const currentConfigBytes = await readFile(project.configPath);
324
+ projectMigrations.push({
325
+ workspaceRoot: project.workspaceRoot,
326
+ configPath: project.configPath,
327
+ fromConfigSha256: project.configSha256,
328
+ fromConfigBase64: project.configBytes.toString("base64"),
329
+ toConfigSha256: sha256(currentConfigBytes),
330
+ migrationReceipt: initialized.migrationReceipt ?? null,
331
+ });
332
+ }
333
+ const receipt = {
334
+ schemaVersion: "OwlCodaRunKitSkillUpgradeReceiptV1",
335
+ operation: "upgrade",
336
+ targetRoot: path.resolve(targetRoot),
337
+ archiveRoot: path.resolve(archiveRoot),
338
+ fromManifestSha256: prior.manifestSha256,
339
+ toManifestSha256: desired.manifestSha256,
340
+ priorArchivePath: archivePath,
341
+ projects: projectMigrations,
342
+ authorizationGranted: false,
343
+ };
344
+ const upgradeReceipt = await writeAppendOnlyReceipt(
345
+ path.join(archiveRoot, "receipts"),
346
+ `skill-upgrade-${prior.manifestSha256.slice(7)}-to-${desired.manifestSha256.slice(7)}`,
347
+ receipt,
348
+ );
349
+ return {
350
+ status: "updated",
351
+ manifestSha256: desired.manifestSha256,
352
+ previousInstallation: {
353
+ manifestSha256: prior.manifestSha256,
354
+ archivePath,
355
+ },
356
+ upgradeReceipt,
357
+ projectMigrations,
358
+ };
187
359
  } catch (error) {
360
+ if (await exists(targetRoot)) {
361
+ const failedArchivePath = await nextAvailablePath(
362
+ archiveRoot,
363
+ `owlcoda-runkit-failed-${desired.manifestSha256.slice(7)}`,
364
+ );
365
+ await rename(targetRoot, failedArchivePath);
366
+ }
188
367
  await rename(archivePath, targetRoot);
368
+ await restoreProjectConfigs(projects);
189
369
  throw error;
190
370
  }
191
- return {
192
- status: "updated",
193
- manifestSha256: desired.manifestSha256,
194
- previousInstallation: { manifestSha256: prior.manifestSha256, archivePath },
195
- };
196
371
  } finally {
197
372
  if (stagingPresent) await rm(stagingRoot, { recursive: true, force: true });
198
373
  }
199
374
  }
200
375
 
376
+ export async function restoreCodexSkill({
377
+ targetRoot,
378
+ archiveRoot,
379
+ upgradeReceiptPath,
380
+ } = {}) {
381
+ if (
382
+ typeof targetRoot !== "string"
383
+ || typeof archiveRoot !== "string"
384
+ || typeof upgradeReceiptPath !== "string"
385
+ ) {
386
+ throw new Error("targetRoot, archiveRoot, and upgradeReceiptPath are required");
387
+ }
388
+ const resolvedTarget = path.resolve(targetRoot);
389
+ const resolvedArchive = path.resolve(archiveRoot);
390
+ const resolvedReceipt = path.resolve(upgradeReceiptPath);
391
+ const receiptRelative = path.relative(
392
+ path.join(resolvedArchive, "receipts"),
393
+ resolvedReceipt,
394
+ );
395
+ if (
396
+ receiptRelative.startsWith("..")
397
+ || path.isAbsolute(receiptRelative)
398
+ || !receiptRelative.endsWith(".json")
399
+ ) {
400
+ throw new Error("skill_rollback_receipt_outside_archive");
401
+ }
402
+ const receiptBytes = await readFile(resolvedReceipt);
403
+ const receipt = JSON.parse(receiptBytes.toString("utf8"));
404
+ if (
405
+ receipt.schemaVersion !== "OwlCodaRunKitSkillUpgradeReceiptV1"
406
+ || receipt.operation !== "upgrade"
407
+ || receipt.targetRoot !== resolvedTarget
408
+ || receipt.archiveRoot !== resolvedArchive
409
+ || receipt.authorizationGranted !== false
410
+ || !Array.isArray(receipt.projects)
411
+ || receipt.projects.length === 0
412
+ ) {
413
+ throw new Error("skill_rollback_receipt_invalid");
414
+ }
415
+ const resolvedPriorArchive = path.resolve(receipt.priorArchivePath);
416
+ const priorArchiveRelative = path.relative(
417
+ resolvedArchive,
418
+ resolvedPriorArchive,
419
+ );
420
+ if (
421
+ receipt.priorArchivePath !== resolvedPriorArchive
422
+ || priorArchiveRelative.startsWith("..")
423
+ || path.isAbsolute(priorArchiveRelative)
424
+ || priorArchiveRelative.length === 0
425
+ || priorArchiveRelative === "receipts"
426
+ || priorArchiveRelative.startsWith(`receipts${path.sep}`)
427
+ ) {
428
+ throw new Error("skill_rollback_prior_archive_outside_root");
429
+ }
430
+ const current = await inspectInstalledSkill({ targetRoot: resolvedTarget });
431
+ if (!current.valid || current.manifestSha256 !== receipt.toManifestSha256) {
432
+ throw new Error("skill_rollback_current_identity_mismatch");
433
+ }
434
+ const prior = await inspectInstalledSkill({
435
+ targetRoot: resolvedPriorArchive,
436
+ });
437
+ if (!prior.valid || prior.manifestSha256 !== receipt.fromManifestSha256) {
438
+ throw new Error("skill_rollback_prior_identity_mismatch");
439
+ }
440
+ const workspaceRoots = receipt.projects.map((project) => project.workspaceRoot);
441
+ const currentProjects = await inspectFleetForSkillChange(workspaceRoots);
442
+ const receiptProjectByRoot = new Map(
443
+ receipt.projects.map((project) => [project.workspaceRoot, project]),
444
+ );
445
+ for (const project of currentProjects) {
446
+ const expected = receiptProjectByRoot.get(project.workspaceRoot);
447
+ if (
448
+ !expected
449
+ || expected.configPath !== project.configPath
450
+ || expected.toConfigSha256 !== project.configSha256
451
+ || typeof expected.fromConfigBase64 !== "string"
452
+ || sha256(Buffer.from(expected.fromConfigBase64, "base64"))
453
+ !== expected.fromConfigSha256
454
+ ) {
455
+ throw new Error(
456
+ `skill_rollback_project_identity_mismatch:${project.workspaceRoot}`,
457
+ );
458
+ }
459
+ }
460
+ const currentArchivePath = await nextAvailablePath(
461
+ resolvedArchive,
462
+ `owlcoda-runkit-${current.manifestSha256.slice(7)}`,
463
+ );
464
+ await rename(resolvedTarget, currentArchivePath);
465
+ try {
466
+ await rename(resolvedPriorArchive, resolvedTarget);
467
+ for (const project of currentProjects) {
468
+ const expected = receiptProjectByRoot.get(project.workspaceRoot);
469
+ await atomicWriteBytes(
470
+ project.configPath,
471
+ Buffer.from(expected.fromConfigBase64, "base64"),
472
+ );
473
+ }
474
+ const rollbackReceipt = await writeAppendOnlyReceipt(
475
+ path.join(resolvedArchive, "receipts"),
476
+ `skill-rollback-${receipt.toManifestSha256.slice(7)}-to-${receipt.fromManifestSha256.slice(7)}`,
477
+ {
478
+ schemaVersion: "OwlCodaRunKitSkillRollbackReceiptV1",
479
+ operation: "rollback",
480
+ sourceUpgradeReceiptPath: resolvedReceipt,
481
+ sourceUpgradeReceiptSha256: sha256(receiptBytes),
482
+ targetRoot: resolvedTarget,
483
+ archiveRoot: resolvedArchive,
484
+ fromManifestSha256: receipt.toManifestSha256,
485
+ toManifestSha256: receipt.fromManifestSha256,
486
+ restoredArchivePath: resolvedPriorArchive,
487
+ replacedInstallationArchivePath: currentArchivePath,
488
+ projects: receipt.projects.map((project) => ({
489
+ workspaceRoot: project.workspaceRoot,
490
+ restoredConfigSha256: project.fromConfigSha256,
491
+ })),
492
+ authorizationGranted: false,
493
+ },
494
+ );
495
+ return {
496
+ status: "restored",
497
+ manifestSha256: receipt.fromManifestSha256,
498
+ rollbackReceipt,
499
+ replacedInstallation: {
500
+ manifestSha256: receipt.toManifestSha256,
501
+ archivePath: currentArchivePath,
502
+ },
503
+ authorizationGranted: false,
504
+ };
505
+ } catch (error) {
506
+ if (await exists(resolvedTarget)) {
507
+ await rename(resolvedTarget, resolvedPriorArchive);
508
+ }
509
+ await rename(currentArchivePath, resolvedTarget);
510
+ await restoreProjectConfigs(currentProjects);
511
+ throw error;
512
+ }
513
+ }
514
+
515
+ export async function recoverCodexSkillForActiveFleet({
516
+ targetRoot,
517
+ archiveRoot,
518
+ archivedInstallationPath,
519
+ workspaceRoots,
520
+ } = {}) {
521
+ if (
522
+ typeof targetRoot !== "string"
523
+ || typeof archiveRoot !== "string"
524
+ || typeof archivedInstallationPath !== "string"
525
+ || !Array.isArray(workspaceRoots)
526
+ || workspaceRoots.length === 0
527
+ ) {
528
+ throw new Error(
529
+ "targetRoot, archiveRoot, archivedInstallationPath, and workspaceRoots are required",
530
+ );
531
+ }
532
+ const resolvedTarget = path.resolve(targetRoot);
533
+ const resolvedArchive = path.resolve(archiveRoot);
534
+ const resolvedPrior = path.resolve(archivedInstallationPath);
535
+ const priorRelative = path.relative(resolvedArchive, resolvedPrior);
536
+ if (
537
+ priorRelative.startsWith("..")
538
+ || path.isAbsolute(priorRelative)
539
+ || priorRelative.length === 0
540
+ ) {
541
+ throw new Error("skill_recovery_archive_outside_root");
542
+ }
543
+ const current = await inspectInstalledSkill({ targetRoot: resolvedTarget });
544
+ const prior = await inspectInstalledSkill({ targetRoot: resolvedPrior });
545
+ if (!current.valid || !prior.valid) {
546
+ throw new Error("skill_recovery_installation_identity_invalid");
547
+ }
548
+ const archivedCoreModule = await import(pathToFileURL(path.join(
549
+ resolvedPrior,
550
+ "scripts/runkit-contract/core-contract.mjs",
551
+ )).href);
552
+ const archivedCore = archivedCoreModule.currentCoreIdentity?.();
553
+ if (!archivedCore || typeof archivedCore.coreManifestSha256 !== "string") {
554
+ throw new Error("skill_recovery_archived_core_identity_missing");
555
+ }
556
+ const fleetRoots = [];
557
+ for (const workspaceRoot of workspaceRoots) {
558
+ fleetRoots.push(await realpath(workspaceRoot));
559
+ }
560
+ const fleet = [...new Set(fleetRoots)].sort();
561
+ const projects = [];
562
+ let activeRunCount = 0;
563
+ for (const workspaceRoot of fleet) {
564
+ const configPath = path.join(
565
+ workspaceRoot,
566
+ ".owlcoda/runkit/config.json",
567
+ );
568
+ const configBytes = await readFile(configPath);
569
+ const config = JSON.parse(configBytes.toString("utf8"));
570
+ if (!sameCoreIdentity(config.core, archivedCore)) {
571
+ throw new Error(
572
+ `skill_recovery_config_core_mismatch:${workspaceRoot}`,
573
+ );
574
+ }
575
+ const safety = inspectProjectUpgradeSafety({ workspaceRoot });
576
+ if (safety.issues.length > 0) {
577
+ throw new Error(
578
+ `skill_recovery_control_state_invalid:${workspaceRoot}:${safety.issues.join(",")}`,
579
+ );
580
+ }
581
+ const activeRunSet = new Set(safety.activeRunIds);
582
+ if (safety.activeLeaseIds.some((leaseId) => (
583
+ !activeRunSet.has(leaseId.split(":", 1)[0])
584
+ ))) {
585
+ throw new Error(
586
+ `skill_recovery_lease_without_matching_active_run:${workspaceRoot}`,
587
+ );
588
+ }
589
+ for (const runId of safety.activeRunIds) {
590
+ const enginePinPath = path.join(
591
+ workspaceRoot,
592
+ ".owlcoda/runkit/executions",
593
+ runId,
594
+ "engine-pin.json",
595
+ );
596
+ const pinStat = await lstat(enginePinPath);
597
+ if (pinStat.isSymbolicLink() || !pinStat.isFile()) {
598
+ throw new Error(
599
+ `skill_recovery_engine_pin_invalid:${workspaceRoot}:${runId}`,
600
+ );
601
+ }
602
+ const enginePin = JSON.parse(await readFile(enginePinPath, "utf8"));
603
+ if (!sameCoreIdentity(enginePin, archivedCore)) {
604
+ throw new Error(
605
+ `skill_recovery_active_pin_mismatch:${workspaceRoot}:${runId}`,
606
+ );
607
+ }
608
+ activeRunCount += 1;
609
+ }
610
+ projects.push({
611
+ workspaceRoot,
612
+ configSha256: sha256(configBytes),
613
+ activeRunIds: safety.activeRunIds,
614
+ activeLeaseIds: safety.activeLeaseIds,
615
+ });
616
+ }
617
+ if (activeRunCount === 0) {
618
+ throw new Error("skill_recovery_active_execution_required");
619
+ }
620
+ const replacedInstallationArchivePath = await nextAvailablePath(
621
+ resolvedArchive,
622
+ `owlcoda-runkit-replaced-${current.manifestSha256.slice(7)}`,
623
+ );
624
+ await rename(resolvedTarget, replacedInstallationArchivePath);
625
+ try {
626
+ await rename(resolvedPrior, resolvedTarget);
627
+ const recoveryReceipt = await writeAppendOnlyReceipt(
628
+ path.join(resolvedArchive, "receipts"),
629
+ `skill-active-recovery-${current.manifestSha256.slice(7)}-to-${prior.manifestSha256.slice(7)}`,
630
+ {
631
+ schemaVersion: "OwlCodaRunKitSkillActiveRecoveryReceiptV1",
632
+ operation: "active_execution_recovery",
633
+ targetRoot: resolvedTarget,
634
+ archiveRoot: resolvedArchive,
635
+ fromManifestSha256: current.manifestSha256,
636
+ toManifestSha256: prior.manifestSha256,
637
+ restoredCore: archivedCore,
638
+ restoredArchivePath: resolvedPrior,
639
+ replacedInstallationArchivePath,
640
+ projects,
641
+ authorizationGranted: false,
642
+ },
643
+ );
644
+ return {
645
+ status: "recovered_active_fleet",
646
+ manifestSha256: prior.manifestSha256,
647
+ core: archivedCore,
648
+ recoveryReceipt,
649
+ replacedInstallationArchivePath,
650
+ authorizationGranted: false,
651
+ };
652
+ } catch (error) {
653
+ if (await exists(resolvedTarget)) {
654
+ await rename(resolvedTarget, resolvedPrior);
655
+ }
656
+ await rename(replacedInstallationArchivePath, resolvedTarget);
657
+ throw error;
658
+ }
659
+ }
660
+
201
661
  function parseOptions(values) {
202
662
  const options = {};
203
663
  for (let index = 0; index < values.length; index += 2) {
204
664
  const flag = values[index];
205
665
  const value = values[index + 1];
206
666
  if (!flag?.startsWith("--") || value === undefined) throw new Error("Options must be --name value pairs");
207
- options[flag.slice(2)] = value;
667
+ const name = flag.slice(2);
668
+ if (name === "workspace") {
669
+ options.workspace ??= [];
670
+ options.workspace.push(value);
671
+ } else {
672
+ options[name] = value;
673
+ }
208
674
  }
209
675
  return options;
210
676
  }
@@ -219,6 +685,7 @@ export async function runCli(argv = process.argv.slice(2)) {
219
685
  targetRoot: options.target,
220
686
  archiveRoot: options.archive,
221
687
  expectedUnmanagedManifestSha256: options["expected-unmanaged"] ?? null,
688
+ workspaceRoots: options.workspace ?? null,
222
689
  });
223
690
  return { exitCode: 0, ...output };
224
691
  }
@@ -226,8 +693,35 @@ export async function runCli(argv = process.argv.slice(2)) {
226
693
  const output = await inspectInstalledSkill({ targetRoot: options.target });
227
694
  return { exitCode: output.valid ? 0 : 2, ...output };
228
695
  }
229
- throw new Error("Usage: install-codex-skill.mjs <install|inspect> [options]");
696
+ if (command === "restore") {
697
+ const output = await restoreCodexSkill({
698
+ targetRoot: options.target,
699
+ archiveRoot: options.archive,
700
+ upgradeReceiptPath: options.receipt,
701
+ });
702
+ return { exitCode: 0, ...output };
703
+ }
704
+ if (command === "recover-active") {
705
+ const output = await recoverCodexSkillForActiveFleet({
706
+ targetRoot: options.target,
707
+ archiveRoot: options.archive,
708
+ archivedInstallationPath: options["archived-installation"],
709
+ workspaceRoots: options.workspace,
710
+ });
711
+ return { exitCode: 0, ...output };
712
+ }
713
+ throw new Error(
714
+ "Usage: install-codex-skill.mjs <install|inspect|restore|recover-active> [options]",
715
+ );
230
716
  } catch (error) {
717
+ const issue = error instanceof Error ? error.message : String(error);
718
+ if (issue.startsWith("skill_update_blocked_active_execution:")) {
719
+ return {
720
+ status: "skill_update_blocked_active_execution",
721
+ exitCode: 2,
722
+ issues: [issue],
723
+ };
724
+ }
231
725
  return { status: "invalid_input", exitCode: 3, issues: [error instanceof Error ? error.message : String(error)] };
232
726
  }
233
727
  }