@pleri/olam-cli 0.1.101 → 0.1.102

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)
@@ -10284,7 +10330,9 @@ __export(manager_exports, {
10284
10330
  WorldManager: () => WorldManager,
10285
10331
  applyPostgresNetworkOverrides: () => applyPostgresNetworkOverrides,
10286
10332
  buildManifestRuntimeForTest: () => buildManifestRuntime,
10333
+ cleanupWorldTailscale: () => cleanupWorldTailscale,
10287
10334
  deriveReadinessChain: () => deriveReadinessChain,
10335
+ exposeWorldOverTailscale: () => exposeWorldOverTailscale,
10288
10336
  getTokenScopes: () => getTokenScopes,
10289
10337
  planManifestPipeline: () => planManifestPipeline,
10290
10338
  runManifestRuntime: () => runManifestRuntime
@@ -10400,7 +10448,7 @@ ${stderr.split("\n").slice(0, 3).join(" ")}`);
10400
10448
  Likely cause: corrupt local image, a custom devbox image without
10401
10449
  the olam user (line 10 of devbox.Dockerfile), or NSS resolver
10402
10450
  failure under cross-arch QEMU emulation.
10403
- Remedy: docker rmi olam-devbox:latest && olam upgrade -y
10451
+ Remedy: docker rmi olam-devbox:base && olam upgrade -y
10404
10452
  (or set OLAM_DEVBOX_IMAGE to a known-good ref before \`olam create\`).
10405
10453
  PR push from inside the world will not work until this is resolved.`);
10406
10454
  return;
@@ -10634,6 +10682,71 @@ function buildManifestRuntime(worldId, repos) {
10634
10682
  }
10635
10683
  return { worldId, repos: runtimeRepos };
10636
10684
  }
10685
+ function exposeWorldOverTailscale(appPortUrls, worldId, registry, _exec = execSync5) {
10686
+ if (process.env["OLAM_TAILSCALE_SERVE"] !== "true")
10687
+ return;
10688
+ if (appPortUrls.length === 0)
10689
+ return;
10690
+ const bin = resolveTailscaleBin(_exec);
10691
+ if (!bin) {
10692
+ console.warn("[tailscale] WARN: OLAM_TAILSCALE_SERVE=true but no tailscale binary found. Set TAILSCALE_BIN or install tailscale. Skipping serve mapping.");
10693
+ return;
10694
+ }
10695
+ const registeredPaths = [];
10696
+ for (const { repoName, hostPort } of appPortUrls) {
10697
+ const servePath = `/world/${worldId}/${repoName}`;
10698
+ try {
10699
+ _exec(`"${bin}" serve --bg --set-path=${servePath} http://localhost:${hostPort}`, {
10700
+ timeout: 1e4
10701
+ });
10702
+ console.log(`[tailscale] exposed ${worldId}/${repoName} at https://<machine>.<tailnet>/world/${worldId}/${repoName}`);
10703
+ registeredPaths.push(servePath);
10704
+ } catch (err) {
10705
+ const msg = err instanceof Error ? err.message : String(err);
10706
+ console.warn(`[tailscale] WARN: serve failed for ${repoName} (port ${hostPort}): ${msg}`);
10707
+ }
10708
+ }
10709
+ if (registeredPaths.length > 0) {
10710
+ registry.storeTailscalePaths(worldId, registeredPaths);
10711
+ }
10712
+ }
10713
+ function cleanupWorldTailscale(worldId, registry, _exec = execSync5) {
10714
+ const paths = registry.loadTailscalePaths(worldId);
10715
+ if (paths.length === 0)
10716
+ return;
10717
+ const bin = resolveTailscaleBin(_exec);
10718
+ if (!bin) {
10719
+ console.warn("[tailscale] WARN: Cannot cleanup tailscale serve paths \u2014 binary not found.");
10720
+ return;
10721
+ }
10722
+ for (const servePath of paths) {
10723
+ try {
10724
+ _exec(`"${bin}" serve --bg --set-path=${servePath} off`, { timeout: 1e4 });
10725
+ console.log(`[tailscale] cleaned up serve path ${servePath}`);
10726
+ } catch (err) {
10727
+ const msg = err instanceof Error ? err.message : String(err);
10728
+ console.warn(`[tailscale] WARN: cleanup failed for ${servePath}: ${msg}`);
10729
+ }
10730
+ }
10731
+ }
10732
+ function resolveTailscaleBin(_exec = execSync5) {
10733
+ const candidates = [
10734
+ process.env["TAILSCALE_BIN"],
10735
+ "/Applications/Tailscale.app/Contents/MacOS/Tailscale",
10736
+ "/usr/local/bin/tailscale",
10737
+ "/opt/homebrew/bin/tailscale"
10738
+ ];
10739
+ for (const c of candidates) {
10740
+ if (!c)
10741
+ continue;
10742
+ try {
10743
+ _exec(`test -x "${c}"`, { timeout: 2e3 });
10744
+ return c;
10745
+ } catch {
10746
+ }
10747
+ }
10748
+ return void 0;
10749
+ }
10637
10750
  function applyPostgresNetworkOverrides(worldEnv, enrichedRepos) {
10638
10751
  const hasPostgres = enrichedRepos.some((r) => r.manifest?.services?.["postgres"] !== void 0);
10639
10752
  if (!hasPostgres)
@@ -11379,6 +11492,10 @@ ${opts.task}`;
11379
11492
  }
11380
11493
  } catch {
11381
11494
  }
11495
+ try {
11496
+ exposeWorldOverTailscale(appPortUrls, worldId, this.registry);
11497
+ } catch {
11498
+ }
11382
11499
  return {
11383
11500
  ...metadata,
11384
11501
  status: "running",
@@ -11426,6 +11543,10 @@ ${opts.task}`;
11426
11543
  } catch {
11427
11544
  }
11428
11545
  }
11546
+ try {
11547
+ cleanupWorldTailscale(worldId, this.registry);
11548
+ } catch {
11549
+ }
11429
11550
  try {
11430
11551
  await this.provider.destroyWorld(worldId);
11431
11552
  } catch {
@@ -12827,7 +12948,7 @@ function isCloudflaredAvailable() {
12827
12948
  }
12828
12949
  }
12829
12950
  function startTunnel(port) {
12830
- return new Promise((resolve10, reject) => {
12951
+ return new Promise((resolve11, reject) => {
12831
12952
  const child = spawn2("cloudflared", ["tunnel", "--url", `http://localhost:${port}`], {
12832
12953
  stdio: ["ignore", "pipe", "pipe"],
12833
12954
  detached: false
@@ -12849,7 +12970,7 @@ function startTunnel(port) {
12849
12970
  if (match2) {
12850
12971
  resolved = true;
12851
12972
  clearTimeout(timeout);
12852
- resolve10(match2[0]);
12973
+ resolve11(match2[0]);
12853
12974
  }
12854
12975
  }
12855
12976
  child.stdout?.on("data", scan);
@@ -12936,8 +13057,8 @@ var init_dashboard = __esm({
12936
13057
  }
12937
13058
  throw err;
12938
13059
  }
12939
- await new Promise((resolve10, reject) => {
12940
- this.server.on("listening", resolve10);
13060
+ await new Promise((resolve11, reject) => {
13061
+ this.server.on("listening", resolve11);
12941
13062
  this.server.on("error", reject);
12942
13063
  });
12943
13064
  this.info = { localUrl: `http://localhost:${port}` };
@@ -12983,8 +13104,8 @@ var init_dashboard = __esm({
12983
13104
  async stop() {
12984
13105
  stopTunnel();
12985
13106
  if (this.server) {
12986
- await new Promise((resolve10) => {
12987
- this.server.close(() => resolve10());
13107
+ await new Promise((resolve11) => {
13108
+ this.server.close(() => resolve11());
12988
13109
  });
12989
13110
  this.server = null;
12990
13111
  }
@@ -13673,10 +13794,10 @@ async function readHostCpToken2() {
13673
13794
  if (!fs25.existsSync(tp)) return null;
13674
13795
  return fs25.readFileSync(tp, "utf-8").trim();
13675
13796
  }
13676
- async function callHostCpProxy(method, worldId, path49, body) {
13797
+ async function callHostCpProxy(method, worldId, path50, body) {
13677
13798
  const token = await readHostCpToken2();
13678
13799
  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}`;
13800
+ const url = `http://127.0.0.1:${HOST_CP_PORT}/api/world/${encodeURIComponent(worldId)}${path50}`;
13680
13801
  try {
13681
13802
  const headers = {
13682
13803
  Authorization: `Bearer ${token}`
@@ -14561,9 +14682,9 @@ var UnknownArchetypeError = class extends Error {
14561
14682
  };
14562
14683
  var ArchetypeCycleError = class extends Error {
14563
14684
  path;
14564
- constructor(path49) {
14565
- super(`Archetype inheritance cycle detected: ${path49.join(" \u2192 ")} \u2192 ${path49[0] ?? "?"}`);
14566
- this.path = path49;
14685
+ constructor(path50) {
14686
+ super(`Archetype inheritance cycle detected: ${path50.join(" \u2192 ")} \u2192 ${path50[0] ?? "?"}`);
14687
+ this.path = path50;
14567
14688
  this.name = "ArchetypeCycleError";
14568
14689
  }
14569
14690
  };
@@ -14837,7 +14958,7 @@ var realDocker = {
14837
14958
  }
14838
14959
  };
14839
14960
  function spawnAsync(cmd, args, opts = {}) {
14840
- return new Promise((resolve10) => {
14961
+ return new Promise((resolve11) => {
14841
14962
  const child = spawn3(cmd, [...args], {
14842
14963
  stdio: ["ignore", "pipe", "pipe"],
14843
14964
  signal: opts.signal
@@ -14851,10 +14972,10 @@ function spawnAsync(cmd, args, opts = {}) {
14851
14972
  stderr += chunk.toString();
14852
14973
  });
14853
14974
  child.on("error", (err) => {
14854
- resolve10({ exitCode: -1, stdout, stderr: stderr + err.message });
14975
+ resolve11({ exitCode: -1, stdout, stderr: stderr + err.message });
14855
14976
  });
14856
14977
  child.on("close", (code) => {
14857
- resolve10({ exitCode: code ?? -1, stdout, stderr });
14978
+ resolve11({ exitCode: code ?? -1, stdout, stderr });
14858
14979
  });
14859
14980
  });
14860
14981
  }
@@ -15197,10 +15318,10 @@ async function confirm(message) {
15197
15318
  if (!process.stdin.isTTY) return true;
15198
15319
  const { createInterface: createInterface5 } = await import("node:readline");
15199
15320
  const rl = createInterface5({ input: process.stdin, output: process.stdout });
15200
- return new Promise((resolve10) => {
15321
+ return new Promise((resolve11) => {
15201
15322
  rl.question(`${message} [y/N] `, (answer) => {
15202
15323
  rl.close();
15203
- resolve10(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
15324
+ resolve11(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
15204
15325
  });
15205
15326
  });
15206
15327
  }
@@ -15574,7 +15695,7 @@ var McpAuthContainerController = class {
15574
15695
  }
15575
15696
  };
15576
15697
  function sleep3(ms) {
15577
- return new Promise((resolve10) => setTimeout(resolve10, ms));
15698
+ return new Promise((resolve11) => setTimeout(resolve11, ms));
15578
15699
  }
15579
15700
  function dumpContainerLogs(container, tail = 40) {
15580
15701
  try {
@@ -15861,9 +15982,10 @@ import pc10 from "picocolors";
15861
15982
  import { execSync as execSync7 } from "node:child_process";
15862
15983
  import { existsSync as existsSync26, statSync as statSync6 } from "node:fs";
15863
15984
  import { join as join31 } from "node:path";
15864
- var DEFAULT_DEVBOX_IMAGE = "olam-devbox:latest";
15985
+ var DEFAULT_DEVBOX_IMAGE = "olam-devbox:base";
15865
15986
  var DEVBOX_BAKED_SOURCES = [
15866
- "packages/adapters/src/docker/devbox.Dockerfile",
15987
+ "packages/adapters/src/docker/devbox.base.Dockerfile",
15988
+ "packages/adapters/src/docker/devbox.browser.Dockerfile",
15867
15989
  "packages/control-plane/standalone/server.mjs",
15868
15990
  "packages/control-plane/standalone/intelligence-bridge.mjs",
15869
15991
  "packages/control-plane/standalone/d1-sqlite-adapter.mjs",
@@ -15928,9 +16050,9 @@ function formatFreshnessWarning(result, image = DEFAULT_DEVBOX_IMAGE) {
15928
16050
  "These source files have changed since the image was built; the",
15929
16051
  "changes will NOT take effect in fresh worlds until you rebuild:"
15930
16052
  ];
15931
- for (const { path: path49, mtimeMs } of result.newerSources) {
16053
+ for (const { path: path50, mtimeMs } of result.newerSources) {
15932
16054
  const when = new Date(mtimeMs).toISOString();
15933
- lines.push(` \u2022 ${path49} (modified ${when})`);
16055
+ lines.push(` \u2022 ${path50} (modified ${when})`);
15934
16056
  }
15935
16057
  lines.push("");
15936
16058
  lines.push("Rebuild with:");
@@ -16089,15 +16211,15 @@ init_host_cp();
16089
16211
  var HOST_CP_URL = "http://127.0.0.1:19000";
16090
16212
  async function readHostCpTokenForCreate() {
16091
16213
  try {
16092
- const { default: fs47 } = await import("node:fs");
16214
+ const { default: fs48 } = await import("node:fs");
16093
16215
  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"),
16216
+ const { default: path50 } = await import("node:path");
16217
+ const tp = path50.join(
16218
+ process.env.OLAM_HOME ?? path50.join(os28.homedir(), ".olam"),
16097
16219
  "host-cp.token"
16098
16220
  );
16099
- if (!fs47.existsSync(tp)) return null;
16100
- return fs47.readFileSync(tp, "utf-8").trim();
16221
+ if (!fs48.existsSync(tp)) return null;
16222
+ return fs48.readFileSync(tp, "utf-8").trim();
16101
16223
  } catch {
16102
16224
  return null;
16103
16225
  }
@@ -16460,12 +16582,12 @@ function defaultNameFromPrompt(prompt) {
16460
16582
  }
16461
16583
  async function readHostCpToken3() {
16462
16584
  try {
16463
- const { default: fs47 } = await import("node:fs");
16585
+ const { default: fs48 } = await import("node:fs");
16464
16586
  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();
16587
+ const { default: path50 } = await import("node:path");
16588
+ const tp = path50.join(os28.homedir(), ".olam", "host-cp.token");
16589
+ if (!fs48.existsSync(tp)) return null;
16590
+ const raw = fs48.readFileSync(tp, "utf-8").trim();
16469
16591
  return raw.length > 0 ? raw : null;
16470
16592
  } catch {
16471
16593
  return null;
@@ -17133,14 +17255,14 @@ function printTable(entries) {
17133
17255
  async function confirmInteractive() {
17134
17256
  process.stdout.write(" Type `yes` to proceed: ");
17135
17257
  const buf = [];
17136
- return new Promise((resolve10) => {
17258
+ return new Promise((resolve11) => {
17137
17259
  const onData = (chunk) => {
17138
17260
  buf.push(chunk);
17139
17261
  if (Buffer.concat(buf).toString("utf-8").includes("\n")) {
17140
17262
  process.stdin.removeListener("data", onData);
17141
17263
  process.stdin.pause();
17142
17264
  const answer = Buffer.concat(buf).toString("utf-8").trim();
17143
- resolve10(answer.toLowerCase() === "yes");
17265
+ resolve11(answer.toLowerCase() === "yes");
17144
17266
  }
17145
17267
  };
17146
17268
  process.stdin.resume();
@@ -20196,11 +20318,11 @@ function zodIssueToError(issue, doc, lineCounter) {
20196
20318
  suggestion: deriveSuggestion(issue)
20197
20319
  };
20198
20320
  }
20199
- function formatJsonPath(path49) {
20200
- if (path49.length === 0)
20321
+ function formatJsonPath(path50) {
20322
+ if (path50.length === 0)
20201
20323
  return "<root>";
20202
20324
  let out = "";
20203
- for (const seg of path49) {
20325
+ for (const seg of path50) {
20204
20326
  if (typeof seg === "number") {
20205
20327
  out += `[${seg}]`;
20206
20328
  } else {
@@ -20209,11 +20331,11 @@ function formatJsonPath(path49) {
20209
20331
  }
20210
20332
  return out;
20211
20333
  }
20212
- function resolveYamlLocation(path49, doc, lineCounter) {
20334
+ function resolveYamlLocation(path50, doc, lineCounter) {
20213
20335
  let bestLine = 0;
20214
20336
  let bestColumn = 0;
20215
- for (let depth = path49.length; depth >= 0; depth -= 1) {
20216
- const segment = path49.slice(0, depth);
20337
+ for (let depth = path50.length; depth >= 0; depth -= 1) {
20338
+ const segment = path50.slice(0, depth);
20217
20339
  try {
20218
20340
  const node = doc.getIn(segment, true);
20219
20341
  if (node && typeof node === "object" && "range" in node) {
@@ -20431,11 +20553,11 @@ function topoSort(nodes) {
20431
20553
  }
20432
20554
  function traceCycle(start, byId) {
20433
20555
  const seen = /* @__PURE__ */ new Set();
20434
- const path49 = [];
20556
+ const path50 = [];
20435
20557
  let current = start;
20436
20558
  while (current && !seen.has(current)) {
20437
20559
  seen.add(current);
20438
- path49.push(current);
20560
+ path50.push(current);
20439
20561
  const node = byId.get(current);
20440
20562
  const next = node?.dependsOn[0];
20441
20563
  if (next === void 0)
@@ -20443,10 +20565,10 @@ function traceCycle(start, byId) {
20443
20565
  current = next;
20444
20566
  }
20445
20567
  if (current && seen.has(current)) {
20446
- const idx = path49.indexOf(current);
20447
- return [...path49.slice(idx), current];
20568
+ const idx = path50.indexOf(current);
20569
+ return [...path50.slice(idx), current];
20448
20570
  }
20449
- return path49;
20571
+ return path50;
20450
20572
  }
20451
20573
 
20452
20574
  // ../core/dist/executor/types.js
@@ -22855,10 +22977,10 @@ async function confirm2(message) {
22855
22977
  if (!process.stdin.isTTY) return true;
22856
22978
  const { createInterface: createInterface5 } = await import("node:readline");
22857
22979
  const rl = createInterface5({ input: process.stdin, output: process.stdout });
22858
- return new Promise((resolve10) => {
22980
+ return new Promise((resolve11) => {
22859
22981
  rl.question(`${message} [y/N] `, (answer) => {
22860
22982
  rl.close();
22861
- resolve10(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
22983
+ resolve11(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
22862
22984
  });
22863
22985
  });
22864
22986
  }
@@ -22977,11 +23099,18 @@ async function runUpgradePullByDigest(deps = {}) {
22977
23099
  }
22978
23100
  infoSpinner.succeed("docker daemon reachable");
22979
23101
  const hasMcpAuthDigest = typeof digests["mcp-auth"] === "string" && digests["mcp-auth"].length > 0;
23102
+ const hasDevboxBaseDigest = typeof digests["devbox-base"] === "string" && digests["devbox-base"].length > 0;
22980
23103
  const baseRefs = [
22981
23104
  { name: "host-cp", ref: `${registry}/olam-host-cp@${digests["host-cp"]}` },
22982
23105
  { name: "auth", ref: `${registry}/olam-auth@${digests.auth}` },
22983
23106
  { name: "devbox", ref: `${registry}/olam-devbox@${digests.devbox}` }
22984
23107
  ];
23108
+ if (hasDevboxBaseDigest) {
23109
+ baseRefs.push({
23110
+ name: "devbox-base",
23111
+ ref: `${registry}/olam-devbox@${digests["devbox-base"]}`
23112
+ });
23113
+ }
22985
23114
  if (hasMcpAuthDigest) {
22986
23115
  baseRefs.push({
22987
23116
  name: "mcp-auth",
@@ -23046,18 +23175,29 @@ async function runUpgradePullByDigest(deps = {}) {
23046
23175
  }
23047
23176
  }
23048
23177
  handshakeSpinner.succeed(`Protocol handshake passed (all ${imageRefs.length} images)`);
23178
+ const refByName = (n) => {
23179
+ const found = imageRefs.find((r) => r.name === n);
23180
+ if (!found) throw new Error(`upgrade: missing imageRefs entry for "${n}"`);
23181
+ return found.ref;
23182
+ };
23049
23183
  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)" }
23184
+ { from: refByName("host-cp"), to: "olam-host-cp:latest", name: "host-cp (bare)" },
23185
+ { from: refByName("host-cp"), to: `${registry}/olam-host-cp:latest`, name: "host-cp (registry)" },
23186
+ { from: refByName("auth"), to: "olam-auth:local", name: "auth (bare)" },
23187
+ { from: refByName("auth"), to: `${registry}/olam-auth:latest`, name: "auth (registry)" },
23188
+ { from: refByName("devbox"), to: "olam-devbox:latest", name: "devbox (bare)" },
23189
+ { from: refByName("devbox"), to: `${registry}/olam-devbox:latest`, name: "devbox (registry)" }
23056
23190
  ];
23057
- if (hasMcpAuthDigest && imageRefs[3]) {
23191
+ if (hasDevboxBaseDigest) {
23058
23192
  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)" }
23193
+ { from: refByName("devbox-base"), to: "olam-devbox:base", name: "devbox-base (bare)" },
23194
+ { from: refByName("devbox-base"), to: `${registry}/olam-devbox:base`, name: "devbox-base (registry)" }
23195
+ );
23196
+ }
23197
+ if (hasMcpAuthDigest) {
23198
+ tagPlanBase.push(
23199
+ { from: refByName("mcp-auth"), to: "olam-mcp-auth:local", name: "mcp-auth (bare)" },
23200
+ { from: refByName("mcp-auth"), to: `${registry}/olam-mcp-auth:latest`, name: "mcp-auth (registry)" }
23061
23201
  );
23062
23202
  }
23063
23203
  const tagPlan = tagPlanBase;
@@ -25599,8 +25739,8 @@ var SECRET = process.env["OLAM_MCP_AUTH_SECRET"] ?? "";
25599
25739
  function authHeaders() {
25600
25740
  return SECRET ? { "X-Olam-Mcp-Secret": SECRET } : {};
25601
25741
  }
25602
- async function apiFetch(path49, init = {}) {
25603
- const res = await fetch(`${BASE_URL}${path49}`, {
25742
+ async function apiFetch(path50, init = {}) {
25743
+ const res = await fetch(`${BASE_URL}${path50}`, {
25604
25744
  ...init,
25605
25745
  headers: {
25606
25746
  "Content-Type": "application/json",
@@ -25687,7 +25827,7 @@ function registerMcpLogin(cmd) {
25687
25827
  init_output();
25688
25828
  import * as readline2 from "node:readline";
25689
25829
  async function readTokenSilent(prompt) {
25690
- return new Promise((resolve10, reject) => {
25830
+ return new Promise((resolve11, reject) => {
25691
25831
  const rl = readline2.createInterface({
25692
25832
  input: process.stdin,
25693
25833
  output: process.stdout,
@@ -25705,7 +25845,7 @@ async function readTokenSilent(prompt) {
25705
25845
  process.stdin.removeListener("data", onData);
25706
25846
  process.stdout.write("\n");
25707
25847
  rl.close();
25708
- resolve10(token);
25848
+ resolve11(token);
25709
25849
  } else if (char === "") {
25710
25850
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
25711
25851
  process.stdin.removeListener("data", onData);
@@ -25980,7 +26120,7 @@ async function discoverMcpSources(repoPaths) {
25980
26120
  import { spawn as spawn6 } from "node:child_process";
25981
26121
  var VALIDATION_TIMEOUT_MS = 5e3;
25982
26122
  async function validateMcpEntry(entry) {
25983
- return new Promise((resolve10) => {
26123
+ return new Promise((resolve11) => {
25984
26124
  let stdout = "";
25985
26125
  let timedOut = false;
25986
26126
  let child;
@@ -25990,7 +26130,7 @@ async function validateMcpEntry(entry) {
25990
26130
  env: { ...process.env, ...entry.env ?? {} }
25991
26131
  });
25992
26132
  } catch (err) {
25993
- resolve10({
26133
+ resolve11({
25994
26134
  name: entry.name,
25995
26135
  validated: false,
25996
26136
  reason: err instanceof Error ? err.message : "spawn failed"
@@ -26007,11 +26147,11 @@ async function validateMcpEntry(entry) {
26007
26147
  child.on("close", (code) => {
26008
26148
  clearTimeout(timer);
26009
26149
  if (timedOut) {
26010
- resolve10({ name: entry.name, validated: false, reason: "timeout (5s)" });
26150
+ resolve11({ name: entry.name, validated: false, reason: "timeout (5s)" });
26011
26151
  return;
26012
26152
  }
26013
26153
  const validated = code === 0 && stdout.trim().length > 0;
26014
- resolve10({
26154
+ resolve11({
26015
26155
  name: entry.name,
26016
26156
  validated,
26017
26157
  reason: validated ? "ok" : `exit code ${code ?? "null"}`
@@ -26019,7 +26159,7 @@ async function validateMcpEntry(entry) {
26019
26159
  });
26020
26160
  child.on("error", (err) => {
26021
26161
  clearTimeout(timer);
26022
- resolve10({ name: entry.name, validated: false, reason: err.message });
26162
+ resolve11({ name: entry.name, validated: false, reason: err.message });
26023
26163
  });
26024
26164
  });
26025
26165
  }
@@ -26034,11 +26174,11 @@ async function multiSelectPicker(entries) {
26034
26174
  );
26035
26175
  });
26036
26176
  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) => {
26177
+ const answer = await new Promise((resolve11) => {
26038
26178
  const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
26039
26179
  rl.question("> ", (ans) => {
26040
26180
  rl.close();
26041
- resolve10(ans.trim());
26181
+ resolve11(ans.trim());
26042
26182
  });
26043
26183
  });
26044
26184
  if (!answer || answer === "") return [];
@@ -26176,19 +26316,217 @@ function registerMcp(program2) {
26176
26316
  registerMcpRevoke(mcp);
26177
26317
  }
26178
26318
 
26319
+ // src/commands/kg-build.ts
26320
+ import { spawnSync as spawnSync14 } from "node:child_process";
26321
+ import fs46 from "node:fs";
26322
+ import path48 from "node:path";
26323
+
26324
+ // ../core/dist/kg/storage-paths.js
26325
+ import { homedir as homedir26 } from "node:os";
26326
+ import { join as join48, resolve as resolve10 } from "node:path";
26327
+
26328
+ // ../core/dist/world/workspace-name.js
26329
+ var InvalidWorkspaceNameError = class extends Error {
26330
+ constructor(name, reason) {
26331
+ super(`invalid workspace name ${JSON.stringify(name)}: ${reason}`);
26332
+ this.name = "InvalidWorkspaceNameError";
26333
+ }
26334
+ };
26335
+ var WORKSPACE_NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
26336
+ function validateWorkspaceName(name) {
26337
+ if (typeof name !== "string" || name.length === 0) {
26338
+ throw new InvalidWorkspaceNameError(String(name), "must be a non-empty string");
26339
+ }
26340
+ if (!WORKSPACE_NAME_RE.test(name)) {
26341
+ throw new InvalidWorkspaceNameError(name, "must match ^[a-z0-9][a-z0-9_-]*$ (lowercase letters, digits, hyphens, underscores; must start with letter or digit)");
26342
+ }
26343
+ }
26344
+
26345
+ // ../core/dist/kg/storage-paths.js
26346
+ function olamHome3() {
26347
+ return process.env.OLAM_HOME ?? join48(homedir26(), ".olam");
26348
+ }
26349
+ function kgRoot() {
26350
+ return join48(olamHome3(), "kg");
26351
+ }
26352
+ function worldsRoot() {
26353
+ return join48(olamHome3(), "worlds");
26354
+ }
26355
+ function assertWithinPrefix(path50, prefix, label) {
26356
+ if (!path50.startsWith(prefix + "/")) {
26357
+ throw new Error(`${label} escape: ${path50} not under ${prefix}/`);
26358
+ }
26359
+ }
26360
+ function kgPristinePath(workspace) {
26361
+ validateWorkspaceName(workspace);
26362
+ const root = kgRoot();
26363
+ const path50 = resolve10(join48(root, workspace));
26364
+ assertWithinPrefix(path50, root, "kgPristinePath");
26365
+ return path50;
26366
+ }
26367
+ var KG_PATHS_INTERNALS = Object.freeze({
26368
+ olamHome: olamHome3,
26369
+ kgRoot,
26370
+ worldsRoot
26371
+ });
26372
+
26373
+ // src/commands/kg-build.ts
26374
+ init_output();
26375
+ var DEFAULT_DEVBOX_IMAGE2 = "olam-devbox:latest";
26376
+ function resolveWorkspace(arg) {
26377
+ const cwd = process.cwd();
26378
+ const name = arg ?? path48.basename(cwd).toLowerCase();
26379
+ validateWorkspaceName(name);
26380
+ return { name, sourcePath: cwd };
26381
+ }
26382
+ function copyWorkspaceToScratch(source, scratch) {
26383
+ if (process.platform === "darwin") {
26384
+ const r2 = spawnSync14("cp", ["-c", "-r", source + "/.", scratch], {
26385
+ stdio: ["ignore", "ignore", "pipe"],
26386
+ encoding: "utf-8"
26387
+ });
26388
+ if (r2.status === 0) return "cp-c-r-reflink";
26389
+ }
26390
+ const r = spawnSync14("cp", ["-r", source + "/.", scratch], {
26391
+ stdio: ["ignore", "ignore", "pipe"],
26392
+ encoding: "utf-8"
26393
+ });
26394
+ if (r.status !== 0) {
26395
+ throw new Error(`cp -r failed: ${(r.stderr ?? "").trim() || r.error?.message}`);
26396
+ }
26397
+ return "cp-r";
26398
+ }
26399
+ function parseNodeCount(graphifyOutDir) {
26400
+ const graphPath = path48.join(graphifyOutDir, "graph.json");
26401
+ if (!fs46.existsSync(graphPath)) return null;
26402
+ try {
26403
+ const content = fs46.readFileSync(graphPath, "utf-8");
26404
+ const data = JSON.parse(content);
26405
+ return Array.isArray(data.nodes) ? data.nodes.length : null;
26406
+ } catch {
26407
+ return null;
26408
+ }
26409
+ }
26410
+ function readGraphifyVersion(image) {
26411
+ const r = spawnSync14(
26412
+ "docker",
26413
+ [
26414
+ "run",
26415
+ "--rm",
26416
+ "--entrypoint=/opt/graphify-venv/bin/pip",
26417
+ image,
26418
+ "show",
26419
+ "graphifyy"
26420
+ ],
26421
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }
26422
+ );
26423
+ if (r.status !== 0) return "unknown";
26424
+ const m = (r.stdout ?? "").match(/^Version:\s*(\S+)/m);
26425
+ return m?.[1] ?? "unknown";
26426
+ }
26427
+ async function runKgBuild(workspaceArg, options = {}) {
26428
+ const image = options.image ?? DEFAULT_DEVBOX_IMAGE2;
26429
+ let workspace;
26430
+ try {
26431
+ workspace = resolveWorkspace(workspaceArg);
26432
+ } catch (err) {
26433
+ printError(err instanceof Error ? err.message : String(err));
26434
+ return { exitCode: 2 };
26435
+ }
26436
+ const outDir = kgPristinePath(workspace.name);
26437
+ const scratchDir = path48.join(outDir, "scratch");
26438
+ fs46.mkdirSync(outDir, { recursive: true });
26439
+ if (fs46.existsSync(scratchDir)) fs46.rmSync(scratchDir, { recursive: true, force: true });
26440
+ fs46.mkdirSync(scratchDir);
26441
+ const human = !options.json;
26442
+ if (human) {
26443
+ printInfo("kg build", `workspace=${workspace.name} source=${workspace.sourcePath}`);
26444
+ printInfo("kg build", `scratch=${scratchDir} \u2192 out=${outDir}/graphify-out/`);
26445
+ }
26446
+ const started = Date.now();
26447
+ let scratchStrategy;
26448
+ try {
26449
+ scratchStrategy = copyWorkspaceToScratch(workspace.sourcePath, scratchDir);
26450
+ if (human) printInfo("kg build", `copied via ${scratchStrategy}`);
26451
+ const dockerArgs = [
26452
+ "run",
26453
+ "--rm",
26454
+ "-v",
26455
+ `${scratchDir}:/work`,
26456
+ "-w",
26457
+ "/work",
26458
+ "--entrypoint=graphify",
26459
+ image,
26460
+ "update",
26461
+ "."
26462
+ ];
26463
+ const r = human ? spawnSync14("docker", dockerArgs, { stdio: "inherit" }) : spawnSync14("docker", dockerArgs, { stdio: ["ignore", "ignore", "pipe"] });
26464
+ if (r.status !== 0) {
26465
+ printError(`graphify update failed (exit ${r.status})`);
26466
+ return { exitCode: r.status ?? 1 };
26467
+ }
26468
+ const scratchOut = path48.join(scratchDir, "graphify-out");
26469
+ const finalOut = path48.join(outDir, "graphify-out");
26470
+ if (!fs46.existsSync(scratchOut)) {
26471
+ printError(`graphify produced no graphify-out/ in scratch (${scratchOut})`);
26472
+ return { exitCode: 1 };
26473
+ }
26474
+ if (fs46.existsSync(finalOut)) fs46.rmSync(finalOut, { recursive: true, force: true });
26475
+ fs46.renameSync(scratchOut, finalOut);
26476
+ const durationMs = Date.now() - started;
26477
+ const nodeCount = parseNodeCount(finalOut);
26478
+ const version = readGraphifyVersion(image);
26479
+ const freshness = {
26480
+ built_at: (/* @__PURE__ */ new Date()).toISOString(),
26481
+ duration_ms: durationMs,
26482
+ node_count: nodeCount,
26483
+ graphify_version: version,
26484
+ workspace: workspace.name,
26485
+ scratch_strategy: scratchStrategy
26486
+ };
26487
+ fs46.writeFileSync(
26488
+ path48.join(outDir, "freshness.json"),
26489
+ JSON.stringify(freshness, null, 2) + "\n",
26490
+ "utf-8"
26491
+ );
26492
+ if (options.json) {
26493
+ process.stdout.write(JSON.stringify(freshness) + "\n");
26494
+ } else {
26495
+ printSuccess(
26496
+ `kg build ${workspace.name} \u2014 ${nodeCount ?? "?"} nodes in ${(durationMs / 1e3).toFixed(1)}s (graphify ${version})`
26497
+ );
26498
+ printInfo("output", `${finalOut}/graph.json`);
26499
+ }
26500
+ return { exitCode: 0, freshness, outputDir: finalOut };
26501
+ } finally {
26502
+ if (fs46.existsSync(scratchDir)) {
26503
+ fs46.rmSync(scratchDir, { recursive: true, force: true });
26504
+ }
26505
+ }
26506
+ }
26507
+ function registerKg(program2) {
26508
+ const kg = program2.command("kg").description("Knowledge-graph operations (graphify-backed)");
26509
+ kg.command("build").description(
26510
+ "Build pristine KG for a workspace (default: current dir). Scratch-dir pattern; ~/.olam/kg/<ws>/graphify-out/"
26511
+ ).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) => {
26512
+ const r = await runKgBuild(workspaceArg, opts);
26513
+ if (r.exitCode !== 0) process.exitCode = r.exitCode;
26514
+ });
26515
+ }
26516
+
26179
26517
  // src/pleri-config.ts
26180
- import * as fs46 from "node:fs";
26181
- import * as path48 from "node:path";
26518
+ import * as fs47 from "node:fs";
26519
+ import * as path49 from "node:path";
26182
26520
  function isPleriConfigured(configDir = process.env.OLAM_CONFIG_DIR ?? ".olam") {
26183
26521
  if (process.env.PLERI_BASE_URL) {
26184
26522
  return true;
26185
26523
  }
26186
- const configPath = path48.join(configDir, "config.yaml");
26187
- if (!fs46.existsSync(configPath)) {
26524
+ const configPath = path49.join(configDir, "config.yaml");
26525
+ if (!fs47.existsSync(configPath)) {
26188
26526
  return false;
26189
26527
  }
26190
26528
  try {
26191
- const contents = fs46.readFileSync(configPath, "utf8");
26529
+ const contents = fs47.readFileSync(configPath, "utf8");
26192
26530
  return /^[^#\n]*\bpleri:/m.test(contents);
26193
26531
  } catch {
26194
26532
  return false;
@@ -26230,6 +26568,7 @@ registerBegin(program);
26230
26568
  registerStop(program);
26231
26569
  registerWorldUpgrade(program);
26232
26570
  registerMcp(program);
26571
+ registerKg(program);
26233
26572
  registerConfig(program);
26234
26573
  registerRepos(program);
26235
26574
  registerRunbooks(program);