@pleri/olam-cli 0.1.11 → 0.1.13

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 (112) hide show
  1. package/dist/__tests__/auth-status.test.js +8 -7
  2. package/dist/__tests__/auth-status.test.js.map +1 -1
  3. package/dist/__tests__/help-output.test.d.ts +2 -0
  4. package/dist/__tests__/help-output.test.d.ts.map +1 -0
  5. package/dist/__tests__/help-output.test.js +74 -0
  6. package/dist/__tests__/help-output.test.js.map +1 -0
  7. package/dist/__tests__/image-presence.test.d.ts +2 -0
  8. package/dist/__tests__/image-presence.test.d.ts.map +1 -0
  9. package/dist/__tests__/image-presence.test.js +44 -0
  10. package/dist/__tests__/image-presence.test.js.map +1 -0
  11. package/dist/__tests__/protocol-version.test.d.ts +2 -0
  12. package/dist/__tests__/protocol-version.test.d.ts.map +1 -0
  13. package/dist/__tests__/protocol-version.test.js +170 -0
  14. package/dist/__tests__/protocol-version.test.js.map +1 -0
  15. package/dist/__tests__/registry-allowlist.test.d.ts +2 -0
  16. package/dist/__tests__/registry-allowlist.test.d.ts.map +1 -0
  17. package/dist/__tests__/registry-allowlist.test.js +129 -0
  18. package/dist/__tests__/registry-allowlist.test.js.map +1 -0
  19. package/dist/commands/__tests__/crystallize.test.d.ts +2 -0
  20. package/dist/commands/__tests__/crystallize.test.d.ts.map +1 -0
  21. package/dist/commands/__tests__/crystallize.test.js +133 -0
  22. package/dist/commands/__tests__/crystallize.test.js.map +1 -0
  23. package/dist/commands/__tests__/upgrade.all-three.test.d.ts +19 -0
  24. package/dist/commands/__tests__/upgrade.all-three.test.d.ts.map +1 -0
  25. package/dist/commands/__tests__/upgrade.all-three.test.js +92 -0
  26. package/dist/commands/__tests__/upgrade.all-three.test.js.map +1 -0
  27. package/dist/commands/__tests__/upgrade.history.test.d.ts +15 -0
  28. package/dist/commands/__tests__/upgrade.history.test.d.ts.map +1 -0
  29. package/dist/commands/__tests__/upgrade.history.test.js +199 -0
  30. package/dist/commands/__tests__/upgrade.history.test.js.map +1 -0
  31. package/dist/commands/__tests__/upgrade.lock.test.d.ts +15 -0
  32. package/dist/commands/__tests__/upgrade.lock.test.d.ts.map +1 -0
  33. package/dist/commands/__tests__/upgrade.lock.test.js +253 -0
  34. package/dist/commands/__tests__/upgrade.lock.test.js.map +1 -0
  35. package/dist/commands/__tests__/upgrade.olam-tag.test.d.ts +21 -0
  36. package/dist/commands/__tests__/upgrade.olam-tag.test.d.ts.map +1 -0
  37. package/dist/commands/__tests__/upgrade.olam-tag.test.js +127 -0
  38. package/dist/commands/__tests__/upgrade.olam-tag.test.js.map +1 -0
  39. package/dist/commands/__tests__/upgrade.poll.test.d.ts +14 -0
  40. package/dist/commands/__tests__/upgrade.poll.test.d.ts.map +1 -0
  41. package/dist/commands/__tests__/upgrade.poll.test.js +136 -0
  42. package/dist/commands/__tests__/upgrade.poll.test.js.map +1 -0
  43. package/dist/commands/__tests__/upgrade.recreate.test.d.ts +17 -0
  44. package/dist/commands/__tests__/upgrade.recreate.test.d.ts.map +1 -0
  45. package/dist/commands/__tests__/upgrade.recreate.test.js +95 -0
  46. package/dist/commands/__tests__/upgrade.recreate.test.js.map +1 -0
  47. package/dist/commands/__tests__/upgrade.rollback.test.d.ts +12 -0
  48. package/dist/commands/__tests__/upgrade.rollback.test.d.ts.map +1 -0
  49. package/dist/commands/__tests__/upgrade.rollback.test.js +275 -0
  50. package/dist/commands/__tests__/upgrade.rollback.test.js.map +1 -0
  51. package/dist/commands/__tests__/upgrade.sha-capture.test.d.ts +12 -0
  52. package/dist/commands/__tests__/upgrade.sha-capture.test.d.ts.map +1 -0
  53. package/dist/commands/__tests__/upgrade.sha-capture.test.js +63 -0
  54. package/dist/commands/__tests__/upgrade.sha-capture.test.js.map +1 -0
  55. package/dist/commands/__tests__/upgrade.smoke.test.d.ts +19 -0
  56. package/dist/commands/__tests__/upgrade.smoke.test.d.ts.map +1 -0
  57. package/dist/commands/__tests__/upgrade.smoke.test.js +101 -0
  58. package/dist/commands/__tests__/upgrade.smoke.test.js.map +1 -0
  59. package/dist/commands/__tests__/upgrade.swap.test.d.ts +19 -0
  60. package/dist/commands/__tests__/upgrade.swap.test.d.ts.map +1 -0
  61. package/dist/commands/__tests__/upgrade.swap.test.js +333 -0
  62. package/dist/commands/__tests__/upgrade.swap.test.js.map +1 -0
  63. package/dist/commands/auth-status.d.ts +8 -1
  64. package/dist/commands/auth-status.d.ts.map +1 -1
  65. package/dist/commands/auth-status.js +2 -1
  66. package/dist/commands/auth-status.js.map +1 -1
  67. package/dist/commands/create.d.ts.map +1 -1
  68. package/dist/commands/create.js +31 -0
  69. package/dist/commands/create.js.map +1 -1
  70. package/dist/commands/crystallize.d.ts +11 -1
  71. package/dist/commands/crystallize.d.ts.map +1 -1
  72. package/dist/commands/crystallize.js +32 -8
  73. package/dist/commands/crystallize.js.map +1 -1
  74. package/dist/commands/upgrade-history.d.ts +17 -0
  75. package/dist/commands/upgrade-history.d.ts.map +1 -0
  76. package/dist/commands/upgrade-history.js +40 -0
  77. package/dist/commands/upgrade-history.js.map +1 -0
  78. package/dist/commands/upgrade-lock.d.ts +102 -0
  79. package/dist/commands/upgrade-lock.d.ts.map +1 -0
  80. package/dist/commands/upgrade-lock.js +225 -0
  81. package/dist/commands/upgrade-lock.js.map +1 -0
  82. package/dist/commands/upgrade-log.d.ts +86 -0
  83. package/dist/commands/upgrade-log.d.ts.map +1 -0
  84. package/dist/commands/upgrade-log.js +146 -0
  85. package/dist/commands/upgrade-log.js.map +1 -0
  86. package/dist/commands/upgrade.d.ts +265 -0
  87. package/dist/commands/upgrade.d.ts.map +1 -1
  88. package/dist/commands/upgrade.js +840 -10
  89. package/dist/commands/upgrade.js.map +1 -1
  90. package/dist/exit-codes.d.ts +35 -0
  91. package/dist/exit-codes.d.ts.map +1 -0
  92. package/dist/exit-codes.js +35 -0
  93. package/dist/exit-codes.js.map +1 -0
  94. package/dist/image-presence.d.ts +40 -0
  95. package/dist/image-presence.d.ts.map +1 -0
  96. package/dist/image-presence.js +39 -0
  97. package/dist/image-presence.js.map +1 -0
  98. package/dist/index.js +1058 -168
  99. package/dist/index.js.map +1 -1
  100. package/dist/pleri-config.d.ts +22 -0
  101. package/dist/pleri-config.d.ts.map +1 -0
  102. package/dist/pleri-config.js +42 -0
  103. package/dist/pleri-config.js.map +1 -0
  104. package/dist/protocol-version.d.ts +79 -0
  105. package/dist/protocol-version.d.ts.map +1 -0
  106. package/dist/protocol-version.js +133 -0
  107. package/dist/protocol-version.js.map +1 -0
  108. package/dist/registry-allowlist.d.ts +47 -0
  109. package/dist/registry-allowlist.d.ts.map +1 -0
  110. package/dist/registry-allowlist.js +67 -0
  111. package/dist/registry-allowlist.js.map +1 -0
  112. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -421,8 +421,8 @@ var init_parseUtil = __esm({
421
421
  init_errors();
422
422
  init_en();
423
423
  makeIssue = (params) => {
424
- const { data, path: path32, errorMaps, issueData } = params;
425
- const fullPath = [...path32, ...issueData.path || []];
424
+ const { data, path: path35, errorMaps, issueData } = params;
425
+ const fullPath = [...path35, ...issueData.path || []];
426
426
  const fullIssue = {
427
427
  ...issueData,
428
428
  path: fullPath
@@ -730,11 +730,11 @@ var init_types = __esm({
730
730
  init_parseUtil();
731
731
  init_util();
732
732
  ParseInputLazyPath = class {
733
- constructor(parent, value, path32, key) {
733
+ constructor(parent, value, path35, key) {
734
734
  this._cachedPath = [];
735
735
  this.parent = parent;
736
736
  this.data = value;
737
- this._path = path32;
737
+ this._path = path35;
738
738
  this._key = key;
739
739
  }
740
740
  get path() {
@@ -4221,7 +4221,7 @@ import YAML from "yaml";
4221
4221
  function bootstrapStepCmd(entry) {
4222
4222
  return typeof entry === "string" ? entry : entry.cmd;
4223
4223
  }
4224
- function refineForbiddenKeys(value, path32, ctx, rejectSource) {
4224
+ function refineForbiddenKeys(value, path35, ctx, rejectSource) {
4225
4225
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
4226
4226
  return;
4227
4227
  }
@@ -4229,12 +4229,12 @@ function refineForbiddenKeys(value, path32, ctx, rejectSource) {
4229
4229
  if (FORBIDDEN_KEYS.has(key)) {
4230
4230
  ctx.addIssue({
4231
4231
  code: external_exports.ZodIssueCode.custom,
4232
- path: [...path32, key],
4232
+ path: [...path35, key],
4233
4233
  message: `forbidden key "${key}" (prototype-pollution surface)`
4234
4234
  });
4235
4235
  continue;
4236
4236
  }
4237
- if (rejectSource && path32.length === 0 && key === "source") {
4237
+ if (rejectSource && path35.length === 0 && key === "source") {
4238
4238
  ctx.addIssue({
4239
4239
  code: external_exports.ZodIssueCode.custom,
4240
4240
  path: ["source"],
@@ -4244,30 +4244,30 @@ function refineForbiddenKeys(value, path32, ctx, rejectSource) {
4244
4244
  }
4245
4245
  refineForbiddenKeys(
4246
4246
  value[key],
4247
- [...path32, key],
4247
+ [...path35, key],
4248
4248
  ctx,
4249
4249
  false
4250
4250
  );
4251
4251
  }
4252
4252
  }
4253
- function rejectForbiddenKeys(value, path32, rejectSource) {
4253
+ function rejectForbiddenKeys(value, path35, rejectSource) {
4254
4254
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
4255
4255
  return;
4256
4256
  }
4257
4257
  for (const key of Object.keys(value)) {
4258
4258
  if (FORBIDDEN_KEYS.has(key)) {
4259
4259
  throw new Error(
4260
- `[manifest] ${path32}: forbidden key "${key}" (prototype-pollution surface)`
4260
+ `[manifest] ${path35}: forbidden key "${key}" (prototype-pollution surface)`
4261
4261
  );
4262
4262
  }
4263
4263
  if (rejectSource && key === "source") {
4264
4264
  throw new Error(
4265
- `[manifest] ${path32}: top-level "source" is loader-stamped \u2014 manifests must not author it`
4265
+ `[manifest] ${path35}: top-level "source" is loader-stamped \u2014 manifests must not author it`
4266
4266
  );
4267
4267
  }
4268
4268
  rejectForbiddenKeys(
4269
4269
  value[key],
4270
- `${path32}.${key}`,
4270
+ `${path35}.${key}`,
4271
4271
  false
4272
4272
  );
4273
4273
  }
@@ -5208,8 +5208,8 @@ var init_client = __esm({
5208
5208
  throw new Error(`failed to report rate-limit for ${accountId} (HTTP ${res.status})`);
5209
5209
  }
5210
5210
  }
5211
- async request(method, path32, body, attempt = 0) {
5212
- const url = `${this.baseUrl}${path32}`;
5211
+ async request(method, path35, body, attempt = 0) {
5212
+ const url = `${this.baseUrl}${path35}`;
5213
5213
  const controller = new AbortController();
5214
5214
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
5215
5215
  const headers = {};
@@ -5225,7 +5225,7 @@ var init_client = __esm({
5225
5225
  } catch (err) {
5226
5226
  if (attempt < RETRY_COUNT && isTransient(err)) {
5227
5227
  await sleep(RETRY_BACKOFF_MS * (attempt + 1));
5228
- return this.request(method, path32, body, attempt + 1);
5228
+ return this.request(method, path35, body, attempt + 1);
5229
5229
  }
5230
5230
  throw err;
5231
5231
  } finally {
@@ -6676,8 +6676,8 @@ var init_provider3 = __esm({
6676
6676
  // -----------------------------------------------------------------------
6677
6677
  // Internal fetch helper
6678
6678
  // -----------------------------------------------------------------------
6679
- async request(path32, method, body) {
6680
- const url = `${this.config.workerUrl}${path32}`;
6679
+ async request(path35, method, body) {
6680
+ const url = `${this.config.workerUrl}${path35}`;
6681
6681
  const bearer = await this.config.mintToken();
6682
6682
  const headers = {
6683
6683
  Authorization: `Bearer ${bearer}`
@@ -7790,8 +7790,8 @@ import { execFileSync as execFileSync3 } from "node:child_process";
7790
7790
  import * as fs13 from "node:fs";
7791
7791
  import * as os9 from "node:os";
7792
7792
  import * as path14 from "node:path";
7793
- function expandHome(p, homedir15) {
7794
- return p.replace(/^~(?=$|\/|\\)/, homedir15());
7793
+ function expandHome(p, homedir17) {
7794
+ return p.replace(/^~(?=$|\/|\\)/, homedir17());
7795
7795
  }
7796
7796
  function sanitizeRepoFilename(name) {
7797
7797
  const sanitized = name.replace(/[^A-Za-z0-9._-]/g, "_");
@@ -7812,7 +7812,7 @@ ${stderr}`;
7812
7812
  }
7813
7813
  function snapshotBaselineDiff(repos, workspacePath, deps = {}) {
7814
7814
  const exec = deps.exec ?? ((cmd, args, opts) => execFileSync3(cmd, args, opts));
7815
- const homedir15 = deps.homedir ?? (() => os9.homedir());
7815
+ const homedir17 = deps.homedir ?? (() => os9.homedir());
7816
7816
  const baselineDir = path14.join(workspacePath, ".olam", "baseline");
7817
7817
  try {
7818
7818
  fs13.mkdirSync(baselineDir, { recursive: true });
@@ -7827,7 +7827,7 @@ function snapshotBaselineDiff(repos, workspacePath, deps = {}) {
7827
7827
  if (!repo.path) continue;
7828
7828
  const filename = `${sanitizeRepoFilename(repo.name)}.diff`;
7829
7829
  const outPath = path14.join(baselineDir, filename);
7830
- const repoPath = expandHome(repo.path, homedir15);
7830
+ const repoPath = expandHome(repo.path, homedir17);
7831
7831
  if (!fs13.existsSync(repoPath)) {
7832
7832
  writeBaselineFile(outPath, `# repo: ${repo.name}
7833
7833
  # (skipped: path ${repoPath} does not exist)
@@ -11780,6 +11780,61 @@ var init_context = __esm({
11780
11780
  }
11781
11781
  });
11782
11782
 
11783
+ // src/registry-allowlist.ts
11784
+ var registry_allowlist_exports = {};
11785
+ __export(registry_allowlist_exports, {
11786
+ decideAllowlist: () => decideAllowlist,
11787
+ resolveDevboxImageOverride: () => resolveDevboxImageOverride
11788
+ });
11789
+ function decideAllowlist(input) {
11790
+ const { imageRef, allowCustomRegistry } = input;
11791
+ const allowedByDefault = DEFAULT_ALLOWLIST_PATTERNS.some((re) => re.test(imageRef));
11792
+ if (allowedByDefault) {
11793
+ return {
11794
+ imageRef,
11795
+ allowedByDefault: true,
11796
+ accepted: true,
11797
+ stderrLine: ""
11798
+ };
11799
+ }
11800
+ if (allowCustomRegistry) {
11801
+ return {
11802
+ imageRef,
11803
+ allowedByDefault: false,
11804
+ accepted: true,
11805
+ stderrLine: `Warning: using custom devbox image '${imageRef}'. (--allow-custom-registry was specified.) Verify the source and digest before proceeding.`
11806
+ };
11807
+ }
11808
+ return {
11809
+ imageRef,
11810
+ allowedByDefault: false,
11811
+ accepted: false,
11812
+ stderrLine: `Error: image '${imageRef}' is outside allowed registries (ghcr.io/pleri/*).
11813
+ To override: re-run with --allow-custom-registry
11814
+ Verify the source and digest before doing so.`
11815
+ };
11816
+ }
11817
+ function resolveDevboxImageOverride(flagValue, env = process.env) {
11818
+ if (flagValue && flagValue.trim().length > 0) {
11819
+ return flagValue.trim();
11820
+ }
11821
+ const envValue = env.OLAM_DEVBOX_IMAGE;
11822
+ if (envValue && envValue.trim().length > 0) {
11823
+ return envValue.trim();
11824
+ }
11825
+ return void 0;
11826
+ }
11827
+ var DEFAULT_ALLOWLIST_PATTERNS;
11828
+ var init_registry_allowlist = __esm({
11829
+ "src/registry-allowlist.ts"() {
11830
+ "use strict";
11831
+ DEFAULT_ALLOWLIST_PATTERNS = [
11832
+ // ghcr.io/pleri/<anything>:<tag> or ghcr.io/pleri/<anything>@sha256:<digest>
11833
+ /^ghcr\.io\/pleri\/[^/\s]+(?::[^\s]+|@sha256:[a-f0-9]+)?$/
11834
+ ];
11835
+ }
11836
+ });
11837
+
11783
11838
  // ../core/src/orchestrator/enter.ts
11784
11839
  var enter_exports = {};
11785
11840
  __export(enter_exports, {
@@ -12249,11 +12304,11 @@ var UnknownArchetypeError = class extends Error {
12249
12304
  known;
12250
12305
  };
12251
12306
  var ArchetypeCycleError = class extends Error {
12252
- constructor(path32) {
12307
+ constructor(path35) {
12253
12308
  super(
12254
- `Archetype inheritance cycle detected: ${path32.join(" \u2192 ")} \u2192 ${path32[0] ?? "?"}`
12309
+ `Archetype inheritance cycle detected: ${path35.join(" \u2192 ")} \u2192 ${path35[0] ?? "?"}`
12255
12310
  );
12256
- this.path = path32;
12311
+ this.path = path35;
12257
12312
  this.name = "ArchetypeCycleError";
12258
12313
  }
12259
12314
  path;
@@ -12496,6 +12551,13 @@ function nextCooldownReset(accounts, now = Date.now()) {
12496
12551
 
12497
12552
  // src/commands/auth-status.ts
12498
12553
  init_auth();
12554
+
12555
+ // src/exit-codes.ts
12556
+ var EXIT_GENERIC_ERROR = 1;
12557
+ var EXIT_PLERI_NOT_CONFIGURED = 2;
12558
+ var EXIT_AUTH_NEEDS_ATTENTION = 5;
12559
+
12560
+ // src/commands/auth-status.ts
12499
12561
  var LOCAL_DATA_DIR = path9.join(os5.homedir(), ".olam", "auth-data");
12500
12562
  function localHHMM(isoStr) {
12501
12563
  const d = new Date(isoStr);
@@ -12572,7 +12634,7 @@ function formatAuthStatus(accounts, now = Date.now()) {
12572
12634
  lines.push(
12573
12635
  resetIso ? pc5.yellow(`Next reset: ${localHHMM(resetIso)}`) : pc5.dim("No reset scheduled")
12574
12636
  );
12575
- return { output: lines.join("\n"), exitCode: 2 };
12637
+ return { output: lines.join("\n"), exitCode: EXIT_AUTH_NEEDS_ATTENTION };
12576
12638
  }
12577
12639
  return { output: lines.join("\n"), exitCode: 0 };
12578
12640
  }
@@ -13065,10 +13127,10 @@ async function readHostCpToken2() {
13065
13127
  if (!fs19.existsSync(tp)) return null;
13066
13128
  return fs19.readFileSync(tp, "utf-8").trim();
13067
13129
  }
13068
- async function callHostCpProxy(method, worldId, path32, body) {
13130
+ async function callHostCpProxy(method, worldId, path35, body) {
13069
13131
  const token = await readHostCpToken2();
13070
13132
  if (!token) return { ok: false, status: 0, error: "no token (host CP not started)" };
13071
- const url = `http://127.0.0.1:${HOST_CP_PORT}/api/world/${encodeURIComponent(worldId)}${path32}`;
13133
+ const url = `http://127.0.0.1:${HOST_CP_PORT}/api/world/${encodeURIComponent(worldId)}${path35}`;
13072
13134
  try {
13073
13135
  const headers = {
13074
13136
  Authorization: `Bearer ${token}`
@@ -13679,9 +13741,9 @@ function formatFreshnessWarning(result, image = DEFAULT_DEVBOX_IMAGE) {
13679
13741
  "These source files have changed since the image was built; the",
13680
13742
  "changes will NOT take effect in fresh worlds until you rebuild:"
13681
13743
  ];
13682
- for (const { path: path32, mtimeMs } of result.newerSources) {
13744
+ for (const { path: path35, mtimeMs } of result.newerSources) {
13683
13745
  const when = new Date(mtimeMs).toISOString();
13684
- lines.push(` \u2022 ${path32} (modified ${when})`);
13746
+ lines.push(` \u2022 ${path35} (modified ${when})`);
13685
13747
  }
13686
13748
  lines.push("");
13687
13749
  lines.push("Rebuild with:");
@@ -13841,15 +13903,15 @@ init_context();
13841
13903
  var HOST_CP_URL = "http://127.0.0.1:19000";
13842
13904
  async function readHostCpTokenForCreate() {
13843
13905
  try {
13844
- const { default: fs28 } = await import("node:fs");
13845
- const { default: os16 } = await import("node:os");
13846
- const { default: path32 } = await import("node:path");
13847
- const tp = path32.join(
13848
- process.env.OLAM_HOME ?? path32.join(os16.homedir(), ".olam"),
13906
+ const { default: fs31 } = await import("node:fs");
13907
+ const { default: os18 } = await import("node:os");
13908
+ const { default: path35 } = await import("node:path");
13909
+ const tp = path35.join(
13910
+ process.env.OLAM_HOME ?? path35.join(os18.homedir(), ".olam"),
13849
13911
  "host-cp.token"
13850
13912
  );
13851
- if (!fs28.existsSync(tp)) return null;
13852
- return fs28.readFileSync(tp, "utf-8").trim();
13913
+ if (!fs31.existsSync(tp)) return null;
13914
+ return fs31.readFileSync(tp, "utf-8").trim();
13853
13915
  } catch {
13854
13916
  return null;
13855
13917
  }
@@ -13858,7 +13920,23 @@ function registerCreate(program2) {
13858
13920
  program2.command("create").description("Create a new development world").option("--name <name>", "World name (required unless --from-prompt is set; auto-derived in that case)").option("--repos <repos...>", "Repos to include (names from .olam/config.yaml; wins over --workspace)").option("--workspace <name>", "Named workspace from the host catalog (~/.olam/workspaces/<name>.yaml)").option("--task <task>", "Initial task to dispatch").option("--branch <branch>", "Override default branch name").option("--plan <file>", "Path to a plan file to inject").option("--no-auth", "Skip auto-injecting host credentials").option("--no-host-cp", 'Suppress the host CP "you might want to start it" hint').option("--auto-codex-review", "Spawn a parallel codex-review lane that critiques main as it works").option("--no-open", "Suppress auto-opening the Host CP UI in the browser on success").option("--rebuild-base", "Rebuild olam-devbox:latest before creating (slow)").option("--no-freshness-check", "Skip the devbox image freshness check").option("--from-prompt <prompt>", "NL prompt \u2192 infer workspace + dispatch (CLI parity for olam_create_from_prompt MCP tool)").option("--keep-after-merge", "Disable auto-destroy when the world's PR merges (useful for inspection/debugging)").option("--carry-uncommitted", "Preserve operator's uncommitted edits in the world's worktree").option(
13859
13921
  "--allow-bootstrap-failure",
13860
13922
  "Treat bootstrap step failures as warnings instead of destroying the world (dogfood escape hatch for cross-repo seed coupling)"
13861
- ).action(async (opts) => {
13923
+ ).option("--devbox-image <ref>", "Override the default devbox image (full registry/name:tag or @sha256: ref)").option("--allow-custom-registry", "Allow --devbox-image refs outside ghcr.io/pleri/* (logs a warning)").action(async (opts) => {
13924
+ const { resolveDevboxImageOverride: resolveDevboxImageOverride2, decideAllowlist: decideAllowlist2 } = await Promise.resolve().then(() => (init_registry_allowlist(), registry_allowlist_exports));
13925
+ const overrideRef = resolveDevboxImageOverride2(opts.devboxImage);
13926
+ if (overrideRef) {
13927
+ const decision = decideAllowlist2({
13928
+ imageRef: overrideRef,
13929
+ allowCustomRegistry: opts.allowCustomRegistry === true
13930
+ });
13931
+ if (!decision.accepted) {
13932
+ process.stderr.write(decision.stderrLine + "\n");
13933
+ process.exitCode = 1;
13934
+ return;
13935
+ }
13936
+ if (decision.stderrLine) {
13937
+ process.stderr.write(decision.stderrLine + "\n");
13938
+ }
13939
+ }
13862
13940
  let resolvedName = opts.name;
13863
13941
  let resolvedWorkspace = opts.workspace;
13864
13942
  let resolvedRepos = opts.repos;
@@ -14163,12 +14241,12 @@ function defaultNameFromPrompt(prompt) {
14163
14241
  }
14164
14242
  async function readHostCpToken3() {
14165
14243
  try {
14166
- const { default: fs28 } = await import("node:fs");
14167
- const { default: os16 } = await import("node:os");
14168
- const { default: path32 } = await import("node:path");
14169
- const tp = path32.join(os16.homedir(), ".olam", "host-cp.token");
14170
- if (!fs28.existsSync(tp)) return null;
14171
- const raw = fs28.readFileSync(tp, "utf-8").trim();
14244
+ const { default: fs31 } = await import("node:fs");
14245
+ const { default: os18 } = await import("node:os");
14246
+ const { default: path35 } = await import("node:path");
14247
+ const tp = path35.join(os18.homedir(), ".olam", "host-cp.token");
14248
+ if (!fs31.existsSync(tp)) return null;
14249
+ const raw = fs31.readFileSync(tp, "utf-8").trim();
14172
14250
  return raw.length > 0 ? raw : null;
14173
14251
  } catch {
14174
14252
  return null;
@@ -14602,29 +14680,38 @@ import * as fs21 from "node:fs";
14602
14680
  import "node:path";
14603
14681
  import ora4 from "ora";
14604
14682
  init_world_paths();
14605
- function registerCrystallize(program2) {
14606
- program2.command("crystallize").description("Crystallize thoughts from a world to Pleri Plane").argument("<world>", "World ID").action(async (worldId) => {
14683
+ function registerCrystallize(program2, options = {}) {
14684
+ const cmd = program2.command("crystallize").description("Crystallize thoughts from a world to Pleri Plane").argument("<world>", "World ID").action(async (worldId) => {
14607
14685
  const { ctx, error } = await loadContext();
14686
+ if (!ctx && error?.name === "OlamConfigNotFoundError") {
14687
+ process.stderr.write(
14688
+ "warn: crystallize requires PLERI_BASE_URL \u2014 skipping (Olam is not initialised; run `olam init` to set up a PLERI block, then re-run).\n"
14689
+ );
14690
+ process.exitCode = EXIT_PLERI_NOT_CONFIGURED;
14691
+ return;
14692
+ }
14608
14693
  if (!ctx) {
14609
14694
  printError(error?.message ?? "Olam is not configured. Run `olam init` first.");
14610
- process.exitCode = 1;
14695
+ process.exitCode = EXIT_GENERIC_ERROR;
14611
14696
  return;
14612
14697
  }
14613
14698
  if (!ctx.pleriClient) {
14614
- printError("Pleri Plane is not configured. Add pleri section to .olam/config.yaml.");
14615
- process.exitCode = 1;
14699
+ process.stderr.write(
14700
+ "warn: crystallize requires PLERI_BASE_URL \u2014 skipping (no work performed). Configure pleri in .olam/config.yaml or set PLERI_BASE_URL to enable.\n"
14701
+ );
14702
+ process.exitCode = EXIT_PLERI_NOT_CONFIGURED;
14616
14703
  return;
14617
14704
  }
14618
14705
  const world = ctx.worldManager.getWorld(worldId);
14619
14706
  if (!world) {
14620
14707
  printError(`World "${worldId}" not found.`);
14621
- process.exitCode = 1;
14708
+ process.exitCode = EXIT_GENERIC_ERROR;
14622
14709
  return;
14623
14710
  }
14624
14711
  const thoughtDbPath = getWorldDbPath(world.workspacePath);
14625
14712
  if (!fs21.existsSync(thoughtDbPath)) {
14626
14713
  printError(`No thoughts captured yet for "${worldId}". Run a dispatch first.`);
14627
- process.exitCode = 1;
14714
+ process.exitCode = EXIT_GENERIC_ERROR;
14628
14715
  return;
14629
14716
  }
14630
14717
  const spinner = ora4("Crystallizing thoughts...").start();
@@ -14682,9 +14769,12 @@ function registerCrystallize(program2) {
14682
14769
  } catch (err) {
14683
14770
  spinner.fail("Crystallization failed");
14684
14771
  printError(err instanceof Error ? err.message : String(err));
14685
- process.exitCode = 1;
14772
+ process.exitCode = EXIT_GENERIC_ERROR;
14686
14773
  }
14687
14774
  });
14775
+ if (options.hidden) {
14776
+ cmd._hidden = true;
14777
+ }
14688
14778
  }
14689
14779
 
14690
14780
  // src/commands/pr.ts
@@ -16821,17 +16911,246 @@ function registerPolicyCheck(program2) {
16821
16911
  }
16822
16912
 
16823
16913
  // src/commands/upgrade.ts
16914
+ import * as fs24 from "node:fs";
16915
+ import * as path28 from "node:path";
16916
+ import { spawnSync as spawnSync7 } from "node:child_process";
16917
+ import pc15 from "picocolors";
16918
+
16919
+ // src/commands/upgrade-lock.ts
16824
16920
  import * as fs22 from "node:fs";
16921
+ import * as os13 from "node:os";
16825
16922
  import * as path26 from "node:path";
16826
16923
  import { spawnSync as spawnSync6 } from "node:child_process";
16827
- import pc15 from "picocolors";
16924
+ var LOCK_FILE_PATH = path26.join(os13.homedir(), ".olam", ".upgrade.lock");
16925
+ var STALE_LOCK_TIMEOUT_MS = 5 * 60 * 1e3;
16926
+ function readLockFile(lockPath) {
16927
+ try {
16928
+ if (!fs22.existsSync(lockPath)) return null;
16929
+ const raw = fs22.readFileSync(lockPath, "utf-8").trim();
16930
+ if (raw.length === 0) return null;
16931
+ const parsed = JSON.parse(raw);
16932
+ if (typeof parsed.pid !== "number" || typeof parsed.startTs !== "number") return null;
16933
+ return { pid: parsed.pid, startTs: parsed.startTs };
16934
+ } catch {
16935
+ return null;
16936
+ }
16937
+ }
16938
+ function isPidAlive(pid) {
16939
+ try {
16940
+ process.kill(pid, 0);
16941
+ return true;
16942
+ } catch {
16943
+ return false;
16944
+ }
16945
+ }
16946
+ var PS_UNAVAILABLE = "__ps_unavailable__";
16947
+ function getPidCommand(pid) {
16948
+ const result = spawnSync6("ps", ["-p", String(pid), "-o", "comm="], {
16949
+ encoding: "utf-8",
16950
+ stdio: ["ignore", "pipe", "ignore"]
16951
+ });
16952
+ if (result.status === null || result.error !== void 0) return PS_UNAVAILABLE;
16953
+ if (result.status !== 0) return null;
16954
+ const out = result.stdout.trim();
16955
+ return out.length === 0 ? null : out;
16956
+ }
16957
+ function isOlamUpgradeCommand(comm) {
16958
+ if (!comm) return false;
16959
+ if (comm === PS_UNAVAILABLE) return false;
16960
+ const base = comm.split("/").pop() ?? comm;
16961
+ const stripped = base.replace(/\s*\(.*\)\s*$/, "").trim();
16962
+ return stripped === "node" || stripped === "olam" || stripped === "olam-cli";
16963
+ }
16964
+ function isStaleLock(content, nowMs = Date.now()) {
16965
+ if (!content) return true;
16966
+ if (nowMs - content.startTs > STALE_LOCK_TIMEOUT_MS) return true;
16967
+ if (!isPidAlive(content.pid)) return true;
16968
+ const comm = getPidCommand(content.pid);
16969
+ if (comm === PS_UNAVAILABLE) return false;
16970
+ if (!isOlamUpgradeCommand(comm)) return true;
16971
+ return false;
16972
+ }
16973
+ function acquireLock(lockPath = LOCK_FILE_PATH, nowMs = Date.now()) {
16974
+ const dir = path26.dirname(lockPath);
16975
+ fs22.mkdirSync(dir, { recursive: true });
16976
+ for (let attempt = 0; attempt < 2; attempt++) {
16977
+ try {
16978
+ const fd = fs22.openSync(lockPath, "wx", 420);
16979
+ try {
16980
+ const content = { pid: process.pid, startTs: nowMs };
16981
+ fs22.writeSync(fd, JSON.stringify(content));
16982
+ } finally {
16983
+ fs22.closeSync(fd);
16984
+ }
16985
+ return { acquired: true, lockPath };
16986
+ } catch (err) {
16987
+ const code = err.code;
16988
+ if (code !== "EEXIST") throw err;
16989
+ const existing2 = readLockFile(lockPath);
16990
+ if (isStaleLock(existing2, nowMs)) {
16991
+ try {
16992
+ fs22.unlinkSync(lockPath);
16993
+ } catch (unlinkErr) {
16994
+ const ucode = unlinkErr.code;
16995
+ if (ucode !== "ENOENT") throw unlinkErr;
16996
+ }
16997
+ continue;
16998
+ }
16999
+ return {
17000
+ acquired: false,
17001
+ reason: "live",
17002
+ ...existing2?.pid !== void 0 && { existingPid: existing2.pid },
17003
+ ...existing2?.startTs !== void 0 && { existingStartTs: existing2.startTs }
17004
+ };
17005
+ }
17006
+ }
17007
+ const existing = readLockFile(lockPath);
17008
+ return {
17009
+ acquired: false,
17010
+ reason: "live",
17011
+ ...existing?.pid !== void 0 && { existingPid: existing.pid },
17012
+ ...existing?.startTs !== void 0 && { existingStartTs: existing.startTs }
17013
+ };
17014
+ }
17015
+ function releaseLock(lockPath = LOCK_FILE_PATH) {
17016
+ try {
17017
+ fs22.unlinkSync(lockPath);
17018
+ } catch (err) {
17019
+ const code = err.code;
17020
+ if (code !== "ENOENT") throw err;
17021
+ }
17022
+ }
17023
+ function formatRefusalMessage(result, lockPath = LOCK_FILE_PATH) {
17024
+ const pidStr = result.existingPid !== void 0 ? ` (pid ${result.existingPid})` : "";
17025
+ const lines = [
17026
+ `Upgrade in progress${pidStr}.`,
17027
+ "Wait for the running upgrade to finish, or:",
17028
+ " - Check progress: olam upgrade --history",
17029
+ ` - If stale (crashed CLI): rm ${lockPath}`
17030
+ ];
17031
+ return lines.join("\n");
17032
+ }
17033
+
17034
+ // src/commands/upgrade-log.ts
17035
+ import * as fs23 from "node:fs";
17036
+ import * as os14 from "node:os";
17037
+ import * as path27 from "node:path";
17038
+ function getUpgradeLogPath() {
17039
+ const home = process.env["HOME"] ?? os14.homedir();
17040
+ return path27.join(home, ".olam", "upgrade.log");
17041
+ }
17042
+ var UPGRADE_LOG_PATH = getUpgradeLogPath();
17043
+ function appendUpgradeLog(row, logPath = getUpgradeLogPath()) {
17044
+ try {
17045
+ fs23.mkdirSync(path27.dirname(logPath), { recursive: true });
17046
+ const line = JSON.stringify(row) + "\n";
17047
+ fs23.appendFileSync(logPath, line, { mode: 420 });
17048
+ } catch (err) {
17049
+ process.stderr.write(
17050
+ `[upgrade-log] failed to append: ${err instanceof Error ? err.message : String(err)}
17051
+ `
17052
+ );
17053
+ }
17054
+ }
17055
+ function readUpgradeLog(limit = 10, logPath = getUpgradeLogPath()) {
17056
+ if (!fs23.existsSync(logPath)) return [];
17057
+ let raw;
17058
+ try {
17059
+ raw = fs23.readFileSync(logPath, "utf-8");
17060
+ } catch (err) {
17061
+ process.stderr.write(
17062
+ `[upgrade-log] failed to read: ${err instanceof Error ? err.message : String(err)}
17063
+ `
17064
+ );
17065
+ return [];
17066
+ }
17067
+ const lines = raw.split("\n").filter((l) => l.length > 0);
17068
+ const rows = [];
17069
+ for (let i = 0; i < lines.length; i++) {
17070
+ const line = lines[i];
17071
+ try {
17072
+ const parsed = JSON.parse(line);
17073
+ if (typeof parsed.ts === "string" && typeof parsed.started_at === "number" && typeof parsed.status === "string") {
17074
+ rows.push(parsed);
17075
+ } else {
17076
+ process.stderr.write(`[upgrade-log] skipped malformed row at line ${i + 1}
17077
+ `);
17078
+ }
17079
+ } catch {
17080
+ process.stderr.write(`[upgrade-log] skipped corrupt JSON at line ${i + 1}
17081
+ `);
17082
+ }
17083
+ }
17084
+ return rows.slice(-Math.max(0, limit));
17085
+ }
17086
+ function formatDuration(ms) {
17087
+ if (ms < 1e3) return `${ms}ms`;
17088
+ const totalSec = Math.round(ms / 1e3);
17089
+ if (totalSec < 60) return `${totalSec}s`;
17090
+ const min = Math.floor(totalSec / 60);
17091
+ const sec = totalSec % 60;
17092
+ if (min < 60) return `${min}m${String(sec).padStart(2, "0")}s`;
17093
+ const hr = Math.floor(min / 60);
17094
+ const remMin = min % 60;
17095
+ return `${hr}h${String(remMin).padStart(2, "0")}m`;
17096
+ }
17097
+ function formatHistoryTable(rows) {
17098
+ if (rows.length === 0) {
17099
+ return "No upgrade history yet. Run `olam upgrade` to create your first record.";
17100
+ }
17101
+ const lines = [];
17102
+ lines.push("TIMESTAMP SHA STATUS DURATION FAILED-STEP");
17103
+ lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
17104
+ for (const r of rows) {
17105
+ const ts = r.ts.slice(0, 19).replace("T", " ");
17106
+ const sha = r.sha_target.slice(0, 8);
17107
+ const statusIcon = r.status === "success" ? "\u2713 success" : r.status === "rolled_back" ? "\u21A9 rolled_back" : "\u2717 failed";
17108
+ const dur = formatDuration(r.ended_at - r.started_at);
17109
+ const failed = r.failed_step ?? "";
17110
+ lines.push(
17111
+ `${ts.padEnd(28)}${sha.padEnd(10)}${statusIcon.padEnd(15)}${dur.padEnd(11)}${failed}`
17112
+ );
17113
+ }
17114
+ return lines.join("\n");
17115
+ }
17116
+ function formatHistoryJson(rows) {
17117
+ return rows.map((r) => JSON.stringify(r)).join("\n");
17118
+ }
17119
+
17120
+ // src/commands/upgrade-history.ts
17121
+ function parseHistoryOpts(raw) {
17122
+ const rawLimit = raw.n;
17123
+ const limit = typeof rawLimit === "number" ? rawLimit : typeof rawLimit === "string" ? Number.parseInt(rawLimit, 10) : 10;
17124
+ return {
17125
+ limit: Number.isFinite(limit) && limit > 0 ? limit : 10,
17126
+ json: raw.json === true
17127
+ };
17128
+ }
17129
+ function handleHistory(opts) {
17130
+ const rows = readUpgradeLog(opts.limit);
17131
+ if (opts.json) {
17132
+ process.stdout.write(formatHistoryJson(rows) + "\n");
17133
+ return;
17134
+ }
17135
+ if (rows.length === 0) {
17136
+ printInfo("Log file", UPGRADE_LOG_PATH);
17137
+ process.stdout.write(formatHistoryTable(rows) + "\n");
17138
+ return;
17139
+ }
17140
+ printInfo("Log file", UPGRADE_LOG_PATH);
17141
+ process.stdout.write(formatHistoryTable(rows) + "\n");
17142
+ }
17143
+
17144
+ // src/commands/upgrade.ts
17145
+ init_auth();
17146
+ var AUTH_HEALTH_URL2 = "http://127.0.0.1:9999/health";
16828
17147
  function isNodeModulesInSync(cwd) {
16829
- const lockPath = path26.join(cwd, "package-lock.json");
16830
- const markerPath = path26.join(cwd, "node_modules", ".package-lock.json");
16831
- if (!fs22.existsSync(lockPath) || !fs22.existsSync(markerPath)) return false;
17148
+ const lockPath = path28.join(cwd, "package-lock.json");
17149
+ const markerPath = path28.join(cwd, "node_modules", ".package-lock.json");
17150
+ if (!fs24.existsSync(lockPath) || !fs24.existsSync(markerPath)) return false;
16832
17151
  try {
16833
- const lockStat = fs22.statSync(lockPath);
16834
- const markerStat = fs22.statSync(markerPath);
17152
+ const lockStat = fs24.statSync(lockPath);
17153
+ const markerStat = fs24.statSync(markerPath);
16835
17154
  return markerStat.mtimeMs >= lockStat.mtimeMs;
16836
17155
  } catch {
16837
17156
  return false;
@@ -16847,8 +17166,8 @@ function shouldSkipInstall(opts, cwd) {
16847
17166
  return { skip: false };
16848
17167
  }
16849
17168
  function validateRepoRoot(cwd) {
16850
- const marker = path26.join(cwd, "packages/host-cp/compose.yaml");
16851
- if (!fs22.existsSync(marker)) {
17169
+ const marker = path28.join(cwd, "packages/host-cp/compose.yaml");
17170
+ if (!fs24.existsSync(marker)) {
16852
17171
  return {
16853
17172
  ok: false,
16854
17173
  error: `Not an olam repo root (expected ${marker}).
@@ -16858,11 +17177,19 @@ Run \`olam upgrade\` from the root of your olam checkout.`
16858
17177
  return { ok: true };
16859
17178
  }
16860
17179
  function parseUpgradeOpts(raw) {
17180
+ const rawN = raw.n;
17181
+ const historyN = typeof rawN === "number" ? rawN : typeof rawN === "string" ? Number.parseInt(rawN, 10) : 10;
16861
17182
  return {
16862
17183
  yes: raw.yes === true,
16863
17184
  skipImage: raw.skipImage === true,
16864
17185
  skipInstall: raw.skipInstall === true,
16865
- branch: raw.branch ?? null
17186
+ branch: raw.branch ?? null,
17187
+ rollback: raw.rollback === true,
17188
+ force: raw.force === true,
17189
+ noCache: raw.noCache === true,
17190
+ history: raw.history === true,
17191
+ historyN: Number.isFinite(historyN) && historyN > 0 ? historyN : 10,
17192
+ historyJson: raw.json === true
16866
17193
  };
16867
17194
  }
16868
17195
  function extractBundleHash(indexHtml) {
@@ -16872,7 +17199,7 @@ function extractBundleHash(indexHtml) {
16872
17199
  function runStep2(label, cmd, args, opts = {}) {
16873
17200
  const start = Date.now();
16874
17201
  process.stdout.write(` ${pc15.dim(label.padEnd(34))}`);
16875
- const result = spawnSync6(cmd, [...args], {
17202
+ const result = spawnSync7(cmd, [...args], {
16876
17203
  encoding: "utf-8",
16877
17204
  stdio: ["ignore", "pipe", "pipe"],
16878
17205
  cwd: opts.cwd ?? process.cwd(),
@@ -16891,7 +17218,7 @@ function runStep2(label, cmd, args, opts = {}) {
16891
17218
  };
16892
17219
  }
16893
17220
  function isGitDirty(cwd) {
16894
- const result = spawnSync6("git", ["status", "--porcelain"], {
17221
+ const result = spawnSync7("git", ["status", "--porcelain"], {
16895
17222
  encoding: "utf-8",
16896
17223
  stdio: ["ignore", "pipe", "pipe"],
16897
17224
  cwd
@@ -16899,13 +17226,194 @@ function isGitDirty(cwd) {
16899
17226
  return (result.stdout ?? "").trim().length > 0;
16900
17227
  }
16901
17228
  function hasGitUpstream(cwd) {
16902
- const result = spawnSync6("git", ["rev-parse", "--abbrev-ref", "@{u}"], {
17229
+ const result = spawnSync7("git", ["rev-parse", "--abbrev-ref", "@{u}"], {
16903
17230
  encoding: "utf-8",
16904
17231
  stdio: ["ignore", "pipe", "pipe"],
16905
17232
  cwd
16906
17233
  });
16907
17234
  return result.status === 0;
16908
17235
  }
17236
+ function captureHeadSha(cwd) {
17237
+ const result = spawnSync7("git", ["rev-parse", "HEAD"], {
17238
+ encoding: "utf-8",
17239
+ stdio: ["ignore", "pipe", "pipe"],
17240
+ cwd
17241
+ });
17242
+ if (result.status !== 0) return null;
17243
+ const sha = (result.stdout ?? "").trim();
17244
+ if (!/^[0-9a-f]{40}$/.test(sha)) return null;
17245
+ return sha;
17246
+ }
17247
+ function abbreviateSha(sha) {
17248
+ return sha.slice(0, 8);
17249
+ }
17250
+ function imageExists(tag) {
17251
+ try {
17252
+ const result = spawnSync7("docker", ["image", "inspect", "--format", "{{.Id}}", tag], {
17253
+ encoding: "utf-8",
17254
+ stdio: ["ignore", "pipe", "ignore"]
17255
+ });
17256
+ return result.status === 0;
17257
+ } catch {
17258
+ return false;
17259
+ }
17260
+ }
17261
+ function checkRollbackSetExists(plan) {
17262
+ const missing = plan.filter((p) => !imageExists(p.rollback)).map((p) => p.rollback);
17263
+ if (missing.length === 0) return null;
17264
+ return missing.join(", ");
17265
+ }
17266
+ function smokeImage(image, targetSha) {
17267
+ const createResult = spawnSync7("docker", ["create", "--name", `olam-smoke-${Date.now()}`, image], {
17268
+ encoding: "utf-8",
17269
+ stdio: ["ignore", "pipe", "pipe"]
17270
+ });
17271
+ if (createResult.status !== 0) {
17272
+ return {
17273
+ image,
17274
+ ok: false,
17275
+ bakedSha: null,
17276
+ error: `docker create failed: ${(createResult.stderr ?? "").trim()}`
17277
+ };
17278
+ }
17279
+ const containerId = (createResult.stdout ?? "").trim();
17280
+ const inspectResult = spawnSync7(
17281
+ "docker",
17282
+ ["inspect", "--format", '{{index .Config.Labels "olam_build_sha"}}', image],
17283
+ {
17284
+ encoding: "utf-8",
17285
+ stdio: ["ignore", "pipe", "pipe"]
17286
+ }
17287
+ );
17288
+ if (containerId.length > 0) {
17289
+ spawnSync7("docker", ["rm", "-f", containerId], {
17290
+ encoding: "utf-8",
17291
+ stdio: ["ignore", "ignore", "ignore"]
17292
+ });
17293
+ }
17294
+ if (inspectResult.status !== 0) {
17295
+ return {
17296
+ image,
17297
+ ok: false,
17298
+ bakedSha: null,
17299
+ error: `docker inspect failed: ${(inspectResult.stderr ?? "").trim()}`
17300
+ };
17301
+ }
17302
+ const bakedSha = (inspectResult.stdout ?? "").trim();
17303
+ if (bakedSha.length === 0) {
17304
+ return {
17305
+ image,
17306
+ ok: false,
17307
+ bakedSha: null,
17308
+ error: "olam_build_sha label is missing or empty"
17309
+ };
17310
+ }
17311
+ if (bakedSha !== targetSha) {
17312
+ return {
17313
+ image,
17314
+ ok: false,
17315
+ bakedSha,
17316
+ error: `baked SHA ${abbreviateSha(bakedSha)} \u2260 target SHA ${abbreviateSha(targetSha)}`
17317
+ };
17318
+ }
17319
+ return { image, ok: true, bakedSha };
17320
+ }
17321
+ var PRODUCTION_SWAP_PLAN = [
17322
+ { transient: "olam-auth:olam-next", canonical: "olam-auth:local", rollback: "olam-auth:olam-rollback" },
17323
+ { transient: "olam-devbox:olam-next", canonical: "olam-devbox:latest", rollback: "olam-devbox:olam-rollback" },
17324
+ { transient: "olam-host-cp:olam-next", canonical: "olam-host-cp:latest", rollback: "olam-host-cp:olam-rollback" }
17325
+ ];
17326
+ function dockerTag(source, dest) {
17327
+ try {
17328
+ const result = spawnSync7("docker", ["tag", source, dest], {
17329
+ encoding: "utf-8",
17330
+ stdio: ["ignore", "ignore", "pipe"]
17331
+ });
17332
+ if (result.status === 0 && result.error === void 0) return { ok: true };
17333
+ return {
17334
+ ok: false,
17335
+ error: (result.stderr ?? "").trim() || result.error?.message || "docker tag failed"
17336
+ };
17337
+ } catch (err) {
17338
+ return {
17339
+ ok: false,
17340
+ error: err instanceof Error ? `spawnSync threw: ${err.message}` : "spawnSync threw"
17341
+ };
17342
+ }
17343
+ }
17344
+ function performAtomicSwap(plan) {
17345
+ const steps = plan.map((p) => ({
17346
+ image: p.canonical,
17347
+ rollbackSaved: false,
17348
+ canonicalAdvanced: false
17349
+ }));
17350
+ for (let i = 0; i < plan.length; i++) {
17351
+ const p = plan[i];
17352
+ const r = dockerTag(p.canonical, p.rollback);
17353
+ steps[i] = {
17354
+ ...steps[i],
17355
+ rollbackSaved: r.ok,
17356
+ ...r.error !== void 0 && { rollbackError: r.error }
17357
+ };
17358
+ }
17359
+ let advanceFailed = false;
17360
+ let firstFailureIdx = -1;
17361
+ for (let i = 0; i < plan.length; i++) {
17362
+ const p = plan[i];
17363
+ if (advanceFailed) {
17364
+ steps[i] = { ...steps[i], canonicalAdvanced: false };
17365
+ continue;
17366
+ }
17367
+ const r = dockerTag(p.transient, p.canonical);
17368
+ steps[i] = {
17369
+ ...steps[i],
17370
+ canonicalAdvanced: r.ok,
17371
+ ...r.error !== void 0 && { canonicalError: r.error }
17372
+ };
17373
+ if (!r.ok) {
17374
+ advanceFailed = true;
17375
+ firstFailureIdx = i;
17376
+ }
17377
+ }
17378
+ const allAdvanced = steps.every((s) => s.canonicalAdvanced);
17379
+ const noneAdvanced = steps.every((s) => !s.canonicalAdvanced);
17380
+ const partialAdvance = !allAdvanced && !noneAdvanced;
17381
+ const rollbackCoherent = steps.every((s) => s.rollbackSaved);
17382
+ let summary;
17383
+ if (allAdvanced) {
17384
+ const rollbacks = steps.filter((s) => s.rollbackSaved).length;
17385
+ summary = `Swapped ${plan.length} canonical tags; ${rollbacks} :olam-rollback preserved`;
17386
+ } else if (partialAdvance) {
17387
+ const advanced = steps.filter((s) => s.canonicalAdvanced).length;
17388
+ const failedStep = steps[firstFailureIdx];
17389
+ const recoveryHint = rollbackCoherent ? `Run \`olam upgrade --rollback\` to restore coherent prior state.` : `Rollback set INCOHERENT (${steps.filter((s) => s.rollbackSaved).length} of ${plan.length} :olam-rollback tags written). Manual recovery required: inspect images and re-tag canonical from a known-good source.`;
17390
+ summary = `PARTIAL: ${advanced} of ${plan.length} canonical tags advanced before failure on ${failedStep?.image}: ${failedStep?.canonicalError}. ${recoveryHint}`;
17391
+ } else {
17392
+ const failedStep = steps[firstFailureIdx];
17393
+ summary = `Failed on first canonical-advance (${failedStep?.image}): ${failedStep?.canonicalError}. Canonical tags untouched.`;
17394
+ }
17395
+ return {
17396
+ ok: allAdvanced,
17397
+ steps,
17398
+ partialAdvance,
17399
+ rollbackCoherent,
17400
+ summary
17401
+ };
17402
+ }
17403
+ function performRollbackSwap(plan) {
17404
+ const results = [];
17405
+ for (const p of plan) {
17406
+ const r = dockerTag(p.rollback, p.canonical);
17407
+ results.push({
17408
+ image: p.canonical,
17409
+ ok: r.ok,
17410
+ ...r.error !== void 0 && { error: r.error }
17411
+ });
17412
+ }
17413
+ const allOk = results.every((r) => r.ok);
17414
+ const summary = allOk ? `Rolled back ${plan.length} canonical tags from :olam-rollback` : `PARTIAL rollback: ${results.filter((r) => r.ok).length} of ${plan.length} succeeded; failed: ${results.filter((r) => !r.ok).map((r) => r.image).join(", ")}`;
17415
+ return { ok: allOk, results, summary };
17416
+ }
16909
17417
  async function confirm2(message) {
16910
17418
  if (!process.stdin.isTTY) return true;
16911
17419
  const { createInterface: createInterface2 } = await import("node:readline");
@@ -16931,10 +17439,87 @@ async function waitForHealth(timeoutMs = 1e4) {
16931
17439
  }
16932
17440
  return false;
16933
17441
  }
17442
+ async function waitForVersionMatch(targetSha, timeoutMs = 6e4, pollIntervalMs = 1e3) {
17443
+ const deadline = Date.now() + timeoutMs;
17444
+ let lastSnapshot = null;
17445
+ while (Date.now() < deadline) {
17446
+ try {
17447
+ const res = await fetch("http://127.0.0.1:19000/api/version/status", {
17448
+ signal: AbortSignal.timeout(2e3)
17449
+ });
17450
+ if (res.ok) {
17451
+ const snapshot = await res.json();
17452
+ lastSnapshot = snapshot;
17453
+ if (snapshot.hostCp?.running === targetSha && snapshot.authService?.running === targetSha && snapshot.devbox?.running === targetSha) {
17454
+ return { matched: true, snapshot };
17455
+ }
17456
+ }
17457
+ } catch {
17458
+ }
17459
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
17460
+ }
17461
+ return { matched: false, snapshot: lastSnapshot };
17462
+ }
17463
+ function formatVersionMismatch(targetSha, snapshot) {
17464
+ if (!snapshot) return "No /api/version/status response received within timeout.";
17465
+ const lines = [];
17466
+ for (const [name, comp] of [
17467
+ ["host-cp", snapshot.hostCp],
17468
+ ["auth-service", snapshot.authService],
17469
+ ["devbox", snapshot.devbox]
17470
+ ]) {
17471
+ const match2 = comp?.running === targetSha;
17472
+ lines.push(` ${match2 ? "\u2713" : "\u2717"} ${name}: running=${abbreviateSha(comp?.running ?? "unknown")} target=${abbreviateSha(targetSha)}`);
17473
+ }
17474
+ return lines.join("\n");
17475
+ }
17476
+ async function waitForAuthHealthLocal(timeoutMs = 15e3) {
17477
+ const deadline = Date.now() + timeoutMs;
17478
+ while (Date.now() < deadline) {
17479
+ try {
17480
+ const res = await fetch(AUTH_HEALTH_URL2, { signal: AbortSignal.timeout(2e3) });
17481
+ if (res.ok) return true;
17482
+ } catch {
17483
+ }
17484
+ await new Promise((r) => setTimeout(r, 500));
17485
+ }
17486
+ return false;
17487
+ }
17488
+ async function recreateAuthService() {
17489
+ const start = Date.now();
17490
+ try {
17491
+ spawnSync7("docker", ["stop", "olam-auth"], {
17492
+ encoding: "utf-8",
17493
+ stdio: ["ignore", "ignore", "ignore"]
17494
+ });
17495
+ spawnSync7("docker", ["rm", "olam-auth"], {
17496
+ encoding: "utf-8",
17497
+ stdio: ["ignore", "ignore", "ignore"]
17498
+ });
17499
+ const controller = new AuthContainerController();
17500
+ controller.start();
17501
+ const healthy = await waitForAuthHealthLocal(15e3);
17502
+ const durationMs = Date.now() - start;
17503
+ if (!healthy) {
17504
+ return {
17505
+ ok: false,
17506
+ durationMs,
17507
+ error: "auth-service /health did not respond within 15s after recreate"
17508
+ };
17509
+ }
17510
+ return { ok: true, durationMs };
17511
+ } catch (err) {
17512
+ return {
17513
+ ok: false,
17514
+ durationMs: Date.now() - start,
17515
+ error: err instanceof Error ? err.message : String(err)
17516
+ };
17517
+ }
17518
+ }
16934
17519
  function readBundleHash(cwd) {
16935
- const indexPath = path26.join(cwd, "packages/control-plane/public/index.html");
16936
- if (!fs22.existsSync(indexPath)) return null;
16937
- return extractBundleHash(fs22.readFileSync(indexPath, "utf-8"));
17520
+ const indexPath = path28.join(cwd, "packages/control-plane/public/index.html");
17521
+ if (!fs24.existsSync(indexPath)) return null;
17522
+ return extractBundleHash(fs24.readFileSync(indexPath, "utf-8"));
16938
17523
  }
16939
17524
  async function handleUpgrade(opts) {
16940
17525
  const cwd = process.cwd();
@@ -16969,6 +17554,142 @@ async function handleUpgrade(opts) {
16969
17554
  return;
16970
17555
  }
16971
17556
  }
17557
+ if (opts.history) {
17558
+ handleHistory(parseHistoryOpts({ n: opts.historyN, json: opts.historyJson }));
17559
+ return;
17560
+ }
17561
+ if (opts.rollback) {
17562
+ return await handleRollback();
17563
+ }
17564
+ const lock = acquireLock();
17565
+ if (!lock.acquired) {
17566
+ printError(formatRefusalMessage(lock, LOCK_FILE_PATH));
17567
+ process.exitCode = 1;
17568
+ return;
17569
+ }
17570
+ let signalReleased = false;
17571
+ const releaseOnSignal = (signal) => {
17572
+ if (signalReleased) return;
17573
+ signalReleased = true;
17574
+ try {
17575
+ releaseLock();
17576
+ } catch {
17577
+ }
17578
+ process.exit(signal === "SIGINT" ? 130 : 143);
17579
+ };
17580
+ process.once("SIGINT", releaseOnSignal);
17581
+ process.once("SIGTERM", releaseOnSignal);
17582
+ const logRow = {
17583
+ started_at: Date.now(),
17584
+ durations_ms: {},
17585
+ sha_target: "",
17586
+ failed_step: null,
17587
+ status: "failed"
17588
+ // default; flipped to 'success' on clean exit
17589
+ };
17590
+ try {
17591
+ await runUpgradeStepsWithLockHeld(opts, cwd, logRow);
17592
+ if (process.exitCode !== 1) logRow.status = "success";
17593
+ } finally {
17594
+ const ended_at = Date.now();
17595
+ const row = {
17596
+ ts: new Date(ended_at).toISOString(),
17597
+ started_at: logRow.started_at,
17598
+ ended_at,
17599
+ sha_target: logRow.sha_target,
17600
+ status: logRow.status,
17601
+ failed_step: logRow.failed_step,
17602
+ durations_ms: logRow.durations_ms
17603
+ };
17604
+ appendUpgradeLog(row);
17605
+ releaseLock();
17606
+ process.removeListener("SIGINT", releaseOnSignal);
17607
+ process.removeListener("SIGTERM", releaseOnSignal);
17608
+ }
17609
+ }
17610
+ async function handleRollback() {
17611
+ printHeader("olam upgrade --rollback");
17612
+ const missing = checkRollbackSetExists(PRODUCTION_SWAP_PLAN);
17613
+ if (missing !== null) {
17614
+ printError(
17615
+ `No rollback-set available \u2014 missing :olam-rollback tag(s): ${missing}
17616
+
17617
+ A rollback-set is created by the FIRST successful \`olam upgrade\`. If this
17618
+ is your first install, run \`olam upgrade\` to populate the rollback set.
17619
+ If a previous upgrade was incomplete, the rollback set may be partial;
17620
+ manually inspect images with \`docker images olam-*:olam-rollback\`.`
17621
+ );
17622
+ process.exitCode = 1;
17623
+ return;
17624
+ }
17625
+ const lock = acquireLock();
17626
+ if (!lock.acquired) {
17627
+ printError(formatRefusalMessage(lock, LOCK_FILE_PATH));
17628
+ process.exitCode = 1;
17629
+ return;
17630
+ }
17631
+ let signalReleased = false;
17632
+ const releaseOnSignal = (signal) => {
17633
+ if (signalReleased) return;
17634
+ signalReleased = true;
17635
+ try {
17636
+ releaseLock();
17637
+ } catch {
17638
+ }
17639
+ process.exit(signal === "SIGINT" ? 130 : 143);
17640
+ };
17641
+ process.once("SIGINT", releaseOnSignal);
17642
+ process.once("SIGTERM", releaseOnSignal);
17643
+ try {
17644
+ process.stdout.write(` ${pc15.dim("rollback retag (3 ops)".padEnd(34))}`);
17645
+ const swapStart = Date.now();
17646
+ const swapResult = performRollbackSwap(PRODUCTION_SWAP_PLAN);
17647
+ const swapDur = `${((Date.now() - swapStart) / 1e3).toFixed(1)}s`;
17648
+ process.stdout.write(`${swapResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${swapDur}
17649
+ `);
17650
+ if (!swapResult.ok) {
17651
+ printError(`Rollback retag failed: ${swapResult.summary}`);
17652
+ process.exitCode = 1;
17653
+ return;
17654
+ }
17655
+ printInfo("Rollback", swapResult.summary);
17656
+ const cwd = process.cwd();
17657
+ const composeFile = path28.join(cwd, "packages/host-cp/compose.yaml");
17658
+ const authSecret = readAuthSecret2();
17659
+ process.stdout.write(` ${pc15.dim("docker compose recreate host-cp".padEnd(34))}`);
17660
+ const composeStart = Date.now();
17661
+ const composeResult = runCompose(["up", "-d", "--force-recreate", "host-cp"], composeFile, buildComposeEnv(authSecret));
17662
+ const composeDur = `${((Date.now() - composeStart) / 1e3).toFixed(1)}s`;
17663
+ process.stdout.write(`${composeResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${composeDur}
17664
+ `);
17665
+ if (!composeResult.ok) {
17666
+ printError(
17667
+ `Rollback compose recreate failed:
17668
+ ${composeResult.stderr}
17669
+ Canonical tags are at :olam-rollback (good); container restart pending. Manually: \`docker compose -f packages/host-cp/compose.yaml up -d --force-recreate host-cp\`.`
17670
+ );
17671
+ process.exitCode = 1;
17672
+ return;
17673
+ }
17674
+ process.stdout.write(` ${pc15.dim("recreate auth-service".padEnd(34))}`);
17675
+ const authResult = await recreateAuthService();
17676
+ const authDur = `${(authResult.durationMs / 1e3).toFixed(1)}s`;
17677
+ process.stdout.write(`${authResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${authDur}
17678
+ `);
17679
+ if (!authResult.ok) {
17680
+ printError(`Auth-service recreate failed: ${authResult.error ?? "unknown"}`);
17681
+ process.exitCode = 1;
17682
+ return;
17683
+ }
17684
+ process.stdout.write("\n");
17685
+ printSuccess("Rollback complete \u2014 canonical tags restored from :olam-rollback");
17686
+ } finally {
17687
+ releaseLock();
17688
+ process.removeListener("SIGINT", releaseOnSignal);
17689
+ process.removeListener("SIGTERM", releaseOnSignal);
17690
+ }
17691
+ }
17692
+ async function runUpgradeStepsWithLockHeld(opts, cwd, logRow) {
16972
17693
  if (opts.branch !== null) {
16973
17694
  if (isGitDirty(cwd)) {
16974
17695
  printError(
@@ -17026,6 +17747,17 @@ If there are conflicts, resolve them manually then re-run \`olam upgrade\`.`
17026
17747
  process.exitCode = 1;
17027
17748
  return;
17028
17749
  }
17750
+ const _targetSha = captureHeadSha(cwd);
17751
+ logRow.sha_target = _targetSha ?? "";
17752
+ if (_targetSha === null) {
17753
+ logRow.failed_step = "capture HEAD SHA";
17754
+ printError(
17755
+ "Failed to capture HEAD SHA via `git rev-parse HEAD`. Aborting upgrade.\nRe-run from a clean git checkout; ensure `git rev-parse HEAD` returns a 40-char SHA."
17756
+ );
17757
+ process.exitCode = 1;
17758
+ return;
17759
+ }
17760
+ printInfo("Target SHA", abbreviateSha(_targetSha));
17029
17761
  const installDecision = shouldSkipInstall(opts, cwd);
17030
17762
  if (installDecision.skip) {
17031
17763
  printInfo("npm install", `skipped \u2014 ${installDecision.reason}`);
@@ -17063,7 +17795,7 @@ ${buildResult.stderr}`);
17063
17795
  return;
17064
17796
  }
17065
17797
  const authSecret = readAuthSecret2();
17066
- const spaDir = path26.join(cwd, "packages/control-plane/app");
17798
+ const spaDir = path28.join(cwd, "packages/control-plane/app");
17067
17799
  const spaResult = runStep2(
17068
17800
  "vite build (SPA)",
17069
17801
  "npx",
@@ -17085,21 +17817,107 @@ ${spaResult.stderr}`);
17085
17817
  printTimings2(timings);
17086
17818
  return;
17087
17819
  }
17088
- const buildScript = path26.join(cwd, "packages/adapters/src/docker/build-host-cp.sh");
17089
- const imageResult = runStep2(
17090
- "bash build-host-cp.sh",
17091
- "bash",
17092
- [buildScript],
17093
- { cwd }
17094
- );
17095
- timings.push({ label: "docker image build", durationMs: imageResult.durationMs });
17096
- if (!imageResult.ok) {
17097
- printError(`Docker image build failed:
17098
- ${imageResult.stderr}`);
17820
+ const olamTagEnv = { OLAM_TAG: "olam-next" };
17821
+ if (opts.noCache) {
17822
+ olamTagEnv.DOCKER_BUILD_NO_CACHE = "1";
17823
+ }
17824
+ const buildScripts = [
17825
+ { label: "bash build-auth.sh", relPath: "packages/adapters/src/docker/build-auth.sh", tee: false },
17826
+ { label: "bash build-devbox.sh", relPath: "packages/adapters/src/docker/build-devbox.sh", tee: true },
17827
+ { label: "bash build-host-cp.sh", relPath: "packages/adapters/src/docker/build-host-cp.sh", tee: false }
17828
+ ];
17829
+ for (const step of buildScripts) {
17830
+ const scriptPath = path28.join(cwd, step.relPath);
17831
+ if (step.tee) {
17832
+ process.stdout.write(` ${pc15.dim(step.label.padEnd(34))}
17833
+ `);
17834
+ const start = Date.now();
17835
+ const result = spawnSync7("bash", [scriptPath], {
17836
+ stdio: "inherit",
17837
+ cwd,
17838
+ env: { ...process.env, ...olamTagEnv }
17839
+ });
17840
+ const durationMs = Date.now() - start;
17841
+ const ok = result.status === 0 && result.error === void 0;
17842
+ const dur = `${(durationMs / 1e3).toFixed(1)}s`;
17843
+ process.stdout.write(` ${pc15.dim(step.label.padEnd(34))}${ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${dur}
17844
+ `);
17845
+ timings.push({ label: step.label, durationMs });
17846
+ if (!ok) {
17847
+ printError(`${step.label} failed (see output above for details).`);
17848
+ process.exitCode = 1;
17849
+ return;
17850
+ }
17851
+ } else {
17852
+ const result = runStep2(step.label, "bash", [scriptPath], {
17853
+ cwd,
17854
+ env: olamTagEnv
17855
+ });
17856
+ timings.push({ label: step.label, durationMs: result.durationMs });
17857
+ logRow.durations_ms[step.label] = result.durationMs;
17858
+ if (!result.ok) {
17859
+ logRow.failed_step = step.label;
17860
+ printError(`${step.label} failed:
17861
+ ${result.stderr.split("\n").slice(-3).join("\n")}`);
17862
+ process.exitCode = 1;
17863
+ return;
17864
+ }
17865
+ }
17866
+ }
17867
+ for (const t of timings) logRow.durations_ms[t.label] = t.durationMs;
17868
+ const smokeStart = Date.now();
17869
+ process.stdout.write(` ${pc15.dim("smoke (docker create + inspect)".padEnd(34))}`);
17870
+ const smokeImages = [
17871
+ "olam-auth:olam-next",
17872
+ "olam-devbox:olam-next",
17873
+ "olam-host-cp:olam-next"
17874
+ ];
17875
+ const smokeResults = smokeImages.map((img) => smokeImage(img, _targetSha));
17876
+ const smokeFailures = smokeResults.filter((r) => !r.ok);
17877
+ const smokeDurationMs = Date.now() - smokeStart;
17878
+ const smokeDur = `${(smokeDurationMs / 1e3).toFixed(1)}s`;
17879
+ process.stdout.write(`${smokeFailures.length === 0 ? pc15.green("\u2713") : pc15.red("\u2717")} ${smokeDur}
17880
+ `);
17881
+ timings.push({ label: "smoke", durationMs: smokeDurationMs });
17882
+ if (smokeFailures.length > 0) {
17883
+ printError(
17884
+ `Smoke failed for ${smokeFailures.length} of ${smokeResults.length} images:
17885
+ ` + smokeFailures.map((r) => ` - ${r.image}: ${r.error}`).join("\n") + "\nCanonical tags (`:latest`/`:local`) untouched. Investigate the failed image(s), then re-run `olam upgrade` (--no-cache if cache-poisoning suspected)."
17886
+ );
17099
17887
  process.exitCode = 1;
17100
17888
  return;
17101
17889
  }
17102
- const composeFile = path26.join(cwd, "packages/host-cp/compose.yaml");
17890
+ const swapBoundarySha = captureHeadSha(cwd);
17891
+ if (swapBoundarySha !== null && swapBoundarySha !== _targetSha && !opts.force) {
17892
+ printError(
17893
+ `HEAD drifted during build window:
17894
+ captured (after pull): ${abbreviateSha(_targetSha)}
17895
+ current at swap: ${abbreviateSha(swapBoundarySha)}
17896
+
17897
+ Operator-driven \`git checkout\` or \`git reset\` triggered drift.
17898
+ Recovery options:
17899
+ \u2022 Re-run \`olam upgrade\` (will rebuild against current HEAD).
17900
+ \u2022 Pass \`--force\` to swap anyway (canonical advances to the
17901
+ captured-at-pull SHA, NOT current HEAD).`
17902
+ );
17903
+ process.exitCode = 1;
17904
+ return;
17905
+ }
17906
+ process.stdout.write(` ${pc15.dim("atomic 6-tag swap".padEnd(34))}`);
17907
+ const swapStart = Date.now();
17908
+ const swapResult = performAtomicSwap(PRODUCTION_SWAP_PLAN);
17909
+ const swapDurationMs = Date.now() - swapStart;
17910
+ const swapDur = `${(swapDurationMs / 1e3).toFixed(1)}s`;
17911
+ process.stdout.write(`${swapResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${swapDur}
17912
+ `);
17913
+ timings.push({ label: "atomic swap", durationMs: swapDurationMs });
17914
+ if (!swapResult.ok) {
17915
+ printError(`Atomic swap failed: ${swapResult.summary}`);
17916
+ process.exitCode = 1;
17917
+ return;
17918
+ }
17919
+ printInfo("Swap", swapResult.summary);
17920
+ const composeFile = path28.join(cwd, "packages/host-cp/compose.yaml");
17103
17921
  process.stdout.write(` ${pc15.dim("docker compose recreate".padEnd(34))}`);
17104
17922
  const composeStart = Date.now();
17105
17923
  const composeResult = runCompose(
@@ -17114,8 +17932,33 @@ ${imageResult.stderr}`);
17114
17932
  `);
17115
17933
  timings.push({ label: "container recreate", durationMs: composeDurationMs });
17116
17934
  if (!composeOk) {
17117
- printError(`docker compose up --force-recreate failed:
17118
- ${composeResult.stderr}`);
17935
+ printError(
17936
+ `docker compose up --force-recreate failed:
17937
+ ${composeResult.stderr}
17938
+
17939
+ Canonical tags advanced to new SHA but the stack failed to start.
17940
+ Recovery options:
17941
+ \u2022 Run \`olam upgrade --rollback\` to restore the prior :olam-rollback set, then investigate.
17942
+ \u2022 Manually \`docker logs olam-host-cp\` to diagnose; if recoverable, retry recreate without rollback.`
17943
+ );
17944
+ process.exitCode = 1;
17945
+ return;
17946
+ }
17947
+ process.stdout.write(` ${pc15.dim("recreate auth-service".padEnd(34))}`);
17948
+ const authResult = await recreateAuthService();
17949
+ const authDur = `${(authResult.durationMs / 1e3).toFixed(1)}s`;
17950
+ process.stdout.write(`${authResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${authDur}
17951
+ `);
17952
+ timings.push({ label: "auth recreate", durationMs: authResult.durationMs });
17953
+ if (!authResult.ok) {
17954
+ printError(
17955
+ `Auth-service recreate failed: ${authResult.error ?? "unknown"}
17956
+
17957
+ Canonical tags advanced to new SHA; host-cp recreated but auth-service is broken.
17958
+ Recovery options:
17959
+ \u2022 Run \`olam upgrade --rollback\` to restore the prior :olam-rollback set + working stack.
17960
+ \u2022 Manually: \`docker logs olam-auth\` to diagnose; \`olam auth up\` to restart.`
17961
+ );
17119
17962
  process.exitCode = 1;
17120
17963
  return;
17121
17964
  }
@@ -17128,7 +17971,23 @@ ${composeResult.stderr}`);
17128
17971
  `);
17129
17972
  timings.push({ label: "/health", durationMs: healthDurationMs });
17130
17973
  if (!healthy) {
17131
- printWarning("Host CP started but /health did not respond within 10s. Check: docker logs olam-host-cp");
17974
+ printWarning(
17975
+ "Host CP started but /health did not respond within 10s.\n \u2022 Check: docker logs olam-host-cp\n \u2022 If the new SHA is broken: `olam upgrade --rollback` restores the prior set in <30s."
17976
+ );
17977
+ }
17978
+ process.stdout.write(` ${pc15.dim("verify /version/status round-trip".padEnd(34))}`);
17979
+ const versionStart = Date.now();
17980
+ const versionMatch = await waitForVersionMatch(_targetSha, 6e4);
17981
+ const versionDurationMs = Date.now() - versionStart;
17982
+ const versionDur = `${(versionDurationMs / 1e3).toFixed(1)}s`;
17983
+ process.stdout.write(`${versionMatch.matched ? pc15.green("\u2713") : pc15.yellow("?")} ${versionDur}
17984
+ `);
17985
+ timings.push({ label: "/version/status round-trip", durationMs: versionDurationMs });
17986
+ if (!versionMatch.matched) {
17987
+ printWarning(
17988
+ `Version round-trip incomplete after ${(versionDurationMs / 1e3).toFixed(0)}s:
17989
+ ` + formatVersionMismatch(_targetSha, versionMatch.snapshot) + "\n \u2022 Banner may still show UPDATE AVAILABLE until host-cp's next poll cycle (~60s).\n \u2022 If the mismatch persists, `olam upgrade --rollback` restores the prior :olam-rollback set."
17990
+ );
17132
17991
  }
17133
17992
  process.stdout.write("\n");
17134
17993
  printSuccess("Upgrade complete");
@@ -17145,10 +18004,22 @@ function printTimings2(timings) {
17145
18004
  printInfo("total", `${(total / 1e3).toFixed(1)}s`);
17146
18005
  }
17147
18006
  function registerUpgrade(program2) {
17148
- program2.command("upgrade").description("Self-upgrade the local Olam dev stack (pull + rebuild + restart host-cp)").option("-y, --yes", "Skip the confirmation prompt").option("--skip-image", "Skip docker image rebuild + container recreate (source rebuild only)").option(
18007
+ program2.command("upgrade").description("Self-upgrade the local Olam dev stack (pull + rebuild + restart all three components)").option("-y, --yes", "Skip the confirmation prompt").option("--skip-image", "Skip docker image rebuild + container recreate (source rebuild only)").option(
17149
18008
  "--skip-install",
17150
18009
  "Skip npm install entirely (use existing node_modules as-is). Useful when a native-module build failure blocks the normal upgrade path."
17151
- ).option("--branch <name>", "Switch to this branch before pulling (refuses if working tree is dirty)").action(async (opts) => {
18010
+ ).option("--branch <name>", "Switch to this branch before pulling (refuses if working tree is dirty)").option(
18011
+ "--rollback",
18012
+ "Restore canonical tags from the :olam-rollback set (created by the prior successful upgrade).\n No git pull, no build, no smoke \u2014 just retag + recreate."
18013
+ ).option(
18014
+ "--force",
18015
+ "Bypass HEAD-drift refusal at the swap boundary. Swap advances canonical to the\n captured-at-pull SHA even if current HEAD differs."
18016
+ ).option(
18017
+ "--no-cache",
18018
+ "Pass --no-cache to all three build scripts (DOCKER_BUILD_NO_CACHE=1).\n Useful when retrying after a cache-poisoning failure."
18019
+ ).option(
18020
+ "--history",
18021
+ "Print the upgrade history (~/.olam/upgrade.log) and exit.\n No upgrade is performed."
18022
+ ).option("-n <count>", "Number of history rows to print (default 10)", "10").option("--json", "Emit history as JSONL instead of a table").action(async (opts) => {
17152
18023
  await handleUpgrade(parseUpgradeOpts(opts));
17153
18024
  });
17154
18025
  }
@@ -17282,7 +18153,7 @@ function registerLogs(program2) {
17282
18153
  // src/commands/ps.ts
17283
18154
  init_context();
17284
18155
  import pc17 from "picocolors";
17285
- import { spawnSync as spawnSync7 } from "node:child_process";
18156
+ import { spawnSync as spawnSync8 } from "node:child_process";
17286
18157
  var SAFE_IDENT4 = /^[a-z0-9][a-z0-9-]{0,63}$/;
17287
18158
  function parseDockerTop(stdout) {
17288
18159
  const trimmed = stdout.trim();
@@ -17382,7 +18253,7 @@ function registerPs(program2) {
17382
18253
  const containerName = `olam-${worldId}-devbox`;
17383
18254
  let watchInterval;
17384
18255
  function fetchAndPrint() {
17385
- const result = spawnSync7(
18256
+ const result = spawnSync8(
17386
18257
  "docker",
17387
18258
  ["top", containerName, "pid", "user", "pcpu", "pmem", "stime", "stat", "cmd"],
17388
18259
  { encoding: "utf-8", timeout: 3e3 }
@@ -17418,20 +18289,20 @@ ${pc17.dim(`world: ${worldId} sort: ${sortKey} refresh: 5s Ctrl-C to exit`)}
17418
18289
  }
17419
18290
 
17420
18291
  // src/commands/keys.ts
17421
- import * as fs23 from "node:fs";
17422
- import * as os13 from "node:os";
17423
- import * as path27 from "node:path";
18292
+ import * as fs25 from "node:fs";
18293
+ import * as os15 from "node:os";
18294
+ import * as path29 from "node:path";
17424
18295
  import YAML4 from "yaml";
17425
18296
  function olamHome2() {
17426
- return process.env.OLAM_HOME ?? path27.join(os13.homedir(), ".olam");
18297
+ return process.env.OLAM_HOME ?? path29.join(os15.homedir(), ".olam");
17427
18298
  }
17428
18299
  function keysFilePath() {
17429
- return path27.join(olamHome2(), "keys.yaml");
18300
+ return path29.join(olamHome2(), "keys.yaml");
17430
18301
  }
17431
18302
  function readKeysFile() {
17432
18303
  const filePath = keysFilePath();
17433
- if (!fs23.existsSync(filePath)) return null;
17434
- const raw = fs23.readFileSync(filePath, "utf-8").trim();
18304
+ if (!fs25.existsSync(filePath)) return null;
18305
+ const raw = fs25.readFileSync(filePath, "utf-8").trim();
17435
18306
  if (raw.length === 0) return null;
17436
18307
  try {
17437
18308
  const parsed = YAML4.parse(raw);
@@ -17447,13 +18318,13 @@ function readKeysFile() {
17447
18318
  }
17448
18319
  function writeKeysFile(keys) {
17449
18320
  const dir = olamHome2();
17450
- if (!fs23.existsSync(dir)) {
17451
- fs23.mkdirSync(dir, { recursive: true });
18321
+ if (!fs25.existsSync(dir)) {
18322
+ fs25.mkdirSync(dir, { recursive: true });
17452
18323
  }
17453
18324
  const filePath = keysFilePath();
17454
18325
  const content = YAML4.stringify(keys);
17455
- fs23.writeFileSync(filePath, content, { encoding: "utf-8", mode: 384 });
17456
- fs23.chmodSync(filePath, 384);
18326
+ fs25.writeFileSync(filePath, content, { encoding: "utf-8", mode: 384 });
18327
+ fs25.chmodSync(filePath, 384);
17457
18328
  }
17458
18329
  function redact(value) {
17459
18330
  if (value.length <= 8) return value + "...";
@@ -17496,7 +18367,7 @@ function registerKeys(program2) {
17496
18367
  }
17497
18368
  const { [key]: _removed, ...rest } = existing;
17498
18369
  if (Object.keys(rest).length === 0) {
17499
- fs23.unlinkSync(keysFilePath());
18370
+ fs25.unlinkSync(keysFilePath());
17500
18371
  } else {
17501
18372
  writeKeysFile(rest);
17502
18373
  }
@@ -17519,26 +18390,26 @@ function registerKeys(program2) {
17519
18390
  }
17520
18391
 
17521
18392
  // src/commands/world-snapshot.ts
17522
- import * as fs25 from "node:fs";
17523
- import * as path29 from "node:path";
18393
+ import * as fs27 from "node:fs";
18394
+ import * as path31 from "node:path";
17524
18395
  import { execSync as execSync9 } from "node:child_process";
17525
18396
  import pc18 from "picocolors";
17526
18397
 
17527
18398
  // ../core/src/world/snapshot.ts
17528
18399
  import * as crypto6 from "node:crypto";
17529
- import * as fs24 from "node:fs";
17530
- import * as os14 from "node:os";
17531
- import * as path28 from "node:path";
18400
+ import * as fs26 from "node:fs";
18401
+ import * as os16 from "node:os";
18402
+ import * as path30 from "node:path";
17532
18403
  import { execFileSync as execFileSync4 } from "node:child_process";
17533
18404
  function snapshotsDir() {
17534
- return process.env["OLAM_SNAPSHOTS_DIR"] ?? path28.join(os14.homedir(), ".olam", "snapshots");
18405
+ return process.env["OLAM_SNAPSHOTS_DIR"] ?? path30.join(os16.homedir(), ".olam", "snapshots");
17535
18406
  }
17536
18407
  function snapshotKindDir(worldId, kind) {
17537
- return path28.join(snapshotsDir(), worldId, kind);
18408
+ return path30.join(snapshotsDir(), worldId, kind);
17538
18409
  }
17539
18410
  function snapshotTarPath(worldId, kind, repoName, hash) {
17540
18411
  const base = repoName ? `${repoName}-${hash}` : hash;
17541
- return path28.join(snapshotKindDir(worldId, kind), `${base}.tar.gz`);
18412
+ return path30.join(snapshotKindDir(worldId, kind), `${base}.tar.gz`);
17542
18413
  }
17543
18414
  function manifestPath(tarPath) {
17544
18415
  return tarPath.replace(/\.tar\.gz$/, ".manifest.json");
@@ -17555,16 +18426,16 @@ function hashBuffers(entries) {
17555
18426
  return hash.digest("hex").slice(0, 12);
17556
18427
  }
17557
18428
  function computeGemsFingerprint(repoDir) {
17558
- const lockfile = path28.join(repoDir, "Gemfile.lock");
17559
- if (!fs24.existsSync(lockfile)) return null;
17560
- return hashBuffers([{ path: "Gemfile.lock", content: fs24.readFileSync(lockfile) }]);
18429
+ const lockfile = path30.join(repoDir, "Gemfile.lock");
18430
+ if (!fs26.existsSync(lockfile)) return null;
18431
+ return hashBuffers([{ path: "Gemfile.lock", content: fs26.readFileSync(lockfile) }]);
17561
18432
  }
17562
18433
  function computeNodeFingerprint(repoDir) {
17563
18434
  const candidates = ["yarn.lock", "pnpm-lock.yaml", "package-lock.json"];
17564
18435
  for (const name of candidates) {
17565
- const lockfile = path28.join(repoDir, name);
17566
- if (fs24.existsSync(lockfile)) {
17567
- return hashBuffers([{ path: name, content: fs24.readFileSync(lockfile) }]);
18436
+ const lockfile = path30.join(repoDir, name);
18437
+ if (fs26.existsSync(lockfile)) {
18438
+ return hashBuffers([{ path: name, content: fs26.readFileSync(lockfile) }]);
17568
18439
  }
17569
18440
  }
17570
18441
  return null;
@@ -17574,59 +18445,59 @@ function computePgFingerprint(repoDirs) {
17574
18445
  const entries = [];
17575
18446
  for (const repoDir of repoDirs) {
17576
18447
  for (const pattern of patterns) {
17577
- const filePath = path28.join(repoDir, pattern);
17578
- if (fs24.existsSync(filePath)) {
17579
- entries.push({ path: filePath, content: fs24.readFileSync(filePath) });
18448
+ const filePath = path30.join(repoDir, pattern);
18449
+ if (fs26.existsSync(filePath)) {
18450
+ entries.push({ path: filePath, content: fs26.readFileSync(filePath) });
17580
18451
  }
17581
18452
  }
17582
18453
  }
17583
18454
  return entries.length > 0 ? hashBuffers(entries) : null;
17584
18455
  }
17585
18456
  function packTarball(srcDir, destPath, opts = {}) {
17586
- fs24.mkdirSync(path28.dirname(destPath), { recursive: true });
18457
+ fs26.mkdirSync(path30.dirname(destPath), { recursive: true });
17587
18458
  const tmp = `${destPath}.tmp`;
17588
18459
  const args = [];
17589
18460
  if (opts.followSymlinks) args.push("-h");
17590
18461
  args.push("-czf", tmp, "-C", srcDir, ".");
17591
18462
  try {
17592
18463
  execFileSync4("tar", args, { stdio: "pipe" });
17593
- fs24.renameSync(tmp, destPath);
18464
+ fs26.renameSync(tmp, destPath);
17594
18465
  } catch (err) {
17595
18466
  try {
17596
- fs24.rmSync(tmp, { force: true });
18467
+ fs26.rmSync(tmp, { force: true });
17597
18468
  } catch {
17598
18469
  }
17599
18470
  throw err;
17600
18471
  }
17601
18472
  }
17602
18473
  function writeManifest(manifest, tarPath) {
17603
- fs24.writeFileSync(manifestPath(tarPath), JSON.stringify(manifest, null, 2), "utf-8");
18474
+ fs26.writeFileSync(manifestPath(tarPath), JSON.stringify(manifest, null, 2), "utf-8");
17604
18475
  }
17605
18476
  function readManifest(tarPath) {
17606
18477
  const mPath = manifestPath(tarPath);
17607
- if (!fs24.existsSync(mPath)) return null;
18478
+ if (!fs26.existsSync(mPath)) return null;
17608
18479
  try {
17609
- return JSON.parse(fs24.readFileSync(mPath, "utf-8"));
18480
+ return JSON.parse(fs26.readFileSync(mPath, "utf-8"));
17610
18481
  } catch {
17611
18482
  return null;
17612
18483
  }
17613
18484
  }
17614
18485
  function listSnapshots(worldIdFilter) {
17615
18486
  const root = snapshotsDir();
17616
- if (!fs24.existsSync(root)) return [];
18487
+ if (!fs26.existsSync(root)) return [];
17617
18488
  const now = Date.now();
17618
18489
  const results = [];
17619
- const worlds = worldIdFilter ? [worldIdFilter] : fs24.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
18490
+ const worlds = worldIdFilter ? [worldIdFilter] : fs26.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
17620
18491
  for (const worldId of worlds) {
17621
- const worldDir = path28.join(root, worldId);
17622
- if (!fs24.existsSync(worldDir) || !fs24.statSync(worldDir).isDirectory()) continue;
18492
+ const worldDir = path30.join(root, worldId);
18493
+ if (!fs26.existsSync(worldDir) || !fs26.statSync(worldDir).isDirectory()) continue;
17623
18494
  for (const kind of ["gems", "node", "pg"]) {
17624
- const kindDir = path28.join(worldDir, kind);
17625
- if (!fs24.existsSync(kindDir)) continue;
17626
- const tarballs = fs24.readdirSync(kindDir).filter((f) => f.endsWith(".tar.gz"));
18495
+ const kindDir = path30.join(worldDir, kind);
18496
+ if (!fs26.existsSync(kindDir)) continue;
18497
+ const tarballs = fs26.readdirSync(kindDir).filter((f) => f.endsWith(".tar.gz"));
17627
18498
  for (const tarFile of tarballs) {
17628
- const tarPath = path28.join(kindDir, tarFile);
17629
- const stat = fs24.statSync(tarPath);
18499
+ const tarPath = path30.join(kindDir, tarFile);
18500
+ const stat = fs26.statSync(tarPath);
17630
18501
  const manifest = readManifest(tarPath);
17631
18502
  if (!manifest) continue;
17632
18503
  results.push({ manifest, tarPath, ageMs: now - stat.mtimeMs });
@@ -17705,17 +18576,17 @@ function resolveKinds(arg) {
17705
18576
  return [];
17706
18577
  }
17707
18578
  async function captureGems(worldId, workspacePath, repo) {
17708
- const repoDir = path29.join(workspacePath, repo);
18579
+ const repoDir = path31.join(workspacePath, repo);
17709
18580
  const fingerprint = computeGemsFingerprint(repoDir);
17710
18581
  if (!fingerprint) {
17711
18582
  return { ok: false, tarPath: "", msg: "no Gemfile.lock \u2014 layer does not apply" };
17712
18583
  }
17713
18584
  const tarPath = snapshotTarPath(worldId, "gems", repo, fingerprint);
17714
- const vendorBundle = path29.join(repoDir, "vendor", "bundle");
17715
- if (fs25.existsSync(vendorBundle)) {
18585
+ const vendorBundle = path31.join(repoDir, "vendor", "bundle");
18586
+ if (fs27.existsSync(vendorBundle)) {
17716
18587
  try {
17717
18588
  packTarball(vendorBundle, tarPath);
17718
- const stat = fs25.statSync(tarPath);
18589
+ const stat = fs27.statSync(tarPath);
17719
18590
  const manifest = {
17720
18591
  kind: "gems",
17721
18592
  worldId,
@@ -17748,10 +18619,10 @@ async function captureGems(worldId, workspacePath, repo) {
17748
18619
  `docker exec ${containerName} sh -c 'mkdir -p "$(dirname ${tmpTar})" && tar -czf ${tmpTar}.tmp -C ${bundlePath} . && mv ${tmpTar}.tmp ${tmpTar}'`,
17749
18620
  { stdio: "pipe", timeout: 12e4 }
17750
18621
  );
17751
- fs25.mkdirSync(path29.dirname(tarPath), { recursive: true });
18622
+ fs27.mkdirSync(path31.dirname(tarPath), { recursive: true });
17752
18623
  execSync9(`docker cp ${containerName}:${tmpTar} "${tarPath}"`, { stdio: "pipe", timeout: 12e4 });
17753
18624
  execSync9(`docker exec ${containerName} rm -f ${tmpTar}`, { stdio: "pipe" });
17754
- const stat = fs25.statSync(tarPath);
18625
+ const stat = fs27.statSync(tarPath);
17755
18626
  const manifest = {
17756
18627
  kind: "gems",
17757
18628
  worldId,
@@ -17768,19 +18639,19 @@ async function captureGems(worldId, workspacePath, repo) {
17768
18639
  }
17769
18640
  }
17770
18641
  async function captureNode(worldId, workspacePath, repo) {
17771
- const repoDir = path29.join(workspacePath, repo);
18642
+ const repoDir = path31.join(workspacePath, repo);
17772
18643
  const fingerprint = computeNodeFingerprint(repoDir);
17773
18644
  if (!fingerprint) {
17774
18645
  return { ok: false, tarPath: "", msg: "no lockfile \u2014 layer does not apply" };
17775
18646
  }
17776
- const nodeModules = path29.join(repoDir, "node_modules");
17777
- if (!fs25.existsSync(nodeModules)) {
18647
+ const nodeModules = path31.join(repoDir, "node_modules");
18648
+ if (!fs27.existsSync(nodeModules)) {
17778
18649
  return { ok: false, tarPath: "", msg: "node_modules not installed yet" };
17779
18650
  }
17780
18651
  const tarPath = snapshotTarPath(worldId, "node", repo, fingerprint);
17781
18652
  try {
17782
18653
  packTarball(nodeModules, tarPath);
17783
- const stat = fs25.statSync(tarPath);
18654
+ const stat = fs27.statSync(tarPath);
17784
18655
  const manifest = {
17785
18656
  kind: "node",
17786
18657
  worldId,
@@ -17797,7 +18668,7 @@ async function captureNode(worldId, workspacePath, repo) {
17797
18668
  }
17798
18669
  }
17799
18670
  async function capturePg(worldId, workspacePath, repoNames) {
17800
- const repoDirs = repoNames.map((r) => path29.join(workspacePath, r));
18671
+ const repoDirs = repoNames.map((r) => path31.join(workspacePath, r));
17801
18672
  const fingerprint = computePgFingerprint(repoDirs);
17802
18673
  if (!fingerprint) {
17803
18674
  return { ok: false, tarPath: "", msg: "no Gemfile.lock / schema.rb \u2014 layer does not apply" };
@@ -17812,13 +18683,13 @@ async function capturePg(worldId, workspacePath, repoNames) {
17812
18683
  }
17813
18684
  try {
17814
18685
  execSync9(`docker stop ${containerName}`, { stdio: "pipe", timeout: 3e4 });
17815
- fs25.mkdirSync(path29.dirname(tarPath), { recursive: true });
18686
+ fs27.mkdirSync(path31.dirname(tarPath), { recursive: true });
17816
18687
  execSync9(
17817
- `docker run --rm -v "${volumeName2}:/pgdata:ro" -v "${path29.dirname(tarPath)}:/dest" alpine sh -c 'tar -czf /dest/${path29.basename(tarPath)}.tmp -C /pgdata . && mv /dest/${path29.basename(tarPath)}.tmp /dest/${path29.basename(tarPath)}'`,
18688
+ `docker run --rm -v "${volumeName2}:/pgdata:ro" -v "${path31.dirname(tarPath)}:/dest" alpine sh -c 'tar -czf /dest/${path31.basename(tarPath)}.tmp -C /pgdata . && mv /dest/${path31.basename(tarPath)}.tmp /dest/${path31.basename(tarPath)}'`,
17818
18689
  { stdio: "pipe", timeout: 18e4 }
17819
18690
  );
17820
18691
  execSync9(`docker start ${containerName}`, { stdio: "pipe", timeout: 3e4 });
17821
- const stat = fs25.statSync(tarPath);
18692
+ const stat = fs27.statSync(tarPath);
17822
18693
  const manifest = {
17823
18694
  kind: "pg",
17824
18695
  worldId,
@@ -17892,35 +18763,35 @@ function formatAge2(ms) {
17892
18763
 
17893
18764
  // src/commands/refresh.ts
17894
18765
  init_context();
17895
- import * as fs27 from "node:fs";
17896
- import * as os15 from "node:os";
17897
- import * as path31 from "node:path";
17898
- import { spawnSync as spawnSync8 } from "node:child_process";
18766
+ import * as fs29 from "node:fs";
18767
+ import * as os17 from "node:os";
18768
+ import * as path33 from "node:path";
18769
+ import { spawnSync as spawnSync9 } from "node:child_process";
17899
18770
  import ora5 from "ora";
17900
18771
 
17901
18772
  // src/commands/refresh-helpers.ts
17902
- import * as fs26 from "node:fs";
17903
- import * as path30 from "node:path";
18773
+ import * as fs28 from "node:fs";
18774
+ import * as path32 from "node:path";
17904
18775
  function collectCpSourceFiles(standaloneDir) {
17905
- if (!fs26.existsSync(standaloneDir)) {
18776
+ if (!fs28.existsSync(standaloneDir)) {
17906
18777
  throw new Error(`CP standalone dir not found: ${standaloneDir}`);
17907
18778
  }
17908
18779
  const entries = [];
17909
- const topLevel = fs26.readdirSync(standaloneDir).filter((f) => {
17910
- const stat = fs26.statSync(path30.join(standaloneDir, f));
18780
+ const topLevel = fs28.readdirSync(standaloneDir).filter((f) => {
18781
+ const stat = fs28.statSync(path32.join(standaloneDir, f));
17911
18782
  return stat.isFile() && f.endsWith(".mjs") && !f.endsWith(".test.mjs");
17912
18783
  }).sort();
17913
18784
  for (const f of topLevel) {
17914
- entries.push({ srcPath: path30.join(standaloneDir, f), destRelPath: f });
18785
+ entries.push({ srcPath: path32.join(standaloneDir, f), destRelPath: f });
17915
18786
  }
17916
- const libDir = path30.join(standaloneDir, "lib");
17917
- if (fs26.existsSync(libDir) && fs26.statSync(libDir).isDirectory()) {
17918
- const libFiles = fs26.readdirSync(libDir).filter((f) => {
17919
- const stat = fs26.statSync(path30.join(libDir, f));
18787
+ const libDir = path32.join(standaloneDir, "lib");
18788
+ if (fs28.existsSync(libDir) && fs28.statSync(libDir).isDirectory()) {
18789
+ const libFiles = fs28.readdirSync(libDir).filter((f) => {
18790
+ const stat = fs28.statSync(path32.join(libDir, f));
17920
18791
  return stat.isFile() && f.endsWith(".mjs") && !f.endsWith(".test.mjs");
17921
18792
  }).sort();
17922
18793
  for (const f of libFiles) {
17923
- entries.push({ srcPath: path30.join(libDir, f), destRelPath: `lib/${f}` });
18794
+ entries.push({ srcPath: path32.join(libDir, f), destRelPath: `lib/${f}` });
17924
18795
  }
17925
18796
  }
17926
18797
  return entries;
@@ -17939,7 +18810,7 @@ var RESTART_TIMEOUT_S = 30;
17939
18810
  var HEALTH_POLL_MS = 500;
17940
18811
  var HEALTH_TIMEOUT_MS = 3e4;
17941
18812
  function docker(args) {
17942
- const result = spawnSync8("docker", args, {
18813
+ const result = spawnSync9("docker", args, {
17943
18814
  encoding: "utf-8",
17944
18815
  stdio: ["ignore", "pipe", "pipe"]
17945
18816
  });
@@ -17978,16 +18849,16 @@ async function refreshWorld(worldId, portOffset, standaloneDir, opts) {
17978
18849
  error: err instanceof Error ? err.message : String(err)
17979
18850
  };
17980
18851
  }
17981
- const stagingDir = fs27.mkdtempSync(
17982
- path31.join(os15.tmpdir(), `olam-refresh-${worldId}-`)
18852
+ const stagingDir = fs29.mkdtempSync(
18853
+ path33.join(os17.tmpdir(), `olam-refresh-${worldId}-`)
17983
18854
  );
17984
18855
  try {
17985
18856
  const hasLib = entries.some((e) => e.destRelPath.startsWith("lib/"));
17986
18857
  if (hasLib) {
17987
- fs27.mkdirSync(path31.join(stagingDir, "lib"), { recursive: true });
18858
+ fs29.mkdirSync(path33.join(stagingDir, "lib"), { recursive: true });
17988
18859
  }
17989
18860
  for (const { srcPath, destRelPath } of entries) {
17990
- fs27.copyFileSync(srcPath, path31.join(stagingDir, destRelPath));
18861
+ fs29.copyFileSync(srcPath, path33.join(stagingDir, destRelPath));
17991
18862
  }
17992
18863
  const cpResult = docker([
17993
18864
  "cp",
@@ -18002,7 +18873,7 @@ async function refreshWorld(worldId, portOffset, standaloneDir, opts) {
18002
18873
  };
18003
18874
  }
18004
18875
  } finally {
18005
- fs27.rmSync(stagingDir, { recursive: true, force: true });
18876
+ fs29.rmSync(stagingDir, { recursive: true, force: true });
18006
18877
  }
18007
18878
  if (opts.restart) {
18008
18879
  const restartResult = docker([
@@ -18039,11 +18910,11 @@ function registerRefresh(program2) {
18039
18910
  process.exitCode = 1;
18040
18911
  return;
18041
18912
  }
18042
- const standaloneDir = path31.join(
18913
+ const standaloneDir = path33.join(
18043
18914
  process.cwd(),
18044
18915
  "packages/control-plane/standalone"
18045
18916
  );
18046
- if (!fs27.existsSync(standaloneDir)) {
18917
+ if (!fs29.existsSync(standaloneDir)) {
18047
18918
  printError(
18048
18919
  `CP standalone source not found at ${standaloneDir}.
18049
18920
  Run \`olam refresh\` from the olam repo root.`
@@ -18121,6 +18992,25 @@ Run \`olam refresh\` from the olam repo root.`
18121
18992
  });
18122
18993
  }
18123
18994
 
18995
+ // src/pleri-config.ts
18996
+ import * as fs30 from "node:fs";
18997
+ import * as path34 from "node:path";
18998
+ function isPleriConfigured(configDir = process.env.OLAM_CONFIG_DIR ?? ".olam") {
18999
+ if (process.env.PLERI_BASE_URL) {
19000
+ return true;
19001
+ }
19002
+ const configPath = path34.join(configDir, "config.yaml");
19003
+ if (!fs30.existsSync(configPath)) {
19004
+ return false;
19005
+ }
19006
+ try {
19007
+ const contents = fs30.readFileSync(configPath, "utf8");
19008
+ return /^[^#\n]*\bpleri:/m.test(contents);
19009
+ } catch {
19010
+ return false;
19011
+ }
19012
+ }
19013
+
18124
19014
  // src/index.ts
18125
19015
  var program = new Command();
18126
19016
  program.name("olam").description("Olam \u2014 isolated development worlds with thought graph capture").version("0.1.0");
@@ -18134,7 +19024,7 @@ registerList(program);
18134
19024
  registerStatus(program);
18135
19025
  registerDestroy(program);
18136
19026
  registerEnter(program);
18137
- registerCrystallize(program);
19027
+ registerCrystallize(program, { hidden: !isPleriConfigured() });
18138
19028
  registerPr(program);
18139
19029
  registerWorkspace(program);
18140
19030
  registerHostCp(program);