@pleri/olam-cli 0.1.12 → 0.1.14

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 (86) hide show
  1. package/dist/__tests__/image-presence.test.d.ts +2 -0
  2. package/dist/__tests__/image-presence.test.d.ts.map +1 -0
  3. package/dist/__tests__/image-presence.test.js +44 -0
  4. package/dist/__tests__/image-presence.test.js.map +1 -0
  5. package/dist/__tests__/protocol-version.test.d.ts +2 -0
  6. package/dist/__tests__/protocol-version.test.d.ts.map +1 -0
  7. package/dist/__tests__/protocol-version.test.js +170 -0
  8. package/dist/__tests__/protocol-version.test.js.map +1 -0
  9. package/dist/__tests__/registry-allowlist.test.d.ts +2 -0
  10. package/dist/__tests__/registry-allowlist.test.d.ts.map +1 -0
  11. package/dist/__tests__/registry-allowlist.test.js +129 -0
  12. package/dist/__tests__/registry-allowlist.test.js.map +1 -0
  13. package/dist/commands/__tests__/upgrade.all-three.test.d.ts +19 -0
  14. package/dist/commands/__tests__/upgrade.all-three.test.d.ts.map +1 -0
  15. package/dist/commands/__tests__/upgrade.all-three.test.js +92 -0
  16. package/dist/commands/__tests__/upgrade.all-three.test.js.map +1 -0
  17. package/dist/commands/__tests__/upgrade.history.test.d.ts +15 -0
  18. package/dist/commands/__tests__/upgrade.history.test.d.ts.map +1 -0
  19. package/dist/commands/__tests__/upgrade.history.test.js +199 -0
  20. package/dist/commands/__tests__/upgrade.history.test.js.map +1 -0
  21. package/dist/commands/__tests__/upgrade.lock.test.d.ts +15 -0
  22. package/dist/commands/__tests__/upgrade.lock.test.d.ts.map +1 -0
  23. package/dist/commands/__tests__/upgrade.lock.test.js +253 -0
  24. package/dist/commands/__tests__/upgrade.lock.test.js.map +1 -0
  25. package/dist/commands/__tests__/upgrade.olam-tag.test.d.ts +21 -0
  26. package/dist/commands/__tests__/upgrade.olam-tag.test.d.ts.map +1 -0
  27. package/dist/commands/__tests__/upgrade.olam-tag.test.js +127 -0
  28. package/dist/commands/__tests__/upgrade.olam-tag.test.js.map +1 -0
  29. package/dist/commands/__tests__/upgrade.poll.test.d.ts +14 -0
  30. package/dist/commands/__tests__/upgrade.poll.test.d.ts.map +1 -0
  31. package/dist/commands/__tests__/upgrade.poll.test.js +136 -0
  32. package/dist/commands/__tests__/upgrade.poll.test.js.map +1 -0
  33. package/dist/commands/__tests__/upgrade.recreate.test.d.ts +17 -0
  34. package/dist/commands/__tests__/upgrade.recreate.test.d.ts.map +1 -0
  35. package/dist/commands/__tests__/upgrade.recreate.test.js +95 -0
  36. package/dist/commands/__tests__/upgrade.recreate.test.js.map +1 -0
  37. package/dist/commands/__tests__/upgrade.rollback.test.d.ts +12 -0
  38. package/dist/commands/__tests__/upgrade.rollback.test.d.ts.map +1 -0
  39. package/dist/commands/__tests__/upgrade.rollback.test.js +275 -0
  40. package/dist/commands/__tests__/upgrade.rollback.test.js.map +1 -0
  41. package/dist/commands/__tests__/upgrade.sha-capture.test.d.ts +12 -0
  42. package/dist/commands/__tests__/upgrade.sha-capture.test.d.ts.map +1 -0
  43. package/dist/commands/__tests__/upgrade.sha-capture.test.js +63 -0
  44. package/dist/commands/__tests__/upgrade.sha-capture.test.js.map +1 -0
  45. package/dist/commands/__tests__/upgrade.smoke.test.d.ts +19 -0
  46. package/dist/commands/__tests__/upgrade.smoke.test.d.ts.map +1 -0
  47. package/dist/commands/__tests__/upgrade.smoke.test.js +101 -0
  48. package/dist/commands/__tests__/upgrade.smoke.test.js.map +1 -0
  49. package/dist/commands/__tests__/upgrade.swap.test.d.ts +19 -0
  50. package/dist/commands/__tests__/upgrade.swap.test.d.ts.map +1 -0
  51. package/dist/commands/__tests__/upgrade.swap.test.js +333 -0
  52. package/dist/commands/__tests__/upgrade.swap.test.js.map +1 -0
  53. package/dist/commands/create.d.ts.map +1 -1
  54. package/dist/commands/create.js +31 -0
  55. package/dist/commands/create.js.map +1 -1
  56. package/dist/commands/upgrade-history.d.ts +17 -0
  57. package/dist/commands/upgrade-history.d.ts.map +1 -0
  58. package/dist/commands/upgrade-history.js +40 -0
  59. package/dist/commands/upgrade-history.js.map +1 -0
  60. package/dist/commands/upgrade-lock.d.ts +102 -0
  61. package/dist/commands/upgrade-lock.d.ts.map +1 -0
  62. package/dist/commands/upgrade-lock.js +225 -0
  63. package/dist/commands/upgrade-lock.js.map +1 -0
  64. package/dist/commands/upgrade-log.d.ts +86 -0
  65. package/dist/commands/upgrade-log.d.ts.map +1 -0
  66. package/dist/commands/upgrade-log.js +146 -0
  67. package/dist/commands/upgrade-log.js.map +1 -0
  68. package/dist/commands/upgrade.d.ts +265 -0
  69. package/dist/commands/upgrade.d.ts.map +1 -1
  70. package/dist/commands/upgrade.js +849 -13
  71. package/dist/commands/upgrade.js.map +1 -1
  72. package/dist/image-presence.d.ts +40 -0
  73. package/dist/image-presence.d.ts.map +1 -0
  74. package/dist/image-presence.js +39 -0
  75. package/dist/image-presence.js.map +1 -0
  76. package/dist/index.js +1043 -167
  77. package/dist/index.js.map +1 -1
  78. package/dist/protocol-version.d.ts +79 -0
  79. package/dist/protocol-version.d.ts.map +1 -0
  80. package/dist/protocol-version.js +133 -0
  81. package/dist/protocol-version.js.map +1 -0
  82. package/dist/registry-allowlist.d.ts +47 -0
  83. package/dist/registry-allowlist.d.ts.map +1 -0
  84. package/dist/registry-allowlist.js +67 -0
  85. package/dist/registry-allowlist.js.map +1 -0
  86. 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: path33, errorMaps, issueData } = params;
425
- const fullPath = [...path33, ...issueData.path || []];
424
+ const { data, path: path36, errorMaps, issueData } = params;
425
+ const fullPath = [...path36, ...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, path33, key) {
733
+ constructor(parent, value, path36, key) {
734
734
  this._cachedPath = [];
735
735
  this.parent = parent;
736
736
  this.data = value;
737
- this._path = path33;
737
+ this._path = path36;
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, path33, ctx, rejectSource) {
4224
+ function refineForbiddenKeys(value, path36, 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, path33, ctx, rejectSource) {
4229
4229
  if (FORBIDDEN_KEYS.has(key)) {
4230
4230
  ctx.addIssue({
4231
4231
  code: external_exports.ZodIssueCode.custom,
4232
- path: [...path33, key],
4232
+ path: [...path36, key],
4233
4233
  message: `forbidden key "${key}" (prototype-pollution surface)`
4234
4234
  });
4235
4235
  continue;
4236
4236
  }
4237
- if (rejectSource && path33.length === 0 && key === "source") {
4237
+ if (rejectSource && path36.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, path33, ctx, rejectSource) {
4244
4244
  }
4245
4245
  refineForbiddenKeys(
4246
4246
  value[key],
4247
- [...path33, key],
4247
+ [...path36, key],
4248
4248
  ctx,
4249
4249
  false
4250
4250
  );
4251
4251
  }
4252
4252
  }
4253
- function rejectForbiddenKeys(value, path33, rejectSource) {
4253
+ function rejectForbiddenKeys(value, path36, 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] ${path33}: forbidden key "${key}" (prototype-pollution surface)`
4260
+ `[manifest] ${path36}: forbidden key "${key}" (prototype-pollution surface)`
4261
4261
  );
4262
4262
  }
4263
4263
  if (rejectSource && key === "source") {
4264
4264
  throw new Error(
4265
- `[manifest] ${path33}: top-level "source" is loader-stamped \u2014 manifests must not author it`
4265
+ `[manifest] ${path36}: top-level "source" is loader-stamped \u2014 manifests must not author it`
4266
4266
  );
4267
4267
  }
4268
4268
  rejectForbiddenKeys(
4269
4269
  value[key],
4270
- `${path33}.${key}`,
4270
+ `${path36}.${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, path33, body, attempt = 0) {
5212
- const url = `${this.baseUrl}${path33}`;
5211
+ async request(method, path36, body, attempt = 0) {
5212
+ const url = `${this.baseUrl}${path36}`;
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, path33, body, attempt + 1);
5228
+ return this.request(method, path36, 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(path33, method, body) {
6680
- const url = `${this.config.workerUrl}${path33}`;
6679
+ async request(path36, method, body) {
6680
+ const url = `${this.config.workerUrl}${path36}`;
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, {
@@ -11843,6 +11898,9 @@ var init_checksum = __esm({
11843
11898
 
11844
11899
  // src/index.ts
11845
11900
  import { Command } from "commander";
11901
+ import * as fs31 from "node:fs";
11902
+ import * as path35 from "node:path";
11903
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
11846
11904
 
11847
11905
  // src/commands/init.ts
11848
11906
  import * as fs5 from "node:fs";
@@ -12249,11 +12307,11 @@ var UnknownArchetypeError = class extends Error {
12249
12307
  known;
12250
12308
  };
12251
12309
  var ArchetypeCycleError = class extends Error {
12252
- constructor(path33) {
12310
+ constructor(path36) {
12253
12311
  super(
12254
- `Archetype inheritance cycle detected: ${path33.join(" \u2192 ")} \u2192 ${path33[0] ?? "?"}`
12312
+ `Archetype inheritance cycle detected: ${path36.join(" \u2192 ")} \u2192 ${path36[0] ?? "?"}`
12255
12313
  );
12256
- this.path = path33;
12314
+ this.path = path36;
12257
12315
  this.name = "ArchetypeCycleError";
12258
12316
  }
12259
12317
  path;
@@ -13072,10 +13130,10 @@ async function readHostCpToken2() {
13072
13130
  if (!fs19.existsSync(tp)) return null;
13073
13131
  return fs19.readFileSync(tp, "utf-8").trim();
13074
13132
  }
13075
- async function callHostCpProxy(method, worldId, path33, body) {
13133
+ async function callHostCpProxy(method, worldId, path36, body) {
13076
13134
  const token = await readHostCpToken2();
13077
13135
  if (!token) return { ok: false, status: 0, error: "no token (host CP not started)" };
13078
- const url = `http://127.0.0.1:${HOST_CP_PORT}/api/world/${encodeURIComponent(worldId)}${path33}`;
13136
+ const url = `http://127.0.0.1:${HOST_CP_PORT}/api/world/${encodeURIComponent(worldId)}${path36}`;
13079
13137
  try {
13080
13138
  const headers = {
13081
13139
  Authorization: `Bearer ${token}`
@@ -13686,9 +13744,9 @@ function formatFreshnessWarning(result, image = DEFAULT_DEVBOX_IMAGE) {
13686
13744
  "These source files have changed since the image was built; the",
13687
13745
  "changes will NOT take effect in fresh worlds until you rebuild:"
13688
13746
  ];
13689
- for (const { path: path33, mtimeMs } of result.newerSources) {
13747
+ for (const { path: path36, mtimeMs } of result.newerSources) {
13690
13748
  const when = new Date(mtimeMs).toISOString();
13691
- lines.push(` \u2022 ${path33} (modified ${when})`);
13749
+ lines.push(` \u2022 ${path36} (modified ${when})`);
13692
13750
  }
13693
13751
  lines.push("");
13694
13752
  lines.push("Rebuild with:");
@@ -13848,15 +13906,15 @@ init_context();
13848
13906
  var HOST_CP_URL = "http://127.0.0.1:19000";
13849
13907
  async function readHostCpTokenForCreate() {
13850
13908
  try {
13851
- const { default: fs29 } = await import("node:fs");
13852
- const { default: os16 } = await import("node:os");
13853
- const { default: path33 } = await import("node:path");
13854
- const tp = path33.join(
13855
- process.env.OLAM_HOME ?? path33.join(os16.homedir(), ".olam"),
13909
+ const { default: fs32 } = await import("node:fs");
13910
+ const { default: os18 } = await import("node:os");
13911
+ const { default: path36 } = await import("node:path");
13912
+ const tp = path36.join(
13913
+ process.env.OLAM_HOME ?? path36.join(os18.homedir(), ".olam"),
13856
13914
  "host-cp.token"
13857
13915
  );
13858
- if (!fs29.existsSync(tp)) return null;
13859
- return fs29.readFileSync(tp, "utf-8").trim();
13916
+ if (!fs32.existsSync(tp)) return null;
13917
+ return fs32.readFileSync(tp, "utf-8").trim();
13860
13918
  } catch {
13861
13919
  return null;
13862
13920
  }
@@ -13865,7 +13923,23 @@ function registerCreate(program2) {
13865
13923
  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(
13866
13924
  "--allow-bootstrap-failure",
13867
13925
  "Treat bootstrap step failures as warnings instead of destroying the world (dogfood escape hatch for cross-repo seed coupling)"
13868
- ).action(async (opts) => {
13926
+ ).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) => {
13927
+ const { resolveDevboxImageOverride: resolveDevboxImageOverride2, decideAllowlist: decideAllowlist2 } = await Promise.resolve().then(() => (init_registry_allowlist(), registry_allowlist_exports));
13928
+ const overrideRef = resolveDevboxImageOverride2(opts.devboxImage);
13929
+ if (overrideRef) {
13930
+ const decision = decideAllowlist2({
13931
+ imageRef: overrideRef,
13932
+ allowCustomRegistry: opts.allowCustomRegistry === true
13933
+ });
13934
+ if (!decision.accepted) {
13935
+ process.stderr.write(decision.stderrLine + "\n");
13936
+ process.exitCode = 1;
13937
+ return;
13938
+ }
13939
+ if (decision.stderrLine) {
13940
+ process.stderr.write(decision.stderrLine + "\n");
13941
+ }
13942
+ }
13869
13943
  let resolvedName = opts.name;
13870
13944
  let resolvedWorkspace = opts.workspace;
13871
13945
  let resolvedRepos = opts.repos;
@@ -14170,12 +14244,12 @@ function defaultNameFromPrompt(prompt) {
14170
14244
  }
14171
14245
  async function readHostCpToken3() {
14172
14246
  try {
14173
- const { default: fs29 } = await import("node:fs");
14174
- const { default: os16 } = await import("node:os");
14175
- const { default: path33 } = await import("node:path");
14176
- const tp = path33.join(os16.homedir(), ".olam", "host-cp.token");
14177
- if (!fs29.existsSync(tp)) return null;
14178
- const raw = fs29.readFileSync(tp, "utf-8").trim();
14247
+ const { default: fs32 } = await import("node:fs");
14248
+ const { default: os18 } = await import("node:os");
14249
+ const { default: path36 } = await import("node:path");
14250
+ const tp = path36.join(os18.homedir(), ".olam", "host-cp.token");
14251
+ if (!fs32.existsSync(tp)) return null;
14252
+ const raw = fs32.readFileSync(tp, "utf-8").trim();
14179
14253
  return raw.length > 0 ? raw : null;
14180
14254
  } catch {
14181
14255
  return null;
@@ -16840,17 +16914,246 @@ function registerPolicyCheck(program2) {
16840
16914
  }
16841
16915
 
16842
16916
  // src/commands/upgrade.ts
16917
+ import * as fs24 from "node:fs";
16918
+ import * as path28 from "node:path";
16919
+ import { spawnSync as spawnSync7 } from "node:child_process";
16920
+ import pc15 from "picocolors";
16921
+
16922
+ // src/commands/upgrade-lock.ts
16843
16923
  import * as fs22 from "node:fs";
16924
+ import * as os13 from "node:os";
16844
16925
  import * as path26 from "node:path";
16845
16926
  import { spawnSync as spawnSync6 } from "node:child_process";
16846
- import pc15 from "picocolors";
16927
+ var LOCK_FILE_PATH = path26.join(os13.homedir(), ".olam", ".upgrade.lock");
16928
+ var STALE_LOCK_TIMEOUT_MS = 5 * 60 * 1e3;
16929
+ function readLockFile(lockPath) {
16930
+ try {
16931
+ if (!fs22.existsSync(lockPath)) return null;
16932
+ const raw = fs22.readFileSync(lockPath, "utf-8").trim();
16933
+ if (raw.length === 0) return null;
16934
+ const parsed = JSON.parse(raw);
16935
+ if (typeof parsed.pid !== "number" || typeof parsed.startTs !== "number") return null;
16936
+ return { pid: parsed.pid, startTs: parsed.startTs };
16937
+ } catch {
16938
+ return null;
16939
+ }
16940
+ }
16941
+ function isPidAlive(pid) {
16942
+ try {
16943
+ process.kill(pid, 0);
16944
+ return true;
16945
+ } catch {
16946
+ return false;
16947
+ }
16948
+ }
16949
+ var PS_UNAVAILABLE = "__ps_unavailable__";
16950
+ function getPidCommand(pid) {
16951
+ const result = spawnSync6("ps", ["-p", String(pid), "-o", "comm="], {
16952
+ encoding: "utf-8",
16953
+ stdio: ["ignore", "pipe", "ignore"]
16954
+ });
16955
+ if (result.status === null || result.error !== void 0) return PS_UNAVAILABLE;
16956
+ if (result.status !== 0) return null;
16957
+ const out = result.stdout.trim();
16958
+ return out.length === 0 ? null : out;
16959
+ }
16960
+ function isOlamUpgradeCommand(comm) {
16961
+ if (!comm) return false;
16962
+ if (comm === PS_UNAVAILABLE) return false;
16963
+ const base = comm.split("/").pop() ?? comm;
16964
+ const stripped = base.replace(/\s*\(.*\)\s*$/, "").trim();
16965
+ return stripped === "node" || stripped === "olam" || stripped === "olam-cli";
16966
+ }
16967
+ function isStaleLock(content, nowMs = Date.now()) {
16968
+ if (!content) return true;
16969
+ if (nowMs - content.startTs > STALE_LOCK_TIMEOUT_MS) return true;
16970
+ if (!isPidAlive(content.pid)) return true;
16971
+ const comm = getPidCommand(content.pid);
16972
+ if (comm === PS_UNAVAILABLE) return false;
16973
+ if (!isOlamUpgradeCommand(comm)) return true;
16974
+ return false;
16975
+ }
16976
+ function acquireLock(lockPath = LOCK_FILE_PATH, nowMs = Date.now()) {
16977
+ const dir = path26.dirname(lockPath);
16978
+ fs22.mkdirSync(dir, { recursive: true });
16979
+ for (let attempt = 0; attempt < 2; attempt++) {
16980
+ try {
16981
+ const fd = fs22.openSync(lockPath, "wx", 420);
16982
+ try {
16983
+ const content = { pid: process.pid, startTs: nowMs };
16984
+ fs22.writeSync(fd, JSON.stringify(content));
16985
+ } finally {
16986
+ fs22.closeSync(fd);
16987
+ }
16988
+ return { acquired: true, lockPath };
16989
+ } catch (err) {
16990
+ const code = err.code;
16991
+ if (code !== "EEXIST") throw err;
16992
+ const existing2 = readLockFile(lockPath);
16993
+ if (isStaleLock(existing2, nowMs)) {
16994
+ try {
16995
+ fs22.unlinkSync(lockPath);
16996
+ } catch (unlinkErr) {
16997
+ const ucode = unlinkErr.code;
16998
+ if (ucode !== "ENOENT") throw unlinkErr;
16999
+ }
17000
+ continue;
17001
+ }
17002
+ return {
17003
+ acquired: false,
17004
+ reason: "live",
17005
+ ...existing2?.pid !== void 0 && { existingPid: existing2.pid },
17006
+ ...existing2?.startTs !== void 0 && { existingStartTs: existing2.startTs }
17007
+ };
17008
+ }
17009
+ }
17010
+ const existing = readLockFile(lockPath);
17011
+ return {
17012
+ acquired: false,
17013
+ reason: "live",
17014
+ ...existing?.pid !== void 0 && { existingPid: existing.pid },
17015
+ ...existing?.startTs !== void 0 && { existingStartTs: existing.startTs }
17016
+ };
17017
+ }
17018
+ function releaseLock(lockPath = LOCK_FILE_PATH) {
17019
+ try {
17020
+ fs22.unlinkSync(lockPath);
17021
+ } catch (err) {
17022
+ const code = err.code;
17023
+ if (code !== "ENOENT") throw err;
17024
+ }
17025
+ }
17026
+ function formatRefusalMessage(result, lockPath = LOCK_FILE_PATH) {
17027
+ const pidStr = result.existingPid !== void 0 ? ` (pid ${result.existingPid})` : "";
17028
+ const lines = [
17029
+ `Upgrade in progress${pidStr}.`,
17030
+ "Wait for the running upgrade to finish, or:",
17031
+ " - Check progress: olam upgrade --history",
17032
+ ` - If stale (crashed CLI): rm ${lockPath}`
17033
+ ];
17034
+ return lines.join("\n");
17035
+ }
17036
+
17037
+ // src/commands/upgrade-log.ts
17038
+ import * as fs23 from "node:fs";
17039
+ import * as os14 from "node:os";
17040
+ import * as path27 from "node:path";
17041
+ function getUpgradeLogPath() {
17042
+ const home = process.env["HOME"] ?? os14.homedir();
17043
+ return path27.join(home, ".olam", "upgrade.log");
17044
+ }
17045
+ var UPGRADE_LOG_PATH = getUpgradeLogPath();
17046
+ function appendUpgradeLog(row, logPath = getUpgradeLogPath()) {
17047
+ try {
17048
+ fs23.mkdirSync(path27.dirname(logPath), { recursive: true });
17049
+ const line = JSON.stringify(row) + "\n";
17050
+ fs23.appendFileSync(logPath, line, { mode: 420 });
17051
+ } catch (err) {
17052
+ process.stderr.write(
17053
+ `[upgrade-log] failed to append: ${err instanceof Error ? err.message : String(err)}
17054
+ `
17055
+ );
17056
+ }
17057
+ }
17058
+ function readUpgradeLog(limit = 10, logPath = getUpgradeLogPath()) {
17059
+ if (!fs23.existsSync(logPath)) return [];
17060
+ let raw;
17061
+ try {
17062
+ raw = fs23.readFileSync(logPath, "utf-8");
17063
+ } catch (err) {
17064
+ process.stderr.write(
17065
+ `[upgrade-log] failed to read: ${err instanceof Error ? err.message : String(err)}
17066
+ `
17067
+ );
17068
+ return [];
17069
+ }
17070
+ const lines = raw.split("\n").filter((l) => l.length > 0);
17071
+ const rows = [];
17072
+ for (let i = 0; i < lines.length; i++) {
17073
+ const line = lines[i];
17074
+ try {
17075
+ const parsed = JSON.parse(line);
17076
+ if (typeof parsed.ts === "string" && typeof parsed.started_at === "number" && typeof parsed.status === "string") {
17077
+ rows.push(parsed);
17078
+ } else {
17079
+ process.stderr.write(`[upgrade-log] skipped malformed row at line ${i + 1}
17080
+ `);
17081
+ }
17082
+ } catch {
17083
+ process.stderr.write(`[upgrade-log] skipped corrupt JSON at line ${i + 1}
17084
+ `);
17085
+ }
17086
+ }
17087
+ return rows.slice(-Math.max(0, limit));
17088
+ }
17089
+ function formatDuration(ms) {
17090
+ if (ms < 1e3) return `${ms}ms`;
17091
+ const totalSec = Math.round(ms / 1e3);
17092
+ if (totalSec < 60) return `${totalSec}s`;
17093
+ const min = Math.floor(totalSec / 60);
17094
+ const sec = totalSec % 60;
17095
+ if (min < 60) return `${min}m${String(sec).padStart(2, "0")}s`;
17096
+ const hr = Math.floor(min / 60);
17097
+ const remMin = min % 60;
17098
+ return `${hr}h${String(remMin).padStart(2, "0")}m`;
17099
+ }
17100
+ function formatHistoryTable(rows) {
17101
+ if (rows.length === 0) {
17102
+ return "No upgrade history yet. Run `olam upgrade` to create your first record.";
17103
+ }
17104
+ const lines = [];
17105
+ lines.push("TIMESTAMP SHA STATUS DURATION FAILED-STEP");
17106
+ 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");
17107
+ for (const r of rows) {
17108
+ const ts = r.ts.slice(0, 19).replace("T", " ");
17109
+ const sha = r.sha_target.slice(0, 8);
17110
+ const statusIcon = r.status === "success" ? "\u2713 success" : r.status === "rolled_back" ? "\u21A9 rolled_back" : "\u2717 failed";
17111
+ const dur = formatDuration(r.ended_at - r.started_at);
17112
+ const failed = r.failed_step ?? "";
17113
+ lines.push(
17114
+ `${ts.padEnd(28)}${sha.padEnd(10)}${statusIcon.padEnd(15)}${dur.padEnd(11)}${failed}`
17115
+ );
17116
+ }
17117
+ return lines.join("\n");
17118
+ }
17119
+ function formatHistoryJson(rows) {
17120
+ return rows.map((r) => JSON.stringify(r)).join("\n");
17121
+ }
17122
+
17123
+ // src/commands/upgrade-history.ts
17124
+ function parseHistoryOpts(raw) {
17125
+ const rawLimit = raw.n;
17126
+ const limit = typeof rawLimit === "number" ? rawLimit : typeof rawLimit === "string" ? Number.parseInt(rawLimit, 10) : 10;
17127
+ return {
17128
+ limit: Number.isFinite(limit) && limit > 0 ? limit : 10,
17129
+ json: raw.json === true
17130
+ };
17131
+ }
17132
+ function handleHistory(opts) {
17133
+ const rows = readUpgradeLog(opts.limit);
17134
+ if (opts.json) {
17135
+ process.stdout.write(formatHistoryJson(rows) + "\n");
17136
+ return;
17137
+ }
17138
+ if (rows.length === 0) {
17139
+ printInfo("Log file", UPGRADE_LOG_PATH);
17140
+ process.stdout.write(formatHistoryTable(rows) + "\n");
17141
+ return;
17142
+ }
17143
+ printInfo("Log file", UPGRADE_LOG_PATH);
17144
+ process.stdout.write(formatHistoryTable(rows) + "\n");
17145
+ }
17146
+
17147
+ // src/commands/upgrade.ts
17148
+ init_auth();
17149
+ var AUTH_HEALTH_URL2 = "http://127.0.0.1:9999/health";
16847
17150
  function isNodeModulesInSync(cwd) {
16848
- const lockPath = path26.join(cwd, "package-lock.json");
16849
- const markerPath = path26.join(cwd, "node_modules", ".package-lock.json");
16850
- if (!fs22.existsSync(lockPath) || !fs22.existsSync(markerPath)) return false;
17151
+ const lockPath = path28.join(cwd, "package-lock.json");
17152
+ const markerPath = path28.join(cwd, "node_modules", ".package-lock.json");
17153
+ if (!fs24.existsSync(lockPath) || !fs24.existsSync(markerPath)) return false;
16851
17154
  try {
16852
- const lockStat = fs22.statSync(lockPath);
16853
- const markerStat = fs22.statSync(markerPath);
17155
+ const lockStat = fs24.statSync(lockPath);
17156
+ const markerStat = fs24.statSync(markerPath);
16854
17157
  return markerStat.mtimeMs >= lockStat.mtimeMs;
16855
17158
  } catch {
16856
17159
  return false;
@@ -16866,8 +17169,8 @@ function shouldSkipInstall(opts, cwd) {
16866
17169
  return { skip: false };
16867
17170
  }
16868
17171
  function validateRepoRoot(cwd) {
16869
- const marker = path26.join(cwd, "packages/host-cp/compose.yaml");
16870
- if (!fs22.existsSync(marker)) {
17172
+ const marker = path28.join(cwd, "packages/host-cp/compose.yaml");
17173
+ if (!fs24.existsSync(marker)) {
16871
17174
  return {
16872
17175
  ok: false,
16873
17176
  error: `Not an olam repo root (expected ${marker}).
@@ -16877,11 +17180,19 @@ Run \`olam upgrade\` from the root of your olam checkout.`
16877
17180
  return { ok: true };
16878
17181
  }
16879
17182
  function parseUpgradeOpts(raw) {
17183
+ const rawN = raw.n;
17184
+ const historyN = typeof rawN === "number" ? rawN : typeof rawN === "string" ? Number.parseInt(rawN, 10) : 10;
16880
17185
  return {
16881
17186
  yes: raw.yes === true,
16882
17187
  skipImage: raw.skipImage === true,
16883
17188
  skipInstall: raw.skipInstall === true,
16884
- branch: raw.branch ?? null
17189
+ branch: raw.branch ?? null,
17190
+ rollback: raw.rollback === true,
17191
+ force: raw.force === true,
17192
+ noCache: raw.noCache === true,
17193
+ history: raw.history === true,
17194
+ historyN: Number.isFinite(historyN) && historyN > 0 ? historyN : 10,
17195
+ historyJson: raw.json === true
16885
17196
  };
16886
17197
  }
16887
17198
  function extractBundleHash(indexHtml) {
@@ -16891,7 +17202,7 @@ function extractBundleHash(indexHtml) {
16891
17202
  function runStep2(label, cmd, args, opts = {}) {
16892
17203
  const start = Date.now();
16893
17204
  process.stdout.write(` ${pc15.dim(label.padEnd(34))}`);
16894
- const result = spawnSync6(cmd, [...args], {
17205
+ const result = spawnSync7(cmd, [...args], {
16895
17206
  encoding: "utf-8",
16896
17207
  stdio: ["ignore", "pipe", "pipe"],
16897
17208
  cwd: opts.cwd ?? process.cwd(),
@@ -16910,7 +17221,7 @@ function runStep2(label, cmd, args, opts = {}) {
16910
17221
  };
16911
17222
  }
16912
17223
  function isGitDirty(cwd) {
16913
- const result = spawnSync6("git", ["status", "--porcelain"], {
17224
+ const result = spawnSync7("git", ["status", "--porcelain"], {
16914
17225
  encoding: "utf-8",
16915
17226
  stdio: ["ignore", "pipe", "pipe"],
16916
17227
  cwd
@@ -16918,13 +17229,194 @@ function isGitDirty(cwd) {
16918
17229
  return (result.stdout ?? "").trim().length > 0;
16919
17230
  }
16920
17231
  function hasGitUpstream(cwd) {
16921
- const result = spawnSync6("git", ["rev-parse", "--abbrev-ref", "@{u}"], {
17232
+ const result = spawnSync7("git", ["rev-parse", "--abbrev-ref", "@{u}"], {
16922
17233
  encoding: "utf-8",
16923
17234
  stdio: ["ignore", "pipe", "pipe"],
16924
17235
  cwd
16925
17236
  });
16926
17237
  return result.status === 0;
16927
17238
  }
17239
+ function captureHeadSha(cwd) {
17240
+ const result = spawnSync7("git", ["rev-parse", "HEAD"], {
17241
+ encoding: "utf-8",
17242
+ stdio: ["ignore", "pipe", "pipe"],
17243
+ cwd
17244
+ });
17245
+ if (result.status !== 0) return null;
17246
+ const sha = (result.stdout ?? "").trim();
17247
+ if (!/^[0-9a-f]{40}$/.test(sha)) return null;
17248
+ return sha;
17249
+ }
17250
+ function abbreviateSha(sha) {
17251
+ return sha.slice(0, 8);
17252
+ }
17253
+ function imageExists(tag) {
17254
+ try {
17255
+ const result = spawnSync7("docker", ["image", "inspect", "--format", "{{.Id}}", tag], {
17256
+ encoding: "utf-8",
17257
+ stdio: ["ignore", "pipe", "ignore"]
17258
+ });
17259
+ return result.status === 0;
17260
+ } catch {
17261
+ return false;
17262
+ }
17263
+ }
17264
+ function checkRollbackSetExists(plan) {
17265
+ const missing = plan.filter((p) => !imageExists(p.rollback)).map((p) => p.rollback);
17266
+ if (missing.length === 0) return null;
17267
+ return missing.join(", ");
17268
+ }
17269
+ function smokeImage(image, targetSha) {
17270
+ const createResult = spawnSync7("docker", ["create", "--name", `olam-smoke-${Date.now()}`, image], {
17271
+ encoding: "utf-8",
17272
+ stdio: ["ignore", "pipe", "pipe"]
17273
+ });
17274
+ if (createResult.status !== 0) {
17275
+ return {
17276
+ image,
17277
+ ok: false,
17278
+ bakedSha: null,
17279
+ error: `docker create failed: ${(createResult.stderr ?? "").trim()}`
17280
+ };
17281
+ }
17282
+ const containerId = (createResult.stdout ?? "").trim();
17283
+ const inspectResult = spawnSync7(
17284
+ "docker",
17285
+ ["inspect", "--format", '{{index .Config.Labels "olam_build_sha"}}', image],
17286
+ {
17287
+ encoding: "utf-8",
17288
+ stdio: ["ignore", "pipe", "pipe"]
17289
+ }
17290
+ );
17291
+ if (containerId.length > 0) {
17292
+ spawnSync7("docker", ["rm", "-f", containerId], {
17293
+ encoding: "utf-8",
17294
+ stdio: ["ignore", "ignore", "ignore"]
17295
+ });
17296
+ }
17297
+ if (inspectResult.status !== 0) {
17298
+ return {
17299
+ image,
17300
+ ok: false,
17301
+ bakedSha: null,
17302
+ error: `docker inspect failed: ${(inspectResult.stderr ?? "").trim()}`
17303
+ };
17304
+ }
17305
+ const bakedSha = (inspectResult.stdout ?? "").trim();
17306
+ if (bakedSha.length === 0) {
17307
+ return {
17308
+ image,
17309
+ ok: false,
17310
+ bakedSha: null,
17311
+ error: "olam_build_sha label is missing or empty"
17312
+ };
17313
+ }
17314
+ if (bakedSha !== targetSha) {
17315
+ return {
17316
+ image,
17317
+ ok: false,
17318
+ bakedSha,
17319
+ error: `baked SHA ${abbreviateSha(bakedSha)} \u2260 target SHA ${abbreviateSha(targetSha)}`
17320
+ };
17321
+ }
17322
+ return { image, ok: true, bakedSha };
17323
+ }
17324
+ var PRODUCTION_SWAP_PLAN = [
17325
+ { transient: "olam-auth:olam-next", canonical: "olam-auth:local", rollback: "olam-auth:olam-rollback" },
17326
+ { transient: "olam-devbox:olam-next", canonical: "olam-devbox:latest", rollback: "olam-devbox:olam-rollback" },
17327
+ { transient: "olam-host-cp:olam-next", canonical: "olam-host-cp:latest", rollback: "olam-host-cp:olam-rollback" }
17328
+ ];
17329
+ function dockerTag(source, dest) {
17330
+ try {
17331
+ const result = spawnSync7("docker", ["tag", source, dest], {
17332
+ encoding: "utf-8",
17333
+ stdio: ["ignore", "ignore", "pipe"]
17334
+ });
17335
+ if (result.status === 0 && result.error === void 0) return { ok: true };
17336
+ return {
17337
+ ok: false,
17338
+ error: (result.stderr ?? "").trim() || result.error?.message || "docker tag failed"
17339
+ };
17340
+ } catch (err) {
17341
+ return {
17342
+ ok: false,
17343
+ error: err instanceof Error ? `spawnSync threw: ${err.message}` : "spawnSync threw"
17344
+ };
17345
+ }
17346
+ }
17347
+ function performAtomicSwap(plan) {
17348
+ const steps = plan.map((p) => ({
17349
+ image: p.canonical,
17350
+ rollbackSaved: false,
17351
+ canonicalAdvanced: false
17352
+ }));
17353
+ for (let i = 0; i < plan.length; i++) {
17354
+ const p = plan[i];
17355
+ const r = dockerTag(p.canonical, p.rollback);
17356
+ steps[i] = {
17357
+ ...steps[i],
17358
+ rollbackSaved: r.ok,
17359
+ ...r.error !== void 0 && { rollbackError: r.error }
17360
+ };
17361
+ }
17362
+ let advanceFailed = false;
17363
+ let firstFailureIdx = -1;
17364
+ for (let i = 0; i < plan.length; i++) {
17365
+ const p = plan[i];
17366
+ if (advanceFailed) {
17367
+ steps[i] = { ...steps[i], canonicalAdvanced: false };
17368
+ continue;
17369
+ }
17370
+ const r = dockerTag(p.transient, p.canonical);
17371
+ steps[i] = {
17372
+ ...steps[i],
17373
+ canonicalAdvanced: r.ok,
17374
+ ...r.error !== void 0 && { canonicalError: r.error }
17375
+ };
17376
+ if (!r.ok) {
17377
+ advanceFailed = true;
17378
+ firstFailureIdx = i;
17379
+ }
17380
+ }
17381
+ const allAdvanced = steps.every((s) => s.canonicalAdvanced);
17382
+ const noneAdvanced = steps.every((s) => !s.canonicalAdvanced);
17383
+ const partialAdvance = !allAdvanced && !noneAdvanced;
17384
+ const rollbackCoherent = steps.every((s) => s.rollbackSaved);
17385
+ let summary;
17386
+ if (allAdvanced) {
17387
+ const rollbacks = steps.filter((s) => s.rollbackSaved).length;
17388
+ summary = `Swapped ${plan.length} canonical tags; ${rollbacks} :olam-rollback preserved`;
17389
+ } else if (partialAdvance) {
17390
+ const advanced = steps.filter((s) => s.canonicalAdvanced).length;
17391
+ const failedStep = steps[firstFailureIdx];
17392
+ 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.`;
17393
+ summary = `PARTIAL: ${advanced} of ${plan.length} canonical tags advanced before failure on ${failedStep?.image}: ${failedStep?.canonicalError}. ${recoveryHint}`;
17394
+ } else {
17395
+ const failedStep = steps[firstFailureIdx];
17396
+ summary = `Failed on first canonical-advance (${failedStep?.image}): ${failedStep?.canonicalError}. Canonical tags untouched.`;
17397
+ }
17398
+ return {
17399
+ ok: allAdvanced,
17400
+ steps,
17401
+ partialAdvance,
17402
+ rollbackCoherent,
17403
+ summary
17404
+ };
17405
+ }
17406
+ function performRollbackSwap(plan) {
17407
+ const results = [];
17408
+ for (const p of plan) {
17409
+ const r = dockerTag(p.rollback, p.canonical);
17410
+ results.push({
17411
+ image: p.canonical,
17412
+ ok: r.ok,
17413
+ ...r.error !== void 0 && { error: r.error }
17414
+ });
17415
+ }
17416
+ const allOk = results.every((r) => r.ok);
17417
+ 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(", ")}`;
17418
+ return { ok: allOk, results, summary };
17419
+ }
16928
17420
  async function confirm2(message) {
16929
17421
  if (!process.stdin.isTTY) return true;
16930
17422
  const { createInterface: createInterface2 } = await import("node:readline");
@@ -16950,10 +17442,87 @@ async function waitForHealth(timeoutMs = 1e4) {
16950
17442
  }
16951
17443
  return false;
16952
17444
  }
17445
+ async function waitForVersionMatch(targetSha, timeoutMs = 6e4, pollIntervalMs = 1e3) {
17446
+ const deadline = Date.now() + timeoutMs;
17447
+ let lastSnapshot = null;
17448
+ while (Date.now() < deadline) {
17449
+ try {
17450
+ const res = await fetch("http://127.0.0.1:19000/api/version/status", {
17451
+ signal: AbortSignal.timeout(2e3)
17452
+ });
17453
+ if (res.ok) {
17454
+ const snapshot = await res.json();
17455
+ lastSnapshot = snapshot;
17456
+ if (snapshot.hostCp?.running === targetSha && snapshot.authService?.running === targetSha && snapshot.devbox?.running === targetSha) {
17457
+ return { matched: true, snapshot };
17458
+ }
17459
+ }
17460
+ } catch {
17461
+ }
17462
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
17463
+ }
17464
+ return { matched: false, snapshot: lastSnapshot };
17465
+ }
17466
+ function formatVersionMismatch(targetSha, snapshot) {
17467
+ if (!snapshot) return "No /api/version/status response received within timeout.";
17468
+ const lines = [];
17469
+ for (const [name, comp] of [
17470
+ ["host-cp", snapshot.hostCp],
17471
+ ["auth-service", snapshot.authService],
17472
+ ["devbox", snapshot.devbox]
17473
+ ]) {
17474
+ const match2 = comp?.running === targetSha;
17475
+ lines.push(` ${match2 ? "\u2713" : "\u2717"} ${name}: running=${abbreviateSha(comp?.running ?? "unknown")} target=${abbreviateSha(targetSha)}`);
17476
+ }
17477
+ return lines.join("\n");
17478
+ }
17479
+ async function waitForAuthHealthLocal(timeoutMs = 15e3) {
17480
+ const deadline = Date.now() + timeoutMs;
17481
+ while (Date.now() < deadline) {
17482
+ try {
17483
+ const res = await fetch(AUTH_HEALTH_URL2, { signal: AbortSignal.timeout(2e3) });
17484
+ if (res.ok) return true;
17485
+ } catch {
17486
+ }
17487
+ await new Promise((r) => setTimeout(r, 500));
17488
+ }
17489
+ return false;
17490
+ }
17491
+ async function recreateAuthService() {
17492
+ const start = Date.now();
17493
+ try {
17494
+ spawnSync7("docker", ["stop", "olam-auth"], {
17495
+ encoding: "utf-8",
17496
+ stdio: ["ignore", "ignore", "ignore"]
17497
+ });
17498
+ spawnSync7("docker", ["rm", "olam-auth"], {
17499
+ encoding: "utf-8",
17500
+ stdio: ["ignore", "ignore", "ignore"]
17501
+ });
17502
+ const controller = new AuthContainerController();
17503
+ controller.start();
17504
+ const healthy = await waitForAuthHealthLocal(15e3);
17505
+ const durationMs = Date.now() - start;
17506
+ if (!healthy) {
17507
+ return {
17508
+ ok: false,
17509
+ durationMs,
17510
+ error: "auth-service /health did not respond within 15s after recreate"
17511
+ };
17512
+ }
17513
+ return { ok: true, durationMs };
17514
+ } catch (err) {
17515
+ return {
17516
+ ok: false,
17517
+ durationMs: Date.now() - start,
17518
+ error: err instanceof Error ? err.message : String(err)
17519
+ };
17520
+ }
17521
+ }
16953
17522
  function readBundleHash(cwd) {
16954
- const indexPath = path26.join(cwd, "packages/control-plane/public/index.html");
16955
- if (!fs22.existsSync(indexPath)) return null;
16956
- return extractBundleHash(fs22.readFileSync(indexPath, "utf-8"));
17523
+ const indexPath = path28.join(cwd, "packages/control-plane/public/index.html");
17524
+ if (!fs24.existsSync(indexPath)) return null;
17525
+ return extractBundleHash(fs24.readFileSync(indexPath, "utf-8"));
16957
17526
  }
16958
17527
  async function handleUpgrade(opts) {
16959
17528
  const cwd = process.cwd();
@@ -16963,6 +17532,10 @@ async function handleUpgrade(opts) {
16963
17532
  process.exitCode = 1;
16964
17533
  return;
16965
17534
  }
17535
+ if (opts.history) {
17536
+ handleHistory(parseHistoryOpts({ n: opts.historyN, json: opts.historyJson }));
17537
+ return;
17538
+ }
16966
17539
  printHeader("olam upgrade");
16967
17540
  const steps = [
16968
17541
  "git fetch origin --prune",
@@ -16971,9 +17544,13 @@ async function handleUpgrade(opts) {
16971
17544
  "npm run build (TS workspaces)",
16972
17545
  "vite build (SPA)",
16973
17546
  ...opts.skipImage ? [] : [
16974
- "bash build-host-cp.sh (docker image)",
16975
- "docker compose up -d --force-recreate",
16976
- "wait for /health"
17547
+ "bash build-auth.sh (auth-service image)",
17548
+ "bash build-devbox.sh (devbox image)",
17549
+ "bash build-host-cp.sh (host-cp image)",
17550
+ "smoke (docker create + inspect)",
17551
+ "atomic 6-tag swap (canonical -> :olam-rollback; :olam-next -> canonical)",
17552
+ "docker compose --force-recreate host-cp + AuthContainerController.start auth",
17553
+ "poll /api/version/status until SHAs match"
16977
17554
  ]
16978
17555
  ];
16979
17556
  printInfo("Steps", steps.join(", "));
@@ -16988,6 +17565,138 @@ async function handleUpgrade(opts) {
16988
17565
  return;
16989
17566
  }
16990
17567
  }
17568
+ if (opts.rollback) {
17569
+ return await handleRollback();
17570
+ }
17571
+ const lock = acquireLock();
17572
+ if (!lock.acquired) {
17573
+ printError(formatRefusalMessage(lock, LOCK_FILE_PATH));
17574
+ process.exitCode = 1;
17575
+ return;
17576
+ }
17577
+ let signalReleased = false;
17578
+ const releaseOnSignal = (signal) => {
17579
+ if (signalReleased) return;
17580
+ signalReleased = true;
17581
+ try {
17582
+ releaseLock();
17583
+ } catch {
17584
+ }
17585
+ process.exit(signal === "SIGINT" ? 130 : 143);
17586
+ };
17587
+ process.once("SIGINT", releaseOnSignal);
17588
+ process.once("SIGTERM", releaseOnSignal);
17589
+ const logRow = {
17590
+ started_at: Date.now(),
17591
+ durations_ms: {},
17592
+ sha_target: "",
17593
+ failed_step: null,
17594
+ status: "failed"
17595
+ // default; flipped to 'success' on clean exit
17596
+ };
17597
+ try {
17598
+ await runUpgradeStepsWithLockHeld(opts, cwd, logRow);
17599
+ if (process.exitCode !== 1) logRow.status = "success";
17600
+ } finally {
17601
+ const ended_at = Date.now();
17602
+ const row = {
17603
+ ts: new Date(ended_at).toISOString(),
17604
+ started_at: logRow.started_at,
17605
+ ended_at,
17606
+ sha_target: logRow.sha_target,
17607
+ status: logRow.status,
17608
+ failed_step: logRow.failed_step,
17609
+ durations_ms: logRow.durations_ms
17610
+ };
17611
+ appendUpgradeLog(row);
17612
+ releaseLock();
17613
+ process.removeListener("SIGINT", releaseOnSignal);
17614
+ process.removeListener("SIGTERM", releaseOnSignal);
17615
+ }
17616
+ }
17617
+ async function handleRollback() {
17618
+ printHeader("olam upgrade --rollback");
17619
+ const missing = checkRollbackSetExists(PRODUCTION_SWAP_PLAN);
17620
+ if (missing !== null) {
17621
+ printError(
17622
+ `No rollback-set available \u2014 missing :olam-rollback tag(s): ${missing}
17623
+
17624
+ A rollback-set is created by the FIRST successful \`olam upgrade\`. If this
17625
+ is your first install, run \`olam upgrade\` to populate the rollback set.
17626
+ If a previous upgrade was incomplete, the rollback set may be partial;
17627
+ manually inspect images with \`docker images olam-*:olam-rollback\`.`
17628
+ );
17629
+ process.exitCode = 1;
17630
+ return;
17631
+ }
17632
+ const lock = acquireLock();
17633
+ if (!lock.acquired) {
17634
+ printError(formatRefusalMessage(lock, LOCK_FILE_PATH));
17635
+ process.exitCode = 1;
17636
+ return;
17637
+ }
17638
+ let signalReleased = false;
17639
+ const releaseOnSignal = (signal) => {
17640
+ if (signalReleased) return;
17641
+ signalReleased = true;
17642
+ try {
17643
+ releaseLock();
17644
+ } catch {
17645
+ }
17646
+ process.exit(signal === "SIGINT" ? 130 : 143);
17647
+ };
17648
+ process.once("SIGINT", releaseOnSignal);
17649
+ process.once("SIGTERM", releaseOnSignal);
17650
+ try {
17651
+ process.stdout.write(` ${pc15.dim("rollback retag (3 ops)".padEnd(34))}`);
17652
+ const swapStart = Date.now();
17653
+ const swapResult = performRollbackSwap(PRODUCTION_SWAP_PLAN);
17654
+ const swapDur = `${((Date.now() - swapStart) / 1e3).toFixed(1)}s`;
17655
+ process.stdout.write(`${swapResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${swapDur}
17656
+ `);
17657
+ if (!swapResult.ok) {
17658
+ printError(`Rollback retag failed: ${swapResult.summary}`);
17659
+ process.exitCode = 1;
17660
+ return;
17661
+ }
17662
+ printInfo("Rollback", swapResult.summary);
17663
+ const cwd = process.cwd();
17664
+ const composeFile = path28.join(cwd, "packages/host-cp/compose.yaml");
17665
+ const authSecret = readAuthSecret2();
17666
+ process.stdout.write(` ${pc15.dim("docker compose recreate host-cp".padEnd(34))}`);
17667
+ const composeStart = Date.now();
17668
+ const composeResult = runCompose(["up", "-d", "--force-recreate", "host-cp"], composeFile, buildComposeEnv(authSecret));
17669
+ const composeDur = `${((Date.now() - composeStart) / 1e3).toFixed(1)}s`;
17670
+ process.stdout.write(`${composeResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${composeDur}
17671
+ `);
17672
+ if (!composeResult.ok) {
17673
+ printError(
17674
+ `Rollback compose recreate failed:
17675
+ ${composeResult.stderr}
17676
+ 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\`.`
17677
+ );
17678
+ process.exitCode = 1;
17679
+ return;
17680
+ }
17681
+ process.stdout.write(` ${pc15.dim("recreate auth-service".padEnd(34))}`);
17682
+ const authResult = await recreateAuthService();
17683
+ const authDur = `${(authResult.durationMs / 1e3).toFixed(1)}s`;
17684
+ process.stdout.write(`${authResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${authDur}
17685
+ `);
17686
+ if (!authResult.ok) {
17687
+ printError(`Auth-service recreate failed: ${authResult.error ?? "unknown"}`);
17688
+ process.exitCode = 1;
17689
+ return;
17690
+ }
17691
+ process.stdout.write("\n");
17692
+ printSuccess("Rollback complete \u2014 canonical tags restored from :olam-rollback");
17693
+ } finally {
17694
+ releaseLock();
17695
+ process.removeListener("SIGINT", releaseOnSignal);
17696
+ process.removeListener("SIGTERM", releaseOnSignal);
17697
+ }
17698
+ }
17699
+ async function runUpgradeStepsWithLockHeld(opts, cwd, logRow) {
16991
17700
  if (opts.branch !== null) {
16992
17701
  if (isGitDirty(cwd)) {
16993
17702
  printError(
@@ -17045,6 +17754,17 @@ If there are conflicts, resolve them manually then re-run \`olam upgrade\`.`
17045
17754
  process.exitCode = 1;
17046
17755
  return;
17047
17756
  }
17757
+ const _targetSha = captureHeadSha(cwd);
17758
+ logRow.sha_target = _targetSha ?? "";
17759
+ if (_targetSha === null) {
17760
+ logRow.failed_step = "capture HEAD SHA";
17761
+ printError(
17762
+ "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."
17763
+ );
17764
+ process.exitCode = 1;
17765
+ return;
17766
+ }
17767
+ printInfo("Target SHA", abbreviateSha(_targetSha));
17048
17768
  const installDecision = shouldSkipInstall(opts, cwd);
17049
17769
  if (installDecision.skip) {
17050
17770
  printInfo("npm install", `skipped \u2014 ${installDecision.reason}`);
@@ -17082,7 +17802,7 @@ ${buildResult.stderr}`);
17082
17802
  return;
17083
17803
  }
17084
17804
  const authSecret = readAuthSecret2();
17085
- const spaDir = path26.join(cwd, "packages/control-plane/app");
17805
+ const spaDir = path28.join(cwd, "packages/control-plane/app");
17086
17806
  const spaResult = runStep2(
17087
17807
  "vite build (SPA)",
17088
17808
  "npx",
@@ -17104,21 +17824,107 @@ ${spaResult.stderr}`);
17104
17824
  printTimings2(timings);
17105
17825
  return;
17106
17826
  }
17107
- const buildScript = path26.join(cwd, "packages/adapters/src/docker/build-host-cp.sh");
17108
- const imageResult = runStep2(
17109
- "bash build-host-cp.sh",
17110
- "bash",
17111
- [buildScript],
17112
- { cwd }
17113
- );
17114
- timings.push({ label: "docker image build", durationMs: imageResult.durationMs });
17115
- if (!imageResult.ok) {
17116
- printError(`Docker image build failed:
17117
- ${imageResult.stderr}`);
17827
+ const olamTagEnv = { OLAM_TAG: "olam-next" };
17828
+ if (opts.noCache) {
17829
+ olamTagEnv.DOCKER_BUILD_NO_CACHE = "1";
17830
+ }
17831
+ const buildScripts = [
17832
+ { label: "bash build-auth.sh", relPath: "packages/adapters/src/docker/build-auth.sh", tee: false },
17833
+ { label: "bash build-devbox.sh", relPath: "packages/adapters/src/docker/build-devbox.sh", tee: true },
17834
+ { label: "bash build-host-cp.sh", relPath: "packages/adapters/src/docker/build-host-cp.sh", tee: false }
17835
+ ];
17836
+ for (const step of buildScripts) {
17837
+ const scriptPath = path28.join(cwd, step.relPath);
17838
+ if (step.tee) {
17839
+ process.stdout.write(` ${pc15.dim(step.label.padEnd(34))}
17840
+ `);
17841
+ const start = Date.now();
17842
+ const result = spawnSync7("bash", [scriptPath], {
17843
+ stdio: "inherit",
17844
+ cwd,
17845
+ env: { ...process.env, ...olamTagEnv }
17846
+ });
17847
+ const durationMs = Date.now() - start;
17848
+ const ok = result.status === 0 && result.error === void 0;
17849
+ const dur = `${(durationMs / 1e3).toFixed(1)}s`;
17850
+ process.stdout.write(` ${pc15.dim(step.label.padEnd(34))}${ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${dur}
17851
+ `);
17852
+ timings.push({ label: step.label, durationMs });
17853
+ if (!ok) {
17854
+ printError(`${step.label} failed (see output above for details).`);
17855
+ process.exitCode = 1;
17856
+ return;
17857
+ }
17858
+ } else {
17859
+ const result = runStep2(step.label, "bash", [scriptPath], {
17860
+ cwd,
17861
+ env: olamTagEnv
17862
+ });
17863
+ timings.push({ label: step.label, durationMs: result.durationMs });
17864
+ logRow.durations_ms[step.label] = result.durationMs;
17865
+ if (!result.ok) {
17866
+ logRow.failed_step = step.label;
17867
+ printError(`${step.label} failed:
17868
+ ${result.stderr.split("\n").slice(-3).join("\n")}`);
17869
+ process.exitCode = 1;
17870
+ return;
17871
+ }
17872
+ }
17873
+ }
17874
+ for (const t of timings) logRow.durations_ms[t.label] = t.durationMs;
17875
+ const smokeStart = Date.now();
17876
+ process.stdout.write(` ${pc15.dim("smoke (docker create + inspect)".padEnd(34))}`);
17877
+ const smokeImages = [
17878
+ "olam-auth:olam-next",
17879
+ "olam-devbox:olam-next",
17880
+ "olam-host-cp:olam-next"
17881
+ ];
17882
+ const smokeResults = smokeImages.map((img) => smokeImage(img, _targetSha));
17883
+ const smokeFailures = smokeResults.filter((r) => !r.ok);
17884
+ const smokeDurationMs = Date.now() - smokeStart;
17885
+ const smokeDur = `${(smokeDurationMs / 1e3).toFixed(1)}s`;
17886
+ process.stdout.write(`${smokeFailures.length === 0 ? pc15.green("\u2713") : pc15.red("\u2717")} ${smokeDur}
17887
+ `);
17888
+ timings.push({ label: "smoke", durationMs: smokeDurationMs });
17889
+ if (smokeFailures.length > 0) {
17890
+ printError(
17891
+ `Smoke failed for ${smokeFailures.length} of ${smokeResults.length} images:
17892
+ ` + 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)."
17893
+ );
17894
+ process.exitCode = 1;
17895
+ return;
17896
+ }
17897
+ const swapBoundarySha = captureHeadSha(cwd);
17898
+ if (swapBoundarySha !== null && swapBoundarySha !== _targetSha && !opts.force) {
17899
+ printError(
17900
+ `HEAD drifted during build window:
17901
+ captured (after pull): ${abbreviateSha(_targetSha)}
17902
+ current at swap: ${abbreviateSha(swapBoundarySha)}
17903
+
17904
+ Operator-driven \`git checkout\` or \`git reset\` triggered drift.
17905
+ Recovery options:
17906
+ \u2022 Re-run \`olam upgrade\` (will rebuild against current HEAD).
17907
+ \u2022 Pass \`--force\` to swap anyway (canonical advances to the
17908
+ captured-at-pull SHA, NOT current HEAD).`
17909
+ );
17910
+ process.exitCode = 1;
17911
+ return;
17912
+ }
17913
+ process.stdout.write(` ${pc15.dim("atomic 6-tag swap".padEnd(34))}`);
17914
+ const swapStart = Date.now();
17915
+ const swapResult = performAtomicSwap(PRODUCTION_SWAP_PLAN);
17916
+ const swapDurationMs = Date.now() - swapStart;
17917
+ const swapDur = `${(swapDurationMs / 1e3).toFixed(1)}s`;
17918
+ process.stdout.write(`${swapResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${swapDur}
17919
+ `);
17920
+ timings.push({ label: "atomic swap", durationMs: swapDurationMs });
17921
+ if (!swapResult.ok) {
17922
+ printError(`Atomic swap failed: ${swapResult.summary}`);
17118
17923
  process.exitCode = 1;
17119
17924
  return;
17120
17925
  }
17121
- const composeFile = path26.join(cwd, "packages/host-cp/compose.yaml");
17926
+ printInfo("Swap", swapResult.summary);
17927
+ const composeFile = path28.join(cwd, "packages/host-cp/compose.yaml");
17122
17928
  process.stdout.write(` ${pc15.dim("docker compose recreate".padEnd(34))}`);
17123
17929
  const composeStart = Date.now();
17124
17930
  const composeResult = runCompose(
@@ -17133,8 +17939,33 @@ ${imageResult.stderr}`);
17133
17939
  `);
17134
17940
  timings.push({ label: "container recreate", durationMs: composeDurationMs });
17135
17941
  if (!composeOk) {
17136
- printError(`docker compose up --force-recreate failed:
17137
- ${composeResult.stderr}`);
17942
+ printError(
17943
+ `docker compose up --force-recreate failed:
17944
+ ${composeResult.stderr}
17945
+
17946
+ Canonical tags advanced to new SHA but the stack failed to start.
17947
+ Recovery options:
17948
+ \u2022 Run \`olam upgrade --rollback\` to restore the prior :olam-rollback set, then investigate.
17949
+ \u2022 Manually \`docker logs olam-host-cp\` to diagnose; if recoverable, retry recreate without rollback.`
17950
+ );
17951
+ process.exitCode = 1;
17952
+ return;
17953
+ }
17954
+ process.stdout.write(` ${pc15.dim("recreate auth-service".padEnd(34))}`);
17955
+ const authResult = await recreateAuthService();
17956
+ const authDur = `${(authResult.durationMs / 1e3).toFixed(1)}s`;
17957
+ process.stdout.write(`${authResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${authDur}
17958
+ `);
17959
+ timings.push({ label: "auth recreate", durationMs: authResult.durationMs });
17960
+ if (!authResult.ok) {
17961
+ printError(
17962
+ `Auth-service recreate failed: ${authResult.error ?? "unknown"}
17963
+
17964
+ Canonical tags advanced to new SHA; host-cp recreated but auth-service is broken.
17965
+ Recovery options:
17966
+ \u2022 Run \`olam upgrade --rollback\` to restore the prior :olam-rollback set + working stack.
17967
+ \u2022 Manually: \`docker logs olam-auth\` to diagnose; \`olam auth up\` to restart.`
17968
+ );
17138
17969
  process.exitCode = 1;
17139
17970
  return;
17140
17971
  }
@@ -17147,7 +17978,23 @@ ${composeResult.stderr}`);
17147
17978
  `);
17148
17979
  timings.push({ label: "/health", durationMs: healthDurationMs });
17149
17980
  if (!healthy) {
17150
- printWarning("Host CP started but /health did not respond within 10s. Check: docker logs olam-host-cp");
17981
+ printWarning(
17982
+ "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."
17983
+ );
17984
+ }
17985
+ process.stdout.write(` ${pc15.dim("verify /version/status round-trip".padEnd(34))}`);
17986
+ const versionStart = Date.now();
17987
+ const versionMatch = await waitForVersionMatch(_targetSha, 6e4);
17988
+ const versionDurationMs = Date.now() - versionStart;
17989
+ const versionDur = `${(versionDurationMs / 1e3).toFixed(1)}s`;
17990
+ process.stdout.write(`${versionMatch.matched ? pc15.green("\u2713") : pc15.yellow("?")} ${versionDur}
17991
+ `);
17992
+ timings.push({ label: "/version/status round-trip", durationMs: versionDurationMs });
17993
+ if (!versionMatch.matched) {
17994
+ printWarning(
17995
+ `Version round-trip incomplete after ${(versionDurationMs / 1e3).toFixed(0)}s:
17996
+ ` + 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."
17997
+ );
17151
17998
  }
17152
17999
  process.stdout.write("\n");
17153
18000
  printSuccess("Upgrade complete");
@@ -17164,10 +18011,22 @@ function printTimings2(timings) {
17164
18011
  printInfo("total", `${(total / 1e3).toFixed(1)}s`);
17165
18012
  }
17166
18013
  function registerUpgrade(program2) {
17167
- 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(
18014
+ 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(
17168
18015
  "--skip-install",
17169
18016
  "Skip npm install entirely (use existing node_modules as-is). Useful when a native-module build failure blocks the normal upgrade path."
17170
- ).option("--branch <name>", "Switch to this branch before pulling (refuses if working tree is dirty)").action(async (opts) => {
18017
+ ).option("--branch <name>", "Switch to this branch before pulling (refuses if working tree is dirty)").option(
18018
+ "--rollback",
18019
+ "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."
18020
+ ).option(
18021
+ "--force",
18022
+ "Bypass HEAD-drift refusal at the swap boundary. Swap advances canonical to the\n captured-at-pull SHA even if current HEAD differs."
18023
+ ).option(
18024
+ "--no-cache",
18025
+ "Pass --no-cache to all three build scripts (DOCKER_BUILD_NO_CACHE=1).\n Useful when retrying after a cache-poisoning failure."
18026
+ ).option(
18027
+ "--history",
18028
+ "Print the upgrade history (~/.olam/upgrade.log) and exit.\n No upgrade is performed."
18029
+ ).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) => {
17171
18030
  await handleUpgrade(parseUpgradeOpts(opts));
17172
18031
  });
17173
18032
  }
@@ -17301,7 +18160,7 @@ function registerLogs(program2) {
17301
18160
  // src/commands/ps.ts
17302
18161
  init_context();
17303
18162
  import pc17 from "picocolors";
17304
- import { spawnSync as spawnSync7 } from "node:child_process";
18163
+ import { spawnSync as spawnSync8 } from "node:child_process";
17305
18164
  var SAFE_IDENT4 = /^[a-z0-9][a-z0-9-]{0,63}$/;
17306
18165
  function parseDockerTop(stdout) {
17307
18166
  const trimmed = stdout.trim();
@@ -17401,7 +18260,7 @@ function registerPs(program2) {
17401
18260
  const containerName = `olam-${worldId}-devbox`;
17402
18261
  let watchInterval;
17403
18262
  function fetchAndPrint() {
17404
- const result = spawnSync7(
18263
+ const result = spawnSync8(
17405
18264
  "docker",
17406
18265
  ["top", containerName, "pid", "user", "pcpu", "pmem", "stime", "stat", "cmd"],
17407
18266
  { encoding: "utf-8", timeout: 3e3 }
@@ -17437,20 +18296,20 @@ ${pc17.dim(`world: ${worldId} sort: ${sortKey} refresh: 5s Ctrl-C to exit`)}
17437
18296
  }
17438
18297
 
17439
18298
  // src/commands/keys.ts
17440
- import * as fs23 from "node:fs";
17441
- import * as os13 from "node:os";
17442
- import * as path27 from "node:path";
18299
+ import * as fs25 from "node:fs";
18300
+ import * as os15 from "node:os";
18301
+ import * as path29 from "node:path";
17443
18302
  import YAML4 from "yaml";
17444
18303
  function olamHome2() {
17445
- return process.env.OLAM_HOME ?? path27.join(os13.homedir(), ".olam");
18304
+ return process.env.OLAM_HOME ?? path29.join(os15.homedir(), ".olam");
17446
18305
  }
17447
18306
  function keysFilePath() {
17448
- return path27.join(olamHome2(), "keys.yaml");
18307
+ return path29.join(olamHome2(), "keys.yaml");
17449
18308
  }
17450
18309
  function readKeysFile() {
17451
18310
  const filePath = keysFilePath();
17452
- if (!fs23.existsSync(filePath)) return null;
17453
- const raw = fs23.readFileSync(filePath, "utf-8").trim();
18311
+ if (!fs25.existsSync(filePath)) return null;
18312
+ const raw = fs25.readFileSync(filePath, "utf-8").trim();
17454
18313
  if (raw.length === 0) return null;
17455
18314
  try {
17456
18315
  const parsed = YAML4.parse(raw);
@@ -17466,13 +18325,13 @@ function readKeysFile() {
17466
18325
  }
17467
18326
  function writeKeysFile(keys) {
17468
18327
  const dir = olamHome2();
17469
- if (!fs23.existsSync(dir)) {
17470
- fs23.mkdirSync(dir, { recursive: true });
18328
+ if (!fs25.existsSync(dir)) {
18329
+ fs25.mkdirSync(dir, { recursive: true });
17471
18330
  }
17472
18331
  const filePath = keysFilePath();
17473
18332
  const content = YAML4.stringify(keys);
17474
- fs23.writeFileSync(filePath, content, { encoding: "utf-8", mode: 384 });
17475
- fs23.chmodSync(filePath, 384);
18333
+ fs25.writeFileSync(filePath, content, { encoding: "utf-8", mode: 384 });
18334
+ fs25.chmodSync(filePath, 384);
17476
18335
  }
17477
18336
  function redact(value) {
17478
18337
  if (value.length <= 8) return value + "...";
@@ -17515,7 +18374,7 @@ function registerKeys(program2) {
17515
18374
  }
17516
18375
  const { [key]: _removed, ...rest } = existing;
17517
18376
  if (Object.keys(rest).length === 0) {
17518
- fs23.unlinkSync(keysFilePath());
18377
+ fs25.unlinkSync(keysFilePath());
17519
18378
  } else {
17520
18379
  writeKeysFile(rest);
17521
18380
  }
@@ -17538,26 +18397,26 @@ function registerKeys(program2) {
17538
18397
  }
17539
18398
 
17540
18399
  // src/commands/world-snapshot.ts
17541
- import * as fs25 from "node:fs";
17542
- import * as path29 from "node:path";
18400
+ import * as fs27 from "node:fs";
18401
+ import * as path31 from "node:path";
17543
18402
  import { execSync as execSync9 } from "node:child_process";
17544
18403
  import pc18 from "picocolors";
17545
18404
 
17546
18405
  // ../core/src/world/snapshot.ts
17547
18406
  import * as crypto6 from "node:crypto";
17548
- import * as fs24 from "node:fs";
17549
- import * as os14 from "node:os";
17550
- import * as path28 from "node:path";
18407
+ import * as fs26 from "node:fs";
18408
+ import * as os16 from "node:os";
18409
+ import * as path30 from "node:path";
17551
18410
  import { execFileSync as execFileSync4 } from "node:child_process";
17552
18411
  function snapshotsDir() {
17553
- return process.env["OLAM_SNAPSHOTS_DIR"] ?? path28.join(os14.homedir(), ".olam", "snapshots");
18412
+ return process.env["OLAM_SNAPSHOTS_DIR"] ?? path30.join(os16.homedir(), ".olam", "snapshots");
17554
18413
  }
17555
18414
  function snapshotKindDir(worldId, kind) {
17556
- return path28.join(snapshotsDir(), worldId, kind);
18415
+ return path30.join(snapshotsDir(), worldId, kind);
17557
18416
  }
17558
18417
  function snapshotTarPath(worldId, kind, repoName, hash) {
17559
18418
  const base = repoName ? `${repoName}-${hash}` : hash;
17560
- return path28.join(snapshotKindDir(worldId, kind), `${base}.tar.gz`);
18419
+ return path30.join(snapshotKindDir(worldId, kind), `${base}.tar.gz`);
17561
18420
  }
17562
18421
  function manifestPath(tarPath) {
17563
18422
  return tarPath.replace(/\.tar\.gz$/, ".manifest.json");
@@ -17574,16 +18433,16 @@ function hashBuffers(entries) {
17574
18433
  return hash.digest("hex").slice(0, 12);
17575
18434
  }
17576
18435
  function computeGemsFingerprint(repoDir) {
17577
- const lockfile = path28.join(repoDir, "Gemfile.lock");
17578
- if (!fs24.existsSync(lockfile)) return null;
17579
- return hashBuffers([{ path: "Gemfile.lock", content: fs24.readFileSync(lockfile) }]);
18436
+ const lockfile = path30.join(repoDir, "Gemfile.lock");
18437
+ if (!fs26.existsSync(lockfile)) return null;
18438
+ return hashBuffers([{ path: "Gemfile.lock", content: fs26.readFileSync(lockfile) }]);
17580
18439
  }
17581
18440
  function computeNodeFingerprint(repoDir) {
17582
18441
  const candidates = ["yarn.lock", "pnpm-lock.yaml", "package-lock.json"];
17583
18442
  for (const name of candidates) {
17584
- const lockfile = path28.join(repoDir, name);
17585
- if (fs24.existsSync(lockfile)) {
17586
- return hashBuffers([{ path: name, content: fs24.readFileSync(lockfile) }]);
18443
+ const lockfile = path30.join(repoDir, name);
18444
+ if (fs26.existsSync(lockfile)) {
18445
+ return hashBuffers([{ path: name, content: fs26.readFileSync(lockfile) }]);
17587
18446
  }
17588
18447
  }
17589
18448
  return null;
@@ -17593,59 +18452,59 @@ function computePgFingerprint(repoDirs) {
17593
18452
  const entries = [];
17594
18453
  for (const repoDir of repoDirs) {
17595
18454
  for (const pattern of patterns) {
17596
- const filePath = path28.join(repoDir, pattern);
17597
- if (fs24.existsSync(filePath)) {
17598
- entries.push({ path: filePath, content: fs24.readFileSync(filePath) });
18455
+ const filePath = path30.join(repoDir, pattern);
18456
+ if (fs26.existsSync(filePath)) {
18457
+ entries.push({ path: filePath, content: fs26.readFileSync(filePath) });
17599
18458
  }
17600
18459
  }
17601
18460
  }
17602
18461
  return entries.length > 0 ? hashBuffers(entries) : null;
17603
18462
  }
17604
18463
  function packTarball(srcDir, destPath, opts = {}) {
17605
- fs24.mkdirSync(path28.dirname(destPath), { recursive: true });
18464
+ fs26.mkdirSync(path30.dirname(destPath), { recursive: true });
17606
18465
  const tmp = `${destPath}.tmp`;
17607
18466
  const args = [];
17608
18467
  if (opts.followSymlinks) args.push("-h");
17609
18468
  args.push("-czf", tmp, "-C", srcDir, ".");
17610
18469
  try {
17611
18470
  execFileSync4("tar", args, { stdio: "pipe" });
17612
- fs24.renameSync(tmp, destPath);
18471
+ fs26.renameSync(tmp, destPath);
17613
18472
  } catch (err) {
17614
18473
  try {
17615
- fs24.rmSync(tmp, { force: true });
18474
+ fs26.rmSync(tmp, { force: true });
17616
18475
  } catch {
17617
18476
  }
17618
18477
  throw err;
17619
18478
  }
17620
18479
  }
17621
18480
  function writeManifest(manifest, tarPath) {
17622
- fs24.writeFileSync(manifestPath(tarPath), JSON.stringify(manifest, null, 2), "utf-8");
18481
+ fs26.writeFileSync(manifestPath(tarPath), JSON.stringify(manifest, null, 2), "utf-8");
17623
18482
  }
17624
18483
  function readManifest(tarPath) {
17625
18484
  const mPath = manifestPath(tarPath);
17626
- if (!fs24.existsSync(mPath)) return null;
18485
+ if (!fs26.existsSync(mPath)) return null;
17627
18486
  try {
17628
- return JSON.parse(fs24.readFileSync(mPath, "utf-8"));
18487
+ return JSON.parse(fs26.readFileSync(mPath, "utf-8"));
17629
18488
  } catch {
17630
18489
  return null;
17631
18490
  }
17632
18491
  }
17633
18492
  function listSnapshots(worldIdFilter) {
17634
18493
  const root = snapshotsDir();
17635
- if (!fs24.existsSync(root)) return [];
18494
+ if (!fs26.existsSync(root)) return [];
17636
18495
  const now = Date.now();
17637
18496
  const results = [];
17638
- const worlds = worldIdFilter ? [worldIdFilter] : fs24.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
18497
+ const worlds = worldIdFilter ? [worldIdFilter] : fs26.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
17639
18498
  for (const worldId of worlds) {
17640
- const worldDir = path28.join(root, worldId);
17641
- if (!fs24.existsSync(worldDir) || !fs24.statSync(worldDir).isDirectory()) continue;
18499
+ const worldDir = path30.join(root, worldId);
18500
+ if (!fs26.existsSync(worldDir) || !fs26.statSync(worldDir).isDirectory()) continue;
17642
18501
  for (const kind of ["gems", "node", "pg"]) {
17643
- const kindDir = path28.join(worldDir, kind);
17644
- if (!fs24.existsSync(kindDir)) continue;
17645
- const tarballs = fs24.readdirSync(kindDir).filter((f) => f.endsWith(".tar.gz"));
18502
+ const kindDir = path30.join(worldDir, kind);
18503
+ if (!fs26.existsSync(kindDir)) continue;
18504
+ const tarballs = fs26.readdirSync(kindDir).filter((f) => f.endsWith(".tar.gz"));
17646
18505
  for (const tarFile of tarballs) {
17647
- const tarPath = path28.join(kindDir, tarFile);
17648
- const stat = fs24.statSync(tarPath);
18506
+ const tarPath = path30.join(kindDir, tarFile);
18507
+ const stat = fs26.statSync(tarPath);
17649
18508
  const manifest = readManifest(tarPath);
17650
18509
  if (!manifest) continue;
17651
18510
  results.push({ manifest, tarPath, ageMs: now - stat.mtimeMs });
@@ -17724,17 +18583,17 @@ function resolveKinds(arg) {
17724
18583
  return [];
17725
18584
  }
17726
18585
  async function captureGems(worldId, workspacePath, repo) {
17727
- const repoDir = path29.join(workspacePath, repo);
18586
+ const repoDir = path31.join(workspacePath, repo);
17728
18587
  const fingerprint = computeGemsFingerprint(repoDir);
17729
18588
  if (!fingerprint) {
17730
18589
  return { ok: false, tarPath: "", msg: "no Gemfile.lock \u2014 layer does not apply" };
17731
18590
  }
17732
18591
  const tarPath = snapshotTarPath(worldId, "gems", repo, fingerprint);
17733
- const vendorBundle = path29.join(repoDir, "vendor", "bundle");
17734
- if (fs25.existsSync(vendorBundle)) {
18592
+ const vendorBundle = path31.join(repoDir, "vendor", "bundle");
18593
+ if (fs27.existsSync(vendorBundle)) {
17735
18594
  try {
17736
18595
  packTarball(vendorBundle, tarPath);
17737
- const stat = fs25.statSync(tarPath);
18596
+ const stat = fs27.statSync(tarPath);
17738
18597
  const manifest = {
17739
18598
  kind: "gems",
17740
18599
  worldId,
@@ -17767,10 +18626,10 @@ async function captureGems(worldId, workspacePath, repo) {
17767
18626
  `docker exec ${containerName} sh -c 'mkdir -p "$(dirname ${tmpTar})" && tar -czf ${tmpTar}.tmp -C ${bundlePath} . && mv ${tmpTar}.tmp ${tmpTar}'`,
17768
18627
  { stdio: "pipe", timeout: 12e4 }
17769
18628
  );
17770
- fs25.mkdirSync(path29.dirname(tarPath), { recursive: true });
18629
+ fs27.mkdirSync(path31.dirname(tarPath), { recursive: true });
17771
18630
  execSync9(`docker cp ${containerName}:${tmpTar} "${tarPath}"`, { stdio: "pipe", timeout: 12e4 });
17772
18631
  execSync9(`docker exec ${containerName} rm -f ${tmpTar}`, { stdio: "pipe" });
17773
- const stat = fs25.statSync(tarPath);
18632
+ const stat = fs27.statSync(tarPath);
17774
18633
  const manifest = {
17775
18634
  kind: "gems",
17776
18635
  worldId,
@@ -17787,19 +18646,19 @@ async function captureGems(worldId, workspacePath, repo) {
17787
18646
  }
17788
18647
  }
17789
18648
  async function captureNode(worldId, workspacePath, repo) {
17790
- const repoDir = path29.join(workspacePath, repo);
18649
+ const repoDir = path31.join(workspacePath, repo);
17791
18650
  const fingerprint = computeNodeFingerprint(repoDir);
17792
18651
  if (!fingerprint) {
17793
18652
  return { ok: false, tarPath: "", msg: "no lockfile \u2014 layer does not apply" };
17794
18653
  }
17795
- const nodeModules = path29.join(repoDir, "node_modules");
17796
- if (!fs25.existsSync(nodeModules)) {
18654
+ const nodeModules = path31.join(repoDir, "node_modules");
18655
+ if (!fs27.existsSync(nodeModules)) {
17797
18656
  return { ok: false, tarPath: "", msg: "node_modules not installed yet" };
17798
18657
  }
17799
18658
  const tarPath = snapshotTarPath(worldId, "node", repo, fingerprint);
17800
18659
  try {
17801
18660
  packTarball(nodeModules, tarPath);
17802
- const stat = fs25.statSync(tarPath);
18661
+ const stat = fs27.statSync(tarPath);
17803
18662
  const manifest = {
17804
18663
  kind: "node",
17805
18664
  worldId,
@@ -17816,7 +18675,7 @@ async function captureNode(worldId, workspacePath, repo) {
17816
18675
  }
17817
18676
  }
17818
18677
  async function capturePg(worldId, workspacePath, repoNames) {
17819
- const repoDirs = repoNames.map((r) => path29.join(workspacePath, r));
18678
+ const repoDirs = repoNames.map((r) => path31.join(workspacePath, r));
17820
18679
  const fingerprint = computePgFingerprint(repoDirs);
17821
18680
  if (!fingerprint) {
17822
18681
  return { ok: false, tarPath: "", msg: "no Gemfile.lock / schema.rb \u2014 layer does not apply" };
@@ -17831,13 +18690,13 @@ async function capturePg(worldId, workspacePath, repoNames) {
17831
18690
  }
17832
18691
  try {
17833
18692
  execSync9(`docker stop ${containerName}`, { stdio: "pipe", timeout: 3e4 });
17834
- fs25.mkdirSync(path29.dirname(tarPath), { recursive: true });
18693
+ fs27.mkdirSync(path31.dirname(tarPath), { recursive: true });
17835
18694
  execSync9(
17836
- `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)}'`,
18695
+ `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)}'`,
17837
18696
  { stdio: "pipe", timeout: 18e4 }
17838
18697
  );
17839
18698
  execSync9(`docker start ${containerName}`, { stdio: "pipe", timeout: 3e4 });
17840
- const stat = fs25.statSync(tarPath);
18699
+ const stat = fs27.statSync(tarPath);
17841
18700
  const manifest = {
17842
18701
  kind: "pg",
17843
18702
  worldId,
@@ -17911,35 +18770,35 @@ function formatAge2(ms) {
17911
18770
 
17912
18771
  // src/commands/refresh.ts
17913
18772
  init_context();
17914
- import * as fs27 from "node:fs";
17915
- import * as os15 from "node:os";
17916
- import * as path31 from "node:path";
17917
- import { spawnSync as spawnSync8 } from "node:child_process";
18773
+ import * as fs29 from "node:fs";
18774
+ import * as os17 from "node:os";
18775
+ import * as path33 from "node:path";
18776
+ import { spawnSync as spawnSync9 } from "node:child_process";
17918
18777
  import ora5 from "ora";
17919
18778
 
17920
18779
  // src/commands/refresh-helpers.ts
17921
- import * as fs26 from "node:fs";
17922
- import * as path30 from "node:path";
18780
+ import * as fs28 from "node:fs";
18781
+ import * as path32 from "node:path";
17923
18782
  function collectCpSourceFiles(standaloneDir) {
17924
- if (!fs26.existsSync(standaloneDir)) {
18783
+ if (!fs28.existsSync(standaloneDir)) {
17925
18784
  throw new Error(`CP standalone dir not found: ${standaloneDir}`);
17926
18785
  }
17927
18786
  const entries = [];
17928
- const topLevel = fs26.readdirSync(standaloneDir).filter((f) => {
17929
- const stat = fs26.statSync(path30.join(standaloneDir, f));
18787
+ const topLevel = fs28.readdirSync(standaloneDir).filter((f) => {
18788
+ const stat = fs28.statSync(path32.join(standaloneDir, f));
17930
18789
  return stat.isFile() && f.endsWith(".mjs") && !f.endsWith(".test.mjs");
17931
18790
  }).sort();
17932
18791
  for (const f of topLevel) {
17933
- entries.push({ srcPath: path30.join(standaloneDir, f), destRelPath: f });
18792
+ entries.push({ srcPath: path32.join(standaloneDir, f), destRelPath: f });
17934
18793
  }
17935
- const libDir = path30.join(standaloneDir, "lib");
17936
- if (fs26.existsSync(libDir) && fs26.statSync(libDir).isDirectory()) {
17937
- const libFiles = fs26.readdirSync(libDir).filter((f) => {
17938
- const stat = fs26.statSync(path30.join(libDir, f));
18794
+ const libDir = path32.join(standaloneDir, "lib");
18795
+ if (fs28.existsSync(libDir) && fs28.statSync(libDir).isDirectory()) {
18796
+ const libFiles = fs28.readdirSync(libDir).filter((f) => {
18797
+ const stat = fs28.statSync(path32.join(libDir, f));
17939
18798
  return stat.isFile() && f.endsWith(".mjs") && !f.endsWith(".test.mjs");
17940
18799
  }).sort();
17941
18800
  for (const f of libFiles) {
17942
- entries.push({ srcPath: path30.join(libDir, f), destRelPath: `lib/${f}` });
18801
+ entries.push({ srcPath: path32.join(libDir, f), destRelPath: `lib/${f}` });
17943
18802
  }
17944
18803
  }
17945
18804
  return entries;
@@ -17958,7 +18817,7 @@ var RESTART_TIMEOUT_S = 30;
17958
18817
  var HEALTH_POLL_MS = 500;
17959
18818
  var HEALTH_TIMEOUT_MS = 3e4;
17960
18819
  function docker(args) {
17961
- const result = spawnSync8("docker", args, {
18820
+ const result = spawnSync9("docker", args, {
17962
18821
  encoding: "utf-8",
17963
18822
  stdio: ["ignore", "pipe", "pipe"]
17964
18823
  });
@@ -17997,16 +18856,16 @@ async function refreshWorld(worldId, portOffset, standaloneDir, opts) {
17997
18856
  error: err instanceof Error ? err.message : String(err)
17998
18857
  };
17999
18858
  }
18000
- const stagingDir = fs27.mkdtempSync(
18001
- path31.join(os15.tmpdir(), `olam-refresh-${worldId}-`)
18859
+ const stagingDir = fs29.mkdtempSync(
18860
+ path33.join(os17.tmpdir(), `olam-refresh-${worldId}-`)
18002
18861
  );
18003
18862
  try {
18004
18863
  const hasLib = entries.some((e) => e.destRelPath.startsWith("lib/"));
18005
18864
  if (hasLib) {
18006
- fs27.mkdirSync(path31.join(stagingDir, "lib"), { recursive: true });
18865
+ fs29.mkdirSync(path33.join(stagingDir, "lib"), { recursive: true });
18007
18866
  }
18008
18867
  for (const { srcPath, destRelPath } of entries) {
18009
- fs27.copyFileSync(srcPath, path31.join(stagingDir, destRelPath));
18868
+ fs29.copyFileSync(srcPath, path33.join(stagingDir, destRelPath));
18010
18869
  }
18011
18870
  const cpResult = docker([
18012
18871
  "cp",
@@ -18021,7 +18880,7 @@ async function refreshWorld(worldId, portOffset, standaloneDir, opts) {
18021
18880
  };
18022
18881
  }
18023
18882
  } finally {
18024
- fs27.rmSync(stagingDir, { recursive: true, force: true });
18883
+ fs29.rmSync(stagingDir, { recursive: true, force: true });
18025
18884
  }
18026
18885
  if (opts.restart) {
18027
18886
  const restartResult = docker([
@@ -18058,11 +18917,11 @@ function registerRefresh(program2) {
18058
18917
  process.exitCode = 1;
18059
18918
  return;
18060
18919
  }
18061
- const standaloneDir = path31.join(
18920
+ const standaloneDir = path33.join(
18062
18921
  process.cwd(),
18063
18922
  "packages/control-plane/standalone"
18064
18923
  );
18065
- if (!fs27.existsSync(standaloneDir)) {
18924
+ if (!fs29.existsSync(standaloneDir)) {
18066
18925
  printError(
18067
18926
  `CP standalone source not found at ${standaloneDir}.
18068
18927
  Run \`olam refresh\` from the olam repo root.`
@@ -18141,18 +19000,18 @@ Run \`olam refresh\` from the olam repo root.`
18141
19000
  }
18142
19001
 
18143
19002
  // src/pleri-config.ts
18144
- import * as fs28 from "node:fs";
18145
- import * as path32 from "node:path";
19003
+ import * as fs30 from "node:fs";
19004
+ import * as path34 from "node:path";
18146
19005
  function isPleriConfigured(configDir = process.env.OLAM_CONFIG_DIR ?? ".olam") {
18147
19006
  if (process.env.PLERI_BASE_URL) {
18148
19007
  return true;
18149
19008
  }
18150
- const configPath = path32.join(configDir, "config.yaml");
18151
- if (!fs28.existsSync(configPath)) {
19009
+ const configPath = path34.join(configDir, "config.yaml");
19010
+ if (!fs30.existsSync(configPath)) {
18152
19011
  return false;
18153
19012
  }
18154
19013
  try {
18155
- const contents = fs28.readFileSync(configPath, "utf8");
19014
+ const contents = fs30.readFileSync(configPath, "utf8");
18156
19015
  return /^[^#\n]*\bpleri:/m.test(contents);
18157
19016
  } catch {
18158
19017
  return false;
@@ -18161,7 +19020,24 @@ function isPleriConfigured(configDir = process.env.OLAM_CONFIG_DIR ?? ".olam") {
18161
19020
 
18162
19021
  // src/index.ts
18163
19022
  var program = new Command();
18164
- program.name("olam").description("Olam \u2014 isolated development worlds with thought graph capture").version("0.1.0");
19023
+ function readCliVersion() {
19024
+ try {
19025
+ const here = path35.dirname(fileURLToPath3(import.meta.url));
19026
+ for (const candidate of [
19027
+ path35.join(here, "package.json"),
19028
+ path35.join(here, "..", "package.json"),
19029
+ path35.join(here, "..", "..", "package.json")
19030
+ ]) {
19031
+ if (fs31.existsSync(candidate)) {
19032
+ const pkg = JSON.parse(fs31.readFileSync(candidate, "utf-8"));
19033
+ if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
19034
+ }
19035
+ }
19036
+ } catch {
19037
+ }
19038
+ return "0.0.0-unknown";
19039
+ }
19040
+ program.name("olam").description("Olam \u2014 isolated development worlds with thought graph capture").version(readCliVersion());
18165
19041
  registerInit(program);
18166
19042
  registerInstall(program);
18167
19043
  registerAuth(program);