@pleri/olam-cli 0.1.150 → 0.1.152

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -15629,15 +15629,15 @@ var init_registry_allowlist = __esm({
15629
15629
 
15630
15630
  // ../core/dist/world/world-yaml.js
15631
15631
  import * as fs34 from "node:fs";
15632
- import * as path35 from "node:path";
15632
+ import * as path36 from "node:path";
15633
15633
  import { parse as parseYaml4, stringify as stringifyYaml4 } from "yaml";
15634
15634
  function writeWorldYaml(worldPath, data) {
15635
- const olamDir = path35.join(worldPath, ".olam");
15635
+ const olamDir = path36.join(worldPath, ".olam");
15636
15636
  fs34.mkdirSync(olamDir, { recursive: true });
15637
- fs34.writeFileSync(path35.join(olamDir, "world.yaml"), stringifyYaml4(data), "utf-8");
15637
+ fs34.writeFileSync(path36.join(olamDir, "world.yaml"), stringifyYaml4(data), "utf-8");
15638
15638
  }
15639
15639
  function readWorldYaml(worldPath) {
15640
- const yamlPath = path35.join(worldPath, ".olam", "world.yaml");
15640
+ const yamlPath = path36.join(worldPath, ".olam", "world.yaml");
15641
15641
  if (!fs34.existsSync(yamlPath))
15642
15642
  return null;
15643
15643
  try {
@@ -21011,7 +21011,7 @@ function registerServices(program2) {
21011
21011
 
21012
21012
  // src/lib/auth-refresh-kubernetes.ts
21013
21013
  import * as fs32 from "node:fs";
21014
- import * as path33 from "node:path";
21014
+ import * as path34 from "node:path";
21015
21015
  import pc10 from "picocolors";
21016
21016
  import { stringify as yamlStringify2 } from "yaml";
21017
21017
 
@@ -21019,842 +21019,1233 @@ import { stringify as yamlStringify2 } from "yaml";
21019
21019
  init_output();
21020
21020
  import * as fs31 from "node:fs";
21021
21021
  import "node:os";
21022
- import * as path32 from "node:path";
21022
+ import * as path33 from "node:path";
21023
21023
  import { parse as yamlParse, stringify as yamlStringify } from "yaml";
21024
21024
  import ora3 from "ora";
21025
21025
  import pc9 from "picocolors";
21026
21026
 
21027
- // src/lib/port-forward.ts
21028
- import { spawn as spawn5 } from "node:child_process";
21029
- import * as fs29 from "node:fs";
21030
- import * as net3 from "node:net";
21031
- import * as path30 from "node:path";
21032
- import * as os16 from "node:os";
21033
- var PORT_FORWARD_PORT = 19e3;
21034
- var PORT_FORWARD_LOCK_PATH = path30.join(OLAM_STATE_DIR, "port-forward.lock");
21035
- var PORT_FORWARD_PID_PATH = path30.join(OLAM_STATE_DIR, "port-forward.pid");
21036
- var TCP_PROBE_TIMEOUT_MS = 2e3;
21037
- async function probePortForwardLiveness(deps = {}) {
21038
- const probe2 = deps.tcpProbeImpl ?? realTcpProbe;
21039
- return probe2("127.0.0.1", PORT_FORWARD_PORT, TCP_PROBE_TIMEOUT_MS);
21040
- }
21041
- function realTcpProbe(host, port2, timeoutMs) {
21042
- return new Promise((resolve23) => {
21043
- const socket = new net3.Socket();
21044
- let done = false;
21045
- function finish(result) {
21046
- if (done) return;
21047
- done = true;
21048
- socket.destroy();
21049
- resolve23(result);
21050
- }
21051
- const timer = setTimeout(() => finish(false), timeoutMs);
21052
- socket.once("connect", () => {
21053
- clearTimeout(timer);
21054
- finish(true);
21055
- });
21056
- socket.once("error", () => {
21057
- clearTimeout(timer);
21058
- finish(false);
21059
- });
21060
- socket.once("timeout", () => {
21061
- clearTimeout(timer);
21062
- finish(false);
21063
- });
21064
- socket.connect(port2, host);
21027
+ // src/lib/health-probes.ts
21028
+ import { spawnSync as spawnSync12 } from "node:child_process";
21029
+ import { existsSync as existsSync28, readdirSync as readdirSync7, readFileSync as readFileSync22, statSync as statSync6 } from "node:fs";
21030
+ import { homedir as homedir18 } from "node:os";
21031
+ import path30 from "node:path";
21032
+
21033
+ // src/lib/kg-caps.ts
21034
+ var SOFT_CAP_BYTES = 15e8;
21035
+ var HARD_CAP_BYTES = 5e9;
21036
+
21037
+ // src/lib/health-probes.ts
21038
+ var HEALTH_TIMEOUT_MS = 5e3;
21039
+ var COLIMA_010_WARN_TEXT = (
21040
+ // eslint-disable-next-line @typescript-eslint/quotes
21041
+ "Colima 0.10.1's --kubernetes mode is broken upstream. Either switch to OLAM_HOST_CP_ENGINE=docker (recommended) or downgrade Colima to 0.9.x. Track abiosoft/colima issues for fix."
21042
+ );
21043
+ var COLIMA_010_RANGE = /^v?0\.10\.\d+$/;
21044
+ var defaultDockerExec2 = (cmd, args) => {
21045
+ const r = spawnSync12(cmd, [...args], {
21046
+ encoding: "utf-8",
21047
+ stdio: ["ignore", "pipe", "pipe"]
21065
21048
  });
21066
- }
21067
- function acquireAdvisoryLock(lockPath) {
21068
- const fd = fs29.openSync(lockPath, fs29.constants.O_CREAT | fs29.constants.O_EXCL | fs29.constants.O_WRONLY);
21069
- fs29.closeSync(fd);
21070
- return () => {
21071
- try {
21072
- fs29.unlinkSync(lockPath);
21073
- } catch {
21074
- }
21049
+ return {
21050
+ status: r.status,
21051
+ stdout: r.stdout ?? "",
21052
+ stderr: r.stderr ?? ""
21075
21053
  };
21076
- }
21077
- function pidFilePath(deps) {
21078
- const stateDir = deps.stateDir ?? OLAM_STATE_DIR;
21079
- return path30.join(stateDir, "port-forward.pid");
21080
- }
21081
- function lockFilePath(deps) {
21082
- const stateDir = deps.stateDir ?? OLAM_STATE_DIR;
21083
- return path30.join(stateDir, "port-forward.lock");
21084
- }
21085
- function writePidFile(pidPath2, pid) {
21086
- const dir = path30.dirname(pidPath2);
21087
- if (!fs29.existsSync(dir)) {
21088
- fs29.mkdirSync(dir, { recursive: true });
21054
+ };
21055
+ var defaultFetch = (input2, init) => fetch(input2, init);
21056
+ async function probeDockerDaemon(dockerExec = defaultDockerExec2) {
21057
+ const r = dockerExec("docker", ["info", "--format", "{{.ServerVersion}}"]);
21058
+ if (r.status === 0 && r.stdout.trim().length > 0) {
21059
+ return { ok: true, message: `docker ${r.stdout.trim()} reachable` };
21089
21060
  }
21090
- const tmp = `${pidPath2}.tmp.${process.pid}`;
21091
- fs29.writeFileSync(tmp, String(pid) + "\n", { encoding: "utf8", mode: 384 });
21092
- fs29.renameSync(tmp, pidPath2);
21061
+ return {
21062
+ ok: false,
21063
+ message: "docker daemon not reachable",
21064
+ remedy: "Start Docker Desktop (or your docker daemon) and re-run."
21065
+ };
21093
21066
  }
21094
- async function spawnPortForward(context, namespace, target, localPort = PORT_FORWARD_PORT, remotePort = PORT_FORWARD_PORT, deps = {}) {
21095
- const lockPath = lockFilePath(deps);
21096
- const pidPath2 = pidFilePath(deps);
21097
- const spawnImpl = deps.spawnImpl ?? spawn5;
21098
- const stateDir = deps.stateDir ?? OLAM_STATE_DIR;
21099
- if (!fs29.existsSync(stateDir)) {
21100
- fs29.mkdirSync(stateDir, { recursive: true });
21101
- }
21102
- let releaseLock2 = null;
21103
- try {
21104
- releaseLock2 = acquireAdvisoryLock(lockPath);
21105
- } catch (err) {
21106
- const code = err.code;
21107
- if (code === "EEXIST") {
21108
- return { spawned: false, reason: "lock-held" };
21067
+ async function probeImagePresence(refs, dockerExec = defaultDockerExec2) {
21068
+ for (const ref of refs) {
21069
+ const r = dockerExec("docker", ["image", "inspect", "--format", "{{.Id}}", ref]);
21070
+ if (r.status !== 0) {
21071
+ return {
21072
+ ok: false,
21073
+ message: `image ${ref} not present locally`,
21074
+ remedy: "Run `olam bootstrap` to pull the published image, or build locally."
21075
+ };
21109
21076
  }
21110
- throw err;
21111
21077
  }
21078
+ return { ok: true, message: `${refs.length} image(s) present` };
21079
+ }
21080
+ async function probeHostCpHealth(fetchImpl = defaultFetch) {
21081
+ const controller = new AbortController();
21082
+ const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
21112
21083
  try {
21113
- const live = await probePortForwardLiveness(deps);
21114
- if (live) {
21115
- return { spawned: false, reason: "live" };
21084
+ const res = await fetchImpl("http://127.0.0.1:19000/health", { signal: controller.signal });
21085
+ const body = await res.text().catch(() => "");
21086
+ if (res.status !== 200) {
21087
+ return {
21088
+ ok: false,
21089
+ message: `host-cp /health returned ${res.status}`,
21090
+ remedy: "Restart host-cp via `olam host-cp start`; verify port 19000 is bound to 127.0.0.1."
21091
+ };
21116
21092
  }
21117
- const child = spawnImpl(
21118
- "kubectl",
21119
- [
21120
- "--context",
21121
- context,
21122
- "port-forward",
21123
- "-n",
21124
- namespace,
21125
- target,
21126
- `${localPort}:${remotePort}`
21127
- ],
21128
- {
21129
- stdio: ["ignore", "ignore", "ignore"],
21130
- detached: true
21131
- }
21132
- );
21133
- if (typeof child.pid !== "number" || child.pid <= 0) {
21134
- return { spawned: false, reason: "lock-held" };
21093
+ if (isHostCpHealthEnvelope(body)) {
21094
+ return { ok: true, message: "host-cp /health returned 200" };
21135
21095
  }
21136
- writePidFile(pidPath2, child.pid);
21137
- child.unref();
21138
- child.once("exit", () => {
21139
- try {
21140
- fs29.unlinkSync(pidPath2);
21141
- } catch {
21142
- }
21143
- });
21144
- return { spawned: true, pid: child.pid };
21096
+ return {
21097
+ ok: false,
21098
+ message: "host-cp /health returned 200 but body did not match the host-cp envelope (possible port collision on :19000)",
21099
+ remedy: "Check `lsof -iTCP:19000`; another process may have bound the port. Restart host-cp via `olam host-cp start`."
21100
+ };
21101
+ } catch (err) {
21102
+ const e = err;
21103
+ const reason = e.name === "AbortError" ? `timeout (${HEALTH_TIMEOUT_MS}ms)` : e.message;
21104
+ return {
21105
+ ok: false,
21106
+ message: `host-cp /health unreachable: ${reason}`,
21107
+ remedy: "Restart host-cp via `olam host-cp start`; verify port 19000 is bound to 127.0.0.1."
21108
+ };
21145
21109
  } finally {
21146
- releaseLock2();
21110
+ clearTimeout(timeout);
21147
21111
  }
21148
21112
  }
21149
- function peripheralPidPath(name, stateDir) {
21150
- return path30.join(stateDir, `port-forward-${name}.pid`);
21151
- }
21152
- function peripheralLockPath(name, stateDir) {
21153
- return path30.join(stateDir, `port-forward-${name}.lock`);
21113
+ function isHostCpHealthEnvelope(body) {
21114
+ let parsed;
21115
+ try {
21116
+ parsed = JSON.parse(body);
21117
+ } catch {
21118
+ return false;
21119
+ }
21120
+ if (!parsed || typeof parsed !== "object") return false;
21121
+ const obj = parsed;
21122
+ if (typeof obj.phase !== "string") return false;
21123
+ if (!obj.cache || typeof obj.cache !== "object") return false;
21124
+ const mode = obj.mode;
21125
+ if (!mode || typeof mode !== "object") return false;
21126
+ if (typeof mode.deployment !== "string") return false;
21127
+ return true;
21154
21128
  }
21155
- async function spawnPeripheralPortForward(peripheral, context, namespace, deps = {}) {
21156
- const stateDir = deps.stateDir ?? OLAM_STATE_DIR;
21157
- const spawnImpl = deps.spawnImpl ?? spawn5;
21158
- const tcpProbeImpl = deps.tcpProbeImpl ?? realTcpProbe;
21159
- if (!fs29.existsSync(stateDir)) {
21160
- fs29.mkdirSync(stateDir, { recursive: true });
21129
+ async function probeAuthVault(olamHomeOverride) {
21130
+ const vaultPath = resolveAccountsPath(olamHomeOverride);
21131
+ if (!existsSync28(vaultPath)) {
21132
+ return {
21133
+ ok: false,
21134
+ message: `auth vault missing at ${vaultPath}`,
21135
+ remedy: "Run `olam auth up && olam auth login` to provision the vault."
21136
+ };
21161
21137
  }
21162
- const lockPath = peripheralLockPath(peripheral.name, stateDir);
21163
- const pidPath2 = peripheralPidPath(peripheral.name, stateDir);
21164
- let releaseLock2 = null;
21138
+ let parsed;
21165
21139
  try {
21166
- releaseLock2 = acquireAdvisoryLock(lockPath);
21140
+ parsed = JSON.parse(readFileSync22(vaultPath, "utf-8"));
21167
21141
  } catch (err) {
21168
- const code = err.code;
21169
- if (code === "EEXIST") {
21170
- return false;
21171
- }
21172
- throw err;
21142
+ return {
21143
+ ok: false,
21144
+ message: `auth vault malformed: ${err.message}`,
21145
+ remedy: "Inspect the accounts.json file; restore from backup or re-run `olam auth up`."
21146
+ };
21173
21147
  }
21174
- try {
21175
- const alive = await tcpProbeImpl("127.0.0.1", peripheral.port, TCP_PROBE_TIMEOUT_MS);
21176
- if (alive) {
21177
- return false;
21178
- }
21179
- const child = spawnImpl(
21180
- "kubectl",
21181
- [
21182
- "--context",
21183
- context,
21184
- "port-forward",
21185
- "-n",
21186
- namespace,
21187
- `service/${peripheral.k8sServiceName}`,
21188
- `${peripheral.port}:${peripheral.port}`
21189
- ],
21190
- {
21191
- stdio: ["ignore", "ignore", "ignore"],
21192
- detached: true
21193
- }
21194
- );
21195
- if (typeof child.pid !== "number" || child.pid <= 0) {
21196
- return false;
21197
- }
21198
- writePidFile(pidPath2, child.pid);
21199
- child.unref();
21200
- child.once("exit", () => {
21201
- try {
21202
- fs29.unlinkSync(pidPath2);
21203
- } catch {
21204
- }
21205
- });
21206
- return true;
21207
- } finally {
21208
- releaseLock2();
21148
+ if (!Array.isArray(parsed)) {
21149
+ return {
21150
+ ok: false,
21151
+ message: "auth vault malformed: expected JSON array at top level",
21152
+ remedy: "Restore the vault from backup or re-run `olam auth up && olam auth login`."
21153
+ };
21154
+ }
21155
+ const accounts = parsed.filter((a) => a !== null && typeof a === "object");
21156
+ const now = Date.now();
21157
+ const activeClaude = accounts.filter((a) => a.provider === "claude" && effectiveState2(a, now) === "active");
21158
+ if (activeClaude.length === 0) {
21159
+ return {
21160
+ ok: false,
21161
+ message: `no active claude credentials (vault has ${accounts.length} account(s))`,
21162
+ remedy: "Run `olam auth login` to add a credential, or wait for the next reset."
21163
+ };
21209
21164
  }
21165
+ return { ok: true, message: `${activeClaude.length} active claude credential(s)` };
21210
21166
  }
21211
- async function spawnAllPeripheralPortForwards(context = "olam", namespace = "olam", deps = {}) {
21212
- await Promise.all(
21213
- PERIPHERALS.map((p) => spawnPeripheralPortForward(p, context, namespace, deps))
21214
- );
21167
+ function resolveAccountsPath(olamHomeOverride) {
21168
+ const explicit = process.env.OLAM_AUTH_DATA_PATH;
21169
+ if (explicit) return explicit;
21170
+ const olamHome5 = olamHomeOverride ?? process.env.OLAM_HOME ?? path30.join(homedir18(), ".olam");
21171
+ return path30.join(olamHome5, "auth-data", "accounts.json");
21215
21172
  }
21216
-
21217
- // src/lib/instrumentation.ts
21218
- var INSTRUMENTATION_SCHEMA = "olam.instr.v1";
21219
- var INSTRUMENTATION_PREFIX = "OLAM_INSTR ";
21220
- function nowIso(now) {
21221
- const d = now ? now() : /* @__PURE__ */ new Date();
21222
- return d.toISOString();
21173
+ function effectiveState2(account, now) {
21174
+ const persisted = account.state ?? (account.rateLimited ? "cooldown" : "active");
21175
+ if (persisted === "disabled") return "disabled";
21176
+ if (typeof account.expiresAt === "number" && account.expiresAt <= now) return "expired";
21177
+ if (persisted === "cooldown") {
21178
+ const reset = account.rateLimitResetsAt ? new Date(account.rateLimitResetsAt).getTime() : 0;
21179
+ if (reset > 0 && reset <= now) return "active";
21180
+ }
21181
+ return persisted;
21223
21182
  }
21224
- function resolveInstallId(opts) {
21183
+ async function probeKgStorage(olamHomeOverride) {
21184
+ const olamHome5 = olamHomeOverride ?? process.env.OLAM_HOME ?? path30.join(homedir18(), ".olam");
21185
+ const kgRoot3 = path30.join(olamHome5, "kg");
21186
+ const worldsRoot3 = path30.join(olamHome5, "worlds");
21187
+ const kgBytes = enumerateGraphifyOut(kgRoot3, "pristine");
21188
+ const overlayBytes = enumerateGraphifyOut(worldsRoot3, "overlay");
21189
+ const totalBytes = kgBytes + overlayBytes;
21190
+ if (totalBytes >= HARD_CAP_BYTES) {
21191
+ return {
21192
+ ok: false,
21193
+ message: `KG storage ${formatBytes(totalBytes)} exceeds 5 GB hard cap`,
21194
+ remedy: "Prune stale pristines: `rm -rf ~/.olam/kg/<workspace>` after operator review."
21195
+ };
21196
+ }
21197
+ if (totalBytes >= SOFT_CAP_BYTES) {
21198
+ return {
21199
+ ok: true,
21200
+ message: `KG storage ${formatBytes(totalBytes)} (soft-warn \u2014 exceeds 1.5 GB; consider pruning)`
21201
+ };
21202
+ }
21203
+ return { ok: true, message: `KG storage ${formatBytes(totalBytes)} (under cap)` };
21204
+ }
21205
+ function enumerateGraphifyOut(root, layout) {
21206
+ if (!existsSync28(root)) return 0;
21207
+ let total = 0;
21208
+ let entries;
21225
21209
  try {
21226
- if (opts.installIdProvider) return opts.installIdProvider();
21227
- const cfg = readConfig();
21228
- return cfg.install_id;
21210
+ entries = readdirSync7(root, { withFileTypes: true });
21229
21211
  } catch {
21230
- return "unknown";
21212
+ return 0;
21213
+ }
21214
+ for (const entry of entries) {
21215
+ if (!entry.isDirectory()) continue;
21216
+ if (layout === "pristine") {
21217
+ total += dirSizeBytes(path30.join(root, entry.name, "graphify-out"));
21218
+ } else {
21219
+ const worldDir = path30.join(root, entry.name);
21220
+ let clones;
21221
+ try {
21222
+ clones = readdirSync7(worldDir, { withFileTypes: true });
21223
+ } catch {
21224
+ continue;
21225
+ }
21226
+ for (const clone of clones) {
21227
+ if (!clone.isDirectory()) continue;
21228
+ total += dirSizeBytes(path30.join(worldDir, clone.name, "graphify-out"));
21229
+ }
21230
+ }
21231
21231
  }
21232
+ return total;
21232
21233
  }
21233
- function writeLine(stderr, payload) {
21234
- try {
21235
- const line = `${INSTRUMENTATION_PREFIX}${JSON.stringify(payload)}
21236
- `;
21237
- stderr.write(line);
21238
- } catch (err) {
21234
+ function dirSizeBytes(dir) {
21235
+ if (!existsSync28(dir)) return 0;
21236
+ let total = 0;
21237
+ const stack = [dir];
21238
+ while (stack.length > 0) {
21239
+ const cur = stack.pop();
21240
+ let entries;
21239
21241
  try {
21240
- stderr.write(
21241
- `[WARN] olam instrumentation emit failed: ${err instanceof Error ? err.message : String(err)}
21242
- `
21243
- );
21242
+ entries = readdirSync7(cur, { withFileTypes: true });
21244
21243
  } catch {
21244
+ continue;
21245
+ }
21246
+ for (const entry of entries) {
21247
+ const full = path30.join(cur, entry.name);
21248
+ if (entry.isSymbolicLink()) continue;
21249
+ if (entry.isDirectory()) {
21250
+ stack.push(full);
21251
+ continue;
21252
+ }
21253
+ try {
21254
+ total += statSync6(full).size;
21255
+ } catch {
21256
+ }
21245
21257
  }
21246
21258
  }
21259
+ return total;
21247
21260
  }
21248
- function emitSubstrateSet(payload, opts = {}) {
21249
- const event = {
21250
- schema: INSTRUMENTATION_SCHEMA,
21251
- event: "substrate.set",
21252
- install_id: resolveInstallId(opts),
21253
- ts: nowIso(opts.now),
21254
- substrate: payload.substrate,
21255
- ...payload.flavor !== void 0 ? { flavor: payload.flavor } : {}
21256
- };
21257
- writeLine(opts.stderr ?? process.stderr, event);
21261
+ function formatBytes(n) {
21262
+ if (n < 1024) return `${n} B`;
21263
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
21264
+ if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
21265
+ return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
21258
21266
  }
21259
- function emitUpgradeComplete(payload, opts = {}) {
21260
- const event = {
21261
- schema: INSTRUMENTATION_SCHEMA,
21262
- event: "upgrade.complete",
21263
- install_id: resolveInstallId(opts),
21264
- ts: nowIso(opts.now),
21265
- substrate: payload.substrate,
21266
- duration_ms: payload.duration_ms,
21267
- ...payload.flavor !== void 0 ? { flavor: payload.flavor } : {}
21268
- };
21269
- writeLine(opts.stderr ?? process.stderr, event);
21267
+ async function probeEngine(fetchImpl = defaultFetch) {
21268
+ const controller = new AbortController();
21269
+ const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
21270
+ try {
21271
+ const res = await fetchImpl("http://127.0.0.1:19000/health", { signal: controller.signal });
21272
+ await res.text().catch(() => "");
21273
+ if (res.status !== 200) {
21274
+ return {
21275
+ ok: false,
21276
+ message: `host-cp /health returned ${res.status}; engine unknown`,
21277
+ remedy: "Restart host-cp via `olam host-cp start`; verify port 19000 is bound to 127.0.0.1."
21278
+ };
21279
+ }
21280
+ const engineHeader = res.headers.get("x-olam-engine") ?? res.headers.get("X-Olam-Engine");
21281
+ if (!engineHeader) {
21282
+ return {
21283
+ ok: true,
21284
+ message: "engine unknown (X-Olam-Engine header absent \u2014 older host-cp?)"
21285
+ };
21286
+ }
21287
+ return {
21288
+ ok: true,
21289
+ message: `engine ${engineHeader}`,
21290
+ engineName: engineHeader
21291
+ };
21292
+ } catch (err) {
21293
+ const e = err;
21294
+ const reason = e.name === "AbortError" ? `timeout (${HEALTH_TIMEOUT_MS}ms)` : e.message;
21295
+ return {
21296
+ ok: false,
21297
+ message: `engine probe unreachable: ${reason}`,
21298
+ remedy: "Restart host-cp via `olam host-cp start`; verify port 19000 is bound to 127.0.0.1."
21299
+ };
21300
+ } finally {
21301
+ clearTimeout(timeout);
21302
+ }
21270
21303
  }
21271
-
21272
- // src/lib/manifest-refresh.ts
21273
- import * as fs30 from "node:fs";
21274
- import * as path31 from "node:path";
21275
- init_install_root();
21276
- var MANIFEST_REFRESH_AUDIT_LOG = path31.join(OLAM_STATE_DIR, "manifest-refresh-audit.jsonl");
21277
- var SECURITY_SENSITIVE_FIELDS = [
21278
- "securityContext",
21279
- "resources.limits",
21280
- "capabilities",
21281
- "readOnlyRootFilesystem",
21282
- "rules"
21283
- // RBAC Role rules
21284
- ];
21285
- function seedManifestsFromBundle(forceRefresh, deps = {}) {
21286
- const existsSyncImpl = deps.existsSync ?? fs30.existsSync;
21287
- const cpSyncImpl = deps.cpSync ?? fs30.cpSync;
21288
- const mkdirSyncImpl = deps.mkdirSync ?? fs30.mkdirSync;
21289
- const rootDir = deps.installRootDir ?? installRoot();
21290
- const sourcePath = path31.join(rootDir, "host-cp", "k8s", "manifests");
21291
- const OLAM_HOME3 = path31.join(
21292
- process.env.HOME ?? process.env.USERPROFILE ?? "/tmp",
21293
- ".olam"
21294
- );
21295
- const targetPath = deps.targetManifestsDir ?? path31.join(OLAM_HOME3, "k8s", "manifests");
21296
- if (!forceRefresh && existsSyncImpl(targetPath)) {
21304
+ async function probeColimaVersion(dockerExec = defaultDockerExec2) {
21305
+ const r = dockerExec("colima", ["version"]);
21306
+ if (r.status === null || r.status !== 0 && /not found|ENOENT|not.found/i.test(r.stderr)) {
21307
+ return { ok: true, message: "colima not installed (not in use)" };
21308
+ }
21309
+ if (r.status !== 0) {
21297
21310
  return {
21298
- seeded: false,
21299
- skipped: true,
21300
- reason: `~/.olam/k8s/manifests/ already exists \u2014 skipping seed (use --force-refresh-manifests to overwrite)`
21311
+ ok: true,
21312
+ message: `colima present but version probe failed: ${(r.stderr || "").trim()}`
21301
21313
  };
21302
21314
  }
21303
- if (!existsSyncImpl(sourcePath)) {
21315
+ const tokens = r.stdout.split(/\s+/);
21316
+ const version = tokens.find((t) => /^v?\d+\.\d+\.\d+/.test(t)) ?? "";
21317
+ if (!version) {
21304
21318
  return {
21305
- seeded: false,
21306
- skipped: true,
21307
- reason: `Bundled manifests not found at ${sourcePath} \u2014 skipping seed (dev/monorepo context).
21308
- If running from a published install, re-run npm install to restore the bundle.`
21319
+ ok: true,
21320
+ message: `colima present but version unrecognised: ${r.stdout.trim().slice(0, 80)}`
21309
21321
  };
21310
21322
  }
21311
- try {
21312
- mkdirSyncImpl(path31.dirname(targetPath), { recursive: true });
21313
- cpSyncImpl(sourcePath, targetPath, { recursive: true });
21314
- } catch (err) {
21323
+ if (COLIMA_010_RANGE.test(version)) {
21315
21324
  return {
21316
- seeded: false,
21317
- skipped: false,
21318
- error: `Failed to seed manifests from ${sourcePath} to ${targetPath}: ${err instanceof Error ? err.message : String(err)}`
21325
+ ok: true,
21326
+ warn: true,
21327
+ message: `colima ${version} detected \u2014 known kubernetes-mode regression`,
21328
+ remedy: COLIMA_010_WARN_TEXT
21319
21329
  };
21320
21330
  }
21331
+ return { ok: true, message: `colima ${version} (clear)` };
21332
+ }
21333
+ async function probeK8sApiReachable(exec = defaultDockerExec2) {
21334
+ const r = exec("kubectl", ["version", "--output=json", "--request-timeout=2s"]);
21335
+ if (r.status === 0 && r.stdout.length > 0) {
21336
+ let parsed;
21337
+ try {
21338
+ parsed = JSON.parse(r.stdout);
21339
+ } catch {
21340
+ parsed = null;
21341
+ }
21342
+ const obj = parsed && typeof parsed === "object" ? parsed : {};
21343
+ const serverInfo = obj.serverVersion;
21344
+ const gitVersion = typeof serverInfo?.gitVersion === "string" ? serverInfo.gitVersion : "unknown";
21345
+ return { ok: true, message: `api server ${gitVersion} reachable` };
21346
+ }
21321
21347
  return {
21322
- seeded: true,
21323
- source: sourcePath,
21324
- target: targetPath,
21325
- message: forceRefresh ? `Manifests force-refreshed from ${sourcePath} \u2192 ${targetPath}` : `Manifests seeded from ${sourcePath} \u2192 ${targetPath}`
21348
+ ok: false,
21349
+ message: "api server not reachable",
21350
+ remedy: "Confirm the cluster is up (e.g. `k3d cluster list`) and your context points at it."
21326
21351
  };
21327
21352
  }
21328
- function extractField(obj, fieldKey) {
21329
- if (typeof obj !== "object" || obj === null) return "";
21330
- const keys = fieldKey.split(".");
21331
- let current = obj;
21332
- for (const k of keys) {
21333
- if (typeof current !== "object" || current === null) return "";
21334
- current = current[k];
21335
- }
21336
- if (current === void 0) return "";
21337
- return JSON.stringify(current);
21338
- }
21339
- function extractSecurityFieldsFromParsed(parsed) {
21340
- const result = {};
21341
- for (const field of SECURITY_SENSITIVE_FIELDS) {
21342
- result[field] = extractField(parsed, field);
21343
- }
21344
- return result;
21345
- }
21346
- function diffManifestSecurityFields(oldContent, newContent) {
21347
- let oldParsed;
21348
- let newParsed;
21349
- try {
21350
- oldParsed = JSON.parse(oldContent);
21351
- } catch {
21352
- oldParsed = { _raw: oldContent };
21353
- }
21354
- try {
21355
- newParsed = JSON.parse(newContent);
21356
- } catch {
21357
- newParsed = { _raw: newContent };
21358
- }
21359
- const oldFields = extractSecurityFieldsFromParsed(oldParsed);
21360
- const newFields = extractSecurityFieldsFromParsed(newParsed);
21361
- const changedFields = [];
21362
- for (const field of SECURITY_SENSITIVE_FIELDS) {
21363
- if (oldFields[field] !== newFields[field]) {
21364
- changedFields.push(field);
21365
- }
21366
- }
21367
- return {
21368
- hasSecurityRegression: changedFields.length > 0,
21369
- changedFields
21370
- };
21371
- }
21372
- function appendAuditEntry(entry, auditLogPath, writeFileSyncImpl) {
21373
- const line = JSON.stringify(entry) + "\n";
21374
- try {
21375
- writeFileSyncImpl(auditLogPath, line, {
21376
- encoding: "utf8",
21377
- flag: "a",
21378
- mode: 384
21379
- });
21380
- } catch (err) {
21381
- throw new Error(`audit log unavailable: ${err instanceof Error ? err.message : String(err)}`);
21382
- }
21383
- }
21384
- async function runManifestRefresh(manifestsDir, acceptRegression, deps = {}, peripheral) {
21385
- const auditLogPath = deps.auditLogPath ?? MANIFEST_REFRESH_AUDIT_LOG;
21386
- const readdirSync25 = deps.readdirSync ?? fs30.readdirSync;
21387
- const readFileSync74 = deps.readFileSync ?? fs30.readFileSync;
21388
- const writeFileSyncImpl = deps.writeFileSync ?? fs30.writeFileSync;
21389
- const existsSync99 = deps.existsSync ?? fs30.existsSync;
21390
- const now = deps.now ? deps.now() : /* @__PURE__ */ new Date();
21391
- const targetDir = peripheral ? path31.join(manifestsDir, peripheral) : manifestsDir;
21392
- if (!existsSync99(targetDir)) {
21353
+ async function probeK8sImagePresence(refs, exec = defaultDockerExec2) {
21354
+ const r = exec("kubectl", ["get", "nodes", "-o", "json", "--request-timeout=2s"]);
21355
+ if (r.status !== 0) {
21393
21356
  return {
21394
21357
  ok: false,
21395
- message: peripheral ? `peripheral manifests directory not found: ${targetDir}` : `manifests directory not found: ${targetDir}`
21358
+ message: "unable to list cluster nodes for image presence check",
21359
+ remedy: "Confirm the cluster is up and your context points at it; re-run `olam doctor`."
21396
21360
  };
21397
21361
  }
21398
- let files;
21362
+ let parsed;
21399
21363
  try {
21400
- const entries = readdirSync25(targetDir, { withFileTypes: true });
21401
- files = entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".json"))).map((e) => e.name);
21402
- } catch (err) {
21364
+ parsed = JSON.parse(r.stdout);
21365
+ } catch {
21403
21366
  return {
21404
21367
  ok: false,
21405
- message: `failed to read manifests dir: ${err instanceof Error ? err.message : String(err)}`
21368
+ message: "cluster node list returned malformed output",
21369
+ remedy: "Restart your cluster; if persistent, re-create it."
21406
21370
  };
21407
21371
  }
21408
- const allChangedFields = [];
21409
- for (const file of files) {
21410
- const filePath = path31.join(targetDir, file);
21411
- let content;
21412
- try {
21413
- content = readFileSync74(filePath, "utf8");
21414
- } catch {
21415
- continue;
21416
- }
21417
- const diff = diffManifestSecurityFields("{}", content);
21418
- for (const f of diff.changedFields) {
21419
- if (!allChangedFields.includes(f)) allChangedFields.push(f);
21420
- }
21421
- }
21422
- const hasSecurityRegression = allChangedFields.length > 0;
21423
- if (hasSecurityRegression && !acceptRegression) {
21372
+ const nodes = parsed && typeof parsed === "object" ? parsed.items : void 0;
21373
+ if (!Array.isArray(nodes) || nodes.length === 0) {
21424
21374
  return {
21425
21375
  ok: false,
21426
- message: `manifest refresh refused: security-sensitive fields changed (${allChangedFields.join(", ")}).
21427
- Re-run with --accept-security-regression to acknowledge and proceed.`
21376
+ message: "cluster has no nodes",
21377
+ remedy: "Start the cluster and try again."
21428
21378
  };
21429
21379
  }
21430
- const entry = {
21431
- ts: now.toISOString(),
21432
- manifests_path: targetDir,
21433
- security_regression: hasSecurityRegression,
21434
- changed_fields: allChangedFields,
21435
- accepted: acceptRegression,
21436
- operator_pid: process.pid,
21437
- ...peripheral !== void 0 ? { peripheral } : {}
21438
- };
21439
- try {
21440
- appendAuditEntry(entry, auditLogPath, writeFileSyncImpl);
21441
- } catch (err) {
21442
- return {
21443
- ok: false,
21444
- message: err instanceof Error ? err.message : String(err)
21445
- };
21380
+ const presentImages = /* @__PURE__ */ new Set();
21381
+ for (const node of nodes) {
21382
+ const status2 = node.status;
21383
+ const images = status2?.images;
21384
+ if (!Array.isArray(images)) continue;
21385
+ for (const img of images) {
21386
+ const names = img.names;
21387
+ if (!Array.isArray(names)) continue;
21388
+ for (const n of names) presentImages.add(n);
21389
+ }
21446
21390
  }
21447
- return {
21448
- ok: true,
21449
- message: hasSecurityRegression ? `Security-sensitive fields changed (${allChangedFields.join(", ")}); accepted via --accept-security-regression. Audit entry written.` : "Manifest refresh completed (no security-sensitive field changes). Audit entry written."
21450
- };
21451
- }
21452
-
21453
- // src/lib/upgrade-kubernetes.ts
21454
- var OLAM_K8S_MANIFESTS_DIR = path32.join(OLAM_HOME, "k8s", "manifests");
21455
- var K8S_NAMESPACE2 = "olam";
21456
- var HOST_CP_SECRET_NAME = "olam-host-cp-secret";
21457
- var HOST_CP_DEPLOYMENT_NAME = "olam-host-cp";
21458
- var PORT_FORWARD_TARGET = "service/olam-host-cp";
21459
- var HOST_CP_HEALTH_URL = "http://127.0.0.1:19000/health";
21460
- var SUBSTRATE_AUDIT_LOG = path32.join(OLAM_STATE_DIR, "substrate-audit.jsonl");
21461
- var PLACEHOLDER_VALUES = /* @__PURE__ */ new Set(["OLAM_AUTH_SECRET", "GH_TOKEN"]);
21462
- var REQUIRED_SECRET_KEYS = ["OLAM_AUTH_SECRET", "GH_TOKEN"];
21463
- var PERIPHERAL_SECRETS = [
21464
- { name: "auth-service", secretName: "olam-auth-service-secret", keys: ["OLAM_AUTH_DB_SECRET"] },
21465
- { name: "mcp-auth-service", secretName: "olam-mcp-auth-service-secret", keys: ["OLAM_MCP_AUTH_JWT_SECRET"] },
21466
- { name: "kg-service", secretName: "olam-kg-service-secret", keys: ["OLAM_KG_BEARER_TOKEN"] },
21467
- { name: "memory-service", secretName: "olam-memory-service-secret", keys: ["OLAM_MEMORY_BEARER_SECRET"] }
21468
- ];
21469
- var K8S_DNS_SUFFIX = "olam.svc.cluster.local";
21470
- function buildK8sDnsUrl(k8sServiceName, port2) {
21471
- return `http://${k8sServiceName}.${K8S_DNS_SUFFIX}:${port2}`;
21472
- }
21473
- function appendSubstrateAuditEntry(entry, stderr) {
21474
- try {
21475
- const line = JSON.stringify(entry) + "\n";
21476
- const dir = path32.dirname(SUBSTRATE_AUDIT_LOG);
21477
- if (!fs31.existsSync(dir)) fs31.mkdirSync(dir, { recursive: true });
21478
- fs31.writeFileSync(SUBSTRATE_AUDIT_LOG, line, { encoding: "utf8", flag: "a", mode: 384 });
21479
- } catch (err) {
21480
- stderr.write(
21481
- `${pc9.yellow("[warn]")} could not write substrate audit log: ${err instanceof Error ? err.message : String(err)}
21482
- `
21483
- );
21391
+ for (const ref of refs) {
21392
+ if (!hasImageRef(presentImages, ref)) {
21393
+ return {
21394
+ ok: false,
21395
+ message: `image ${ref} not present on any cluster node`,
21396
+ remedy: "Run `olam bootstrap` to import the published image into the cluster."
21397
+ };
21398
+ }
21484
21399
  }
21400
+ return { ok: true, message: `${refs.length} image(s) present on cluster` };
21485
21401
  }
21486
- function isContextAllowed(context) {
21487
- return typeof context === "string" && context.length > 0;
21402
+ function hasImageRef(present, ref) {
21403
+ if (present.has(ref)) return true;
21404
+ const variants = [`docker.io/library/${ref}`, `docker.io/${ref}`];
21405
+ return variants.some((v) => present.has(v));
21488
21406
  }
21489
- var MANAGED_K8S_URL_PATTERNS = [
21490
- { pattern: /\.amazonaws\.com(:\d+)?$/, provider: "EKS (Amazon)" },
21491
- { pattern: /\.googleapis\.com(:\d+)?$/, provider: "GKE (Google)" },
21492
- { pattern: /\.gke\.io(:\d+)?$/, provider: "GKE (Google)" },
21493
- { pattern: /\.azmk8s\.io(:\d+)?$/, provider: "AKS (Azure)" },
21494
- { pattern: /\.k8s\.ondigitalocean\.com(:\d+)?$/, provider: "DOKS (DigitalOcean)" },
21495
- { pattern: /\.civo\.com(:\d+)?$/, provider: "Civo" }
21496
- ];
21497
- async function detectManagedK8sProvider(deps) {
21498
- if (deps.getClusterServerUrlImpl) {
21499
- const url3 = await deps.getClusterServerUrlImpl();
21500
- if (!url3) return null;
21501
- for (const { pattern, provider } of MANAGED_K8S_URL_PATTERNS) {
21502
- if (pattern.test(url3)) return provider;
21503
- }
21504
- return null;
21505
- }
21506
- const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
21507
- const result = await wrap(
21508
- ["config", "view", "--minify", "-o", "jsonpath={.clusters[0].cluster.server}"],
21509
- { timeout: 5e3 }
21510
- );
21511
- if (!result.ok || !result.stdout.trim()) return null;
21512
- const url2 = result.stdout.trim();
21513
- for (const { pattern, provider } of MANAGED_K8S_URL_PATTERNS) {
21514
- if (pattern.test(url2)) return provider;
21515
- }
21516
- return null;
21407
+ function defaultDetectK8sClusterType(kubectlContext) {
21408
+ const ctx = kubectlContext ?? readCurrentKubectlContext();
21409
+ if (!ctx) return "unknown";
21410
+ if (ctx.startsWith("colima")) return "colima";
21411
+ if (ctx.startsWith("k3d-")) return "k3d";
21412
+ return "unknown";
21517
21413
  }
21518
- async function checkDockerSocketAccessible(context, deps) {
21519
- if (deps.checkDockerSocketImpl) {
21520
- return deps.checkDockerSocketImpl(context);
21414
+ function readCurrentKubectlContext() {
21415
+ const r = spawnSync12("kubectl", ["config", "current-context"], {
21416
+ encoding: "utf-8",
21417
+ stdio: ["ignore", "pipe", "pipe"]
21418
+ });
21419
+ if (r.status !== 0) return void 0;
21420
+ return r.stdout?.trim() || void 0;
21421
+ }
21422
+ function buildDockerSocketRemedy(clusterType) {
21423
+ if (clusterType === "colima") {
21424
+ return "Recreate the Colima k3s cluster with the docker socket bind-mount:\n colima stop\n colima start --kubernetes \\\n --mount /var/run/docker.sock:/var/run/docker.sock:w \\\n --mount ~/.config/gh:/host/.config/gh:r\nThen run: olam upgrade";
21521
21425
  }
21522
- const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
21523
- const result = await wrap(
21524
- [
21525
- "--context",
21526
- context,
21527
- "exec",
21528
- "deploy/olam-host-cp",
21529
- "-n",
21530
- "olam",
21531
- "--",
21532
- "test",
21533
- "-S",
21534
- "/var/run/docker.sock"
21535
- ],
21536
- { timeout: 1e4 }
21537
- );
21538
- return result.ok;
21426
+ return "Recreate the k3d cluster with the docker socket bind-mount:\n k3d cluster create olam-host \\\n --volume /var/run/docker.sock:/var/run/docker.sock@server:* \\\n --volume ~/.config/gh:/host/.config/gh \\\n --wait --timeout 90s\nThen run: olam upgrade";
21539
21427
  }
21540
- async function probeKubernetesApiReachable(context, deps) {
21541
- const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
21542
- const result = await wrap(
21543
- ["--context", context, "cluster-info"],
21544
- { timeout: 5e3 }
21545
- );
21546
- return result.ok;
21428
+
21429
+ // src/lib/port-forward.ts
21430
+ import { spawn as spawn5 } from "node:child_process";
21431
+ import * as fs29 from "node:fs";
21432
+ import * as net3 from "node:net";
21433
+ import * as path31 from "node:path";
21434
+ import * as os16 from "node:os";
21435
+ var PORT_FORWARD_PORT = 19e3;
21436
+ var PORT_FORWARD_LOCK_PATH = path31.join(OLAM_STATE_DIR, "port-forward.lock");
21437
+ var PORT_FORWARD_PID_PATH = path31.join(OLAM_STATE_DIR, "port-forward.pid");
21438
+ var TCP_PROBE_TIMEOUT_MS = 2e3;
21439
+ async function probePortForwardLiveness(deps = {}) {
21440
+ const probe2 = deps.tcpProbeImpl ?? realTcpProbe;
21441
+ return probe2("127.0.0.1", PORT_FORWARD_PORT, TCP_PROBE_TIMEOUT_MS);
21547
21442
  }
21548
- async function checkSecretPreCondition(context, deps) {
21549
- const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
21550
- const result = await wrap(
21551
- [
21552
- "--context",
21553
- context,
21554
- "get",
21555
- "secret",
21556
- HOST_CP_SECRET_NAME,
21557
- "-n",
21558
- K8S_NAMESPACE2,
21559
- "-o",
21560
- "json"
21561
- ],
21562
- { timeout: 15e3 }
21563
- );
21564
- if (!result.ok) {
21565
- return `Secret "${HOST_CP_SECRET_NAME}" not found in namespace "${K8S_NAMESPACE2}".
21566
- Create it first: kubectl --context ${context} apply -f <your-secret.yaml>
21567
- ${result.stderr.split("\n")[0] ?? ""}`;
21568
- }
21569
- let secretJson;
21570
- try {
21571
- secretJson = JSON.parse(result.stdout);
21572
- } catch {
21573
- return `Failed to parse Secret JSON: ${result.stdout.slice(0, 200)}`;
21574
- }
21575
- const data = secretJson.data ?? {};
21576
- for (const key of REQUIRED_SECRET_KEYS) {
21577
- if (!(key in data)) {
21578
- return `Secret "${HOST_CP_SECRET_NAME}" is missing required key "${key}".
21579
- Keys found: ${Object.keys(data).join(", ") || "(none)"}
21580
- Add the key and re-run.`;
21443
+ function realTcpProbe(host, port2, timeoutMs) {
21444
+ return new Promise((resolve23) => {
21445
+ const socket = new net3.Socket();
21446
+ let done = false;
21447
+ function finish(result) {
21448
+ if (done) return;
21449
+ done = true;
21450
+ socket.destroy();
21451
+ resolve23(result);
21581
21452
  }
21582
- const b64 = data[key] ?? "";
21583
- const decoded = Buffer.from(b64, "base64").toString("utf8");
21584
- if (PLACEHOLDER_VALUES.has(decoded)) {
21585
- return `Secret "${HOST_CP_SECRET_NAME}" key "${key}" still holds its placeholder value.
21586
- Replace the placeholder with a real value, then re-run.`;
21453
+ const timer = setTimeout(() => finish(false), timeoutMs);
21454
+ socket.once("connect", () => {
21455
+ clearTimeout(timer);
21456
+ finish(true);
21457
+ });
21458
+ socket.once("error", () => {
21459
+ clearTimeout(timer);
21460
+ finish(false);
21461
+ });
21462
+ socket.once("timeout", () => {
21463
+ clearTimeout(timer);
21464
+ finish(false);
21465
+ });
21466
+ socket.connect(port2, host);
21467
+ });
21468
+ }
21469
+ function acquireAdvisoryLock(lockPath) {
21470
+ const fd = fs29.openSync(lockPath, fs29.constants.O_CREAT | fs29.constants.O_EXCL | fs29.constants.O_WRONLY);
21471
+ fs29.closeSync(fd);
21472
+ return () => {
21473
+ try {
21474
+ fs29.unlinkSync(lockPath);
21475
+ } catch {
21587
21476
  }
21477
+ };
21478
+ }
21479
+ function pidFilePath(deps) {
21480
+ const stateDir = deps.stateDir ?? OLAM_STATE_DIR;
21481
+ return path31.join(stateDir, "port-forward.pid");
21482
+ }
21483
+ function lockFilePath(deps) {
21484
+ const stateDir = deps.stateDir ?? OLAM_STATE_DIR;
21485
+ return path31.join(stateDir, "port-forward.lock");
21486
+ }
21487
+ function writePidFile(pidPath2, pid) {
21488
+ const dir = path31.dirname(pidPath2);
21489
+ if (!fs29.existsSync(dir)) {
21490
+ fs29.mkdirSync(dir, { recursive: true });
21588
21491
  }
21589
- return null;
21492
+ const tmp = `${pidPath2}.tmp.${process.pid}`;
21493
+ fs29.writeFileSync(tmp, String(pid) + "\n", { encoding: "utf8", mode: 384 });
21494
+ fs29.renameSync(tmp, pidPath2);
21590
21495
  }
21591
- async function applyConfigMapSubstitution(context, manifestsDir, deps) {
21592
- const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
21593
- const readFileSync74 = deps.readFileSyncImpl ?? fs31.readFileSync;
21594
- const configMapPath = path32.join(manifestsDir, "30-configmap.yaml");
21595
- let rawYaml;
21496
+ async function spawnPortForward(context, namespace, target, localPort = PORT_FORWARD_PORT, remotePort = PORT_FORWARD_PORT, deps = {}) {
21497
+ const lockPath = lockFilePath(deps);
21498
+ const pidPath2 = pidFilePath(deps);
21499
+ const spawnImpl = deps.spawnImpl ?? spawn5;
21500
+ const stateDir = deps.stateDir ?? OLAM_STATE_DIR;
21501
+ if (!fs29.existsSync(stateDir)) {
21502
+ fs29.mkdirSync(stateDir, { recursive: true });
21503
+ }
21504
+ let releaseLock2 = null;
21596
21505
  try {
21597
- rawYaml = readFileSync74(configMapPath, "utf8");
21506
+ releaseLock2 = acquireAdvisoryLock(lockPath);
21598
21507
  } catch (err) {
21599
- return `Failed to read ConfigMap at ${configMapPath}: ${err instanceof Error ? err.message : String(err)}`;
21508
+ const code = err.code;
21509
+ if (code === "EEXIST") {
21510
+ return { spawned: false, reason: "lock-held" };
21511
+ }
21512
+ throw err;
21600
21513
  }
21601
- let parsed;
21602
21514
  try {
21603
- parsed = yamlParse(rawYaml);
21604
- } catch (err) {
21605
- return `Failed to parse ConfigMap YAML at ${configMapPath}: ${err instanceof Error ? err.message : String(err)}`;
21515
+ const live = await probePortForwardLiveness(deps);
21516
+ if (live) {
21517
+ return { spawned: false, reason: "live" };
21518
+ }
21519
+ const child = spawnImpl(
21520
+ "kubectl",
21521
+ [
21522
+ "--context",
21523
+ context,
21524
+ "port-forward",
21525
+ "-n",
21526
+ namespace,
21527
+ target,
21528
+ `${localPort}:${remotePort}`
21529
+ ],
21530
+ {
21531
+ stdio: ["ignore", "ignore", "ignore"],
21532
+ detached: true
21533
+ }
21534
+ );
21535
+ if (typeof child.pid !== "number" || child.pid <= 0) {
21536
+ return { spawned: false, reason: "lock-held" };
21537
+ }
21538
+ writePidFile(pidPath2, child.pid);
21539
+ child.unref();
21540
+ child.once("exit", () => {
21541
+ try {
21542
+ fs29.unlinkSync(pidPath2);
21543
+ } catch {
21544
+ }
21545
+ });
21546
+ return { spawned: true, pid: child.pid };
21547
+ } finally {
21548
+ releaseLock2();
21606
21549
  }
21607
- const data = parsed["data"] ?? {};
21608
- for (const peripheral of PERIPHERALS) {
21609
- data[peripheral.configMapKeyInHostCp] = buildK8sDnsUrl(peripheral.k8sServiceName, peripheral.port);
21550
+ }
21551
+ function peripheralPidPath(name, stateDir) {
21552
+ return path31.join(stateDir, `port-forward-${name}.pid`);
21553
+ }
21554
+ function peripheralLockPath(name, stateDir) {
21555
+ return path31.join(stateDir, `port-forward-${name}.lock`);
21556
+ }
21557
+ async function spawnPeripheralPortForward(peripheral, context, namespace, deps = {}) {
21558
+ const stateDir = deps.stateDir ?? OLAM_STATE_DIR;
21559
+ const spawnImpl = deps.spawnImpl ?? spawn5;
21560
+ const tcpProbeImpl = deps.tcpProbeImpl ?? realTcpProbe;
21561
+ if (!fs29.existsSync(stateDir)) {
21562
+ fs29.mkdirSync(stateDir, { recursive: true });
21610
21563
  }
21611
- parsed["data"] = data;
21612
- const patchedYaml = yamlStringify(parsed);
21613
- const result = await wrap(
21614
- ["--context", context, "apply", "-f", "-"],
21615
- { timeout: 3e4, stdin: patchedYaml }
21616
- );
21617
- if (!result.ok) {
21618
- return `kubectl apply (ConfigMap substitution) failed: ${result.stderr.split("\n")[0] ?? ""}`;
21564
+ const lockPath = peripheralLockPath(peripheral.name, stateDir);
21565
+ const pidPath2 = peripheralPidPath(peripheral.name, stateDir);
21566
+ let releaseLock2 = null;
21567
+ try {
21568
+ releaseLock2 = acquireAdvisoryLock(lockPath);
21569
+ } catch (err) {
21570
+ const code = err.code;
21571
+ if (code === "EEXIST") {
21572
+ return false;
21573
+ }
21574
+ throw err;
21619
21575
  }
21620
- return null;
21621
- }
21622
- async function checkPeripheralSecrets(context, deps) {
21623
- const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
21624
- for (const { name, secretName, keys } of PERIPHERAL_SECRETS) {
21625
- const result = await wrap(
21576
+ try {
21577
+ const alive = await tcpProbeImpl("127.0.0.1", peripheral.port, TCP_PROBE_TIMEOUT_MS);
21578
+ if (alive) {
21579
+ return false;
21580
+ }
21581
+ const child = spawnImpl(
21582
+ "kubectl",
21626
21583
  [
21627
21584
  "--context",
21628
21585
  context,
21629
- "get",
21630
- "secret",
21631
- secretName,
21586
+ "port-forward",
21632
21587
  "-n",
21633
- K8S_NAMESPACE2,
21634
- "-o",
21635
- "json"
21588
+ namespace,
21589
+ `service/${peripheral.k8sServiceName}`,
21590
+ `${peripheral.port}:${peripheral.port}`
21636
21591
  ],
21637
- { timeout: 15e3 }
21592
+ {
21593
+ stdio: ["ignore", "ignore", "ignore"],
21594
+ detached: true
21595
+ }
21638
21596
  );
21639
- if (!result.ok) {
21640
- return `Peripheral Secret "${secretName}" (${name}) not found in namespace "${K8S_NAMESPACE2}".
21641
- Create it first: kubectl --context ${context} apply -f <your-${name}-secret.yaml>
21642
- ${result.stderr.split("\n")[0] ?? ""}`;
21643
- }
21644
- let secretJson;
21645
- try {
21646
- secretJson = JSON.parse(result.stdout);
21647
- } catch {
21648
- return `Failed to parse Secret JSON for "${secretName}" (${name}): ${result.stdout.slice(0, 200)}`;
21597
+ if (typeof child.pid !== "number" || child.pid <= 0) {
21598
+ return false;
21649
21599
  }
21650
- const data = secretJson.data ?? {};
21651
- for (const key of keys) {
21652
- if (!(key in data)) {
21653
- return `Peripheral Secret "${secretName}" (${name}) is missing required key "${key}".
21654
- Keys found: ${Object.keys(data).join(", ") || "(none)"}
21655
- Add the key and re-run.`;
21656
- }
21657
- const b64 = data[key] ?? "";
21658
- const decoded = Buffer.from(b64, "base64").toString("utf8");
21659
- if (decoded.startsWith("REPLACE_ME_")) {
21660
- return `Peripheral Secret "${secretName}" (${name}) key "${key}" still holds its placeholder value.
21661
- Replace the placeholder with a real value, then re-run.`;
21600
+ writePidFile(pidPath2, child.pid);
21601
+ child.unref();
21602
+ child.once("exit", () => {
21603
+ try {
21604
+ fs29.unlinkSync(pidPath2);
21605
+ } catch {
21662
21606
  }
21663
- }
21607
+ });
21608
+ return true;
21609
+ } finally {
21610
+ releaseLock2();
21664
21611
  }
21665
- return null;
21666
21612
  }
21667
- async function waitForCoreDns(context, deps) {
21668
- const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
21669
- const result = await wrap(
21670
- [
21671
- "--context",
21672
- context,
21673
- "wait",
21674
- "--for=condition=Available",
21675
- "deployment/coredns",
21676
- "-n",
21677
- "kube-system",
21678
- "--timeout=30s"
21679
- ],
21680
- { timeout: 35e3 }
21613
+ async function spawnAllPeripheralPortForwards(context = "olam", namespace = "olam", deps = {}) {
21614
+ await Promise.all(
21615
+ PERIPHERALS.map((p) => spawnPeripheralPortForward(p, context, namespace, deps))
21681
21616
  );
21682
- return result.ok;
21683
21617
  }
21684
- async function verifyHealthHeader(deps) {
21685
- const fetchImpl = deps.fetchImpl ?? fetch;
21618
+
21619
+ // src/lib/instrumentation.ts
21620
+ var INSTRUMENTATION_SCHEMA = "olam.instr.v1";
21621
+ var INSTRUMENTATION_PREFIX = "OLAM_INSTR ";
21622
+ function nowIso(now) {
21623
+ const d = now ? now() : /* @__PURE__ */ new Date();
21624
+ return d.toISOString();
21625
+ }
21626
+ function resolveInstallId(opts) {
21686
21627
  try {
21687
- const res = await fetchImpl(HOST_CP_HEALTH_URL, {
21688
- signal: AbortSignal.timeout(5e3)
21689
- });
21690
- const engine = res.headers.get("x-olam-engine") ?? res.headers.get("X-Olam-Engine");
21691
- return { ok: engine === "kubernetes", engineHeader: engine };
21628
+ if (opts.installIdProvider) return opts.installIdProvider();
21629
+ const cfg = readConfig();
21630
+ return cfg.install_id;
21692
21631
  } catch {
21693
- return { ok: false, engineHeader: null };
21632
+ return "unknown";
21694
21633
  }
21695
21634
  }
21696
- async function runUpgradeKubernetes(opts = {}, deps = {}) {
21697
- const stdout = deps.stdout ?? process.stdout;
21698
- const stderr = deps.stderr ?? process.stderr;
21699
- const manifestsDir = deps.manifestsDir ?? OLAM_K8S_MANIFESTS_DIR;
21700
- const startMs = Date.now();
21701
- {
21702
- const seedImpl = deps.seedManifestsImpl ?? seedManifestsFromBundle;
21703
- const seedResult = seedImpl(opts.forceRefreshManifests === true, deps.seedManifestsDeps ?? {});
21704
- if (seedResult.seeded) {
21705
- stdout.write(`${pc9.dim("[seed]")} ${seedResult.message}
21706
- `);
21707
- } else if (!seedResult.skipped) {
21708
- stderr.write(`${pc9.red("error:")} Manifest seed failed: ${seedResult.error}
21709
- `);
21710
- return { exitCode: 1, summary: "manifest seed failed" };
21711
- }
21712
- }
21713
- {
21714
- const managedProvider = await detectManagedK8sProvider(deps);
21715
- if (managedProvider !== null) {
21635
+ function writeLine(stderr, payload) {
21636
+ try {
21637
+ const line = `${INSTRUMENTATION_PREFIX}${JSON.stringify(payload)}
21638
+ `;
21639
+ stderr.write(line);
21640
+ } catch (err) {
21641
+ try {
21716
21642
  stderr.write(
21717
- `${pc9.red("error:")} olam upgrade supports local k3d/k3s clusters only.
21718
- Detected managed-k8s context: ${managedProvider}
21719
- hostPath /var/run/docker.sock is not accessible on managed clusters \u2014 the docker
21720
- socket does not reach the operator's host daemon on cloud-managed nodes.
21721
- See Decision #18 and docs/operator/kubernetes-substrate-beta.md
21722
- for supported cluster types and the active retraction of managed-k8s support.
21643
+ `[WARN] olam instrumentation emit failed: ${err instanceof Error ? err.message : String(err)}
21723
21644
  `
21724
21645
  );
21725
- return { exitCode: 1, summary: "managed-k8s context detected (Decision #18 retraction)" };
21646
+ } catch {
21726
21647
  }
21727
21648
  }
21728
- {
21729
- const preflightContext = process.env.OLAM_K8S_CONTEXT_ACK ?? "default";
21730
- const socketAccessible = await checkDockerSocketAccessible(preflightContext, deps);
21731
- if (!socketAccessible) {
21732
- stderr.write(
21733
- `${pc9.yellow("[WARN]")} docker socket not accessible at /var/run/docker.sock inside the host-cp pod.
21734
- This means the k3d cluster was not created with the docker socket bind-mount,
21735
- or host-cp is not yet deployed (first install \u2014 this warning is expected).
21736
- For subsequent upgrades, recreate the cluster with:
21737
- k3d cluster create olam-host \\
21738
- --volume /var/run/docker.sock:/var/run/docker.sock@server:* \\
21739
- --volume ~/.config/gh:/host/.config/gh \\
21740
- --volume <olam-repo>:/host/olam \\
21741
- --wait --timeout 90s
21742
- Run olam doctor to check probe 28 (probeDockerSocketBindMount).
21743
- `
21744
- );
21745
- }
21649
+ }
21650
+ function emitSubstrateSet(payload, opts = {}) {
21651
+ const event = {
21652
+ schema: INSTRUMENTATION_SCHEMA,
21653
+ event: "substrate.set",
21654
+ install_id: resolveInstallId(opts),
21655
+ ts: nowIso(opts.now),
21656
+ substrate: payload.substrate,
21657
+ ...payload.flavor !== void 0 ? { flavor: payload.flavor } : {}
21658
+ };
21659
+ writeLine(opts.stderr ?? process.stderr, event);
21660
+ }
21661
+ function emitUpgradeComplete(payload, opts = {}) {
21662
+ const event = {
21663
+ schema: INSTRUMENTATION_SCHEMA,
21664
+ event: "upgrade.complete",
21665
+ install_id: resolveInstallId(opts),
21666
+ ts: nowIso(opts.now),
21667
+ substrate: payload.substrate,
21668
+ duration_ms: payload.duration_ms,
21669
+ ...payload.flavor !== void 0 ? { flavor: payload.flavor } : {}
21670
+ };
21671
+ writeLine(opts.stderr ?? process.stderr, event);
21672
+ }
21673
+
21674
+ // src/lib/manifest-refresh.ts
21675
+ import * as fs30 from "node:fs";
21676
+ import * as path32 from "node:path";
21677
+ init_install_root();
21678
+ var MANIFEST_REFRESH_AUDIT_LOG = path32.join(OLAM_STATE_DIR, "manifest-refresh-audit.jsonl");
21679
+ var SECURITY_SENSITIVE_FIELDS = [
21680
+ "securityContext",
21681
+ "resources.limits",
21682
+ "capabilities",
21683
+ "readOnlyRootFilesystem",
21684
+ "rules"
21685
+ // RBAC Role rules
21686
+ ];
21687
+ function seedManifestsFromBundle(forceRefresh, deps = {}) {
21688
+ const existsSyncImpl = deps.existsSync ?? fs30.existsSync;
21689
+ const cpSyncImpl = deps.cpSync ?? fs30.cpSync;
21690
+ const mkdirSyncImpl = deps.mkdirSync ?? fs30.mkdirSync;
21691
+ const rootDir = deps.installRootDir ?? installRoot();
21692
+ const sourcePath = path32.join(rootDir, "host-cp", "k8s", "manifests");
21693
+ const OLAM_HOME3 = path32.join(
21694
+ process.env.HOME ?? process.env.USERPROFILE ?? "/tmp",
21695
+ ".olam"
21696
+ );
21697
+ const targetPath = deps.targetManifestsDir ?? path32.join(OLAM_HOME3, "k8s", "manifests");
21698
+ if (!forceRefresh && existsSyncImpl(targetPath)) {
21699
+ return {
21700
+ seeded: false,
21701
+ skipped: true,
21702
+ reason: `~/.olam/k8s/manifests/ already exists \u2014 skipping seed (use --force-refresh-manifests to overwrite)`
21703
+ };
21746
21704
  }
21747
- const step0Spinner = ora3("Probing Kubernetes API reachability").start();
21748
- const context = process.env.OLAM_K8S_CONTEXT_ACK ?? "";
21749
- const probeContext = context.length > 0 ? context : "default";
21750
- const reachable = await probeKubernetesApiReachable(probeContext, deps);
21751
- if (!reachable) {
21752
- step0Spinner.fail("Kubernetes API not reachable");
21753
- stderr.write(
21754
- `${pc9.red("error:")} kubectl cluster-info failed (5s timeout).
21755
- Ensure your kubectl context is correct and the cluster is reachable.
21756
- Set OLAM_K8S_CONTEXT_ACK=<context-name> to your active kubectl context.
21757
- `
21758
- );
21759
- return { exitCode: 1, summary: "kubernetes api not reachable" };
21705
+ if (!existsSyncImpl(sourcePath)) {
21706
+ return {
21707
+ seeded: false,
21708
+ skipped: true,
21709
+ reason: `Bundled manifests not found at ${sourcePath} \u2014 skipping seed (dev/monorepo context).
21710
+ If running from a published install, re-run npm install to restore the bundle.`
21711
+ };
21760
21712
  }
21761
- step0Spinner.succeed("Kubernetes API reachable");
21762
- const step1Spinner = ora3("Verifying kubectl context (D10)").start();
21763
- const ackValue = process.env.OLAM_K8S_CONTEXT_ACK ?? "";
21764
- if (ackValue.length === 0 || !isContextAllowed(ackValue)) {
21765
- step1Spinner.fail("Context ACK missing or disallowed");
21766
- stderr.write(
21767
- `${pc9.red("error:")} OLAM_K8S_CONTEXT_ACK is not set or empty.
21768
- Set it to your kubectl context name (byte-for-byte match):
21769
- export OLAM_K8S_CONTEXT_ACK=<your-context-name>
21770
- See GATES.md G-001 for the context-allowlist requirement.
21771
- `
21772
- );
21773
- return { exitCode: 1, summary: "context ack not set" };
21713
+ try {
21714
+ mkdirSyncImpl(path32.dirname(targetPath), { recursive: true });
21715
+ cpSyncImpl(sourcePath, targetPath, { recursive: true });
21716
+ } catch (err) {
21717
+ return {
21718
+ seeded: false,
21719
+ skipped: false,
21720
+ error: `Failed to seed manifests from ${sourcePath} to ${targetPath}: ${err instanceof Error ? err.message : String(err)}`
21721
+ };
21774
21722
  }
21775
- const pinnedContext = ackValue;
21776
- process.stderr.write(
21777
- `${pc9.yellow("[WARN]")} OLAM_K8S_UNSAFE_CONTEXT_ACK ctx=${pinnedContext}
21778
- `
21779
- );
21780
- step1Spinner.succeed(`Context pinned: ${pc9.bold(pinnedContext)}`);
21781
- if (opts.forceRefreshManifests) {
21782
- const step14Spinner = ora3("Refreshing manifests (D14)").start();
21783
- const refreshImpl = deps.manifestRefreshImpl ?? runManifestRefresh;
21784
- const refreshResult = await refreshImpl(
21785
- manifestsDir,
21786
- opts.acceptSecurityRegression === true,
21787
- {}
21788
- );
21789
- if (!refreshResult.ok) {
21790
- step14Spinner.fail("Manifest refresh refused");
21791
- stderr.write(`${pc9.red("error:")} ${refreshResult.message}
21792
- `);
21793
- return { exitCode: 1, summary: "manifest refresh refused" };
21794
- }
21795
- step14Spinner.succeed(refreshResult.message);
21723
+ return {
21724
+ seeded: true,
21725
+ source: sourcePath,
21726
+ target: targetPath,
21727
+ message: forceRefresh ? `Manifests force-refreshed from ${sourcePath} \u2192 ${targetPath}` : `Manifests seeded from ${sourcePath} \u2192 ${targetPath}`
21728
+ };
21729
+ }
21730
+ function extractField(obj, fieldKey) {
21731
+ if (typeof obj !== "object" || obj === null) return "";
21732
+ const keys = fieldKey.split(".");
21733
+ let current = obj;
21734
+ for (const k of keys) {
21735
+ if (typeof current !== "object" || current === null) return "";
21736
+ current = current[k];
21796
21737
  }
21797
- const step2Spinner = ora3(`Checking Secret ${HOST_CP_SECRET_NAME} (D12)`).start();
21798
- const secretError = await checkSecretPreCondition(pinnedContext, deps);
21799
- if (secretError !== null) {
21800
- step2Spinner.fail("Secret pre-check failed");
21801
- stderr.write(`${pc9.red("error:")} ${secretError}
21802
- `);
21803
- return { exitCode: 1, summary: "secret pre-check failed" };
21738
+ if (current === void 0) return "";
21739
+ return JSON.stringify(current);
21740
+ }
21741
+ function extractSecurityFieldsFromParsed(parsed) {
21742
+ const result = {};
21743
+ for (const field of SECURITY_SENSITIVE_FIELDS) {
21744
+ result[field] = extractField(parsed, field);
21804
21745
  }
21805
- step2Spinner.succeed(`Secret ${HOST_CP_SECRET_NAME} verified`);
21806
- appendSubstrateAuditEntry(
21807
- {
21808
- ts: new Date(deps.nowImpl ? deps.nowImpl() : Date.now()).toISOString(),
21809
- op: "upgrade",
21810
- substrate: "kubernetes",
21811
- phase2: { flag_removed: true }
21812
- },
21813
- stderr
21814
- );
21815
- const step25Spinner = ora3("Patching ConfigMap with K8s DNS URLs (C1/D4)").start();
21816
- const configMapError = await applyConfigMapSubstitution(pinnedContext, manifestsDir, deps);
21817
- if (configMapError !== null) {
21818
- step25Spinner.fail("ConfigMap substitution failed");
21819
- stderr.write(`${pc9.red("error:")} ${configMapError}
21820
- `);
21821
- return { exitCode: 1, summary: "configmap substitution failed" };
21746
+ return result;
21747
+ }
21748
+ function diffManifestSecurityFields(oldContent, newContent) {
21749
+ let oldParsed;
21750
+ let newParsed;
21751
+ try {
21752
+ oldParsed = JSON.parse(oldContent);
21753
+ } catch {
21754
+ oldParsed = { _raw: oldContent };
21822
21755
  }
21823
- step25Spinner.succeed("ConfigMap patched with K8s DNS URLs");
21824
- const step26Spinner = ora3("Checking peripheral Secrets (C2)").start();
21825
- const peripheralSecretError = await checkPeripheralSecrets(pinnedContext, deps);
21826
- if (peripheralSecretError !== null) {
21827
- step26Spinner.fail("Peripheral Secret pre-check failed");
21828
- stderr.write(`${pc9.red("error:")} ${peripheralSecretError}
21829
- `);
21830
- return { exitCode: 1, summary: "peripheral secret pre-check failed" };
21756
+ try {
21757
+ newParsed = JSON.parse(newContent);
21758
+ } catch {
21759
+ newParsed = { _raw: newContent };
21831
21760
  }
21832
- step26Spinner.succeed("All peripheral Secrets verified");
21833
- const step27Spinner = ora3("Waiting for CoreDNS (C3, 30s timeout)").start();
21834
- const coreDnsOk = await waitForCoreDns(pinnedContext, deps);
21835
- if (!coreDnsOk) {
21836
- step27Spinner.warn("CoreDNS not Available within 30s \u2014 continuing (DNS may be degraded)");
21837
- } else {
21838
- step27Spinner.succeed("CoreDNS Available");
21761
+ const oldFields = extractSecurityFieldsFromParsed(oldParsed);
21762
+ const newFields = extractSecurityFieldsFromParsed(newParsed);
21763
+ const changedFields = [];
21764
+ for (const field of SECURITY_SENSITIVE_FIELDS) {
21765
+ if (oldFields[field] !== newFields[field]) {
21766
+ changedFields.push(field);
21767
+ }
21839
21768
  }
21840
- const step3Spinner = ora3("Applying manifests").start();
21841
- const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
21842
- const applyResult = await wrap(
21843
- ["--context", pinnedContext, "apply", "-f", manifestsDir],
21844
- { timeout: 12e4 }
21845
- );
21846
- if (!applyResult.ok) {
21847
- step3Spinner.fail("kubectl apply failed");
21848
- stderr.write(
21849
- `${pc9.red("error:")} kubectl apply failed (exit ${applyResult.exitCode}):
21850
- ${applyResult.stderr.split("\n").slice(0, 5).join("\n ")}
21851
- `
21769
+ return {
21770
+ hasSecurityRegression: changedFields.length > 0,
21771
+ changedFields
21772
+ };
21773
+ }
21774
+ function appendAuditEntry(entry, auditLogPath, writeFileSyncImpl) {
21775
+ const line = JSON.stringify(entry) + "\n";
21776
+ try {
21777
+ writeFileSyncImpl(auditLogPath, line, {
21778
+ encoding: "utf8",
21779
+ flag: "a",
21780
+ mode: 384
21781
+ });
21782
+ } catch (err) {
21783
+ throw new Error(`audit log unavailable: ${err instanceof Error ? err.message : String(err)}`);
21784
+ }
21785
+ }
21786
+ async function runManifestRefresh(manifestsDir, acceptRegression, deps = {}, peripheral) {
21787
+ const auditLogPath = deps.auditLogPath ?? MANIFEST_REFRESH_AUDIT_LOG;
21788
+ const readdirSync25 = deps.readdirSync ?? fs30.readdirSync;
21789
+ const readFileSync74 = deps.readFileSync ?? fs30.readFileSync;
21790
+ const writeFileSyncImpl = deps.writeFileSync ?? fs30.writeFileSync;
21791
+ const existsSync99 = deps.existsSync ?? fs30.existsSync;
21792
+ const now = deps.now ? deps.now() : /* @__PURE__ */ new Date();
21793
+ const targetDir = peripheral ? path32.join(manifestsDir, peripheral) : manifestsDir;
21794
+ if (!existsSync99(targetDir)) {
21795
+ return {
21796
+ ok: false,
21797
+ message: peripheral ? `peripheral manifests directory not found: ${targetDir}` : `manifests directory not found: ${targetDir}`
21798
+ };
21799
+ }
21800
+ let files;
21801
+ try {
21802
+ const entries = readdirSync25(targetDir, { withFileTypes: true });
21803
+ files = entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".json"))).map((e) => e.name);
21804
+ } catch (err) {
21805
+ return {
21806
+ ok: false,
21807
+ message: `failed to read manifests dir: ${err instanceof Error ? err.message : String(err)}`
21808
+ };
21809
+ }
21810
+ const allChangedFields = [];
21811
+ for (const file of files) {
21812
+ const filePath = path32.join(targetDir, file);
21813
+ let content;
21814
+ try {
21815
+ content = readFileSync74(filePath, "utf8");
21816
+ } catch {
21817
+ continue;
21818
+ }
21819
+ const diff = diffManifestSecurityFields("{}", content);
21820
+ for (const f of diff.changedFields) {
21821
+ if (!allChangedFields.includes(f)) allChangedFields.push(f);
21822
+ }
21823
+ }
21824
+ const hasSecurityRegression = allChangedFields.length > 0;
21825
+ if (hasSecurityRegression && !acceptRegression) {
21826
+ return {
21827
+ ok: false,
21828
+ message: `manifest refresh refused: security-sensitive fields changed (${allChangedFields.join(", ")}).
21829
+ Re-run with --accept-security-regression to acknowledge and proceed.`
21830
+ };
21831
+ }
21832
+ const entry = {
21833
+ ts: now.toISOString(),
21834
+ manifests_path: targetDir,
21835
+ security_regression: hasSecurityRegression,
21836
+ changed_fields: allChangedFields,
21837
+ accepted: acceptRegression,
21838
+ operator_pid: process.pid,
21839
+ ...peripheral !== void 0 ? { peripheral } : {}
21840
+ };
21841
+ try {
21842
+ appendAuditEntry(entry, auditLogPath, writeFileSyncImpl);
21843
+ } catch (err) {
21844
+ return {
21845
+ ok: false,
21846
+ message: err instanceof Error ? err.message : String(err)
21847
+ };
21848
+ }
21849
+ return {
21850
+ ok: true,
21851
+ message: hasSecurityRegression ? `Security-sensitive fields changed (${allChangedFields.join(", ")}); accepted via --accept-security-regression. Audit entry written.` : "Manifest refresh completed (no security-sensitive field changes). Audit entry written."
21852
+ };
21853
+ }
21854
+
21855
+ // src/lib/upgrade-kubernetes.ts
21856
+ var OLAM_K8S_MANIFESTS_DIR = path33.join(OLAM_HOME, "k8s", "manifests");
21857
+ var K8S_NAMESPACE2 = "olam";
21858
+ var HOST_CP_SECRET_NAME = "olam-host-cp-secret";
21859
+ var HOST_CP_DEPLOYMENT_NAME = "olam-host-cp";
21860
+ var PORT_FORWARD_TARGET = "service/olam-host-cp";
21861
+ var HOST_CP_HEALTH_URL = "http://127.0.0.1:19000/health";
21862
+ var SUBSTRATE_AUDIT_LOG = path33.join(OLAM_STATE_DIR, "substrate-audit.jsonl");
21863
+ var PLACEHOLDER_VALUES = /* @__PURE__ */ new Set(["OLAM_AUTH_SECRET", "GH_TOKEN"]);
21864
+ var REQUIRED_SECRET_KEYS = ["OLAM_AUTH_SECRET", "GH_TOKEN"];
21865
+ var PERIPHERAL_SECRETS = [
21866
+ { name: "auth-service", secretName: "olam-auth-service-secret", keys: ["OLAM_AUTH_DB_SECRET"] },
21867
+ { name: "mcp-auth-service", secretName: "olam-mcp-auth-service-secret", keys: ["OLAM_MCP_AUTH_JWT_SECRET"] },
21868
+ { name: "kg-service", secretName: "olam-kg-service-secret", keys: ["OLAM_KG_BEARER_TOKEN"] },
21869
+ { name: "memory-service", secretName: "olam-memory-service-secret", keys: ["OLAM_MEMORY_BEARER_SECRET"] }
21870
+ ];
21871
+ var K8S_DNS_SUFFIX = "olam.svc.cluster.local";
21872
+ function buildK8sDnsUrl(k8sServiceName, port2) {
21873
+ return `http://${k8sServiceName}.${K8S_DNS_SUFFIX}:${port2}`;
21874
+ }
21875
+ function appendSubstrateAuditEntry(entry, stderr) {
21876
+ try {
21877
+ const line = JSON.stringify(entry) + "\n";
21878
+ const dir = path33.dirname(SUBSTRATE_AUDIT_LOG);
21879
+ if (!fs31.existsSync(dir)) fs31.mkdirSync(dir, { recursive: true });
21880
+ fs31.writeFileSync(SUBSTRATE_AUDIT_LOG, line, { encoding: "utf8", flag: "a", mode: 384 });
21881
+ } catch (err) {
21882
+ stderr.write(
21883
+ `${pc9.yellow("[warn]")} could not write substrate audit log: ${err instanceof Error ? err.message : String(err)}
21884
+ `
21885
+ );
21886
+ }
21887
+ }
21888
+ function isContextAllowed(context) {
21889
+ return typeof context === "string" && context.length > 0;
21890
+ }
21891
+ var MANAGED_K8S_URL_PATTERNS = [
21892
+ { pattern: /\.amazonaws\.com(:\d+)?$/, provider: "EKS (Amazon)" },
21893
+ { pattern: /\.googleapis\.com(:\d+)?$/, provider: "GKE (Google)" },
21894
+ { pattern: /\.gke\.io(:\d+)?$/, provider: "GKE (Google)" },
21895
+ { pattern: /\.azmk8s\.io(:\d+)?$/, provider: "AKS (Azure)" },
21896
+ { pattern: /\.k8s\.ondigitalocean\.com(:\d+)?$/, provider: "DOKS (DigitalOcean)" },
21897
+ { pattern: /\.civo\.com(:\d+)?$/, provider: "Civo" }
21898
+ ];
21899
+ async function detectManagedK8sProvider(deps) {
21900
+ if (deps.getClusterServerUrlImpl) {
21901
+ const url3 = await deps.getClusterServerUrlImpl();
21902
+ if (!url3) return null;
21903
+ for (const { pattern, provider } of MANAGED_K8S_URL_PATTERNS) {
21904
+ if (pattern.test(url3)) return provider;
21905
+ }
21906
+ return null;
21907
+ }
21908
+ const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
21909
+ const result = await wrap(
21910
+ ["config", "view", "--minify", "-o", "jsonpath={.clusters[0].cluster.server}"],
21911
+ { timeout: 5e3 }
21912
+ );
21913
+ if (!result.ok || !result.stdout.trim()) return null;
21914
+ const url2 = result.stdout.trim();
21915
+ for (const { pattern, provider } of MANAGED_K8S_URL_PATTERNS) {
21916
+ if (pattern.test(url2)) return provider;
21917
+ }
21918
+ return null;
21919
+ }
21920
+ async function checkDockerSocketAccessible(context, deps) {
21921
+ if (deps.checkDockerSocketImpl) {
21922
+ return deps.checkDockerSocketImpl(context);
21923
+ }
21924
+ const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
21925
+ const result = await wrap(
21926
+ [
21927
+ "--context",
21928
+ context,
21929
+ "exec",
21930
+ "deploy/olam-host-cp",
21931
+ "-n",
21932
+ "olam",
21933
+ "--",
21934
+ "test",
21935
+ "-S",
21936
+ "/var/run/docker.sock"
21937
+ ],
21938
+ { timeout: 1e4 }
21939
+ );
21940
+ return result.ok;
21941
+ }
21942
+ async function probeKubernetesApiReachable(context, deps) {
21943
+ const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
21944
+ const result = await wrap(
21945
+ ["--context", context, "cluster-info"],
21946
+ { timeout: 5e3 }
21947
+ );
21948
+ return result.ok;
21949
+ }
21950
+ async function checkSecretPreCondition(context, deps) {
21951
+ const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
21952
+ const result = await wrap(
21953
+ [
21954
+ "--context",
21955
+ context,
21956
+ "get",
21957
+ "secret",
21958
+ HOST_CP_SECRET_NAME,
21959
+ "-n",
21960
+ K8S_NAMESPACE2,
21961
+ "-o",
21962
+ "json"
21963
+ ],
21964
+ { timeout: 15e3 }
21965
+ );
21966
+ if (!result.ok) {
21967
+ return `Secret "${HOST_CP_SECRET_NAME}" not found in namespace "${K8S_NAMESPACE2}".
21968
+ Create it first: kubectl --context ${context} apply -f <your-secret.yaml>
21969
+ ${result.stderr.split("\n")[0] ?? ""}`;
21970
+ }
21971
+ let secretJson;
21972
+ try {
21973
+ secretJson = JSON.parse(result.stdout);
21974
+ } catch {
21975
+ return `Failed to parse Secret JSON: ${result.stdout.slice(0, 200)}`;
21976
+ }
21977
+ const data = secretJson.data ?? {};
21978
+ for (const key of REQUIRED_SECRET_KEYS) {
21979
+ if (!(key in data)) {
21980
+ return `Secret "${HOST_CP_SECRET_NAME}" is missing required key "${key}".
21981
+ Keys found: ${Object.keys(data).join(", ") || "(none)"}
21982
+ Add the key and re-run.`;
21983
+ }
21984
+ const b64 = data[key] ?? "";
21985
+ const decoded = Buffer.from(b64, "base64").toString("utf8");
21986
+ if (PLACEHOLDER_VALUES.has(decoded)) {
21987
+ return `Secret "${HOST_CP_SECRET_NAME}" key "${key}" still holds its placeholder value.
21988
+ Replace the placeholder with a real value, then re-run.`;
21989
+ }
21990
+ }
21991
+ return null;
21992
+ }
21993
+ async function applyConfigMapSubstitution(context, manifestsDir, deps) {
21994
+ const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
21995
+ const readFileSync74 = deps.readFileSyncImpl ?? fs31.readFileSync;
21996
+ const configMapPath = path33.join(manifestsDir, "30-configmap.yaml");
21997
+ let rawYaml;
21998
+ try {
21999
+ rawYaml = readFileSync74(configMapPath, "utf8");
22000
+ } catch (err) {
22001
+ return `Failed to read ConfigMap at ${configMapPath}: ${err instanceof Error ? err.message : String(err)}`;
22002
+ }
22003
+ let parsed;
22004
+ try {
22005
+ parsed = yamlParse(rawYaml);
22006
+ } catch (err) {
22007
+ return `Failed to parse ConfigMap YAML at ${configMapPath}: ${err instanceof Error ? err.message : String(err)}`;
22008
+ }
22009
+ const data = parsed["data"] ?? {};
22010
+ for (const peripheral of PERIPHERALS) {
22011
+ data[peripheral.configMapKeyInHostCp] = buildK8sDnsUrl(peripheral.k8sServiceName, peripheral.port);
22012
+ }
22013
+ parsed["data"] = data;
22014
+ const patchedYaml = yamlStringify(parsed);
22015
+ const result = await wrap(
22016
+ ["--context", context, "apply", "-f", "-"],
22017
+ { timeout: 3e4, stdin: patchedYaml }
22018
+ );
22019
+ if (!result.ok) {
22020
+ return `kubectl apply (ConfigMap substitution) failed: ${result.stderr.split("\n")[0] ?? ""}`;
22021
+ }
22022
+ return null;
22023
+ }
22024
+ async function checkPeripheralSecrets(context, deps) {
22025
+ const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
22026
+ for (const { name, secretName, keys } of PERIPHERAL_SECRETS) {
22027
+ const result = await wrap(
22028
+ [
22029
+ "--context",
22030
+ context,
22031
+ "get",
22032
+ "secret",
22033
+ secretName,
22034
+ "-n",
22035
+ K8S_NAMESPACE2,
22036
+ "-o",
22037
+ "json"
22038
+ ],
22039
+ { timeout: 15e3 }
22040
+ );
22041
+ if (!result.ok) {
22042
+ return `Peripheral Secret "${secretName}" (${name}) not found in namespace "${K8S_NAMESPACE2}".
22043
+ Create it first: kubectl --context ${context} apply -f <your-${name}-secret.yaml>
22044
+ ${result.stderr.split("\n")[0] ?? ""}`;
22045
+ }
22046
+ let secretJson;
22047
+ try {
22048
+ secretJson = JSON.parse(result.stdout);
22049
+ } catch {
22050
+ return `Failed to parse Secret JSON for "${secretName}" (${name}): ${result.stdout.slice(0, 200)}`;
22051
+ }
22052
+ const data = secretJson.data ?? {};
22053
+ for (const key of keys) {
22054
+ if (!(key in data)) {
22055
+ return `Peripheral Secret "${secretName}" (${name}) is missing required key "${key}".
22056
+ Keys found: ${Object.keys(data).join(", ") || "(none)"}
22057
+ Add the key and re-run.`;
22058
+ }
22059
+ const b64 = data[key] ?? "";
22060
+ const decoded = Buffer.from(b64, "base64").toString("utf8");
22061
+ if (decoded.startsWith("REPLACE_ME_")) {
22062
+ return `Peripheral Secret "${secretName}" (${name}) key "${key}" still holds its placeholder value.
22063
+ Replace the placeholder with a real value, then re-run.`;
22064
+ }
22065
+ }
22066
+ }
22067
+ return null;
22068
+ }
22069
+ async function waitForCoreDns(context, deps) {
22070
+ const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
22071
+ const result = await wrap(
22072
+ [
22073
+ "--context",
22074
+ context,
22075
+ "wait",
22076
+ "--for=condition=Available",
22077
+ "deployment/coredns",
22078
+ "-n",
22079
+ "kube-system",
22080
+ "--timeout=30s"
22081
+ ],
22082
+ { timeout: 35e3 }
22083
+ );
22084
+ return result.ok;
22085
+ }
22086
+ async function verifyHealthHeader(deps) {
22087
+ const fetchImpl = deps.fetchImpl ?? fetch;
22088
+ try {
22089
+ const res = await fetchImpl(HOST_CP_HEALTH_URL, {
22090
+ signal: AbortSignal.timeout(5e3)
22091
+ });
22092
+ const engine = res.headers.get("x-olam-engine") ?? res.headers.get("X-Olam-Engine");
22093
+ return { ok: engine === "kubernetes", engineHeader: engine };
22094
+ } catch {
22095
+ return { ok: false, engineHeader: null };
22096
+ }
22097
+ }
22098
+ async function runUpgradeKubernetes(opts = {}, deps = {}) {
22099
+ const stdout = deps.stdout ?? process.stdout;
22100
+ const stderr = deps.stderr ?? process.stderr;
22101
+ const manifestsDir = deps.manifestsDir ?? OLAM_K8S_MANIFESTS_DIR;
22102
+ const startMs = Date.now();
22103
+ {
22104
+ const seedImpl = deps.seedManifestsImpl ?? seedManifestsFromBundle;
22105
+ const seedResult = seedImpl(opts.forceRefreshManifests === true, deps.seedManifestsDeps ?? {});
22106
+ if (seedResult.seeded) {
22107
+ stdout.write(`${pc9.dim("[seed]")} ${seedResult.message}
22108
+ `);
22109
+ } else if (!seedResult.skipped) {
22110
+ stderr.write(`${pc9.red("error:")} Manifest seed failed: ${seedResult.error}
22111
+ `);
22112
+ return { exitCode: 1, summary: "manifest seed failed" };
22113
+ }
22114
+ }
22115
+ {
22116
+ const managedProvider = await detectManagedK8sProvider(deps);
22117
+ if (managedProvider !== null) {
22118
+ stderr.write(
22119
+ `${pc9.red("error:")} olam upgrade supports local k3d/k3s clusters only.
22120
+ Detected managed-k8s context: ${managedProvider}
22121
+ hostPath /var/run/docker.sock is not accessible on managed clusters \u2014 the docker
22122
+ socket does not reach the operator's host daemon on cloud-managed nodes.
22123
+ See Decision #18 and docs/operator/kubernetes-substrate-beta.md
22124
+ for supported cluster types and the active retraction of managed-k8s support.
22125
+ `
22126
+ );
22127
+ return { exitCode: 1, summary: "managed-k8s context detected (Decision #18 retraction)" };
22128
+ }
22129
+ }
22130
+ {
22131
+ const preflightContext = process.env.OLAM_K8S_CONTEXT_ACK ?? "default";
22132
+ const socketAccessible = await checkDockerSocketAccessible(preflightContext, deps);
22133
+ if (!socketAccessible) {
22134
+ const clusterType = defaultDetectK8sClusterType(preflightContext);
22135
+ const remedyText = buildDockerSocketRemedy(clusterType).split("\n").map((line) => ` ${line}`).join("\n");
22136
+ stderr.write(
22137
+ `${pc9.yellow("[WARN]")} docker socket not accessible at /var/run/docker.sock inside the host-cp pod.
22138
+ This means the cluster was not created with the docker socket bind-mount,
22139
+ or host-cp is not yet deployed (first install \u2014 this warning is expected).
22140
+ For subsequent upgrades:
22141
+ ${remedyText}
22142
+ Run olam doctor to check probe 28 (probeDockerSocketBindMount).
22143
+ `
22144
+ );
22145
+ }
22146
+ }
22147
+ const step0Spinner = ora3("Probing Kubernetes API reachability").start();
22148
+ const context = process.env.OLAM_K8S_CONTEXT_ACK ?? "";
22149
+ const probeContext = context.length > 0 ? context : "default";
22150
+ const reachable = await probeKubernetesApiReachable(probeContext, deps);
22151
+ if (!reachable) {
22152
+ step0Spinner.fail("Kubernetes API not reachable");
22153
+ stderr.write(
22154
+ `${pc9.red("error:")} kubectl cluster-info failed (5s timeout).
22155
+ Ensure your kubectl context is correct and the cluster is reachable.
22156
+ Set OLAM_K8S_CONTEXT_ACK=<context-name> to your active kubectl context.
22157
+ `
22158
+ );
22159
+ return { exitCode: 1, summary: "kubernetes api not reachable" };
22160
+ }
22161
+ step0Spinner.succeed("Kubernetes API reachable");
22162
+ const step1Spinner = ora3("Verifying kubectl context (D10)").start();
22163
+ const ackValue = process.env.OLAM_K8S_CONTEXT_ACK ?? "";
22164
+ if (ackValue.length === 0 || !isContextAllowed(ackValue)) {
22165
+ step1Spinner.fail("Context ACK missing or disallowed");
22166
+ stderr.write(
22167
+ `${pc9.red("error:")} OLAM_K8S_CONTEXT_ACK is not set or empty.
22168
+ Set it to your kubectl context name (byte-for-byte match):
22169
+ export OLAM_K8S_CONTEXT_ACK=<your-context-name>
22170
+ See GATES.md G-001 for the context-allowlist requirement.
22171
+ `
22172
+ );
22173
+ return { exitCode: 1, summary: "context ack not set" };
22174
+ }
22175
+ const pinnedContext = ackValue;
22176
+ process.stderr.write(
22177
+ `${pc9.yellow("[WARN]")} OLAM_K8S_UNSAFE_CONTEXT_ACK ctx=${pinnedContext}
22178
+ `
22179
+ );
22180
+ step1Spinner.succeed(`Context pinned: ${pc9.bold(pinnedContext)}`);
22181
+ if (opts.forceRefreshManifests) {
22182
+ const step14Spinner = ora3("Refreshing manifests (D14)").start();
22183
+ const refreshImpl = deps.manifestRefreshImpl ?? runManifestRefresh;
22184
+ const refreshResult = await refreshImpl(
22185
+ manifestsDir,
22186
+ opts.acceptSecurityRegression === true,
22187
+ {}
22188
+ );
22189
+ if (!refreshResult.ok) {
22190
+ step14Spinner.fail("Manifest refresh refused");
22191
+ stderr.write(`${pc9.red("error:")} ${refreshResult.message}
22192
+ `);
22193
+ return { exitCode: 1, summary: "manifest refresh refused" };
22194
+ }
22195
+ step14Spinner.succeed(refreshResult.message);
22196
+ }
22197
+ const step2Spinner = ora3(`Checking Secret ${HOST_CP_SECRET_NAME} (D12)`).start();
22198
+ const secretError = await checkSecretPreCondition(pinnedContext, deps);
22199
+ if (secretError !== null) {
22200
+ step2Spinner.fail("Secret pre-check failed");
22201
+ stderr.write(`${pc9.red("error:")} ${secretError}
22202
+ `);
22203
+ return { exitCode: 1, summary: "secret pre-check failed" };
22204
+ }
22205
+ step2Spinner.succeed(`Secret ${HOST_CP_SECRET_NAME} verified`);
22206
+ appendSubstrateAuditEntry(
22207
+ {
22208
+ ts: new Date(deps.nowImpl ? deps.nowImpl() : Date.now()).toISOString(),
22209
+ op: "upgrade",
22210
+ substrate: "kubernetes",
22211
+ phase2: { flag_removed: true }
22212
+ },
22213
+ stderr
22214
+ );
22215
+ const step26Spinner = ora3("Checking peripheral Secrets (C2)").start();
22216
+ const peripheralSecretError = await checkPeripheralSecrets(pinnedContext, deps);
22217
+ if (peripheralSecretError !== null) {
22218
+ step26Spinner.fail("Peripheral Secret pre-check failed");
22219
+ stderr.write(`${pc9.red("error:")} ${peripheralSecretError}
22220
+ `);
22221
+ return { exitCode: 1, summary: "peripheral secret pre-check failed" };
22222
+ }
22223
+ step26Spinner.succeed("All peripheral Secrets verified");
22224
+ const step27Spinner = ora3("Waiting for CoreDNS (C3, 30s timeout)").start();
22225
+ const coreDnsOk = await waitForCoreDns(pinnedContext, deps);
22226
+ if (!coreDnsOk) {
22227
+ step27Spinner.warn("CoreDNS not Available within 30s \u2014 continuing (DNS may be degraded)");
22228
+ } else {
22229
+ step27Spinner.succeed("CoreDNS Available");
22230
+ }
22231
+ const step3Spinner = ora3("Applying manifests").start();
22232
+ const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
22233
+ const applyResult = await wrap(
22234
+ ["--context", pinnedContext, "apply", "-f", manifestsDir],
22235
+ { timeout: 12e4 }
22236
+ );
22237
+ if (!applyResult.ok) {
22238
+ step3Spinner.fail("kubectl apply failed");
22239
+ stderr.write(
22240
+ `${pc9.red("error:")} kubectl apply failed (exit ${applyResult.exitCode}):
22241
+ ${applyResult.stderr.split("\n").slice(0, 5).join("\n ")}
22242
+ `
21852
22243
  );
21853
22244
  return { exitCode: 1, summary: "kubectl apply failed" };
21854
22245
  }
21855
22246
  const peripheralNames = [...PERIPHERALS.map((p) => p.name)].sort();
21856
22247
  for (const name of peripheralNames) {
21857
- const peripheralManifestsDir = path32.join(manifestsDir, name);
22248
+ const peripheralManifestsDir = path33.join(manifestsDir, name);
21858
22249
  const peripheralApplyResult = await wrap(
21859
22250
  ["--context", pinnedContext, "apply", "-f", peripheralManifestsDir],
21860
22251
  { timeout: 12e4 }
@@ -21870,6 +22261,15 @@ async function runUpgradeKubernetes(opts = {}, deps = {}) {
21870
22261
  }
21871
22262
  }
21872
22263
  step3Spinner.succeed("All manifests applied (host-cp + 4 peripherals)");
22264
+ const step35Spinner = ora3("Re-applying patched ConfigMap with K8s DNS URLs (C1/D4 \u2014 B7 fix)").start();
22265
+ const configMapError = await applyConfigMapSubstitution(pinnedContext, manifestsDir, deps);
22266
+ if (configMapError !== null) {
22267
+ step35Spinner.fail("ConfigMap substitution failed");
22268
+ stderr.write(`${pc9.red("error:")} ${configMapError}
22269
+ `);
22270
+ return { exitCode: 1, summary: "configmap substitution failed" };
22271
+ }
22272
+ step35Spinner.succeed("ConfigMap patched with K8s DNS URLs (survives bulk apply)");
21873
22273
  const step4Spinner = ora3("Waiting for rollout (all 5 deployments, 90s each)").start();
21874
22274
  const deploymentNames = [
21875
22275
  HOST_CP_DEPLOYMENT_NAME,
@@ -22009,10 +22409,10 @@ async function applyK8sAuthRefresh(pinnedContext, deps = {}) {
22009
22409
  const stdout = deps.stdout ?? process.stdout;
22010
22410
  const stderr = deps.stderr ?? process.stderr;
22011
22411
  const olamHome5 = deps.olamHomeOverride ?? OLAM_HOME;
22012
- const authSecretFile = path33.join(olamHome5, "auth-secret");
22412
+ const authSecretFile = path34.join(olamHome5, "auth-secret");
22013
22413
  const readFn = deps.readFileSyncImpl ?? ((p, enc) => fs32.readFileSync(p, enc));
22014
22414
  const writeFn = deps.writeFileSyncImpl ?? ((p, data, opts) => {
22015
- fs32.mkdirSync(path33.dirname(p), { recursive: true });
22415
+ fs32.mkdirSync(path34.dirname(p), { recursive: true });
22016
22416
  fs32.writeFileSync(p, data, { encoding: "utf8", mode: opts.mode });
22017
22417
  });
22018
22418
  const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
@@ -22262,15 +22662,15 @@ ${pc11.dim("Next: olam create --name my-world")}`);
22262
22662
 
22263
22663
  // src/commands/create.ts
22264
22664
  init_manager();
22265
- import { spawnSync as spawnSync14 } from "node:child_process";
22266
- import { existsSync as existsSync35 } from "node:fs";
22665
+ import { spawnSync as spawnSync15 } from "node:child_process";
22666
+ import { existsSync as existsSync36 } from "node:fs";
22267
22667
  import { dirname as dirname26, resolve as resolve14 } from "node:path";
22268
22668
  import ora5 from "ora";
22269
22669
  import pc12 from "picocolors";
22270
22670
 
22271
22671
  // ../core/dist/world/devbox-freshness.js
22272
22672
  import { execSync as execSync7 } from "node:child_process";
22273
- import { existsSync as existsSync31, statSync as statSync6 } from "node:fs";
22673
+ import { existsSync as existsSync32, statSync as statSync7 } from "node:fs";
22274
22674
  import { join as join38 } from "node:path";
22275
22675
  var DEFAULT_DEVBOX_IMAGE = "olam-devbox:base";
22276
22676
  var DEVBOX_BAKED_SOURCES = [
@@ -22361,9 +22761,9 @@ function defaultDockerInspect(image) {
22361
22761
  }
22362
22762
  function defaultStatMtime(absPath) {
22363
22763
  try {
22364
- if (!existsSync31(absPath))
22764
+ if (!existsSync32(absPath))
22365
22765
  return null;
22366
- return statSync6(absPath).mtimeMs;
22766
+ return statSync7(absPath).mtimeMs;
22367
22767
  } catch {
22368
22768
  return null;
22369
22769
  }
@@ -22500,12 +22900,12 @@ init_output();
22500
22900
  init_host_cp();
22501
22901
 
22502
22902
  // src/lib/memory-secret.ts
22503
- import { existsSync as existsSync32, mkdirSync as mkdirSync23, readFileSync as readFileSync26, statSync as statSync7, writeFileSync as writeFileSync18, renameSync as renameSync6, chmodSync as chmodSync3 } from "node:fs";
22504
- import { homedir as homedir18 } from "node:os";
22903
+ import { existsSync as existsSync33, mkdirSync as mkdirSync23, readFileSync as readFileSync27, statSync as statSync8, writeFileSync as writeFileSync18, renameSync as renameSync6, chmodSync as chmodSync3 } from "node:fs";
22904
+ import { homedir as homedir19 } from "node:os";
22505
22905
  import { join as join39, dirname as dirname25 } from "node:path";
22506
22906
  import { randomBytes as randomBytes6 } from "node:crypto";
22507
- var MEMORY_SECRET_PATH = join39(homedir18(), ".olam", "memory-secret");
22508
- var CLOUD_MEMORY_SECRET_PATH = join39(homedir18(), ".olam", "cloud-memory-secret");
22907
+ var MEMORY_SECRET_PATH = join39(homedir19(), ".olam", "memory-secret");
22908
+ var CLOUD_MEMORY_SECRET_PATH = join39(homedir19(), ".olam", "cloud-memory-secret");
22509
22909
  var SECRET_LEN_BYTES = 32;
22510
22910
  function generateSecret() {
22511
22911
  return randomBytes6(SECRET_LEN_BYTES).toString("hex");
@@ -22518,15 +22918,15 @@ function writeSecretAtPath(path89, value) {
22518
22918
  renameSync6(tmp, path89);
22519
22919
  }
22520
22920
  function readSecretAtPathOrNull(path89) {
22521
- if (!existsSync32(path89)) return null;
22522
- const mode = statSync7(path89).mode & 511;
22921
+ if (!existsSync33(path89)) return null;
22922
+ const mode = statSync8(path89).mode & 511;
22523
22923
  if (mode !== 384) {
22524
22924
  process.stderr.write(
22525
22925
  `warn: ${path89} has mode 0${mode.toString(8)}; expected 0600. Run 'olam memory secret rotate' to regenerate.
22526
22926
  `
22527
22927
  );
22528
22928
  }
22529
- return readFileSync26(path89, "utf8").trim();
22929
+ return readFileSync27(path89, "utf8").trim();
22530
22930
  }
22531
22931
  function readSecretAtPath(path89) {
22532
22932
  const v = readSecretAtPathOrNull(path89);
@@ -22556,16 +22956,16 @@ function rotateMemorySecret(path89 = MEMORY_SECRET_PATH) {
22556
22956
  return fresh;
22557
22957
  }
22558
22958
  function hasMemorySecret(path89 = MEMORY_SECRET_PATH) {
22559
- return existsSync32(path89);
22959
+ return existsSync33(path89);
22560
22960
  }
22561
22961
  function writeCloudMemorySecret(value, path89 = CLOUD_MEMORY_SECRET_PATH) {
22562
22962
  writeSecretAtPath(path89, value);
22563
22963
  }
22564
22964
 
22565
22965
  // src/lib/world-mcp-register.ts
22566
- import { spawnSync as spawnSync12 } from "node:child_process";
22966
+ import { spawnSync as spawnSync13 } from "node:child_process";
22567
22967
  var DEFAULT_DOCKER_EXEC_DEPS = {
22568
- spawn: spawnSync12,
22968
+ spawn: spawnSync13,
22569
22969
  log: (msg) => console.log(msg)
22570
22970
  };
22571
22971
  var MCP_NAME = "agentmemory";
@@ -22629,13 +23029,13 @@ function registerAgentMemoryMcp(opts, deps = DEFAULT_DOCKER_EXEC_DEPS) {
22629
23029
  // src/lib/build-if-stale.ts
22630
23030
  init_install_root();
22631
23031
  import * as fs33 from "node:fs";
22632
- import * as path34 from "node:path";
22633
- import { spawnSync as spawnSync13 } from "node:child_process";
23032
+ import * as path35 from "node:path";
23033
+ import { spawnSync as spawnSync14 } from "node:child_process";
22634
23034
  import ora4 from "ora";
22635
23035
 
22636
23036
  // src/lib/bundle-source.ts
22637
23037
  init_install_root();
22638
- import { existsSync as existsSync33 } from "node:fs";
23038
+ import { existsSync as existsSync34 } from "node:fs";
22639
23039
  import { join as join40, resolve as resolve13 } from "node:path";
22640
23040
  var BundleSourceNotFoundError = class extends Error {
22641
23041
  constructor(sourceModePath, installModePath) {
@@ -22659,7 +23059,7 @@ function resolveBundleSource(env = process.env, installRootDir = installRoot())
22659
23059
  const repoRoot2 = resolve13(installRootDir, "..", "..");
22660
23060
  const sourcePath2 = join40(repoRoot2, "packages", "intelligence", "dist", "agent-stream");
22661
23061
  const installPath2 = join40(installRootDir, "dist", "agent-stream");
22662
- if (!existsSync33(sourcePath2)) {
23062
+ if (!existsSync34(sourcePath2)) {
22663
23063
  throw new BundleSourceNotFoundError(sourcePath2, installPath2);
22664
23064
  }
22665
23065
  return { mode: "source", path: sourcePath2 };
@@ -22667,7 +23067,7 @@ function resolveBundleSource(env = process.env, installRootDir = installRoot())
22667
23067
  const installPath = join40(installRootDir, "dist", "agent-stream");
22668
23068
  const repoRoot = resolve13(installRootDir, "..", "..");
22669
23069
  const sourcePath = join40(repoRoot, "packages", "intelligence", "dist", "agent-stream");
22670
- if (!existsSync33(installPath)) {
23070
+ if (!existsSync34(installPath)) {
22671
23071
  throw new BundleSourceNotFoundError(sourcePath, installPath);
22672
23072
  }
22673
23073
  return { mode: "install", path: installPath };
@@ -22686,11 +23086,11 @@ function maxMtimeMs(dir) {
22686
23086
  return;
22687
23087
  }
22688
23088
  for (const entry of entries) {
22689
- const full = path34.join(d, entry.name);
23089
+ const full = path35.join(d, entry.name);
22690
23090
  if (entry.isDirectory()) {
22691
23091
  if (entry.name === "__tests__" || entry.name === "node_modules") continue;
22692
23092
  scan(full);
22693
- } else if (entry.isFile() && TS_EXTS.has(path34.extname(entry.name))) {
23093
+ } else if (entry.isFile() && TS_EXTS.has(path35.extname(entry.name))) {
22694
23094
  try {
22695
23095
  const mt = fs33.statSync(full).mtimeMs;
22696
23096
  if (mt > max) max = mt;
@@ -22715,7 +23115,7 @@ function minDistMtimeMs(dir) {
22715
23115
  for (const entry of entries) {
22716
23116
  if (entry.isFile() && entry.name.endsWith(".js")) {
22717
23117
  try {
22718
- const mt = fs33.statSync(path34.join(dir, entry.name)).mtimeMs;
23118
+ const mt = fs33.statSync(path35.join(dir, entry.name)).mtimeMs;
22719
23119
  if (mt < min) min = mt;
22720
23120
  found = true;
22721
23121
  } catch {
@@ -22740,8 +23140,8 @@ function buildIfStale(repoRoot, env = process.env) {
22740
23140
  if (!isDevMode(env)) {
22741
23141
  return { ok: true, built: false, message: "install-mode: skipping build check" };
22742
23142
  }
22743
- const srcDir = path34.join(repoRoot, "packages", "intelligence", "src", "agent-stream");
22744
- const distDir = path34.join(repoRoot, "packages", "intelligence", "dist", "agent-stream");
23143
+ const srcDir = path35.join(repoRoot, "packages", "intelligence", "src", "agent-stream");
23144
+ const distDir = path35.join(repoRoot, "packages", "intelligence", "dist", "agent-stream");
22745
23145
  const srcMtime = maxMtimeMs(srcDir);
22746
23146
  const distMtime = minDistMtimeMs(distDir);
22747
23147
  if (distMtime > 0 && srcMtime <= distMtime) {
@@ -22753,7 +23153,7 @@ function buildIfStale(repoRoot, env = process.env) {
22753
23153
  }
22754
23154
  const reason = distMtime === 0 ? "dist is absent" : `source newer by ${Math.round((srcMtime - distMtime) / 1e3)}s`;
22755
23155
  const spinner = ora4(`Rebuilding agent-stream bundle\u2026 (${reason})`).start();
22756
- const result = spawnSync13(
23156
+ const result = spawnSync14(
22757
23157
  "npm",
22758
23158
  ["run", "build", "--workspace=@olam/intelligence"],
22759
23159
  {
@@ -22817,7 +23217,7 @@ function registerCreate(program2) {
22817
23217
  if (decision.stderrLine) {
22818
23218
  process.stderr.write(decision.stderrLine + "\n");
22819
23219
  }
22820
- spawnSync14("docker", ["pull", overrideRef], { stdio: "pipe" });
23220
+ spawnSync15("docker", ["pull", overrideRef], { stdio: "pipe" });
22821
23221
  const { inspectImageProtocolVersions: inspectImageProtocolVersions2, checkProtocolOverlap: checkProtocolOverlap2 } = await Promise.resolve().then(() => (init_protocol_version(), protocol_version_exports));
22822
23222
  const inspect = inspectImageProtocolVersions2(overrideRef);
22823
23223
  if (inspect.inspectFailed) {
@@ -22934,7 +23334,7 @@ function registerCreate(program2) {
22934
23334
  throw err;
22935
23335
  }
22936
23336
  const spinner2 = ora5("Rebuilding olam-devbox:latest\u2026").start();
22937
- const rebuild = spawnSync14(
23337
+ const rebuild = spawnSync15(
22938
23338
  "bash",
22939
23339
  [buildScript],
22940
23340
  { cwd: repoRoot, stdio: "inherit" }
@@ -23215,7 +23615,7 @@ ${pc12.cyan("Host CP UI:")} ${worldUrl}`);
23215
23615
  function resolveRepoRoot(start) {
23216
23616
  let cur = start;
23217
23617
  while (true) {
23218
- if (existsSync35(resolve14(cur, "packages")) && existsSync35(resolve14(cur, "package.json"))) {
23618
+ if (existsSync36(resolve14(cur, "packages")) && existsSync36(resolve14(cur, "package.json"))) {
23219
23619
  return cur;
23220
23620
  }
23221
23621
  const parent = dirname26(cur);
@@ -23449,7 +23849,7 @@ init_output();
23449
23849
  import * as fs35 from "node:fs";
23450
23850
  import * as http3 from "node:http";
23451
23851
  import * as os18 from "node:os";
23452
- import * as path36 from "node:path";
23852
+ import * as path37 from "node:path";
23453
23853
  var CLI_VERSION2 = process.env["OLAM_CLI_VERSION"] ?? "0.0.0";
23454
23854
  var HOST_CP_PORT2 = 19e3;
23455
23855
  var STATE_ENUM = [
@@ -23540,7 +23940,7 @@ async function getMachineStatus(_probe, _loadCtx, _readToken) {
23540
23940
  }
23541
23941
  } catch {
23542
23942
  }
23543
- const manifestPath2 = path36.join(os18.homedir(), ".olam", "cache", "manifest.json");
23943
+ const manifestPath2 = path37.join(os18.homedir(), ".olam", "cache", "manifest.json");
23544
23944
  let updateAvailable = null;
23545
23945
  let lastUpdateCheck = null;
23546
23946
  if (fs35.existsSync(manifestPath2)) {
@@ -23686,7 +24086,7 @@ init_context();
23686
24086
  init_output();
23687
24087
  import fs36 from "node:fs";
23688
24088
  import os19 from "node:os";
23689
- import path37 from "node:path";
24089
+ import path38 from "node:path";
23690
24090
  import { execFileSync as execFileSync8 } from "node:child_process";
23691
24091
  function registerClean(program2) {
23692
24092
  program2.command("clean").description("Reap orphan world filesystem state under ~/.olam/worlds/").option("--apply", "Actually delete the orphans (default is dry-run)", false).option(
@@ -23710,7 +24110,7 @@ async function runClean(opts) {
23710
24110
  printError(error?.message ?? "Olam is not configured. Run `olam init` first.");
23711
24111
  return 1;
23712
24112
  }
23713
- const worldsDir = path37.join(os19.homedir(), ".olam", "worlds");
24113
+ const worldsDir = path38.join(os19.homedir(), ".olam", "worlds");
23714
24114
  if (!fs36.existsSync(worldsDir)) {
23715
24115
  if (opts.json) {
23716
24116
  process.stdout.write(`${JSON.stringify({ worldsDir, entries: [] })}
@@ -23727,7 +24127,7 @@ async function runClean(opts) {
23727
24127
  const worktreeMap = collectWorktrees(worldsDir);
23728
24128
  const entries = [];
23729
24129
  for (const id of fs36.readdirSync(worldsDir).sort()) {
23730
- const fullPath = path37.join(worldsDir, id);
24130
+ const fullPath = path38.join(worldsDir, id);
23731
24131
  const stat = safeStat(fullPath);
23732
24132
  if (!stat || !stat.isDirectory()) continue;
23733
24133
  entries.push(classifyWorld({ id, fullPath, liveIds, worktreeMap }));
@@ -23741,7 +24141,7 @@ async function runClean(opts) {
23741
24141
  const deletable = entries.filter((e) => isDeletable(e, opts));
23742
24142
  const reclaimable = deletable.reduce((acc, e) => acc + e.bytes, 0);
23743
24143
  process.stdout.write(`
23744
- Reclaimable: ${formatBytes(reclaimable)} across ${deletable.length} entries`);
24144
+ Reclaimable: ${formatBytes2(reclaimable)} across ${deletable.length} entries`);
23745
24145
  if (entries.length > deletable.length) {
23746
24146
  process.stdout.write(
23747
24147
  ` (preserved ${entries.length - deletable.length}: live registry + ${opts.includeDirty ? "none" : "dirty worktrees"})`
@@ -23759,7 +24159,7 @@ async function runClean(opts) {
23759
24159
  if (!opts.yes) {
23760
24160
  process.stdout.write(
23761
24161
  `
23762
- About to delete ${deletable.length} entries (${formatBytes(reclaimable)}). Pass --yes to skip this prompt.
24162
+ About to delete ${deletable.length} entries (${formatBytes2(reclaimable)}). Pass --yes to skip this prompt.
23763
24163
  `
23764
24164
  );
23765
24165
  const confirmed = await confirmInteractive();
@@ -23782,7 +24182,7 @@ async function runClean(opts) {
23782
24182
  }
23783
24183
  process.stdout.write(
23784
24184
  `
23785
- Reaped ${removedCount}/${deletable.length} entries \xB7 ${formatBytes(removedBytes)} reclaimed.
24185
+ Reaped ${removedCount}/${deletable.length} entries \xB7 ${formatBytes2(removedBytes)} reclaimed.
23786
24186
  `
23787
24187
  );
23788
24188
  return removedCount === deletable.length ? 0 : 1;
@@ -23793,7 +24193,7 @@ function classifyWorld(args) {
23793
24193
  if (liveIds.has(id)) {
23794
24194
  return { id, path: fullPath, bytes, category: "active", note: "in registry" };
23795
24195
  }
23796
- const worktreeChild = path37.join(fullPath, "olam");
24196
+ const worktreeChild = path38.join(fullPath, "olam");
23797
24197
  const worktreeInfo = worktreeMap.get(worktreeChild);
23798
24198
  if (worktreeInfo) {
23799
24199
  if (worktreeInfo.dirty > 0 || worktreeInfo.unpushed > 0) {
@@ -23850,8 +24250,8 @@ function reapEntry(entry) {
23850
24250
  function collectWorktrees(worldsDir) {
23851
24251
  const out = /* @__PURE__ */ new Map();
23852
24252
  for (const id of fs36.readdirSync(worldsDir)) {
23853
- const child = path37.join(worldsDir, id, "olam");
23854
- const gitMarker = path37.join(child, ".git");
24253
+ const child = path38.join(worldsDir, id, "olam");
24254
+ const gitMarker = path38.join(child, ".git");
23855
24255
  if (!fs36.existsSync(gitMarker)) continue;
23856
24256
  const gitDir = resolveGitDirForWorktree(child);
23857
24257
  if (!gitDir) continue;
@@ -23863,7 +24263,7 @@ function collectWorktrees(worldsDir) {
23863
24263
  return out;
23864
24264
  }
23865
24265
  function resolveGitDirForWorktree(worktreePath) {
23866
- const gitMarker = path37.join(worktreePath, ".git");
24266
+ const gitMarker = path38.join(worktreePath, ".git");
23867
24267
  try {
23868
24268
  const top = execFileSync8("git", ["rev-parse", "--show-toplevel"], {
23869
24269
  cwd: worktreePath,
@@ -23877,7 +24277,7 @@ function resolveGitDirForWorktree(worktreePath) {
23877
24277
  stdio: "pipe"
23878
24278
  }).trim();
23879
24279
  if (!common) return top;
23880
- return path37.dirname(path37.resolve(worktreePath, common));
24280
+ return path38.dirname(path38.resolve(worktreePath, common));
23881
24281
  } catch {
23882
24282
  return null;
23883
24283
  }
@@ -23944,14 +24344,14 @@ function computeBytes(p) {
23944
24344
  } catch {
23945
24345
  continue;
23946
24346
  }
23947
- for (const name of entries) stack.push(path37.join(cur, name));
24347
+ for (const name of entries) stack.push(path38.join(cur, name));
23948
24348
  } else {
23949
24349
  total += st.size;
23950
24350
  }
23951
24351
  }
23952
24352
  return total;
23953
24353
  }
23954
- function formatBytes(n) {
24354
+ function formatBytes2(n) {
23955
24355
  if (n < 1024) return `${n}B`;
23956
24356
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
23957
24357
  if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)}MB`;
@@ -23970,7 +24370,7 @@ function printTable(entries) {
23970
24370
  `);
23971
24371
  for (const e of entries) {
23972
24372
  process.stdout.write(
23973
- ` ${e.category.padEnd(10)} ${formatBytes(e.bytes).padStart(10)} ${e.id.padEnd(28)} ${e.note ?? ""}
24373
+ ` ${e.category.padEnd(10)} ${formatBytes2(e.bytes).padStart(10)} ${e.id.padEnd(28)} ${e.note ?? ""}
23974
24374
  `
23975
24375
  );
23976
24376
  }
@@ -23998,7 +24398,7 @@ init_output();
23998
24398
  import { execFileSync as execFileSync9 } from "node:child_process";
23999
24399
  import * as fs37 from "node:fs";
24000
24400
  import * as os20 from "node:os";
24001
- import * as path38 from "node:path";
24401
+ import * as path39 from "node:path";
24002
24402
  import { createInterface as createInterface2 } from "node:readline/promises";
24003
24403
  import pc16 from "picocolors";
24004
24404
  var NPM_PACKAGE = "@pleri/olam-cli";
@@ -24020,7 +24420,7 @@ var realExec = (cmd, args) => {
24020
24420
  };
24021
24421
  function surveyOlam(opts = {}) {
24022
24422
  const exec = opts.exec ?? realExec;
24023
- const home = opts.olamHome ?? path38.join(os20.homedir(), ".olam");
24423
+ const home = opts.olamHome ?? path39.join(os20.homedir(), ".olam");
24024
24424
  const containers = listDocker(exec, ["ps", "-a", "--format", "{{.Names}}"]).filter(
24025
24425
  (n) => n.startsWith("olam-")
24026
24426
  );
@@ -24034,11 +24434,11 @@ function surveyOlam(opts = {}) {
24034
24434
  (n) => n.startsWith("olam-") && n !== "olam-host-cp-internal-default"
24035
24435
  );
24036
24436
  const worlds = [];
24037
- const worldsDir = path38.join(home, "worlds");
24437
+ const worldsDir = path39.join(home, "worlds");
24038
24438
  if (fs37.existsSync(worldsDir)) {
24039
24439
  for (const entry of fs37.readdirSync(worldsDir, { withFileTypes: true })) {
24040
24440
  if (!entry.isDirectory()) continue;
24041
- const wPath = path38.join(worldsDir, entry.name);
24441
+ const wPath = path39.join(worldsDir, entry.name);
24042
24442
  const bytes = dirSize(wPath);
24043
24443
  const { dirty, note } = inspectWorldGitState(wPath);
24044
24444
  worlds.push({ id: entry.name, bytes, dirty, note, path: wPath });
@@ -24047,7 +24447,7 @@ function surveyOlam(opts = {}) {
24047
24447
  const homeBytesNonWorld = fs37.existsSync(home) ? Math.max(0, dirSize(home) - worlds.reduce((acc, w) => acc + w.bytes, 0)) : 0;
24048
24448
  const npmRoot = exec("npm", ["root", "-g"]);
24049
24449
  const npmGlobalRoot = npmRoot.exitCode === 0 ? npmRoot.stdout.trim() : null;
24050
- const npmInstalled = npmGlobalRoot !== null && fs37.existsSync(path38.join(npmGlobalRoot, NPM_PACKAGE));
24450
+ const npmInstalled = npmGlobalRoot !== null && fs37.existsSync(path39.join(npmGlobalRoot, NPM_PACKAGE));
24051
24451
  return {
24052
24452
  containers,
24053
24453
  images,
@@ -24077,7 +24477,7 @@ function dirSize(p) {
24077
24477
  continue;
24078
24478
  }
24079
24479
  for (const e of entries) {
24080
- const full = path38.join(cur, e.name);
24480
+ const full = path39.join(cur, e.name);
24081
24481
  try {
24082
24482
  if (e.isDirectory()) stack.push(full);
24083
24483
  else if (e.isFile()) total += fs37.statSync(full).size;
@@ -24095,8 +24495,8 @@ function inspectWorldGitState(worldPath) {
24095
24495
  try {
24096
24496
  for (const entry of fs37.readdirSync(worldPath, { withFileTypes: true })) {
24097
24497
  if (!entry.isDirectory()) continue;
24098
- const repo = path38.join(worldPath, entry.name);
24099
- const gitDir = path38.join(repo, ".git");
24498
+ const repo = path39.join(worldPath, entry.name);
24499
+ const gitDir = path39.join(repo, ".git");
24100
24500
  if (!fs37.existsSync(gitDir)) continue;
24101
24501
  try {
24102
24502
  const status2 = execFileSync9("git", ["status", "--porcelain"], {
@@ -24166,8 +24566,8 @@ function renderSurvey(survey, opts) {
24166
24566
  lines.push(
24167
24567
  ` worlds: ${survey.worlds.length} (${dirtyWorlds.length} dirty, ${cleanWorlds.length} clean)`
24168
24568
  );
24169
- lines.push(` ~/.olam/* (non-world): ${formatBytes2(survey.homeBytesNonWorld)}`);
24170
- lines.push(` worlds total: ${formatBytes2(worldBytes)}`);
24569
+ lines.push(` ~/.olam/* (non-world): ${formatBytes3(survey.homeBytesNonWorld)}`);
24570
+ lines.push(` worlds total: ${formatBytes3(worldBytes)}`);
24171
24571
  lines.push(
24172
24572
  ` npm package installed: ${survey.npmInstalled ? `yes (at ${survey.npmGlobalRoot}/${NPM_PACKAGE})` : "no"}`
24173
24573
  );
@@ -24175,14 +24575,14 @@ function renderSurvey(survey, opts) {
24175
24575
  lines.push("");
24176
24576
  lines.push(pc16.yellow(` \u26A0 dirty worlds (${dirtyWorlds.length}) \u2014 pass --include-dirty to destroy:`));
24177
24577
  for (const w of dirtyWorlds) {
24178
- lines.push(` - ${w.id} ${formatBytes2(w.bytes)} (${w.note})`);
24578
+ lines.push(` - ${w.id} ${formatBytes3(w.bytes)} (${w.note})`);
24179
24579
  }
24180
24580
  }
24181
24581
  lines.push("");
24182
24582
  lines.push(pc16.dim(" Run with --yes to actually destroy. Default is dry-run."));
24183
24583
  return lines.join("\n");
24184
24584
  }
24185
- function formatBytes2(n) {
24585
+ function formatBytes3(n) {
24186
24586
  if (n < 1024) return `${n} B`;
24187
24587
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
24188
24588
  if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
@@ -24190,7 +24590,7 @@ function formatBytes2(n) {
24190
24590
  }
24191
24591
  async function executeImplode(survey, opts) {
24192
24592
  const exec = opts.exec ?? realExec;
24193
- const home = opts.olamHome ?? path38.join(os20.homedir(), ".olam");
24593
+ const home = opts.olamHome ?? path39.join(os20.homedir(), ".olam");
24194
24594
  const removed = [];
24195
24595
  const skipped = [];
24196
24596
  const errors = [];
@@ -24254,7 +24654,7 @@ async function executeImplode(survey, opts) {
24254
24654
  skipped.push(`fs:~/.olam/auth-data (--keep-vault)`);
24255
24655
  continue;
24256
24656
  }
24257
- const target = path38.join(home, entry.name);
24657
+ const target = path39.join(home, entry.name);
24258
24658
  try {
24259
24659
  fs37.rmSync(target, { recursive: true, force: true });
24260
24660
  removed.push(`fs:${target}`);
@@ -25928,11 +26328,11 @@ var qmarksTestNoExtDot = ([$0]) => {
25928
26328
  return (f) => f.length === len && f !== "." && f !== "..";
25929
26329
  };
25930
26330
  var defaultPlatform = typeof process === "object" && process ? typeof process.env === "object" && process.env && process.env.__MINIMATCH_TESTING_PLATFORM__ || process.platform : "posix";
25931
- var path40 = {
26331
+ var path41 = {
25932
26332
  win32: { sep: "\\" },
25933
26333
  posix: { sep: "/" }
25934
26334
  };
25935
- var sep2 = defaultPlatform === "win32" ? path40.win32.sep : path40.posix.sep;
26335
+ var sep2 = defaultPlatform === "win32" ? path41.win32.sep : path41.posix.sep;
25936
26336
  minimatch.sep = sep2;
25937
26337
  var GLOBSTAR = /* @__PURE__ */ Symbol("globstar **");
25938
26338
  minimatch.GLOBSTAR = GLOBSTAR;
@@ -26758,7 +27158,7 @@ function registerPolicyCheck(program2) {
26758
27158
  }
26759
27159
 
26760
27160
  // src/commands/worldspec/compile.ts
26761
- import { existsSync as existsSync40, mkdirSync as mkdirSync25, readFileSync as readFileSync29, writeFileSync as writeFileSync20 } from "node:fs";
27161
+ import { existsSync as existsSync41, mkdirSync as mkdirSync25, readFileSync as readFileSync30, writeFileSync as writeFileSync20 } from "node:fs";
26762
27162
  import { resolve as resolvePath } from "node:path";
26763
27163
  import YAML5 from "yaml";
26764
27164
 
@@ -27561,13 +27961,13 @@ function registerWorldspecCompile(parent) {
27561
27961
  const sourcePolicies = [];
27562
27962
  for (const p of paths) {
27563
27963
  const abs = resolvePath(process.cwd(), p);
27564
- if (!existsSync40(abs)) {
27964
+ if (!existsSync41(abs)) {
27565
27965
  printError(`worldspec not found at ${abs}`);
27566
27966
  process.exit(EXIT_WORLDSPEC_FILE_NOT_FOUND);
27567
27967
  }
27568
27968
  let yaml;
27569
27969
  try {
27570
- yaml = YAML5.parse(readFileSync29(abs, "utf8"));
27970
+ yaml = YAML5.parse(readFileSync30(abs, "utf8"));
27571
27971
  } catch (err) {
27572
27972
  printError(
27573
27973
  `${p}: YAML parse error: ${err.message}`
@@ -27652,7 +28052,7 @@ function getPkgVersion() {
27652
28052
  // src/commands/worldspec/init.ts
27653
28053
  init_exit_codes();
27654
28054
  init_output();
27655
- import { existsSync as existsSync41, mkdirSync as mkdirSync26, readFileSync as readFileSync30, writeFileSync as writeFileSync21 } from "node:fs";
28055
+ import { existsSync as existsSync42, mkdirSync as mkdirSync26, readFileSync as readFileSync31, writeFileSync as writeFileSync21 } from "node:fs";
27656
28056
  import { execSync as execSync10 } from "node:child_process";
27657
28057
  import { basename as basename3, resolve as resolvePath2 } from "node:path";
27658
28058
  function registerWorldspecInit(parent) {
@@ -27670,7 +28070,7 @@ function registerWorldspecInit(parent) {
27670
28070
  const cwd = process.cwd();
27671
28071
  const targetDir = resolvePath2(cwd, ".olam/worldspecs");
27672
28072
  const targetFile = resolvePath2(targetDir, "default.yaml");
27673
- if (existsSync41(targetFile) && !opts.force) {
28073
+ if (existsSync42(targetFile) && !opts.force) {
27674
28074
  printError(
27675
28075
  `${targetFile} already exists; pass --force to overwrite`
27676
28076
  );
@@ -27704,14 +28104,14 @@ function registerWorldspecInit(parent) {
27704
28104
  });
27705
28105
  }
27706
28106
  function detectProjectShape(root, useAdbYaml) {
27707
- const hasGemfile = existsSync41(resolvePath2(root, "Gemfile"));
27708
- const hasNodePackageJson = existsSync41(resolvePath2(root, "package.json"));
27709
- const hasAdbYaml = existsSync41(resolvePath2(root, ".adb.yaml"));
28107
+ const hasGemfile = existsSync42(resolvePath2(root, "Gemfile"));
28108
+ const hasNodePackageJson = existsSync42(resolvePath2(root, "package.json"));
28109
+ const hasAdbYaml = existsSync42(resolvePath2(root, ".adb.yaml"));
27710
28110
  let rubyVersion = null;
27711
28111
  const rubyVersionPath = resolvePath2(root, ".ruby-version");
27712
- if (existsSync41(rubyVersionPath)) {
28112
+ if (existsSync42(rubyVersionPath)) {
27713
28113
  try {
27714
- const raw = readFileSync30(rubyVersionPath, "utf8").trim();
28114
+ const raw = readFileSync31(rubyVersionPath, "utf8").trim();
27715
28115
  const match2 = raw.match(/(\d+\.\d+\.\d+)/);
27716
28116
  if (match2) rubyVersion = match2[1] ?? null;
27717
28117
  } catch {
@@ -29121,7 +29521,7 @@ function registerWorldspecSchema(parent) {
29121
29521
  }
29122
29522
 
29123
29523
  // src/commands/worldspec/validate.ts
29124
- import { existsSync as existsSync42, readFileSync as readFileSync31 } from "node:fs";
29524
+ import { existsSync as existsSync43, readFileSync as readFileSync32 } from "node:fs";
29125
29525
  import { resolve as resolvePath4 } from "node:path";
29126
29526
  init_exit_codes();
29127
29527
  init_output();
@@ -29138,13 +29538,13 @@ function registerWorldspecValidate(parent) {
29138
29538
  "human"
29139
29539
  ).action(async (pathArg, opts) => {
29140
29540
  const absPath = resolvePath4(process.cwd(), pathArg);
29141
- if (!existsSync42(absPath)) {
29541
+ if (!existsSync43(absPath)) {
29142
29542
  printError(`worldspec not found at ${absPath}`);
29143
29543
  process.exit(EXIT_WORLDSPEC_FILE_NOT_FOUND);
29144
29544
  }
29145
29545
  let yamlSource;
29146
29546
  try {
29147
- yamlSource = readFileSync31(absPath, "utf8");
29547
+ yamlSource = readFileSync32(absPath, "utf8");
29148
29548
  } catch (err) {
29149
29549
  printError(
29150
29550
  `failed to read ${absPath}: ${err.message}`
@@ -29191,17 +29591,17 @@ function registerWorldspec(program2) {
29191
29591
  init_output();
29192
29592
  init_host_cp();
29193
29593
  import * as fs41 from "node:fs";
29194
- import * as path43 from "node:path";
29195
- import { spawnSync as spawnSync16 } from "node:child_process";
29594
+ import * as path44 from "node:path";
29595
+ import { spawnSync as spawnSync17 } from "node:child_process";
29196
29596
  import ora9 from "ora";
29197
29597
  import pc20 from "picocolors";
29198
29598
 
29199
29599
  // src/commands/upgrade-lock.ts
29200
29600
  import * as fs39 from "node:fs";
29201
29601
  import * as os21 from "node:os";
29202
- import * as path41 from "node:path";
29203
- import { spawnSync as spawnSync15 } from "node:child_process";
29204
- var LOCK_FILE_PATH = path41.join(os21.homedir(), ".olam", ".upgrade.lock");
29602
+ import * as path42 from "node:path";
29603
+ import { spawnSync as spawnSync16 } from "node:child_process";
29604
+ var LOCK_FILE_PATH = path42.join(os21.homedir(), ".olam", ".upgrade.lock");
29205
29605
  var STALE_LOCK_TIMEOUT_MS = 5 * 60 * 1e3;
29206
29606
  function readLockFile(lockPath) {
29207
29607
  try {
@@ -29225,7 +29625,7 @@ function isPidAlive2(pid) {
29225
29625
  }
29226
29626
  var PS_UNAVAILABLE = "__ps_unavailable__";
29227
29627
  function getPidCommand(pid) {
29228
- const result = spawnSync15("ps", ["-p", String(pid), "-o", "comm="], {
29628
+ const result = spawnSync16("ps", ["-p", String(pid), "-o", "comm="], {
29229
29629
  encoding: "utf-8",
29230
29630
  stdio: ["ignore", "pipe", "ignore"]
29231
29631
  });
@@ -29251,7 +29651,7 @@ function isStaleLock(content, nowMs = Date.now()) {
29251
29651
  return false;
29252
29652
  }
29253
29653
  function acquireLock(lockPath = LOCK_FILE_PATH, nowMs = Date.now()) {
29254
- const dir = path41.dirname(lockPath);
29654
+ const dir = path42.dirname(lockPath);
29255
29655
  fs39.mkdirSync(dir, { recursive: true });
29256
29656
  for (let attempt = 0; attempt < 2; attempt++) {
29257
29657
  try {
@@ -29314,15 +29714,15 @@ function formatRefusalMessage(result, lockPath = LOCK_FILE_PATH) {
29314
29714
  // src/commands/upgrade-log.ts
29315
29715
  import * as fs40 from "node:fs";
29316
29716
  import * as os22 from "node:os";
29317
- import * as path42 from "node:path";
29717
+ import * as path43 from "node:path";
29318
29718
  function getUpgradeLogPath() {
29319
29719
  const home = process.env["HOME"] ?? os22.homedir();
29320
- return path42.join(home, ".olam", "upgrade.log");
29720
+ return path43.join(home, ".olam", "upgrade.log");
29321
29721
  }
29322
29722
  var UPGRADE_LOG_PATH = getUpgradeLogPath();
29323
29723
  function appendUpgradeLog(row, logPath = getUpgradeLogPath()) {
29324
29724
  try {
29325
- fs40.mkdirSync(path42.dirname(logPath), { recursive: true });
29725
+ fs40.mkdirSync(path43.dirname(logPath), { recursive: true });
29326
29726
  const line = JSON.stringify(row) + "\n";
29327
29727
  fs40.appendFileSync(logPath, line, { mode: 420 });
29328
29728
  } catch (err) {
@@ -29429,8 +29829,8 @@ init_protocol_version();
29429
29829
  init_install_root();
29430
29830
  var AUTH_HEALTH_URL2 = "http://127.0.0.1:9999/health";
29431
29831
  function isNodeModulesInSync(cwd) {
29432
- const lockPath = path43.join(cwd, "package-lock.json");
29433
- const markerPath = path43.join(cwd, "node_modules", ".package-lock.json");
29832
+ const lockPath = path44.join(cwd, "package-lock.json");
29833
+ const markerPath = path44.join(cwd, "node_modules", ".package-lock.json");
29434
29834
  if (!fs41.existsSync(lockPath) || !fs41.existsSync(markerPath)) return false;
29435
29835
  try {
29436
29836
  const lockStat = fs41.statSync(lockPath);
@@ -29450,7 +29850,7 @@ function shouldSkipInstall(opts, cwd) {
29450
29850
  return { skip: false };
29451
29851
  }
29452
29852
  function validateRepoRoot(cwd) {
29453
- const marker = path43.join(cwd, "packages/host-cp/compose.yaml");
29853
+ const marker = path44.join(cwd, "packages/host-cp/compose.yaml");
29454
29854
  if (!fs41.existsSync(marker)) {
29455
29855
  return {
29456
29856
  ok: false,
@@ -29486,7 +29886,7 @@ function extractBundleHash(indexHtml) {
29486
29886
  function runStep2(label, cmd, args, opts = {}) {
29487
29887
  const start = Date.now();
29488
29888
  process.stdout.write(` ${pc20.dim(label.padEnd(34))}`);
29489
- const result = spawnSync16(cmd, [...args], {
29889
+ const result = spawnSync17(cmd, [...args], {
29490
29890
  encoding: "utf-8",
29491
29891
  stdio: ["ignore", "pipe", "pipe"],
29492
29892
  cwd: opts.cwd ?? process.cwd(),
@@ -29505,7 +29905,7 @@ function runStep2(label, cmd, args, opts = {}) {
29505
29905
  };
29506
29906
  }
29507
29907
  function isGitDirty(cwd) {
29508
- const result = spawnSync16("git", ["status", "--porcelain"], {
29908
+ const result = spawnSync17("git", ["status", "--porcelain"], {
29509
29909
  encoding: "utf-8",
29510
29910
  stdio: ["ignore", "pipe", "pipe"],
29511
29911
  cwd
@@ -29513,7 +29913,7 @@ function isGitDirty(cwd) {
29513
29913
  return (result.stdout ?? "").trim().length > 0;
29514
29914
  }
29515
29915
  function hasGitUpstream(cwd) {
29516
- const result = spawnSync16("git", ["rev-parse", "--abbrev-ref", "@{u}"], {
29916
+ const result = spawnSync17("git", ["rev-parse", "--abbrev-ref", "@{u}"], {
29517
29917
  encoding: "utf-8",
29518
29918
  stdio: ["ignore", "pipe", "pipe"],
29519
29919
  cwd
@@ -29521,7 +29921,7 @@ function hasGitUpstream(cwd) {
29521
29921
  return result.status === 0;
29522
29922
  }
29523
29923
  function captureHeadSha(cwd) {
29524
- const result = spawnSync16("git", ["rev-parse", "HEAD"], {
29924
+ const result = spawnSync17("git", ["rev-parse", "HEAD"], {
29525
29925
  encoding: "utf-8",
29526
29926
  stdio: ["ignore", "pipe", "pipe"],
29527
29927
  cwd
@@ -29536,7 +29936,7 @@ function abbreviateSha(sha) {
29536
29936
  }
29537
29937
  function imageExists(tag) {
29538
29938
  try {
29539
- const result = spawnSync16("docker", ["image", "inspect", "--format", "{{.Id}}", tag], {
29939
+ const result = spawnSync17("docker", ["image", "inspect", "--format", "{{.Id}}", tag], {
29540
29940
  encoding: "utf-8",
29541
29941
  stdio: ["ignore", "pipe", "ignore"]
29542
29942
  });
@@ -29552,7 +29952,7 @@ function checkRollbackSetExists(plan) {
29552
29952
  }
29553
29953
  var SMOKE_DOCKER_TIMEOUT_MS = 3e4;
29554
29954
  function smokeImage(image, targetSha) {
29555
- const createResult = spawnSync16("docker", ["create", "--name", `olam-smoke-${Date.now()}`, image], {
29955
+ const createResult = spawnSync17("docker", ["create", "--name", `olam-smoke-${Date.now()}`, image], {
29556
29956
  encoding: "utf-8",
29557
29957
  stdio: ["ignore", "pipe", "pipe"],
29558
29958
  timeout: SMOKE_DOCKER_TIMEOUT_MS
@@ -29566,7 +29966,7 @@ function smokeImage(image, targetSha) {
29566
29966
  };
29567
29967
  }
29568
29968
  const containerId = (createResult.stdout ?? "").trim();
29569
- const inspectResult = spawnSync16(
29969
+ const inspectResult = spawnSync17(
29570
29970
  "docker",
29571
29971
  ["inspect", "--format", '{{index .Config.Labels "olam.build.sha"}}', image],
29572
29972
  {
@@ -29576,7 +29976,7 @@ function smokeImage(image, targetSha) {
29576
29976
  }
29577
29977
  );
29578
29978
  if (containerId.length > 0) {
29579
- spawnSync16("docker", ["rm", "-f", containerId], {
29979
+ spawnSync17("docker", ["rm", "-f", containerId], {
29580
29980
  encoding: "utf-8",
29581
29981
  stdio: ["ignore", "ignore", "ignore"],
29582
29982
  timeout: SMOKE_DOCKER_TIMEOUT_MS
@@ -29618,7 +30018,7 @@ var PRODUCTION_SWAP_PLAN = [
29618
30018
  ];
29619
30019
  function dockerTag(source, dest) {
29620
30020
  try {
29621
- const result = spawnSync16("docker", ["tag", source, dest], {
30021
+ const result = spawnSync17("docker", ["tag", source, dest], {
29622
30022
  encoding: "utf-8",
29623
30023
  stdio: ["ignore", "ignore", "pipe"]
29624
30024
  });
@@ -29782,11 +30182,11 @@ async function waitForAuthHealthLocal(timeoutMs = AUTH_HEALTH_TIMEOUT_MS) {
29782
30182
  async function recreateAuthService() {
29783
30183
  const start = Date.now();
29784
30184
  try {
29785
- spawnSync16("docker", ["stop", "olam-auth"], {
30185
+ spawnSync17("docker", ["stop", "olam-auth"], {
29786
30186
  encoding: "utf-8",
29787
30187
  stdio: ["ignore", "ignore", "ignore"]
29788
30188
  });
29789
- spawnSync16("docker", ["rm", "olam-auth"], {
30189
+ spawnSync17("docker", ["rm", "olam-auth"], {
29790
30190
  encoding: "utf-8",
29791
30191
  stdio: ["ignore", "ignore", "ignore"]
29792
30192
  });
@@ -29811,7 +30211,7 @@ async function recreateAuthService() {
29811
30211
  }
29812
30212
  }
29813
30213
  function readBundleHash(cwd) {
29814
- const indexPath = path43.join(cwd, "packages/control-plane/public/index.html");
30214
+ const indexPath = path44.join(cwd, "packages/control-plane/public/index.html");
29815
30215
  if (!fs41.existsSync(indexPath)) return null;
29816
30216
  return extractBundleHash(fs41.readFileSync(indexPath, "utf-8"));
29817
30217
  }
@@ -30084,11 +30484,11 @@ async function defaultRecreateMcpAuthForUpgrade() {
30084
30484
  }
30085
30485
  async function defaultRecreateAuthForUpgrade() {
30086
30486
  try {
30087
- spawnSync16("docker", ["stop", "olam-auth"], {
30487
+ spawnSync17("docker", ["stop", "olam-auth"], {
30088
30488
  encoding: "utf-8",
30089
30489
  stdio: ["ignore", "ignore", "ignore"]
30090
30490
  });
30091
- spawnSync16("docker", ["rm", "olam-auth"], {
30491
+ spawnSync17("docker", ["rm", "olam-auth"], {
30092
30492
  encoding: "utf-8",
30093
30493
  stdio: ["ignore", "ignore", "ignore"]
30094
30494
  });
@@ -30110,7 +30510,7 @@ async function defaultRecreateAuthForUpgrade() {
30110
30510
  }
30111
30511
  }
30112
30512
  function defaultInspectContainerLabels(containerName) {
30113
- const result = spawnSync16(
30513
+ const result = spawnSync17(
30114
30514
  "docker",
30115
30515
  [
30116
30516
  "inspect",
@@ -30134,7 +30534,7 @@ function defaultInspectContainerLabels(containerName) {
30134
30534
  return { ok: false, exists: false, project: "", service: "", stderr };
30135
30535
  }
30136
30536
  function defaultRemoveContainer(containerName) {
30137
- const result = spawnSync16("docker", ["rm", "-f", containerName], {
30537
+ const result = spawnSync17("docker", ["rm", "-f", containerName], {
30138
30538
  encoding: "utf-8",
30139
30539
  stdio: ["ignore", "pipe", "pipe"]
30140
30540
  });
@@ -30417,7 +30817,7 @@ ${buildResult.stderr}`);
30417
30817
  return;
30418
30818
  }
30419
30819
  const authSecret = readAuthSecret2();
30420
- const spaDir = path43.join(cwd, "packages/control-plane/app");
30820
+ const spaDir = path44.join(cwd, "packages/control-plane/app");
30421
30821
  const spaResult = runStep2(
30422
30822
  "vite build (SPA)",
30423
30823
  "npx",
@@ -30465,7 +30865,7 @@ ${spaResult.stderr}`);
30465
30865
  process.stdout.write(` ${pc20.dim(step.label.padEnd(34))}
30466
30866
  `);
30467
30867
  const start = Date.now();
30468
- const result = spawnSync16("bash", [scriptPath], {
30868
+ const result = spawnSync17("bash", [scriptPath], {
30469
30869
  stdio: "inherit",
30470
30870
  cwd,
30471
30871
  env: { ...process.env, ...olamTagEnv }
@@ -30810,7 +31210,7 @@ function registerLogs(program2) {
30810
31210
  init_context();
30811
31211
  init_output();
30812
31212
  import pc22 from "picocolors";
30813
- import { spawnSync as spawnSync17 } from "node:child_process";
31213
+ import { spawnSync as spawnSync18 } from "node:child_process";
30814
31214
  var SAFE_IDENT4 = /^[a-z0-9][a-z0-9-]{0,63}$/;
30815
31215
  function parseDockerTop(stdout) {
30816
31216
  const trimmed = stdout.trim();
@@ -30910,7 +31310,7 @@ function registerPs(program2) {
30910
31310
  const containerName = `olam-${worldId}-devbox`;
30911
31311
  let watchInterval;
30912
31312
  function fetchAndPrint() {
30913
- const result = spawnSync17(
31313
+ const result = spawnSync18(
30914
31314
  "docker",
30915
31315
  ["top", containerName, "pid", "user", "pcpu", "pmem", "stime", "stat", "cmd"],
30916
31316
  { encoding: "utf-8", timeout: 3e3 }
@@ -30949,13 +31349,13 @@ ${pc22.dim(`world: ${worldId} sort: ${sortKey} refresh: 5s Ctrl-C to exit`)}
30949
31349
  init_output();
30950
31350
  import * as fs42 from "node:fs";
30951
31351
  import * as os23 from "node:os";
30952
- import * as path44 from "node:path";
31352
+ import * as path45 from "node:path";
30953
31353
  import YAML6 from "yaml";
30954
31354
  function olamHome3() {
30955
- return process.env.OLAM_HOME ?? path44.join(os23.homedir(), ".olam");
31355
+ return process.env.OLAM_HOME ?? path45.join(os23.homedir(), ".olam");
30956
31356
  }
30957
31357
  function keysFilePath() {
30958
- return path44.join(olamHome3(), "keys.yaml");
31358
+ return path45.join(olamHome3(), "keys.yaml");
30959
31359
  }
30960
31360
  function readKeysFile() {
30961
31361
  const filePath = keysFilePath();
@@ -31051,7 +31451,7 @@ function registerKeys(program2) {
31051
31451
  init_snapshot();
31052
31452
  init_output();
31053
31453
  import * as fs43 from "node:fs";
31054
- import * as path45 from "node:path";
31454
+ import * as path46 from "node:path";
31055
31455
  import { execSync as execSync11 } from "node:child_process";
31056
31456
  import pc23 from "picocolors";
31057
31457
 
@@ -31074,9 +31474,9 @@ function emitDeprecationWarning(subcommand) {
31074
31474
  }
31075
31475
  function bumpDeprecationCounter() {
31076
31476
  if (process.env[INTERNAL_SENTINEL_ENV] === "1") return;
31077
- const counterPath = path45.join(snapshotsDir(), ".deprecation-counter");
31477
+ const counterPath = path46.join(snapshotsDir(), ".deprecation-counter");
31078
31478
  try {
31079
- fs43.mkdirSync(path45.dirname(counterPath), { recursive: true });
31479
+ fs43.mkdirSync(path46.dirname(counterPath), { recursive: true });
31080
31480
  let current = 0;
31081
31481
  if (fs43.existsSync(counterPath)) {
31082
31482
  const raw = fs43.readFileSync(counterPath, "utf-8").trim();
@@ -31159,13 +31559,13 @@ function resolveKinds(arg) {
31159
31559
  return [];
31160
31560
  }
31161
31561
  async function captureGems(worldId, workspacePath, repo) {
31162
- const repoDir = path45.join(workspacePath, repo);
31562
+ const repoDir = path46.join(workspacePath, repo);
31163
31563
  const fingerprint = computeGemsFingerprint(repoDir);
31164
31564
  if (!fingerprint) {
31165
31565
  return { ok: false, tarPath: "", msg: "no Gemfile.lock \u2014 layer does not apply" };
31166
31566
  }
31167
31567
  const tarPath = snapshotTarPath(worldId, "gems", repo, fingerprint);
31168
- const vendorBundle = path45.join(repoDir, "vendor", "bundle");
31568
+ const vendorBundle = path46.join(repoDir, "vendor", "bundle");
31169
31569
  if (fs43.existsSync(vendorBundle)) {
31170
31570
  try {
31171
31571
  packTarball(vendorBundle, tarPath);
@@ -31202,7 +31602,7 @@ async function captureGems(worldId, workspacePath, repo) {
31202
31602
  `docker exec ${containerName} sh -c 'mkdir -p "$(dirname ${tmpTar})" && tar -czf ${tmpTar}.tmp -C ${bundlePath} . && mv ${tmpTar}.tmp ${tmpTar}'`,
31203
31603
  { stdio: "pipe", timeout: 12e4 }
31204
31604
  );
31205
- fs43.mkdirSync(path45.dirname(tarPath), { recursive: true });
31605
+ fs43.mkdirSync(path46.dirname(tarPath), { recursive: true });
31206
31606
  execSync11(`docker cp ${containerName}:${tmpTar} "${tarPath}"`, { stdio: "pipe", timeout: 12e4 });
31207
31607
  execSync11(`docker exec ${containerName} rm -f ${tmpTar}`, { stdio: "pipe" });
31208
31608
  const stat = fs43.statSync(tarPath);
@@ -31222,12 +31622,12 @@ async function captureGems(worldId, workspacePath, repo) {
31222
31622
  }
31223
31623
  }
31224
31624
  async function captureNode(worldId, workspacePath, repo) {
31225
- const repoDir = path45.join(workspacePath, repo);
31625
+ const repoDir = path46.join(workspacePath, repo);
31226
31626
  const fingerprint = computeNodeFingerprint(repoDir);
31227
31627
  if (!fingerprint) {
31228
31628
  return { ok: false, tarPath: "", msg: "no lockfile \u2014 layer does not apply" };
31229
31629
  }
31230
- const nodeModules = path45.join(repoDir, "node_modules");
31630
+ const nodeModules = path46.join(repoDir, "node_modules");
31231
31631
  if (!fs43.existsSync(nodeModules)) {
31232
31632
  return { ok: false, tarPath: "", msg: "node_modules not installed yet" };
31233
31633
  }
@@ -31251,7 +31651,7 @@ async function captureNode(worldId, workspacePath, repo) {
31251
31651
  }
31252
31652
  }
31253
31653
  async function capturePg(worldId, workspacePath, repoNames) {
31254
- const repoDirs = repoNames.map((r) => path45.join(workspacePath, r));
31654
+ const repoDirs = repoNames.map((r) => path46.join(workspacePath, r));
31255
31655
  const fingerprint = computePgFingerprint(repoDirs);
31256
31656
  if (!fingerprint) {
31257
31657
  return { ok: false, tarPath: "", msg: "no Gemfile.lock / schema.rb \u2014 layer does not apply" };
@@ -31266,9 +31666,9 @@ async function capturePg(worldId, workspacePath, repoNames) {
31266
31666
  }
31267
31667
  try {
31268
31668
  execSync11(`docker stop ${containerName}`, { stdio: "pipe", timeout: 3e4 });
31269
- fs43.mkdirSync(path45.dirname(tarPath), { recursive: true });
31669
+ fs43.mkdirSync(path46.dirname(tarPath), { recursive: true });
31270
31670
  execSync11(
31271
- `docker run --rm -v "${volumeName2}:/pgdata:ro" -v "${path45.dirname(tarPath)}:/dest" alpine sh -c 'tar -czf /dest/${path45.basename(tarPath)}.tmp -C /pgdata . && mv /dest/${path45.basename(tarPath)}.tmp /dest/${path45.basename(tarPath)}'`,
31671
+ `docker run --rm -v "${volumeName2}:/pgdata:ro" -v "${path46.dirname(tarPath)}:/dest" alpine sh -c 'tar -czf /dest/${path46.basename(tarPath)}.tmp -C /pgdata . && mv /dest/${path46.basename(tarPath)}.tmp /dest/${path46.basename(tarPath)}'`,
31272
31672
  { stdio: "pipe", timeout: 18e4 }
31273
31673
  );
31274
31674
  execSync11(`docker start ${containerName}`, { stdio: "pipe", timeout: 3e4 });
@@ -31303,7 +31703,7 @@ async function handleEvict(opts) {
31303
31703
  const allTars = [];
31304
31704
  const walk3 = (d) => {
31305
31705
  for (const entry of fs43.readdirSync(d, { withFileTypes: true })) {
31306
- const full = path45.join(d, entry.name);
31706
+ const full = path46.join(d, entry.name);
31307
31707
  if (entry.isDirectory()) {
31308
31708
  walk3(full);
31309
31709
  } else if (entry.name.endsWith(".tar.gz")) {
@@ -31315,7 +31715,7 @@ async function handleEvict(opts) {
31315
31715
  walk3(root);
31316
31716
  const total = allTars.reduce((a, t) => a + t.size, 0);
31317
31717
  if (total <= maxBytes) {
31318
- console.log(`${formatBytes3(total)} total \u2264 ${formatBytes3(maxBytes)} cap; nothing to evict.`);
31718
+ console.log(`${formatBytes4(total)} total \u2264 ${formatBytes4(maxBytes)} cap; nothing to evict.`);
31319
31719
  return;
31320
31720
  }
31321
31721
  allTars.sort((a, b) => a.mtime - b.mtime);
@@ -31326,19 +31726,19 @@ async function handleEvict(opts) {
31326
31726
  toEvict.push(t);
31327
31727
  remaining -= t.size;
31328
31728
  }
31329
- printHeader(`Would evict ${toEvict.length} snapshot(s) (${formatBytes3(total - remaining)})`);
31729
+ printHeader(`Would evict ${toEvict.length} snapshot(s) (${formatBytes4(total - remaining)})`);
31330
31730
  console.log();
31331
31731
  for (const t of toEvict) {
31332
31732
  const age = formatAge2(Date.now() - t.mtime);
31333
- console.log(` ${pc23.dim(formatBytes3(t.size).padStart(8))} ${age.padStart(10)} ${t.path}`);
31733
+ console.log(` ${pc23.dim(formatBytes4(t.size).padStart(8))} ${age.padStart(10)} ${t.path}`);
31334
31734
  }
31335
31735
  console.log();
31336
- console.log(pc23.dim(`Total after eviction: ${formatBytes3(remaining)} (cap: ${formatBytes3(maxBytes)})`));
31736
+ console.log(pc23.dim(`Total after eviction: ${formatBytes4(remaining)} (cap: ${formatBytes4(maxBytes)})`));
31337
31737
  return;
31338
31738
  }
31339
31739
  const freed = evictOldSnapshotsWithFlock(maxBytes);
31340
31740
  if (freed > 0) {
31341
- printSuccess(`Evicted ${formatBytes3(freed)} (cap: ${formatBytes3(maxBytes)})`);
31741
+ printSuccess(`Evicted ${formatBytes4(freed)} (cap: ${formatBytes4(maxBytes)})`);
31342
31742
  } else {
31343
31743
  console.log(pc23.dim("No eviction needed (or another process held the lock)."));
31344
31744
  }
@@ -31361,7 +31761,7 @@ function handleList2(worldIdFilter) {
31361
31761
  lastWorldId = manifest.worldId;
31362
31762
  }
31363
31763
  const repo = manifest.repo ?? "(all repos)";
31364
- const size = formatBytes3(manifest.sizeBytes);
31764
+ const size = formatBytes4(manifest.sizeBytes);
31365
31765
  const age = formatAge2(entry.ageMs);
31366
31766
  const kindColor = manifest.kind === "gems" ? pc23.magenta(manifest.kind) : manifest.kind === "node" ? pc23.cyan(manifest.kind) : pc23.yellow(manifest.kind);
31367
31767
  console.log(
@@ -31382,7 +31782,7 @@ async function loadWorldMeta(worldId) {
31382
31782
  }
31383
31783
  return { workspacePath: world.workspacePath, repoNames: [...world.repos] };
31384
31784
  }
31385
- function formatBytes3(bytes) {
31785
+ function formatBytes4(bytes) {
31386
31786
  if (bytes < 1024) return `${bytes}B`;
31387
31787
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
31388
31788
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
@@ -31402,33 +31802,33 @@ init_context();
31402
31802
  init_output();
31403
31803
  import * as fs45 from "node:fs";
31404
31804
  import * as os24 from "node:os";
31405
- import * as path47 from "node:path";
31406
- import { spawnSync as spawnSync18 } from "node:child_process";
31805
+ import * as path48 from "node:path";
31806
+ import { spawnSync as spawnSync19 } from "node:child_process";
31407
31807
  import ora10 from "ora";
31408
31808
 
31409
31809
  // src/commands/refresh-helpers.ts
31410
31810
  import * as fs44 from "node:fs";
31411
- import * as path46 from "node:path";
31811
+ import * as path47 from "node:path";
31412
31812
  function collectCpSourceFiles(standaloneDir) {
31413
31813
  if (!fs44.existsSync(standaloneDir)) {
31414
31814
  throw new Error(`CP standalone dir not found: ${standaloneDir}`);
31415
31815
  }
31416
31816
  const entries = [];
31417
31817
  const topLevel = fs44.readdirSync(standaloneDir).filter((f) => {
31418
- const stat = fs44.statSync(path46.join(standaloneDir, f));
31818
+ const stat = fs44.statSync(path47.join(standaloneDir, f));
31419
31819
  return stat.isFile() && f.endsWith(".mjs") && !f.endsWith(".test.mjs");
31420
31820
  }).sort();
31421
31821
  for (const f of topLevel) {
31422
- entries.push({ srcPath: path46.join(standaloneDir, f), destRelPath: f });
31822
+ entries.push({ srcPath: path47.join(standaloneDir, f), destRelPath: f });
31423
31823
  }
31424
- const libDir = path46.join(standaloneDir, "lib");
31824
+ const libDir = path47.join(standaloneDir, "lib");
31425
31825
  if (fs44.existsSync(libDir) && fs44.statSync(libDir).isDirectory()) {
31426
31826
  const libFiles = fs44.readdirSync(libDir).filter((f) => {
31427
- const stat = fs44.statSync(path46.join(libDir, f));
31827
+ const stat = fs44.statSync(path47.join(libDir, f));
31428
31828
  return stat.isFile() && f.endsWith(".mjs") && !f.endsWith(".test.mjs");
31429
31829
  }).sort();
31430
31830
  for (const f of libFiles) {
31431
- entries.push({ srcPath: path46.join(libDir, f), destRelPath: `lib/${f}` });
31831
+ entries.push({ srcPath: path47.join(libDir, f), destRelPath: `lib/${f}` });
31432
31832
  }
31433
31833
  }
31434
31834
  return entries;
@@ -31445,9 +31845,9 @@ function validateServerMjsPresent(entries) {
31445
31845
  var HOST_CP_BASE_PORT = 19080;
31446
31846
  var RESTART_TIMEOUT_S = 30;
31447
31847
  var HEALTH_POLL_MS = 500;
31448
- var HEALTH_TIMEOUT_MS = 3e4;
31848
+ var HEALTH_TIMEOUT_MS2 = 3e4;
31449
31849
  function docker(args) {
31450
- const result = spawnSync18("docker", args, {
31850
+ const result = spawnSync19("docker", args, {
31451
31851
  encoding: "utf-8",
31452
31852
  stdio: ["ignore", "pipe", "pipe"]
31453
31853
  });
@@ -31457,7 +31857,7 @@ function docker(args) {
31457
31857
  stderr: result.stderr ?? ""
31458
31858
  };
31459
31859
  }
31460
- async function waitForCpHealth(port2, timeoutMs = HEALTH_TIMEOUT_MS) {
31860
+ async function waitForCpHealth(port2, timeoutMs = HEALTH_TIMEOUT_MS2) {
31461
31861
  const deadline = Date.now() + timeoutMs;
31462
31862
  while (Date.now() < deadline) {
31463
31863
  try {
@@ -31487,15 +31887,15 @@ async function refreshWorld(worldId, portOffset, standaloneDir, opts) {
31487
31887
  };
31488
31888
  }
31489
31889
  const stagingDir = fs45.mkdtempSync(
31490
- path47.join(os24.tmpdir(), `olam-refresh-${worldId}-`)
31890
+ path48.join(os24.tmpdir(), `olam-refresh-${worldId}-`)
31491
31891
  );
31492
31892
  try {
31493
31893
  const hasLib = entries.some((e) => e.destRelPath.startsWith("lib/"));
31494
31894
  if (hasLib) {
31495
- fs45.mkdirSync(path47.join(stagingDir, "lib"), { recursive: true });
31895
+ fs45.mkdirSync(path48.join(stagingDir, "lib"), { recursive: true });
31496
31896
  }
31497
31897
  for (const { srcPath, destRelPath } of entries) {
31498
- fs45.copyFileSync(srcPath, path47.join(stagingDir, destRelPath));
31898
+ fs45.copyFileSync(srcPath, path48.join(stagingDir, destRelPath));
31499
31899
  }
31500
31900
  const cpResult = docker([
31501
31901
  "cp",
@@ -31529,7 +31929,7 @@ async function refreshWorld(worldId, portOffset, standaloneDir, opts) {
31529
31929
  const cpPort = HOST_CP_BASE_PORT + portOffset;
31530
31930
  const healthy = await waitForCpHealth(cpPort);
31531
31931
  if (!healthy) {
31532
- return { worldId, ok: true, error: `healthy but /health at :${cpPort} didn't respond within ${HEALTH_TIMEOUT_MS / 1e3}s` };
31932
+ return { worldId, ok: true, error: `healthy but /health at :${cpPort} didn't respond within ${HEALTH_TIMEOUT_MS2 / 1e3}s` };
31533
31933
  }
31534
31934
  }
31535
31935
  return { worldId, ok: true };
@@ -31547,7 +31947,7 @@ function registerRefresh(program2) {
31547
31947
  process.exitCode = 1;
31548
31948
  return;
31549
31949
  }
31550
- const standaloneDir = path47.join(
31950
+ const standaloneDir = path48.join(
31551
31951
  process.cwd(),
31552
31952
  "packages/control-plane/standalone"
31553
31953
  );
@@ -31625,637 +32025,256 @@ Run \`olam refresh\` from the olam repo root.`
31625
32025
  }
31626
32026
  if (results.some((r) => !r.ok)) {
31627
32027
  process.exitCode = 1;
31628
- }
31629
- });
31630
- }
31631
-
31632
- // src/commands/restart.ts
31633
- init_context();
31634
- init_output();
31635
- import { existsSync as existsSync50 } from "node:fs";
31636
- import { dirname as dirname31, resolve as resolve15 } from "node:path";
31637
- import { spawnSync as spawnSync19 } from "node:child_process";
31638
- var RESTART_TIMEOUT_S2 = 30;
31639
- function docker2(args) {
31640
- const result = spawnSync19("docker", args, {
31641
- encoding: "utf-8",
31642
- stdio: ["ignore", "pipe", "pipe"]
31643
- });
31644
- return {
31645
- ok: result.status === 0 && result.error === void 0,
31646
- stderr: result.stderr ?? ""
31647
- };
31648
- }
31649
- function resolveRepoRoot2(start) {
31650
- let cur = start;
31651
- while (true) {
31652
- if (existsSync50(resolve15(cur, "packages")) && existsSync50(resolve15(cur, "package.json"))) {
31653
- return cur;
31654
- }
31655
- const parent = dirname31(cur);
31656
- if (parent === cur) return start;
31657
- cur = parent;
31658
- }
31659
- }
31660
- function registerRestart(program2) {
31661
- program2.command("restart").description("Restart a world container (auto-builds agent-stream bundle when stale)").argument("<world>", "World ID or name").action(async (worldIdOrName) => {
31662
- const { ctx, error } = await loadContext();
31663
- if (!ctx) {
31664
- printError(error?.message ?? "Olam is not configured. Run `olam init` first.");
31665
- process.exitCode = 1;
31666
- return;
31667
- }
31668
- const world = ctx.worldManager.getWorld(worldIdOrName);
31669
- if (!world) {
31670
- printError(`World "${worldIdOrName}" not found. Run \`olam list\` to see worlds.`);
31671
- process.exitCode = 1;
31672
- return;
31673
- }
31674
- printHeader(`olam restart: ${world.name}`);
31675
- try {
31676
- const bundleSource = resolveBundleSource();
31677
- if (bundleSource.mode === "install") {
31678
- process.stderr.write("bundle source: install-mode; skipping rebuild\n");
31679
- } else {
31680
- const repoRoot = resolveRepoRoot2(process.cwd());
31681
- const buildResult = buildIfStale(repoRoot);
31682
- if (!buildResult.ok) {
31683
- printError(
31684
- "Agent-stream bundle build failed. Not restarting with a stale bundle.\nFix the build error and re-run `olam restart`."
31685
- );
31686
- process.exitCode = 1;
31687
- return;
31688
- }
31689
- printInfo("Bundle", buildResult.message);
31690
- }
31691
- } catch (err) {
31692
- if (err instanceof BundleSourceNotFoundError) {
31693
- process.stderr.write(
31694
- `Warning: agent-stream bundle path unresolved; container uses image-baked dist.
31695
- `
31696
- );
31697
- } else {
31698
- throw err;
31699
- }
31700
- }
31701
- const containerName = `olam-${world.id}-devbox`;
31702
- printInfo("Container", containerName);
31703
- const result = docker2(["restart", "--time", String(RESTART_TIMEOUT_S2), containerName]);
31704
- if (!result.ok) {
31705
- printError(
31706
- `docker restart failed: ${result.stderr.trim() || "(no stderr)"}`
31707
- );
31708
- process.exitCode = 1;
31709
- return;
31710
- }
31711
- printInfo("Status", "restarted");
31712
- });
31713
- }
31714
-
31715
- // src/commands/diagnose.ts
31716
- import * as fs46 from "node:fs";
31717
- import * as os25 from "node:os";
31718
- import * as path48 from "node:path";
31719
- import { execFileSync as execFileSync11, execSync as execSync12 } from "node:child_process";
31720
- import pc24 from "picocolors";
31721
-
31722
- // ../core/dist/diagnose/secret-stripper.js
31723
- var SECRET_PATTERNS = [
31724
- { name: "anthropic-key", re: /sk-ant-[A-Za-z0-9\-_]{8,}/g },
31725
- { name: "github-pat", re: /gh[pourstu]_[A-Za-z0-9]{20,}/g },
31726
- { name: "github-pat-fgp", re: /github_pat_[A-Za-z0-9_]{22,}/g },
31727
- { name: "aws-access-key", re: /AKIA[A-Z0-9]{16}/g },
31728
- { name: "bearer-header", re: /Bearer\s+[A-Za-z0-9._\-+/=]{16,}/g },
31729
- { name: "slack-token", re: /xox[bpsa]-[A-Za-z0-9-]{10,}/g },
31730
- { name: "host-cp-token", re: /(?:host[-_.]?cp[-_.]?token|"token")\s*[:=]\s*"?([A-Za-z0-9._\-]{16,})"?/gi },
31731
- { name: "generic-hex-key", re: /\bkey\s*[:=]\s*[0-9a-f]{32,}/gi },
31732
- { name: "db-url", re: /(?:postgres|mysql|redis|mongodb):\/\/[^:]+:[^@]+@[^\s'"]+/gi },
31733
- { name: "env-secret", re: /(?:API_KEY|SECRET_KEY|AUTH_TOKEN|ACCESS_TOKEN|PRIVATE_KEY|DATABASE_URL|REDIS_URL)\s*[=:]\s*\S+/gi },
31734
- { name: "pem-key", re: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC )?PRIVATE KEY-----/g }
31735
- ];
31736
- var REDACTED = "[REDACTED]";
31737
- function stripSecrets(input2) {
31738
- let out = input2;
31739
- for (const { re } of SECRET_PATTERNS) {
31740
- re.lastIndex = 0;
31741
- out = out.replace(re, (match2) => {
31742
- const colonOrEq = match2.indexOf(":") !== -1 ? match2.indexOf(":") : match2.indexOf("=");
31743
- if (colonOrEq !== -1 && (match2.includes("token") || match2.includes("key") || match2.includes("KEY"))) {
31744
- return match2.slice(0, colonOrEq + 1) + " " + REDACTED;
31745
- }
31746
- return REDACTED;
31747
- });
31748
- }
31749
- return out;
31750
- }
31751
-
31752
- // src/commands/diagnose.ts
31753
- var DIAGNOSTICS_DIR = path48.join(os25.homedir(), ".olam", "diagnostics");
31754
- var LOG_DIR = path48.join(os25.homedir(), ".olam", "log");
31755
- var CACHE_DIR = path48.join(os25.homedir(), ".olam", "cache");
31756
- var LOG_TAIL_LINES = 200;
31757
- function safeExec(cmd) {
31758
- try {
31759
- return execSync12(cmd, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
31760
- } catch {
31761
- return "";
31762
- }
31763
- }
31764
- function defaultZip(zipPath, files) {
31765
- execFileSync11("zip", ["-j", zipPath, ...files]);
31766
- }
31767
- async function buildDiagnosticsZip(_exec = (cmd) => safeExec(cmd), _outDir = DIAGNOSTICS_DIR, _logDir = LOG_DIR, _zip = defaultZip) {
31768
- const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 23);
31769
- const zipPath = path48.join(_outDir, `olam-diag-${ts}.zip`);
31770
- const tmpDir = fs46.mkdtempSync(path48.join(os25.tmpdir(), "olam-diag-"));
31771
- try {
31772
- fs46.mkdirSync(_outDir, { recursive: true });
31773
- const entries = [];
31774
- const version = process.env["OLAM_CLI_VERSION"] ?? "unknown";
31775
- const nodeVersion = process.version;
31776
- const platform = `${process.platform}/${process.arch}`;
31777
- const versionContent = `olam: ${version}
31778
- node: ${nodeVersion}
31779
- platform: ${platform}
31780
- `;
31781
- _writeEntry(tmpDir, "version.txt", stripSecrets(versionContent), entries);
31782
- const osContent = [
31783
- `os: ${os25.type()} ${os25.release()}`,
31784
- `arch: ${os25.arch()}`,
31785
- `uptime: ${os25.uptime()}s`
31786
- ].join("\n") + "\n";
31787
- _writeEntry(tmpDir, "os-info.txt", stripSecrets(osContent), entries);
31788
- const depsFile = path48.join(CACHE_DIR, "deps.json");
31789
- if (fs46.existsSync(depsFile)) {
31790
- const deps = fs46.readFileSync(depsFile, "utf-8");
31791
- _writeEntry(tmpDir, "deps.json", stripSecrets(deps), entries);
31792
- }
31793
- const latestLog = _latestLog(_logDir);
31794
- if (latestLog) {
31795
- const lines = fs46.readFileSync(latestLog, "utf-8").split("\n");
31796
- const tail = lines.slice(-LOG_TAIL_LINES).join("\n");
31797
- _writeEntry(tmpDir, "log-tail.txt", stripSecrets(tail), entries);
31798
- }
31799
- const statusOut = _exec("olam status --json");
31800
- if (statusOut) {
31801
- _writeEntry(tmpDir, "status.json", stripSecrets(statusOut), entries);
31802
- }
31803
- const authAudit = _exec("npm run audit:auth-callers --if-present 2>&1");
31804
- if (authAudit) {
31805
- _writeEntry(tmpDir, "audit-auth-callers.txt", stripSecrets(authAudit), entries);
31806
- }
31807
- const fileArgs = entries.map((e) => path48.join(tmpDir, e));
31808
- try {
31809
- _zip(zipPath, fileArgs);
31810
- } catch (err) {
31811
- throw new Error(`zip command produced no output file. zip stderr: ${err.message}`);
31812
- }
31813
- if (!fs46.existsSync(zipPath)) {
31814
- throw new Error("zip command produced no output file.");
31815
- }
31816
- return { zipPath, entries };
31817
- } finally {
31818
- try {
31819
- fs46.rmSync(tmpDir, { recursive: true, force: true });
31820
- } catch {
31821
- }
31822
- }
31823
- }
31824
- function _writeEntry(dir, name, content, entries) {
31825
- fs46.writeFileSync(path48.join(dir, name), content, { mode: 420 });
31826
- entries.push(name);
31827
- }
31828
- function _latestLog(logDir) {
31829
- if (!fs46.existsSync(logDir)) return null;
31830
- const files = fs46.readdirSync(logDir).filter((f) => f.startsWith("host-")).sort().reverse();
31831
- return files.length > 0 ? path48.join(logDir, files[0]) : null;
31832
- }
31833
- async function buildTelemetryPayload() {
31834
- const channel = "stable";
31835
- const manifestFile = path48.join(CACHE_DIR, "manifest.json");
31836
- let manifestAgeHours = null;
31837
- if (fs46.existsSync(manifestFile)) {
31838
- const mtime = fs46.statSync(manifestFile).mtime.getTime();
31839
- manifestAgeHours = Math.round((Date.now() - mtime) / 36e5);
31840
- }
31841
- return {
31842
- version: process.env["OLAM_CLI_VERSION"] ?? "unknown",
31843
- os: process.platform,
31844
- arch: process.arch,
31845
- channel,
31846
- manifest_age_hours: manifestAgeHours
31847
- // No userid, no project, no path — privacy per design doc
31848
- };
31849
- }
31850
- function registerDiagnose(program2) {
31851
- program2.command("diagnose").description("Bundle diagnostics into a zip file for sharing with maintainers").option("--show-telemetry", "Print the telemetry payload that would be sent (no endpoint yet)").option("--no-telemetry", "Suppress telemetry payload (future opt-out)").option("--upload", "Share zip with maintainers (endpoint not yet provisioned)").option("--quiet", "Suppress progress output").action(async (opts) => {
31852
- if (opts.showTelemetry) {
31853
- const payload = await buildTelemetryPayload();
31854
- console.log(JSON.stringify(payload, null, 2));
31855
- return;
31856
- }
31857
- if (!opts.quiet) {
31858
- console.log(pc24.cyan("Building diagnostics zip\u2026"));
31859
- }
31860
- try {
31861
- const { zipPath, entries } = await buildDiagnosticsZip();
31862
- if (!opts.quiet) {
31863
- console.log(pc24.green(`Done: ${zipPath}`));
31864
- console.log(pc24.dim(`Contents: ${entries.join(", ")}`));
31865
- }
31866
- if (opts.upload) {
31867
- console.log(pc24.yellow(
31868
- `Telemetry endpoint not yet provisioned. Share the zip manually:
31869
- File: ${zipPath}
31870
- See docs/runbooks/share-diagnostics.md for instructions.`
31871
- ));
31872
- }
31873
- } catch (err) {
31874
- console.error(pc24.red(`Diagnose failed: ${err.message}`));
31875
- process.exitCode = 1;
31876
- }
31877
- });
31878
- }
31879
-
31880
- // src/lib/health-probes.ts
31881
- import { spawnSync as spawnSync20 } from "node:child_process";
31882
- import { existsSync as existsSync52, readdirSync as readdirSync13, readFileSync as readFileSync38, statSync as statSync15 } from "node:fs";
31883
- import { homedir as homedir25 } from "node:os";
31884
- import path49 from "node:path";
31885
-
31886
- // src/lib/kg-caps.ts
31887
- var SOFT_CAP_BYTES = 15e8;
31888
- var HARD_CAP_BYTES = 5e9;
31889
-
31890
- // src/lib/health-probes.ts
31891
- var HEALTH_TIMEOUT_MS2 = 5e3;
31892
- var COLIMA_010_WARN_TEXT = (
31893
- // eslint-disable-next-line @typescript-eslint/quotes
31894
- "Colima 0.10.1's --kubernetes mode is broken upstream. Either switch to OLAM_HOST_CP_ENGINE=docker (recommended) or downgrade Colima to 0.9.x. Track abiosoft/colima issues for fix."
31895
- );
31896
- var COLIMA_010_RANGE = /^v?0\.10\.\d+$/;
31897
- var defaultDockerExec2 = (cmd, args) => {
31898
- const r = spawnSync20(cmd, [...args], {
32028
+ }
32029
+ });
32030
+ }
32031
+
32032
+ // src/commands/restart.ts
32033
+ init_context();
32034
+ init_output();
32035
+ import { existsSync as existsSync51 } from "node:fs";
32036
+ import { dirname as dirname31, resolve as resolve15 } from "node:path";
32037
+ import { spawnSync as spawnSync20 } from "node:child_process";
32038
+ var RESTART_TIMEOUT_S2 = 30;
32039
+ function docker2(args) {
32040
+ const result = spawnSync20("docker", args, {
31899
32041
  encoding: "utf-8",
31900
32042
  stdio: ["ignore", "pipe", "pipe"]
31901
32043
  });
31902
32044
  return {
31903
- status: r.status,
31904
- stdout: r.stdout ?? "",
31905
- stderr: r.stderr ?? ""
31906
- };
31907
- };
31908
- var defaultFetch = (input2, init) => fetch(input2, init);
31909
- async function probeDockerDaemon(dockerExec = defaultDockerExec2) {
31910
- const r = dockerExec("docker", ["info", "--format", "{{.ServerVersion}}"]);
31911
- if (r.status === 0 && r.stdout.trim().length > 0) {
31912
- return { ok: true, message: `docker ${r.stdout.trim()} reachable` };
31913
- }
31914
- return {
31915
- ok: false,
31916
- message: "docker daemon not reachable",
31917
- remedy: "Start Docker Desktop (or your docker daemon) and re-run."
32045
+ ok: result.status === 0 && result.error === void 0,
32046
+ stderr: result.stderr ?? ""
31918
32047
  };
31919
32048
  }
31920
- async function probeImagePresence(refs, dockerExec = defaultDockerExec2) {
31921
- for (const ref of refs) {
31922
- const r = dockerExec("docker", ["image", "inspect", "--format", "{{.Id}}", ref]);
31923
- if (r.status !== 0) {
31924
- return {
31925
- ok: false,
31926
- message: `image ${ref} not present locally`,
31927
- remedy: "Run `olam bootstrap` to pull the published image, or build locally."
31928
- };
32049
+ function resolveRepoRoot2(start) {
32050
+ let cur = start;
32051
+ while (true) {
32052
+ if (existsSync51(resolve15(cur, "packages")) && existsSync51(resolve15(cur, "package.json"))) {
32053
+ return cur;
31929
32054
  }
32055
+ const parent = dirname31(cur);
32056
+ if (parent === cur) return start;
32057
+ cur = parent;
31930
32058
  }
31931
- return { ok: true, message: `${refs.length} image(s) present` };
31932
32059
  }
31933
- async function probeHostCpHealth(fetchImpl = defaultFetch) {
31934
- const controller = new AbortController();
31935
- const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS2);
31936
- try {
31937
- const res = await fetchImpl("http://127.0.0.1:19000/health", { signal: controller.signal });
31938
- const body = await res.text().catch(() => "");
31939
- if (res.status !== 200) {
31940
- return {
31941
- ok: false,
31942
- message: `host-cp /health returned ${res.status}`,
31943
- remedy: "Restart host-cp via `olam host-cp start`; verify port 19000 is bound to 127.0.0.1."
31944
- };
32060
+ function registerRestart(program2) {
32061
+ program2.command("restart").description("Restart a world container (auto-builds agent-stream bundle when stale)").argument("<world>", "World ID or name").action(async (worldIdOrName) => {
32062
+ const { ctx, error } = await loadContext();
32063
+ if (!ctx) {
32064
+ printError(error?.message ?? "Olam is not configured. Run `olam init` first.");
32065
+ process.exitCode = 1;
32066
+ return;
31945
32067
  }
31946
- if (isHostCpHealthEnvelope(body)) {
31947
- return { ok: true, message: "host-cp /health returned 200" };
32068
+ const world = ctx.worldManager.getWorld(worldIdOrName);
32069
+ if (!world) {
32070
+ printError(`World "${worldIdOrName}" not found. Run \`olam list\` to see worlds.`);
32071
+ process.exitCode = 1;
32072
+ return;
31948
32073
  }
31949
- return {
31950
- ok: false,
31951
- message: "host-cp /health returned 200 but body did not match the host-cp envelope (possible port collision on :19000)",
31952
- remedy: "Check `lsof -iTCP:19000`; another process may have bound the port. Restart host-cp via `olam host-cp start`."
31953
- };
31954
- } catch (err) {
31955
- const e = err;
31956
- const reason = e.name === "AbortError" ? `timeout (${HEALTH_TIMEOUT_MS2}ms)` : e.message;
31957
- return {
31958
- ok: false,
31959
- message: `host-cp /health unreachable: ${reason}`,
31960
- remedy: "Restart host-cp via `olam host-cp start`; verify port 19000 is bound to 127.0.0.1."
31961
- };
31962
- } finally {
31963
- clearTimeout(timeout);
31964
- }
31965
- }
31966
- function isHostCpHealthEnvelope(body) {
31967
- let parsed;
31968
- try {
31969
- parsed = JSON.parse(body);
31970
- } catch {
31971
- return false;
31972
- }
31973
- if (!parsed || typeof parsed !== "object") return false;
31974
- const obj = parsed;
31975
- if (typeof obj.phase !== "string") return false;
31976
- if (!obj.cache || typeof obj.cache !== "object") return false;
31977
- const mode = obj.mode;
31978
- if (!mode || typeof mode !== "object") return false;
31979
- if (typeof mode.deployment !== "string") return false;
31980
- return true;
31981
- }
31982
- async function probeAuthVault(olamHomeOverride) {
31983
- const vaultPath = resolveAccountsPath(olamHomeOverride);
31984
- if (!existsSync52(vaultPath)) {
31985
- return {
31986
- ok: false,
31987
- message: `auth vault missing at ${vaultPath}`,
31988
- remedy: "Run `olam auth up && olam auth login` to provision the vault."
31989
- };
31990
- }
31991
- let parsed;
31992
- try {
31993
- parsed = JSON.parse(readFileSync38(vaultPath, "utf-8"));
31994
- } catch (err) {
31995
- return {
31996
- ok: false,
31997
- message: `auth vault malformed: ${err.message}`,
31998
- remedy: "Inspect the accounts.json file; restore from backup or re-run `olam auth up`."
31999
- };
32000
- }
32001
- if (!Array.isArray(parsed)) {
32002
- return {
32003
- ok: false,
32004
- message: "auth vault malformed: expected JSON array at top level",
32005
- remedy: "Restore the vault from backup or re-run `olam auth up && olam auth login`."
32006
- };
32007
- }
32008
- const accounts = parsed.filter((a) => a !== null && typeof a === "object");
32009
- const now = Date.now();
32010
- const activeClaude = accounts.filter((a) => a.provider === "claude" && effectiveState2(a, now) === "active");
32011
- if (activeClaude.length === 0) {
32012
- return {
32013
- ok: false,
32014
- message: `no active claude credentials (vault has ${accounts.length} account(s))`,
32015
- remedy: "Run `olam auth login` to add a credential, or wait for the next reset."
32016
- };
32017
- }
32018
- return { ok: true, message: `${activeClaude.length} active claude credential(s)` };
32019
- }
32020
- function resolveAccountsPath(olamHomeOverride) {
32021
- const explicit = process.env.OLAM_AUTH_DATA_PATH;
32022
- if (explicit) return explicit;
32023
- const olamHome5 = olamHomeOverride ?? process.env.OLAM_HOME ?? path49.join(homedir25(), ".olam");
32024
- return path49.join(olamHome5, "auth-data", "accounts.json");
32025
- }
32026
- function effectiveState2(account, now) {
32027
- const persisted = account.state ?? (account.rateLimited ? "cooldown" : "active");
32028
- if (persisted === "disabled") return "disabled";
32029
- if (typeof account.expiresAt === "number" && account.expiresAt <= now) return "expired";
32030
- if (persisted === "cooldown") {
32031
- const reset = account.rateLimitResetsAt ? new Date(account.rateLimitResetsAt).getTime() : 0;
32032
- if (reset > 0 && reset <= now) return "active";
32033
- }
32034
- return persisted;
32035
- }
32036
- async function probeKgStorage(olamHomeOverride) {
32037
- const olamHome5 = olamHomeOverride ?? process.env.OLAM_HOME ?? path49.join(homedir25(), ".olam");
32038
- const kgRoot3 = path49.join(olamHome5, "kg");
32039
- const worldsRoot3 = path49.join(olamHome5, "worlds");
32040
- const kgBytes = enumerateGraphifyOut(kgRoot3, "pristine");
32041
- const overlayBytes = enumerateGraphifyOut(worldsRoot3, "overlay");
32042
- const totalBytes = kgBytes + overlayBytes;
32043
- if (totalBytes >= HARD_CAP_BYTES) {
32044
- return {
32045
- ok: false,
32046
- message: `KG storage ${formatBytes4(totalBytes)} exceeds 5 GB hard cap`,
32047
- remedy: "Prune stale pristines: `rm -rf ~/.olam/kg/<workspace>` after operator review."
32048
- };
32049
- }
32050
- if (totalBytes >= SOFT_CAP_BYTES) {
32051
- return {
32052
- ok: true,
32053
- message: `KG storage ${formatBytes4(totalBytes)} (soft-warn \u2014 exceeds 1.5 GB; consider pruning)`
32054
- };
32055
- }
32056
- return { ok: true, message: `KG storage ${formatBytes4(totalBytes)} (under cap)` };
32057
- }
32058
- function enumerateGraphifyOut(root, layout) {
32059
- if (!existsSync52(root)) return 0;
32060
- let total = 0;
32061
- let entries;
32062
- try {
32063
- entries = readdirSync13(root, { withFileTypes: true });
32064
- } catch {
32065
- return 0;
32066
- }
32067
- for (const entry of entries) {
32068
- if (!entry.isDirectory()) continue;
32069
- if (layout === "pristine") {
32070
- total += dirSizeBytes(path49.join(root, entry.name, "graphify-out"));
32071
- } else {
32072
- const worldDir = path49.join(root, entry.name);
32073
- let clones;
32074
- try {
32075
- clones = readdirSync13(worldDir, { withFileTypes: true });
32076
- } catch {
32077
- continue;
32074
+ printHeader(`olam restart: ${world.name}`);
32075
+ try {
32076
+ const bundleSource = resolveBundleSource();
32077
+ if (bundleSource.mode === "install") {
32078
+ process.stderr.write("bundle source: install-mode; skipping rebuild\n");
32079
+ } else {
32080
+ const repoRoot = resolveRepoRoot2(process.cwd());
32081
+ const buildResult = buildIfStale(repoRoot);
32082
+ if (!buildResult.ok) {
32083
+ printError(
32084
+ "Agent-stream bundle build failed. Not restarting with a stale bundle.\nFix the build error and re-run `olam restart`."
32085
+ );
32086
+ process.exitCode = 1;
32087
+ return;
32088
+ }
32089
+ printInfo("Bundle", buildResult.message);
32078
32090
  }
32079
- for (const clone of clones) {
32080
- if (!clone.isDirectory()) continue;
32081
- total += dirSizeBytes(path49.join(worldDir, clone.name, "graphify-out"));
32091
+ } catch (err) {
32092
+ if (err instanceof BundleSourceNotFoundError) {
32093
+ process.stderr.write(
32094
+ `Warning: agent-stream bundle path unresolved; container uses image-baked dist.
32095
+ `
32096
+ );
32097
+ } else {
32098
+ throw err;
32082
32099
  }
32083
32100
  }
32084
- }
32085
- return total;
32086
- }
32087
- function dirSizeBytes(dir) {
32088
- if (!existsSync52(dir)) return 0;
32089
- let total = 0;
32090
- const stack = [dir];
32091
- while (stack.length > 0) {
32092
- const cur = stack.pop();
32093
- let entries;
32094
- try {
32095
- entries = readdirSync13(cur, { withFileTypes: true });
32096
- } catch {
32097
- continue;
32101
+ const containerName = `olam-${world.id}-devbox`;
32102
+ printInfo("Container", containerName);
32103
+ const result = docker2(["restart", "--time", String(RESTART_TIMEOUT_S2), containerName]);
32104
+ if (!result.ok) {
32105
+ printError(
32106
+ `docker restart failed: ${result.stderr.trim() || "(no stderr)"}`
32107
+ );
32108
+ process.exitCode = 1;
32109
+ return;
32098
32110
  }
32099
- for (const entry of entries) {
32100
- const full = path49.join(cur, entry.name);
32101
- if (entry.isSymbolicLink()) continue;
32102
- if (entry.isDirectory()) {
32103
- stack.push(full);
32104
- continue;
32105
- }
32106
- try {
32107
- total += statSync15(full).size;
32108
- } catch {
32111
+ printInfo("Status", "restarted");
32112
+ });
32113
+ }
32114
+
32115
+ // src/commands/diagnose.ts
32116
+ import * as fs46 from "node:fs";
32117
+ import * as os25 from "node:os";
32118
+ import * as path49 from "node:path";
32119
+ import { execFileSync as execFileSync11, execSync as execSync12 } from "node:child_process";
32120
+ import pc24 from "picocolors";
32121
+
32122
+ // ../core/dist/diagnose/secret-stripper.js
32123
+ var SECRET_PATTERNS = [
32124
+ { name: "anthropic-key", re: /sk-ant-[A-Za-z0-9\-_]{8,}/g },
32125
+ { name: "github-pat", re: /gh[pourstu]_[A-Za-z0-9]{20,}/g },
32126
+ { name: "github-pat-fgp", re: /github_pat_[A-Za-z0-9_]{22,}/g },
32127
+ { name: "aws-access-key", re: /AKIA[A-Z0-9]{16}/g },
32128
+ { name: "bearer-header", re: /Bearer\s+[A-Za-z0-9._\-+/=]{16,}/g },
32129
+ { name: "slack-token", re: /xox[bpsa]-[A-Za-z0-9-]{10,}/g },
32130
+ { name: "host-cp-token", re: /(?:host[-_.]?cp[-_.]?token|"token")\s*[:=]\s*"?([A-Za-z0-9._\-]{16,})"?/gi },
32131
+ { name: "generic-hex-key", re: /\bkey\s*[:=]\s*[0-9a-f]{32,}/gi },
32132
+ { name: "db-url", re: /(?:postgres|mysql|redis|mongodb):\/\/[^:]+:[^@]+@[^\s'"]+/gi },
32133
+ { name: "env-secret", re: /(?:API_KEY|SECRET_KEY|AUTH_TOKEN|ACCESS_TOKEN|PRIVATE_KEY|DATABASE_URL|REDIS_URL)\s*[=:]\s*\S+/gi },
32134
+ { name: "pem-key", re: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC )?PRIVATE KEY-----/g }
32135
+ ];
32136
+ var REDACTED = "[REDACTED]";
32137
+ function stripSecrets(input2) {
32138
+ let out = input2;
32139
+ for (const { re } of SECRET_PATTERNS) {
32140
+ re.lastIndex = 0;
32141
+ out = out.replace(re, (match2) => {
32142
+ const colonOrEq = match2.indexOf(":") !== -1 ? match2.indexOf(":") : match2.indexOf("=");
32143
+ if (colonOrEq !== -1 && (match2.includes("token") || match2.includes("key") || match2.includes("KEY"))) {
32144
+ return match2.slice(0, colonOrEq + 1) + " " + REDACTED;
32109
32145
  }
32110
- }
32146
+ return REDACTED;
32147
+ });
32111
32148
  }
32112
- return total;
32149
+ return out;
32113
32150
  }
32114
- function formatBytes4(n) {
32115
- if (n < 1024) return `${n} B`;
32116
- if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
32117
- if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
32118
- return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
32151
+
32152
+ // src/commands/diagnose.ts
32153
+ var DIAGNOSTICS_DIR = path49.join(os25.homedir(), ".olam", "diagnostics");
32154
+ var LOG_DIR = path49.join(os25.homedir(), ".olam", "log");
32155
+ var CACHE_DIR = path49.join(os25.homedir(), ".olam", "cache");
32156
+ var LOG_TAIL_LINES = 200;
32157
+ function safeExec(cmd) {
32158
+ try {
32159
+ return execSync12(cmd, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
32160
+ } catch {
32161
+ return "";
32162
+ }
32119
32163
  }
32120
- async function probeEngine(fetchImpl = defaultFetch) {
32121
- const controller = new AbortController();
32122
- const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS2);
32164
+ function defaultZip(zipPath, files) {
32165
+ execFileSync11("zip", ["-j", zipPath, ...files]);
32166
+ }
32167
+ async function buildDiagnosticsZip(_exec = (cmd) => safeExec(cmd), _outDir = DIAGNOSTICS_DIR, _logDir = LOG_DIR, _zip = defaultZip) {
32168
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 23);
32169
+ const zipPath = path49.join(_outDir, `olam-diag-${ts}.zip`);
32170
+ const tmpDir = fs46.mkdtempSync(path49.join(os25.tmpdir(), "olam-diag-"));
32123
32171
  try {
32124
- const res = await fetchImpl("http://127.0.0.1:19000/health", { signal: controller.signal });
32125
- await res.text().catch(() => "");
32126
- if (res.status !== 200) {
32127
- return {
32128
- ok: false,
32129
- message: `host-cp /health returned ${res.status}; engine unknown`,
32130
- remedy: "Restart host-cp via `olam host-cp start`; verify port 19000 is bound to 127.0.0.1."
32131
- };
32172
+ fs46.mkdirSync(_outDir, { recursive: true });
32173
+ const entries = [];
32174
+ const version = process.env["OLAM_CLI_VERSION"] ?? "unknown";
32175
+ const nodeVersion = process.version;
32176
+ const platform = `${process.platform}/${process.arch}`;
32177
+ const versionContent = `olam: ${version}
32178
+ node: ${nodeVersion}
32179
+ platform: ${platform}
32180
+ `;
32181
+ _writeEntry(tmpDir, "version.txt", stripSecrets(versionContent), entries);
32182
+ const osContent = [
32183
+ `os: ${os25.type()} ${os25.release()}`,
32184
+ `arch: ${os25.arch()}`,
32185
+ `uptime: ${os25.uptime()}s`
32186
+ ].join("\n") + "\n";
32187
+ _writeEntry(tmpDir, "os-info.txt", stripSecrets(osContent), entries);
32188
+ const depsFile = path49.join(CACHE_DIR, "deps.json");
32189
+ if (fs46.existsSync(depsFile)) {
32190
+ const deps = fs46.readFileSync(depsFile, "utf-8");
32191
+ _writeEntry(tmpDir, "deps.json", stripSecrets(deps), entries);
32132
32192
  }
32133
- const engineHeader = res.headers.get("x-olam-engine") ?? res.headers.get("X-Olam-Engine");
32134
- if (!engineHeader) {
32135
- return {
32136
- ok: true,
32137
- message: "engine unknown (X-Olam-Engine header absent \u2014 older host-cp?)"
32138
- };
32193
+ const latestLog = _latestLog(_logDir);
32194
+ if (latestLog) {
32195
+ const lines = fs46.readFileSync(latestLog, "utf-8").split("\n");
32196
+ const tail = lines.slice(-LOG_TAIL_LINES).join("\n");
32197
+ _writeEntry(tmpDir, "log-tail.txt", stripSecrets(tail), entries);
32139
32198
  }
32140
- return {
32141
- ok: true,
32142
- message: `engine ${engineHeader}`,
32143
- engineName: engineHeader
32144
- };
32145
- } catch (err) {
32146
- const e = err;
32147
- const reason = e.name === "AbortError" ? `timeout (${HEALTH_TIMEOUT_MS2}ms)` : e.message;
32148
- return {
32149
- ok: false,
32150
- message: `engine probe unreachable: ${reason}`,
32151
- remedy: "Restart host-cp via `olam host-cp start`; verify port 19000 is bound to 127.0.0.1."
32152
- };
32199
+ const statusOut = _exec("olam status --json");
32200
+ if (statusOut) {
32201
+ _writeEntry(tmpDir, "status.json", stripSecrets(statusOut), entries);
32202
+ }
32203
+ const authAudit = _exec("npm run audit:auth-callers --if-present 2>&1");
32204
+ if (authAudit) {
32205
+ _writeEntry(tmpDir, "audit-auth-callers.txt", stripSecrets(authAudit), entries);
32206
+ }
32207
+ const fileArgs = entries.map((e) => path49.join(tmpDir, e));
32208
+ try {
32209
+ _zip(zipPath, fileArgs);
32210
+ } catch (err) {
32211
+ throw new Error(`zip command produced no output file. zip stderr: ${err.message}`);
32212
+ }
32213
+ if (!fs46.existsSync(zipPath)) {
32214
+ throw new Error("zip command produced no output file.");
32215
+ }
32216
+ return { zipPath, entries };
32153
32217
  } finally {
32154
- clearTimeout(timeout);
32155
- }
32156
- }
32157
- async function probeColimaVersion(dockerExec = defaultDockerExec2) {
32158
- const r = dockerExec("colima", ["version"]);
32159
- if (r.status === null || r.status !== 0 && /not found|ENOENT|not.found/i.test(r.stderr)) {
32160
- return { ok: true, message: "colima not installed (not in use)" };
32161
- }
32162
- if (r.status !== 0) {
32163
- return {
32164
- ok: true,
32165
- message: `colima present but version probe failed: ${(r.stderr || "").trim()}`
32166
- };
32167
- }
32168
- const tokens = r.stdout.split(/\s+/);
32169
- const version = tokens.find((t) => /^v?\d+\.\d+\.\d+/.test(t)) ?? "";
32170
- if (!version) {
32171
- return {
32172
- ok: true,
32173
- message: `colima present but version unrecognised: ${r.stdout.trim().slice(0, 80)}`
32174
- };
32175
- }
32176
- if (COLIMA_010_RANGE.test(version)) {
32177
- return {
32178
- ok: true,
32179
- warn: true,
32180
- message: `colima ${version} detected \u2014 known kubernetes-mode regression`,
32181
- remedy: COLIMA_010_WARN_TEXT
32182
- };
32183
- }
32184
- return { ok: true, message: `colima ${version} (clear)` };
32185
- }
32186
- async function probeK8sApiReachable(exec = defaultDockerExec2) {
32187
- const r = exec("kubectl", ["version", "--output=json", "--request-timeout=2s"]);
32188
- if (r.status === 0 && r.stdout.length > 0) {
32189
- let parsed;
32190
32218
  try {
32191
- parsed = JSON.parse(r.stdout);
32219
+ fs46.rmSync(tmpDir, { recursive: true, force: true });
32192
32220
  } catch {
32193
- parsed = null;
32194
32221
  }
32195
- const obj = parsed && typeof parsed === "object" ? parsed : {};
32196
- const serverInfo = obj.serverVersion;
32197
- const gitVersion = typeof serverInfo?.gitVersion === "string" ? serverInfo.gitVersion : "unknown";
32198
- return { ok: true, message: `api server ${gitVersion} reachable` };
32222
+ }
32223
+ }
32224
+ function _writeEntry(dir, name, content, entries) {
32225
+ fs46.writeFileSync(path49.join(dir, name), content, { mode: 420 });
32226
+ entries.push(name);
32227
+ }
32228
+ function _latestLog(logDir) {
32229
+ if (!fs46.existsSync(logDir)) return null;
32230
+ const files = fs46.readdirSync(logDir).filter((f) => f.startsWith("host-")).sort().reverse();
32231
+ return files.length > 0 ? path49.join(logDir, files[0]) : null;
32232
+ }
32233
+ async function buildTelemetryPayload() {
32234
+ const channel = "stable";
32235
+ const manifestFile = path49.join(CACHE_DIR, "manifest.json");
32236
+ let manifestAgeHours = null;
32237
+ if (fs46.existsSync(manifestFile)) {
32238
+ const mtime = fs46.statSync(manifestFile).mtime.getTime();
32239
+ manifestAgeHours = Math.round((Date.now() - mtime) / 36e5);
32199
32240
  }
32200
32241
  return {
32201
- ok: false,
32202
- message: "api server not reachable",
32203
- remedy: "Confirm the cluster is up (e.g. `k3d cluster list`) and your context points at it."
32242
+ version: process.env["OLAM_CLI_VERSION"] ?? "unknown",
32243
+ os: process.platform,
32244
+ arch: process.arch,
32245
+ channel,
32246
+ manifest_age_hours: manifestAgeHours
32247
+ // No userid, no project, no path — privacy per design doc
32204
32248
  };
32205
32249
  }
32206
- async function probeK8sImagePresence(refs, exec = defaultDockerExec2) {
32207
- const r = exec("kubectl", ["get", "nodes", "-o", "json", "--request-timeout=2s"]);
32208
- if (r.status !== 0) {
32209
- return {
32210
- ok: false,
32211
- message: "unable to list cluster nodes for image presence check",
32212
- remedy: "Confirm the cluster is up and your context points at it; re-run `olam doctor`."
32213
- };
32214
- }
32215
- let parsed;
32216
- try {
32217
- parsed = JSON.parse(r.stdout);
32218
- } catch {
32219
- return {
32220
- ok: false,
32221
- message: "cluster node list returned malformed output",
32222
- remedy: "Restart your cluster; if persistent, re-create it."
32223
- };
32224
- }
32225
- const nodes = parsed && typeof parsed === "object" ? parsed.items : void 0;
32226
- if (!Array.isArray(nodes) || nodes.length === 0) {
32227
- return {
32228
- ok: false,
32229
- message: "cluster has no nodes",
32230
- remedy: "Start the cluster and try again."
32231
- };
32232
- }
32233
- const presentImages = /* @__PURE__ */ new Set();
32234
- for (const node of nodes) {
32235
- const status2 = node.status;
32236
- const images = status2?.images;
32237
- if (!Array.isArray(images)) continue;
32238
- for (const img of images) {
32239
- const names = img.names;
32240
- if (!Array.isArray(names)) continue;
32241
- for (const n of names) presentImages.add(n);
32250
+ function registerDiagnose(program2) {
32251
+ program2.command("diagnose").description("Bundle diagnostics into a zip file for sharing with maintainers").option("--show-telemetry", "Print the telemetry payload that would be sent (no endpoint yet)").option("--no-telemetry", "Suppress telemetry payload (future opt-out)").option("--upload", "Share zip with maintainers (endpoint not yet provisioned)").option("--quiet", "Suppress progress output").action(async (opts) => {
32252
+ if (opts.showTelemetry) {
32253
+ const payload = await buildTelemetryPayload();
32254
+ console.log(JSON.stringify(payload, null, 2));
32255
+ return;
32242
32256
  }
32243
- }
32244
- for (const ref of refs) {
32245
- if (!hasImageRef(presentImages, ref)) {
32246
- return {
32247
- ok: false,
32248
- message: `image ${ref} not present on any cluster node`,
32249
- remedy: "Run `olam bootstrap` to import the published image into the cluster."
32250
- };
32257
+ if (!opts.quiet) {
32258
+ console.log(pc24.cyan("Building diagnostics zip\u2026"));
32251
32259
  }
32252
- }
32253
- return { ok: true, message: `${refs.length} image(s) present on cluster` };
32254
- }
32255
- function hasImageRef(present, ref) {
32256
- if (present.has(ref)) return true;
32257
- const variants = [`docker.io/library/${ref}`, `docker.io/${ref}`];
32258
- return variants.some((v) => present.has(v));
32260
+ try {
32261
+ const { zipPath, entries } = await buildDiagnosticsZip();
32262
+ if (!opts.quiet) {
32263
+ console.log(pc24.green(`Done: ${zipPath}`));
32264
+ console.log(pc24.dim(`Contents: ${entries.join(", ")}`));
32265
+ }
32266
+ if (opts.upload) {
32267
+ console.log(pc24.yellow(
32268
+ `Telemetry endpoint not yet provisioned. Share the zip manually:
32269
+ File: ${zipPath}
32270
+ See docs/runbooks/share-diagnostics.md for instructions.`
32271
+ ));
32272
+ }
32273
+ } catch (err) {
32274
+ console.error(pc24.red(`Diagnose failed: ${err.message}`));
32275
+ process.exitCode = 1;
32276
+ }
32277
+ });
32259
32278
  }
32260
32279
 
32261
32280
  // src/lib/bundle-freshness.ts
@@ -32497,7 +32516,8 @@ async function runDoctor(opts, deps = {}) {
32497
32516
  const nodeMemResult = probeNodeMemory(dockerExec, k8sCtx);
32498
32517
  rows.push({ name: "node memory", result: nodeMemResult, position: 27 });
32499
32518
  const socketCheckImpl = deps.dockerSocketCheckImpl;
32500
- const socketResult = await probeDockerSocketBindMount(k8sCtx, wrapImpl, socketCheckImpl);
32519
+ const detectClusterTypeImpl = deps.detectK8sClusterTypeImpl;
32520
+ const socketResult = await probeDockerSocketBindMount(k8sCtx, wrapImpl, socketCheckImpl, detectClusterTypeImpl);
32501
32521
  rows.push({ name: "docker socket bind-mount", result: socketResult, position: 28 });
32502
32522
  } else {
32503
32523
  const bundleResult = await probeBundleFreshness({
@@ -32731,19 +32751,26 @@ async function defaultDockerSocketCheck(kubectlContext, wrapImpl) {
32731
32751
  );
32732
32752
  return result.ok;
32733
32753
  }
32734
- async function probeDockerSocketBindMount(kubectlContext, wrapImpl, checkImpl) {
32754
+ async function probeDockerSocketBindMount(kubectlContext, wrapImpl, checkImpl, detectClusterTypeImpl) {
32735
32755
  const check = checkImpl ?? defaultDockerSocketCheck;
32736
32756
  const accessible = await check(kubectlContext, wrapImpl);
32737
32757
  if (accessible) {
32738
32758
  return { ok: true, message: "docker socket accessible at /var/run/docker.sock" };
32739
32759
  }
32760
+ const clusterType = (detectClusterTypeImpl ?? defaultDetectK8sClusterType)(kubectlContext);
32740
32761
  return {
32741
32762
  ok: true,
32742
32763
  warn: true,
32743
32764
  message: "docker socket not accessible at /var/run/docker.sock",
32744
- remedy: "Recreate the k3d cluster with the docker socket bind-mount:\n k3d cluster create olam-host \\\n --volume /var/run/docker.sock:/var/run/docker.sock@server:* \\\n --volume ~/.config/gh:/host/.config/gh \\\n --volume <olam-repo>:/host/olam \\\n --wait --timeout 90s\nThen run: olam upgrade"
32765
+ remedy: buildDockerSocketRemedy(clusterType)
32745
32766
  };
32746
32767
  }
32768
+ async function runK8sPreflight(opts, deps = {}) {
32769
+ const dockerExec = deps.dockerExec;
32770
+ const result = await probeK8sApiReachable(dockerExec);
32771
+ const rows = [{ name: "api server", result, position: 1 }];
32772
+ return emit(makeReport(rows), opts);
32773
+ }
32747
32774
  function makeReport(rows) {
32748
32775
  const failureCount = rows.filter((r) => !r.result.ok).length;
32749
32776
  const warnCount = rows.filter((r) => isWarn(r.result)).length;
@@ -32968,6 +32995,7 @@ async function handleSet(target, deps = {}) {
32968
32995
  const write = deps.writeConfig ?? writeConfig;
32969
32996
  const emit2 = deps.emit ?? emitSubstrateSet;
32970
32997
  const runDoctorFn = deps.runDoctor ?? runDoctor;
32998
+ const k8sPreflightFn = deps.runK8sPreflightFn ?? runK8sPreflight;
32971
32999
  const countWorlds = deps.countWorlds ?? defaultCountWorlds;
32972
33000
  if (target !== "compose" && target !== "kubernetes") {
32973
33001
  stderr.write(
@@ -33011,7 +33039,11 @@ async function handleSet(target, deps = {}) {
33011
33039
  `);
33012
33040
  let doctorResult;
33013
33041
  try {
33014
- doctorResult = await runDoctorFn({ json: true }, { engine });
33042
+ if (engine === "kubernetes") {
33043
+ doctorResult = await k8sPreflightFn({ json: true });
33044
+ } else {
33045
+ doctorResult = await runDoctorFn({ json: true }, { engine });
33046
+ }
33015
33047
  } catch (err) {
33016
33048
  stderr.write(
33017
33049
  `${pc26.red("error:")} pre-flight crashed: ${err instanceof Error ? err.message : String(err)}
@@ -33507,6 +33539,11 @@ async function runProjectSweepPhase(opts, deps, sweepDeps = {}) {
33507
33539
  // gitUrl is required by schema; file:// is in the URL_PATTERN allow-list.
33508
33540
  gitUrl: `file://${projectsRoot}`,
33509
33541
  trustMethod: "none",
33542
+ // Extra passthrough fields (rootPath, matchedCount, etc.) are
33543
+ // accepted via TrustAuditEntrySchema's `.passthrough()` — the
33544
+ // inferred type carries an index signature for unknown keys, so
33545
+ // TS accepts these without a cast. Schema discriminated-union to
33546
+ // land in follow-up plan-hard pass; see Phase D CP3 finding ADV-D-03.
33510
33547
  rootPath: projectsRoot,
33511
33548
  matchedCount: matches2.length,
33512
33549
  builtCount,