@pleri/olam-cli 0.1.101 → 0.1.103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -487,8 +487,8 @@ var init_parseUtil = __esm({
487
487
  init_errors();
488
488
  init_en();
489
489
  makeIssue = (params) => {
490
- const { data, path: path49, errorMaps, issueData } = params;
491
- const fullPath = [...path49, ...issueData.path || []];
490
+ const { data, path: path50, errorMaps, issueData } = params;
491
+ const fullPath = [...path50, ...issueData.path || []];
492
492
  const fullIssue = {
493
493
  ...issueData,
494
494
  path: fullPath
@@ -796,11 +796,11 @@ var init_types = __esm({
796
796
  init_parseUtil();
797
797
  init_util();
798
798
  ParseInputLazyPath = class {
799
- constructor(parent, value, path49, key) {
799
+ constructor(parent, value, path50, key) {
800
800
  this._cachedPath = [];
801
801
  this.parent = parent;
802
802
  this.data = value;
803
- this._path = path49;
803
+ this._path = path50;
804
804
  this._key = key;
805
805
  }
806
806
  get path() {
@@ -4281,7 +4281,7 @@ import YAML from "yaml";
4281
4281
  function bootstrapStepCmd(entry) {
4282
4282
  return typeof entry === "string" ? entry : entry.cmd;
4283
4283
  }
4284
- function refineForbiddenKeys(value, path49, ctx, rejectSource) {
4284
+ function refineForbiddenKeys(value, path50, ctx, rejectSource) {
4285
4285
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
4286
4286
  return;
4287
4287
  }
@@ -4289,12 +4289,12 @@ function refineForbiddenKeys(value, path49, ctx, rejectSource) {
4289
4289
  if (FORBIDDEN_KEYS.has(key)) {
4290
4290
  ctx.addIssue({
4291
4291
  code: external_exports.ZodIssueCode.custom,
4292
- path: [...path49, key],
4292
+ path: [...path50, key],
4293
4293
  message: `forbidden key "${key}" (prototype-pollution surface)`
4294
4294
  });
4295
4295
  continue;
4296
4296
  }
4297
- if (rejectSource && path49.length === 0 && key === "source") {
4297
+ if (rejectSource && path50.length === 0 && key === "source") {
4298
4298
  ctx.addIssue({
4299
4299
  code: external_exports.ZodIssueCode.custom,
4300
4300
  path: ["source"],
@@ -4302,21 +4302,21 @@ function refineForbiddenKeys(value, path49, ctx, rejectSource) {
4302
4302
  });
4303
4303
  continue;
4304
4304
  }
4305
- refineForbiddenKeys(value[key], [...path49, key], ctx, false);
4305
+ refineForbiddenKeys(value[key], [...path50, key], ctx, false);
4306
4306
  }
4307
4307
  }
4308
- function rejectForbiddenKeys(value, path49, rejectSource) {
4308
+ function rejectForbiddenKeys(value, path50, rejectSource) {
4309
4309
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
4310
4310
  return;
4311
4311
  }
4312
4312
  for (const key of Object.keys(value)) {
4313
4313
  if (FORBIDDEN_KEYS.has(key)) {
4314
- throw new Error(`[manifest] ${path49}: forbidden key "${key}" (prototype-pollution surface)`);
4314
+ throw new Error(`[manifest] ${path50}: forbidden key "${key}" (prototype-pollution surface)`);
4315
4315
  }
4316
4316
  if (rejectSource && key === "source") {
4317
- throw new Error(`[manifest] ${path49}: top-level "source" is loader-stamped \u2014 manifests must not author it`);
4317
+ throw new Error(`[manifest] ${path50}: top-level "source" is loader-stamped \u2014 manifests must not author it`);
4318
4318
  }
4319
- rejectForbiddenKeys(value[key], `${path49}.${key}`, false);
4319
+ rejectForbiddenKeys(value[key], `${path50}.${key}`, false);
4320
4320
  }
4321
4321
  }
4322
4322
  function unknownTopLevelKeys(parsed) {
@@ -5198,7 +5198,7 @@ async function safeText(res) {
5198
5198
  }
5199
5199
  }
5200
5200
  function sleep(ms) {
5201
- return new Promise((resolve10) => setTimeout(resolve10, ms));
5201
+ return new Promise((resolve11) => setTimeout(resolve11, ms));
5202
5202
  }
5203
5203
  var DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, RETRY_COUNT, RETRY_BACKOFF_MS, AuthClient;
5204
5204
  var init_client = __esm({
@@ -5309,8 +5309,8 @@ var init_client = __esm({
5309
5309
  throw new Error(`failed to report rate-limit for ${accountId} (HTTP ${res.status})`);
5310
5310
  }
5311
5311
  }
5312
- async request(method, path49, body, attempt = 0) {
5313
- const url = `${this.baseUrl}${path49}`;
5312
+ async request(method, path50, body, attempt = 0) {
5313
+ const url = `${this.baseUrl}${path50}`;
5314
5314
  const controller = new AbortController();
5315
5315
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
5316
5316
  const headers = {};
@@ -5328,7 +5328,7 @@ var init_client = __esm({
5328
5328
  } catch (err) {
5329
5329
  if (attempt < RETRY_COUNT && isTransient(err)) {
5330
5330
  await sleep(RETRY_BACKOFF_MS * (attempt + 1));
5331
- return this.request(method, path49, body, attempt + 1);
5331
+ return this.request(method, path50, body, attempt + 1);
5332
5332
  }
5333
5333
  throw err;
5334
5334
  } finally {
@@ -5350,7 +5350,7 @@ function resolveAuthServicePath() {
5350
5350
  return path9.join(pkgsDir, "auth-service");
5351
5351
  }
5352
5352
  function sleep2(ms) {
5353
- return new Promise((resolve10) => setTimeout(resolve10, ms));
5353
+ return new Promise((resolve11) => setTimeout(resolve11, ms));
5354
5354
  }
5355
5355
  var DEFAULT_PORT, DEFAULT_VOLUME, DEFAULT_CONTAINER, DEFAULT_IMAGE, AuthContainerController;
5356
5356
  var init_container = __esm({
@@ -5798,7 +5798,7 @@ var init_container2 = __esm({
5798
5798
  await container.start();
5799
5799
  return container;
5800
5800
  };
5801
- DEFAULT_IMAGE2 = "olam-devbox:latest";
5801
+ DEFAULT_IMAGE2 = "olam-devbox:base";
5802
5802
  CONTROL_PLANE_PORT = 8080;
5803
5803
  HOST_CONTROL_PLANE_BASE = 19080;
5804
5804
  HOST_APP_PORT_BASE_DELTA = 1e4;
@@ -5951,7 +5951,7 @@ var demuxStream, execInContainer;
5951
5951
  var init_exec = __esm({
5952
5952
  "../adapters/dist/docker/exec.js"() {
5953
5953
  "use strict";
5954
- demuxStream = (stream) => new Promise((resolve10, reject) => {
5954
+ demuxStream = (stream) => new Promise((resolve11, reject) => {
5955
5955
  const stdoutChunks = [];
5956
5956
  const stderrChunks = [];
5957
5957
  const stdout = new PassThrough();
@@ -5965,7 +5965,7 @@ var init_exec = __esm({
5965
5965
  stream.pipe(stdout);
5966
5966
  }
5967
5967
  stream.on("end", () => {
5968
- resolve10({
5968
+ resolve11({
5969
5969
  stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
5970
5970
  stderr: Buffer.concat(stderrChunks).toString("utf-8")
5971
5971
  });
@@ -6003,6 +6003,51 @@ var init_exec = __esm({
6003
6003
  }
6004
6004
  });
6005
6005
 
6006
+ // ../adapters/dist/docker/host.js
6007
+ import { spawnSync as spawnSync2 } from "node:child_process";
6008
+ function resolveDockerHostOptions(spawnImpl = spawnSync2) {
6009
+ if (process.env.DOCKER_HOST && process.env.DOCKER_HOST.length > 0) {
6010
+ return {};
6011
+ }
6012
+ try {
6013
+ const result = spawnImpl("docker", ["context", "inspect", "--format", "{{.Endpoints.docker.Host}}"], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
6014
+ if (result.status === 0) {
6015
+ const host = (result.stdout ?? "").trim();
6016
+ if (host.startsWith("unix://")) {
6017
+ return { socketPath: host.slice("unix://".length) };
6018
+ }
6019
+ if (host.startsWith("npipe://")) {
6020
+ return { socketPath: host.slice("npipe://".length) };
6021
+ }
6022
+ if (host.startsWith("tcp://") || host.startsWith("http://") || host.startsWith("https://")) {
6023
+ const url = new URL(host.startsWith("tcp://") ? host.replace("tcp://", "http://") : host);
6024
+ const protocol = host.startsWith("https://") ? "https" : "http";
6025
+ const port = url.port ? parseInt(url.port, 10) : protocol === "https" ? 2376 : 2375;
6026
+ return { host: url.hostname, port, protocol };
6027
+ }
6028
+ }
6029
+ } catch {
6030
+ }
6031
+ return { socketPath: "/var/run/docker.sock" };
6032
+ }
6033
+ function describeDockerEndpoint(opts) {
6034
+ if (!opts || Object.keys(opts).length === 0) {
6035
+ return process.env.DOCKER_HOST ? `DOCKER_HOST=${process.env.DOCKER_HOST}` : "/var/run/docker.sock (default)";
6036
+ }
6037
+ if (opts.socketPath) {
6038
+ return `socketPath=${opts.socketPath}`;
6039
+ }
6040
+ if (opts.host) {
6041
+ return `${opts.protocol ?? "http"}://${opts.host}:${opts.port ?? "?"}`;
6042
+ }
6043
+ return JSON.stringify(opts);
6044
+ }
6045
+ var init_host = __esm({
6046
+ "../adapters/dist/docker/host.js"() {
6047
+ "use strict";
6048
+ }
6049
+ });
6050
+
6006
6051
  // ../adapters/dist/docker/provider.js
6007
6052
  import Dockerode from "dockerode";
6008
6053
  var DockerWorld, DockerProvider, dockerStateToWorldStatus;
@@ -6012,6 +6057,7 @@ var init_provider = __esm({
6012
6057
  init_types2();
6013
6058
  init_container2();
6014
6059
  init_exec();
6060
+ init_host();
6015
6061
  init_network();
6016
6062
  init_volume();
6017
6063
  DockerWorld = class {
@@ -6065,9 +6111,11 @@ var init_provider = __esm({
6065
6111
  maxLifetimeMinutes: null
6066
6112
  };
6067
6113
  docker;
6114
+ dockerOptions;
6068
6115
  constructor(dockerOptions) {
6069
6116
  super();
6070
6117
  this.docker = new Dockerode(dockerOptions);
6118
+ this.dockerOptions = dockerOptions;
6071
6119
  }
6072
6120
  // -----------------------------------------------------------------------
6073
6121
  // createWorld
@@ -6076,8 +6124,10 @@ var init_provider = __esm({
6076
6124
  const { id, name, services = [], portOffset = 0 } = config;
6077
6125
  try {
6078
6126
  await this.docker.ping();
6079
- } catch {
6080
- throw new Error("Docker daemon is not available. Ensure Docker is running and accessible.");
6127
+ } catch (err) {
6128
+ const where = describeDockerEndpoint(this.dockerOptions);
6129
+ const cause = err instanceof Error ? err.message : String(err);
6130
+ throw new Error(`Docker daemon is not available at ${where}. Ensure Docker is running and the resolved endpoint is correct. (underlying: ${cause})`);
6081
6131
  }
6082
6132
  await createNetwork(this.docker, id, name);
6083
6133
  for (const svc of services) {
@@ -6339,7 +6389,7 @@ var init_connection = __esm({
6339
6389
  // -----------------------------------------------------------------------
6340
6390
  async exec(host, command) {
6341
6391
  const client = await this.getConnection(host);
6342
- return new Promise((resolve10, reject) => {
6392
+ return new Promise((resolve11, reject) => {
6343
6393
  client.exec(command, (err, stream) => {
6344
6394
  if (err) {
6345
6395
  reject(new Error(`SSH exec failed on ${host}: ${err.message}`));
@@ -6354,7 +6404,7 @@ var init_connection = __esm({
6354
6404
  stderr += data.toString();
6355
6405
  });
6356
6406
  stream.on("close", (code) => {
6357
- resolve10({
6407
+ resolve11({
6358
6408
  exitCode: code ?? 0,
6359
6409
  stdout: stdout.trimEnd(),
6360
6410
  stderr: stderr.trimEnd()
@@ -6385,10 +6435,10 @@ var init_connection = __esm({
6385
6435
  throw new Error(`No SSH configuration found for host: ${host}`);
6386
6436
  }
6387
6437
  const client = new SSHClient();
6388
- return new Promise((resolve10, reject) => {
6438
+ return new Promise((resolve11, reject) => {
6389
6439
  client.on("ready", () => {
6390
6440
  this.connections.set(host, client);
6391
- resolve10(client);
6441
+ resolve11(client);
6392
6442
  }).on("error", (err) => {
6393
6443
  this.connections.delete(host);
6394
6444
  reject(new Error(`SSH connection to ${host} failed: ${err.message}`));
@@ -6827,8 +6877,8 @@ var init_provider3 = __esm({
6827
6877
  // -----------------------------------------------------------------------
6828
6878
  // Internal fetch helper
6829
6879
  // -----------------------------------------------------------------------
6830
- async request(path49, method, body) {
6831
- const url = `${this.config.workerUrl}${path49}`;
6880
+ async request(path50, method, body) {
6881
+ const url = `${this.config.workerUrl}${path50}`;
6832
6882
  const bearer = await this.config.mintToken();
6833
6883
  const headers = {
6834
6884
  Authorization: `Bearer ${bearer}`
@@ -6960,7 +7010,9 @@ __export(dist_exports, {
6960
7010
  SSHProvider: () => SSHProvider,
6961
7011
  auditPortsForZombies: () => auditPortsForZombies,
6962
7012
  buildPackageManagerCacheBinds: () => buildPackageManagerCacheBinds,
6963
- cleanupOrphanedResources: () => cleanupOrphanedResources
7013
+ cleanupOrphanedResources: () => cleanupOrphanedResources,
7014
+ describeDockerEndpoint: () => describeDockerEndpoint,
7015
+ resolveDockerHostOptions: () => resolveDockerHostOptions
6964
7016
  });
6965
7017
  var init_dist = __esm({
6966
7018
  "../adapters/dist/index.js"() {
@@ -6968,6 +7020,7 @@ var init_dist = __esm({
6968
7020
  init_types2();
6969
7021
  init_provider();
6970
7022
  init_cleanup();
7023
+ init_host();
6971
7024
  init_container2();
6972
7025
  init_container2();
6973
7026
  init_provider2();
@@ -6978,41 +7031,13 @@ var init_dist = __esm({
6978
7031
  // src/docker-host.ts
6979
7032
  var docker_host_exports = {};
6980
7033
  __export(docker_host_exports, {
7034
+ describeDockerEndpoint: () => describeDockerEndpoint,
6981
7035
  resolveDockerHostOptions: () => resolveDockerHostOptions
6982
7036
  });
6983
- import { spawnSync as spawnSync2 } from "node:child_process";
6984
- function resolveDockerHostOptions(spawnImpl = spawnSync2) {
6985
- if (process.env.DOCKER_HOST && process.env.DOCKER_HOST.length > 0) {
6986
- return {};
6987
- }
6988
- try {
6989
- const result = spawnImpl(
6990
- "docker",
6991
- ["context", "inspect", "--format", "{{.Endpoints.docker.Host}}"],
6992
- { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }
6993
- );
6994
- if (result.status === 0) {
6995
- const host = (result.stdout ?? "").trim();
6996
- if (host.startsWith("unix://")) {
6997
- return { socketPath: host.slice("unix://".length) };
6998
- }
6999
- if (host.startsWith("npipe://")) {
7000
- return { socketPath: host.slice("npipe://".length) };
7001
- }
7002
- if (host.startsWith("tcp://") || host.startsWith("http://") || host.startsWith("https://")) {
7003
- const url = new URL(host.startsWith("tcp://") ? host.replace("tcp://", "http://") : host);
7004
- const protocol = host.startsWith("https://") ? "https" : "http";
7005
- const port = url.port ? parseInt(url.port, 10) : protocol === "https" ? 2376 : 2375;
7006
- return { host: url.hostname, port, protocol };
7007
- }
7008
- }
7009
- } catch {
7010
- }
7011
- return { socketPath: "/var/run/docker.sock" };
7012
- }
7013
7037
  var init_docker_host = __esm({
7014
7038
  "src/docker-host.ts"() {
7015
7039
  "use strict";
7040
+ init_dist();
7016
7041
  }
7017
7042
  });
7018
7043
 
@@ -7224,7 +7249,7 @@ CREATE TABLE IF NOT EXISTS meta (
7224
7249
  this.db.pragma("foreign_keys = ON");
7225
7250
  this.db.exec(CREATE_TABLE);
7226
7251
  this.db.exec(CREATE_META);
7227
- for (const col of ["readiness_chain TEXT", "expected_services TEXT", "app_port_urls TEXT"]) {
7252
+ for (const col of ["readiness_chain TEXT", "expected_services TEXT", "app_port_urls TEXT", "tailscale_paths TEXT"]) {
7228
7253
  try {
7229
7254
  this.db.exec(`ALTER TABLE worlds ADD COLUMN ${col}`);
7230
7255
  } catch {
@@ -7342,6 +7367,27 @@ CREATE TABLE IF NOT EXISTS meta (
7342
7367
  const max = rows.length > 0 ? rows[rows.length - 1].port_offset : -100;
7343
7368
  return max + 100;
7344
7369
  }
7370
+ /**
7371
+ * Persist tailscale serve paths registered for a world.
7372
+ * Paths are stored as a JSON array in the tailscale_paths column.
7373
+ */
7374
+ storeTailscalePaths(worldId, paths) {
7375
+ this.db.prepare("UPDATE worlds SET tailscale_paths = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(paths), (/* @__PURE__ */ new Date()).toISOString(), worldId);
7376
+ }
7377
+ /**
7378
+ * Load tailscale serve paths for a world. Returns [] when none stored or
7379
+ * column absent (pre-migration rows).
7380
+ */
7381
+ loadTailscalePaths(worldId) {
7382
+ const row = this.db.prepare("SELECT tailscale_paths FROM worlds WHERE id = ?").get(worldId);
7383
+ if (!row?.tailscale_paths)
7384
+ return [];
7385
+ try {
7386
+ return JSON.parse(row.tailscale_paths);
7387
+ } catch {
7388
+ return [];
7389
+ }
7390
+ }
7345
7391
  close() {
7346
7392
  this.db.close();
7347
7393
  }
@@ -8124,8 +8170,8 @@ import { execFileSync as execFileSync3 } from "node:child_process";
8124
8170
  import * as fs14 from "node:fs";
8125
8171
  import * as os9 from "node:os";
8126
8172
  import * as path15 from "node:path";
8127
- function expandHome(p, homedir26) {
8128
- return p.replace(/^~(?=$|\/|\\)/, homedir26());
8173
+ function expandHome(p, homedir27) {
8174
+ return p.replace(/^~(?=$|\/|\\)/, homedir27());
8129
8175
  }
8130
8176
  function sanitizeRepoFilename(name) {
8131
8177
  const sanitized = name.replace(/[^A-Za-z0-9._-]/g, "_");
@@ -8148,7 +8194,7 @@ ${stderr}`;
8148
8194
  }
8149
8195
  function snapshotBaselineDiff(repos, workspacePath, deps = {}) {
8150
8196
  const exec = deps.exec ?? ((cmd, args, opts) => execFileSync3(cmd, args, opts));
8151
- const homedir26 = deps.homedir ?? (() => os9.homedir());
8197
+ const homedir27 = deps.homedir ?? (() => os9.homedir());
8152
8198
  const baselineDir = path15.join(workspacePath, ".olam", "baseline");
8153
8199
  try {
8154
8200
  fs14.mkdirSync(baselineDir, { recursive: true });
@@ -8164,7 +8210,7 @@ function snapshotBaselineDiff(repos, workspacePath, deps = {}) {
8164
8210
  continue;
8165
8211
  const filename = `${sanitizeRepoFilename(repo.name)}.diff`;
8166
8212
  const outPath = path15.join(baselineDir, filename);
8167
- const repoPath = expandHome(repo.path, homedir26);
8213
+ const repoPath = expandHome(repo.path, homedir27);
8168
8214
  if (!fs14.existsSync(repoPath)) {
8169
8215
  writeBaselineFile(outPath, `# repo: ${repo.name}
8170
8216
  # (skipped: path ${repoPath} does not exist)
@@ -10172,6 +10218,34 @@ var init_bootstrap_hooks = __esm({
10172
10218
  });
10173
10219
 
10174
10220
  // ../core/dist/world/tmux-supervisor.js
10221
+ function injectBindAll(start) {
10222
+ let result = start;
10223
+ const userBoundRails = /(^|\s)(-b|--binding)(\s+|=)\S+/.test(start);
10224
+ const userBoundNext = /(^|\s)(-H|--hostname)(\s+|=)\S+/.test(start);
10225
+ const userBoundVite = /(^|\s)--host(\s+\S+|=\S+)/.test(start);
10226
+ const userBoundGunicorn = /(^|\s)(-h|--bind)(\s+|=)\S+/.test(start);
10227
+ const userHostEnv = /(^|\s)HOST=\S+/.test(start);
10228
+ const anyUserBound = userBoundRails || userBoundNext || userBoundVite || userBoundGunicorn;
10229
+ if (!userHostEnv && !anyUserBound) {
10230
+ result = `HOST=0.0.0.0 ${result}`;
10231
+ }
10232
+ if (!userBoundRails && /\brails\s+s(?:erver)?\b/.test(start)) {
10233
+ result = `${result} -b 0.0.0.0`;
10234
+ }
10235
+ if (!userBoundNext && /\bnext\s+dev\b/.test(start)) {
10236
+ result = `${result} -H 0.0.0.0`;
10237
+ }
10238
+ if (!userBoundVite && /\b(?:^|\s|=)vite\b/.test(start)) {
10239
+ result = `${result} --host 0.0.0.0`;
10240
+ }
10241
+ if (!userBoundVite && /\b(?:flask\s+run|uvicorn|hypercorn)\b/.test(start)) {
10242
+ result = `${result} --host 0.0.0.0`;
10243
+ }
10244
+ if (!userBoundGunicorn && /\bgunicorn\b/.test(start)) {
10245
+ result = `${result} --bind 0.0.0.0`;
10246
+ }
10247
+ return result;
10248
+ }
10175
10249
  async function startSupervisedApps(containerName, worldId, repos, exec, options = {}) {
10176
10250
  if (!SAFE_IDENT2.test(containerName)) {
10177
10251
  throw new Error(`containerName "${containerName}" must match ${SAFE_IDENT2} (defensive guard)`);
@@ -10207,7 +10281,7 @@ async function startSupervisedApps(containerName, worldId, repos, exec, options
10207
10281
  windowsExisting.push(repo.name);
10208
10282
  continue;
10209
10283
  }
10210
- const expandedStart = repo.start.replace(/\$\{?PORT\}?/g, String(repo.hostPort));
10284
+ const expandedStart = injectBindAll(repo.start).replace(/\$\{?PORT\}?/g, String(repo.hostPort));
10211
10285
  const startCmd = `cd ${shellQuote2(repo.dir)} && exec ${expandedStart}`;
10212
10286
  exec(containerName, `tmux new-window -t ${sessionName} -n ${repo.name} -d ${shellQuote2(startCmd)}`);
10213
10287
  windowsCreated.push(repo.name);
@@ -10284,7 +10358,9 @@ __export(manager_exports, {
10284
10358
  WorldManager: () => WorldManager,
10285
10359
  applyPostgresNetworkOverrides: () => applyPostgresNetworkOverrides,
10286
10360
  buildManifestRuntimeForTest: () => buildManifestRuntime,
10361
+ cleanupWorldTailscale: () => cleanupWorldTailscale,
10287
10362
  deriveReadinessChain: () => deriveReadinessChain,
10363
+ exposeWorldOverTailscale: () => exposeWorldOverTailscale,
10288
10364
  getTokenScopes: () => getTokenScopes,
10289
10365
  planManifestPipeline: () => planManifestPipeline,
10290
10366
  runManifestRuntime: () => runManifestRuntime
@@ -10400,7 +10476,7 @@ ${stderr.split("\n").slice(0, 3).join(" ")}`);
10400
10476
  Likely cause: corrupt local image, a custom devbox image without
10401
10477
  the olam user (line 10 of devbox.Dockerfile), or NSS resolver
10402
10478
  failure under cross-arch QEMU emulation.
10403
- Remedy: docker rmi olam-devbox:latest && olam upgrade -y
10479
+ Remedy: docker rmi olam-devbox:base && olam upgrade -y
10404
10480
  (or set OLAM_DEVBOX_IMAGE to a known-good ref before \`olam create\`).
10405
10481
  PR push from inside the world will not work until this is resolved.`);
10406
10482
  return;
@@ -10634,6 +10710,71 @@ function buildManifestRuntime(worldId, repos) {
10634
10710
  }
10635
10711
  return { worldId, repos: runtimeRepos };
10636
10712
  }
10713
+ function exposeWorldOverTailscale(appPortUrls, worldId, registry, _exec = execSync5) {
10714
+ if (process.env["OLAM_TAILSCALE_SERVE"] !== "true")
10715
+ return;
10716
+ if (appPortUrls.length === 0)
10717
+ return;
10718
+ const bin = resolveTailscaleBin(_exec);
10719
+ if (!bin) {
10720
+ console.warn("[tailscale] WARN: OLAM_TAILSCALE_SERVE=true but no tailscale binary found. Set TAILSCALE_BIN or install tailscale. Skipping serve mapping.");
10721
+ return;
10722
+ }
10723
+ const registeredPaths = [];
10724
+ for (const { repoName, hostPort } of appPortUrls) {
10725
+ const servePath = `/world/${worldId}/${repoName}`;
10726
+ try {
10727
+ _exec(`"${bin}" serve --bg --set-path=${servePath} http://localhost:${hostPort}`, {
10728
+ timeout: 1e4
10729
+ });
10730
+ console.log(`[tailscale] exposed ${worldId}/${repoName} at https://<machine>.<tailnet>/world/${worldId}/${repoName}`);
10731
+ registeredPaths.push(servePath);
10732
+ } catch (err) {
10733
+ const msg = err instanceof Error ? err.message : String(err);
10734
+ console.warn(`[tailscale] WARN: serve failed for ${repoName} (port ${hostPort}): ${msg}`);
10735
+ }
10736
+ }
10737
+ if (registeredPaths.length > 0) {
10738
+ registry.storeTailscalePaths(worldId, registeredPaths);
10739
+ }
10740
+ }
10741
+ function cleanupWorldTailscale(worldId, registry, _exec = execSync5) {
10742
+ const paths = registry.loadTailscalePaths(worldId);
10743
+ if (paths.length === 0)
10744
+ return;
10745
+ const bin = resolveTailscaleBin(_exec);
10746
+ if (!bin) {
10747
+ console.warn("[tailscale] WARN: Cannot cleanup tailscale serve paths \u2014 binary not found.");
10748
+ return;
10749
+ }
10750
+ for (const servePath of paths) {
10751
+ try {
10752
+ _exec(`"${bin}" serve --bg --set-path=${servePath} off`, { timeout: 1e4 });
10753
+ console.log(`[tailscale] cleaned up serve path ${servePath}`);
10754
+ } catch (err) {
10755
+ const msg = err instanceof Error ? err.message : String(err);
10756
+ console.warn(`[tailscale] WARN: cleanup failed for ${servePath}: ${msg}`);
10757
+ }
10758
+ }
10759
+ }
10760
+ function resolveTailscaleBin(_exec = execSync5) {
10761
+ const candidates = [
10762
+ process.env["TAILSCALE_BIN"],
10763
+ "/Applications/Tailscale.app/Contents/MacOS/Tailscale",
10764
+ "/usr/local/bin/tailscale",
10765
+ "/opt/homebrew/bin/tailscale"
10766
+ ];
10767
+ for (const c of candidates) {
10768
+ if (!c)
10769
+ continue;
10770
+ try {
10771
+ _exec(`test -x "${c}"`, { timeout: 2e3 });
10772
+ return c;
10773
+ } catch {
10774
+ }
10775
+ }
10776
+ return void 0;
10777
+ }
10637
10778
  function applyPostgresNetworkOverrides(worldEnv, enrichedRepos) {
10638
10779
  const hasPostgres = enrichedRepos.some((r) => r.manifest?.services?.["postgres"] !== void 0);
10639
10780
  if (!hasPostgres)
@@ -11379,6 +11520,10 @@ ${opts.task}`;
11379
11520
  }
11380
11521
  } catch {
11381
11522
  }
11523
+ try {
11524
+ exposeWorldOverTailscale(appPortUrls, worldId, this.registry);
11525
+ } catch {
11526
+ }
11382
11527
  return {
11383
11528
  ...metadata,
11384
11529
  status: "running",
@@ -11426,6 +11571,10 @@ ${opts.task}`;
11426
11571
  } catch {
11427
11572
  }
11428
11573
  }
11574
+ try {
11575
+ cleanupWorldTailscale(worldId, this.registry);
11576
+ } catch {
11577
+ }
11429
11578
  try {
11430
11579
  await this.provider.destroyWorld(worldId);
11431
11580
  } catch {
@@ -12827,7 +12976,7 @@ function isCloudflaredAvailable() {
12827
12976
  }
12828
12977
  }
12829
12978
  function startTunnel(port) {
12830
- return new Promise((resolve10, reject) => {
12979
+ return new Promise((resolve11, reject) => {
12831
12980
  const child = spawn2("cloudflared", ["tunnel", "--url", `http://localhost:${port}`], {
12832
12981
  stdio: ["ignore", "pipe", "pipe"],
12833
12982
  detached: false
@@ -12849,7 +12998,7 @@ function startTunnel(port) {
12849
12998
  if (match2) {
12850
12999
  resolved = true;
12851
13000
  clearTimeout(timeout);
12852
- resolve10(match2[0]);
13001
+ resolve11(match2[0]);
12853
13002
  }
12854
13003
  }
12855
13004
  child.stdout?.on("data", scan);
@@ -12936,8 +13085,8 @@ var init_dashboard = __esm({
12936
13085
  }
12937
13086
  throw err;
12938
13087
  }
12939
- await new Promise((resolve10, reject) => {
12940
- this.server.on("listening", resolve10);
13088
+ await new Promise((resolve11, reject) => {
13089
+ this.server.on("listening", resolve11);
12941
13090
  this.server.on("error", reject);
12942
13091
  });
12943
13092
  this.info = { localUrl: `http://localhost:${port}` };
@@ -12983,8 +13132,8 @@ var init_dashboard = __esm({
12983
13132
  async stop() {
12984
13133
  stopTunnel();
12985
13134
  if (this.server) {
12986
- await new Promise((resolve10) => {
12987
- this.server.close(() => resolve10());
13135
+ await new Promise((resolve11) => {
13136
+ this.server.close(() => resolve11());
12988
13137
  });
12989
13138
  this.server = null;
12990
13139
  }
@@ -13673,10 +13822,10 @@ async function readHostCpToken2() {
13673
13822
  if (!fs25.existsSync(tp)) return null;
13674
13823
  return fs25.readFileSync(tp, "utf-8").trim();
13675
13824
  }
13676
- async function callHostCpProxy(method, worldId, path49, body) {
13825
+ async function callHostCpProxy(method, worldId, path50, body) {
13677
13826
  const token = await readHostCpToken2();
13678
13827
  if (!token) return { ok: false, status: 0, error: "no token (host CP not started)" };
13679
- const url = `http://127.0.0.1:${HOST_CP_PORT}/api/world/${encodeURIComponent(worldId)}${path49}`;
13828
+ const url = `http://127.0.0.1:${HOST_CP_PORT}/api/world/${encodeURIComponent(worldId)}${path50}`;
13680
13829
  try {
13681
13830
  const headers = {
13682
13831
  Authorization: `Bearer ${token}`
@@ -14561,9 +14710,9 @@ var UnknownArchetypeError = class extends Error {
14561
14710
  };
14562
14711
  var ArchetypeCycleError = class extends Error {
14563
14712
  path;
14564
- constructor(path49) {
14565
- super(`Archetype inheritance cycle detected: ${path49.join(" \u2192 ")} \u2192 ${path49[0] ?? "?"}`);
14566
- this.path = path49;
14713
+ constructor(path50) {
14714
+ super(`Archetype inheritance cycle detected: ${path50.join(" \u2192 ")} \u2192 ${path50[0] ?? "?"}`);
14715
+ this.path = path50;
14567
14716
  this.name = "ArchetypeCycleError";
14568
14717
  }
14569
14718
  };
@@ -14837,7 +14986,7 @@ var realDocker = {
14837
14986
  }
14838
14987
  };
14839
14988
  function spawnAsync(cmd, args, opts = {}) {
14840
- return new Promise((resolve10) => {
14989
+ return new Promise((resolve11) => {
14841
14990
  const child = spawn3(cmd, [...args], {
14842
14991
  stdio: ["ignore", "pipe", "pipe"],
14843
14992
  signal: opts.signal
@@ -14851,10 +15000,10 @@ function spawnAsync(cmd, args, opts = {}) {
14851
15000
  stderr += chunk.toString();
14852
15001
  });
14853
15002
  child.on("error", (err) => {
14854
- resolve10({ exitCode: -1, stdout, stderr: stderr + err.message });
15003
+ resolve11({ exitCode: -1, stdout, stderr: stderr + err.message });
14855
15004
  });
14856
15005
  child.on("close", (code) => {
14857
- resolve10({ exitCode: code ?? -1, stdout, stderr });
15006
+ resolve11({ exitCode: code ?? -1, stdout, stderr });
14858
15007
  });
14859
15008
  });
14860
15009
  }
@@ -15197,10 +15346,10 @@ async function confirm(message) {
15197
15346
  if (!process.stdin.isTTY) return true;
15198
15347
  const { createInterface: createInterface5 } = await import("node:readline");
15199
15348
  const rl = createInterface5({ input: process.stdin, output: process.stdout });
15200
- return new Promise((resolve10) => {
15349
+ return new Promise((resolve11) => {
15201
15350
  rl.question(`${message} [y/N] `, (answer) => {
15202
15351
  rl.close();
15203
- resolve10(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
15352
+ resolve11(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
15204
15353
  });
15205
15354
  });
15206
15355
  }
@@ -15574,7 +15723,7 @@ var McpAuthContainerController = class {
15574
15723
  }
15575
15724
  };
15576
15725
  function sleep3(ms) {
15577
- return new Promise((resolve10) => setTimeout(resolve10, ms));
15726
+ return new Promise((resolve11) => setTimeout(resolve11, ms));
15578
15727
  }
15579
15728
  function dumpContainerLogs(container, tail = 40) {
15580
15729
  try {
@@ -15861,9 +16010,10 @@ import pc10 from "picocolors";
15861
16010
  import { execSync as execSync7 } from "node:child_process";
15862
16011
  import { existsSync as existsSync26, statSync as statSync6 } from "node:fs";
15863
16012
  import { join as join31 } from "node:path";
15864
- var DEFAULT_DEVBOX_IMAGE = "olam-devbox:latest";
16013
+ var DEFAULT_DEVBOX_IMAGE = "olam-devbox:base";
15865
16014
  var DEVBOX_BAKED_SOURCES = [
15866
- "packages/adapters/src/docker/devbox.Dockerfile",
16015
+ "packages/adapters/src/docker/devbox.base.Dockerfile",
16016
+ "packages/adapters/src/docker/devbox.browser.Dockerfile",
15867
16017
  "packages/control-plane/standalone/server.mjs",
15868
16018
  "packages/control-plane/standalone/intelligence-bridge.mjs",
15869
16019
  "packages/control-plane/standalone/d1-sqlite-adapter.mjs",
@@ -15928,9 +16078,9 @@ function formatFreshnessWarning(result, image = DEFAULT_DEVBOX_IMAGE) {
15928
16078
  "These source files have changed since the image was built; the",
15929
16079
  "changes will NOT take effect in fresh worlds until you rebuild:"
15930
16080
  ];
15931
- for (const { path: path49, mtimeMs } of result.newerSources) {
16081
+ for (const { path: path50, mtimeMs } of result.newerSources) {
15932
16082
  const when = new Date(mtimeMs).toISOString();
15933
- lines.push(` \u2022 ${path49} (modified ${when})`);
16083
+ lines.push(` \u2022 ${path50} (modified ${when})`);
15934
16084
  }
15935
16085
  lines.push("");
15936
16086
  lines.push("Rebuild with:");
@@ -16089,15 +16239,15 @@ init_host_cp();
16089
16239
  var HOST_CP_URL = "http://127.0.0.1:19000";
16090
16240
  async function readHostCpTokenForCreate() {
16091
16241
  try {
16092
- const { default: fs47 } = await import("node:fs");
16242
+ const { default: fs48 } = await import("node:fs");
16093
16243
  const { default: os28 } = await import("node:os");
16094
- const { default: path49 } = await import("node:path");
16095
- const tp = path49.join(
16096
- process.env.OLAM_HOME ?? path49.join(os28.homedir(), ".olam"),
16244
+ const { default: path50 } = await import("node:path");
16245
+ const tp = path50.join(
16246
+ process.env.OLAM_HOME ?? path50.join(os28.homedir(), ".olam"),
16097
16247
  "host-cp.token"
16098
16248
  );
16099
- if (!fs47.existsSync(tp)) return null;
16100
- return fs47.readFileSync(tp, "utf-8").trim();
16249
+ if (!fs48.existsSync(tp)) return null;
16250
+ return fs48.readFileSync(tp, "utf-8").trim();
16101
16251
  } catch {
16102
16252
  return null;
16103
16253
  }
@@ -16460,12 +16610,12 @@ function defaultNameFromPrompt(prompt) {
16460
16610
  }
16461
16611
  async function readHostCpToken3() {
16462
16612
  try {
16463
- const { default: fs47 } = await import("node:fs");
16613
+ const { default: fs48 } = await import("node:fs");
16464
16614
  const { default: os28 } = await import("node:os");
16465
- const { default: path49 } = await import("node:path");
16466
- const tp = path49.join(os28.homedir(), ".olam", "host-cp.token");
16467
- if (!fs47.existsSync(tp)) return null;
16468
- const raw = fs47.readFileSync(tp, "utf-8").trim();
16615
+ const { default: path50 } = await import("node:path");
16616
+ const tp = path50.join(os28.homedir(), ".olam", "host-cp.token");
16617
+ if (!fs48.existsSync(tp)) return null;
16618
+ const raw = fs48.readFileSync(tp, "utf-8").trim();
16469
16619
  return raw.length > 0 ? raw : null;
16470
16620
  } catch {
16471
16621
  return null;
@@ -17133,14 +17283,14 @@ function printTable(entries) {
17133
17283
  async function confirmInteractive() {
17134
17284
  process.stdout.write(" Type `yes` to proceed: ");
17135
17285
  const buf = [];
17136
- return new Promise((resolve10) => {
17286
+ return new Promise((resolve11) => {
17137
17287
  const onData = (chunk) => {
17138
17288
  buf.push(chunk);
17139
17289
  if (Buffer.concat(buf).toString("utf-8").includes("\n")) {
17140
17290
  process.stdin.removeListener("data", onData);
17141
17291
  process.stdin.pause();
17142
17292
  const answer = Buffer.concat(buf).toString("utf-8").trim();
17143
- resolve10(answer.toLowerCase() === "yes");
17293
+ resolve11(answer.toLowerCase() === "yes");
17144
17294
  }
17145
17295
  };
17146
17296
  process.stdin.resume();
@@ -20196,11 +20346,11 @@ function zodIssueToError(issue, doc, lineCounter) {
20196
20346
  suggestion: deriveSuggestion(issue)
20197
20347
  };
20198
20348
  }
20199
- function formatJsonPath(path49) {
20200
- if (path49.length === 0)
20349
+ function formatJsonPath(path50) {
20350
+ if (path50.length === 0)
20201
20351
  return "<root>";
20202
20352
  let out = "";
20203
- for (const seg of path49) {
20353
+ for (const seg of path50) {
20204
20354
  if (typeof seg === "number") {
20205
20355
  out += `[${seg}]`;
20206
20356
  } else {
@@ -20209,11 +20359,11 @@ function formatJsonPath(path49) {
20209
20359
  }
20210
20360
  return out;
20211
20361
  }
20212
- function resolveYamlLocation(path49, doc, lineCounter) {
20362
+ function resolveYamlLocation(path50, doc, lineCounter) {
20213
20363
  let bestLine = 0;
20214
20364
  let bestColumn = 0;
20215
- for (let depth = path49.length; depth >= 0; depth -= 1) {
20216
- const segment = path49.slice(0, depth);
20365
+ for (let depth = path50.length; depth >= 0; depth -= 1) {
20366
+ const segment = path50.slice(0, depth);
20217
20367
  try {
20218
20368
  const node = doc.getIn(segment, true);
20219
20369
  if (node && typeof node === "object" && "range" in node) {
@@ -20431,11 +20581,11 @@ function topoSort(nodes) {
20431
20581
  }
20432
20582
  function traceCycle(start, byId) {
20433
20583
  const seen = /* @__PURE__ */ new Set();
20434
- const path49 = [];
20584
+ const path50 = [];
20435
20585
  let current = start;
20436
20586
  while (current && !seen.has(current)) {
20437
20587
  seen.add(current);
20438
- path49.push(current);
20588
+ path50.push(current);
20439
20589
  const node = byId.get(current);
20440
20590
  const next = node?.dependsOn[0];
20441
20591
  if (next === void 0)
@@ -20443,10 +20593,10 @@ function traceCycle(start, byId) {
20443
20593
  current = next;
20444
20594
  }
20445
20595
  if (current && seen.has(current)) {
20446
- const idx = path49.indexOf(current);
20447
- return [...path49.slice(idx), current];
20596
+ const idx = path50.indexOf(current);
20597
+ return [...path50.slice(idx), current];
20448
20598
  }
20449
- return path49;
20599
+ return path50;
20450
20600
  }
20451
20601
 
20452
20602
  // ../core/dist/executor/types.js
@@ -22855,10 +23005,10 @@ async function confirm2(message) {
22855
23005
  if (!process.stdin.isTTY) return true;
22856
23006
  const { createInterface: createInterface5 } = await import("node:readline");
22857
23007
  const rl = createInterface5({ input: process.stdin, output: process.stdout });
22858
- return new Promise((resolve10) => {
23008
+ return new Promise((resolve11) => {
22859
23009
  rl.question(`${message} [y/N] `, (answer) => {
22860
23010
  rl.close();
22861
- resolve10(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
23011
+ resolve11(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
22862
23012
  });
22863
23013
  });
22864
23014
  }
@@ -22977,11 +23127,18 @@ async function runUpgradePullByDigest(deps = {}) {
22977
23127
  }
22978
23128
  infoSpinner.succeed("docker daemon reachable");
22979
23129
  const hasMcpAuthDigest = typeof digests["mcp-auth"] === "string" && digests["mcp-auth"].length > 0;
23130
+ const hasDevboxBaseDigest = typeof digests["devbox-base"] === "string" && digests["devbox-base"].length > 0;
22980
23131
  const baseRefs = [
22981
23132
  { name: "host-cp", ref: `${registry}/olam-host-cp@${digests["host-cp"]}` },
22982
23133
  { name: "auth", ref: `${registry}/olam-auth@${digests.auth}` },
22983
23134
  { name: "devbox", ref: `${registry}/olam-devbox@${digests.devbox}` }
22984
23135
  ];
23136
+ if (hasDevboxBaseDigest) {
23137
+ baseRefs.push({
23138
+ name: "devbox-base",
23139
+ ref: `${registry}/olam-devbox@${digests["devbox-base"]}`
23140
+ });
23141
+ }
22985
23142
  if (hasMcpAuthDigest) {
22986
23143
  baseRefs.push({
22987
23144
  name: "mcp-auth",
@@ -23046,18 +23203,29 @@ async function runUpgradePullByDigest(deps = {}) {
23046
23203
  }
23047
23204
  }
23048
23205
  handshakeSpinner.succeed(`Protocol handshake passed (all ${imageRefs.length} images)`);
23206
+ const refByName = (n) => {
23207
+ const found = imageRefs.find((r) => r.name === n);
23208
+ if (!found) throw new Error(`upgrade: missing imageRefs entry for "${n}"`);
23209
+ return found.ref;
23210
+ };
23049
23211
  const tagPlanBase = [
23050
- { from: imageRefs[0].ref, to: "olam-host-cp:latest", name: "host-cp (bare)" },
23051
- { from: imageRefs[0].ref, to: `${registry}/olam-host-cp:latest`, name: "host-cp (registry)" },
23052
- { from: imageRefs[1].ref, to: "olam-auth:local", name: "auth (bare)" },
23053
- { from: imageRefs[1].ref, to: `${registry}/olam-auth:latest`, name: "auth (registry)" },
23054
- { from: imageRefs[2].ref, to: "olam-devbox:latest", name: "devbox (bare)" },
23055
- { from: imageRefs[2].ref, to: `${registry}/olam-devbox:latest`, name: "devbox (registry)" }
23212
+ { from: refByName("host-cp"), to: "olam-host-cp:latest", name: "host-cp (bare)" },
23213
+ { from: refByName("host-cp"), to: `${registry}/olam-host-cp:latest`, name: "host-cp (registry)" },
23214
+ { from: refByName("auth"), to: "olam-auth:local", name: "auth (bare)" },
23215
+ { from: refByName("auth"), to: `${registry}/olam-auth:latest`, name: "auth (registry)" },
23216
+ { from: refByName("devbox"), to: "olam-devbox:latest", name: "devbox (bare)" },
23217
+ { from: refByName("devbox"), to: `${registry}/olam-devbox:latest`, name: "devbox (registry)" }
23056
23218
  ];
23057
- if (hasMcpAuthDigest && imageRefs[3]) {
23219
+ if (hasDevboxBaseDigest) {
23058
23220
  tagPlanBase.push(
23059
- { from: imageRefs[3].ref, to: "olam-mcp-auth:local", name: "mcp-auth (bare)" },
23060
- { from: imageRefs[3].ref, to: `${registry}/olam-mcp-auth:latest`, name: "mcp-auth (registry)" }
23221
+ { from: refByName("devbox-base"), to: "olam-devbox:base", name: "devbox-base (bare)" },
23222
+ { from: refByName("devbox-base"), to: `${registry}/olam-devbox:base`, name: "devbox-base (registry)" }
23223
+ );
23224
+ }
23225
+ if (hasMcpAuthDigest) {
23226
+ tagPlanBase.push(
23227
+ { from: refByName("mcp-auth"), to: "olam-mcp-auth:local", name: "mcp-auth (bare)" },
23228
+ { from: refByName("mcp-auth"), to: `${registry}/olam-mcp-auth:latest`, name: "mcp-auth (registry)" }
23061
23229
  );
23062
23230
  }
23063
23231
  const tagPlan = tagPlanBase;
@@ -25599,8 +25767,8 @@ var SECRET = process.env["OLAM_MCP_AUTH_SECRET"] ?? "";
25599
25767
  function authHeaders() {
25600
25768
  return SECRET ? { "X-Olam-Mcp-Secret": SECRET } : {};
25601
25769
  }
25602
- async function apiFetch(path49, init = {}) {
25603
- const res = await fetch(`${BASE_URL}${path49}`, {
25770
+ async function apiFetch(path50, init = {}) {
25771
+ const res = await fetch(`${BASE_URL}${path50}`, {
25604
25772
  ...init,
25605
25773
  headers: {
25606
25774
  "Content-Type": "application/json",
@@ -25687,7 +25855,7 @@ function registerMcpLogin(cmd) {
25687
25855
  init_output();
25688
25856
  import * as readline2 from "node:readline";
25689
25857
  async function readTokenSilent(prompt) {
25690
- return new Promise((resolve10, reject) => {
25858
+ return new Promise((resolve11, reject) => {
25691
25859
  const rl = readline2.createInterface({
25692
25860
  input: process.stdin,
25693
25861
  output: process.stdout,
@@ -25705,7 +25873,7 @@ async function readTokenSilent(prompt) {
25705
25873
  process.stdin.removeListener("data", onData);
25706
25874
  process.stdout.write("\n");
25707
25875
  rl.close();
25708
- resolve10(token);
25876
+ resolve11(token);
25709
25877
  } else if (char === "") {
25710
25878
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
25711
25879
  process.stdin.removeListener("data", onData);
@@ -25980,7 +26148,7 @@ async function discoverMcpSources(repoPaths) {
25980
26148
  import { spawn as spawn6 } from "node:child_process";
25981
26149
  var VALIDATION_TIMEOUT_MS = 5e3;
25982
26150
  async function validateMcpEntry(entry) {
25983
- return new Promise((resolve10) => {
26151
+ return new Promise((resolve11) => {
25984
26152
  let stdout = "";
25985
26153
  let timedOut = false;
25986
26154
  let child;
@@ -25990,7 +26158,7 @@ async function validateMcpEntry(entry) {
25990
26158
  env: { ...process.env, ...entry.env ?? {} }
25991
26159
  });
25992
26160
  } catch (err) {
25993
- resolve10({
26161
+ resolve11({
25994
26162
  name: entry.name,
25995
26163
  validated: false,
25996
26164
  reason: err instanceof Error ? err.message : "spawn failed"
@@ -26007,11 +26175,11 @@ async function validateMcpEntry(entry) {
26007
26175
  child.on("close", (code) => {
26008
26176
  clearTimeout(timer);
26009
26177
  if (timedOut) {
26010
- resolve10({ name: entry.name, validated: false, reason: "timeout (5s)" });
26178
+ resolve11({ name: entry.name, validated: false, reason: "timeout (5s)" });
26011
26179
  return;
26012
26180
  }
26013
26181
  const validated = code === 0 && stdout.trim().length > 0;
26014
- resolve10({
26182
+ resolve11({
26015
26183
  name: entry.name,
26016
26184
  validated,
26017
26185
  reason: validated ? "ok" : `exit code ${code ?? "null"}`
@@ -26019,7 +26187,7 @@ async function validateMcpEntry(entry) {
26019
26187
  });
26020
26188
  child.on("error", (err) => {
26021
26189
  clearTimeout(timer);
26022
- resolve10({ name: entry.name, validated: false, reason: err.message });
26190
+ resolve11({ name: entry.name, validated: false, reason: err.message });
26023
26191
  });
26024
26192
  });
26025
26193
  }
@@ -26034,11 +26202,11 @@ async function multiSelectPicker(entries) {
26034
26202
  );
26035
26203
  });
26036
26204
  console.log("\n" + pc29.dim('Enter numbers to import (e.g. 1,2,3 or "all" or Enter to skip):'));
26037
- const answer = await new Promise((resolve10) => {
26205
+ const answer = await new Promise((resolve11) => {
26038
26206
  const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
26039
26207
  rl.question("> ", (ans) => {
26040
26208
  rl.close();
26041
- resolve10(ans.trim());
26209
+ resolve11(ans.trim());
26042
26210
  });
26043
26211
  });
26044
26212
  if (!answer || answer === "") return [];
@@ -26176,19 +26344,217 @@ function registerMcp(program2) {
26176
26344
  registerMcpRevoke(mcp);
26177
26345
  }
26178
26346
 
26347
+ // src/commands/kg-build.ts
26348
+ import { spawnSync as spawnSync14 } from "node:child_process";
26349
+ import fs46 from "node:fs";
26350
+ import path48 from "node:path";
26351
+
26352
+ // ../core/dist/kg/storage-paths.js
26353
+ import { homedir as homedir26 } from "node:os";
26354
+ import { join as join48, resolve as resolve10 } from "node:path";
26355
+
26356
+ // ../core/dist/world/workspace-name.js
26357
+ var InvalidWorkspaceNameError = class extends Error {
26358
+ constructor(name, reason) {
26359
+ super(`invalid workspace name ${JSON.stringify(name)}: ${reason}`);
26360
+ this.name = "InvalidWorkspaceNameError";
26361
+ }
26362
+ };
26363
+ var WORKSPACE_NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
26364
+ function validateWorkspaceName(name) {
26365
+ if (typeof name !== "string" || name.length === 0) {
26366
+ throw new InvalidWorkspaceNameError(String(name), "must be a non-empty string");
26367
+ }
26368
+ if (!WORKSPACE_NAME_RE.test(name)) {
26369
+ throw new InvalidWorkspaceNameError(name, "must match ^[a-z0-9][a-z0-9_-]*$ (lowercase letters, digits, hyphens, underscores; must start with letter or digit)");
26370
+ }
26371
+ }
26372
+
26373
+ // ../core/dist/kg/storage-paths.js
26374
+ function olamHome3() {
26375
+ return process.env.OLAM_HOME ?? join48(homedir26(), ".olam");
26376
+ }
26377
+ function kgRoot() {
26378
+ return join48(olamHome3(), "kg");
26379
+ }
26380
+ function worldsRoot() {
26381
+ return join48(olamHome3(), "worlds");
26382
+ }
26383
+ function assertWithinPrefix(path50, prefix, label) {
26384
+ if (!path50.startsWith(prefix + "/")) {
26385
+ throw new Error(`${label} escape: ${path50} not under ${prefix}/`);
26386
+ }
26387
+ }
26388
+ function kgPristinePath(workspace) {
26389
+ validateWorkspaceName(workspace);
26390
+ const root = kgRoot();
26391
+ const path50 = resolve10(join48(root, workspace));
26392
+ assertWithinPrefix(path50, root, "kgPristinePath");
26393
+ return path50;
26394
+ }
26395
+ var KG_PATHS_INTERNALS = Object.freeze({
26396
+ olamHome: olamHome3,
26397
+ kgRoot,
26398
+ worldsRoot
26399
+ });
26400
+
26401
+ // src/commands/kg-build.ts
26402
+ init_output();
26403
+ var DEFAULT_DEVBOX_IMAGE2 = "olam-devbox:latest";
26404
+ function resolveWorkspace(arg) {
26405
+ const cwd = process.cwd();
26406
+ const name = arg ?? path48.basename(cwd).toLowerCase();
26407
+ validateWorkspaceName(name);
26408
+ return { name, sourcePath: cwd };
26409
+ }
26410
+ function copyWorkspaceToScratch(source, scratch) {
26411
+ if (process.platform === "darwin") {
26412
+ const r2 = spawnSync14("cp", ["-c", "-r", source + "/.", scratch], {
26413
+ stdio: ["ignore", "ignore", "pipe"],
26414
+ encoding: "utf-8"
26415
+ });
26416
+ if (r2.status === 0) return "cp-c-r-reflink";
26417
+ }
26418
+ const r = spawnSync14("cp", ["-r", source + "/.", scratch], {
26419
+ stdio: ["ignore", "ignore", "pipe"],
26420
+ encoding: "utf-8"
26421
+ });
26422
+ if (r.status !== 0) {
26423
+ throw new Error(`cp -r failed: ${(r.stderr ?? "").trim() || r.error?.message}`);
26424
+ }
26425
+ return "cp-r";
26426
+ }
26427
+ function parseNodeCount(graphifyOutDir) {
26428
+ const graphPath = path48.join(graphifyOutDir, "graph.json");
26429
+ if (!fs46.existsSync(graphPath)) return null;
26430
+ try {
26431
+ const content = fs46.readFileSync(graphPath, "utf-8");
26432
+ const data = JSON.parse(content);
26433
+ return Array.isArray(data.nodes) ? data.nodes.length : null;
26434
+ } catch {
26435
+ return null;
26436
+ }
26437
+ }
26438
+ function readGraphifyVersion(image) {
26439
+ const r = spawnSync14(
26440
+ "docker",
26441
+ [
26442
+ "run",
26443
+ "--rm",
26444
+ "--entrypoint=/opt/graphify-venv/bin/pip",
26445
+ image,
26446
+ "show",
26447
+ "graphifyy"
26448
+ ],
26449
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }
26450
+ );
26451
+ if (r.status !== 0) return "unknown";
26452
+ const m = (r.stdout ?? "").match(/^Version:\s*(\S+)/m);
26453
+ return m?.[1] ?? "unknown";
26454
+ }
26455
+ async function runKgBuild(workspaceArg, options = {}) {
26456
+ const image = options.image ?? DEFAULT_DEVBOX_IMAGE2;
26457
+ let workspace;
26458
+ try {
26459
+ workspace = resolveWorkspace(workspaceArg);
26460
+ } catch (err) {
26461
+ printError(err instanceof Error ? err.message : String(err));
26462
+ return { exitCode: 2 };
26463
+ }
26464
+ const outDir = kgPristinePath(workspace.name);
26465
+ const scratchDir = path48.join(outDir, "scratch");
26466
+ fs46.mkdirSync(outDir, { recursive: true });
26467
+ if (fs46.existsSync(scratchDir)) fs46.rmSync(scratchDir, { recursive: true, force: true });
26468
+ fs46.mkdirSync(scratchDir);
26469
+ const human = !options.json;
26470
+ if (human) {
26471
+ printInfo("kg build", `workspace=${workspace.name} source=${workspace.sourcePath}`);
26472
+ printInfo("kg build", `scratch=${scratchDir} \u2192 out=${outDir}/graphify-out/`);
26473
+ }
26474
+ const started = Date.now();
26475
+ let scratchStrategy;
26476
+ try {
26477
+ scratchStrategy = copyWorkspaceToScratch(workspace.sourcePath, scratchDir);
26478
+ if (human) printInfo("kg build", `copied via ${scratchStrategy}`);
26479
+ const dockerArgs = [
26480
+ "run",
26481
+ "--rm",
26482
+ "-v",
26483
+ `${scratchDir}:/work`,
26484
+ "-w",
26485
+ "/work",
26486
+ "--entrypoint=graphify",
26487
+ image,
26488
+ "update",
26489
+ "."
26490
+ ];
26491
+ const r = human ? spawnSync14("docker", dockerArgs, { stdio: "inherit" }) : spawnSync14("docker", dockerArgs, { stdio: ["ignore", "ignore", "pipe"] });
26492
+ if (r.status !== 0) {
26493
+ printError(`graphify update failed (exit ${r.status})`);
26494
+ return { exitCode: r.status ?? 1 };
26495
+ }
26496
+ const scratchOut = path48.join(scratchDir, "graphify-out");
26497
+ const finalOut = path48.join(outDir, "graphify-out");
26498
+ if (!fs46.existsSync(scratchOut)) {
26499
+ printError(`graphify produced no graphify-out/ in scratch (${scratchOut})`);
26500
+ return { exitCode: 1 };
26501
+ }
26502
+ if (fs46.existsSync(finalOut)) fs46.rmSync(finalOut, { recursive: true, force: true });
26503
+ fs46.renameSync(scratchOut, finalOut);
26504
+ const durationMs = Date.now() - started;
26505
+ const nodeCount = parseNodeCount(finalOut);
26506
+ const version = readGraphifyVersion(image);
26507
+ const freshness = {
26508
+ built_at: (/* @__PURE__ */ new Date()).toISOString(),
26509
+ duration_ms: durationMs,
26510
+ node_count: nodeCount,
26511
+ graphify_version: version,
26512
+ workspace: workspace.name,
26513
+ scratch_strategy: scratchStrategy
26514
+ };
26515
+ fs46.writeFileSync(
26516
+ path48.join(outDir, "freshness.json"),
26517
+ JSON.stringify(freshness, null, 2) + "\n",
26518
+ "utf-8"
26519
+ );
26520
+ if (options.json) {
26521
+ process.stdout.write(JSON.stringify(freshness) + "\n");
26522
+ } else {
26523
+ printSuccess(
26524
+ `kg build ${workspace.name} \u2014 ${nodeCount ?? "?"} nodes in ${(durationMs / 1e3).toFixed(1)}s (graphify ${version})`
26525
+ );
26526
+ printInfo("output", `${finalOut}/graph.json`);
26527
+ }
26528
+ return { exitCode: 0, freshness, outputDir: finalOut };
26529
+ } finally {
26530
+ if (fs46.existsSync(scratchDir)) {
26531
+ fs46.rmSync(scratchDir, { recursive: true, force: true });
26532
+ }
26533
+ }
26534
+ }
26535
+ function registerKg(program2) {
26536
+ const kg = program2.command("kg").description("Knowledge-graph operations (graphify-backed)");
26537
+ kg.command("build").description(
26538
+ "Build pristine KG for a workspace (default: current dir). Scratch-dir pattern; ~/.olam/kg/<ws>/graphify-out/"
26539
+ ).argument("[workspace]", "workspace name (lowercase alphanumeric + hyphens/underscores)").option("--image <ref>", `devbox image to invoke (default: ${DEFAULT_DEVBOX_IMAGE2})`).option("--json", "emit freshness record as JSON instead of human-readable status").action(async (workspaceArg, opts) => {
26540
+ const r = await runKgBuild(workspaceArg, opts);
26541
+ if (r.exitCode !== 0) process.exitCode = r.exitCode;
26542
+ });
26543
+ }
26544
+
26179
26545
  // src/pleri-config.ts
26180
- import * as fs46 from "node:fs";
26181
- import * as path48 from "node:path";
26546
+ import * as fs47 from "node:fs";
26547
+ import * as path49 from "node:path";
26182
26548
  function isPleriConfigured(configDir = process.env.OLAM_CONFIG_DIR ?? ".olam") {
26183
26549
  if (process.env.PLERI_BASE_URL) {
26184
26550
  return true;
26185
26551
  }
26186
- const configPath = path48.join(configDir, "config.yaml");
26187
- if (!fs46.existsSync(configPath)) {
26552
+ const configPath = path49.join(configDir, "config.yaml");
26553
+ if (!fs47.existsSync(configPath)) {
26188
26554
  return false;
26189
26555
  }
26190
26556
  try {
26191
- const contents = fs46.readFileSync(configPath, "utf8");
26557
+ const contents = fs47.readFileSync(configPath, "utf8");
26192
26558
  return /^[^#\n]*\bpleri:/m.test(contents);
26193
26559
  } catch {
26194
26560
  return false;
@@ -26230,6 +26596,7 @@ registerBegin(program);
26230
26596
  registerStop(program);
26231
26597
  registerWorldUpgrade(program);
26232
26598
  registerMcp(program);
26599
+ registerKg(program);
26233
26600
  registerConfig(program);
26234
26601
  registerRepos(program);
26235
26602
  registerRunbooks(program);