@pleri/olam-cli 0.1.37 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/__tests__/upgrade.test.js +1 -1
  2. package/dist/__tests__/upgrade.test.js.map +1 -1
  3. package/dist/commands/__tests__/begin.test.d.ts +7 -0
  4. package/dist/commands/__tests__/begin.test.d.ts.map +1 -0
  5. package/dist/commands/__tests__/begin.test.js +72 -0
  6. package/dist/commands/__tests__/begin.test.js.map +1 -0
  7. package/dist/commands/__tests__/status.test.d.ts +8 -0
  8. package/dist/commands/__tests__/status.test.d.ts.map +1 -0
  9. package/dist/commands/__tests__/status.test.js +62 -0
  10. package/dist/commands/__tests__/status.test.js.map +1 -0
  11. package/dist/commands/__tests__/stop.test.d.ts +5 -0
  12. package/dist/commands/__tests__/stop.test.d.ts.map +1 -0
  13. package/dist/commands/__tests__/stop.test.js +30 -0
  14. package/dist/commands/__tests__/stop.test.js.map +1 -0
  15. package/dist/commands/__tests__/world-upgrade.test.d.ts +8 -0
  16. package/dist/commands/__tests__/world-upgrade.test.d.ts.map +1 -0
  17. package/dist/commands/__tests__/world-upgrade.test.js +73 -0
  18. package/dist/commands/__tests__/world-upgrade.test.js.map +1 -0
  19. package/dist/commands/auth-upgrade.d.ts.map +1 -1
  20. package/dist/commands/auth-upgrade.js +4 -2
  21. package/dist/commands/auth-upgrade.js.map +1 -1
  22. package/dist/commands/begin.d.ts +27 -0
  23. package/dist/commands/begin.d.ts.map +1 -0
  24. package/dist/commands/begin.js +45 -0
  25. package/dist/commands/begin.js.map +1 -0
  26. package/dist/commands/dispatch.d.ts.map +1 -1
  27. package/dist/commands/dispatch.js +5 -0
  28. package/dist/commands/dispatch.js.map +1 -1
  29. package/dist/commands/enter.d.ts.map +1 -1
  30. package/dist/commands/enter.js +6 -0
  31. package/dist/commands/enter.js.map +1 -1
  32. package/dist/commands/host-cp.d.ts +8 -0
  33. package/dist/commands/host-cp.d.ts.map +1 -1
  34. package/dist/commands/host-cp.js +9 -1
  35. package/dist/commands/host-cp.js.map +1 -1
  36. package/dist/commands/observe.d.ts.map +1 -1
  37. package/dist/commands/observe.js +5 -0
  38. package/dist/commands/observe.js.map +1 -1
  39. package/dist/commands/status.d.ts +33 -1
  40. package/dist/commands/status.d.ts.map +1 -1
  41. package/dist/commands/status.js +98 -4
  42. package/dist/commands/status.js.map +1 -1
  43. package/dist/commands/stop.d.ts +10 -0
  44. package/dist/commands/stop.d.ts.map +1 -0
  45. package/dist/commands/stop.js +17 -0
  46. package/dist/commands/stop.js.map +1 -0
  47. package/dist/commands/upgrade.d.ts.map +1 -1
  48. package/dist/commands/upgrade.js +27 -7
  49. package/dist/commands/upgrade.js.map +1 -1
  50. package/dist/commands/world-snapshot.d.ts.map +1 -1
  51. package/dist/commands/world-snapshot.js +2 -3
  52. package/dist/commands/world-snapshot.js.map +1 -1
  53. package/dist/commands/world-upgrade.d.ts +33 -0
  54. package/dist/commands/world-upgrade.d.ts.map +1 -0
  55. package/dist/commands/world-upgrade.js +82 -0
  56. package/dist/commands/world-upgrade.js.map +1 -0
  57. package/dist/commands/world.d.ts +12 -0
  58. package/dist/commands/world.d.ts.map +1 -0
  59. package/dist/commands/world.js +18 -0
  60. package/dist/commands/world.js.map +1 -0
  61. package/dist/image-digests.json +3 -3
  62. package/dist/index.js +2147 -1756
  63. package/dist/index.js.map +1 -1
  64. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9,6 +9,39 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/output.ts
13
+ import pc from "picocolors";
14
+ function printError(message) {
15
+ console.error(`${pc.red("error")} ${message}`);
16
+ }
17
+ function printSuccess(message) {
18
+ console.log(`${pc.green("ok")} ${message}`);
19
+ }
20
+ function printWarning(message) {
21
+ console.log(`${pc.yellow("warn")} ${message}`);
22
+ }
23
+ function printInfo(label, value) {
24
+ console.log(` ${pc.dim(label.padEnd(14))} ${value}`);
25
+ }
26
+ function printHeader(title) {
27
+ console.log(`
28
+ ${pc.bold(title)}`);
29
+ }
30
+ function formatAge(createdAt) {
31
+ const ms = Date.now() - new Date(createdAt).getTime();
32
+ const minutes = Math.floor(ms / 6e4);
33
+ if (minutes < 60) return `${minutes}m`;
34
+ const hours = Math.floor(minutes / 60);
35
+ if (hours < 24) return `${hours}h ${minutes % 60}m`;
36
+ const days = Math.floor(hours / 24);
37
+ return `${days}d ${hours % 24}h`;
38
+ }
39
+ var init_output = __esm({
40
+ "src/output.ts"() {
41
+ "use strict";
42
+ }
43
+ });
44
+
12
45
  // ../../node_modules/zod/v3/helpers/util.js
13
46
  var util, objectUtil, ZodParsedType, getParsedType;
14
47
  var init_util = __esm({
@@ -421,8 +454,8 @@ var init_parseUtil = __esm({
421
454
  init_errors();
422
455
  init_en();
423
456
  makeIssue = (params) => {
424
- const { data, path: path40, errorMaps, issueData } = params;
425
- const fullPath = [...path40, ...issueData.path || []];
457
+ const { data, path: path42, errorMaps, issueData } = params;
458
+ const fullPath = [...path42, ...issueData.path || []];
426
459
  const fullIssue = {
427
460
  ...issueData,
428
461
  path: fullPath
@@ -730,11 +763,11 @@ var init_types = __esm({
730
763
  init_parseUtil();
731
764
  init_util();
732
765
  ParseInputLazyPath = class {
733
- constructor(parent, value, path40, key) {
766
+ constructor(parent, value, path42, key) {
734
767
  this._cachedPath = [];
735
768
  this.parent = parent;
736
769
  this.data = value;
737
- this._path = path40;
770
+ this._path = path42;
738
771
  this._key = key;
739
772
  }
740
773
  get path() {
@@ -4221,7 +4254,7 @@ import YAML from "yaml";
4221
4254
  function bootstrapStepCmd(entry) {
4222
4255
  return typeof entry === "string" ? entry : entry.cmd;
4223
4256
  }
4224
- function refineForbiddenKeys(value, path40, ctx, rejectSource) {
4257
+ function refineForbiddenKeys(value, path42, ctx, rejectSource) {
4225
4258
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
4226
4259
  return;
4227
4260
  }
@@ -4229,12 +4262,12 @@ function refineForbiddenKeys(value, path40, ctx, rejectSource) {
4229
4262
  if (FORBIDDEN_KEYS.has(key)) {
4230
4263
  ctx.addIssue({
4231
4264
  code: external_exports.ZodIssueCode.custom,
4232
- path: [...path40, key],
4265
+ path: [...path42, key],
4233
4266
  message: `forbidden key "${key}" (prototype-pollution surface)`
4234
4267
  });
4235
4268
  continue;
4236
4269
  }
4237
- if (rejectSource && path40.length === 0 && key === "source") {
4270
+ if (rejectSource && path42.length === 0 && key === "source") {
4238
4271
  ctx.addIssue({
4239
4272
  code: external_exports.ZodIssueCode.custom,
4240
4273
  path: ["source"],
@@ -4244,30 +4277,30 @@ function refineForbiddenKeys(value, path40, ctx, rejectSource) {
4244
4277
  }
4245
4278
  refineForbiddenKeys(
4246
4279
  value[key],
4247
- [...path40, key],
4280
+ [...path42, key],
4248
4281
  ctx,
4249
4282
  false
4250
4283
  );
4251
4284
  }
4252
4285
  }
4253
- function rejectForbiddenKeys(value, path40, rejectSource) {
4286
+ function rejectForbiddenKeys(value, path42, rejectSource) {
4254
4287
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
4255
4288
  return;
4256
4289
  }
4257
4290
  for (const key of Object.keys(value)) {
4258
4291
  if (FORBIDDEN_KEYS.has(key)) {
4259
4292
  throw new Error(
4260
- `[manifest] ${path40}: forbidden key "${key}" (prototype-pollution surface)`
4293
+ `[manifest] ${path42}: forbidden key "${key}" (prototype-pollution surface)`
4261
4294
  );
4262
4295
  }
4263
4296
  if (rejectSource && key === "source") {
4264
4297
  throw new Error(
4265
- `[manifest] ${path40}: top-level "source" is loader-stamped \u2014 manifests must not author it`
4298
+ `[manifest] ${path42}: top-level "source" is loader-stamped \u2014 manifests must not author it`
4266
4299
  );
4267
4300
  }
4268
4301
  rejectForbiddenKeys(
4269
4302
  value[key],
4270
- `${path40}.${key}`,
4303
+ `${path42}.${key}`,
4271
4304
  false
4272
4305
  );
4273
4306
  }
@@ -5208,8 +5241,8 @@ var init_client = __esm({
5208
5241
  throw new Error(`failed to report rate-limit for ${accountId} (HTTP ${res.status})`);
5209
5242
  }
5210
5243
  }
5211
- async request(method, path40, body, attempt = 0) {
5212
- const url = `${this.baseUrl}${path40}`;
5244
+ async request(method, path42, body, attempt = 0) {
5245
+ const url = `${this.baseUrl}${path42}`;
5213
5246
  const controller = new AbortController();
5214
5247
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
5215
5248
  const headers = {};
@@ -5225,7 +5258,7 @@ var init_client = __esm({
5225
5258
  } catch (err) {
5226
5259
  if (attempt < RETRY_COUNT && isTransient(err)) {
5227
5260
  await sleep(RETRY_BACKOFF_MS * (attempt + 1));
5228
- return this.request(method, path40, body, attempt + 1);
5261
+ return this.request(method, path42, body, attempt + 1);
5229
5262
  }
5230
5263
  throw err;
5231
5264
  } finally {
@@ -6715,8 +6748,8 @@ var init_provider3 = __esm({
6715
6748
  // -----------------------------------------------------------------------
6716
6749
  // Internal fetch helper
6717
6750
  // -----------------------------------------------------------------------
6718
- async request(path40, method, body) {
6719
- const url = `${this.config.workerUrl}${path40}`;
6751
+ async request(path42, method, body) {
6752
+ const url = `${this.config.workerUrl}${path42}`;
6720
6753
  const bearer = await this.config.mintToken();
6721
6754
  const headers = {
6722
6755
  Authorization: `Bearer ${bearer}`
@@ -6904,6 +6937,52 @@ var init_docker_host = __esm({
6904
6937
  }
6905
6938
  });
6906
6939
 
6940
+ // ../core/src/util/open-url.ts
6941
+ import { spawnSync as spawnSync3 } from "node:child_process";
6942
+ function openUrl(url) {
6943
+ if (process.env.CI === "true") {
6944
+ return { opened: false, reason: "CI environment detected" };
6945
+ }
6946
+ if (!process.stdout.isTTY) {
6947
+ return { opened: false, reason: "non-interactive shell" };
6948
+ }
6949
+ if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
6950
+ return { opened: false, reason: "headless linux (no DISPLAY/WAYLAND_DISPLAY)" };
6951
+ }
6952
+ let cmd;
6953
+ let args;
6954
+ if (process.platform === "darwin") {
6955
+ cmd = "open";
6956
+ args = [url];
6957
+ } else if (process.platform === "win32") {
6958
+ cmd = "cmd";
6959
+ args = ["/c", "start", "", url];
6960
+ } else {
6961
+ cmd = "xdg-open";
6962
+ args = [url];
6963
+ }
6964
+ try {
6965
+ const result = spawnSync3(cmd, args, { stdio: "ignore", timeout: 5e3 });
6966
+ if (result.error) {
6967
+ return { opened: false, reason: `spawn ${cmd}: ${result.error.message}` };
6968
+ }
6969
+ if (result.status !== 0 && result.status !== null) {
6970
+ return { opened: false, reason: `${cmd} exited ${result.status}` };
6971
+ }
6972
+ return { opened: true };
6973
+ } catch (err) {
6974
+ return {
6975
+ opened: false,
6976
+ reason: err instanceof Error ? err.message : String(err)
6977
+ };
6978
+ }
6979
+ }
6980
+ var init_open_url = __esm({
6981
+ "../core/src/util/open-url.ts"() {
6982
+ "use strict";
6983
+ }
6984
+ });
6985
+
6907
6986
  // ../core/src/world/state.ts
6908
6987
  var VALID_TRANSITIONS, WorldStateMachine;
6909
6988
  var init_state = __esm({
@@ -7870,8 +7949,8 @@ import { execFileSync as execFileSync3 } from "node:child_process";
7870
7949
  import * as fs13 from "node:fs";
7871
7950
  import * as os9 from "node:os";
7872
7951
  import * as path14 from "node:path";
7873
- function expandHome(p, homedir20) {
7874
- return p.replace(/^~(?=$|\/|\\)/, homedir20());
7952
+ function expandHome(p, homedir21) {
7953
+ return p.replace(/^~(?=$|\/|\\)/, homedir21());
7875
7954
  }
7876
7955
  function sanitizeRepoFilename(name) {
7877
7956
  const sanitized = name.replace(/[^A-Za-z0-9._-]/g, "_");
@@ -7892,7 +7971,7 @@ ${stderr}`;
7892
7971
  }
7893
7972
  function snapshotBaselineDiff(repos, workspacePath, deps = {}) {
7894
7973
  const exec = deps.exec ?? ((cmd, args, opts) => execFileSync3(cmd, args, opts));
7895
- const homedir20 = deps.homedir ?? (() => os9.homedir());
7974
+ const homedir21 = deps.homedir ?? (() => os9.homedir());
7896
7975
  const baselineDir = path14.join(workspacePath, ".olam", "baseline");
7897
7976
  try {
7898
7977
  fs13.mkdirSync(baselineDir, { recursive: true });
@@ -7907,7 +7986,7 @@ function snapshotBaselineDiff(repos, workspacePath, deps = {}) {
7907
7986
  if (!repo.path) continue;
7908
7987
  const filename = `${sanitizeRepoFilename(repo.name)}.diff`;
7909
7988
  const outPath = path14.join(baselineDir, filename);
7910
- const repoPath = expandHome(repo.path, homedir20);
7989
+ const repoPath = expandHome(repo.path, homedir21);
7911
7990
  if (!fs13.existsSync(repoPath)) {
7912
7991
  writeBaselineFile(outPath, `# repo: ${repo.name}
7913
7992
  # (skipped: path ${repoPath} does not exist)
@@ -11861,1688 +11940,1769 @@ var init_context = __esm({
11861
11940
  }
11862
11941
  });
11863
11942
 
11864
- // src/install-root.ts
11865
- var install_root_exports = {};
11866
- __export(install_root_exports, {
11867
- MissingBuildScriptError: () => MissingBuildScriptError,
11868
- installRoot: () => installRoot,
11869
- isDevMode: () => isDevMode,
11870
- resolveBuildScript: () => resolveBuildScript
11943
+ // src/commands/host-cp.ts
11944
+ var host_cp_exports = {};
11945
+ __export(host_cp_exports, {
11946
+ HOST_CP_PORT: () => HOST_CP_PORT,
11947
+ authSecretPath: () => authSecretPath,
11948
+ buildComposeEnv: () => buildComposeEnv,
11949
+ callHostCpProxy: () => callHostCpProxy,
11950
+ callHostCpRegistry: () => callHostCpRegistry,
11951
+ captureGhToken: () => captureGhToken,
11952
+ findHostCpContainer: () => findHostCpContainer,
11953
+ gatherProbeFailureDiagnostics: () => gatherProbeFailureDiagnostics,
11954
+ openHostCpUrl: () => openUrl,
11955
+ probeHostCp: () => probeHostCp,
11956
+ r2CredentialsPath: () => r2CredentialsPath,
11957
+ readAuthSecret: () => readAuthSecret2,
11958
+ readPid: () => readPid,
11959
+ readR2Credentials: () => readR2Credentials,
11960
+ readToken: () => readToken,
11961
+ registerHostCp: () => registerHostCp,
11962
+ removePid: () => removePid,
11963
+ removeToken: () => removeToken,
11964
+ runCompose: () => runCompose,
11965
+ startHostCp: () => startHostCp,
11966
+ stopHostCp: () => stopHostCp,
11967
+ writePid: () => writePid,
11968
+ writeToken: () => writeToken
11871
11969
  });
11872
- import { existsSync as existsSync18 } from "node:fs";
11873
- import { dirname as dirname13, join as join24, resolve as resolve6 } from "node:path";
11874
- import { fileURLToPath as fileURLToPath3 } from "node:url";
11875
- function installRoot(metaUrl = import.meta.url) {
11876
- const here = fileURLToPath3(metaUrl);
11877
- return resolve6(dirname13(here), "..");
11970
+ import * as crypto5 from "node:crypto";
11971
+ import * as fs19 from "node:fs";
11972
+ import * as os12 from "node:os";
11973
+ import * as path22 from "node:path";
11974
+ import { spawnSync as spawnSync4 } from "node:child_process";
11975
+ import Dockerode2 from "dockerode";
11976
+ function findComposeFile() {
11977
+ const candidates = [
11978
+ // Bundled path: dist/index.js lives at <pkg>/dist/; host-cp/ is a sibling of dist/
11979
+ path22.resolve(path22.dirname(new URL(import.meta.url).pathname), "../host-cp/compose.yaml"),
11980
+ // Source-mode: cwd is monorepo root
11981
+ path22.resolve(process.cwd(), "packages/host-cp/compose.yaml"),
11982
+ // Source-mode: cwd is one level inside the monorepo
11983
+ path22.resolve(process.cwd(), "../packages/host-cp/compose.yaml")
11984
+ ];
11985
+ for (const c of candidates) {
11986
+ if (fs19.existsSync(c)) return c;
11987
+ }
11988
+ return path22.resolve(process.cwd(), "packages/host-cp/compose.yaml");
11878
11989
  }
11879
- function isDevMode(env = process.env, installRootDir = installRoot()) {
11880
- if (env.OLAM_DEV !== "1") return false;
11881
- const repoRoot = resolve6(installRootDir, "..", "..");
11882
- return existsSync18(join24(repoRoot, "packages")) && existsSync18(join24(repoRoot, "package.json"));
11990
+ function olamHome() {
11991
+ return process.env.OLAM_HOME ?? path22.join(os12.homedir(), ".olam");
11883
11992
  }
11884
- function resolveBuildScript(input) {
11885
- const { scriptRelPath, env = process.env, installRootDir = installRoot() } = input;
11886
- if (!isDevMode(env, installRootDir)) {
11887
- throw new MissingBuildScriptError(scriptRelPath);
11888
- }
11889
- const repoRoot = resolve6(installRootDir, "..", "..");
11890
- return join24(repoRoot, scriptRelPath);
11993
+ function tokenPath() {
11994
+ return path22.join(olamHome(), "host-cp.token");
11891
11995
  }
11892
- var MissingBuildScriptError;
11893
- var init_install_root = __esm({
11894
- "src/install-root.ts"() {
11895
- "use strict";
11896
- MissingBuildScriptError = class extends Error {
11897
- constructor(scriptRelPath) {
11898
- super(
11899
- `Build script ${scriptRelPath} is not available in this CLI install.
11900
- Source-build paths require a monorepo clone:
11901
- git clone https://github.com/pleri/olam && cd olam
11902
- OLAM_DEV=1 olam <command> --from-source
11903
- For published-image upgrades (Phase B+), drop the --from-source flag
11904
- and the CLI will pull pre-built images from ghcr.io/pleri/* by digest.`
11905
- );
11906
- this.name = "MissingBuildScriptError";
11907
- }
11908
- };
11909
- }
11910
- });
11911
-
11912
- // src/protocol-version.ts
11913
- var protocol_version_exports = {};
11914
- __export(protocol_version_exports, {
11915
- OLAM_PROTOCOL_VERSIONS_SUPPORTED: () => OLAM_PROTOCOL_VERSIONS_SUPPORTED,
11916
- checkProtocolOverlap: () => checkProtocolOverlap,
11917
- inspectImageProtocolVersions: () => inspectImageProtocolVersions,
11918
- parseProtocolVersionsLabel: () => parseProtocolVersionsLabel
11919
- });
11920
- import { spawnSync as spawnSync5 } from "node:child_process";
11921
- function parseProtocolVersionsLabel(labelValue) {
11922
- if (!labelValue) return [];
11923
- const parts = labelValue.split(",").map((s) => s.trim()).filter(Boolean);
11924
- const versions = /* @__PURE__ */ new Set();
11925
- for (const part of parts) {
11926
- const n = Number.parseInt(part, 10);
11927
- if (Number.isFinite(n) && n > 0 && String(n) === part) {
11928
- versions.add(n);
11929
- }
11930
- }
11931
- return Array.from(versions).sort((a, b) => a - b);
11996
+ function pidPath() {
11997
+ return path22.join(olamHome(), "host-cp.pid");
11932
11998
  }
11933
- function checkProtocolOverlap(imageVersions, cliVersions = OLAM_PROTOCOL_VERSIONS_SUPPORTED) {
11934
- const cliSet = new Set(cliVersions);
11935
- const overlap = imageVersions.filter((v) => cliSet.has(v));
11936
- if (imageVersions.length === 0) {
11937
- return {
11938
- overlap: [],
11939
- imageVersions: [],
11940
- cliVersions,
11941
- compatible: false,
11942
- remedy: `Devbox image is missing the \`olam.protocol.versions\` LABEL. This CLI requires versions [${cliVersions.join(", ")}]. See docs/architecture/devbox-contract.md \xA71 for the contract; rebuild the image with \`LABEL olam.protocol.versions="1"\`.`
11943
- };
11944
- }
11945
- if (overlap.length === 0) {
11946
- const imgLow = Math.min(...imageVersions);
11947
- const imgHigh = Math.max(...imageVersions);
11948
- const cliLow = Math.min(...cliVersions);
11949
- const cliHigh = Math.max(...cliVersions);
11950
- return {
11951
- overlap: [],
11952
- imageVersions: [...imageVersions],
11953
- cliVersions,
11954
- compatible: false,
11955
- remedy: `Devbox image protocol versions [${imageVersions.join(", ")}] don't overlap CLI's [${cliVersions.join(", ")}]. ` + (imgHigh < cliLow ? `The image is older than this CLI supports \u2014 rebuild against the contract version ${cliLow}+ at docs/architecture/devbox-contract.md.` : imgLow > cliHigh ? `The image is newer than this CLI supports \u2014 pin a compatible CLI: \`npm install -g @pleri/olam-cli@<version-with-protocol-${imgLow}>\`.` : `Pin a compatible CLI version that overlaps with the image's range.`)
11956
- };
11957
- }
11958
- return {
11959
- overlap,
11960
- imageVersions: [...imageVersions],
11961
- cliVersions,
11962
- compatible: true,
11963
- remedy: ""
11964
- };
11999
+ function authSecretPath() {
12000
+ return path22.join(olamHome(), "auth-secret");
11965
12001
  }
11966
- function inspectImageProtocolVersions(imageRef, dockerInspect = realDockerInspect) {
11967
- const { exitCode, stdout, stderr } = dockerInspect(imageRef);
11968
- if (exitCode !== 0) {
12002
+ function readAuthSecret2() {
12003
+ const filePath = authSecretPath();
12004
+ if (!fs19.existsSync(filePath)) return null;
12005
+ const raw = fs19.readFileSync(filePath, "utf-8").trim();
12006
+ return raw.length > 0 ? raw : null;
12007
+ }
12008
+ function r2CredentialsPath() {
12009
+ return path22.join(olamHome(), "r2-credentials.json");
12010
+ }
12011
+ function readR2Credentials() {
12012
+ const filePath = r2CredentialsPath();
12013
+ if (!fs19.existsSync(filePath)) return null;
12014
+ const raw = fs19.readFileSync(filePath, "utf-8").trim();
12015
+ if (raw.length === 0) return null;
12016
+ try {
12017
+ const parsed = JSON.parse(raw);
12018
+ if (typeof parsed !== "object" || parsed === null) return null;
12019
+ const creds = parsed;
12020
+ if (typeof creds.account_id !== "string" || typeof creds.bucket !== "string" || typeof creds.access_key_id !== "string" || typeof creds.secret_access_key !== "string" || typeof creds.public_url_base !== "string") {
12021
+ return null;
12022
+ }
11969
12023
  return {
11970
- imageRef,
11971
- versions: [],
11972
- inspectFailed: true,
11973
- inspectError: stderr.trim() || `docker inspect exited ${exitCode}`
12024
+ account_id: creds.account_id,
12025
+ bucket: creds.bucket,
12026
+ access_key_id: creds.access_key_id,
12027
+ secret_access_key: creds.secret_access_key,
12028
+ public_url_base: creds.public_url_base
11974
12029
  };
12030
+ } catch {
12031
+ return null;
11975
12032
  }
11976
- const raw = stdout.trim();
11977
- const value = raw === "<no value>" ? "" : raw;
11978
- return {
11979
- imageRef,
11980
- versions: parseProtocolVersionsLabel(value),
11981
- inspectFailed: false,
11982
- inspectError: void 0
11983
- };
11984
12033
  }
11985
- var OLAM_PROTOCOL_VERSIONS_SUPPORTED, realDockerInspect;
11986
- var init_protocol_version = __esm({
11987
- "src/protocol-version.ts"() {
11988
- "use strict";
11989
- OLAM_PROTOCOL_VERSIONS_SUPPORTED = [1];
11990
- realDockerInspect = (imageRef) => {
11991
- const result = spawnSync5(
11992
- "docker",
11993
- ["inspect", imageRef, "--format", '{{ index .Config.Labels "olam.protocol.versions" }}'],
11994
- { encoding: "utf8", timeout: 1e4 }
11995
- );
11996
- return {
11997
- exitCode: result.status ?? -1,
11998
- stdout: result.stdout ?? "",
11999
- stderr: result.stderr ?? ""
12000
- };
12001
- };
12002
- }
12003
- });
12004
-
12005
- // src/registry-allowlist.ts
12006
- var registry_allowlist_exports = {};
12007
- __export(registry_allowlist_exports, {
12008
- decideAllowlist: () => decideAllowlist,
12009
- resolveDevboxImageOverride: () => resolveDevboxImageOverride
12010
- });
12011
- function decideAllowlist(input) {
12012
- const { imageRef, allowCustomRegistry } = input;
12013
- const allowedByDefault = DEFAULT_ALLOWLIST_PATTERNS.some((re) => re.test(imageRef));
12014
- if (allowedByDefault) {
12015
- return {
12016
- imageRef,
12017
- allowedByDefault: true,
12018
- accepted: true,
12019
- stderrLine: ""
12020
- };
12021
- }
12022
- if (allowCustomRegistry) {
12023
- return {
12024
- imageRef,
12025
- allowedByDefault: false,
12026
- accepted: true,
12027
- stderrLine: `Warning: using custom devbox image '${imageRef}'. (--allow-custom-registry was specified.) Verify the source and digest before proceeding.`
12028
- };
12034
+ function writeToken() {
12035
+ const token = crypto5.randomBytes(32).toString("hex");
12036
+ const filePath = tokenPath();
12037
+ fs19.mkdirSync(path22.dirname(filePath), { recursive: true });
12038
+ fs19.writeFileSync(filePath, token, { mode: 384 });
12039
+ return token;
12040
+ }
12041
+ function readToken() {
12042
+ const filePath = tokenPath();
12043
+ if (!fs19.existsSync(filePath)) return null;
12044
+ return fs19.readFileSync(filePath, "utf-8").trim();
12045
+ }
12046
+ function removeToken() {
12047
+ const filePath = tokenPath();
12048
+ if (!fs19.existsSync(filePath)) return false;
12049
+ fs19.unlinkSync(filePath);
12050
+ return true;
12051
+ }
12052
+ function writePid(pid) {
12053
+ const filePath = pidPath();
12054
+ fs19.mkdirSync(path22.dirname(filePath), { recursive: true });
12055
+ fs19.writeFileSync(filePath, String(pid), { mode: 420 });
12056
+ }
12057
+ function readPid() {
12058
+ const filePath = pidPath();
12059
+ if (!fs19.existsSync(filePath)) return null;
12060
+ const raw = fs19.readFileSync(filePath, "utf-8").trim();
12061
+ const n = parseInt(raw, 10);
12062
+ return Number.isFinite(n) ? n : null;
12063
+ }
12064
+ function removePid() {
12065
+ const filePath = pidPath();
12066
+ if (!fs19.existsSync(filePath)) return false;
12067
+ fs19.unlinkSync(filePath);
12068
+ return true;
12069
+ }
12070
+ async function findHostCpContainer() {
12071
+ const docker2 = new Dockerode2(resolveDockerHostOptions());
12072
+ const containers = await docker2.listContainers({ all: true });
12073
+ for (const c of containers) {
12074
+ const names = (c.Names ?? []).map((n) => n.replace(/^\//, ""));
12075
+ if (names.includes("olam-host-cp")) {
12076
+ return {
12077
+ id: c.Id.slice(0, 12),
12078
+ name: "olam-host-cp",
12079
+ state: c.State,
12080
+ status: c.Status
12081
+ };
12082
+ }
12029
12083
  }
12030
- return {
12031
- imageRef,
12032
- allowedByDefault: false,
12033
- accepted: false,
12034
- stderrLine: `Error: image '${imageRef}' is outside allowed registries (ghcr.io/pleri/*).
12035
- To override: re-run with --allow-custom-registry
12036
- Verify the source and digest before doing so.`
12037
- };
12084
+ return null;
12038
12085
  }
12039
- function resolveDevboxImageOverride(flagValue, env = process.env) {
12040
- if (flagValue && flagValue.trim().length > 0) {
12041
- return flagValue.trim();
12086
+ async function probeHostCp() {
12087
+ const candidateUrl = `http://127.0.0.1:${HOST_CP_PORT}`;
12088
+ let httpOk = false;
12089
+ try {
12090
+ const res = await fetch(`${candidateUrl}/api/bootstrap`, {
12091
+ signal: AbortSignal.timeout(2e3)
12092
+ });
12093
+ httpOk = res.ok;
12094
+ } catch {
12095
+ httpOk = false;
12042
12096
  }
12043
- const envValue = env.OLAM_DEVBOX_IMAGE;
12044
- if (envValue && envValue.trim().length > 0) {
12045
- return envValue.trim();
12097
+ if (httpOk) {
12098
+ let mode = "bare";
12099
+ try {
12100
+ const container = await findHostCpContainer();
12101
+ if (container && container.state === "running") {
12102
+ mode = "container";
12103
+ }
12104
+ } catch {
12105
+ }
12106
+ return { url: candidateUrl, mode };
12046
12107
  }
12047
- return void 0;
12108
+ try {
12109
+ const container = await findHostCpContainer();
12110
+ if (container && container.state === "running") {
12111
+ try {
12112
+ const res = await fetch(`${candidateUrl}/api/bootstrap`, {
12113
+ signal: AbortSignal.timeout(2e3)
12114
+ });
12115
+ if (res.ok) {
12116
+ return { url: candidateUrl, mode: "container" };
12117
+ }
12118
+ } catch {
12119
+ }
12120
+ }
12121
+ } catch {
12122
+ }
12123
+ return null;
12048
12124
  }
12049
- var DEFAULT_ALLOWLIST_PATTERNS;
12050
- var init_registry_allowlist = __esm({
12051
- "src/registry-allowlist.ts"() {
12052
- "use strict";
12053
- DEFAULT_ALLOWLIST_PATTERNS = [
12054
- // ghcr.io/pleri/<anything>:<tag> or ghcr.io/pleri/<anything>@sha256:<digest>
12055
- /^ghcr\.io\/pleri\/[^/\s]+(?::[^\s]+|@sha256:[a-f0-9]+)?$/
12056
- ];
12125
+ async function gatherProbeFailureDiagnostics() {
12126
+ let bootstrapStatus = "no response";
12127
+ try {
12128
+ const res = await fetch(`http://127.0.0.1:${HOST_CP_PORT}/api/bootstrap`, {
12129
+ signal: AbortSignal.timeout(2e3)
12130
+ });
12131
+ bootstrapStatus = `HTTP ${res.status}`;
12132
+ } catch (err) {
12133
+ bootstrapStatus = err instanceof Error ? err.message : "connection refused";
12057
12134
  }
12058
- });
12059
-
12060
- // ../core/src/orchestrator/enter.ts
12061
- var enter_exports = {};
12062
- __export(enter_exports, {
12063
- getEnterCommand: () => getEnterCommand
12064
- });
12065
- function getEnterCommand(worldId, containerId, provider, sshHost) {
12066
- if (provider === "docker") {
12067
- const command = `docker exec -it ${containerId} claude`;
12068
- return {
12069
- command,
12070
- instructions: [
12071
- "Run this command in your terminal to enter the world:",
12072
- "",
12073
- ` ${command}`,
12074
- "",
12075
- "Press Ctrl+C to exit."
12076
- ].join("\n")
12077
- };
12135
+ let containerStatus = "not found";
12136
+ try {
12137
+ const container = await findHostCpContainer();
12138
+ if (!container) {
12139
+ containerStatus = "not found";
12140
+ } else {
12141
+ containerStatus = `found (state: ${container.state})`;
12142
+ }
12143
+ } catch {
12144
+ containerStatus = "docker not available";
12078
12145
  }
12079
- if (provider === "ssh" && sshHost) {
12080
- const command = `ssh -t ${sshHost} docker exec -it ${containerId} claude`;
12081
- return {
12082
- command,
12083
- instructions: [
12084
- "Run this command to enter the remote world:",
12085
- "",
12086
- ` ${command}`,
12087
- "",
12088
- "Press Ctrl+C to exit."
12089
- ].join("\n")
12090
- };
12146
+ return { bootstrapStatus, containerStatus };
12147
+ }
12148
+ async function probeHealth() {
12149
+ try {
12150
+ const res = await fetch(`http://127.0.0.1:${HOST_CP_PORT}/health`, {
12151
+ signal: AbortSignal.timeout(2e3)
12152
+ });
12153
+ if (!res.ok) return null;
12154
+ return await res.json();
12155
+ } catch {
12156
+ return null;
12091
12157
  }
12158
+ }
12159
+ function runCompose(args, composeFile, extraEnv = {}) {
12160
+ const result = spawnSync4("docker", ["compose", "-f", composeFile, ...args], {
12161
+ encoding: "utf-8",
12162
+ stdio: ["ignore", "pipe", "pipe"],
12163
+ env: { ...process.env, ...extraEnv }
12164
+ });
12092
12165
  return {
12093
- command: "",
12094
- instructions: `Cannot determine enter command for provider: ${provider}`
12166
+ ok: result.status === 0,
12167
+ stdout: result.stdout ?? "",
12168
+ stderr: result.stderr ?? ""
12095
12169
  };
12096
12170
  }
12097
- var init_enter = __esm({
12098
- "../core/src/orchestrator/enter.ts"() {
12099
- "use strict";
12171
+ function buildComposeEnv(authSecret, ghToken) {
12172
+ const env = {};
12173
+ if (authSecret !== null && authSecret.length > 0) {
12174
+ env.OLAM_AUTH_SECRET = authSecret;
12100
12175
  }
12101
- });
12102
-
12103
- // ../core/src/crystallize/checksum.ts
12104
- var checksum_exports = {};
12105
- __export(checksum_exports, {
12106
- computeGraphChecksum: () => computeGraphChecksum
12107
- });
12108
- import { createHash as createHash2 } from "node:crypto";
12109
- function computeGraphChecksum(nodes, edges) {
12110
- const sortedNodes = [...nodes].sort((a, b) => a.id.localeCompare(b.id));
12111
- const sortedEdges = [...edges].sort((a, b) => a.id.localeCompare(b.id));
12112
- const payload = JSON.stringify({ nodes: sortedNodes, edges: sortedEdges });
12113
- return createHash2("sha256").update(payload).digest("hex");
12114
- }
12115
- var init_checksum = __esm({
12116
- "../core/src/crystallize/checksum.ts"() {
12117
- "use strict";
12176
+ if (ghToken != null && ghToken.length > 0) {
12177
+ env.GH_TOKEN = ghToken;
12118
12178
  }
12119
- });
12120
-
12121
- // ../core/src/config/machine-schema.ts
12122
- var machine_schema_exports = {};
12123
- __export(machine_schema_exports, {
12124
- MachineConfigSchema: () => MachineConfigSchema,
12125
- initMachineConfig: () => initMachineConfig,
12126
- readMachineConfig: () => readMachineConfig,
12127
- writeMachineConfig: () => writeMachineConfig
12128
- });
12129
- import * as fs32 from "node:fs";
12130
- import * as path36 from "node:path";
12131
- import * as os19 from "node:os";
12132
- import { parse as parseYaml4, stringify as stringifyYaml4 } from "yaml";
12133
- function readMachineConfig(configPath) {
12134
- const p = configPath ?? DEFAULT_CONFIG_PATH;
12135
- if (!fs32.existsSync(p)) return null;
12179
+ return env;
12180
+ }
12181
+ function captureGhToken() {
12136
12182
  try {
12137
- const raw = fs32.readFileSync(p, "utf-8");
12138
- const parsed = parseYaml4(raw);
12139
- return MachineConfigSchema.parse(parsed);
12183
+ const result = spawnSync4("gh", ["auth", "token"], {
12184
+ encoding: "utf-8",
12185
+ stdio: ["ignore", "pipe", "pipe"]
12186
+ });
12187
+ if (result.status === 0) {
12188
+ const token = (result.stdout ?? "").trim();
12189
+ return token.length > 0 ? token : null;
12190
+ }
12191
+ return null;
12140
12192
  } catch {
12141
12193
  return null;
12142
12194
  }
12143
12195
  }
12144
- function writeMachineConfig(config, configPath) {
12145
- const p = configPath ?? DEFAULT_CONFIG_PATH;
12146
- fs32.mkdirSync(path36.dirname(p), { recursive: true });
12147
- fs32.writeFileSync(p, stringifyYaml4({ ...config }), { mode: 420 });
12196
+ async function startHostCp(opts) {
12197
+ return handleStart(opts);
12148
12198
  }
12149
- function initMachineConfig(opts = {}) {
12150
- const configPath = opts.configPath ?? DEFAULT_CONFIG_PATH;
12151
- const existing = readMachineConfig(configPath);
12152
- if (existing) return existing;
12153
- const config = MachineConfigSchema.parse({
12154
- telemetry: opts.telemetry ?? true
12155
- });
12156
- writeMachineConfig(config, configPath);
12157
- return config;
12158
- }
12159
- var MachineConfigSchema, DEFAULT_CONFIG_PATH;
12160
- var init_machine_schema = __esm({
12161
- "../core/src/config/machine-schema.ts"() {
12162
- "use strict";
12163
- init_zod();
12164
- MachineConfigSchema = external_exports.object({
12165
- schema_version: external_exports.literal(1).default(1),
12166
- channel: external_exports.enum(["stable", "beta", "edge"]).default("stable"),
12167
- auto_update: external_exports.boolean().default(true),
12168
- telemetry: external_exports.boolean().default(true),
12169
- worlds_dir: external_exports.string().default(() => path36.join(os19.homedir(), ".olam", "worlds"))
12170
- });
12171
- DEFAULT_CONFIG_PATH = path36.join(os19.homedir(), ".olam", "config.yaml");
12172
- }
12173
- });
12174
-
12175
- // src/index.ts
12176
- import { Command } from "commander";
12177
- import * as fs35 from "node:fs";
12178
- import * as path39 from "node:path";
12179
- import { fileURLToPath as fileURLToPath4 } from "node:url";
12180
-
12181
- // src/commands/init.ts
12182
- import * as fs5 from "node:fs";
12183
- import * as path5 from "node:path";
12184
- import { execSync } from "node:child_process";
12185
- import pc3 from "picocolors";
12186
-
12187
- // src/output.ts
12188
- import pc from "picocolors";
12189
- function printError(message) {
12190
- console.error(`${pc.red("error")} ${message}`);
12191
- }
12192
- function printSuccess(message) {
12193
- console.log(`${pc.green("ok")} ${message}`);
12194
- }
12195
- function printWarning(message) {
12196
- console.log(`${pc.yellow("warn")} ${message}`);
12197
- }
12198
- function printInfo(label, value) {
12199
- console.log(` ${pc.dim(label.padEnd(14))} ${value}`);
12200
- }
12201
- function printHeader(title) {
12202
- console.log(`
12203
- ${pc.bold(title)}`);
12204
- }
12205
- function formatAge(createdAt) {
12206
- const ms = Date.now() - new Date(createdAt).getTime();
12207
- const minutes = Math.floor(ms / 6e4);
12208
- if (minutes < 60) return `${minutes}m`;
12209
- const hours = Math.floor(minutes / 60);
12210
- if (hours < 24) return `${hours}h ${minutes % 60}m`;
12211
- const days = Math.floor(hours / 24);
12212
- return `${days}d ${hours % 24}h`;
12213
- }
12214
-
12215
- // src/commands/workspace.ts
12216
- init_workspace();
12217
- init_loader();
12218
- import * as fs4 from "node:fs";
12219
- import * as path4 from "node:path";
12220
- import pc2 from "picocolors";
12221
- import { stringify as stringifyYaml2 } from "yaml";
12222
- function printWorkspaceNotFound(name) {
12223
- printError(`No workspace named "${name}" under ${workspacesDir()}`);
12224
- }
12225
- function parseRepoFlag(raw) {
12226
- const hashIdx = raw.indexOf("#");
12227
- const url = hashIdx === -1 ? raw : raw.slice(0, hashIdx);
12228
- const branch = hashIdx === -1 ? void 0 : raw.slice(hashIdx + 1);
12229
- if (url.length === 0) throw new Error(`invalid --repo value "${raw}" (empty url)`);
12230
- if (branch !== void 0 && branch.length === 0) {
12231
- throw new Error(`invalid --repo value "${raw}" (empty branch after #)`);
12232
- }
12233
- const nameFromUrl = url.replace(/\.git$/, "").split(/[\/:]/).filter(Boolean).at(-1) ?? "repo";
12234
- return branch ? { name: nameFromUrl, url, branch } : { name: nameFromUrl, url };
12235
- }
12236
- function registerWorkspace(program2) {
12237
- const workspace = program2.command("workspace").description("Manage the named catalog of repo bundles that worlds instantiate from");
12238
- workspace.command("list").description("List all workspaces (name, repoCount, updatedAt)").action(() => {
12239
- const all = listWorkspaces();
12240
- if (all.length === 0) {
12241
- console.log(pc2.dim(`No workspaces under ${workspacesDir()}`));
12199
+ async function handleStart(opts) {
12200
+ const existing = await findHostCpContainer();
12201
+ if (existing && existing.state === "running") {
12202
+ const health = await probeHealth();
12203
+ if (health) {
12204
+ printSuccess(`Host CP already running at http://127.0.0.1:${HOST_CP_PORT}`);
12205
+ printInfo("Container", existing.id);
12206
+ printInfo("Uptime", String(health["uptime_seconds"] ?? "unknown") + "s");
12242
12207
  return;
12243
12208
  }
12244
- printHeader(`${all.length} workspace(s)`);
12245
- for (const ws of all) {
12246
- const when = new Date(ws.updatedAt).toISOString().slice(0, 10);
12247
- console.log(
12248
- ` ${pc2.bold(ws.name.padEnd(24))} ${String(ws.repos.length).padEnd(3)} repos ${pc2.dim(when)}`
12249
- );
12250
- }
12251
- });
12252
- workspace.command("show").description("Show a workspace as YAML").argument("<name>", "Workspace name").action((name) => {
12253
- try {
12254
- const ws = readWorkspace(name);
12255
- if (!ws) {
12256
- printWorkspaceNotFound(name);
12257
- process.exitCode = 1;
12258
- return;
12259
- }
12260
- process.stdout.write(stringifyYaml2(ws));
12261
- } catch (err) {
12262
- printError(err instanceof Error ? err.message : String(err));
12263
- process.exitCode = 1;
12264
- }
12265
- });
12266
- workspace.command("remove").description("Delete a workspace (does NOT touch worlds that already referenced it)").argument("<name>", "Workspace name").option("--force", "Skip confirmation", false).action((name, _opts) => {
12267
- try {
12268
- if (removeWorkspace(name)) {
12269
- printSuccess(`Removed workspace "${name}"`);
12270
- } else {
12271
- printWorkspaceNotFound(name);
12272
- process.exitCode = 1;
12273
- }
12274
- } catch (err) {
12275
- printError(err instanceof Error ? err.message : String(err));
12276
- process.exitCode = 1;
12277
- }
12278
- });
12279
- workspace.command("add").description("Create a workspace from --from-config (reads current .olam/config.yaml) or repeated --repo flags").argument("<name>", "Workspace name").option("--from-config", "Seed from the repos in the current project's .olam/config.yaml", false).option("--repo <url>", "Repeatable. Format: <url> or <url>#<branch>", (val, acc) => {
12280
- acc.push(parseRepoFlag(val));
12281
- return acc;
12282
- }, []).option("--default-branch <branch>", "Fallback branch for repos that don't specify one").option("--force", "Overwrite an existing workspace with the same name", false).action((name, opts) => {
12283
- try {
12284
- const repos = [...opts.repo];
12285
- if (opts.fromConfig) {
12286
- try {
12287
- const cfg = loadConfig(process.cwd());
12288
- for (const r of cfg.repos) {
12289
- repos.push({
12290
- name: r.name,
12291
- url: r.url,
12292
- ...r.submodules ? { submodules: true } : {}
12293
- });
12294
- }
12295
- } catch (err) {
12296
- printError(`--from-config: ${err instanceof Error ? err.message : String(err)}`);
12297
- process.exitCode = 1;
12298
- return;
12299
- }
12300
- }
12301
- if (repos.length === 0) {
12302
- printError("No repos provided \u2014 pass --from-config or at least one --repo");
12303
- process.exitCode = 1;
12304
- return;
12305
- }
12306
- const ws = {
12307
- name,
12308
- repos,
12309
- ...opts.defaultBranch ? { defaults: { branch: opts.defaultBranch } } : {},
12310
- updatedAt: Date.now()
12311
- };
12312
- writeWorkspace(ws, { force: opts.force });
12313
- const file = path4.join(workspacesDir(), `${name}.yaml`);
12314
- printSuccess(`Created workspace "${name}" (${repos.length} repo${repos.length === 1 ? "" : "s"})`);
12315
- printInfo("File", file);
12316
- printInfo("Next", `olam create --name <world> --workspace ${name} --task "..."`);
12317
- } catch (err) {
12318
- if (err instanceof WorkspaceExistsError || err instanceof WorkspaceNameError) {
12319
- printError(err.message);
12320
- process.exitCode = 1;
12321
- return;
12322
- }
12323
- printError(err instanceof Error ? err.message : String(err));
12324
- process.exitCode = 1;
12325
- }
12326
- });
12327
- }
12328
- function ensureProjectWorkspaceFromConfig(projectRoot, workspaceName) {
12329
- const existing = (() => {
12330
- try {
12331
- return readWorkspace(workspaceName);
12332
- } catch {
12333
- return null;
12334
- }
12335
- })();
12336
- if (existing) {
12337
- return { created: false, file: path4.join(workspacesDir(), `${workspaceName}.yaml`) };
12209
+ printWarning("Host CP container running but /health not responding. Wait a few seconds and retry, or stop+start.");
12210
+ return;
12338
12211
  }
12339
- const cfg = loadConfig(projectRoot);
12340
- const repos = cfg.repos.map((r) => ({
12341
- name: r.name,
12342
- url: r.url,
12343
- ...r.submodules ? { submodules: true } : {}
12344
- }));
12345
- const ws = {
12346
- name: workspaceName,
12347
- repos,
12348
- updatedAt: Date.now()
12349
- };
12350
- writeWorkspace(ws);
12351
- const file = path4.join(workspacesDir(), `${workspaceName}.yaml`);
12352
- return { created: true, file };
12353
- }
12354
-
12355
- // src/commands/init.ts
12356
- function detectProjectType(root) {
12357
- if (fs5.existsSync(path5.join(root, "Gemfile")) || fs5.existsSync(path5.join(root, "config", "routes.rb"))) return "rails";
12358
- if (fs5.existsSync(path5.join(root, "package.json"))) return "node";
12359
- if (fs5.existsSync(path5.join(root, "pyproject.toml")) || fs5.existsSync(path5.join(root, "requirements.txt"))) return "python";
12360
- return "generic";
12361
- }
12362
- function getRepoName(root) {
12363
12212
  try {
12364
- const url = execSync("git remote get-url origin", {
12365
- cwd: root,
12366
- encoding: "utf-8"
12367
- }).trim();
12368
- const match2 = url.match(/\/([^/]+?)(?:\.git)?$/);
12369
- if (match2) return match2[1];
12370
- } catch {
12371
- }
12372
- return path5.basename(root);
12373
- }
12374
- function findProjectRoot(startDir) {
12375
- let current = path5.resolve(startDir);
12376
- while (true) {
12377
- if (fs5.existsSync(path5.join(current, ".git"))) return current;
12378
- const parent = path5.dirname(current);
12379
- if (parent === current) return startDir;
12380
- current = parent;
12381
- }
12382
- }
12383
- function registerInit(program2) {
12384
- program2.command("init").description("Initialize olam in the current project").option("--path <path>", "Project root path", process.cwd()).option("--skip-pleri", "Skip Pleri setup").action(async (opts) => {
12385
- try {
12386
- const projectRoot = findProjectRoot(opts.path);
12387
- const olamDir = path5.join(projectRoot, ".olam");
12388
- if (fs5.existsSync(path5.join(olamDir, "config.yaml"))) {
12389
- printError(`Already initialized at ${olamDir}/config.yaml`);
12390
- process.exitCode = 1;
12391
- return;
12392
- }
12393
- const projectType = detectProjectType(projectRoot);
12394
- const repoName = getRepoName(projectRoot);
12395
- let remoteUrl;
12396
- try {
12397
- remoteUrl = execSync("git remote get-url origin", {
12398
- cwd: projectRoot,
12399
- encoding: "utf-8"
12400
- }).trim();
12401
- } catch {
12402
- remoteUrl = `git@github.com:your-org/${repoName}.git`;
12403
- }
12404
- fs5.mkdirSync(path5.join(olamDir, "state"), { recursive: true });
12405
- fs5.mkdirSync(path5.join(olamDir, "thoughts"), { recursive: true });
12406
- const pleriSection = opts.skipPleri ? "# pleri:\n# base_url: ${PLERI_BASE_URL}\n# plane_id: ${PLERI_PLANE_ID}\n# api_key: ${PLERI_API_KEY}\n" : "pleri:\n base_url: ${PLERI_BASE_URL}\n plane_id: ${PLERI_PLANE_ID}\n api_key: ${PLERI_API_KEY}\n";
12407
- const config = [
12408
- "version: 2",
12409
- "",
12410
- pleriSection,
12411
- "repos:",
12412
- ` - name: ${repoName}`,
12413
- ` url: ${remoteUrl}`,
12414
- ` path: ${projectRoot}`,
12415
- ` type: ${projectType}`,
12416
- " services: []",
12417
- "",
12418
- "compute:",
12419
- " default: docker",
12420
- "",
12421
- "cost:",
12422
- " # All values in USD (Anthropic's billing currency).",
12423
- " # Convert from your local currency: USD 25 \u2248 SGD 33 / EUR 23 / GBP 20",
12424
- " # at typical 2026 rates. Dashboard display localization is a future feature.",
12425
- " max_per_world_usd: 25",
12426
- " max_daily_usd: 100",
12427
- " warning_threshold: 0.8",
12428
- "",
12429
- "auth:",
12430
- " mode: oauth",
12431
- ""
12432
- ].join("\n");
12433
- fs5.writeFileSync(path5.join(olamDir, "config.yaml"), config);
12434
- const envExample = [
12435
- "# Pleri credentials",
12436
- "PLERI_BASE_URL=https://pleri.dev/api",
12437
- "PLERI_PLANE_ID=",
12438
- "PLERI_API_KEY=",
12439
- ""
12440
- ].join("\n");
12441
- fs5.writeFileSync(path5.join(olamDir, ".env.example"), envExample);
12442
- printHeader("Olam initialized");
12443
- printInfo("Config", `${olamDir}/config.yaml`);
12444
- printInfo("Project", `${projectType} (detected)`);
12445
- printInfo("Repo", repoName);
12446
- try {
12447
- const result = ensureProjectWorkspaceFromConfig(projectRoot, repoName);
12448
- if (result.created) {
12449
- printInfo("Workspace", `${repoName} \u2192 ${result.file}`);
12450
- } else {
12451
- printInfo("Workspace", `${repoName} (already registered)`);
12452
- }
12453
- } catch (err) {
12454
- printError(`Workspace auto-register failed: ${err instanceof Error ? err.message : String(err)}`);
12455
- }
12456
- console.log(`
12457
- ${pc3.dim(`Next: olam create --name my-world --workspace ${repoName} --task "..."`)}`);
12458
- } catch (err) {
12459
- printError(err instanceof Error ? err.message : String(err));
12213
+ const docker2 = new Dockerode2(resolveDockerHostOptions());
12214
+ await auditPortsForZombies(docker2, [HOST_CP_PORT]);
12215
+ } catch (err) {
12216
+ if (err instanceof PortHeldByZombieError) {
12217
+ printError(`Port ${HOST_CP_PORT} held by zombie container "${err.containerName}" (state: ${err.state}).`);
12218
+ printError(`Run: docker rm ${err.containerName}`);
12460
12219
  process.exitCode = 1;
12220
+ return;
12461
12221
  }
12462
- });
12463
- }
12464
-
12465
- // src/commands/install.ts
12466
- import * as fs6 from "node:fs";
12467
- import * as os3 from "node:os";
12468
- import * as path6 from "node:path";
12469
- import pc4 from "picocolors";
12470
- import { stringify as stringifyYaml3 } from "yaml";
12471
-
12472
- // ../core/src/archetypes/capabilities.ts
12473
- var CAPABILITY_NAMES = [
12474
- // World operations
12475
- "world.create",
12476
- "world.operate",
12477
- "world.destroy",
12478
- // Workspace catalog
12479
- "workspace.read",
12480
- "workspace.write",
12481
- // Auth container + credentials. Phase 6b.2/A5: `auth.manage`
12482
- // removed — its only consumer (/auth/revoke) was reclassified to
12483
- // `auth.login` (self-managed auth flow) in A2, and the only
12484
- // archetype that held it (`bootstrapper`) is also removed below.
12485
- "auth.login",
12486
- "auth.read",
12487
- // PR gate
12488
- "pr-gate.read",
12489
- "pr-gate.decide",
12490
- // Policy
12491
- "policy.set",
12492
- // Phase 6b.2/A5: `role.manage` removed — Pylon owns role grants
12493
- // (`pylon role grant ...` from a user terminal). The `/api/roles*`
12494
- // route handlers were deleted in A2.
12495
- // Phase 6b.2/A5: `bootstrap.install` and `bootstrap.cf-deploy`
12496
- // removed — `/bootstrap` route handler deleted in A2; the
12497
- // `bootstrapper` archetype is also removed in this commit.
12498
- // Dev-only (repo clone privileges)
12499
- "dev.build",
12500
- "dev.test",
12501
- "dev.release"
12502
- ];
12503
- var CAPABILITY_SET = new Set(CAPABILITY_NAMES);
12504
- function isCapability(value) {
12505
- return CAPABILITY_SET.has(value);
12506
- }
12507
- function assertCapability(value) {
12508
- if (!isCapability(value)) {
12509
- throw new Error(
12510
- `Unknown capability "${value}". Valid capabilities: ${CAPABILITY_NAMES.join(", ")}`
12222
+ throw err;
12223
+ }
12224
+ const token = writeToken();
12225
+ const composeFile = findComposeFile();
12226
+ if (!fs19.existsSync(composeFile)) {
12227
+ printError(`compose.yaml not found at ${composeFile}. Run from the olam project root.`);
12228
+ removeToken();
12229
+ process.exitCode = 1;
12230
+ return;
12231
+ }
12232
+ const authSecret = readAuthSecret2();
12233
+ if (authSecret === null) {
12234
+ printWarning(
12235
+ `${authSecretPath()} not found or empty. host-cp will boot, but credential surfaces (auth fleet, hotswap) will fail with 401 until you run \`olam auth up\` to (re)generate the shared secret.`
12511
12236
  );
12512
12237
  }
12513
- return value;
12514
- }
12515
-
12516
- // ../core/src/archetypes/registry.ts
12517
- var ARCHETYPES = [
12518
- {
12519
- name: "user",
12520
- description: "Day-to-day world operator. Creates, runs, and tears down worlds; decides PR gates; reads the workspace catalog.",
12521
- capabilities: [
12522
- "world.create",
12523
- "world.operate",
12524
- "world.destroy",
12525
- "workspace.read",
12526
- "auth.login",
12527
- "auth.read",
12528
- "pr-gate.read",
12529
- "pr-gate.decide"
12530
- ]
12531
- },
12532
- {
12533
- name: "workspace-curator",
12534
- description: "User who also curates the workspace catalog. Typical: team lead scoping their squad's repo bundles.",
12535
- inherits: ["user"],
12536
- capabilities: ["workspace.write"]
12537
- },
12538
- {
12539
- name: "policy-admin",
12540
- description: "User who also sets deployment policy (permission mode, PR-gate defaults). No catalog or role authority.",
12541
- inherits: ["user"],
12542
- capabilities: ["policy.set"]
12543
- },
12544
- // Phase 6b.2/A5: `bootstrapper` archetype removed. Its capabilities
12545
- // (bootstrap.install, bootstrap.cf-deploy, auth.manage) are no
12546
- // longer enforced — `/bootstrap` route + `auth.manage` route
12547
- // gating are gone (deleted in A2). The trim keeps `admin` valid
12548
- // (no dangling inherits ref) and lets schema push succeed.
12549
- {
12550
- name: "admin",
12551
- description: "Full operational admin. Unions user + workspace-curator + policy-admin. Pylon owns role grants now (no role.manage).",
12552
- inherits: ["user", "workspace-curator", "policy-admin"],
12553
- capabilities: []
12554
- },
12555
- {
12556
- name: "dev",
12557
- description: "Contributor to Olam itself. Everything an admin has, plus repo-local dev operations (build, test, release).",
12558
- inherits: ["admin"],
12559
- capabilities: ["dev.build", "dev.test", "dev.release"]
12238
+ const ghToken = captureGhToken();
12239
+ if (ghToken === null) {
12240
+ printWarning(
12241
+ "GitHub CLI not authenticated; PR badges will not appear in the inbox. Run `gh auth login` then `olam host-cp restart`."
12242
+ );
12560
12243
  }
12561
- ];
12562
- var ARCHETYPE_BY_NAME = new Map(
12563
- ARCHETYPES.map((a) => [a.name, a])
12564
- );
12565
- function getArchetype(name) {
12566
- return ARCHETYPE_BY_NAME.get(name);
12244
+ const composeEnv = buildComposeEnv(authSecret, ghToken);
12245
+ const PULL_BACKOFF_MS = [0, 1e3, 3e3];
12246
+ let pullOk = false;
12247
+ let lastPullStderr = "";
12248
+ for (let attempt = 0; attempt < PULL_BACKOFF_MS.length; attempt += 1) {
12249
+ if (PULL_BACKOFF_MS[attempt] > 0) {
12250
+ await new Promise((r) => setTimeout(r, PULL_BACKOFF_MS[attempt]));
12251
+ }
12252
+ const pullResult = runCompose(
12253
+ ["pull", "--quiet", "docker-socket-proxy"],
12254
+ composeFile,
12255
+ composeEnv
12256
+ );
12257
+ if (pullResult.ok) {
12258
+ pullOk = true;
12259
+ break;
12260
+ }
12261
+ lastPullStderr = pullResult.stderr;
12262
+ }
12263
+ if (!pullOk) {
12264
+ printError("docker compose pull docker-socket-proxy failed after 3 attempts");
12265
+ process.stderr.write(lastPullStderr);
12266
+ removeToken();
12267
+ process.exitCode = 1;
12268
+ return;
12269
+ }
12270
+ const result = runCompose(["up", "-d"], composeFile, composeEnv);
12271
+ if (!result.ok) {
12272
+ printError("docker compose up failed");
12273
+ process.stderr.write(result.stderr);
12274
+ removeToken();
12275
+ process.exitCode = 1;
12276
+ return;
12277
+ }
12278
+ const deadline = Date.now() + 1e4;
12279
+ let healthy = false;
12280
+ while (Date.now() < deadline) {
12281
+ const h = await probeHealth();
12282
+ if (h) {
12283
+ healthy = true;
12284
+ break;
12285
+ }
12286
+ await new Promise((r) => setTimeout(r, 500));
12287
+ }
12288
+ if (!healthy) {
12289
+ printWarning("Host CP started but /health did not respond within 10s. Check `docker compose logs host-cp`.");
12290
+ }
12291
+ const container = await findHostCpContainer();
12292
+ if (container) {
12293
+ writePid(1);
12294
+ }
12295
+ printSuccess(`Host CP running at http://127.0.0.1:${HOST_CP_PORT}`);
12296
+ if (opts.showToken) {
12297
+ printInfo("Token", token);
12298
+ } else {
12299
+ printInfo("Token", `(written to ${tokenPath()}; pass --show-token to print)`);
12300
+ }
12301
+ printInfo("Open", `http://127.0.0.1:${HOST_CP_PORT}`);
12567
12302
  }
12568
- function listArchetypeNames() {
12569
- return ARCHETYPES.map((a) => a.name);
12303
+ async function stopHostCp() {
12304
+ return handleStop();
12570
12305
  }
12571
-
12572
- // ../core/src/archetypes/expand.ts
12573
- var UnknownArchetypeError = class extends Error {
12574
- constructor(name, known) {
12575
- super(
12576
- `Unknown archetype "${name}". Known archetypes: ${known.join(", ")}`
12577
- );
12578
- this.name = name;
12579
- this.known = known;
12580
- this.name = "UnknownArchetypeError";
12306
+ async function handleStop() {
12307
+ const composeFile = findComposeFile();
12308
+ if (!fs19.existsSync(composeFile)) {
12309
+ printWarning(`compose.yaml not found at ${composeFile}. Cleaning up token + PID anyway.`);
12310
+ removeToken();
12311
+ removePid();
12312
+ return;
12581
12313
  }
12582
- name;
12583
- known;
12584
- };
12585
- var ArchetypeCycleError = class extends Error {
12586
- constructor(path40) {
12587
- super(
12588
- `Archetype inheritance cycle detected: ${path40.join(" \u2192 ")} \u2192 ${path40[0] ?? "?"}`
12589
- );
12590
- this.path = path40;
12591
- this.name = "ArchetypeCycleError";
12314
+ const existing = await findHostCpContainer();
12315
+ if (!existing) {
12316
+ printInfo("Host CP", "not running");
12317
+ removeToken();
12318
+ removePid();
12319
+ return;
12592
12320
  }
12593
- path;
12594
- };
12595
- function expandArchetype(name) {
12596
- const out = /* @__PURE__ */ new Set();
12597
- walk(name, out, []);
12598
- return out;
12321
+ const result = runCompose(["down"], composeFile);
12322
+ if (!result.ok) {
12323
+ printError("docker compose down failed");
12324
+ process.stderr.write(result.stderr);
12325
+ process.exitCode = 1;
12326
+ return;
12327
+ }
12328
+ removeToken();
12329
+ removePid();
12330
+ printSuccess("Host CP stopped");
12599
12331
  }
12600
- function walk(name, acc, stack) {
12601
- if (stack.includes(name)) {
12602
- throw new ArchetypeCycleError([...stack, name]);
12332
+ async function buildStatusReport() {
12333
+ const container = await findHostCpContainer();
12334
+ const health = await probeHealth();
12335
+ const tokenFile = tokenPath();
12336
+ const tokenPresent = fs19.existsSync(tokenFile);
12337
+ let tokenModeOk = false;
12338
+ if (tokenPresent) {
12339
+ const mode = fs19.statSync(tokenFile).mode & 511;
12340
+ tokenModeOk = mode === 384;
12603
12341
  }
12604
- const arch2 = getArchetype(name);
12605
- if (!arch2) {
12606
- throw new UnknownArchetypeError(name, listArchetypeNames());
12342
+ const pidPresent = fs19.existsSync(pidPath());
12343
+ let stack;
12344
+ if (!container) {
12345
+ stack = "not_started";
12346
+ } else if (container.state === "running" && health) {
12347
+ stack = "running";
12348
+ } else if (container.state === "running") {
12349
+ stack = "partial";
12350
+ } else {
12351
+ stack = "stopped";
12607
12352
  }
12608
- const nextStack = [...stack, name];
12609
- for (const parent of arch2.inherits ?? []) {
12610
- walk(parent, acc, nextStack);
12353
+ return {
12354
+ stack,
12355
+ container,
12356
+ health,
12357
+ token_present: tokenPresent,
12358
+ token_mode_ok: tokenModeOk,
12359
+ pid_present: pidPresent,
12360
+ url: `http://127.0.0.1:${HOST_CP_PORT}`
12361
+ };
12362
+ }
12363
+ async function handleStatus(opts) {
12364
+ const report = await buildStatusReport();
12365
+ if (opts.json) {
12366
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
12367
+ process.exitCode = report.stack === "running" ? 0 : 1;
12368
+ return;
12611
12369
  }
12612
- for (const cap of arch2.capabilities) {
12613
- acc.add(cap);
12370
+ printHeader("Host CP Status");
12371
+ printInfo("Stack", report.stack);
12372
+ printInfo("URL", report.url);
12373
+ if (report.container) {
12374
+ printInfo("Container", `${report.container.id} (${report.container.state})`);
12375
+ printInfo("Status line", report.container.status);
12376
+ } else {
12377
+ printInfo("Container", "not found (run `olam host-cp start`)");
12378
+ }
12379
+ if (report.health) {
12380
+ printInfo("Health", "ok");
12381
+ printInfo("Uptime", String(report.health["uptime_seconds"] ?? "unknown") + "s");
12382
+ const cache = report.health["cache"];
12383
+ if (cache) {
12384
+ printInfo("Cached worlds", String(cache.worlds?.length ?? 0));
12385
+ printInfo("Cache TTL", `${cache.ttl_sec ?? "unknown"}s`);
12386
+ }
12387
+ const sse = report.health["sse"];
12388
+ if (sse) {
12389
+ printInfo("SSE active", `${sse.active ?? 0} / ${sse.cap ?? 0}`);
12390
+ }
12391
+ } else {
12392
+ printInfo("Health", "not responding");
12614
12393
  }
12394
+ printInfo("Token file", report.token_present ? report.token_mode_ok ? "present (mode 600)" : "present (BAD MODE \u2014 should be 600)" : "absent");
12395
+ printInfo("PID file", report.pid_present ? "present" : "absent");
12396
+ process.exitCode = report.stack === "running" ? 0 : 1;
12397
+ }
12398
+ function registerHostCp(program2) {
12399
+ const hostCp = program2.command("host-cp").description("Manage the Olam host control plane container");
12400
+ hostCp.command("start").description("Start the host CP container (token regenerated each call)").option("--show-token", "Print the generated token to stdout (default: hide)").action(async (opts) => {
12401
+ await handleStart({ showToken: opts.showToken === true });
12402
+ });
12403
+ hostCp.command("stop").description("Stop the host CP container + remove token + PID files").action(async () => {
12404
+ await handleStop();
12405
+ });
12406
+ hostCp.command("status").description("Show host CP container + health diagnostics").option("--json", "Output as JSON (machine-parseable; sets exit code)").action(async (opts) => {
12407
+ await handleStatus({ json: opts.json === true });
12408
+ });
12409
+ hostCp.command("register").description("Register a world with the running host CP so it appears in the unified UI").requiredOption("--world <id>", "World id (the docker container suffix, e.g. gold-arc-1454)").option("--port <port>", "Override per-world CP port; default: discovered from `olam list`").action(async (opts) => {
12410
+ await handleRegister({ world: opts.world, port: opts.port });
12411
+ });
12412
+ hostCp.command("deregister").description("Remove a world from the host CP registry (does NOT destroy the world)").requiredOption("--world <id>", "World id to remove").action(async (opts) => {
12413
+ await handleDeregister({ world: opts.world });
12414
+ });
12615
12415
  }
12616
-
12617
- // src/commands/install.ts
12618
- var ROLE_FILE_PATH = path6.join(os3.homedir(), ".olam", "role.yaml");
12619
- function readRoleFile() {
12620
- if (!fs6.existsSync(ROLE_FILE_PATH)) return null;
12416
+ async function discoverWorldPort(worldId) {
12621
12417
  try {
12622
- const raw = fs6.readFileSync(ROLE_FILE_PATH, "utf-8");
12623
- const m = /archetype:\s*(\S+)/.exec(raw);
12624
- if (!m) return null;
12625
- const archetype = m[1];
12626
- const customCapabilities = [];
12627
- for (const line of raw.split("\n")) {
12628
- const caps = /^\s+-\s+(\S+)$/.exec(line);
12629
- if (caps) customCapabilities.push(caps[1]);
12630
- }
12631
- const installedAtMatch = /installedAt:\s*(\d+)/.exec(raw);
12632
- return {
12633
- archetype,
12634
- customCapabilities,
12635
- installedAt: installedAtMatch ? Number(installedAtMatch[1]) : 0
12636
- };
12418
+ const { loadContext: loadContext2 } = await Promise.resolve().then(() => (init_context(), context_exports));
12419
+ const { ctx } = await loadContext2();
12420
+ if (!ctx) return null;
12421
+ const world = await ctx.worldManager.getWorld(worldId);
12422
+ if (!world) return null;
12423
+ return 19080 + world.portOffset;
12637
12424
  } catch {
12638
12425
  return null;
12639
12426
  }
12640
12427
  }
12641
- function writeRoleFile(role) {
12642
- fs6.mkdirSync(path6.dirname(ROLE_FILE_PATH), { recursive: true });
12643
- const yaml = stringifyYaml3({
12644
- archetype: role.archetype,
12645
- customCapabilities: [...role.customCapabilities],
12646
- installedAt: role.installedAt
12647
- });
12648
- fs6.writeFileSync(ROLE_FILE_PATH, yaml, { mode: 420 });
12649
- }
12650
- function nextStepsFor(archetype) {
12651
- switch (archetype) {
12652
- case "user":
12653
- return [
12654
- 'You can now run `olam create --task "..."` against a workspace your admin has curated.',
12655
- "Register the Claude Code plugin if you haven't: `claude plugin install ./plugin`."
12656
- ];
12657
- case "workspace-curator":
12658
- return [
12659
- "You can now curate the catalog: `olam workspace add <name> --from-config`.",
12660
- "Share workspace YAML files via your usual dotfiles / rsync flow."
12661
- ];
12662
- case "policy-admin":
12663
- return [
12664
- "Set deployment-wide policy via env vars (OLAM_CLAUDE_PERMISSION_MODE) or wrangler secrets.",
12665
- "PR-gate defaults can be overridden per-workspace under `policy.set`."
12666
- ];
12667
- case "bootstrapper":
12668
- return [
12669
- "Start the auth container: `olam auth up`, then `olam auth login`.",
12670
- "For Cloudflare deployments: `cd packages/cloudflare-worker && pnpm wrangler deploy`."
12671
- ];
12672
- case "admin":
12673
- return [
12674
- "Start the auth container: `olam auth up`, then `olam auth login`.",
12675
- "Curate workspaces: `olam workspace add <name> --from-config`.",
12676
- "Assign narrower archetypes to teammates (CF side, once slice C lands): use `role.manage`."
12677
- ];
12678
- case "dev":
12679
- return [
12680
- "You have repo-local privileges. Run `npm run build:ci && npm run test:ci`.",
12681
- "Everything an admin has is available."
12682
- ];
12683
- default:
12684
- return [];
12685
- }
12428
+ async function readHostCpToken2() {
12429
+ const tp = tokenPath();
12430
+ if (!fs19.existsSync(tp)) return null;
12431
+ return fs19.readFileSync(tp, "utf-8").trim();
12686
12432
  }
12687
- function registerInstall(program2) {
12688
- program2.command("install").description("Pick an archetype preset for this Olam install").option("--as <archetype>", "Archetype name (user, workspace-curator, policy-admin, bootstrapper, admin, dev)").option(
12689
- "--capability <name>",
12690
- "Additional capability to grant beyond the archetype preset (repeatable)",
12691
- (value, acc) => {
12692
- acc.push(value);
12693
- return acc;
12694
- },
12695
- []
12696
- ).option("--force", "Overwrite an existing role.yaml", false).option("--show", "Print the current install's archetype and exit", false).action((opts) => {
12697
- if (opts.show) {
12698
- const existing2 = readRoleFile();
12699
- if (!existing2) {
12700
- console.log(pc4.dim(`No install recorded at ${ROLE_FILE_PATH}`));
12701
- return;
12702
- }
12703
- printHeader("Current install");
12704
- printInfo("Archetype", existing2.archetype);
12705
- printInfo("File", ROLE_FILE_PATH);
12706
- const caps = expandArchetype(existing2.archetype);
12707
- for (const cap of existing2.customCapabilities) caps.add(cap);
12708
- printHeader(`Effective capabilities (${caps.size})`);
12709
- for (const cap of [...caps].sort()) {
12710
- console.log(` ${cap}`);
12433
+ async function callHostCpProxy(method, worldId, path42, body) {
12434
+ const token = await readHostCpToken2();
12435
+ if (!token) return { ok: false, status: 0, error: "no token (host CP not started)" };
12436
+ const url = `http://127.0.0.1:${HOST_CP_PORT}/api/world/${encodeURIComponent(worldId)}${path42}`;
12437
+ try {
12438
+ const headers = {
12439
+ Authorization: `Bearer ${token}`
12440
+ };
12441
+ if (body !== void 0) headers["Content-Type"] = "application/json";
12442
+ const res = await fetch(url, {
12443
+ method,
12444
+ headers,
12445
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
12446
+ });
12447
+ if (!res.ok) {
12448
+ const text = await res.text().catch(() => "");
12449
+ let errMsg = text || `HTTP ${res.status}`;
12450
+ try {
12451
+ const parsed = JSON.parse(text);
12452
+ if (parsed && typeof parsed === "object" && "error" in parsed) {
12453
+ errMsg = String(parsed.error);
12454
+ }
12455
+ } catch {
12711
12456
  }
12712
- return;
12457
+ return { ok: false, status: res.status, error: errMsg };
12713
12458
  }
12714
- if (!opts.as) {
12715
- printError("Missing --as <archetype>. Known archetypes:");
12716
- for (const arch2 of ARCHETYPES) {
12717
- console.log(` ${pc4.bold(arch2.name.padEnd(20))} ${pc4.dim(arch2.description)}`);
12718
- }
12719
- process.exitCode = 1;
12720
- return;
12459
+ const data = await res.json().catch(() => null);
12460
+ return { ok: true, status: res.status, data };
12461
+ } catch (err) {
12462
+ return {
12463
+ ok: false,
12464
+ status: 0,
12465
+ error: err instanceof Error ? err.message : String(err)
12466
+ };
12467
+ }
12468
+ }
12469
+ async function callHostCpRegistry(method, body) {
12470
+ const token = await readHostCpToken2();
12471
+ if (!token) return { ok: false, status: 0, error: "no token (host CP not started)" };
12472
+ const url = method === "DELETE" ? `http://127.0.0.1:${HOST_CP_PORT}/api/admin/registry/${encodeURIComponent(body.id)}` : `http://127.0.0.1:${HOST_CP_PORT}/api/admin/registry`;
12473
+ try {
12474
+ const res = await fetch(url, {
12475
+ method,
12476
+ headers: {
12477
+ Authorization: `Bearer ${token}`,
12478
+ ...method === "POST" ? { "Content-Type": "application/json" } : {}
12479
+ },
12480
+ ...method === "POST" ? { body: JSON.stringify(body) } : {}
12481
+ });
12482
+ if (!res.ok) {
12483
+ const text = await res.text().catch(() => "");
12484
+ return { ok: false, status: res.status, error: text || `HTTP ${res.status}` };
12721
12485
  }
12722
- const archetype = getArchetype(opts.as);
12723
- if (!archetype) {
12724
- printError(
12725
- `Unknown archetype "${opts.as}". Known: ${listArchetypeNames().join(", ")}`
12726
- );
12486
+ return { ok: true, status: res.status };
12487
+ } catch (err) {
12488
+ return {
12489
+ ok: false,
12490
+ status: 0,
12491
+ error: err instanceof Error ? err.message : String(err)
12492
+ };
12493
+ }
12494
+ }
12495
+ async function handleRegister(opts) {
12496
+ printHeader("Register world with host CP");
12497
+ let port = null;
12498
+ if (opts.port) {
12499
+ port = parseInt(opts.port, 10);
12500
+ if (!Number.isFinite(port) || port <= 0) {
12501
+ printError(`Invalid --port value: ${opts.port}`);
12727
12502
  process.exitCode = 1;
12728
12503
  return;
12729
12504
  }
12730
- const extras = [];
12731
- for (const raw of opts.capability) {
12732
- try {
12733
- extras.push(assertCapability(raw));
12734
- } catch (err) {
12735
- printError(err instanceof Error ? err.message : String(err));
12736
- process.exitCode = 1;
12737
- return;
12738
- }
12739
- }
12740
- const existing = readRoleFile();
12741
- if (existing && !opts.force) {
12505
+ } else {
12506
+ port = await discoverWorldPort(opts.world);
12507
+ if (port === null) {
12742
12508
  printError(
12743
- `Install already recorded as "${existing.archetype}" at ${ROLE_FILE_PATH}. Pass --force to overwrite.`
12509
+ `Could not discover port for world ${opts.world}. Pass --port explicitly or check that the world exists in \`olam list\`.`
12744
12510
  );
12745
12511
  process.exitCode = 1;
12746
12512
  return;
12747
12513
  }
12748
- let resolved;
12749
- try {
12750
- resolved = expandArchetype(archetype.name);
12751
- } catch (err) {
12752
- if (err instanceof UnknownArchetypeError) {
12753
- printError(err.message);
12754
- process.exitCode = 1;
12755
- return;
12756
- }
12757
- throw err;
12758
- }
12759
- for (const extra of extras) resolved.add(extra);
12760
- writeRoleFile({
12761
- archetype: archetype.name,
12762
- customCapabilities: extras,
12763
- installedAt: Date.now()
12764
- });
12765
- printHeader(`Installed as ${pc4.bold(archetype.name)}`);
12766
- printInfo("File", ROLE_FILE_PATH);
12767
- printInfo("Description", archetype.description);
12768
- printInfo("Capabilities", `${resolved.size} total`);
12769
- if (extras.length > 0) {
12770
- printInfo("Added beyond preset", extras.join(", "));
12771
- }
12772
- const steps = nextStepsFor(archetype.name);
12773
- if (steps.length > 0) {
12774
- printHeader("Next steps");
12775
- for (const step of steps) console.log(` ${pc4.dim("\u2022")} ${step}`);
12514
+ }
12515
+ const result = await callHostCpRegistry("POST", { id: opts.world, port });
12516
+ if (!result.ok) {
12517
+ printError(`Register failed: ${result.error}`);
12518
+ if (result.status === 0) {
12519
+ printInfo("Hint", "Is host CP running? `olam host-cp status`");
12776
12520
  }
12777
- printSuccess("Done. Run `olam install --show` to see the full capability list.");
12778
- });
12779
- }
12780
-
12781
- // src/commands/auth.ts
12782
- init_auth();
12783
- import pc8 from "picocolors";
12784
- import * as readline from "node:readline/promises";
12785
- import { spawn as spawn3 } from "node:child_process";
12786
-
12787
- // src/commands/auth-status.ts
12788
- import * as fs8 from "node:fs";
12789
- import * as os5 from "node:os";
12790
- import * as path9 from "node:path";
12791
- import pc5 from "picocolors";
12792
-
12793
- // ../auth-logic/dist/effective-state.js
12794
- function effectiveState(account, now = Date.now()) {
12795
- const persisted = account.state ?? (account.rateLimited ? "cooldown" : "active");
12796
- if (persisted === "disabled")
12797
- return "disabled";
12798
- if (account.expiresAt != null && account.expiresAt <= now)
12799
- return "expired";
12800
- if (persisted === "cooldown" || persisted === "usage-capped") {
12801
- const reset = account.rateLimitResetsAt ? new Date(account.rateLimitResetsAt).getTime() : 0;
12802
- if (reset > 0 && reset <= now)
12803
- return "active";
12804
- return persisted;
12521
+ process.exitCode = 1;
12522
+ return;
12805
12523
  }
12806
- return "active";
12524
+ printSuccess(`Registered ${opts.world} \u2192 :${port}`);
12525
+ printInfo("UI", `http://127.0.0.1:${HOST_CP_PORT}/world/${encodeURIComponent(opts.world)}`);
12807
12526
  }
12808
-
12809
- // ../auth-logic/dist/pick-credential.js
12810
- function pickCredential(accounts, now = Date.now()) {
12811
- const active = accounts.filter((a) => effectiveState(a, now) === "active");
12812
- if (active.length === 0)
12813
- return null;
12814
- return [...active].sort((a, b) => {
12815
- const aCount = a.usage?.requestCount5h ?? 0;
12816
- const bCount = b.usage?.requestCount5h ?? 0;
12817
- if (aCount !== bCount)
12818
- return aCount - bCount;
12819
- const aLast = a.lastUsed ? new Date(a.lastUsed).getTime() : 0;
12820
- const bLast = b.lastUsed ? new Date(b.lastUsed).getTime() : 0;
12821
- return aLast - bLast;
12822
- })[0] ?? null;
12527
+ async function handleDeregister(opts) {
12528
+ printHeader("Deregister world from host CP");
12529
+ const result = await callHostCpRegistry("DELETE", { id: opts.world });
12530
+ if (!result.ok) {
12531
+ printError(`Deregister failed: ${result.error}`);
12532
+ if (result.status === 0) {
12533
+ printInfo("Hint", "Is host CP running? `olam host-cp status`");
12534
+ }
12535
+ process.exitCode = 1;
12536
+ return;
12537
+ }
12538
+ printSuccess(`Deregistered ${opts.world}`);
12823
12539
  }
12540
+ var HOST_CP_PORT;
12541
+ var init_host_cp = __esm({
12542
+ "src/commands/host-cp.ts"() {
12543
+ "use strict";
12544
+ init_dist();
12545
+ init_output();
12546
+ init_docker_host();
12547
+ init_open_url();
12548
+ HOST_CP_PORT = 19e3;
12549
+ }
12550
+ });
12824
12551
 
12825
- // ../auth-logic/dist/next-cooldown-reset.js
12826
- function nextCooldownReset(accounts, now = Date.now()) {
12827
- const upcoming = accounts.filter((a) => effectiveState(a, now) === "cooldown").map((a) => a.rateLimitResetsAt ? new Date(a.rateLimitResetsAt).getTime() : 0).filter((ts) => ts > now).sort((a, b) => a - b);
12828
- return upcoming.length > 0 ? new Date(upcoming[0]).toISOString() : null;
12552
+ // src/install-root.ts
12553
+ var install_root_exports = {};
12554
+ __export(install_root_exports, {
12555
+ MissingBuildScriptError: () => MissingBuildScriptError,
12556
+ installRoot: () => installRoot,
12557
+ isDevMode: () => isDevMode,
12558
+ resolveBuildScript: () => resolveBuildScript
12559
+ });
12560
+ import { existsSync as existsSync18 } from "node:fs";
12561
+ import { dirname as dirname13, join as join24, resolve as resolve6 } from "node:path";
12562
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
12563
+ function installRoot(metaUrl = import.meta.url) {
12564
+ const here = fileURLToPath3(metaUrl);
12565
+ return resolve6(dirname13(here), "..");
12566
+ }
12567
+ function isDevMode(env = process.env, installRootDir = installRoot()) {
12568
+ if (env.OLAM_DEV !== "1") return false;
12569
+ const repoRoot = resolve6(installRootDir, "..", "..");
12570
+ return existsSync18(join24(repoRoot, "packages")) && existsSync18(join24(repoRoot, "package.json"));
12571
+ }
12572
+ function resolveBuildScript(input) {
12573
+ const { scriptRelPath, env = process.env, installRootDir = installRoot() } = input;
12574
+ if (!isDevMode(env, installRootDir)) {
12575
+ throw new MissingBuildScriptError(scriptRelPath);
12576
+ }
12577
+ const repoRoot = resolve6(installRootDir, "..", "..");
12578
+ return join24(repoRoot, scriptRelPath);
12829
12579
  }
12580
+ var MissingBuildScriptError;
12581
+ var init_install_root = __esm({
12582
+ "src/install-root.ts"() {
12583
+ "use strict";
12584
+ MissingBuildScriptError = class extends Error {
12585
+ constructor(scriptRelPath) {
12586
+ super(
12587
+ `Build script ${scriptRelPath} is not available in this CLI install.
12588
+ Source-build paths require a monorepo clone:
12589
+ git clone https://github.com/pleri/olam && cd olam
12590
+ OLAM_DEV=1 olam <command> --from-source
12591
+ For published-image upgrades (Phase B+), drop the --from-source flag
12592
+ and the CLI will pull pre-built images from ghcr.io/pleri/* by digest.`
12593
+ );
12594
+ this.name = "MissingBuildScriptError";
12595
+ }
12596
+ };
12597
+ }
12598
+ });
12830
12599
 
12831
- // src/commands/auth-status.ts
12832
- init_auth();
12833
- init_exit_codes();
12834
- var LOCAL_DATA_DIR = path9.join(os5.homedir(), ".olam", "auth-data");
12835
- function localHHMM(isoStr) {
12836
- const d = new Date(isoStr);
12837
- return d.toLocaleTimeString(void 0, {
12838
- hour: "2-digit",
12839
- minute: "2-digit",
12840
- hour12: false
12841
- });
12600
+ // src/protocol-version.ts
12601
+ var protocol_version_exports = {};
12602
+ __export(protocol_version_exports, {
12603
+ OLAM_PROTOCOL_VERSIONS_SUPPORTED: () => OLAM_PROTOCOL_VERSIONS_SUPPORTED,
12604
+ checkProtocolOverlap: () => checkProtocolOverlap,
12605
+ inspectImageProtocolVersions: () => inspectImageProtocolVersions,
12606
+ parseProtocolVersionsLabel: () => parseProtocolVersionsLabel
12607
+ });
12608
+ import { spawnSync as spawnSync5 } from "node:child_process";
12609
+ function parseProtocolVersionsLabel(labelValue) {
12610
+ if (!labelValue) return [];
12611
+ const parts = labelValue.split(",").map((s) => s.trim()).filter(Boolean);
12612
+ const versions = /* @__PURE__ */ new Set();
12613
+ for (const part of parts) {
12614
+ const n = Number.parseInt(part, 10);
12615
+ if (Number.isFinite(n) && n > 0 && String(n) === part) {
12616
+ versions.add(n);
12617
+ }
12618
+ }
12619
+ return Array.from(versions).sort((a, b) => a - b);
12842
12620
  }
12843
- function daysAgoStr(expiresAt, now) {
12844
- const diffDays = Math.floor((now - expiresAt) / (1e3 * 60 * 60 * 24));
12845
- if (diffDays <= 0) return "expired today";
12846
- return `expired ${diffDays} day${diffDays === 1 ? "" : "s"} ago`;
12621
+ function checkProtocolOverlap(imageVersions, cliVersions = OLAM_PROTOCOL_VERSIONS_SUPPORTED) {
12622
+ const cliSet = new Set(cliVersions);
12623
+ const overlap = imageVersions.filter((v) => cliSet.has(v));
12624
+ if (imageVersions.length === 0) {
12625
+ return {
12626
+ overlap: [],
12627
+ imageVersions: [],
12628
+ cliVersions,
12629
+ compatible: false,
12630
+ remedy: `Devbox image is missing the \`olam.protocol.versions\` LABEL. This CLI requires versions [${cliVersions.join(", ")}]. See docs/architecture/devbox-contract.md \xA71 for the contract; rebuild the image with \`LABEL olam.protocol.versions="1"\`.`
12631
+ };
12632
+ }
12633
+ if (overlap.length === 0) {
12634
+ const imgLow = Math.min(...imageVersions);
12635
+ const imgHigh = Math.max(...imageVersions);
12636
+ const cliLow = Math.min(...cliVersions);
12637
+ const cliHigh = Math.max(...cliVersions);
12638
+ return {
12639
+ overlap: [],
12640
+ imageVersions: [...imageVersions],
12641
+ cliVersions,
12642
+ compatible: false,
12643
+ remedy: `Devbox image protocol versions [${imageVersions.join(", ")}] don't overlap CLI's [${cliVersions.join(", ")}]. ` + (imgHigh < cliLow ? `The image is older than this CLI supports \u2014 rebuild against the contract version ${cliLow}+ at docs/architecture/devbox-contract.md.` : imgLow > cliHigh ? `The image is newer than this CLI supports \u2014 pin a compatible CLI: \`npm install -g @pleri/olam-cli@<version-with-protocol-${imgLow}>\`.` : `Pin a compatible CLI version that overlaps with the image's range.`)
12644
+ };
12645
+ }
12646
+ return {
12647
+ overlap,
12648
+ imageVersions: [...imageVersions],
12649
+ cliVersions,
12650
+ compatible: true,
12651
+ remedy: ""
12652
+ };
12847
12653
  }
12848
- function trunc(s, maxLen) {
12849
- return s.length > maxLen ? s.slice(0, maxLen) : s;
12654
+ function inspectImageProtocolVersions(imageRef, dockerInspect = realDockerInspect) {
12655
+ const { exitCode, stdout, stderr } = dockerInspect(imageRef);
12656
+ if (exitCode !== 0) {
12657
+ return {
12658
+ imageRef,
12659
+ versions: [],
12660
+ inspectFailed: true,
12661
+ inspectError: stderr.trim() || `docker inspect exited ${exitCode}`
12662
+ };
12663
+ }
12664
+ const raw = stdout.trim();
12665
+ const value = raw === "<no value>" ? "" : raw;
12666
+ return {
12667
+ imageRef,
12668
+ versions: parseProtocolVersionsLabel(value),
12669
+ inspectFailed: false,
12670
+ inspectError: void 0
12671
+ };
12850
12672
  }
12851
- var STATE_PRIORITY = {
12852
- active: 0,
12853
- cooldown: 1,
12854
- "usage-capped": 2,
12855
- disabled: 3,
12856
- expired: 4
12857
- };
12858
- function formatAuthStatus(accounts, now = Date.now()) {
12859
- const picked = pickCredential(accounts, now);
12860
- const rows = accounts.map((account) => {
12861
- const state = effectiveState(account, now);
12862
- const isPicked = picked != null && account.id === picked.id;
12863
- const req5h = account.usage?.requestCount5h ?? 0;
12864
- const last429 = account.usage?.last429At ? localHHMM(account.usage.last429At) : "never";
12865
- let reason;
12866
- if (isPicked) {
12867
- reason = "\u2190 selected";
12868
- } else if (state === "active") {
12869
- reason = `req5h=${req5h} (higher than candidate)`;
12870
- } else if (state === "cooldown") {
12871
- const resetTime = account.rateLimitResetsAt ? localHHMM(account.rateLimitResetsAt) : "?";
12872
- reason = `cooldown until ${resetTime}`;
12873
- } else if (state === "expired") {
12874
- reason = daysAgoStr(account.expiresAt ?? 0, now);
12875
- } else {
12876
- reason = "disabled";
12877
- }
12878
- return { id: account.id, label: account.accountLabel ?? account.id, state, reason, req5h, last429, isPicked };
12879
- });
12880
- rows.sort((a, b) => {
12881
- if (a.isPicked !== b.isPicked) return a.isPicked ? -1 : 1;
12882
- return STATE_PRIORITY[a.state] - STATE_PRIORITY[b.state];
12883
- });
12884
- const COL = { id: 17, label: 17, state: 11, reason: 28, req5h: 7 };
12885
- const lines = [];
12886
- const hdr = "id".padEnd(COL.id) + "label".padEnd(COL.label) + "state".padEnd(COL.state) + "reason".padEnd(COL.reason) + "req5h".padEnd(COL.req5h) + "last429";
12887
- lines.push(pc5.dim(hdr));
12888
- lines.push(pc5.dim("-".repeat(hdr.length)));
12889
- for (const row of rows) {
12890
- const id = trunc(row.id, 16).padEnd(COL.id);
12891
- const label = trunc(row.label, 16).padEnd(COL.label);
12892
- const stateRaw = row.state.padEnd(COL.state);
12893
- const stateColored = row.state === "active" ? pc5.green(stateRaw) : row.state === "cooldown" ? pc5.yellow(stateRaw) : row.state === "expired" ? pc5.red(stateRaw) : pc5.dim(stateRaw);
12894
- const reason = row.reason.padEnd(COL.reason);
12895
- const req5h = String(row.req5h).padEnd(COL.req5h);
12896
- if (row.isPicked) {
12897
- lines.push(
12898
- pc5.bold(id) + pc5.bold(label) + stateColored + pc5.green(reason) + pc5.dim(req5h) + pc5.dim(row.last429)
12673
+ var OLAM_PROTOCOL_VERSIONS_SUPPORTED, realDockerInspect;
12674
+ var init_protocol_version = __esm({
12675
+ "src/protocol-version.ts"() {
12676
+ "use strict";
12677
+ OLAM_PROTOCOL_VERSIONS_SUPPORTED = [1];
12678
+ realDockerInspect = (imageRef) => {
12679
+ const result = spawnSync5(
12680
+ "docker",
12681
+ ["inspect", imageRef, "--format", '{{ index .Config.Labels "olam.protocol.versions" }}'],
12682
+ { encoding: "utf8", timeout: 1e4 }
12899
12683
  );
12900
- } else {
12901
- lines.push(id + label + stateColored + reason + pc5.dim(req5h) + pc5.dim(row.last429));
12902
- }
12684
+ return {
12685
+ exitCode: result.status ?? -1,
12686
+ stdout: result.stdout ?? "",
12687
+ stderr: result.stderr ?? ""
12688
+ };
12689
+ };
12690
+ }
12691
+ });
12692
+
12693
+ // src/registry-allowlist.ts
12694
+ var registry_allowlist_exports = {};
12695
+ __export(registry_allowlist_exports, {
12696
+ decideAllowlist: () => decideAllowlist,
12697
+ resolveDevboxImageOverride: () => resolveDevboxImageOverride
12698
+ });
12699
+ function decideAllowlist(input) {
12700
+ const { imageRef, allowCustomRegistry } = input;
12701
+ const allowedByDefault = DEFAULT_ALLOWLIST_PATTERNS.some((re) => re.test(imageRef));
12702
+ if (allowedByDefault) {
12703
+ return {
12704
+ imageRef,
12705
+ allowedByDefault: true,
12706
+ accepted: true,
12707
+ stderrLine: ""
12708
+ };
12903
12709
  }
12904
- if (picked == null) {
12905
- const resetIso = nextCooldownReset(accounts, now);
12906
- lines.push("");
12907
- lines.push(
12908
- resetIso ? pc5.yellow(`Next reset: ${localHHMM(resetIso)}`) : pc5.dim("No reset scheduled")
12909
- );
12910
- return { output: lines.join("\n"), exitCode: EXIT_AUTH_NEEDS_ATTENTION };
12710
+ if (allowCustomRegistry) {
12711
+ return {
12712
+ imageRef,
12713
+ allowedByDefault: false,
12714
+ accepted: true,
12715
+ stderrLine: `Warning: using custom devbox image '${imageRef}'. (--allow-custom-registry was specified.) Verify the source and digest before proceeding.`
12716
+ };
12911
12717
  }
12912
- return { output: lines.join("\n"), exitCode: 0 };
12913
- }
12914
- function toSafeAccount(a) {
12915
12718
  return {
12916
- id: a.id,
12917
- accountLabel: a.accountLabel,
12918
- // expiresAt not exposed in summary — state is pre-computed server-side
12919
- rateLimited: a.rateLimited,
12920
- rateLimitResetsAt: a.rateLimitResetsAt,
12921
- lastUsed: a.lastUsed,
12922
- state: a.state,
12923
- usage: a.usage ? { requestCount5h: a.usage.requestCount5h, last429At: a.usage.last429At } : void 0
12719
+ imageRef,
12720
+ allowedByDefault: false,
12721
+ accepted: false,
12722
+ stderrLine: `Error: image '${imageRef}' is outside allowed registries (ghcr.io/pleri/*).
12723
+ To override: re-run with --allow-custom-registry
12724
+ Verify the source and digest before doing so.`
12924
12725
  };
12925
12726
  }
12926
- async function runAuthStatus(getStatus) {
12927
- const fetchStatus = getStatus ?? (() => new AuthClient().status());
12928
- let status;
12929
- try {
12930
- status = await fetchStatus();
12931
- } catch {
12932
- printError("Failed to contact auth service. Run `olam auth up` first.");
12933
- process.exitCode = 1;
12934
- return;
12727
+ function resolveDevboxImageOverride(flagValue, env = process.env) {
12728
+ if (flagValue && flagValue.trim().length > 0) {
12729
+ return flagValue.trim();
12935
12730
  }
12936
- if (!status.reachable) {
12937
- printError("Auth container is not reachable. Run `olam auth up` first.");
12938
- process.exitCode = 1;
12939
- return;
12731
+ const envValue = env.OLAM_DEVBOX_IMAGE;
12732
+ if (envValue && envValue.trim().length > 0) {
12733
+ return envValue.trim();
12940
12734
  }
12941
- if (status.accounts.length === 0) {
12942
- console.log(pc5.dim("No credentials found. Run: olam auth login"));
12943
- return;
12735
+ return void 0;
12736
+ }
12737
+ var DEFAULT_ALLOWLIST_PATTERNS;
12738
+ var init_registry_allowlist = __esm({
12739
+ "src/registry-allowlist.ts"() {
12740
+ "use strict";
12741
+ DEFAULT_ALLOWLIST_PATTERNS = [
12742
+ // ghcr.io/pleri/<anything>:<tag> or ghcr.io/pleri/<anything>@sha256:<digest>
12743
+ /^ghcr\.io\/pleri\/[^/\s]+(?::[^\s]+|@sha256:[a-f0-9]+)?$/
12744
+ ];
12944
12745
  }
12945
- const accounts = status.accounts.map(toSafeAccount);
12946
- const result = formatAuthStatus(accounts);
12947
- console.log(result.output);
12948
- if (result.exitCode !== 0) {
12949
- process.exitCode = result.exitCode;
12746
+ });
12747
+
12748
+ // ../core/src/world/world-yaml.ts
12749
+ import * as fs21 from "node:fs";
12750
+ import * as path24 from "node:path";
12751
+ import { parse as parseYaml4, stringify as stringifyYaml4 } from "yaml";
12752
+ function writeWorldYaml(worldPath, data) {
12753
+ const olamDir = path24.join(worldPath, ".olam");
12754
+ fs21.mkdirSync(olamDir, { recursive: true });
12755
+ fs21.writeFileSync(path24.join(olamDir, "world.yaml"), stringifyYaml4(data), "utf-8");
12756
+ }
12757
+ function readWorldYaml(worldPath) {
12758
+ const yamlPath = path24.join(worldPath, ".olam", "world.yaml");
12759
+ if (!fs21.existsSync(yamlPath)) return null;
12760
+ try {
12761
+ const raw = fs21.readFileSync(yamlPath, "utf-8");
12762
+ const parsed = parseYaml4(raw);
12763
+ return WorldYamlSchema.parse(parsed);
12764
+ } catch {
12765
+ return null;
12950
12766
  }
12951
12767
  }
12952
-
12953
- // src/commands/auth-upgrade.ts
12954
- import * as fs20 from "node:fs";
12955
- import * as path23 from "node:path";
12956
- import { spawnSync as spawnSync7 } from "node:child_process";
12957
- import ora2 from "ora";
12958
- import pc7 from "picocolors";
12959
-
12960
- // src/commands/host-cp.ts
12961
- init_dist();
12962
- import * as crypto5 from "node:crypto";
12963
- import * as fs19 from "node:fs";
12964
- import * as os12 from "node:os";
12965
- import * as path22 from "node:path";
12966
- import { spawnSync as spawnSync4 } from "node:child_process";
12967
- import Dockerode2 from "dockerode";
12968
- init_docker_host();
12969
-
12970
- // ../core/src/util/open-url.ts
12971
- import { spawnSync as spawnSync3 } from "node:child_process";
12972
- function openUrl(url) {
12973
- if (process.env.CI === "true") {
12974
- return { opened: false, reason: "CI environment detected" };
12768
+ var WorldYamlSchema;
12769
+ var init_world_yaml = __esm({
12770
+ "../core/src/world/world-yaml.ts"() {
12771
+ "use strict";
12772
+ init_zod();
12773
+ WorldYamlSchema = external_exports.object({
12774
+ world_type: external_exports.enum(["worktree", "clone"]),
12775
+ origin_url: external_exports.string().min(1),
12776
+ cli_version: external_exports.string().nullable()
12777
+ });
12975
12778
  }
12976
- if (!process.stdout.isTTY) {
12977
- return { opened: false, reason: "non-interactive shell" };
12779
+ });
12780
+
12781
+ // ../core/src/world/version-pin.ts
12782
+ var version_pin_exports = {};
12783
+ __export(version_pin_exports, {
12784
+ CLI_VERSION: () => CLI_VERSION,
12785
+ checkVersionPin: () => checkVersionPin,
12786
+ resetWarnedSet: () => resetWarnedSet,
12787
+ stampVersionPin: () => stampVersionPin
12788
+ });
12789
+ function checkVersionPin(worldId, worldPath, cliVersion = CLI_VERSION, _readYaml = readWorldYaml, _warn = (msg) => console.warn(msg)) {
12790
+ const yaml = _readYaml(worldPath);
12791
+ if (!yaml || yaml.cli_version === null) return true;
12792
+ if (yaml.cli_version === cliVersion) return true;
12793
+ if (!warnedThisSession.has(worldId)) {
12794
+ warnedThisSession.add(worldId);
12795
+ _warn(
12796
+ `[olam] World "${worldId}" was created with CLI v${yaml.cli_version}, current CLI is v${cliVersion}. Run \`olam world upgrade ${worldId}\` to refresh the pin.`
12797
+ );
12978
12798
  }
12979
- if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
12980
- return { opened: false, reason: "headless linux (no DISPLAY/WAYLAND_DISPLAY)" };
12799
+ return false;
12800
+ }
12801
+ function stampVersionPin(worldPath, cliVersion = CLI_VERSION, _readYaml = readWorldYaml, _writeYaml = writeWorldYaml) {
12802
+ const yaml = _readYaml(worldPath);
12803
+ if (!yaml) return false;
12804
+ if (yaml.cli_version === cliVersion) return false;
12805
+ _writeYaml(worldPath, { ...yaml, cli_version: cliVersion });
12806
+ return true;
12807
+ }
12808
+ function resetWarnedSet() {
12809
+ warnedThisSession.clear();
12810
+ }
12811
+ var CLI_VERSION, warnedThisSession;
12812
+ var init_version_pin = __esm({
12813
+ "../core/src/world/version-pin.ts"() {
12814
+ "use strict";
12815
+ init_world_yaml();
12816
+ CLI_VERSION = process.env["OLAM_CLI_VERSION"] ?? "0.0.0";
12817
+ warnedThisSession = /* @__PURE__ */ new Set();
12981
12818
  }
12982
- let cmd;
12983
- let args;
12984
- if (process.platform === "darwin") {
12985
- cmd = "open";
12986
- args = [url];
12987
- } else if (process.platform === "win32") {
12988
- cmd = "cmd";
12989
- args = ["/c", "start", "", url];
12990
- } else {
12991
- cmd = "xdg-open";
12992
- args = [url];
12819
+ });
12820
+
12821
+ // ../core/src/orchestrator/enter.ts
12822
+ var enter_exports = {};
12823
+ __export(enter_exports, {
12824
+ getEnterCommand: () => getEnterCommand
12825
+ });
12826
+ function getEnterCommand(worldId, containerId, provider, sshHost) {
12827
+ if (provider === "docker") {
12828
+ const command = `docker exec -it ${containerId} claude`;
12829
+ return {
12830
+ command,
12831
+ instructions: [
12832
+ "Run this command in your terminal to enter the world:",
12833
+ "",
12834
+ ` ${command}`,
12835
+ "",
12836
+ "Press Ctrl+C to exit."
12837
+ ].join("\n")
12838
+ };
12993
12839
  }
12994
- try {
12995
- const result = spawnSync3(cmd, args, { stdio: "ignore", timeout: 5e3 });
12996
- if (result.error) {
12997
- return { opened: false, reason: `spawn ${cmd}: ${result.error.message}` };
12998
- }
12999
- if (result.status !== 0 && result.status !== null) {
13000
- return { opened: false, reason: `${cmd} exited ${result.status}` };
13001
- }
13002
- return { opened: true };
13003
- } catch (err) {
12840
+ if (provider === "ssh" && sshHost) {
12841
+ const command = `ssh -t ${sshHost} docker exec -it ${containerId} claude`;
13004
12842
  return {
13005
- opened: false,
13006
- reason: err instanceof Error ? err.message : String(err)
12843
+ command,
12844
+ instructions: [
12845
+ "Run this command to enter the remote world:",
12846
+ "",
12847
+ ` ${command}`,
12848
+ "",
12849
+ "Press Ctrl+C to exit."
12850
+ ].join("\n")
13007
12851
  };
13008
12852
  }
12853
+ return {
12854
+ command: "",
12855
+ instructions: `Cannot determine enter command for provider: ${provider}`
12856
+ };
13009
12857
  }
13010
-
13011
- // src/commands/host-cp.ts
13012
- var HOST_CP_PORT = 19e3;
13013
- function findComposeFile() {
13014
- const candidates = [
13015
- // Bundled path: dist/index.js lives at <pkg>/dist/; host-cp/ is a sibling of dist/
13016
- path22.resolve(path22.dirname(new URL(import.meta.url).pathname), "../host-cp/compose.yaml"),
13017
- // Source-mode: cwd is monorepo root
13018
- path22.resolve(process.cwd(), "packages/host-cp/compose.yaml"),
13019
- // Source-mode: cwd is one level inside the monorepo
13020
- path22.resolve(process.cwd(), "../packages/host-cp/compose.yaml")
13021
- ];
13022
- for (const c of candidates) {
13023
- if (fs19.existsSync(c)) return c;
12858
+ var init_enter = __esm({
12859
+ "../core/src/orchestrator/enter.ts"() {
12860
+ "use strict";
13024
12861
  }
13025
- return path22.resolve(process.cwd(), "packages/host-cp/compose.yaml");
13026
- }
13027
- function olamHome() {
13028
- return process.env.OLAM_HOME ?? path22.join(os12.homedir(), ".olam");
13029
- }
13030
- function tokenPath() {
13031
- return path22.join(olamHome(), "host-cp.token");
13032
- }
13033
- function pidPath() {
13034
- return path22.join(olamHome(), "host-cp.pid");
13035
- }
13036
- function authSecretPath() {
13037
- return path22.join(olamHome(), "auth-secret");
13038
- }
13039
- function readAuthSecret2() {
13040
- const filePath = authSecretPath();
13041
- if (!fs19.existsSync(filePath)) return null;
13042
- const raw = fs19.readFileSync(filePath, "utf-8").trim();
13043
- return raw.length > 0 ? raw : null;
13044
- }
13045
- function writeToken() {
13046
- const token = crypto5.randomBytes(32).toString("hex");
13047
- const filePath = tokenPath();
13048
- fs19.mkdirSync(path22.dirname(filePath), { recursive: true });
13049
- fs19.writeFileSync(filePath, token, { mode: 384 });
13050
- return token;
13051
- }
13052
- function readToken() {
13053
- const filePath = tokenPath();
13054
- if (!fs19.existsSync(filePath)) return null;
13055
- return fs19.readFileSync(filePath, "utf-8").trim();
12862
+ });
12863
+
12864
+ // ../core/src/crystallize/checksum.ts
12865
+ var checksum_exports = {};
12866
+ __export(checksum_exports, {
12867
+ computeGraphChecksum: () => computeGraphChecksum
12868
+ });
12869
+ import { createHash as createHash2 } from "node:crypto";
12870
+ function computeGraphChecksum(nodes, edges) {
12871
+ const sortedNodes = [...nodes].sort((a, b) => a.id.localeCompare(b.id));
12872
+ const sortedEdges = [...edges].sort((a, b) => a.id.localeCompare(b.id));
12873
+ const payload = JSON.stringify({ nodes: sortedNodes, edges: sortedEdges });
12874
+ return createHash2("sha256").update(payload).digest("hex");
13056
12875
  }
13057
- function removeToken() {
13058
- const filePath = tokenPath();
13059
- if (!fs19.existsSync(filePath)) return false;
13060
- fs19.unlinkSync(filePath);
13061
- return true;
12876
+ var init_checksum = __esm({
12877
+ "../core/src/crystallize/checksum.ts"() {
12878
+ "use strict";
12879
+ }
12880
+ });
12881
+
12882
+ // ../core/src/config/machine-schema.ts
12883
+ var machine_schema_exports = {};
12884
+ __export(machine_schema_exports, {
12885
+ MachineConfigSchema: () => MachineConfigSchema,
12886
+ initMachineConfig: () => initMachineConfig,
12887
+ readMachineConfig: () => readMachineConfig,
12888
+ writeMachineConfig: () => writeMachineConfig
12889
+ });
12890
+ import * as fs34 from "node:fs";
12891
+ import * as path38 from "node:path";
12892
+ import * as os20 from "node:os";
12893
+ import { parse as parseYaml5, stringify as stringifyYaml5 } from "yaml";
12894
+ function readMachineConfig(configPath) {
12895
+ const p = configPath ?? DEFAULT_CONFIG_PATH;
12896
+ if (!fs34.existsSync(p)) return null;
12897
+ try {
12898
+ const raw = fs34.readFileSync(p, "utf-8");
12899
+ const parsed = parseYaml5(raw);
12900
+ return MachineConfigSchema.parse(parsed);
12901
+ } catch {
12902
+ return null;
12903
+ }
13062
12904
  }
13063
- function writePid(pid) {
13064
- const filePath = pidPath();
13065
- fs19.mkdirSync(path22.dirname(filePath), { recursive: true });
13066
- fs19.writeFileSync(filePath, String(pid), { mode: 420 });
12905
+ function writeMachineConfig(config, configPath) {
12906
+ const p = configPath ?? DEFAULT_CONFIG_PATH;
12907
+ fs34.mkdirSync(path38.dirname(p), { recursive: true });
12908
+ fs34.writeFileSync(p, stringifyYaml5({ ...config }), { mode: 420 });
13067
12909
  }
13068
- function removePid() {
13069
- const filePath = pidPath();
13070
- if (!fs19.existsSync(filePath)) return false;
13071
- fs19.unlinkSync(filePath);
13072
- return true;
12910
+ function initMachineConfig(opts = {}) {
12911
+ const configPath = opts.configPath ?? DEFAULT_CONFIG_PATH;
12912
+ const existing = readMachineConfig(configPath);
12913
+ if (existing) return existing;
12914
+ const config = MachineConfigSchema.parse({
12915
+ telemetry: opts.telemetry ?? true
12916
+ });
12917
+ writeMachineConfig(config, configPath);
12918
+ return config;
13073
12919
  }
13074
- async function findHostCpContainer() {
13075
- const docker2 = new Dockerode2(resolveDockerHostOptions());
13076
- const containers = await docker2.listContainers({ all: true });
13077
- for (const c of containers) {
13078
- const names = (c.Names ?? []).map((n) => n.replace(/^\//, ""));
13079
- if (names.includes("olam-host-cp")) {
13080
- return {
13081
- id: c.Id.slice(0, 12),
13082
- name: "olam-host-cp",
13083
- state: c.State,
13084
- status: c.Status
13085
- };
13086
- }
12920
+ var MachineConfigSchema, DEFAULT_CONFIG_PATH;
12921
+ var init_machine_schema = __esm({
12922
+ "../core/src/config/machine-schema.ts"() {
12923
+ "use strict";
12924
+ init_zod();
12925
+ MachineConfigSchema = external_exports.object({
12926
+ schema_version: external_exports.literal(1).default(1),
12927
+ channel: external_exports.enum(["stable", "beta", "edge"]).default("stable"),
12928
+ auto_update: external_exports.boolean().default(true),
12929
+ telemetry: external_exports.boolean().default(true),
12930
+ worlds_dir: external_exports.string().default(() => path38.join(os20.homedir(), ".olam", "worlds"))
12931
+ });
12932
+ DEFAULT_CONFIG_PATH = path38.join(os20.homedir(), ".olam", "config.yaml");
13087
12933
  }
13088
- return null;
12934
+ });
12935
+
12936
+ // src/index.ts
12937
+ import { Command } from "commander";
12938
+ import * as fs37 from "node:fs";
12939
+ import * as path41 from "node:path";
12940
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
12941
+
12942
+ // src/commands/init.ts
12943
+ init_output();
12944
+ import * as fs5 from "node:fs";
12945
+ import * as path5 from "node:path";
12946
+ import { execSync } from "node:child_process";
12947
+ import pc3 from "picocolors";
12948
+
12949
+ // src/commands/workspace.ts
12950
+ init_workspace();
12951
+ init_loader();
12952
+ init_output();
12953
+ import * as fs4 from "node:fs";
12954
+ import * as path4 from "node:path";
12955
+ import pc2 from "picocolors";
12956
+ import { stringify as stringifyYaml2 } from "yaml";
12957
+ function printWorkspaceNotFound(name) {
12958
+ printError(`No workspace named "${name}" under ${workspacesDir()}`);
13089
12959
  }
13090
- async function probeHostCp() {
13091
- const candidateUrl = `http://127.0.0.1:${HOST_CP_PORT}`;
13092
- let httpOk = false;
13093
- try {
13094
- const res = await fetch(`${candidateUrl}/api/bootstrap`, {
13095
- signal: AbortSignal.timeout(2e3)
13096
- });
13097
- httpOk = res.ok;
13098
- } catch {
13099
- httpOk = false;
12960
+ function parseRepoFlag(raw) {
12961
+ const hashIdx = raw.indexOf("#");
12962
+ const url = hashIdx === -1 ? raw : raw.slice(0, hashIdx);
12963
+ const branch = hashIdx === -1 ? void 0 : raw.slice(hashIdx + 1);
12964
+ if (url.length === 0) throw new Error(`invalid --repo value "${raw}" (empty url)`);
12965
+ if (branch !== void 0 && branch.length === 0) {
12966
+ throw new Error(`invalid --repo value "${raw}" (empty branch after #)`);
13100
12967
  }
13101
- if (httpOk) {
13102
- let mode = "bare";
12968
+ const nameFromUrl = url.replace(/\.git$/, "").split(/[\/:]/).filter(Boolean).at(-1) ?? "repo";
12969
+ return branch ? { name: nameFromUrl, url, branch } : { name: nameFromUrl, url };
12970
+ }
12971
+ function registerWorkspace(program2) {
12972
+ const workspace = program2.command("workspace").description("Manage the named catalog of repo bundles that worlds instantiate from");
12973
+ workspace.command("list").description("List all workspaces (name, repoCount, updatedAt)").action(() => {
12974
+ const all = listWorkspaces();
12975
+ if (all.length === 0) {
12976
+ console.log(pc2.dim(`No workspaces under ${workspacesDir()}`));
12977
+ return;
12978
+ }
12979
+ printHeader(`${all.length} workspace(s)`);
12980
+ for (const ws of all) {
12981
+ const when = new Date(ws.updatedAt).toISOString().slice(0, 10);
12982
+ console.log(
12983
+ ` ${pc2.bold(ws.name.padEnd(24))} ${String(ws.repos.length).padEnd(3)} repos ${pc2.dim(when)}`
12984
+ );
12985
+ }
12986
+ });
12987
+ workspace.command("show").description("Show a workspace as YAML").argument("<name>", "Workspace name").action((name) => {
13103
12988
  try {
13104
- const container = await findHostCpContainer();
13105
- if (container && container.state === "running") {
13106
- mode = "container";
12989
+ const ws = readWorkspace(name);
12990
+ if (!ws) {
12991
+ printWorkspaceNotFound(name);
12992
+ process.exitCode = 1;
12993
+ return;
13107
12994
  }
13108
- } catch {
12995
+ process.stdout.write(stringifyYaml2(ws));
12996
+ } catch (err) {
12997
+ printError(err instanceof Error ? err.message : String(err));
12998
+ process.exitCode = 1;
13109
12999
  }
13110
- return { url: candidateUrl, mode };
13111
- }
13112
- try {
13113
- const container = await findHostCpContainer();
13114
- if (container && container.state === "running") {
13115
- try {
13116
- const res = await fetch(`${candidateUrl}/api/bootstrap`, {
13117
- signal: AbortSignal.timeout(2e3)
13118
- });
13119
- if (res.ok) {
13120
- return { url: candidateUrl, mode: "container" };
13000
+ });
13001
+ workspace.command("remove").description("Delete a workspace (does NOT touch worlds that already referenced it)").argument("<name>", "Workspace name").option("--force", "Skip confirmation", false).action((name, _opts) => {
13002
+ try {
13003
+ if (removeWorkspace(name)) {
13004
+ printSuccess(`Removed workspace "${name}"`);
13005
+ } else {
13006
+ printWorkspaceNotFound(name);
13007
+ process.exitCode = 1;
13008
+ }
13009
+ } catch (err) {
13010
+ printError(err instanceof Error ? err.message : String(err));
13011
+ process.exitCode = 1;
13012
+ }
13013
+ });
13014
+ workspace.command("add").description("Create a workspace from --from-config (reads current .olam/config.yaml) or repeated --repo flags").argument("<name>", "Workspace name").option("--from-config", "Seed from the repos in the current project's .olam/config.yaml", false).option("--repo <url>", "Repeatable. Format: <url> or <url>#<branch>", (val, acc) => {
13015
+ acc.push(parseRepoFlag(val));
13016
+ return acc;
13017
+ }, []).option("--default-branch <branch>", "Fallback branch for repos that don't specify one").option("--force", "Overwrite an existing workspace with the same name", false).action((name, opts) => {
13018
+ try {
13019
+ const repos = [...opts.repo];
13020
+ if (opts.fromConfig) {
13021
+ try {
13022
+ const cfg = loadConfig(process.cwd());
13023
+ for (const r of cfg.repos) {
13024
+ repos.push({
13025
+ name: r.name,
13026
+ url: r.url,
13027
+ ...r.submodules ? { submodules: true } : {}
13028
+ });
13029
+ }
13030
+ } catch (err) {
13031
+ printError(`--from-config: ${err instanceof Error ? err.message : String(err)}`);
13032
+ process.exitCode = 1;
13033
+ return;
13121
13034
  }
13122
- } catch {
13123
13035
  }
13036
+ if (repos.length === 0) {
13037
+ printError("No repos provided \u2014 pass --from-config or at least one --repo");
13038
+ process.exitCode = 1;
13039
+ return;
13040
+ }
13041
+ const ws = {
13042
+ name,
13043
+ repos,
13044
+ ...opts.defaultBranch ? { defaults: { branch: opts.defaultBranch } } : {},
13045
+ updatedAt: Date.now()
13046
+ };
13047
+ writeWorkspace(ws, { force: opts.force });
13048
+ const file = path4.join(workspacesDir(), `${name}.yaml`);
13049
+ printSuccess(`Created workspace "${name}" (${repos.length} repo${repos.length === 1 ? "" : "s"})`);
13050
+ printInfo("File", file);
13051
+ printInfo("Next", `olam create --name <world> --workspace ${name} --task "..."`);
13052
+ } catch (err) {
13053
+ if (err instanceof WorkspaceExistsError || err instanceof WorkspaceNameError) {
13054
+ printError(err.message);
13055
+ process.exitCode = 1;
13056
+ return;
13057
+ }
13058
+ printError(err instanceof Error ? err.message : String(err));
13059
+ process.exitCode = 1;
13124
13060
  }
13125
- } catch {
13126
- }
13127
- return null;
13061
+ });
13128
13062
  }
13129
- async function gatherProbeFailureDiagnostics() {
13130
- let bootstrapStatus = "no response";
13131
- try {
13132
- const res = await fetch(`http://127.0.0.1:${HOST_CP_PORT}/api/bootstrap`, {
13133
- signal: AbortSignal.timeout(2e3)
13134
- });
13135
- bootstrapStatus = `HTTP ${res.status}`;
13136
- } catch (err) {
13137
- bootstrapStatus = err instanceof Error ? err.message : "connection refused";
13138
- }
13139
- let containerStatus = "not found";
13140
- try {
13141
- const container = await findHostCpContainer();
13142
- if (!container) {
13143
- containerStatus = "not found";
13144
- } else {
13145
- containerStatus = `found (state: ${container.state})`;
13063
+ function ensureProjectWorkspaceFromConfig(projectRoot, workspaceName) {
13064
+ const existing = (() => {
13065
+ try {
13066
+ return readWorkspace(workspaceName);
13067
+ } catch {
13068
+ return null;
13146
13069
  }
13147
- } catch {
13148
- containerStatus = "docker not available";
13149
- }
13150
- return { bootstrapStatus, containerStatus };
13151
- }
13152
- async function probeHealth() {
13153
- try {
13154
- const res = await fetch(`http://127.0.0.1:${HOST_CP_PORT}/health`, {
13155
- signal: AbortSignal.timeout(2e3)
13156
- });
13157
- if (!res.ok) return null;
13158
- return await res.json();
13159
- } catch {
13160
- return null;
13070
+ })();
13071
+ if (existing) {
13072
+ return { created: false, file: path4.join(workspacesDir(), `${workspaceName}.yaml`) };
13161
13073
  }
13162
- }
13163
- function runCompose(args, composeFile, extraEnv = {}) {
13164
- const result = spawnSync4("docker", ["compose", "-f", composeFile, ...args], {
13165
- encoding: "utf-8",
13166
- stdio: ["ignore", "pipe", "pipe"],
13167
- env: { ...process.env, ...extraEnv }
13168
- });
13169
- return {
13170
- ok: result.status === 0,
13171
- stdout: result.stdout ?? "",
13172
- stderr: result.stderr ?? ""
13074
+ const cfg = loadConfig(projectRoot);
13075
+ const repos = cfg.repos.map((r) => ({
13076
+ name: r.name,
13077
+ url: r.url,
13078
+ ...r.submodules ? { submodules: true } : {}
13079
+ }));
13080
+ const ws = {
13081
+ name: workspaceName,
13082
+ repos,
13083
+ updatedAt: Date.now()
13173
13084
  };
13085
+ writeWorkspace(ws);
13086
+ const file = path4.join(workspacesDir(), `${workspaceName}.yaml`);
13087
+ return { created: true, file };
13174
13088
  }
13175
- function buildComposeEnv(authSecret, ghToken) {
13176
- const env = {};
13177
- if (authSecret !== null && authSecret.length > 0) {
13178
- env.OLAM_AUTH_SECRET = authSecret;
13179
- }
13180
- if (ghToken != null && ghToken.length > 0) {
13181
- env.GH_TOKEN = ghToken;
13182
- }
13183
- return env;
13089
+
13090
+ // src/commands/init.ts
13091
+ function detectProjectType(root) {
13092
+ if (fs5.existsSync(path5.join(root, "Gemfile")) || fs5.existsSync(path5.join(root, "config", "routes.rb"))) return "rails";
13093
+ if (fs5.existsSync(path5.join(root, "package.json"))) return "node";
13094
+ if (fs5.existsSync(path5.join(root, "pyproject.toml")) || fs5.existsSync(path5.join(root, "requirements.txt"))) return "python";
13095
+ return "generic";
13184
13096
  }
13185
- function captureGhToken() {
13097
+ function getRepoName(root) {
13186
13098
  try {
13187
- const result = spawnSync4("gh", ["auth", "token"], {
13188
- encoding: "utf-8",
13189
- stdio: ["ignore", "pipe", "pipe"]
13190
- });
13191
- if (result.status === 0) {
13192
- const token = (result.stdout ?? "").trim();
13193
- return token.length > 0 ? token : null;
13194
- }
13195
- return null;
13099
+ const url = execSync("git remote get-url origin", {
13100
+ cwd: root,
13101
+ encoding: "utf-8"
13102
+ }).trim();
13103
+ const match2 = url.match(/\/([^/]+?)(?:\.git)?$/);
13104
+ if (match2) return match2[1];
13196
13105
  } catch {
13197
- return null;
13198
13106
  }
13107
+ return path5.basename(root);
13199
13108
  }
13200
- async function handleStart(opts) {
13201
- const existing = await findHostCpContainer();
13202
- if (existing && existing.state === "running") {
13203
- const health = await probeHealth();
13204
- if (health) {
13205
- printSuccess(`Host CP already running at http://127.0.0.1:${HOST_CP_PORT}`);
13206
- printInfo("Container", existing.id);
13207
- printInfo("Uptime", String(health["uptime_seconds"] ?? "unknown") + "s");
13208
- return;
13209
- }
13210
- printWarning("Host CP container running but /health not responding. Wait a few seconds and retry, or stop+start.");
13211
- return;
13109
+ function findProjectRoot(startDir) {
13110
+ let current = path5.resolve(startDir);
13111
+ while (true) {
13112
+ if (fs5.existsSync(path5.join(current, ".git"))) return current;
13113
+ const parent = path5.dirname(current);
13114
+ if (parent === current) return startDir;
13115
+ current = parent;
13212
13116
  }
13213
- try {
13214
- const docker2 = new Dockerode2(resolveDockerHostOptions());
13215
- await auditPortsForZombies(docker2, [HOST_CP_PORT]);
13216
- } catch (err) {
13217
- if (err instanceof PortHeldByZombieError) {
13218
- printError(`Port ${HOST_CP_PORT} held by zombie container "${err.containerName}" (state: ${err.state}).`);
13219
- printError(`Run: docker rm ${err.containerName}`);
13117
+ }
13118
+ function registerInit(program2) {
13119
+ program2.command("init").description("Initialize olam in the current project").option("--path <path>", "Project root path", process.cwd()).option("--skip-pleri", "Skip Pleri setup").action(async (opts) => {
13120
+ try {
13121
+ const projectRoot = findProjectRoot(opts.path);
13122
+ const olamDir = path5.join(projectRoot, ".olam");
13123
+ if (fs5.existsSync(path5.join(olamDir, "config.yaml"))) {
13124
+ printError(`Already initialized at ${olamDir}/config.yaml`);
13125
+ process.exitCode = 1;
13126
+ return;
13127
+ }
13128
+ const projectType = detectProjectType(projectRoot);
13129
+ const repoName = getRepoName(projectRoot);
13130
+ let remoteUrl;
13131
+ try {
13132
+ remoteUrl = execSync("git remote get-url origin", {
13133
+ cwd: projectRoot,
13134
+ encoding: "utf-8"
13135
+ }).trim();
13136
+ } catch {
13137
+ remoteUrl = `git@github.com:your-org/${repoName}.git`;
13138
+ }
13139
+ fs5.mkdirSync(path5.join(olamDir, "state"), { recursive: true });
13140
+ fs5.mkdirSync(path5.join(olamDir, "thoughts"), { recursive: true });
13141
+ const pleriSection = opts.skipPleri ? "# pleri:\n# base_url: ${PLERI_BASE_URL}\n# plane_id: ${PLERI_PLANE_ID}\n# api_key: ${PLERI_API_KEY}\n" : "pleri:\n base_url: ${PLERI_BASE_URL}\n plane_id: ${PLERI_PLANE_ID}\n api_key: ${PLERI_API_KEY}\n";
13142
+ const config = [
13143
+ "version: 2",
13144
+ "",
13145
+ pleriSection,
13146
+ "repos:",
13147
+ ` - name: ${repoName}`,
13148
+ ` url: ${remoteUrl}`,
13149
+ ` path: ${projectRoot}`,
13150
+ ` type: ${projectType}`,
13151
+ " services: []",
13152
+ "",
13153
+ "compute:",
13154
+ " default: docker",
13155
+ "",
13156
+ "cost:",
13157
+ " # All values in USD (Anthropic's billing currency).",
13158
+ " # Convert from your local currency: USD 25 \u2248 SGD 33 / EUR 23 / GBP 20",
13159
+ " # at typical 2026 rates. Dashboard display localization is a future feature.",
13160
+ " max_per_world_usd: 25",
13161
+ " max_daily_usd: 100",
13162
+ " warning_threshold: 0.8",
13163
+ "",
13164
+ "auth:",
13165
+ " mode: oauth",
13166
+ ""
13167
+ ].join("\n");
13168
+ fs5.writeFileSync(path5.join(olamDir, "config.yaml"), config);
13169
+ const envExample = [
13170
+ "# Pleri credentials",
13171
+ "PLERI_BASE_URL=https://pleri.dev/api",
13172
+ "PLERI_PLANE_ID=",
13173
+ "PLERI_API_KEY=",
13174
+ ""
13175
+ ].join("\n");
13176
+ fs5.writeFileSync(path5.join(olamDir, ".env.example"), envExample);
13177
+ printHeader("Olam initialized");
13178
+ printInfo("Config", `${olamDir}/config.yaml`);
13179
+ printInfo("Project", `${projectType} (detected)`);
13180
+ printInfo("Repo", repoName);
13181
+ try {
13182
+ const result = ensureProjectWorkspaceFromConfig(projectRoot, repoName);
13183
+ if (result.created) {
13184
+ printInfo("Workspace", `${repoName} \u2192 ${result.file}`);
13185
+ } else {
13186
+ printInfo("Workspace", `${repoName} (already registered)`);
13187
+ }
13188
+ } catch (err) {
13189
+ printError(`Workspace auto-register failed: ${err instanceof Error ? err.message : String(err)}`);
13190
+ }
13191
+ console.log(`
13192
+ ${pc3.dim(`Next: olam create --name my-world --workspace ${repoName} --task "..."`)}`);
13193
+ } catch (err) {
13194
+ printError(err instanceof Error ? err.message : String(err));
13220
13195
  process.exitCode = 1;
13221
- return;
13222
13196
  }
13223
- throw err;
13224
- }
13225
- const token = writeToken();
13226
- const composeFile = findComposeFile();
13227
- if (!fs19.existsSync(composeFile)) {
13228
- printError(`compose.yaml not found at ${composeFile}. Run from the olam project root.`);
13229
- removeToken();
13230
- process.exitCode = 1;
13231
- return;
13232
- }
13233
- const authSecret = readAuthSecret2();
13234
- if (authSecret === null) {
13235
- printWarning(
13236
- `${authSecretPath()} not found or empty. host-cp will boot, but credential surfaces (auth fleet, hotswap) will fail with 401 until you run \`olam auth up\` to (re)generate the shared secret.`
13197
+ });
13198
+ }
13199
+
13200
+ // src/commands/install.ts
13201
+ import * as fs6 from "node:fs";
13202
+ import * as os3 from "node:os";
13203
+ import * as path6 from "node:path";
13204
+ import pc4 from "picocolors";
13205
+ import { stringify as stringifyYaml3 } from "yaml";
13206
+
13207
+ // ../core/src/archetypes/capabilities.ts
13208
+ var CAPABILITY_NAMES = [
13209
+ // World operations
13210
+ "world.create",
13211
+ "world.operate",
13212
+ "world.destroy",
13213
+ // Workspace catalog
13214
+ "workspace.read",
13215
+ "workspace.write",
13216
+ // Auth container + credentials. Phase 6b.2/A5: `auth.manage`
13217
+ // removed — its only consumer (/auth/revoke) was reclassified to
13218
+ // `auth.login` (self-managed auth flow) in A2, and the only
13219
+ // archetype that held it (`bootstrapper`) is also removed below.
13220
+ "auth.login",
13221
+ "auth.read",
13222
+ // PR gate
13223
+ "pr-gate.read",
13224
+ "pr-gate.decide",
13225
+ // Policy
13226
+ "policy.set",
13227
+ // Phase 6b.2/A5: `role.manage` removed — Pylon owns role grants
13228
+ // (`pylon role grant ...` from a user terminal). The `/api/roles*`
13229
+ // route handlers were deleted in A2.
13230
+ // Phase 6b.2/A5: `bootstrap.install` and `bootstrap.cf-deploy`
13231
+ // removed — `/bootstrap` route handler deleted in A2; the
13232
+ // `bootstrapper` archetype is also removed in this commit.
13233
+ // Dev-only (repo clone privileges)
13234
+ "dev.build",
13235
+ "dev.test",
13236
+ "dev.release"
13237
+ ];
13238
+ var CAPABILITY_SET = new Set(CAPABILITY_NAMES);
13239
+ function isCapability(value) {
13240
+ return CAPABILITY_SET.has(value);
13241
+ }
13242
+ function assertCapability(value) {
13243
+ if (!isCapability(value)) {
13244
+ throw new Error(
13245
+ `Unknown capability "${value}". Valid capabilities: ${CAPABILITY_NAMES.join(", ")}`
13237
13246
  );
13238
13247
  }
13239
- const ghToken = captureGhToken();
13240
- if (ghToken === null) {
13241
- printWarning(
13242
- "GitHub CLI not authenticated; PR badges will not appear in the inbox. Run `gh auth login` then `olam host-cp restart`."
13248
+ return value;
13249
+ }
13250
+
13251
+ // ../core/src/archetypes/registry.ts
13252
+ var ARCHETYPES = [
13253
+ {
13254
+ name: "user",
13255
+ description: "Day-to-day world operator. Creates, runs, and tears down worlds; decides PR gates; reads the workspace catalog.",
13256
+ capabilities: [
13257
+ "world.create",
13258
+ "world.operate",
13259
+ "world.destroy",
13260
+ "workspace.read",
13261
+ "auth.login",
13262
+ "auth.read",
13263
+ "pr-gate.read",
13264
+ "pr-gate.decide"
13265
+ ]
13266
+ },
13267
+ {
13268
+ name: "workspace-curator",
13269
+ description: "User who also curates the workspace catalog. Typical: team lead scoping their squad's repo bundles.",
13270
+ inherits: ["user"],
13271
+ capabilities: ["workspace.write"]
13272
+ },
13273
+ {
13274
+ name: "policy-admin",
13275
+ description: "User who also sets deployment policy (permission mode, PR-gate defaults). No catalog or role authority.",
13276
+ inherits: ["user"],
13277
+ capabilities: ["policy.set"]
13278
+ },
13279
+ // Phase 6b.2/A5: `bootstrapper` archetype removed. Its capabilities
13280
+ // (bootstrap.install, bootstrap.cf-deploy, auth.manage) are no
13281
+ // longer enforced — `/bootstrap` route + `auth.manage` route
13282
+ // gating are gone (deleted in A2). The trim keeps `admin` valid
13283
+ // (no dangling inherits ref) and lets schema push succeed.
13284
+ {
13285
+ name: "admin",
13286
+ description: "Full operational admin. Unions user + workspace-curator + policy-admin. Pylon owns role grants now (no role.manage).",
13287
+ inherits: ["user", "workspace-curator", "policy-admin"],
13288
+ capabilities: []
13289
+ },
13290
+ {
13291
+ name: "dev",
13292
+ description: "Contributor to Olam itself. Everything an admin has, plus repo-local dev operations (build, test, release).",
13293
+ inherits: ["admin"],
13294
+ capabilities: ["dev.build", "dev.test", "dev.release"]
13295
+ }
13296
+ ];
13297
+ var ARCHETYPE_BY_NAME = new Map(
13298
+ ARCHETYPES.map((a) => [a.name, a])
13299
+ );
13300
+ function getArchetype(name) {
13301
+ return ARCHETYPE_BY_NAME.get(name);
13302
+ }
13303
+ function listArchetypeNames() {
13304
+ return ARCHETYPES.map((a) => a.name);
13305
+ }
13306
+
13307
+ // ../core/src/archetypes/expand.ts
13308
+ var UnknownArchetypeError = class extends Error {
13309
+ constructor(name, known) {
13310
+ super(
13311
+ `Unknown archetype "${name}". Known archetypes: ${known.join(", ")}`
13243
13312
  );
13313
+ this.name = name;
13314
+ this.known = known;
13315
+ this.name = "UnknownArchetypeError";
13244
13316
  }
13245
- const composeEnv = buildComposeEnv(authSecret, ghToken);
13246
- const PULL_BACKOFF_MS = [0, 1e3, 3e3];
13247
- let pullOk = false;
13248
- let lastPullStderr = "";
13249
- for (let attempt = 0; attempt < PULL_BACKOFF_MS.length; attempt += 1) {
13250
- if (PULL_BACKOFF_MS[attempt] > 0) {
13251
- await new Promise((r) => setTimeout(r, PULL_BACKOFF_MS[attempt]));
13252
- }
13253
- const pullResult = runCompose(
13254
- ["pull", "--quiet", "docker-socket-proxy"],
13255
- composeFile,
13256
- composeEnv
13317
+ name;
13318
+ known;
13319
+ };
13320
+ var ArchetypeCycleError = class extends Error {
13321
+ constructor(path42) {
13322
+ super(
13323
+ `Archetype inheritance cycle detected: ${path42.join(" \u2192 ")} \u2192 ${path42[0] ?? "?"}`
13257
13324
  );
13258
- if (pullResult.ok) {
13259
- pullOk = true;
13260
- break;
13261
- }
13262
- lastPullStderr = pullResult.stderr;
13263
- }
13264
- if (!pullOk) {
13265
- printError("docker compose pull docker-socket-proxy failed after 3 attempts");
13266
- process.stderr.write(lastPullStderr);
13267
- removeToken();
13268
- process.exitCode = 1;
13269
- return;
13270
- }
13271
- const result = runCompose(["up", "-d"], composeFile, composeEnv);
13272
- if (!result.ok) {
13273
- printError("docker compose up failed");
13274
- process.stderr.write(result.stderr);
13275
- removeToken();
13276
- process.exitCode = 1;
13277
- return;
13278
- }
13279
- const deadline = Date.now() + 1e4;
13280
- let healthy = false;
13281
- while (Date.now() < deadline) {
13282
- const h = await probeHealth();
13283
- if (h) {
13284
- healthy = true;
13285
- break;
13286
- }
13287
- await new Promise((r) => setTimeout(r, 500));
13288
- }
13289
- if (!healthy) {
13290
- printWarning("Host CP started but /health did not respond within 10s. Check `docker compose logs host-cp`.");
13291
- }
13292
- const container = await findHostCpContainer();
13293
- if (container) {
13294
- writePid(1);
13295
- }
13296
- printSuccess(`Host CP running at http://127.0.0.1:${HOST_CP_PORT}`);
13297
- if (opts.showToken) {
13298
- printInfo("Token", token);
13299
- } else {
13300
- printInfo("Token", `(written to ${tokenPath()}; pass --show-token to print)`);
13325
+ this.path = path42;
13326
+ this.name = "ArchetypeCycleError";
13301
13327
  }
13302
- printInfo("Open", `http://127.0.0.1:${HOST_CP_PORT}`);
13328
+ path;
13329
+ };
13330
+ function expandArchetype(name) {
13331
+ const out = /* @__PURE__ */ new Set();
13332
+ walk(name, out, []);
13333
+ return out;
13303
13334
  }
13304
- async function handleStop() {
13305
- const composeFile = findComposeFile();
13306
- if (!fs19.existsSync(composeFile)) {
13307
- printWarning(`compose.yaml not found at ${composeFile}. Cleaning up token + PID anyway.`);
13308
- removeToken();
13309
- removePid();
13310
- return;
13311
- }
13312
- const existing = await findHostCpContainer();
13313
- if (!existing) {
13314
- printInfo("Host CP", "not running");
13315
- removeToken();
13316
- removePid();
13317
- return;
13335
+ function walk(name, acc, stack) {
13336
+ if (stack.includes(name)) {
13337
+ throw new ArchetypeCycleError([...stack, name]);
13318
13338
  }
13319
- const result = runCompose(["down"], composeFile);
13320
- if (!result.ok) {
13321
- printError("docker compose down failed");
13322
- process.stderr.write(result.stderr);
13323
- process.exitCode = 1;
13324
- return;
13339
+ const arch2 = getArchetype(name);
13340
+ if (!arch2) {
13341
+ throw new UnknownArchetypeError(name, listArchetypeNames());
13325
13342
  }
13326
- removeToken();
13327
- removePid();
13328
- printSuccess("Host CP stopped");
13329
- }
13330
- async function buildStatusReport() {
13331
- const container = await findHostCpContainer();
13332
- const health = await probeHealth();
13333
- const tokenFile = tokenPath();
13334
- const tokenPresent = fs19.existsSync(tokenFile);
13335
- let tokenModeOk = false;
13336
- if (tokenPresent) {
13337
- const mode = fs19.statSync(tokenFile).mode & 511;
13338
- tokenModeOk = mode === 384;
13343
+ const nextStack = [...stack, name];
13344
+ for (const parent of arch2.inherits ?? []) {
13345
+ walk(parent, acc, nextStack);
13339
13346
  }
13340
- const pidPresent = fs19.existsSync(pidPath());
13341
- let stack;
13342
- if (!container) {
13343
- stack = "not_started";
13344
- } else if (container.state === "running" && health) {
13345
- stack = "running";
13346
- } else if (container.state === "running") {
13347
- stack = "partial";
13348
- } else {
13349
- stack = "stopped";
13347
+ for (const cap of arch2.capabilities) {
13348
+ acc.add(cap);
13350
13349
  }
13351
- return {
13352
- stack,
13353
- container,
13354
- health,
13355
- token_present: tokenPresent,
13356
- token_mode_ok: tokenModeOk,
13357
- pid_present: pidPresent,
13358
- url: `http://127.0.0.1:${HOST_CP_PORT}`
13359
- };
13360
13350
  }
13361
- async function handleStatus(opts) {
13362
- const report = await buildStatusReport();
13363
- if (opts.json) {
13364
- process.stdout.write(JSON.stringify(report, null, 2) + "\n");
13365
- process.exitCode = report.stack === "running" ? 0 : 1;
13366
- return;
13367
- }
13368
- printHeader("Host CP Status");
13369
- printInfo("Stack", report.stack);
13370
- printInfo("URL", report.url);
13371
- if (report.container) {
13372
- printInfo("Container", `${report.container.id} (${report.container.state})`);
13373
- printInfo("Status line", report.container.status);
13374
- } else {
13375
- printInfo("Container", "not found (run `olam host-cp start`)");
13376
- }
13377
- if (report.health) {
13378
- printInfo("Health", "ok");
13379
- printInfo("Uptime", String(report.health["uptime_seconds"] ?? "unknown") + "s");
13380
- const cache = report.health["cache"];
13381
- if (cache) {
13382
- printInfo("Cached worlds", String(cache.worlds?.length ?? 0));
13383
- printInfo("Cache TTL", `${cache.ttl_sec ?? "unknown"}s`);
13384
- }
13385
- const sse = report.health["sse"];
13386
- if (sse) {
13387
- printInfo("SSE active", `${sse.active ?? 0} / ${sse.cap ?? 0}`);
13351
+
13352
+ // src/commands/install.ts
13353
+ init_output();
13354
+ var ROLE_FILE_PATH = path6.join(os3.homedir(), ".olam", "role.yaml");
13355
+ function readRoleFile() {
13356
+ if (!fs6.existsSync(ROLE_FILE_PATH)) return null;
13357
+ try {
13358
+ const raw = fs6.readFileSync(ROLE_FILE_PATH, "utf-8");
13359
+ const m = /archetype:\s*(\S+)/.exec(raw);
13360
+ if (!m) return null;
13361
+ const archetype = m[1];
13362
+ const customCapabilities = [];
13363
+ for (const line of raw.split("\n")) {
13364
+ const caps = /^\s+-\s+(\S+)$/.exec(line);
13365
+ if (caps) customCapabilities.push(caps[1]);
13388
13366
  }
13389
- } else {
13390
- printInfo("Health", "not responding");
13367
+ const installedAtMatch = /installedAt:\s*(\d+)/.exec(raw);
13368
+ return {
13369
+ archetype,
13370
+ customCapabilities,
13371
+ installedAt: installedAtMatch ? Number(installedAtMatch[1]) : 0
13372
+ };
13373
+ } catch {
13374
+ return null;
13391
13375
  }
13392
- printInfo("Token file", report.token_present ? report.token_mode_ok ? "present (mode 600)" : "present (BAD MODE \u2014 should be 600)" : "absent");
13393
- printInfo("PID file", report.pid_present ? "present" : "absent");
13394
- process.exitCode = report.stack === "running" ? 0 : 1;
13395
13376
  }
13396
- function registerHostCp(program2) {
13397
- const hostCp = program2.command("host-cp").description("Manage the Olam host control plane container");
13398
- hostCp.command("start").description("Start the host CP container (token regenerated each call)").option("--show-token", "Print the generated token to stdout (default: hide)").action(async (opts) => {
13399
- await handleStart({ showToken: opts.showToken === true });
13400
- });
13401
- hostCp.command("stop").description("Stop the host CP container + remove token + PID files").action(async () => {
13402
- await handleStop();
13403
- });
13404
- hostCp.command("status").description("Show host CP container + health diagnostics").option("--json", "Output as JSON (machine-parseable; sets exit code)").action(async (opts) => {
13405
- await handleStatus({ json: opts.json === true });
13406
- });
13407
- hostCp.command("register").description("Register a world with the running host CP so it appears in the unified UI").requiredOption("--world <id>", "World id (the docker container suffix, e.g. gold-arc-1454)").option("--port <port>", "Override per-world CP port; default: discovered from `olam list`").action(async (opts) => {
13408
- await handleRegister({ world: opts.world, port: opts.port });
13409
- });
13410
- hostCp.command("deregister").description("Remove a world from the host CP registry (does NOT destroy the world)").requiredOption("--world <id>", "World id to remove").action(async (opts) => {
13411
- await handleDeregister({ world: opts.world });
13377
+ function writeRoleFile(role) {
13378
+ fs6.mkdirSync(path6.dirname(ROLE_FILE_PATH), { recursive: true });
13379
+ const yaml = stringifyYaml3({
13380
+ archetype: role.archetype,
13381
+ customCapabilities: [...role.customCapabilities],
13382
+ installedAt: role.installedAt
13412
13383
  });
13384
+ fs6.writeFileSync(ROLE_FILE_PATH, yaml, { mode: 420 });
13413
13385
  }
13414
- async function discoverWorldPort(worldId) {
13415
- try {
13416
- const { loadContext: loadContext2 } = await Promise.resolve().then(() => (init_context(), context_exports));
13417
- const { ctx } = await loadContext2();
13418
- if (!ctx) return null;
13419
- const world = await ctx.worldManager.getWorld(worldId);
13420
- if (!world) return null;
13421
- return 19080 + world.portOffset;
13422
- } catch {
13423
- return null;
13386
+ function nextStepsFor(archetype) {
13387
+ switch (archetype) {
13388
+ case "user":
13389
+ return [
13390
+ 'You can now run `olam create --task "..."` against a workspace your admin has curated.',
13391
+ "Register the Claude Code plugin if you haven't: `claude plugin install ./plugin`."
13392
+ ];
13393
+ case "workspace-curator":
13394
+ return [
13395
+ "You can now curate the catalog: `olam workspace add <name> --from-config`.",
13396
+ "Share workspace YAML files via your usual dotfiles / rsync flow."
13397
+ ];
13398
+ case "policy-admin":
13399
+ return [
13400
+ "Set deployment-wide policy via env vars (OLAM_CLAUDE_PERMISSION_MODE) or wrangler secrets.",
13401
+ "PR-gate defaults can be overridden per-workspace under `policy.set`."
13402
+ ];
13403
+ case "bootstrapper":
13404
+ return [
13405
+ "Start the auth container: `olam auth up`, then `olam auth login`.",
13406
+ "For Cloudflare deployments: `cd packages/cloudflare-worker && pnpm wrangler deploy`."
13407
+ ];
13408
+ case "admin":
13409
+ return [
13410
+ "Start the auth container: `olam auth up`, then `olam auth login`.",
13411
+ "Curate workspaces: `olam workspace add <name> --from-config`.",
13412
+ "Assign narrower archetypes to teammates (CF side, once slice C lands): use `role.manage`."
13413
+ ];
13414
+ case "dev":
13415
+ return [
13416
+ "You have repo-local privileges. Run `npm run build:ci && npm run test:ci`.",
13417
+ "Everything an admin has is available."
13418
+ ];
13419
+ default:
13420
+ return [];
13424
13421
  }
13425
13422
  }
13426
- async function readHostCpToken2() {
13427
- const tp = tokenPath();
13428
- if (!fs19.existsSync(tp)) return null;
13429
- return fs19.readFileSync(tp, "utf-8").trim();
13430
- }
13431
- async function callHostCpProxy(method, worldId, path40, body) {
13432
- const token = await readHostCpToken2();
13433
- if (!token) return { ok: false, status: 0, error: "no token (host CP not started)" };
13434
- const url = `http://127.0.0.1:${HOST_CP_PORT}/api/world/${encodeURIComponent(worldId)}${path40}`;
13435
- try {
13436
- const headers = {
13437
- Authorization: `Bearer ${token}`
13438
- };
13439
- if (body !== void 0) headers["Content-Type"] = "application/json";
13440
- const res = await fetch(url, {
13441
- method,
13442
- headers,
13443
- ...body !== void 0 ? { body: JSON.stringify(body) } : {}
13444
- });
13445
- if (!res.ok) {
13446
- const text = await res.text().catch(() => "");
13447
- let errMsg = text || `HTTP ${res.status}`;
13423
+ function registerInstall(program2) {
13424
+ program2.command("install").description("Pick an archetype preset for this Olam install").option("--as <archetype>", "Archetype name (user, workspace-curator, policy-admin, bootstrapper, admin, dev)").option(
13425
+ "--capability <name>",
13426
+ "Additional capability to grant beyond the archetype preset (repeatable)",
13427
+ (value, acc) => {
13428
+ acc.push(value);
13429
+ return acc;
13430
+ },
13431
+ []
13432
+ ).option("--force", "Overwrite an existing role.yaml", false).option("--show", "Print the current install's archetype and exit", false).action((opts) => {
13433
+ if (opts.show) {
13434
+ const existing2 = readRoleFile();
13435
+ if (!existing2) {
13436
+ console.log(pc4.dim(`No install recorded at ${ROLE_FILE_PATH}`));
13437
+ return;
13438
+ }
13439
+ printHeader("Current install");
13440
+ printInfo("Archetype", existing2.archetype);
13441
+ printInfo("File", ROLE_FILE_PATH);
13442
+ const caps = expandArchetype(existing2.archetype);
13443
+ for (const cap of existing2.customCapabilities) caps.add(cap);
13444
+ printHeader(`Effective capabilities (${caps.size})`);
13445
+ for (const cap of [...caps].sort()) {
13446
+ console.log(` ${cap}`);
13447
+ }
13448
+ return;
13449
+ }
13450
+ if (!opts.as) {
13451
+ printError("Missing --as <archetype>. Known archetypes:");
13452
+ for (const arch2 of ARCHETYPES) {
13453
+ console.log(` ${pc4.bold(arch2.name.padEnd(20))} ${pc4.dim(arch2.description)}`);
13454
+ }
13455
+ process.exitCode = 1;
13456
+ return;
13457
+ }
13458
+ const archetype = getArchetype(opts.as);
13459
+ if (!archetype) {
13460
+ printError(
13461
+ `Unknown archetype "${opts.as}". Known: ${listArchetypeNames().join(", ")}`
13462
+ );
13463
+ process.exitCode = 1;
13464
+ return;
13465
+ }
13466
+ const extras = [];
13467
+ for (const raw of opts.capability) {
13448
13468
  try {
13449
- const parsed = JSON.parse(text);
13450
- if (parsed && typeof parsed === "object" && "error" in parsed) {
13451
- errMsg = String(parsed.error);
13452
- }
13453
- } catch {
13469
+ extras.push(assertCapability(raw));
13470
+ } catch (err) {
13471
+ printError(err instanceof Error ? err.message : String(err));
13472
+ process.exitCode = 1;
13473
+ return;
13454
13474
  }
13455
- return { ok: false, status: res.status, error: errMsg };
13456
13475
  }
13457
- const data = await res.json().catch(() => null);
13458
- return { ok: true, status: res.status, data };
13459
- } catch (err) {
13460
- return {
13461
- ok: false,
13462
- status: 0,
13463
- error: err instanceof Error ? err.message : String(err)
13464
- };
13465
- }
13466
- }
13467
- async function callHostCpRegistry(method, body) {
13468
- const token = await readHostCpToken2();
13469
- if (!token) return { ok: false, status: 0, error: "no token (host CP not started)" };
13470
- const url = method === "DELETE" ? `http://127.0.0.1:${HOST_CP_PORT}/api/admin/registry/${encodeURIComponent(body.id)}` : `http://127.0.0.1:${HOST_CP_PORT}/api/admin/registry`;
13471
- try {
13472
- const res = await fetch(url, {
13473
- method,
13474
- headers: {
13475
- Authorization: `Bearer ${token}`,
13476
- ...method === "POST" ? { "Content-Type": "application/json" } : {}
13477
- },
13478
- ...method === "POST" ? { body: JSON.stringify(body) } : {}
13476
+ const existing = readRoleFile();
13477
+ if (existing && !opts.force) {
13478
+ printError(
13479
+ `Install already recorded as "${existing.archetype}" at ${ROLE_FILE_PATH}. Pass --force to overwrite.`
13480
+ );
13481
+ process.exitCode = 1;
13482
+ return;
13483
+ }
13484
+ let resolved;
13485
+ try {
13486
+ resolved = expandArchetype(archetype.name);
13487
+ } catch (err) {
13488
+ if (err instanceof UnknownArchetypeError) {
13489
+ printError(err.message);
13490
+ process.exitCode = 1;
13491
+ return;
13492
+ }
13493
+ throw err;
13494
+ }
13495
+ for (const extra of extras) resolved.add(extra);
13496
+ writeRoleFile({
13497
+ archetype: archetype.name,
13498
+ customCapabilities: extras,
13499
+ installedAt: Date.now()
13479
13500
  });
13480
- if (!res.ok) {
13481
- const text = await res.text().catch(() => "");
13482
- return { ok: false, status: res.status, error: text || `HTTP ${res.status}` };
13501
+ printHeader(`Installed as ${pc4.bold(archetype.name)}`);
13502
+ printInfo("File", ROLE_FILE_PATH);
13503
+ printInfo("Description", archetype.description);
13504
+ printInfo("Capabilities", `${resolved.size} total`);
13505
+ if (extras.length > 0) {
13506
+ printInfo("Added beyond preset", extras.join(", "));
13483
13507
  }
13484
- return { ok: true, status: res.status };
13485
- } catch (err) {
13486
- return {
13487
- ok: false,
13488
- status: 0,
13489
- error: err instanceof Error ? err.message : String(err)
13490
- };
13508
+ const steps = nextStepsFor(archetype.name);
13509
+ if (steps.length > 0) {
13510
+ printHeader("Next steps");
13511
+ for (const step of steps) console.log(` ${pc4.dim("\u2022")} ${step}`);
13512
+ }
13513
+ printSuccess("Done. Run `olam install --show` to see the full capability list.");
13514
+ });
13515
+ }
13516
+
13517
+ // src/commands/auth.ts
13518
+ init_auth();
13519
+ init_output();
13520
+ import pc8 from "picocolors";
13521
+ import * as readline from "node:readline/promises";
13522
+ import { spawn as spawn3 } from "node:child_process";
13523
+
13524
+ // src/commands/auth-status.ts
13525
+ import * as fs8 from "node:fs";
13526
+ import * as os5 from "node:os";
13527
+ import * as path9 from "node:path";
13528
+ import pc5 from "picocolors";
13529
+
13530
+ // ../auth-logic/dist/effective-state.js
13531
+ function effectiveState(account, now = Date.now()) {
13532
+ const persisted = account.state ?? (account.rateLimited ? "cooldown" : "active");
13533
+ if (persisted === "disabled")
13534
+ return "disabled";
13535
+ if (account.expiresAt != null && account.expiresAt <= now)
13536
+ return "expired";
13537
+ if (persisted === "cooldown" || persisted === "usage-capped") {
13538
+ const reset = account.rateLimitResetsAt ? new Date(account.rateLimitResetsAt).getTime() : 0;
13539
+ if (reset > 0 && reset <= now)
13540
+ return "active";
13541
+ return persisted;
13491
13542
  }
13543
+ return "active";
13492
13544
  }
13493
- async function handleRegister(opts) {
13494
- printHeader("Register world with host CP");
13495
- let port = null;
13496
- if (opts.port) {
13497
- port = parseInt(opts.port, 10);
13498
- if (!Number.isFinite(port) || port <= 0) {
13499
- printError(`Invalid --port value: ${opts.port}`);
13500
- process.exitCode = 1;
13501
- return;
13545
+
13546
+ // ../auth-logic/dist/pick-credential.js
13547
+ function pickCredential(accounts, now = Date.now()) {
13548
+ const active = accounts.filter((a) => effectiveState(a, now) === "active");
13549
+ if (active.length === 0)
13550
+ return null;
13551
+ return [...active].sort((a, b) => {
13552
+ const aCount = a.usage?.requestCount5h ?? 0;
13553
+ const bCount = b.usage?.requestCount5h ?? 0;
13554
+ if (aCount !== bCount)
13555
+ return aCount - bCount;
13556
+ const aLast = a.lastUsed ? new Date(a.lastUsed).getTime() : 0;
13557
+ const bLast = b.lastUsed ? new Date(b.lastUsed).getTime() : 0;
13558
+ return aLast - bLast;
13559
+ })[0] ?? null;
13560
+ }
13561
+
13562
+ // ../auth-logic/dist/next-cooldown-reset.js
13563
+ function nextCooldownReset(accounts, now = Date.now()) {
13564
+ const upcoming = accounts.filter((a) => effectiveState(a, now) === "cooldown").map((a) => a.rateLimitResetsAt ? new Date(a.rateLimitResetsAt).getTime() : 0).filter((ts) => ts > now).sort((a, b) => a - b);
13565
+ return upcoming.length > 0 ? new Date(upcoming[0]).toISOString() : null;
13566
+ }
13567
+
13568
+ // src/commands/auth-status.ts
13569
+ init_auth();
13570
+ init_output();
13571
+ init_exit_codes();
13572
+ var LOCAL_DATA_DIR = path9.join(os5.homedir(), ".olam", "auth-data");
13573
+ function localHHMM(isoStr) {
13574
+ const d = new Date(isoStr);
13575
+ return d.toLocaleTimeString(void 0, {
13576
+ hour: "2-digit",
13577
+ minute: "2-digit",
13578
+ hour12: false
13579
+ });
13580
+ }
13581
+ function daysAgoStr(expiresAt, now) {
13582
+ const diffDays = Math.floor((now - expiresAt) / (1e3 * 60 * 60 * 24));
13583
+ if (diffDays <= 0) return "expired today";
13584
+ return `expired ${diffDays} day${diffDays === 1 ? "" : "s"} ago`;
13585
+ }
13586
+ function trunc(s, maxLen) {
13587
+ return s.length > maxLen ? s.slice(0, maxLen) : s;
13588
+ }
13589
+ var STATE_PRIORITY = {
13590
+ active: 0,
13591
+ cooldown: 1,
13592
+ "usage-capped": 2,
13593
+ disabled: 3,
13594
+ expired: 4
13595
+ };
13596
+ function formatAuthStatus(accounts, now = Date.now()) {
13597
+ const picked = pickCredential(accounts, now);
13598
+ const rows = accounts.map((account) => {
13599
+ const state = effectiveState(account, now);
13600
+ const isPicked = picked != null && account.id === picked.id;
13601
+ const req5h = account.usage?.requestCount5h ?? 0;
13602
+ const last429 = account.usage?.last429At ? localHHMM(account.usage.last429At) : "never";
13603
+ let reason;
13604
+ if (isPicked) {
13605
+ reason = "\u2190 selected";
13606
+ } else if (state === "active") {
13607
+ reason = `req5h=${req5h} (higher than candidate)`;
13608
+ } else if (state === "cooldown") {
13609
+ const resetTime = account.rateLimitResetsAt ? localHHMM(account.rateLimitResetsAt) : "?";
13610
+ reason = `cooldown until ${resetTime}`;
13611
+ } else if (state === "expired") {
13612
+ reason = daysAgoStr(account.expiresAt ?? 0, now);
13613
+ } else {
13614
+ reason = "disabled";
13502
13615
  }
13503
- } else {
13504
- port = await discoverWorldPort(opts.world);
13505
- if (port === null) {
13506
- printError(
13507
- `Could not discover port for world ${opts.world}. Pass --port explicitly or check that the world exists in \`olam list\`.`
13616
+ return { id: account.id, label: account.accountLabel ?? account.id, state, reason, req5h, last429, isPicked };
13617
+ });
13618
+ rows.sort((a, b) => {
13619
+ if (a.isPicked !== b.isPicked) return a.isPicked ? -1 : 1;
13620
+ return STATE_PRIORITY[a.state] - STATE_PRIORITY[b.state];
13621
+ });
13622
+ const COL = { id: 17, label: 17, state: 11, reason: 28, req5h: 7 };
13623
+ const lines = [];
13624
+ const hdr = "id".padEnd(COL.id) + "label".padEnd(COL.label) + "state".padEnd(COL.state) + "reason".padEnd(COL.reason) + "req5h".padEnd(COL.req5h) + "last429";
13625
+ lines.push(pc5.dim(hdr));
13626
+ lines.push(pc5.dim("-".repeat(hdr.length)));
13627
+ for (const row of rows) {
13628
+ const id = trunc(row.id, 16).padEnd(COL.id);
13629
+ const label = trunc(row.label, 16).padEnd(COL.label);
13630
+ const stateRaw = row.state.padEnd(COL.state);
13631
+ const stateColored = row.state === "active" ? pc5.green(stateRaw) : row.state === "cooldown" ? pc5.yellow(stateRaw) : row.state === "expired" ? pc5.red(stateRaw) : pc5.dim(stateRaw);
13632
+ const reason = row.reason.padEnd(COL.reason);
13633
+ const req5h = String(row.req5h).padEnd(COL.req5h);
13634
+ if (row.isPicked) {
13635
+ lines.push(
13636
+ pc5.bold(id) + pc5.bold(label) + stateColored + pc5.green(reason) + pc5.dim(req5h) + pc5.dim(row.last429)
13508
13637
  );
13509
- process.exitCode = 1;
13510
- return;
13638
+ } else {
13639
+ lines.push(id + label + stateColored + reason + pc5.dim(req5h) + pc5.dim(row.last429));
13511
13640
  }
13512
13641
  }
13513
- const result = await callHostCpRegistry("POST", { id: opts.world, port });
13514
- if (!result.ok) {
13515
- printError(`Register failed: ${result.error}`);
13516
- if (result.status === 0) {
13517
- printInfo("Hint", "Is host CP running? `olam host-cp status`");
13518
- }
13642
+ if (picked == null) {
13643
+ const resetIso = nextCooldownReset(accounts, now);
13644
+ lines.push("");
13645
+ lines.push(
13646
+ resetIso ? pc5.yellow(`Next reset: ${localHHMM(resetIso)}`) : pc5.dim("No reset scheduled")
13647
+ );
13648
+ return { output: lines.join("\n"), exitCode: EXIT_AUTH_NEEDS_ATTENTION };
13649
+ }
13650
+ return { output: lines.join("\n"), exitCode: 0 };
13651
+ }
13652
+ function toSafeAccount(a) {
13653
+ return {
13654
+ id: a.id,
13655
+ accountLabel: a.accountLabel,
13656
+ // expiresAt not exposed in summary — state is pre-computed server-side
13657
+ rateLimited: a.rateLimited,
13658
+ rateLimitResetsAt: a.rateLimitResetsAt,
13659
+ lastUsed: a.lastUsed,
13660
+ state: a.state,
13661
+ usage: a.usage ? { requestCount5h: a.usage.requestCount5h, last429At: a.usage.last429At } : void 0
13662
+ };
13663
+ }
13664
+ async function runAuthStatus(getStatus) {
13665
+ const fetchStatus = getStatus ?? (() => new AuthClient().status());
13666
+ let status;
13667
+ try {
13668
+ status = await fetchStatus();
13669
+ } catch {
13670
+ printError("Failed to contact auth service. Run `olam auth up` first.");
13519
13671
  process.exitCode = 1;
13520
13672
  return;
13521
13673
  }
13522
- printSuccess(`Registered ${opts.world} \u2192 :${port}`);
13523
- printInfo("UI", `http://127.0.0.1:${HOST_CP_PORT}/world/${encodeURIComponent(opts.world)}`);
13524
- }
13525
- async function handleDeregister(opts) {
13526
- printHeader("Deregister world from host CP");
13527
- const result = await callHostCpRegistry("DELETE", { id: opts.world });
13528
- if (!result.ok) {
13529
- printError(`Deregister failed: ${result.error}`);
13530
- if (result.status === 0) {
13531
- printInfo("Hint", "Is host CP running? `olam host-cp status`");
13532
- }
13674
+ if (!status.reachable) {
13675
+ printError("Auth container is not reachable. Run `olam auth up` first.");
13533
13676
  process.exitCode = 1;
13534
13677
  return;
13535
13678
  }
13536
- printSuccess(`Deregistered ${opts.world}`);
13679
+ if (status.accounts.length === 0) {
13680
+ console.log(pc5.dim("No credentials found. Run: olam auth login"));
13681
+ return;
13682
+ }
13683
+ const accounts = status.accounts.map(toSafeAccount);
13684
+ const result = formatAuthStatus(accounts);
13685
+ console.log(result.output);
13686
+ if (result.exitCode !== 0) {
13687
+ process.exitCode = result.exitCode;
13688
+ }
13537
13689
  }
13538
13690
 
13539
13691
  // src/commands/auth-upgrade.ts
13692
+ init_output();
13693
+ init_host_cp();
13540
13694
  init_auth();
13695
+ import * as fs20 from "node:fs";
13696
+ import * as path23 from "node:path";
13697
+ import { spawnSync as spawnSync7 } from "node:child_process";
13698
+ import ora2 from "ora";
13699
+ import pc7 from "picocolors";
13541
13700
 
13542
13701
  // src/commands/bootstrap.ts
13543
13702
  init_install_root();
13544
13703
  init_exit_codes();
13545
13704
  init_protocol_version();
13705
+ init_output();
13546
13706
  import { spawn as spawn2, spawnSync as spawnSync6 } from "node:child_process";
13547
13707
  import { existsSync as existsSync19, readFileSync as readFileSync15 } from "node:fs";
13548
13708
  import { join as join25 } from "node:path";
@@ -14063,9 +14223,9 @@ async function defaultRecreateAuth() {
14063
14223
  });
14064
14224
  const controller = new AuthContainerController();
14065
14225
  controller.start();
14066
- const healthy = await waitForAuthHealth(15e3);
14226
+ const healthy = await waitForAuthHealth(6e4);
14067
14227
  if (!healthy) {
14068
- return { ok: false, error: "auth-service /health did not respond within 15s" };
14228
+ return { ok: false, error: "auth-service /health did not respond within 60s" };
14069
14229
  }
14070
14230
  return { ok: true };
14071
14231
  } catch (err) {
@@ -14504,9 +14664,9 @@ function formatFreshnessWarning(result, image = DEFAULT_DEVBOX_IMAGE) {
14504
14664
  "These source files have changed since the image was built; the",
14505
14665
  "changes will NOT take effect in fresh worlds until you rebuild:"
14506
14666
  ];
14507
- for (const { path: path40, mtimeMs } of result.newerSources) {
14667
+ for (const { path: path42, mtimeMs } of result.newerSources) {
14508
14668
  const when = new Date(mtimeMs).toISOString();
14509
- lines.push(` \u2022 ${path40} (modified ${when})`);
14669
+ lines.push(` \u2022 ${path42} (modified ${when})`);
14510
14670
  }
14511
14671
  lines.push("");
14512
14672
  lines.push("Rebuild with:");
@@ -14663,18 +14823,20 @@ function decideWorkspaceMatch(input) {
14663
14823
 
14664
14824
  // src/commands/create.ts
14665
14825
  init_context();
14826
+ init_output();
14827
+ init_host_cp();
14666
14828
  var HOST_CP_URL = "http://127.0.0.1:19000";
14667
14829
  async function readHostCpTokenForCreate() {
14668
14830
  try {
14669
- const { default: fs36 } = await import("node:fs");
14670
- const { default: os21 } = await import("node:os");
14671
- const { default: path40 } = await import("node:path");
14672
- const tp = path40.join(
14673
- process.env.OLAM_HOME ?? path40.join(os21.homedir(), ".olam"),
14831
+ const { default: fs38 } = await import("node:fs");
14832
+ const { default: os22 } = await import("node:os");
14833
+ const { default: path42 } = await import("node:path");
14834
+ const tp = path42.join(
14835
+ process.env.OLAM_HOME ?? path42.join(os22.homedir(), ".olam"),
14674
14836
  "host-cp.token"
14675
14837
  );
14676
- if (!fs36.existsSync(tp)) return null;
14677
- return fs36.readFileSync(tp, "utf-8").trim();
14838
+ if (!fs38.existsSync(tp)) return null;
14839
+ return fs38.readFileSync(tp, "utf-8").trim();
14678
14840
  } catch {
14679
14841
  return null;
14680
14842
  }
@@ -15036,12 +15198,12 @@ function defaultNameFromPrompt(prompt) {
15036
15198
  }
15037
15199
  async function readHostCpToken3() {
15038
15200
  try {
15039
- const { default: fs36 } = await import("node:fs");
15040
- const { default: os21 } = await import("node:os");
15041
- const { default: path40 } = await import("node:path");
15042
- const tp = path40.join(os21.homedir(), ".olam", "host-cp.token");
15043
- if (!fs36.existsSync(tp)) return null;
15044
- const raw = fs36.readFileSync(tp, "utf-8").trim();
15201
+ const { default: fs38 } = await import("node:fs");
15202
+ const { default: os22 } = await import("node:os");
15203
+ const { default: path42 } = await import("node:path");
15204
+ const tp = path42.join(os22.homedir(), ".olam", "host-cp.token");
15205
+ if (!fs38.existsSync(tp)) return null;
15206
+ const raw = fs38.readFileSync(tp, "utf-8").trim();
15045
15207
  return raw.length > 0 ? raw : null;
15046
15208
  } catch {
15047
15209
  return null;
@@ -15084,6 +15246,7 @@ async function fetchHostCpExactMatches(projects) {
15084
15246
 
15085
15247
  // src/commands/dispatch.ts
15086
15248
  init_context();
15249
+ init_output();
15087
15250
  import ora4 from "ora";
15088
15251
  import pc10 from "picocolors";
15089
15252
 
@@ -15106,6 +15269,10 @@ function registerDispatch(program2) {
15106
15269
  process.exitCode = 1;
15107
15270
  return;
15108
15271
  }
15272
+ {
15273
+ const { checkVersionPin: checkVersionPin2 } = await Promise.resolve().then(() => (init_version_pin(), version_pin_exports));
15274
+ checkVersionPin2(worldId, worldMeta.workspacePath);
15275
+ }
15109
15276
  if (worldMeta.status !== "running") {
15110
15277
  printError(`World "${worldId}" is ${worldMeta.status}. Only running worlds accept dispatches.`);
15111
15278
  process.exitCode = 1;
@@ -15180,6 +15347,7 @@ ${pc10.dim(`Watch live: docker exec -it ${containerName} tmux attach -t claude-m
15180
15347
 
15181
15348
  // src/commands/observe.ts
15182
15349
  init_context();
15350
+ init_output();
15183
15351
  import pc11 from "picocolors";
15184
15352
  function registerObserve(program2) {
15185
15353
  program2.command("observe").description("Stream thoughts from a world (coming soon)").argument("<world>", "World ID").action(async (worldId) => {
@@ -15195,6 +15363,10 @@ function registerObserve(program2) {
15195
15363
  process.exitCode = 1;
15196
15364
  return;
15197
15365
  }
15366
+ {
15367
+ const { checkVersionPin: checkVersionPin2 } = await Promise.resolve().then(() => (init_version_pin(), version_pin_exports));
15368
+ checkVersionPin2(worldId, world.workspacePath);
15369
+ }
15198
15370
  console.log(
15199
15371
  pc11.yellow("Observation is coming in a future release.")
15200
15372
  );
@@ -15211,6 +15383,7 @@ ${pc11.dim(`For now: docker exec -it ${containerName} tmux attach -t claude-main
15211
15383
 
15212
15384
  // src/commands/list.ts
15213
15385
  init_context();
15386
+ init_output();
15214
15387
  import pc12 from "picocolors";
15215
15388
  function registerList(program2) {
15216
15389
  program2.command("list").alias("ls").description("List active worlds").action(async () => {
@@ -15241,10 +15414,88 @@ function registerList(program2) {
15241
15414
  }
15242
15415
 
15243
15416
  // src/commands/status.ts
15244
- init_context();
15417
+ init_output();
15418
+ import * as fs22 from "node:fs";
15419
+ import * as os13 from "node:os";
15420
+ import * as path25 from "node:path";
15421
+ var CLI_VERSION2 = process.env["OLAM_CLI_VERSION"] ?? "0.0.0";
15422
+ var HOST_CP_PORT2 = 19e3;
15423
+ async function getMachineStatus(_probe, _loadCtx, _readToken) {
15424
+ const probe = _probe ?? (async () => {
15425
+ const { probeHostCp: probeHostCp2 } = await Promise.resolve().then(() => (init_host_cp(), host_cp_exports));
15426
+ return probeHostCp2();
15427
+ });
15428
+ let readToken2;
15429
+ if (_readToken) {
15430
+ readToken2 = _readToken;
15431
+ } else {
15432
+ const { readToken: rt } = await Promise.resolve().then(() => (init_host_cp(), host_cp_exports));
15433
+ readToken2 = rt;
15434
+ }
15435
+ const loadCtx = _loadCtx ?? (async () => {
15436
+ const { loadContext: loadContext2 } = await Promise.resolve().then(() => (init_context(), context_exports));
15437
+ return loadContext2();
15438
+ });
15439
+ const probeResult = await probe();
15440
+ const tokenPresent = readToken2() !== null;
15441
+ const hostCpRunning = probeResult !== null;
15442
+ let worldCount = 0;
15443
+ try {
15444
+ const { ctx } = await loadCtx();
15445
+ if (ctx) {
15446
+ worldCount = ctx.worldManager.listWorlds().length;
15447
+ }
15448
+ } catch {
15449
+ }
15450
+ const manifestPath2 = path25.join(os13.homedir(), ".olam", "cache", "manifest.json");
15451
+ let updateAvailable = null;
15452
+ let lastUpdateCheck = null;
15453
+ if (fs22.existsSync(manifestPath2)) {
15454
+ const mtime = fs22.statSync(manifestPath2).mtime;
15455
+ lastUpdateCheck = mtime.toISOString();
15456
+ try {
15457
+ const manifest = JSON.parse(fs22.readFileSync(manifestPath2, "utf-8"));
15458
+ const latest = manifest["version"];
15459
+ updateAvailable = latest !== void 0 && latest !== CLI_VERSION2;
15460
+ } catch {
15461
+ updateAvailable = null;
15462
+ }
15463
+ }
15464
+ return {
15465
+ version: CLI_VERSION2,
15466
+ port: HOST_CP_PORT2,
15467
+ host_cp: hostCpRunning ? "running" : "stopped",
15468
+ token_present: tokenPresent,
15469
+ worlds: worldCount,
15470
+ update_available: updateAvailable,
15471
+ last_update_check: lastUpdateCheck
15472
+ };
15473
+ }
15245
15474
  function registerStatus(program2) {
15246
- program2.command("status").description("Show detailed world status").argument("<world>", "World ID").option("--json", "Output as JSON").action(async (worldId, opts) => {
15247
- const { ctx, error } = await loadContext();
15475
+ program2.command("status").description("Show status (machine status when no world given; world details with a world ID)").argument("[world]", "World ID \u2014 omit to show machine status").option("--json", "Output as JSON").action(async (worldId, opts) => {
15476
+ if (!worldId) {
15477
+ const ms = await getMachineStatus();
15478
+ if (opts.json) {
15479
+ process.stdout.write(JSON.stringify(ms, null, 2) + "\n");
15480
+ process.exitCode = ms.host_cp === "running" ? 0 : 1;
15481
+ return;
15482
+ }
15483
+ printHeader("Olam Status");
15484
+ printInfo("CLI version", `@pleri/olam-cli@${ms.version}`);
15485
+ printInfo("Host CP", ms.host_cp === "running" ? `running on port ${ms.port}` : "stopped");
15486
+ printInfo("Worlds", String(ms.worlds));
15487
+ printInfo("Token", ms.token_present ? "present" : "absent");
15488
+ if (ms.last_update_check) {
15489
+ printInfo("Last update check", ms.last_update_check);
15490
+ printInfo("Update available", ms.update_available === true ? "yes" : ms.update_available === false ? "no" : "unknown");
15491
+ } else {
15492
+ printInfo("Update check", "never (run `olam update --check`)");
15493
+ }
15494
+ process.exitCode = ms.host_cp === "running" ? 0 : 1;
15495
+ return;
15496
+ }
15497
+ const { loadContext: loadContext2 } = await Promise.resolve().then(() => (init_context(), context_exports));
15498
+ const { ctx, error } = await loadContext2();
15248
15499
  if (!ctx) {
15249
15500
  printError(error?.message ?? "Olam is not configured. Run `olam init` first.");
15250
15501
  process.exitCode = 1;
@@ -15285,6 +15536,8 @@ function registerStatus(program2) {
15285
15536
 
15286
15537
  // src/commands/destroy.ts
15287
15538
  init_context();
15539
+ init_output();
15540
+ init_host_cp();
15288
15541
  import ora5 from "ora";
15289
15542
  function registerDestroy(program2) {
15290
15543
  program2.command("destroy").description("Destroy a world and clean up resources").argument("<world>", "World ID").option("--skip-crystallize", "Skip thought crystallization").action(async (worldId, opts) => {
@@ -15323,6 +15576,7 @@ function registerDestroy(program2) {
15323
15576
 
15324
15577
  // src/commands/enter.ts
15325
15578
  init_context();
15579
+ init_output();
15326
15580
  import { execSync as execSync7 } from "node:child_process";
15327
15581
  import pc13 from "picocolors";
15328
15582
  var SAFE_IDENT3 = /^[a-z0-9][a-z0-9-]{0,63}$/;
@@ -15382,6 +15636,11 @@ function registerEnter(program2) {
15382
15636
  process.exitCode = 1;
15383
15637
  return;
15384
15638
  }
15639
+ const worldMeta = ctx.worldManager.getWorld(worldId);
15640
+ if (worldMeta) {
15641
+ const { checkVersionPin: checkVersionPin2 } = await Promise.resolve().then(() => (init_version_pin(), version_pin_exports));
15642
+ checkVersionPin2(worldId, worldMeta.workspacePath);
15643
+ }
15385
15644
  const computeWorld = await ctx.computeProvider.getWorld(worldId);
15386
15645
  if (!computeWorld) {
15387
15646
  printError(`World "${worldId}" not found.`);
@@ -15471,11 +15730,12 @@ ${pc13.dim(`Observe dispatch: docker exec -it ${containerName} tmux attach -t ol
15471
15730
 
15472
15731
  // src/commands/crystallize.ts
15473
15732
  init_context();
15474
- import * as fs21 from "node:fs";
15475
- import "node:path";
15476
- import ora6 from "ora";
15733
+ init_output();
15477
15734
  init_exit_codes();
15478
15735
  init_world_paths();
15736
+ import * as fs23 from "node:fs";
15737
+ import "node:path";
15738
+ import ora6 from "ora";
15479
15739
  function registerCrystallize(program2, options = {}) {
15480
15740
  const cmd = program2.command("crystallize").description("Crystallize thoughts from a world to Pleri Plane").argument("<world>", "World ID").action(async (worldId) => {
15481
15741
  const { ctx, error } = await loadContext();
@@ -15505,7 +15765,7 @@ function registerCrystallize(program2, options = {}) {
15505
15765
  return;
15506
15766
  }
15507
15767
  const thoughtDbPath = getWorldDbPath(world.workspacePath);
15508
- if (!fs21.existsSync(thoughtDbPath)) {
15768
+ if (!fs23.existsSync(thoughtDbPath)) {
15509
15769
  printError(`No thoughts captured yet for "${worldId}". Run a dispatch first.`);
15510
15770
  process.exitCode = EXIT_GENERIC_ERROR;
15511
15771
  return;
@@ -15620,6 +15880,7 @@ async function decideGate(portOffset, id, payload) {
15620
15880
  }
15621
15881
 
15622
15882
  // src/commands/pr.ts
15883
+ init_output();
15623
15884
  function runningWorlds() {
15624
15885
  const registry = new WorldRegistry();
15625
15886
  try {
@@ -15726,7 +15987,12 @@ function registerPr(program2) {
15726
15987
  decideCommand("reject", "block");
15727
15988
  }
15728
15989
 
15990
+ // src/index.ts
15991
+ init_host_cp();
15992
+
15729
15993
  // src/commands/lanes.ts
15994
+ init_host_cp();
15995
+ init_output();
15730
15996
  async function handleList(world) {
15731
15997
  printHeader(`Lanes in world ${world}`);
15732
15998
  const result = await callHostCpProxy("GET", world, "/lanes");
@@ -16873,11 +17139,11 @@ var qmarksTestNoExtDot = ([$0]) => {
16873
17139
  return (f) => f.length === len && f !== "." && f !== "..";
16874
17140
  };
16875
17141
  var defaultPlatform = typeof process === "object" && process ? typeof process.env === "object" && process.env && process.env.__MINIMATCH_TESTING_PLATFORM__ || process.platform : "posix";
16876
- var path25 = {
17142
+ var path27 = {
16877
17143
  win32: { sep: "\\" },
16878
17144
  posix: { sep: "/" }
16879
17145
  };
16880
- var sep = defaultPlatform === "win32" ? path25.win32.sep : path25.posix.sep;
17146
+ var sep = defaultPlatform === "win32" ? path27.win32.sep : path27.posix.sep;
16881
17147
  minimatch.sep = sep;
16882
17148
  var GLOBSTAR = /* @__PURE__ */ Symbol("globstar **");
16883
17149
  minimatch.GLOBSTAR = GLOBSTAR;
@@ -17707,23 +17973,25 @@ function registerPolicyCheck(program2) {
17707
17973
  }
17708
17974
 
17709
17975
  // src/commands/upgrade.ts
17710
- import * as fs24 from "node:fs";
17711
- import * as path28 from "node:path";
17976
+ init_output();
17977
+ init_host_cp();
17978
+ import * as fs26 from "node:fs";
17979
+ import * as path30 from "node:path";
17712
17980
  import { spawnSync as spawnSync10 } from "node:child_process";
17713
17981
  import ora7 from "ora";
17714
17982
  import pc16 from "picocolors";
17715
17983
 
17716
17984
  // src/commands/upgrade-lock.ts
17717
- import * as fs22 from "node:fs";
17718
- import * as os13 from "node:os";
17719
- import * as path26 from "node:path";
17985
+ import * as fs24 from "node:fs";
17986
+ import * as os14 from "node:os";
17987
+ import * as path28 from "node:path";
17720
17988
  import { spawnSync as spawnSync9 } from "node:child_process";
17721
- var LOCK_FILE_PATH = path26.join(os13.homedir(), ".olam", ".upgrade.lock");
17989
+ var LOCK_FILE_PATH = path28.join(os14.homedir(), ".olam", ".upgrade.lock");
17722
17990
  var STALE_LOCK_TIMEOUT_MS = 5 * 60 * 1e3;
17723
17991
  function readLockFile(lockPath) {
17724
17992
  try {
17725
- if (!fs22.existsSync(lockPath)) return null;
17726
- const raw = fs22.readFileSync(lockPath, "utf-8").trim();
17993
+ if (!fs24.existsSync(lockPath)) return null;
17994
+ const raw = fs24.readFileSync(lockPath, "utf-8").trim();
17727
17995
  if (raw.length === 0) return null;
17728
17996
  const parsed = JSON.parse(raw);
17729
17997
  if (typeof parsed.pid !== "number" || typeof parsed.startTs !== "number") return null;
@@ -17768,16 +18036,16 @@ function isStaleLock(content, nowMs = Date.now()) {
17768
18036
  return false;
17769
18037
  }
17770
18038
  function acquireLock(lockPath = LOCK_FILE_PATH, nowMs = Date.now()) {
17771
- const dir = path26.dirname(lockPath);
17772
- fs22.mkdirSync(dir, { recursive: true });
18039
+ const dir = path28.dirname(lockPath);
18040
+ fs24.mkdirSync(dir, { recursive: true });
17773
18041
  for (let attempt = 0; attempt < 2; attempt++) {
17774
18042
  try {
17775
- const fd = fs22.openSync(lockPath, "wx", 420);
18043
+ const fd = fs24.openSync(lockPath, "wx", 420);
17776
18044
  try {
17777
18045
  const content = { pid: process.pid, startTs: nowMs };
17778
- fs22.writeSync(fd, JSON.stringify(content));
18046
+ fs24.writeSync(fd, JSON.stringify(content));
17779
18047
  } finally {
17780
- fs22.closeSync(fd);
18048
+ fs24.closeSync(fd);
17781
18049
  }
17782
18050
  return { acquired: true, lockPath };
17783
18051
  } catch (err) {
@@ -17786,7 +18054,7 @@ function acquireLock(lockPath = LOCK_FILE_PATH, nowMs = Date.now()) {
17786
18054
  const existing2 = readLockFile(lockPath);
17787
18055
  if (isStaleLock(existing2, nowMs)) {
17788
18056
  try {
17789
- fs22.unlinkSync(lockPath);
18057
+ fs24.unlinkSync(lockPath);
17790
18058
  } catch (unlinkErr) {
17791
18059
  const ucode = unlinkErr.code;
17792
18060
  if (ucode !== "ENOENT") throw unlinkErr;
@@ -17811,7 +18079,7 @@ function acquireLock(lockPath = LOCK_FILE_PATH, nowMs = Date.now()) {
17811
18079
  }
17812
18080
  function releaseLock(lockPath = LOCK_FILE_PATH) {
17813
18081
  try {
17814
- fs22.unlinkSync(lockPath);
18082
+ fs24.unlinkSync(lockPath);
17815
18083
  } catch (err) {
17816
18084
  const code = err.code;
17817
18085
  if (code !== "ENOENT") throw err;
@@ -17829,19 +18097,19 @@ function formatRefusalMessage(result, lockPath = LOCK_FILE_PATH) {
17829
18097
  }
17830
18098
 
17831
18099
  // src/commands/upgrade-log.ts
17832
- import * as fs23 from "node:fs";
17833
- import * as os14 from "node:os";
17834
- import * as path27 from "node:path";
18100
+ import * as fs25 from "node:fs";
18101
+ import * as os15 from "node:os";
18102
+ import * as path29 from "node:path";
17835
18103
  function getUpgradeLogPath() {
17836
- const home = process.env["HOME"] ?? os14.homedir();
17837
- return path27.join(home, ".olam", "upgrade.log");
18104
+ const home = process.env["HOME"] ?? os15.homedir();
18105
+ return path29.join(home, ".olam", "upgrade.log");
17838
18106
  }
17839
18107
  var UPGRADE_LOG_PATH = getUpgradeLogPath();
17840
18108
  function appendUpgradeLog(row, logPath = getUpgradeLogPath()) {
17841
18109
  try {
17842
- fs23.mkdirSync(path27.dirname(logPath), { recursive: true });
18110
+ fs25.mkdirSync(path29.dirname(logPath), { recursive: true });
17843
18111
  const line = JSON.stringify(row) + "\n";
17844
- fs23.appendFileSync(logPath, line, { mode: 420 });
18112
+ fs25.appendFileSync(logPath, line, { mode: 420 });
17845
18113
  } catch (err) {
17846
18114
  process.stderr.write(
17847
18115
  `[upgrade-log] failed to append: ${err instanceof Error ? err.message : String(err)}
@@ -17850,10 +18118,10 @@ function appendUpgradeLog(row, logPath = getUpgradeLogPath()) {
17850
18118
  }
17851
18119
  }
17852
18120
  function readUpgradeLog(limit = 10, logPath = getUpgradeLogPath()) {
17853
- if (!fs23.existsSync(logPath)) return [];
18121
+ if (!fs25.existsSync(logPath)) return [];
17854
18122
  let raw;
17855
18123
  try {
17856
- raw = fs23.readFileSync(logPath, "utf-8");
18124
+ raw = fs25.readFileSync(logPath, "utf-8");
17857
18125
  } catch (err) {
17858
18126
  process.stderr.write(
17859
18127
  `[upgrade-log] failed to read: ${err instanceof Error ? err.message : String(err)}
@@ -17915,6 +18183,7 @@ function formatHistoryJson(rows) {
17915
18183
  }
17916
18184
 
17917
18185
  // src/commands/upgrade-history.ts
18186
+ init_output();
17918
18187
  function parseHistoryOpts(raw) {
17919
18188
  const rawLimit = raw.n;
17920
18189
  const limit = typeof rawLimit === "number" ? rawLimit : typeof rawLimit === "string" ? Number.parseInt(rawLimit, 10) : 10;
@@ -17945,12 +18214,12 @@ init_protocol_version();
17945
18214
  init_install_root();
17946
18215
  var AUTH_HEALTH_URL2 = "http://127.0.0.1:9999/health";
17947
18216
  function isNodeModulesInSync(cwd) {
17948
- const lockPath = path28.join(cwd, "package-lock.json");
17949
- const markerPath = path28.join(cwd, "node_modules", ".package-lock.json");
17950
- if (!fs24.existsSync(lockPath) || !fs24.existsSync(markerPath)) return false;
18217
+ const lockPath = path30.join(cwd, "package-lock.json");
18218
+ const markerPath = path30.join(cwd, "node_modules", ".package-lock.json");
18219
+ if (!fs26.existsSync(lockPath) || !fs26.existsSync(markerPath)) return false;
17951
18220
  try {
17952
- const lockStat = fs24.statSync(lockPath);
17953
- const markerStat = fs24.statSync(markerPath);
18221
+ const lockStat = fs26.statSync(lockPath);
18222
+ const markerStat = fs26.statSync(markerPath);
17954
18223
  return markerStat.mtimeMs >= lockStat.mtimeMs;
17955
18224
  } catch {
17956
18225
  return false;
@@ -17966,8 +18235,8 @@ function shouldSkipInstall(opts, cwd) {
17966
18235
  return { skip: false };
17967
18236
  }
17968
18237
  function validateRepoRoot(cwd) {
17969
- const marker = path28.join(cwd, "packages/host-cp/compose.yaml");
17970
- if (!fs24.existsSync(marker)) {
18238
+ const marker = path30.join(cwd, "packages/host-cp/compose.yaml");
18239
+ if (!fs26.existsSync(marker)) {
17971
18240
  return {
17972
18241
  ok: false,
17973
18242
  error: `Not an olam repo root (expected ${marker}).
@@ -18274,7 +18543,8 @@ function formatVersionMismatch(targetSha, snapshot) {
18274
18543
  }
18275
18544
  return lines.join("\n");
18276
18545
  }
18277
- async function waitForAuthHealthLocal(timeoutMs = 15e3) {
18546
+ var AUTH_HEALTH_TIMEOUT_MS = 6e4;
18547
+ async function waitForAuthHealthLocal(timeoutMs = AUTH_HEALTH_TIMEOUT_MS) {
18278
18548
  const deadline = Date.now() + timeoutMs;
18279
18549
  while (Date.now() < deadline) {
18280
18550
  try {
@@ -18299,13 +18569,13 @@ async function recreateAuthService() {
18299
18569
  });
18300
18570
  const controller = new AuthContainerController();
18301
18571
  controller.start();
18302
- const healthy = await waitForAuthHealthLocal(15e3);
18572
+ const healthy = await waitForAuthHealthLocal();
18303
18573
  const durationMs = Date.now() - start;
18304
18574
  if (!healthy) {
18305
18575
  return {
18306
18576
  ok: false,
18307
18577
  durationMs,
18308
- error: "auth-service /health did not respond within 15s after recreate"
18578
+ error: `auth-service /health did not respond within ${AUTH_HEALTH_TIMEOUT_MS / 1e3}s after recreate`
18309
18579
  };
18310
18580
  }
18311
18581
  return { ok: true, durationMs };
@@ -18318,9 +18588,9 @@ async function recreateAuthService() {
18318
18588
  }
18319
18589
  }
18320
18590
  function readBundleHash(cwd) {
18321
- const indexPath = path28.join(cwd, "packages/control-plane/public/index.html");
18322
- if (!fs24.existsSync(indexPath)) return null;
18323
- return extractBundleHash(fs24.readFileSync(indexPath, "utf-8"));
18591
+ const indexPath = path30.join(cwd, "packages/control-plane/public/index.html");
18592
+ if (!fs26.existsSync(indexPath)) return null;
18593
+ return extractBundleHash(fs26.readFileSync(indexPath, "utf-8"));
18324
18594
  }
18325
18595
  async function runUpgradePullByDigest(deps = {}) {
18326
18596
  const docker2 = deps.docker ?? realDocker;
@@ -18418,12 +18688,12 @@ async function runUpgradePullByDigest(deps = {}) {
18418
18688
  }
18419
18689
  }
18420
18690
  tagSpinner.succeed("Re-tagged 3 images to canonical refs");
18421
- const composeFile = deps.composeFile ?? path28.join(process.cwd(), "packages/host-cp/compose.yaml");
18691
+ const composeFile = deps.composeFile ?? path30.join(process.cwd(), "packages/host-cp/compose.yaml");
18422
18692
  const authSecret = deps.authSecret ?? readAuthSecret2();
18423
18693
  const composeRunner = deps.runComposeImpl ?? runCompose;
18424
18694
  const composeSpinner = ora7("docker compose recreate host-cp").start();
18425
18695
  const composeResult = composeRunner(
18426
- ["up", "-d", "--force-recreate", "host-cp"],
18696
+ ["up", "-d", "--force-recreate", "--no-deps", "host-cp"],
18427
18697
  composeFile,
18428
18698
  buildComposeEnv(authSecret)
18429
18699
  );
@@ -18467,9 +18737,12 @@ async function defaultRecreateAuthForUpgrade() {
18467
18737
  });
18468
18738
  const controller = new AuthContainerController();
18469
18739
  controller.start();
18470
- const healthy = await waitForAuthHealthLocal(15e3);
18740
+ const healthy = await waitForAuthHealthLocal();
18471
18741
  if (!healthy) {
18472
- return { ok: false, error: "auth-service /health did not respond within 15s" };
18742
+ return {
18743
+ ok: false,
18744
+ error: `auth-service /health did not respond within ${AUTH_HEALTH_TIMEOUT_MS / 1e3}s`
18745
+ };
18473
18746
  }
18474
18747
  return { ok: true };
18475
18748
  } catch (err) {
@@ -18616,11 +18889,11 @@ manually inspect images with \`docker images olam-*:olam-rollback\`.`
18616
18889
  }
18617
18890
  printInfo("Rollback", swapResult.summary);
18618
18891
  const cwd = process.cwd();
18619
- const composeFile = path28.join(cwd, "packages/host-cp/compose.yaml");
18892
+ const composeFile = path30.join(cwd, "packages/host-cp/compose.yaml");
18620
18893
  const authSecret = readAuthSecret2();
18621
18894
  process.stdout.write(` ${pc16.dim("docker compose recreate host-cp".padEnd(34))}`);
18622
18895
  const composeStart = Date.now();
18623
- const composeResult = runCompose(["up", "-d", "--force-recreate", "host-cp"], composeFile, buildComposeEnv(authSecret));
18896
+ const composeResult = runCompose(["up", "-d", "--force-recreate", "--no-deps", "host-cp"], composeFile, buildComposeEnv(authSecret));
18624
18897
  const composeDur = `${((Date.now() - composeStart) / 1e3).toFixed(1)}s`;
18625
18898
  process.stdout.write(`${composeResult.ok ? pc16.green("\u2713") : pc16.red("\u2717")} ${composeDur}
18626
18899
  `);
@@ -18757,7 +19030,7 @@ ${buildResult.stderr}`);
18757
19030
  return;
18758
19031
  }
18759
19032
  const authSecret = readAuthSecret2();
18760
- const spaDir = path28.join(cwd, "packages/control-plane/app");
19033
+ const spaDir = path30.join(cwd, "packages/control-plane/app");
18761
19034
  const spaResult = runStep2(
18762
19035
  "vite build (SPA)",
18763
19036
  "npx",
@@ -18890,7 +19163,7 @@ Recovery options:
18890
19163
  return;
18891
19164
  }
18892
19165
  printInfo("Swap", swapResult.summary);
18893
- const composeFile = path28.join(cwd, "packages/host-cp/compose.yaml");
19166
+ const composeFile = path30.join(cwd, "packages/host-cp/compose.yaml");
18894
19167
  process.stdout.write(` ${pc16.dim("docker compose recreate".padEnd(34))}`);
18895
19168
  const composeStart = Date.now();
18896
19169
  const composeResult = runCompose(
@@ -19023,10 +19296,12 @@ function registerUpgrade(program2) {
19023
19296
  }
19024
19297
 
19025
19298
  // src/commands/logs.ts
19299
+ init_host_cp();
19300
+ init_context();
19301
+ init_output();
19026
19302
  import * as http3 from "node:http";
19027
19303
  import pc17 from "picocolors";
19028
- init_context();
19029
- var HOST_CP_PORT2 = 19e3;
19304
+ var HOST_CP_PORT3 = 19e3;
19030
19305
  function colorLine(line) {
19031
19306
  if (/\bERROR\b/.test(line)) return pc17.red(line);
19032
19307
  if (/\bWARN\b/.test(line)) return pc17.yellow(line);
@@ -19073,7 +19348,7 @@ function registerLogs(program2) {
19073
19348
  const tailLimit = Math.max(1, parseInt(opts.tail, 10) || 200);
19074
19349
  const showService = opts.service === void 0;
19075
19350
  const subPath = opts.service ? `/api/logs/${encodeURIComponent(opts.service)}` : "/api/logs";
19076
- const url = `http://127.0.0.1:${HOST_CP_PORT2}/api/world/${encodeURIComponent(worldId)}${subPath}`;
19351
+ const url = `http://127.0.0.1:${HOST_CP_PORT3}/api/world/${encodeURIComponent(worldId)}${subPath}`;
19077
19352
  let lineCount = 0;
19078
19353
  let done = false;
19079
19354
  let resolveStream;
@@ -19150,6 +19425,7 @@ function registerLogs(program2) {
19150
19425
 
19151
19426
  // src/commands/ps.ts
19152
19427
  init_context();
19428
+ init_output();
19153
19429
  import pc18 from "picocolors";
19154
19430
  import { spawnSync as spawnSync11 } from "node:child_process";
19155
19431
  var SAFE_IDENT4 = /^[a-z0-9][a-z0-9-]{0,63}$/;
@@ -19287,20 +19563,21 @@ ${pc18.dim(`world: ${worldId} sort: ${sortKey} refresh: 5s Ctrl-C to exit`)}
19287
19563
  }
19288
19564
 
19289
19565
  // src/commands/keys.ts
19290
- import * as fs25 from "node:fs";
19291
- import * as os15 from "node:os";
19292
- import * as path29 from "node:path";
19566
+ init_output();
19567
+ import * as fs27 from "node:fs";
19568
+ import * as os16 from "node:os";
19569
+ import * as path31 from "node:path";
19293
19570
  import YAML4 from "yaml";
19294
19571
  function olamHome2() {
19295
- return process.env.OLAM_HOME ?? path29.join(os15.homedir(), ".olam");
19572
+ return process.env.OLAM_HOME ?? path31.join(os16.homedir(), ".olam");
19296
19573
  }
19297
19574
  function keysFilePath() {
19298
- return path29.join(olamHome2(), "keys.yaml");
19575
+ return path31.join(olamHome2(), "keys.yaml");
19299
19576
  }
19300
19577
  function readKeysFile() {
19301
19578
  const filePath = keysFilePath();
19302
- if (!fs25.existsSync(filePath)) return null;
19303
- const raw = fs25.readFileSync(filePath, "utf-8").trim();
19579
+ if (!fs27.existsSync(filePath)) return null;
19580
+ const raw = fs27.readFileSync(filePath, "utf-8").trim();
19304
19581
  if (raw.length === 0) return null;
19305
19582
  try {
19306
19583
  const parsed = YAML4.parse(raw);
@@ -19316,13 +19593,13 @@ function readKeysFile() {
19316
19593
  }
19317
19594
  function writeKeysFile(keys) {
19318
19595
  const dir = olamHome2();
19319
- if (!fs25.existsSync(dir)) {
19320
- fs25.mkdirSync(dir, { recursive: true });
19596
+ if (!fs27.existsSync(dir)) {
19597
+ fs27.mkdirSync(dir, { recursive: true });
19321
19598
  }
19322
19599
  const filePath = keysFilePath();
19323
19600
  const content = YAML4.stringify(keys);
19324
- fs25.writeFileSync(filePath, content, { encoding: "utf-8", mode: 384 });
19325
- fs25.chmodSync(filePath, 384);
19601
+ fs27.writeFileSync(filePath, content, { encoding: "utf-8", mode: 384 });
19602
+ fs27.chmodSync(filePath, 384);
19326
19603
  }
19327
19604
  function redact(value) {
19328
19605
  if (value.length <= 8) return value + "...";
@@ -19365,7 +19642,7 @@ function registerKeys(program2) {
19365
19642
  }
19366
19643
  const { [key]: _removed, ...rest } = existing;
19367
19644
  if (Object.keys(rest).length === 0) {
19368
- fs25.unlinkSync(keysFilePath());
19645
+ fs27.unlinkSync(keysFilePath());
19369
19646
  } else {
19370
19647
  writeKeysFile(rest);
19371
19648
  }
@@ -19388,26 +19665,26 @@ function registerKeys(program2) {
19388
19665
  }
19389
19666
 
19390
19667
  // src/commands/world-snapshot.ts
19391
- import * as fs27 from "node:fs";
19392
- import * as path31 from "node:path";
19668
+ import * as fs29 from "node:fs";
19669
+ import * as path33 from "node:path";
19393
19670
  import { execSync as execSync9 } from "node:child_process";
19394
19671
  import pc19 from "picocolors";
19395
19672
 
19396
19673
  // ../core/src/world/snapshot.ts
19397
19674
  import * as crypto6 from "node:crypto";
19398
- import * as fs26 from "node:fs";
19399
- import * as os16 from "node:os";
19400
- import * as path30 from "node:path";
19675
+ import * as fs28 from "node:fs";
19676
+ import * as os17 from "node:os";
19677
+ import * as path32 from "node:path";
19401
19678
  import { execFileSync as execFileSync4 } from "node:child_process";
19402
19679
  function snapshotsDir() {
19403
- return process.env["OLAM_SNAPSHOTS_DIR"] ?? path30.join(os16.homedir(), ".olam", "snapshots");
19680
+ return process.env["OLAM_SNAPSHOTS_DIR"] ?? path32.join(os17.homedir(), ".olam", "snapshots");
19404
19681
  }
19405
19682
  function snapshotKindDir(worldId, kind) {
19406
- return path30.join(snapshotsDir(), worldId, kind);
19683
+ return path32.join(snapshotsDir(), worldId, kind);
19407
19684
  }
19408
19685
  function snapshotTarPath(worldId, kind, repoName, hash) {
19409
19686
  const base = repoName ? `${repoName}-${hash}` : hash;
19410
- return path30.join(snapshotKindDir(worldId, kind), `${base}.tar.gz`);
19687
+ return path32.join(snapshotKindDir(worldId, kind), `${base}.tar.gz`);
19411
19688
  }
19412
19689
  function manifestPath(tarPath) {
19413
19690
  return tarPath.replace(/\.tar\.gz$/, ".manifest.json");
@@ -19424,16 +19701,16 @@ function hashBuffers(entries) {
19424
19701
  return hash.digest("hex").slice(0, 12);
19425
19702
  }
19426
19703
  function computeGemsFingerprint(repoDir) {
19427
- const lockfile = path30.join(repoDir, "Gemfile.lock");
19428
- if (!fs26.existsSync(lockfile)) return null;
19429
- return hashBuffers([{ path: "Gemfile.lock", content: fs26.readFileSync(lockfile) }]);
19704
+ const lockfile = path32.join(repoDir, "Gemfile.lock");
19705
+ if (!fs28.existsSync(lockfile)) return null;
19706
+ return hashBuffers([{ path: "Gemfile.lock", content: fs28.readFileSync(lockfile) }]);
19430
19707
  }
19431
19708
  function computeNodeFingerprint(repoDir) {
19432
19709
  const candidates = ["yarn.lock", "pnpm-lock.yaml", "package-lock.json"];
19433
19710
  for (const name of candidates) {
19434
- const lockfile = path30.join(repoDir, name);
19435
- if (fs26.existsSync(lockfile)) {
19436
- return hashBuffers([{ path: name, content: fs26.readFileSync(lockfile) }]);
19711
+ const lockfile = path32.join(repoDir, name);
19712
+ if (fs28.existsSync(lockfile)) {
19713
+ return hashBuffers([{ path: name, content: fs28.readFileSync(lockfile) }]);
19437
19714
  }
19438
19715
  }
19439
19716
  return null;
@@ -19443,59 +19720,59 @@ function computePgFingerprint(repoDirs) {
19443
19720
  const entries = [];
19444
19721
  for (const repoDir of repoDirs) {
19445
19722
  for (const pattern of patterns) {
19446
- const filePath = path30.join(repoDir, pattern);
19447
- if (fs26.existsSync(filePath)) {
19448
- entries.push({ path: filePath, content: fs26.readFileSync(filePath) });
19723
+ const filePath = path32.join(repoDir, pattern);
19724
+ if (fs28.existsSync(filePath)) {
19725
+ entries.push({ path: filePath, content: fs28.readFileSync(filePath) });
19449
19726
  }
19450
19727
  }
19451
19728
  }
19452
19729
  return entries.length > 0 ? hashBuffers(entries) : null;
19453
19730
  }
19454
19731
  function packTarball(srcDir, destPath, opts = {}) {
19455
- fs26.mkdirSync(path30.dirname(destPath), { recursive: true });
19732
+ fs28.mkdirSync(path32.dirname(destPath), { recursive: true });
19456
19733
  const tmp = `${destPath}.tmp`;
19457
19734
  const args = [];
19458
19735
  if (opts.followSymlinks) args.push("-h");
19459
19736
  args.push("-czf", tmp, "-C", srcDir, ".");
19460
19737
  try {
19461
19738
  execFileSync4("tar", args, { stdio: "pipe" });
19462
- fs26.renameSync(tmp, destPath);
19739
+ fs28.renameSync(tmp, destPath);
19463
19740
  } catch (err) {
19464
19741
  try {
19465
- fs26.rmSync(tmp, { force: true });
19742
+ fs28.rmSync(tmp, { force: true });
19466
19743
  } catch {
19467
19744
  }
19468
19745
  throw err;
19469
19746
  }
19470
19747
  }
19471
19748
  function writeManifest(manifest, tarPath) {
19472
- fs26.writeFileSync(manifestPath(tarPath), JSON.stringify(manifest, null, 2), "utf-8");
19749
+ fs28.writeFileSync(manifestPath(tarPath), JSON.stringify(manifest, null, 2), "utf-8");
19473
19750
  }
19474
19751
  function readManifest(tarPath) {
19475
19752
  const mPath = manifestPath(tarPath);
19476
- if (!fs26.existsSync(mPath)) return null;
19753
+ if (!fs28.existsSync(mPath)) return null;
19477
19754
  try {
19478
- return JSON.parse(fs26.readFileSync(mPath, "utf-8"));
19755
+ return JSON.parse(fs28.readFileSync(mPath, "utf-8"));
19479
19756
  } catch {
19480
19757
  return null;
19481
19758
  }
19482
19759
  }
19483
19760
  function listSnapshots(worldIdFilter) {
19484
19761
  const root = snapshotsDir();
19485
- if (!fs26.existsSync(root)) return [];
19762
+ if (!fs28.existsSync(root)) return [];
19486
19763
  const now = Date.now();
19487
19764
  const results = [];
19488
- const worlds = worldIdFilter ? [worldIdFilter] : fs26.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
19765
+ const worlds = worldIdFilter ? [worldIdFilter] : fs28.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
19489
19766
  for (const worldId of worlds) {
19490
- const worldDir = path30.join(root, worldId);
19491
- if (!fs26.existsSync(worldDir) || !fs26.statSync(worldDir).isDirectory()) continue;
19767
+ const worldDir = path32.join(root, worldId);
19768
+ if (!fs28.existsSync(worldDir) || !fs28.statSync(worldDir).isDirectory()) continue;
19492
19769
  for (const kind of ["gems", "node", "pg"]) {
19493
- const kindDir = path30.join(worldDir, kind);
19494
- if (!fs26.existsSync(kindDir)) continue;
19495
- const tarballs = fs26.readdirSync(kindDir).filter((f) => f.endsWith(".tar.gz"));
19770
+ const kindDir = path32.join(worldDir, kind);
19771
+ if (!fs28.existsSync(kindDir)) continue;
19772
+ const tarballs = fs28.readdirSync(kindDir).filter((f) => f.endsWith(".tar.gz"));
19496
19773
  for (const tarFile of tarballs) {
19497
- const tarPath = path30.join(kindDir, tarFile);
19498
- const stat = fs26.statSync(tarPath);
19774
+ const tarPath = path32.join(kindDir, tarFile);
19775
+ const stat = fs28.statSync(tarPath);
19499
19776
  const manifest = readManifest(tarPath);
19500
19777
  if (!manifest) continue;
19501
19778
  results.push({ manifest, tarPath, ageMs: now - stat.mtimeMs });
@@ -19505,9 +19782,19 @@ function listSnapshots(worldIdFilter) {
19505
19782
  return results.sort((a, b) => a.manifest.createdAt.localeCompare(b.manifest.createdAt));
19506
19783
  }
19507
19784
 
19785
+ // src/commands/world-snapshot.ts
19786
+ init_output();
19787
+
19788
+ // src/commands/world.ts
19789
+ function getOrCreateWorldCommand(program2) {
19790
+ const existing = program2.commands.find((c) => c.name() === "world");
19791
+ if (existing) return existing;
19792
+ return program2.command("world").description("World management subcommands");
19793
+ }
19794
+
19508
19795
  // src/commands/world-snapshot.ts
19509
19796
  function registerWorldSnapshot(program2) {
19510
- const world = program2.command("world").description("World management subcommands");
19797
+ const world = getOrCreateWorldCommand(program2);
19511
19798
  const snapshot = world.command("snapshot").description("Manage world snapshots for fast boot");
19512
19799
  snapshot.command("create <worldId>").description("Capture installed state from a running world into tarballs").option(
19513
19800
  "--kind <kind>",
@@ -19574,17 +19861,17 @@ function resolveKinds(arg) {
19574
19861
  return [];
19575
19862
  }
19576
19863
  async function captureGems(worldId, workspacePath, repo) {
19577
- const repoDir = path31.join(workspacePath, repo);
19864
+ const repoDir = path33.join(workspacePath, repo);
19578
19865
  const fingerprint = computeGemsFingerprint(repoDir);
19579
19866
  if (!fingerprint) {
19580
19867
  return { ok: false, tarPath: "", msg: "no Gemfile.lock \u2014 layer does not apply" };
19581
19868
  }
19582
19869
  const tarPath = snapshotTarPath(worldId, "gems", repo, fingerprint);
19583
- const vendorBundle = path31.join(repoDir, "vendor", "bundle");
19584
- if (fs27.existsSync(vendorBundle)) {
19870
+ const vendorBundle = path33.join(repoDir, "vendor", "bundle");
19871
+ if (fs29.existsSync(vendorBundle)) {
19585
19872
  try {
19586
19873
  packTarball(vendorBundle, tarPath);
19587
- const stat = fs27.statSync(tarPath);
19874
+ const stat = fs29.statSync(tarPath);
19588
19875
  const manifest = {
19589
19876
  kind: "gems",
19590
19877
  worldId,
@@ -19617,10 +19904,10 @@ async function captureGems(worldId, workspacePath, repo) {
19617
19904
  `docker exec ${containerName} sh -c 'mkdir -p "$(dirname ${tmpTar})" && tar -czf ${tmpTar}.tmp -C ${bundlePath} . && mv ${tmpTar}.tmp ${tmpTar}'`,
19618
19905
  { stdio: "pipe", timeout: 12e4 }
19619
19906
  );
19620
- fs27.mkdirSync(path31.dirname(tarPath), { recursive: true });
19907
+ fs29.mkdirSync(path33.dirname(tarPath), { recursive: true });
19621
19908
  execSync9(`docker cp ${containerName}:${tmpTar} "${tarPath}"`, { stdio: "pipe", timeout: 12e4 });
19622
19909
  execSync9(`docker exec ${containerName} rm -f ${tmpTar}`, { stdio: "pipe" });
19623
- const stat = fs27.statSync(tarPath);
19910
+ const stat = fs29.statSync(tarPath);
19624
19911
  const manifest = {
19625
19912
  kind: "gems",
19626
19913
  worldId,
@@ -19637,19 +19924,19 @@ async function captureGems(worldId, workspacePath, repo) {
19637
19924
  }
19638
19925
  }
19639
19926
  async function captureNode(worldId, workspacePath, repo) {
19640
- const repoDir = path31.join(workspacePath, repo);
19927
+ const repoDir = path33.join(workspacePath, repo);
19641
19928
  const fingerprint = computeNodeFingerprint(repoDir);
19642
19929
  if (!fingerprint) {
19643
19930
  return { ok: false, tarPath: "", msg: "no lockfile \u2014 layer does not apply" };
19644
19931
  }
19645
- const nodeModules = path31.join(repoDir, "node_modules");
19646
- if (!fs27.existsSync(nodeModules)) {
19932
+ const nodeModules = path33.join(repoDir, "node_modules");
19933
+ if (!fs29.existsSync(nodeModules)) {
19647
19934
  return { ok: false, tarPath: "", msg: "node_modules not installed yet" };
19648
19935
  }
19649
19936
  const tarPath = snapshotTarPath(worldId, "node", repo, fingerprint);
19650
19937
  try {
19651
19938
  packTarball(nodeModules, tarPath);
19652
- const stat = fs27.statSync(tarPath);
19939
+ const stat = fs29.statSync(tarPath);
19653
19940
  const manifest = {
19654
19941
  kind: "node",
19655
19942
  worldId,
@@ -19666,7 +19953,7 @@ async function captureNode(worldId, workspacePath, repo) {
19666
19953
  }
19667
19954
  }
19668
19955
  async function capturePg(worldId, workspacePath, repoNames) {
19669
- const repoDirs = repoNames.map((r) => path31.join(workspacePath, r));
19956
+ const repoDirs = repoNames.map((r) => path33.join(workspacePath, r));
19670
19957
  const fingerprint = computePgFingerprint(repoDirs);
19671
19958
  if (!fingerprint) {
19672
19959
  return { ok: false, tarPath: "", msg: "no Gemfile.lock / schema.rb \u2014 layer does not apply" };
@@ -19681,13 +19968,13 @@ async function capturePg(worldId, workspacePath, repoNames) {
19681
19968
  }
19682
19969
  try {
19683
19970
  execSync9(`docker stop ${containerName}`, { stdio: "pipe", timeout: 3e4 });
19684
- fs27.mkdirSync(path31.dirname(tarPath), { recursive: true });
19971
+ fs29.mkdirSync(path33.dirname(tarPath), { recursive: true });
19685
19972
  execSync9(
19686
- `docker run --rm -v "${volumeName2}:/pgdata:ro" -v "${path31.dirname(tarPath)}:/dest" alpine sh -c 'tar -czf /dest/${path31.basename(tarPath)}.tmp -C /pgdata . && mv /dest/${path31.basename(tarPath)}.tmp /dest/${path31.basename(tarPath)}'`,
19973
+ `docker run --rm -v "${volumeName2}:/pgdata:ro" -v "${path33.dirname(tarPath)}:/dest" alpine sh -c 'tar -czf /dest/${path33.basename(tarPath)}.tmp -C /pgdata . && mv /dest/${path33.basename(tarPath)}.tmp /dest/${path33.basename(tarPath)}'`,
19687
19974
  { stdio: "pipe", timeout: 18e4 }
19688
19975
  );
19689
19976
  execSync9(`docker start ${containerName}`, { stdio: "pipe", timeout: 3e4 });
19690
- const stat = fs27.statSync(tarPath);
19977
+ const stat = fs29.statSync(tarPath);
19691
19978
  const manifest = {
19692
19979
  kind: "pg",
19693
19980
  worldId,
@@ -19761,35 +20048,36 @@ function formatAge2(ms) {
19761
20048
 
19762
20049
  // src/commands/refresh.ts
19763
20050
  init_context();
19764
- import * as fs29 from "node:fs";
19765
- import * as os17 from "node:os";
19766
- import * as path33 from "node:path";
20051
+ init_output();
20052
+ import * as fs31 from "node:fs";
20053
+ import * as os18 from "node:os";
20054
+ import * as path35 from "node:path";
19767
20055
  import { spawnSync as spawnSync12 } from "node:child_process";
19768
20056
  import ora8 from "ora";
19769
20057
 
19770
20058
  // src/commands/refresh-helpers.ts
19771
- import * as fs28 from "node:fs";
19772
- import * as path32 from "node:path";
20059
+ import * as fs30 from "node:fs";
20060
+ import * as path34 from "node:path";
19773
20061
  function collectCpSourceFiles(standaloneDir) {
19774
- if (!fs28.existsSync(standaloneDir)) {
20062
+ if (!fs30.existsSync(standaloneDir)) {
19775
20063
  throw new Error(`CP standalone dir not found: ${standaloneDir}`);
19776
20064
  }
19777
20065
  const entries = [];
19778
- const topLevel = fs28.readdirSync(standaloneDir).filter((f) => {
19779
- const stat = fs28.statSync(path32.join(standaloneDir, f));
20066
+ const topLevel = fs30.readdirSync(standaloneDir).filter((f) => {
20067
+ const stat = fs30.statSync(path34.join(standaloneDir, f));
19780
20068
  return stat.isFile() && f.endsWith(".mjs") && !f.endsWith(".test.mjs");
19781
20069
  }).sort();
19782
20070
  for (const f of topLevel) {
19783
- entries.push({ srcPath: path32.join(standaloneDir, f), destRelPath: f });
20071
+ entries.push({ srcPath: path34.join(standaloneDir, f), destRelPath: f });
19784
20072
  }
19785
- const libDir = path32.join(standaloneDir, "lib");
19786
- if (fs28.existsSync(libDir) && fs28.statSync(libDir).isDirectory()) {
19787
- const libFiles = fs28.readdirSync(libDir).filter((f) => {
19788
- const stat = fs28.statSync(path32.join(libDir, f));
20073
+ const libDir = path34.join(standaloneDir, "lib");
20074
+ if (fs30.existsSync(libDir) && fs30.statSync(libDir).isDirectory()) {
20075
+ const libFiles = fs30.readdirSync(libDir).filter((f) => {
20076
+ const stat = fs30.statSync(path34.join(libDir, f));
19789
20077
  return stat.isFile() && f.endsWith(".mjs") && !f.endsWith(".test.mjs");
19790
20078
  }).sort();
19791
20079
  for (const f of libFiles) {
19792
- entries.push({ srcPath: path32.join(libDir, f), destRelPath: `lib/${f}` });
20080
+ entries.push({ srcPath: path34.join(libDir, f), destRelPath: `lib/${f}` });
19793
20081
  }
19794
20082
  }
19795
20083
  return entries;
@@ -19847,16 +20135,16 @@ async function refreshWorld(worldId, portOffset, standaloneDir, opts) {
19847
20135
  error: err instanceof Error ? err.message : String(err)
19848
20136
  };
19849
20137
  }
19850
- const stagingDir = fs29.mkdtempSync(
19851
- path33.join(os17.tmpdir(), `olam-refresh-${worldId}-`)
20138
+ const stagingDir = fs31.mkdtempSync(
20139
+ path35.join(os18.tmpdir(), `olam-refresh-${worldId}-`)
19852
20140
  );
19853
20141
  try {
19854
20142
  const hasLib = entries.some((e) => e.destRelPath.startsWith("lib/"));
19855
20143
  if (hasLib) {
19856
- fs29.mkdirSync(path33.join(stagingDir, "lib"), { recursive: true });
20144
+ fs31.mkdirSync(path35.join(stagingDir, "lib"), { recursive: true });
19857
20145
  }
19858
20146
  for (const { srcPath, destRelPath } of entries) {
19859
- fs29.copyFileSync(srcPath, path33.join(stagingDir, destRelPath));
20147
+ fs31.copyFileSync(srcPath, path35.join(stagingDir, destRelPath));
19860
20148
  }
19861
20149
  const cpResult = docker([
19862
20150
  "cp",
@@ -19871,7 +20159,7 @@ async function refreshWorld(worldId, portOffset, standaloneDir, opts) {
19871
20159
  };
19872
20160
  }
19873
20161
  } finally {
19874
- fs29.rmSync(stagingDir, { recursive: true, force: true });
20162
+ fs31.rmSync(stagingDir, { recursive: true, force: true });
19875
20163
  }
19876
20164
  if (opts.restart) {
19877
20165
  const restartResult = docker([
@@ -19908,11 +20196,11 @@ function registerRefresh(program2) {
19908
20196
  process.exitCode = 1;
19909
20197
  return;
19910
20198
  }
19911
- const standaloneDir = path33.join(
20199
+ const standaloneDir = path35.join(
19912
20200
  process.cwd(),
19913
20201
  "packages/control-plane/standalone"
19914
20202
  );
19915
- if (!fs29.existsSync(standaloneDir)) {
20203
+ if (!fs31.existsSync(standaloneDir)) {
19916
20204
  printError(
19917
20205
  `CP standalone source not found at ${standaloneDir}.
19918
20206
  Run \`olam refresh\` from the olam repo root.`
@@ -19991,9 +20279,9 @@ Run \`olam refresh\` from the olam repo root.`
19991
20279
  }
19992
20280
 
19993
20281
  // src/commands/diagnose.ts
19994
- import * as fs30 from "node:fs";
19995
- import * as os18 from "node:os";
19996
- import * as path34 from "node:path";
20282
+ import * as fs32 from "node:fs";
20283
+ import * as os19 from "node:os";
20284
+ import * as path36 from "node:path";
19997
20285
  import { execFileSync as execFileSync5, execSync as execSync10 } from "node:child_process";
19998
20286
  import pc20 from "picocolors";
19999
20287
 
@@ -20028,9 +20316,9 @@ function stripSecrets(input) {
20028
20316
  }
20029
20317
 
20030
20318
  // src/commands/diagnose.ts
20031
- var DIAGNOSTICS_DIR = path34.join(os18.homedir(), ".olam", "diagnostics");
20032
- var LOG_DIR = path34.join(os18.homedir(), ".olam", "log");
20033
- var CACHE_DIR = path34.join(os18.homedir(), ".olam", "cache");
20319
+ var DIAGNOSTICS_DIR = path36.join(os19.homedir(), ".olam", "diagnostics");
20320
+ var LOG_DIR = path36.join(os19.homedir(), ".olam", "log");
20321
+ var CACHE_DIR = path36.join(os19.homedir(), ".olam", "cache");
20034
20322
  var LOG_TAIL_LINES = 200;
20035
20323
  function safeExec(cmd) {
20036
20324
  try {
@@ -20044,10 +20332,10 @@ function defaultZip(zipPath, files) {
20044
20332
  }
20045
20333
  async function buildDiagnosticsZip(_exec = (cmd) => safeExec(cmd), _outDir = DIAGNOSTICS_DIR, _logDir = LOG_DIR, _zip = defaultZip) {
20046
20334
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 23);
20047
- const zipPath = path34.join(_outDir, `olam-diag-${ts}.zip`);
20048
- const tmpDir = fs30.mkdtempSync(path34.join(os18.tmpdir(), "olam-diag-"));
20335
+ const zipPath = path36.join(_outDir, `olam-diag-${ts}.zip`);
20336
+ const tmpDir = fs32.mkdtempSync(path36.join(os19.tmpdir(), "olam-diag-"));
20049
20337
  try {
20050
- fs30.mkdirSync(_outDir, { recursive: true });
20338
+ fs32.mkdirSync(_outDir, { recursive: true });
20051
20339
  const entries = [];
20052
20340
  const version = process.env["OLAM_CLI_VERSION"] ?? "unknown";
20053
20341
  const nodeVersion = process.version;
@@ -20058,19 +20346,19 @@ platform: ${platform}
20058
20346
  `;
20059
20347
  _writeEntry(tmpDir, "version.txt", stripSecrets(versionContent), entries);
20060
20348
  const osContent = [
20061
- `os: ${os18.type()} ${os18.release()}`,
20062
- `arch: ${os18.arch()}`,
20063
- `uptime: ${os18.uptime()}s`
20349
+ `os: ${os19.type()} ${os19.release()}`,
20350
+ `arch: ${os19.arch()}`,
20351
+ `uptime: ${os19.uptime()}s`
20064
20352
  ].join("\n") + "\n";
20065
20353
  _writeEntry(tmpDir, "os-info.txt", stripSecrets(osContent), entries);
20066
- const depsFile = path34.join(CACHE_DIR, "deps.json");
20067
- if (fs30.existsSync(depsFile)) {
20068
- const deps = fs30.readFileSync(depsFile, "utf-8");
20354
+ const depsFile = path36.join(CACHE_DIR, "deps.json");
20355
+ if (fs32.existsSync(depsFile)) {
20356
+ const deps = fs32.readFileSync(depsFile, "utf-8");
20069
20357
  _writeEntry(tmpDir, "deps.json", stripSecrets(deps), entries);
20070
20358
  }
20071
20359
  const latestLog = _latestLog(_logDir);
20072
20360
  if (latestLog) {
20073
- const lines = fs30.readFileSync(latestLog, "utf-8").split("\n");
20361
+ const lines = fs32.readFileSync(latestLog, "utf-8").split("\n");
20074
20362
  const tail = lines.slice(-LOG_TAIL_LINES).join("\n");
20075
20363
  _writeEntry(tmpDir, "log-tail.txt", stripSecrets(tail), entries);
20076
20364
  }
@@ -20082,38 +20370,38 @@ platform: ${platform}
20082
20370
  if (authAudit) {
20083
20371
  _writeEntry(tmpDir, "audit-auth-callers.txt", stripSecrets(authAudit), entries);
20084
20372
  }
20085
- const fileArgs = entries.map((e) => path34.join(tmpDir, e));
20373
+ const fileArgs = entries.map((e) => path36.join(tmpDir, e));
20086
20374
  try {
20087
20375
  _zip(zipPath, fileArgs);
20088
20376
  } catch (err) {
20089
20377
  throw new Error(`zip command produced no output file. zip stderr: ${err.message}`);
20090
20378
  }
20091
- if (!fs30.existsSync(zipPath)) {
20379
+ if (!fs32.existsSync(zipPath)) {
20092
20380
  throw new Error("zip command produced no output file.");
20093
20381
  }
20094
20382
  return { zipPath, entries };
20095
20383
  } finally {
20096
20384
  try {
20097
- fs30.rmSync(tmpDir, { recursive: true, force: true });
20385
+ fs32.rmSync(tmpDir, { recursive: true, force: true });
20098
20386
  } catch {
20099
20387
  }
20100
20388
  }
20101
20389
  }
20102
20390
  function _writeEntry(dir, name, content, entries) {
20103
- fs30.writeFileSync(path34.join(dir, name), content, { mode: 420 });
20391
+ fs32.writeFileSync(path36.join(dir, name), content, { mode: 420 });
20104
20392
  entries.push(name);
20105
20393
  }
20106
20394
  function _latestLog(logDir) {
20107
- if (!fs30.existsSync(logDir)) return null;
20108
- const files = fs30.readdirSync(logDir).filter((f) => f.startsWith("host-")).sort().reverse();
20109
- return files.length > 0 ? path34.join(logDir, files[0]) : null;
20395
+ if (!fs32.existsSync(logDir)) return null;
20396
+ const files = fs32.readdirSync(logDir).filter((f) => f.startsWith("host-")).sort().reverse();
20397
+ return files.length > 0 ? path36.join(logDir, files[0]) : null;
20110
20398
  }
20111
20399
  async function buildTelemetryPayload() {
20112
20400
  const channel = "stable";
20113
- const manifestFile = path34.join(CACHE_DIR, "manifest.json");
20401
+ const manifestFile = path36.join(CACHE_DIR, "manifest.json");
20114
20402
  let manifestAgeHours = null;
20115
- if (fs30.existsSync(manifestFile)) {
20116
- const mtime = fs30.statSync(manifestFile).mtime.getTime();
20403
+ if (fs32.existsSync(manifestFile)) {
20404
+ const mtime = fs32.statSync(manifestFile).mtime.getTime();
20117
20405
  manifestAgeHours = Math.round((Date.now() - mtime) / 36e5);
20118
20406
  }
20119
20407
  return {
@@ -20156,24 +20444,24 @@ function registerDiagnose(program2) {
20156
20444
  }
20157
20445
 
20158
20446
  // src/commands/update.ts
20159
- import * as fs33 from "node:fs";
20160
- import * as os20 from "node:os";
20161
- import * as path37 from "node:path";
20447
+ import * as fs35 from "node:fs";
20448
+ import * as os21 from "node:os";
20449
+ import * as path39 from "node:path";
20162
20450
  import { execSync as execSync11 } from "node:child_process";
20163
20451
  import pc21 from "picocolors";
20164
20452
 
20165
20453
  // src/lib/symlink-reconcile.ts
20166
- import * as fs31 from "node:fs";
20167
- import * as path35 from "node:path";
20454
+ import * as fs33 from "node:fs";
20455
+ import * as path37 from "node:path";
20168
20456
  var realFs = {
20169
- readdirSync: (p) => fs31.readdirSync(p),
20170
- existsSync: (p) => fs31.existsSync(p),
20171
- lstatSync: (p) => fs31.lstatSync(p),
20172
- readlinkSync: (p) => fs31.readlinkSync(p),
20173
- symlinkSync: (t, l) => fs31.symlinkSync(t, l),
20174
- unlinkSync: (p) => fs31.unlinkSync(p),
20457
+ readdirSync: (p) => fs33.readdirSync(p),
20458
+ existsSync: (p) => fs33.existsSync(p),
20459
+ lstatSync: (p) => fs33.lstatSync(p),
20460
+ readlinkSync: (p) => fs33.readlinkSync(p),
20461
+ symlinkSync: (t, l) => fs33.symlinkSync(t, l),
20462
+ unlinkSync: (p) => fs33.unlinkSync(p),
20175
20463
  mkdirSync: (p, o) => {
20176
- fs31.mkdirSync(p, o);
20464
+ fs33.mkdirSync(p, o);
20177
20465
  }
20178
20466
  };
20179
20467
  function reconcileSkillSymlinks(npmSkillsDir, claudeSkillsDir, _fs = realFs) {
@@ -20183,8 +20471,8 @@ function reconcileSkillSymlinks(npmSkillsDir, claudeSkillsDir, _fs = realFs) {
20183
20471
  _fs.mkdirSync(claudeSkillsDir, { recursive: true });
20184
20472
  const sourceSkills = _fs.existsSync(npmSkillsDir) ? _fs.readdirSync(npmSkillsDir).filter((d) => d.startsWith("olam-")) : [];
20185
20473
  for (const skill of sourceSkills) {
20186
- const linkPath = path35.join(claudeSkillsDir, skill);
20187
- const target = path35.join(npmSkillsDir, skill);
20474
+ const linkPath = path37.join(claudeSkillsDir, skill);
20475
+ const target = path37.join(npmSkillsDir, skill);
20188
20476
  if (!_fs.existsSync(linkPath)) {
20189
20477
  try {
20190
20478
  _fs.symlinkSync(target, linkPath);
@@ -20197,7 +20485,7 @@ function reconcileSkillSymlinks(npmSkillsDir, claudeSkillsDir, _fs = realFs) {
20197
20485
  }
20198
20486
  const deployedEntries = _fs.existsSync(claudeSkillsDir) ? _fs.readdirSync(claudeSkillsDir).filter((d) => d.startsWith("olam-")) : [];
20199
20487
  for (const entry of deployedEntries) {
20200
- const linkPath = path35.join(claudeSkillsDir, entry);
20488
+ const linkPath = path37.join(claudeSkillsDir, entry);
20201
20489
  let isSymlink = false;
20202
20490
  try {
20203
20491
  isSymlink = _fs.lstatSync(linkPath).isSymbolicLink();
@@ -20222,9 +20510,9 @@ function reconcileSkillSymlinks(npmSkillsDir, claudeSkillsDir, _fs = realFs) {
20222
20510
 
20223
20511
  // src/commands/update.ts
20224
20512
  var PACKAGE_NAME = "@pleri/olam-cli";
20225
- var CACHE_DIR2 = path37.join(os20.homedir(), ".olam", "cache");
20226
- var LOG_DIR2 = path37.join(os20.homedir(), ".olam", "log");
20227
- var LAST_STABLE_FILE = path37.join(CACHE_DIR2, "last-stable.txt");
20513
+ var CACHE_DIR2 = path39.join(os21.homedir(), ".olam", "cache");
20514
+ var LOG_DIR2 = path39.join(os21.homedir(), ".olam", "log");
20515
+ var LAST_STABLE_FILE = path39.join(CACHE_DIR2, "last-stable.txt");
20228
20516
  function defaultExec(cmd) {
20229
20517
  try {
20230
20518
  const stdout = execSync11(cmd, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
@@ -20246,22 +20534,22 @@ function getCurrentVersion(_exec = defaultExec) {
20246
20534
  }
20247
20535
  function readLastStable(file = LAST_STABLE_FILE) {
20248
20536
  try {
20249
- const v = fs33.readFileSync(file, "utf-8").trim();
20537
+ const v = fs35.readFileSync(file, "utf-8").trim();
20250
20538
  return v || null;
20251
20539
  } catch {
20252
20540
  return null;
20253
20541
  }
20254
20542
  }
20255
20543
  function writeLastStable(version, file = LAST_STABLE_FILE) {
20256
- fs33.mkdirSync(path37.dirname(file), { recursive: true });
20257
- fs33.writeFileSync(file, version, { mode: 420 });
20544
+ fs35.mkdirSync(path39.dirname(file), { recursive: true });
20545
+ fs35.writeFileSync(file, version, { mode: 420 });
20258
20546
  }
20259
20547
  function logUpdateFailure(stderr) {
20260
20548
  const ts = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
20261
- const logFile = path37.join(LOG_DIR2, `update-${ts}.log`);
20549
+ const logFile = path39.join(LOG_DIR2, `update-${ts}.log`);
20262
20550
  try {
20263
- fs33.mkdirSync(LOG_DIR2, { recursive: true });
20264
- fs33.appendFileSync(logFile, `[update-failure ${(/* @__PURE__ */ new Date()).toISOString()}]
20551
+ fs35.mkdirSync(LOG_DIR2, { recursive: true });
20552
+ fs35.appendFileSync(logFile, `[update-failure ${(/* @__PURE__ */ new Date()).toISOString()}]
20265
20553
  ${stderr}
20266
20554
  `, "utf-8");
20267
20555
  } catch {
@@ -20348,8 +20636,8 @@ async function doUpdate(opts, _exec = defaultExec, _reconcile = reconcileSkillSy
20348
20636
  let symlinkResult = { added: [], removed: [] };
20349
20637
  if (npmRootResult.exitCode === 0) {
20350
20638
  const npmRoot = npmRootResult.stdout.trim();
20351
- const npmSkillsDir = path37.join(npmRoot, "@pleri", "olam-cli", "plugin", "skills");
20352
- const claudeSkillsDir = path37.join(os20.homedir(), ".claude", "skills");
20639
+ const npmSkillsDir = path39.join(npmRoot, "@pleri", "olam-cli", "plugin", "skills");
20640
+ const claudeSkillsDir = path39.join(os21.homedir(), ".claude", "skills");
20353
20641
  const rec = _reconcile(npmSkillsDir, claudeSkillsDir);
20354
20642
  symlinkResult = { added: rec.added, removed: rec.removed };
20355
20643
  if (!quiet && (rec.added.length > 0 || rec.removed.length > 0)) {
@@ -20395,8 +20683,8 @@ async function doRollback(_exec = defaultExec, _reconcile = reconcileSkillSymlin
20395
20683
  if (npmRootResult.exitCode === 0) {
20396
20684
  const npmRoot = npmRootResult.stdout.trim();
20397
20685
  _reconcile(
20398
- path37.join(npmRoot, "@pleri", "olam-cli", "plugin", "skills"),
20399
- path37.join(os20.homedir(), ".claude", "skills")
20686
+ path39.join(npmRoot, "@pleri", "olam-cli", "plugin", "skills"),
20687
+ path39.join(os21.homedir(), ".claude", "skills")
20400
20688
  );
20401
20689
  }
20402
20690
  return { action: "rolled-back", restoredVersion: prev, exitCode: 0 };
@@ -20448,19 +20736,119 @@ function registerUpdate(program2) {
20448
20736
  });
20449
20737
  }
20450
20738
 
20739
+ // src/commands/begin.ts
20740
+ init_host_cp();
20741
+ init_open_url();
20742
+ import pc22 from "picocolors";
20743
+ async function doBegin(opts, _startFn = startHostCp, _openFn = openUrl) {
20744
+ const prevExitCode = process.exitCode;
20745
+ await _startFn({ showToken: opts.showToken });
20746
+ const failed = process.exitCode !== void 0 && process.exitCode !== 0 && process.exitCode !== prevExitCode;
20747
+ if (failed) return false;
20748
+ if (opts.browser) {
20749
+ const token = readToken();
20750
+ if (token) {
20751
+ const base = `http://127.0.0.1:${HOST_CP_PORT}/`;
20752
+ console.log(pc22.dim(`Opening ${base}`));
20753
+ _openFn(`${base}?token=${token}`);
20754
+ }
20755
+ }
20756
+ return true;
20757
+ }
20758
+ function registerBegin(program2) {
20759
+ program2.command("begin").description("Start the Olam host control plane (alias: olam host-cp start)").option("--show-token", "Print token to stdout").option("--no-browser", "Skip opening browser").action(async (opts) => {
20760
+ await doBegin({
20761
+ showToken: opts.showToken === true,
20762
+ browser: opts.browser !== false
20763
+ });
20764
+ });
20765
+ }
20766
+
20767
+ // src/commands/stop.ts
20768
+ init_host_cp();
20769
+ async function doStop(_stopFn = stopHostCp) {
20770
+ await _stopFn();
20771
+ }
20772
+ function registerStop(program2) {
20773
+ program2.command("stop").description("Stop the Olam host control plane (alias: olam host-cp stop)").action(async () => {
20774
+ await doStop();
20775
+ });
20776
+ }
20777
+
20778
+ // src/commands/world-upgrade.ts
20779
+ init_output();
20780
+ init_version_pin();
20781
+ async function doWorldUpgrade(opts) {
20782
+ const version = opts.version ?? CLI_VERSION;
20783
+ const stamp = opts.stampPin ?? ((p, v) => stampVersionPin(p, v));
20784
+ let targets;
20785
+ if (opts.all) {
20786
+ targets = opts.getWorlds();
20787
+ } else if (opts.slug) {
20788
+ const w = opts.getWorld(opts.slug);
20789
+ if (!w) return [];
20790
+ targets = [w];
20791
+ } else {
20792
+ return [];
20793
+ }
20794
+ return targets.map((w) => {
20795
+ const stamped = opts.dryRun ? false : stamp(w.workspacePath, version);
20796
+ return { worldId: w.id, stamped, dryRun: opts.dryRun === true };
20797
+ });
20798
+ }
20799
+ function registerWorldUpgrade(program2) {
20800
+ const world = getOrCreateWorldCommand(program2);
20801
+ world.command("upgrade [slug]").description("Refresh cli_version pin in world.yaml to current CLI version").option("--all", "Upgrade all worlds").option("--dry-run", "Preview which worlds would be upgraded without writing").action(async (slug, opts) => {
20802
+ const { loadContext: loadContext2 } = await Promise.resolve().then(() => (init_context(), context_exports));
20803
+ const { ctx, error } = await loadContext2();
20804
+ if (!ctx) {
20805
+ printError(error?.message ?? "Olam is not configured. Run `olam init` first.");
20806
+ process.exitCode = 1;
20807
+ return;
20808
+ }
20809
+ if (!slug && !opts.all) {
20810
+ printError("Specify a world slug or --all.");
20811
+ process.exitCode = 1;
20812
+ return;
20813
+ }
20814
+ const results = await doWorldUpgrade({
20815
+ slug,
20816
+ all: opts.all,
20817
+ dryRun: opts.dryRun,
20818
+ getWorlds: () => ctx.worldManager.listWorlds(),
20819
+ getWorld: (id) => ctx.worldManager.getWorld(id)
20820
+ });
20821
+ if (results.length === 0) {
20822
+ printError(`World "${slug}" not found.`);
20823
+ process.exitCode = 1;
20824
+ return;
20825
+ }
20826
+ printHeader(`World upgrade ${opts.dryRun ? "(dry run)" : ""}`);
20827
+ for (const r of results) {
20828
+ if (opts.dryRun) {
20829
+ printInfo(r.worldId, "would upgrade");
20830
+ } else if (r.stamped) {
20831
+ printInfo(r.worldId, `stamped \u2192 v${CLI_VERSION}`);
20832
+ } else {
20833
+ printInfo(r.worldId, `already at v${CLI_VERSION} (no-op)`);
20834
+ }
20835
+ }
20836
+ });
20837
+ }
20838
+
20451
20839
  // src/pleri-config.ts
20452
- import * as fs34 from "node:fs";
20453
- import * as path38 from "node:path";
20840
+ import * as fs36 from "node:fs";
20841
+ import * as path40 from "node:path";
20454
20842
  function isPleriConfigured(configDir = process.env.OLAM_CONFIG_DIR ?? ".olam") {
20455
20843
  if (process.env.PLERI_BASE_URL) {
20456
20844
  return true;
20457
20845
  }
20458
- const configPath = path38.join(configDir, "config.yaml");
20459
- if (!fs34.existsSync(configPath)) {
20846
+ const configPath = path40.join(configDir, "config.yaml");
20847
+ if (!fs36.existsSync(configPath)) {
20460
20848
  return false;
20461
20849
  }
20462
20850
  try {
20463
- const contents = fs34.readFileSync(configPath, "utf8");
20851
+ const contents = fs36.readFileSync(configPath, "utf8");
20464
20852
  return /^[^#\n]*\bpleri:/m.test(contents);
20465
20853
  } catch {
20466
20854
  return false;
@@ -20471,14 +20859,14 @@ function isPleriConfigured(configDir = process.env.OLAM_CONFIG_DIR ?? ".olam") {
20471
20859
  var program = new Command();
20472
20860
  function readCliVersion() {
20473
20861
  try {
20474
- const here = path39.dirname(fileURLToPath4(import.meta.url));
20862
+ const here = path41.dirname(fileURLToPath4(import.meta.url));
20475
20863
  for (const candidate of [
20476
- path39.join(here, "package.json"),
20477
- path39.join(here, "..", "package.json"),
20478
- path39.join(here, "..", "..", "package.json")
20864
+ path41.join(here, "package.json"),
20865
+ path41.join(here, "..", "package.json"),
20866
+ path41.join(here, "..", "..", "package.json")
20479
20867
  ]) {
20480
- if (fs35.existsSync(candidate)) {
20481
- const pkg = JSON.parse(fs35.readFileSync(candidate, "utf-8"));
20868
+ if (fs37.existsSync(candidate)) {
20869
+ const pkg = JSON.parse(fs37.readFileSync(candidate, "utf-8"));
20482
20870
  if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
20483
20871
  }
20484
20872
  }
@@ -20512,4 +20900,7 @@ registerRefresh(program);
20512
20900
  registerBootstrap(program);
20513
20901
  registerDiagnose(program);
20514
20902
  registerUpdate(program);
20903
+ registerBegin(program);
20904
+ registerStop(program);
20905
+ registerWorldUpgrade(program);
20515
20906
  program.parse();