@mifune/agro 0.19.0 → 0.20.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/agro.js +623 -141
  2. package/package.json +1 -1
package/dist/agro.js CHANGED
@@ -7,7 +7,7 @@ var __export = (target, all) => {
7
7
 
8
8
  // src/cli.ts
9
9
  import { fileURLToPath } from "node:url";
10
- import { dirname as dirname10, join as join19, resolve as resolve11 } from "node:path";
10
+ import { dirname as dirname10, join as join20, resolve as resolve13 } from "node:path";
11
11
  import { existsSync as existsSync23, mkdtempSync as mkdtempSync5, readFileSync as readFileSync13, rmSync as rmSync6 } from "node:fs";
12
12
  import { tmpdir as tmpdir4 } from "node:os";
13
13
 
@@ -844,6 +844,49 @@ import { resolve as resolve3, sep } from "node:path";
844
844
  // src/lib/agro-config.ts
845
845
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
846
846
  import { basename as basename3, dirname as dirname2, resolve as resolve2 } from "node:path";
847
+
848
+ // src/lib/runtimes/catalog.ts
849
+ var CATALOG = [
850
+ {
851
+ id: "docker",
852
+ title: "Docker container",
853
+ tier: "container",
854
+ state: "active",
855
+ provisionable: true,
856
+ docsPath: "docs/runtimes/overview.md"
857
+ },
858
+ {
859
+ id: "microsandbox",
860
+ title: "MicroSandbox",
861
+ tier: "microvm",
862
+ state: "planned",
863
+ provisionable: false,
864
+ notProvisionableReason: (bin) => `microsandbox is not a provisionable runtime yet; see https://github.com/mifunedev/agro/issues/592. Inside a sandbox run \`${bin} tool install microsandbox\`.`,
865
+ docsPath: "docs/runtimes/microsandbox.md"
866
+ },
867
+ {
868
+ id: "openshell",
869
+ title: "NVIDIA OpenShell",
870
+ tier: "container",
871
+ state: "active",
872
+ provisionable: true,
873
+ docsPath: "docs/runtimes/openshell.md"
874
+ }
875
+ ];
876
+ var RUNTIME_CATALOG = Object.freeze(
877
+ CATALOG.map((entry) => Object.freeze({ ...entry }))
878
+ );
879
+ var PROVISIONABLE_RUNTIMES = Object.freeze(
880
+ CATALOG.filter((entry) => entry.provisionable).map((entry) => entry.id)
881
+ );
882
+ function findRuntime(id) {
883
+ return RUNTIME_CATALOG.find((r) => r.id === id);
884
+ }
885
+ function runtimeIds() {
886
+ return RUNTIME_CATALOG.map((r) => r.id);
887
+ }
888
+
889
+ // src/lib/agro-config.ts
847
890
  var AGRO_CONFIG_FILE = NAMES.configFile;
848
891
  var AGRO_CONFIG_MODE = 420;
849
892
  var RESERVED_HOME_PATHS = [
@@ -865,7 +908,7 @@ var RESERVED_HOME_PATHS = [
865
908
  "/usr",
866
909
  "/var"
867
910
  ];
868
- var SANDBOX_RUNTIMES = ["docker"];
911
+ var SANDBOX_RUNTIMES = PROVISIONABLE_RUNTIMES;
869
912
  function configCheckout(config) {
870
913
  const value = config.checkout ?? config.repo;
871
914
  return typeof value === "string" && value !== "" ? value : void 0;
@@ -894,6 +937,13 @@ function defaultAgroConfig(name) {
894
937
  composeOverrides: []
895
938
  };
896
939
  }
940
+ function entryRuntime(root) {
941
+ try {
942
+ return readAgroConfig(agroConfigPath(root)).runtime ?? "docker";
943
+ } catch {
944
+ return "docker";
945
+ }
946
+ }
897
947
  function readAgroConfig(path4) {
898
948
  if (!existsSync4(path4)) return defaultAgroConfig(basename3(dirname2(resolve2(path4))));
899
949
  let raw;
@@ -1283,8 +1333,8 @@ function redact(token) {
1283
1333
  async function ask(question) {
1284
1334
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1285
1335
  try {
1286
- return await new Promise((resolve12) => {
1287
- rl.question(` ${question} `, (answer) => resolve12(answer.trim()));
1336
+ return await new Promise((resolve14) => {
1337
+ rl.question(` ${question} `, (answer) => resolve14(answer.trim()));
1288
1338
  });
1289
1339
  } finally {
1290
1340
  rl.close();
@@ -1296,7 +1346,7 @@ async function askSecret(question) {
1296
1346
  process.stdin.setRawMode(true);
1297
1347
  process.stdin.resume();
1298
1348
  const bytes = [];
1299
- return await new Promise((resolve12) => {
1349
+ return await new Promise((resolve14) => {
1300
1350
  const cleanup = () => {
1301
1351
  try {
1302
1352
  process.stdin.setRawMode(false);
@@ -1314,7 +1364,7 @@ async function askSecret(question) {
1314
1364
  } else if (byte === 13 || byte === 10) {
1315
1365
  cleanup();
1316
1366
  process.stdout.write("\n");
1317
- resolve12(Buffer.from(bytes).toString("utf8").trim());
1367
+ resolve14(Buffer.from(bytes).toString("utf8").trim());
1318
1368
  return;
1319
1369
  } else if (byte === 127 || byte === 8) {
1320
1370
  if (bytes.length > 0) {
@@ -1364,10 +1414,10 @@ async function askSecretPiped(question) {
1364
1414
  };
1365
1415
  process.on("SIGINT", onSigint);
1366
1416
  try {
1367
- return await new Promise((resolve12) => {
1417
+ return await new Promise((resolve14) => {
1368
1418
  rl.question(` ${question} `, (answer) => {
1369
1419
  process.stdout.write("\n");
1370
- resolve12(answer.trim());
1420
+ resolve14(answer.trim());
1371
1421
  });
1372
1422
  });
1373
1423
  } finally {
@@ -1752,6 +1802,9 @@ printf 'free\\n'
1752
1802
  exit 0
1753
1803
  `;
1754
1804
 
1805
+ // agro-asset:/home/runner/work/agro/agro/.devcontainer/openshell-policy.yaml
1806
+ var openshell_policy_default = "version: 1\n\nfilesystem_policy:\n include_workdir: true\n read_only:\n - /bin\n - /usr\n - /lib\n - /etc\n - /opt\n - /proc\n - /var/log\n - /dev/urandom\n read_write:\n - /home/sandbox\n - /tmp\n - /dev/null\n\nprocess:\n run_as_user: sandbox\n run_as_group: sandbox\n\nnetwork_policies:\n github:\n endpoints:\n - host: github.com\n port: 443\n protocol: rest\n enforcement: enforce\n access: read-write\n - host: api.github.com\n port: 443\n protocol: rest\n enforcement: enforce\n access: read-write\n - host: release-assets.githubusercontent.com\n port: 443\n protocol: rest\n enforcement: enforce\n access: read-only\n binaries:\n - path: /usr/bin/git\n - path: /usr/bin/gh\n - path: /usr/bin/curl\n npm:\n endpoints:\n - host: registry.npmjs.org\n port: 443\n protocol: rest\n enforcement: enforce\n access: read-only\n allow_encoded_slash: true\n binaries:\n - path: /usr/local/bin/node\n anthropic:\n endpoints:\n - host: api.anthropic.com\n port: 443\n protocol: rest\n enforcement: enforce\n access: read-write\n binaries:\n - path: /home/sandbox/.local/lib/node_modules/@anthropic-ai/claude-code/bin/*\n";
1807
+
1755
1808
  // src/lib/registry.ts
1756
1809
  var SANDBOX_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
1757
1810
  var DEFAULT_NAME_PREFIX = "agro-sbx-";
@@ -1808,10 +1861,12 @@ function nextDefaultName(run = spawnRunner) {
1808
1861
  if (!taken.has(candidate)) return candidate;
1809
1862
  }
1810
1863
  }
1811
- function materialize(root, opts = {}) {
1812
- const entry = resolve5(root);
1864
+ function openshellFiles(entry) {
1865
+ return [{ dest: join4(entry, "openshell-policy.yaml"), body: openshell_policy_default, mode: 420 }];
1866
+ }
1867
+ function dockerFiles(entry, opts) {
1813
1868
  const scripts = join4(resolveProjectLayout(entry).controlDir, "scripts");
1814
- const files = [
1869
+ return [
1815
1870
  {
1816
1871
  dest: join4(entry, ".devcontainer", "docker-compose.yml"),
1817
1872
  body: (opts.checkout ?? opts.repo) === void 0 ? docker_compose_image_only_default : docker_compose_default,
@@ -1823,6 +1878,10 @@ function materialize(root, opts = {}) {
1823
1878
  { dest: join4(scripts, "paths.sh"), body: paths_default, mode: 420 },
1824
1879
  { dest: join4(scripts, "check-host-port.sh"), body: check_host_port_default, mode: 493 }
1825
1880
  ];
1881
+ }
1882
+ function materialize(root, opts = {}) {
1883
+ const entry = resolve5(root);
1884
+ const files = entryRuntime(entry) === "openshell" ? openshellFiles(entry) : dockerFiles(entry, opts);
1826
1885
  for (const file of files) {
1827
1886
  const dest = resolve5(file.dest);
1828
1887
  if (!dest.startsWith(entry + sep2)) {
@@ -1981,12 +2040,28 @@ function stripQuotes2(s) {
1981
2040
  // src/commands/lifecycle.ts
1982
2041
  import { existsSync as existsSync10, mkdtempSync, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
1983
2042
  import { tmpdir } from "node:os";
1984
- import { basename as basename4, join as join5, sep as sep3 } from "node:path";
2043
+ import { basename as basename6, join as join6, sep as sep3 } from "node:path";
2044
+
2045
+ // src/lib/execution/index.ts
2046
+ import { basename as basename4, resolve as resolve7 } from "node:path";
2047
+
2048
+ // src/lib/version.ts
2049
+ var AGRO_VERSION = true ? "0.20.0-rc.1" : "0.0.0-dev";
2050
+ var OFFICIAL_IMAGE = "ghcr.io/mifunedev/agro";
2051
+ var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z][a-z0-9]*\.[0-9]+)?$/;
2052
+ function parseReleaseVersion(value) {
2053
+ const bare = value.startsWith("v") ? value.slice(1) : value;
2054
+ return RELEASE_VERSION.test(bare) ? bare : void 0;
2055
+ }
2056
+ function officialImageRef(version) {
2057
+ return `${OFFICIAL_IMAGE}:${RELEASE_VERSION.test(version) ? version : "latest"}`;
2058
+ }
1985
2059
 
1986
2060
  // src/lib/execution/detect.ts
1987
2061
  import { existsSync as existsSync9 } from "node:fs";
1988
2062
  var SANDBOX_MARKER_FILE = "/etc/agro/sandbox";
1989
2063
  var DOCKERENV_FILE = "/.dockerenv";
2064
+ var EXECUTION_TARGET_ENV = "AGRO_EXECUTION_TARGET";
1990
2065
  function detectSandbox(env = process.env, fileExists = existsSync9) {
1991
2066
  const override = agroEnvValue(env, "EXECUTION_TARGET");
1992
2067
  if (override === "local") return { inside: true, rule: "override" };
@@ -2224,6 +2299,243 @@ function defaultIdentity() {
2224
2299
  return { name: info2.username, uid: info2.uid };
2225
2300
  }
2226
2301
 
2302
+ // src/lib/execution/openshell-target.ts
2303
+ import { join as join5 } from "node:path";
2304
+ var OPENSHELL_BIN = "openshell";
2305
+ var OPENSHELL_POLICY_FILE = "openshell-policy.yaml";
2306
+ var OPENSHELL_MAIN = "/usr/local/bin/openshell-main.sh";
2307
+ var OPENSHELL_WORKDIR = "/home/sandbox/harness";
2308
+ var OPENSHELL_USER = "sandbox";
2309
+ var OPENSHELL_PINNED_VERSION = "v0.1.2";
2310
+ var OPENSHELL_LOCAL_GATEWAY = "https://127.0.0.1:17670";
2311
+ var OPENSHELL_INSTALL_URL = "https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh";
2312
+ var LOGIN_SHELLS = /* @__PURE__ */ new Set(["zsh", "bash"]);
2313
+ var NOT_FOUND = /sandbox (?:'[^']*' )?not found/;
2314
+ var OPENSHELL_PHASES = [
2315
+ "Unspecified",
2316
+ "Provisioning",
2317
+ "Ready",
2318
+ "Error",
2319
+ "Deleting",
2320
+ "Unknown",
2321
+ "Stopping",
2322
+ "Stopped",
2323
+ "Starting",
2324
+ "Completed"
2325
+ ];
2326
+ var OPENSHELL_PHASE_STATUS = Object.freeze({
2327
+ Ready: "ready",
2328
+ Provisioning: "starting",
2329
+ Starting: "starting",
2330
+ Stopping: "stopped",
2331
+ Stopped: "stopped",
2332
+ Deleting: "stopped",
2333
+ Completed: "stopped",
2334
+ Error: "failed",
2335
+ Unknown: "failed",
2336
+ Unspecified: "failed"
2337
+ });
2338
+ function createArgv(opts) {
2339
+ return [
2340
+ "sandbox",
2341
+ "create",
2342
+ "--name",
2343
+ opts.name,
2344
+ "--from",
2345
+ opts.image,
2346
+ "--policy",
2347
+ join5(opts.entryRoot, OPENSHELL_POLICY_FILE),
2348
+ "--env",
2349
+ `${EXECUTION_TARGET_ENV}=local`,
2350
+ "--env",
2351
+ `SANDBOX_NAME=${opts.name}`,
2352
+ "--detach",
2353
+ "--",
2354
+ OPENSHELL_MAIN
2355
+ ];
2356
+ }
2357
+ function attachArgv(name, request = { argv: ["zsh"] }) {
2358
+ return [
2359
+ "sandbox",
2360
+ "exec",
2361
+ "-n",
2362
+ name,
2363
+ "--tty",
2364
+ "--workdir",
2365
+ request.cwd ?? OPENSHELL_WORKDIR,
2366
+ ...envFlags(request.env),
2367
+ "--",
2368
+ ...asLoginShell(request.argv)
2369
+ ];
2370
+ }
2371
+ function execArgv(name, request) {
2372
+ return [
2373
+ "sandbox",
2374
+ "exec",
2375
+ "-n",
2376
+ name,
2377
+ ...request.cwd !== void 0 ? ["--workdir", request.cwd] : [],
2378
+ ...envFlags(request.env),
2379
+ ...request.timeoutMs !== void 0 ? ["--timeout", String(Math.ceil(request.timeoutMs / 1e3))] : [],
2380
+ "--",
2381
+ ...request.argv
2382
+ ];
2383
+ }
2384
+ function getArgv(name) {
2385
+ return ["sandbox", "get", name, "-o", "json"];
2386
+ }
2387
+ function deleteArgv(name) {
2388
+ return ["sandbox", "delete", name];
2389
+ }
2390
+ function parseOpenShellPhase(json) {
2391
+ let parsed;
2392
+ try {
2393
+ parsed = JSON.parse(json);
2394
+ } catch {
2395
+ return "Unknown";
2396
+ }
2397
+ const phase = typeof parsed === "object" && parsed !== null ? parsed.phase : void 0;
2398
+ return OPENSHELL_PHASES.find((known) => known === phase) ?? "Unknown";
2399
+ }
2400
+ function openshellSandboxMissing(output) {
2401
+ return NOT_FOUND.test(output);
2402
+ }
2403
+ var OPENSHELL_VERB_HINTS = Object.freeze({
2404
+ stop: (name) => `openshell sandbox stop ${name}`,
2405
+ restart: (name) => `openshell sandbox stop ${name} && openshell sandbox start ${name}`,
2406
+ logs: (name) => `openshell logs ${name}`,
2407
+ ps: (name) => `openshell sandbox get ${name}`,
2408
+ config: (name) => `openshell policy get ${name}`
2409
+ });
2410
+ function openshellInstallHint() {
2411
+ return [
2412
+ "install the OpenShell CLI on the host, then run the command again:",
2413
+ ` curl -LsSf ${OPENSHELL_INSTALL_URL} | OPENSHELL_VERSION=${OPENSHELL_PINNED_VERSION} sh`,
2414
+ "review-first alternative \u2014 download the script, inspect it, then run it:",
2415
+ ` curl -LsSf -o openshell-install.sh ${OPENSHELL_INSTALL_URL}`,
2416
+ " less openshell-install.sh",
2417
+ ` OPENSHELL_VERSION=${OPENSHELL_PINNED_VERSION} sh openshell-install.sh`
2418
+ ].join("\n");
2419
+ }
2420
+ function openshellGatewayHint() {
2421
+ return [
2422
+ "the OpenShell gateway is not connected; start the local gateway and register it:",
2423
+ " systemctl --user restart openshell-gateway",
2424
+ ` openshell gateway add ${OPENSHELL_LOCAL_GATEWAY} --local --name openshell`,
2425
+ "then confirm with: openshell status"
2426
+ ].join("\n");
2427
+ }
2428
+ function openshellCleanupHint(name) {
2429
+ return `the OpenShell gateway can keep a failed sandbox record in the Error phase; if ${name} is not another sandbox, remove it with: openshell sandbox delete ${name}`;
2430
+ }
2431
+ function openshellPreflight(run) {
2432
+ const version = run(OPENSHELL_BIN, ["--version"], { stdio: "capture" });
2433
+ if (version.error?.code === "ENOENT") {
2434
+ return { ok: false, reason: "missing-binary", hint: openshellInstallHint() };
2435
+ }
2436
+ assertSpawned(version, `${OPENSHELL_BIN} --version`);
2437
+ const status = run(OPENSHELL_BIN, ["status", "-o", "json"], { stdio: "capture" });
2438
+ assertSpawned(status, `${OPENSHELL_BIN} status`);
2439
+ if (status.status !== 0 || !gatewayConnected(status.stdout ?? "")) {
2440
+ return { ok: false, reason: "gateway-unreachable", hint: openshellGatewayHint() };
2441
+ }
2442
+ return { ok: true };
2443
+ }
2444
+ var OpenShellExecutionTarget = class {
2445
+ kind = "openshell";
2446
+ contractVersion = 1;
2447
+ workspace;
2448
+ name;
2449
+ entryRoot;
2450
+ image;
2451
+ run;
2452
+ env;
2453
+ constructor(opts) {
2454
+ this.name = opts.name;
2455
+ this.entryRoot = opts.entryRoot;
2456
+ this.image = opts.image;
2457
+ this.run = opts.run ?? spawnRunner;
2458
+ this.env = opts.env;
2459
+ this.workspace = { hostRoot: opts.entryRoot, targetRoot: OPENSHELL_WORKDIR };
2460
+ }
2461
+ async provision() {
2462
+ const argv = createArgv({ name: this.name, image: this.image, entryRoot: this.entryRoot });
2463
+ this.runInherited(argv, "sandbox create");
2464
+ }
2465
+ async destroy() {
2466
+ this.runInherited(deleteArgv(this.name), "sandbox delete");
2467
+ }
2468
+ async status() {
2469
+ const r = this.run(OPENSHELL_BIN, getArgv(this.name), { stdio: "capture", ...this.childEnv() });
2470
+ assertSpawned(r, this.what("sandbox get"));
2471
+ if (r.status !== 0) {
2472
+ if (openshellSandboxMissing(`${r.stdout ?? ""}
2473
+ ${r.stderr ?? ""}`)) return "absent";
2474
+ throw new ExecutionExitError(this.what("sandbox get"), r.status ?? 1);
2475
+ }
2476
+ return OPENSHELL_PHASE_STATUS[parseOpenShellPhase(r.stdout ?? "")];
2477
+ }
2478
+ async capabilities() {
2479
+ return /* @__PURE__ */ new Set(["exec", "pty"]);
2480
+ }
2481
+ async exec(request) {
2482
+ requireSandboxUser(request);
2483
+ const inherit = request.stdio === "inherit";
2484
+ const r = this.run(OPENSHELL_BIN, execArgv(this.name, request), {
2485
+ stdio: inherit ? "inherit" : "capture",
2486
+ ...this.childEnv()
2487
+ });
2488
+ assertSpawned(r, this.what("sandbox exec"));
2489
+ return {
2490
+ exitCode: r.status ?? 1,
2491
+ stdout: r.stdout ?? "",
2492
+ stderr: r.stderr ?? ""
2493
+ };
2494
+ }
2495
+ attach(request) {
2496
+ requireSandboxUser(request);
2497
+ const r = this.run(OPENSHELL_BIN, attachArgv(this.name, request), { stdio: "inherit", ...this.childEnv() });
2498
+ assertSpawned(r, this.what("sandbox exec"));
2499
+ return r.status ?? 1;
2500
+ }
2501
+ describe() {
2502
+ return `openshell sandbox ${this.name} (entry ${this.entryRoot})`;
2503
+ }
2504
+ runInherited(argv, verb) {
2505
+ const r = this.run(OPENSHELL_BIN, argv, { stdio: "inherit", ...this.childEnv() });
2506
+ assertSpawned(r, this.what(verb));
2507
+ const code = r.status ?? 1;
2508
+ if (code !== 0) throw new ExecutionExitError(this.what(verb), code);
2509
+ }
2510
+ childEnv() {
2511
+ return this.env ? { env: this.env } : {};
2512
+ }
2513
+ what(verb) {
2514
+ return `${OPENSHELL_BIN} ${verb} ${this.name}`;
2515
+ }
2516
+ };
2517
+ function asLoginShell(argv) {
2518
+ return argv.length === 1 && LOGIN_SHELLS.has(argv[0]) ? [argv[0], "-l"] : [...argv];
2519
+ }
2520
+ function envFlags(env) {
2521
+ return Object.entries(env ?? {}).flatMap(([k, v]) => ["--env", `${k}=${v}`]);
2522
+ }
2523
+ function gatewayConnected(json) {
2524
+ try {
2525
+ const parsed = JSON.parse(json);
2526
+ return typeof parsed === "object" && parsed !== null && parsed.status === "connected";
2527
+ } catch {
2528
+ return false;
2529
+ }
2530
+ }
2531
+ function requireSandboxUser(request) {
2532
+ if (request.user !== void 0 && request.user !== OPENSHELL_USER) {
2533
+ throw new Error(
2534
+ `the openshell runtime runs every command as "${OPENSHELL_USER}" and cannot run as "${request.user}"`
2535
+ );
2536
+ }
2537
+ }
2538
+
2227
2539
  // src/lib/execution/status.ts
2228
2540
  var RUNTIME_ABSENT_STATUS = "no container runtime";
2229
2541
  async function resolveTargetStatus(target) {
@@ -2247,19 +2559,23 @@ function resolveExecutionTarget(opts) {
2247
2559
  ...opts.env ? { env: opts.env } : {}
2248
2560
  });
2249
2561
  }
2562
+ if (entryRuntime(opts.projectRoot) === "openshell") {
2563
+ const config = readAgroConfig(agroConfigPath(opts.projectRoot));
2564
+ return new OpenShellExecutionTarget({
2565
+ name: opts.container ?? nonEmpty(config.name) ?? basename4(resolve7(opts.projectRoot)),
2566
+ entryRoot: opts.projectRoot,
2567
+ image: openshellImage(config),
2568
+ ...opts.run ? { run: opts.run } : {},
2569
+ ...opts.env ? { env: opts.env } : {}
2570
+ });
2571
+ }
2250
2572
  return new DockerComposeExecutionTarget(opts);
2251
2573
  }
2252
-
2253
- // src/lib/version.ts
2254
- var AGRO_VERSION = true ? "0.19.0" : "0.0.0-dev";
2255
- var OFFICIAL_IMAGE = "ghcr.io/mifunedev/agro";
2256
- var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z][a-z0-9]*\.[0-9]+)?$/;
2257
- function parseReleaseVersion(value) {
2258
- const bare = value.startsWith("v") ? value.slice(1) : value;
2259
- return RELEASE_VERSION.test(bare) ? bare : void 0;
2574
+ function openshellImage(config) {
2575
+ return nonEmpty(config.image?.ref) ?? officialImageRef(AGRO_VERSION);
2260
2576
  }
2261
- function officialImageRef(version) {
2262
- return `${OFFICIAL_IMAGE}:${RELEASE_VERSION.test(version) ? version : "latest"}`;
2577
+ function nonEmpty(value) {
2578
+ return value === void 0 || value === "" ? void 0 : value;
2263
2579
  }
2264
2580
 
2265
2581
  // src/lib/config-render.ts
@@ -2325,13 +2641,103 @@ function renderComposeEnv(config) {
2325
2641
  `;
2326
2642
  }
2327
2643
 
2644
+ // src/lib/runtimes/verbs.ts
2645
+ import { basename as basename5, resolve as resolve8 } from "node:path";
2646
+ var viaCompose = Object.freeze({ action: "route", via: "compose" });
2647
+ var viaTarget = Object.freeze({ action: "route", via: "target" });
2648
+ function refuse(hint) {
2649
+ return Object.freeze({ action: "refuse", hint });
2650
+ }
2651
+ var LIFECYCLE_COMMANDS = Object.freeze({
2652
+ shell: "shell",
2653
+ stop: "stop",
2654
+ restart: "restart",
2655
+ logs: "logs",
2656
+ ps: "ps",
2657
+ destroy: "destroy",
2658
+ config: "compose config",
2659
+ upgrade: "sandbox upgrade"
2660
+ });
2661
+ var DOCKER_VERBS = Object.freeze({
2662
+ shell: viaTarget,
2663
+ stop: viaCompose,
2664
+ restart: viaCompose,
2665
+ logs: viaCompose,
2666
+ ps: viaCompose,
2667
+ destroy: viaCompose,
2668
+ config: viaCompose,
2669
+ upgrade: viaTarget
2670
+ });
2671
+ var OPENSHELL_VERBS = Object.freeze({
2672
+ shell: viaTarget,
2673
+ stop: refuse(OPENSHELL_VERB_HINTS.stop),
2674
+ restart: refuse(OPENSHELL_VERB_HINTS.restart),
2675
+ logs: refuse(OPENSHELL_VERB_HINTS.logs),
2676
+ ps: refuse(OPENSHELL_VERB_HINTS.ps),
2677
+ destroy: viaTarget,
2678
+ config: refuse(OPENSHELL_VERB_HINTS.config),
2679
+ upgrade: refuse(
2680
+ (name) => `${activeBin()} destroy ${name}, then ${activeBin()} sandbox install openshell --name ${name} --image=<ref>`
2681
+ )
2682
+ });
2683
+ var RUNTIME_VERBS = Object.freeze({
2684
+ docker: DOCKER_VERBS,
2685
+ openshell: OPENSHELL_VERBS
2686
+ });
2687
+ function configuredEntryName(root) {
2688
+ try {
2689
+ const name = readAgroConfig(agroConfigPath(root)).name;
2690
+ return name === "" ? void 0 : name;
2691
+ } catch {
2692
+ return void 0;
2693
+ }
2694
+ }
2695
+ function entryDeclaredName(root) {
2696
+ return configuredEntryName(root) ?? basename5(resolve8(root));
2697
+ }
2698
+ var RUNTIME_NAMING = Object.freeze({
2699
+ docker: Object.freeze({
2700
+ entryName: () => void 0,
2701
+ shellFailure: (bin, name) => `container \`${name}\` not running? start it with \`${bin} sandbox install docker\``
2702
+ }),
2703
+ openshell: Object.freeze({
2704
+ entryName: entryDeclaredName,
2705
+ shellFailure: (bin, name) => `sandbox \`${name}\` did not accept the shell; check its status with \`${bin} sandbox list\``
2706
+ })
2707
+ });
2708
+ function entrySandboxName(root) {
2709
+ return RUNTIME_NAMING[entryRuntime(root)].entryName(root);
2710
+ }
2711
+ function shellFailureHint(runtime, bin, name) {
2712
+ return RUNTIME_NAMING[runtime].shellFailure(bin, name);
2713
+ }
2714
+ var RuntimeUnsupportedError = class extends Error {
2715
+ runtime;
2716
+ verb;
2717
+ hint;
2718
+ constructor(runtime, verb, hint) {
2719
+ const command = LIFECYCLE_COMMANDS[verb];
2720
+ super(`${activeBin()} ${command}: the ${runtime} runtime does not support ${command}; run: ${hint}`);
2721
+ this.name = "RuntimeUnsupportedError";
2722
+ this.runtime = runtime;
2723
+ this.verb = verb;
2724
+ this.hint = hint;
2725
+ }
2726
+ };
2727
+ function routeVerb(root, verb, name) {
2728
+ const runtime = entryRuntime(root);
2729
+ const policy = RUNTIME_VERBS[runtime][verb];
2730
+ if (policy.action === "refuse") throw new RuntimeUnsupportedError(runtime, verb, policy.hint(name));
2731
+ return { runtime, via: policy.via };
2732
+ }
2733
+
2328
2734
  // src/commands/lifecycle.ts
2329
2735
  var COMPOSE_ENV_DIR_PREFIX = "oh-compose-env-";
2330
2736
  function composeEnvDir() {
2331
- return mkdtempSync(join5(tmpdir(), COMPOSE_ENV_DIR_PREFIX));
2737
+ return mkdtempSync(join6(tmpdir(), COMPOSE_ENV_DIR_PREFIX));
2332
2738
  }
2333
2739
  function writeComposeEnv(root, dir) {
2334
- const file = join5(dir, "compose.env");
2740
+ const file = join6(dir, "compose.env");
2335
2741
  writeFileSync5(file, renderComposeEnv(readAgroConfig(agroConfigPath(root))), {
2336
2742
  mode: 384,
2337
2743
  encoding: "utf8"
@@ -2389,7 +2795,7 @@ async function maybePromptDockerSocket(root, io, bin) {
2389
2795
  if (dockerSocketConfigured(root)) return;
2390
2796
  const interactive = process.stdin.isTTY === true || io.ask !== void 0;
2391
2797
  if (!interactive) return;
2392
- const envDir = join5(root, ".devcontainer");
2798
+ const envDir = join6(root, ".devcontainer");
2393
2799
  if (!existsSync10(envDir)) return;
2394
2800
  const askFn = io.ask ?? ask;
2395
2801
  const answer = (await askFn(
@@ -2470,25 +2876,27 @@ async function runSandbox(opts, io) {
2470
2876
  function configuredContainerName(root) {
2471
2877
  return fromProcessEnv("SANDBOX_NAME") ?? configuredString(root, "name");
2472
2878
  }
2879
+ function sandboxName(root) {
2880
+ return entrySandboxName(root) ?? configuredContainerName(root) ?? DEFAULT_CONTAINER_NAME;
2881
+ }
2473
2882
  function runShell(opts, io) {
2474
2883
  const run = opts.run ?? spawnRunner;
2475
2884
  const root = sandboxRoot(opts);
2476
- const name = configuredContainerName(root) ?? DEFAULT_CONTAINER_NAME;
2885
+ const name = sandboxName(root);
2886
+ const { runtime } = routeVerb(root, "shell", name);
2477
2887
  const target = resolveExecutionTarget({ projectRoot: root, container: name, run });
2478
2888
  let code;
2479
2889
  try {
2480
2890
  code = target.attach({ argv: ["zsh"], user: "sandbox" });
2481
2891
  } catch (err) {
2482
2892
  if (err instanceof ExecutionSpawnError && err.code === "ENOENT") {
2483
- throw new Error(`docker is required for \`${opts.bin} shell\` but was not found on PATH`);
2893
+ throw new Error(`${runtime} is required for \`${opts.bin} shell\` but was not found on PATH`);
2484
2894
  }
2485
2895
  throw err;
2486
2896
  }
2487
2897
  if (code !== 0) {
2488
- io.stderr(
2489
- `container \`${name}\` not running? start it with \`${opts.bin} sandbox install docker\`
2490
- `
2491
- );
2898
+ io.stderr(`${shellFailureHint(runtime, opts.bin, name)}
2899
+ `);
2492
2900
  }
2493
2901
  return code;
2494
2902
  }
@@ -2505,6 +2913,7 @@ function composeVerbs() {
2505
2913
  function runComposeVerb(verb, opts, extra = []) {
2506
2914
  const run = opts.run ?? spawnRunner;
2507
2915
  const root = sandboxRoot(opts);
2916
+ routeVerb(root, verb, sandboxName(root));
2508
2917
  const script = requireLifecycleScript(root, "docker-compose.sh");
2509
2918
  return withComposeEnvFile(root, (extraArgs) => {
2510
2919
  const r = run("bash", [script, ...extraArgs, ...COMPOSE_VERBS[verb], ...extra], {
@@ -2517,6 +2926,7 @@ function runComposeVerb(verb, opts, extra = []) {
2517
2926
  function runComposeConfig(opts, extra = []) {
2518
2927
  const run = opts.run ?? spawnRunner;
2519
2928
  const root = sandboxRoot(opts);
2929
+ routeVerb(root, "config", sandboxName(root));
2520
2930
  const script = requireLifecycleScript(root, "docker-compose.sh");
2521
2931
  return withComposeEnvFile(root, (extraArgs) => {
2522
2932
  const r = run("bash", [script, ...extraArgs, "config", ...extra], { stdio: "inherit" });
@@ -2527,7 +2937,7 @@ function runComposeConfig(opts, extra = []) {
2527
2937
  function namedVolumes(root) {
2528
2938
  let text;
2529
2939
  try {
2530
- text = readFileSync7(join5(root, ".devcontainer", "docker-compose.yml"), "utf8");
2940
+ text = readFileSync7(join6(root, ".devcontainer", "docker-compose.yml"), "utf8");
2531
2941
  } catch {
2532
2942
  return [];
2533
2943
  }
@@ -2543,11 +2953,23 @@ function namedVolumes(root) {
2543
2953
  return names;
2544
2954
  }
2545
2955
  function destroyConfirmationPhrase(root) {
2546
- return configuredContainerName(root) ?? DEFAULT_CONTAINER_NAME;
2956
+ return sandboxName(root);
2957
+ }
2958
+ async function destroyThroughTarget(root, name, run) {
2959
+ const target = resolveExecutionTarget({ projectRoot: root, container: name, run });
2960
+ if (target.destroy === void 0) throw new Error(`${target.describe()} cannot destroy its sandbox`);
2961
+ try {
2962
+ await target.destroy();
2963
+ return 0;
2964
+ } catch (err) {
2965
+ if (err instanceof ExecutionExitError) return err.exitCode;
2966
+ throw err;
2967
+ }
2547
2968
  }
2548
2969
  async function runDestroy(opts, io) {
2549
2970
  const root = sandboxRoot(opts);
2550
2971
  const name = destroyConfirmationPhrase(root);
2972
+ const { via } = routeVerb(root, "destroy", name);
2551
2973
  if (opts.yes !== true) {
2552
2974
  const interactive = process.stdin.isTTY === true || io.ask !== void 0;
2553
2975
  if (!interactive) {
@@ -2562,7 +2984,14 @@ async function runDestroy(opts, io) {
2562
2984
  ${bold(`${opts.bin} destroy \u2014 ${name}`)}
2563
2985
 
2564
2986
  `);
2565
- if (homePath !== void 0 && homePath !== "") {
2987
+ if (via === "target") {
2988
+ io.stdout(
2989
+ `This deletes the sandbox \`${name}\` and everything inside it \u2014 every agent CLI
2990
+ login, the gh CLI token, and the SSH keys. Sign-in starts over.
2991
+
2992
+ `
2993
+ );
2994
+ } else if (homePath !== void 0 && homePath !== "") {
2566
2995
  io.stdout("`docker compose down -v` removes the containers.\n");
2567
2996
  io.stdout(
2568
2997
  `This sandbox keeps its home on a host bind at ${homePath}, which \`down -v\`
@@ -2593,7 +3022,7 @@ stay there. Delete that directory yourself if you also want them gone.
2593
3022
  return 1;
2594
3023
  }
2595
3024
  }
2596
- const code = runComposeVerb("destroy", { ...opts, name: basename4(root) });
3025
+ const code = via === "target" ? await destroyThroughTarget(root, name, opts.run ?? spawnRunner) : runComposeVerb("destroy", { ...opts, name: basename6(root) });
2597
3026
  if (code === 0 && root.startsWith(registryRoot() + sep3)) {
2598
3027
  rmSync(root, { recursive: true, force: true });
2599
3028
  io.stdout(`removed the sandbox entry ${root}
@@ -2883,13 +3312,13 @@ function sameRemote(url, owner, name) {
2883
3312
  // src/commands/secret.ts
2884
3313
  import { spawnSync as spawnSync3 } from "node:child_process";
2885
3314
  import { existsSync as existsSync12 } from "node:fs";
2886
- import { join as join7 } from "node:path";
3315
+ import { join as join8 } from "node:path";
2887
3316
 
2888
3317
  // src/lib/gitignore.ts
2889
3318
  import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
2890
- import { dirname as dirname5, join as join6 } from "node:path";
3319
+ import { dirname as dirname5, join as join7 } from "node:path";
2891
3320
  function appendGitignoreLines(root, lines) {
2892
- const target = join6(root, ".gitignore");
3321
+ const target = join7(root, ".gitignore");
2893
3322
  const existing = existsSync11(target) ? readFileSync8(target, "utf8") : "";
2894
3323
  const present = new Set(existing.split("\n").map((line) => line.trimEnd()));
2895
3324
  const added = [];
@@ -2921,7 +3350,7 @@ function isGitIgnored(root, path4) {
2921
3350
  return result.status === 0;
2922
3351
  }
2923
3352
  function ignoreSecretsFile(root) {
2924
- if (!existsSync12(join7(root, ".git"))) return;
3353
+ if (!existsSync12(join8(root, ".git"))) return;
2925
3354
  if (isGitIgnored(root, ".env")) return;
2926
3355
  appendGitignoreLines(root, [".env"]);
2927
3356
  }
@@ -2980,10 +3409,10 @@ async function runSecretList(opts, io) {
2980
3409
  // src/commands/langfuse.ts
2981
3410
  import { copyFileSync as copyFileSync2, existsSync as existsSync14, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync10, rmSync as rmSync2, unlinkSync as unlinkSync4 } from "node:fs";
2982
3411
  import { homedir as homedir2, tmpdir as tmpdir2 } from "node:os";
2983
- import { dirname as dirname7, join as join10, relative } from "node:path";
3412
+ import { dirname as dirname7, join as join11, relative } from "node:path";
2984
3413
 
2985
3414
  // src/lib/tracing/harness-writers.ts
2986
- import { join as join8 } from "node:path";
3415
+ import { join as join9 } from "node:path";
2987
3416
 
2988
3417
  // src/lib/tracing/writer.ts
2989
3418
  import { chmodSync, existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "node:fs";
@@ -3028,13 +3457,13 @@ function isPlainObject(value) {
3028
3457
  // src/lib/tracing/harness-writers.ts
3029
3458
  var CODEX_TRACING_TAGS = ["codex"];
3030
3459
  function claudeCodeSettingsPath(home) {
3031
- return join8(home, ".claude", "settings.json");
3460
+ return join9(home, ".claude", "settings.json");
3032
3461
  }
3033
3462
  function piTracingConfigPath(home) {
3034
- return join8(home, ".pi", "agent", "langfuse.json");
3463
+ return join9(home, ".pi", "agent", "langfuse.json");
3035
3464
  }
3036
3465
  function codexTracingConfigPath(home) {
3037
- return join8(home, ".codex", "langfuse.json");
3466
+ return join9(home, ".codex", "langfuse.json");
3038
3467
  }
3039
3468
  var claudeCodeTracingWriter = (home, settings) => {
3040
3469
  const path4 = claudeCodeSettingsPath(home);
@@ -3278,7 +3707,7 @@ function harnessBinPath(prefix) {
3278
3707
  }
3279
3708
 
3280
3709
  // src/lib/tracing/providers/langfuse.ts
3281
- import { join as join9 } from "node:path";
3710
+ import { join as join10 } from "node:path";
3282
3711
  var LANGFUSE_FRAGMENT_KEYS = [
3283
3712
  "LANGFUSE_PUBLIC_KEY",
3284
3713
  "LANGFUSE_SECRET_KEY",
@@ -3286,7 +3715,7 @@ var LANGFUSE_FRAGMENT_KEYS = [
3286
3715
  "LANGFUSE_TRACING_ENVIRONMENT"
3287
3716
  ];
3288
3717
  function langfuseFragmentPath(home) {
3289
- return join9(home, ".config", "agro", "langfuse.env");
3718
+ return join10(home, ".config", "agro", "langfuse.env");
3290
3719
  }
3291
3720
  function loadLangfuseCredentials(root) {
3292
3721
  const publicKey = readSecret(root, "LANGFUSE_PUBLIC_KEY");
@@ -3450,7 +3879,7 @@ function displayPath(home, path4) {
3450
3879
  return rel.startsWith("..") ? path4 : `~/${rel}`;
3451
3880
  }
3452
3881
  function withTempHome(fn) {
3453
- const dir = mkdtempSync2(join10(tmpdir2(), "agro-langfuse-"));
3882
+ const dir = mkdtempSync2(join11(tmpdir2(), "agro-langfuse-"));
3454
3883
  try {
3455
3884
  return fn(dir);
3456
3885
  } finally {
@@ -3459,10 +3888,10 @@ function withTempHome(fn) {
3459
3888
  }
3460
3889
  function probeGeneratedFile(home, render) {
3461
3890
  const rel = withTempHome((scratch) => relative(scratch, render(scratch).path));
3462
- const path4 = join10(home, rel);
3891
+ const path4 = join11(home, rel);
3463
3892
  if (!existsSync14(path4)) return { path: path4, state: "missing" };
3464
3893
  const outcome = withTempHome((scratch) => {
3465
- const copy = join10(scratch, rel);
3894
+ const copy = join11(scratch, rel);
3466
3895
  mkdirSync6(dirname7(copy), { recursive: true });
3467
3896
  copyFileSync2(path4, copy);
3468
3897
  return render(scratch).outcome;
@@ -3525,7 +3954,7 @@ async function runLangfuseApply(opts, io) {
3525
3954
  return renderAll(ctx, resolved, io, opts.bin, "apply");
3526
3955
  }
3527
3956
  function zshenvWarning(home) {
3528
- const zshenv = join10(home, ".zshenv");
3957
+ const zshenv = join11(home, ".zshenv");
3529
3958
  if (!existsSync14(zshenv)) return void 0;
3530
3959
  if (readFileSync10(zshenv, "utf8").includes("langfuse.env")) return void 0;
3531
3960
  return `~/.zshenv exists but does not source ${LANGFUSE_FRAGMENT_LABEL} \u2014 non-interactive shells will carry no Langfuse credentials; add the source block from .agro/install/.zshenv
@@ -3806,45 +4235,18 @@ async function runLangfuseSetup(opts, io) {
3806
4235
  // src/commands/sandbox.ts
3807
4236
  import { existsSync as existsSync16, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, rmSync as rmSync3, statSync as statSync4 } from "node:fs";
3808
4237
  import { tmpdir as tmpdir3 } from "node:os";
3809
- import { join as join12, resolve as resolve7 } from "node:path";
3810
-
3811
- // src/lib/runtimes/catalog.ts
3812
- var RUNTIME_CATALOG = Object.freeze([
3813
- Object.freeze({
3814
- id: "docker",
3815
- title: "Docker container",
3816
- tier: "container",
3817
- state: "active",
3818
- provisionable: true,
3819
- docsPath: "docs/runtimes/overview.md"
3820
- }),
3821
- Object.freeze({
3822
- id: "microsandbox",
3823
- title: "MicroSandbox",
3824
- tier: "microvm",
3825
- state: "planned",
3826
- provisionable: false,
3827
- notProvisionableReason: (bin) => `microsandbox is not a provisionable runtime yet; see https://github.com/mifunedev/agro/issues/592. Inside a sandbox run \`${bin} tool install microsandbox\`.`,
3828
- docsPath: "docs/runtimes/microsandbox.md"
3829
- })
3830
- ]);
3831
- function findRuntime(id) {
3832
- return RUNTIME_CATALOG.find((r) => r.id === id);
3833
- }
3834
- function runtimeIds() {
3835
- return RUNTIME_CATALOG.map((r) => r.id);
3836
- }
4238
+ import { join as join13, resolve as resolve9 } from "node:path";
3837
4239
 
3838
4240
  // src/lib/star-prompt.ts
3839
4241
  import { existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "node:fs";
3840
- import { join as join11 } from "node:path";
4242
+ import { join as join12 } from "node:path";
3841
4243
  var STAR_PROMPT_LINE = "\u2B50 If AGRO helps, star https://github.com/mifunedev/agro";
3842
4244
  var MARKER = "star-prompt-shown";
3843
4245
  function maybePrintStarPrompt(io, env = process.env, isTTY = process.stdout.isTTY === true, stateHome = resolveUserStateHome(env)) {
3844
4246
  if (env.AGRO_NO_STAR_PROMPT === "1") return;
3845
4247
  if (env.CI !== void 0 && env.CI !== "") return;
3846
4248
  if (!isTTY) return;
3847
- const marker = join11(stateHome, MARKER);
4249
+ const marker = join12(stateHome, MARKER);
3848
4250
  if (existsSync15(marker)) return;
3849
4251
  io.stdout(`${STAR_PROMPT_LINE}
3850
4252
  `);
@@ -3859,7 +4261,7 @@ function maybePrintStarPrompt(io, env = process.env, isTTY = process.stdout.isTT
3859
4261
  // src/commands/sandbox-list.ts
3860
4262
  async function entryStatus(root, name, run) {
3861
4263
  try {
3862
- const target = resolveExecutionTarget({ projectRoot: root, container: name, run });
4264
+ const target = resolveExecutionTarget({ projectRoot: root, container: entrySandboxName(root) ?? name, run });
3863
4265
  return await target.status();
3864
4266
  } catch {
3865
4267
  return "unknown";
@@ -3921,10 +4323,10 @@ function gitIdentity(run, key) {
3921
4323
  return (result.stdout ?? "").trim();
3922
4324
  }
3923
4325
  function isBuildCapable(repo) {
3924
- return repo !== void 0 && existsSync16(join12(repo, ".devcontainer", "Dockerfile"));
4326
+ return repo !== void 0 && existsSync16(join13(repo, ".devcontainer", "Dockerfile"));
3925
4327
  }
3926
4328
  function prepareHomeMount(value) {
3927
- const path4 = resolve7(value);
4329
+ const path4 = resolve9(value);
3928
4330
  if (existsSync16(path4)) {
3929
4331
  if (!statSync4(path4).isDirectory()) {
3930
4332
  throw new Error(`--home-mount path exists and is not a directory: ${path4}`);
@@ -3968,18 +4370,18 @@ function mergeSettings(...sources) {
3968
4370
  for (const source of present) overlaySettings(merged, source);
3969
4371
  return merged;
3970
4372
  }
3971
- function nonEmpty(value) {
4373
+ function nonEmpty2(value) {
3972
4374
  return value === void 0 || value === "" ? void 0 : value;
3973
4375
  }
3974
- function seedConfig(name, checkout, seed, run) {
4376
+ function seedConfig(name, checkout, seed, run, runtime = "docker") {
3975
4377
  const config = overlaySettings(defaultAgroConfig(name), seed);
3976
4378
  config.name = name;
3977
- config.runtime = "docker";
4379
+ config.runtime = runtime;
3978
4380
  if (checkout !== void 0) config.checkout = checkout;
3979
- config.timezone = nonEmpty(seed?.timezone) ?? hostTimezone();
4381
+ config.timezone = nonEmpty2(seed?.timezone) ?? hostTimezone();
3980
4382
  config.git = {
3981
- userName: nonEmpty(seed?.git?.userName) ?? gitIdentity(run, "user.name"),
3982
- userEmail: nonEmpty(seed?.git?.userEmail) ?? gitIdentity(run, "user.email")
4383
+ userName: nonEmpty2(seed?.git?.userName) ?? gitIdentity(run, "user.name"),
4384
+ userEmail: nonEmpty2(seed?.git?.userEmail) ?? gitIdentity(run, "user.email")
3983
4385
  };
3984
4386
  config.image = {
3985
4387
  ...config.image,
@@ -4020,7 +4422,7 @@ async function runWizard(config, io) {
4020
4422
  "Host path for /home/sandbox (blank keeps the Docker-managed volume)",
4021
4423
  config.storage?.homePath ?? ""
4022
4424
  );
4023
- if (homePath !== "") config.storage = { ...config.storage, homePath: resolve7(homePath) };
4425
+ if (homePath !== "") config.storage = { ...config.storage, homePath: resolve9(homePath) };
4024
4426
  }
4025
4427
  async function runSandboxInstall(opts, io) {
4026
4428
  const run = opts.run ?? spawnRunner;
@@ -4037,7 +4439,8 @@ async function runSandboxInstall(opts, io) {
4037
4439
  `);
4038
4440
  return 1;
4039
4441
  }
4040
- const checkout = opts.checkout === void 0 ? void 0 : resolve7(opts.checkout);
4442
+ if (runtime.id === "openshell") return await installOpenShell(opts, io, run);
4443
+ const checkout = opts.checkout === void 0 ? void 0 : resolve9(opts.checkout);
4041
4444
  if (checkout !== void 0 && !existsSync16(checkout)) {
4042
4445
  io.stderr(`${opts.bin} sandbox install: --checkout directory does not exist: ${checkout}
4043
4446
  `);
@@ -4055,9 +4458,9 @@ async function runSandboxInstall(opts, io) {
4055
4458
  const config = seedConfig(name, checkout, mergeSettings(readEntryConfig(name), repoSeed), run);
4056
4459
  const buildRequested = config.image?.mode === "build" && opts.noBuild !== true && opts.image !== true && opts.imageRef === void 0;
4057
4460
  if (buildRequested && !isBuildCapable(configCheckout(config))) {
4058
- const target = configCheckout(config) ?? resolve7(opts.checkout ?? ".");
4461
+ const target = configCheckout(config) ?? resolve9(opts.checkout ?? ".");
4059
4462
  io.stderr(
4060
- `${opts.bin} sandbox install: image.mode is "build" but ${join12(target, ".devcontainer", "Dockerfile")} does not exist \u2014 point --checkout <dir> at a harness checkout that has .devcontainer/Dockerfile, or set image.mode to "image" to run the prebuilt image
4463
+ `${opts.bin} sandbox install: image.mode is "build" but ${join13(target, ".devcontainer", "Dockerfile")} does not exist \u2014 point --checkout <dir> at a harness checkout that has .devcontainer/Dockerfile, or set image.mode to "image" to run the prebuilt image
4061
4464
  `
4062
4465
  );
4063
4466
  return 1;
@@ -4101,7 +4504,7 @@ async function runSandboxInstall(opts, io) {
4101
4504
  ...useNoBuild ? { noBuild: true } : {}
4102
4505
  };
4103
4506
  if (opts.printArgv === true) {
4104
- const preview = mkdtempSync3(join12(tmpdir3(), "oh-sandbox-preview-"));
4507
+ const preview = mkdtempSync3(join13(tmpdir3(), "oh-sandbox-preview-"));
4105
4508
  try {
4106
4509
  writeAgroConfig(preview, config);
4107
4510
  materialize(preview, { ...configCheckout(config) !== void 0 ? { checkout: configCheckout(config) } : {} });
@@ -4122,16 +4525,77 @@ async function runSandboxInstall(opts, io) {
4122
4525
  }
4123
4526
  return code;
4124
4527
  }
4528
+ async function installOpenShell(opts, io, run) {
4529
+ const fail2 = (message) => {
4530
+ io.stderr(`${opts.bin} sandbox install: ${message}
4531
+ `);
4532
+ return 1;
4533
+ };
4534
+ const unsupported = (what) => fail2(`${what} is not supported on the openshell runtime`);
4535
+ if (opts.checkout !== void 0) return unsupported("--checkout");
4536
+ if (opts.homeMount !== void 0) return unsupported("--home-mount");
4537
+ const name = opts.name ?? nextDefaultName(run);
4538
+ try {
4539
+ assertSandboxName(name);
4540
+ } catch (error) {
4541
+ return fail2(error instanceof Error ? error.message : String(error));
4542
+ }
4543
+ const config = seedConfig(name, void 0, readEntryConfig(name), run, "openshell");
4544
+ if (configCheckout(config) !== void 0) return unsupported("checkout");
4545
+ if (nonEmpty2(config.storage?.homePath) !== void 0) return unsupported("storage.homePath");
4546
+ const imageChosen = opts.image === true || opts.imageRef !== void 0;
4547
+ if (config.image?.mode === "build" && !imageChosen) {
4548
+ return unsupported('image.mode "build"');
4549
+ }
4550
+ config.image = {
4551
+ ...config.image,
4552
+ mode: "image",
4553
+ ...opts.imageRef !== void 0 ? { ref: opts.imageRef } : {}
4554
+ };
4555
+ const root = entryRoot(name);
4556
+ const image = openshellImage(config);
4557
+ if (opts.printArgv === true) {
4558
+ io.stdout(`${[OPENSHELL_BIN, ...createArgv({ name, image, entryRoot: root })].join(" ")}
4559
+ `);
4560
+ return 0;
4561
+ }
4562
+ if (runningInsideSandbox()) return fail2(new HostOnlyError(`\`${opts.bin} sandbox\``).message);
4563
+ try {
4564
+ const preflight = openshellPreflight(run);
4565
+ if (!preflight.ok) return fail2(preflight.hint);
4566
+ } catch (error) {
4567
+ return fail2(error instanceof Error ? error.message : String(error));
4568
+ }
4569
+ const created = !existsSync16(root);
4570
+ mkdirSync8(root, { recursive: true });
4571
+ writeAgroConfig(root, config);
4572
+ materialize(root);
4573
+ try {
4574
+ await new OpenShellExecutionTarget({ name, entryRoot: root, image, run }).provision();
4575
+ } catch (error) {
4576
+ if (created) rmSync3(root, { recursive: true, force: true });
4577
+ io.stderr(`${opts.bin} sandbox install: ${error instanceof Error ? error.message : String(error)}
4578
+ `);
4579
+ if (!(error instanceof ExecutionExitError)) return 1;
4580
+ io.stderr(`${opts.bin} sandbox install: ${openshellCleanupHint(name)}
4581
+ `);
4582
+ return error.exitCode;
4583
+ }
4584
+ io.stdout(`next: ${opts.bin} shell ${name}
4585
+ `);
4586
+ maybePrintStarPrompt(io);
4587
+ return 0;
4588
+ }
4125
4589
 
4126
4590
  // src/services/sandbox-upgrade.ts
4127
4591
  import { closeSync as closeSync2, existsSync as existsSync17, openSync as openSync2, readFileSync as readFileSync11, unlinkSync as unlinkSync5, writeSync } from "node:fs";
4128
- import { join as join13 } from "node:path";
4592
+ import { join as join14 } from "node:path";
4129
4593
  function errorMessage(error) {
4130
4594
  return error instanceof Error ? error.message : String(error);
4131
4595
  }
4132
4596
  function conflictingDotenvImage(root, imageRef) {
4133
- const rootEnv = join13(root, ".env");
4134
- const fallbackEnv = join13(root, ".devcontainer", ".env");
4597
+ const rootEnv = join14(root, ".env");
4598
+ const fallbackEnv = join14(root, ".devcontainer", ".env");
4135
4599
  const file = existsSync17(rootEnv) ? rootEnv : fallbackEnv;
4136
4600
  if (!existsSync17(file)) return void 0;
4137
4601
  for (const line of readFileSync11(file, "utf8").split(/\r?\n/)) {
@@ -4169,7 +4633,8 @@ async function runSandboxUpgrade(opts, io) {
4169
4633
  `);
4170
4634
  return 1;
4171
4635
  }
4172
- const lock = join13(root, ".sandbox-upgrade.lock");
4636
+ routeVerb(root, "upgrade", entrySandboxName(root) ?? opts.name);
4637
+ const lock = join14(root, ".sandbox-upgrade.lock");
4173
4638
  let fd;
4174
4639
  try {
4175
4640
  fd = openSync2(lock, "wx", 384);
@@ -4284,6 +4749,10 @@ identity, SSH (with its host port), the host Docker socket, and the host path
4284
4749
  for /home/sandbox. With --checkout it seeds those answers from that
4285
4750
  checkout's ${stateNames(bin).configFile}.
4286
4751
 
4752
+ \`install openshell\` never prompts. The command needs the host \`openshell\` CLI
4753
+ and a connected gateway, and runs the prebuilt image. The command refuses
4754
+ --checkout, --home-mount, and image.mode "build".
4755
+
4287
4756
  Flags:
4288
4757
  --name <name> Registry entry name (default: the lowest free ${DEFAULT_NAME_PREFIX}<n>)
4289
4758
  --checkout <dir> Bind this checkout into the sandbox and seed the defaults
@@ -4306,7 +4775,7 @@ Flags:
4306
4775
  ${stateNames(bin).configFile} image.ref > ${officialImageRef(AGRO_VERSION)}
4307
4776
  --image=<ref> Run a custom image (implies --image); conflicts with --version
4308
4777
  --no-build Suppress the local build and reuse an existing image
4309
- --print-argv Print the docker compose argv that would run, then exit
4778
+ --print-argv Print the docker compose or openshell argv that would run, then exit
4310
4779
  without writing an entry
4311
4780
  --json Machine-readable output (list)
4312
4781
 
@@ -4499,7 +4968,7 @@ async function runSandboxCommand(rest, bin) {
4499
4968
  // src/commands/harness.ts
4500
4969
  import { existsSync as existsSync20 } from "node:fs";
4501
4970
  import { homedir as homedir3 } from "node:os";
4502
- import { delimiter as delimiter2, join as join16 } from "node:path";
4971
+ import { delimiter as delimiter2, join as join17 } from "node:path";
4503
4972
 
4504
4973
  // src/lib/docs.ts
4505
4974
  var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
@@ -4513,7 +4982,7 @@ function sourceDocsUrl(docsPath) {
4513
4982
 
4514
4983
  // src/lib/host-config.ts
4515
4984
  import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, renameSync as renameSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "node:fs";
4516
- import { basename as basename5, dirname as dirname8, isAbsolute, join as join14, resolve as resolve8 } from "node:path";
4985
+ import { basename as basename7, dirname as dirname8, isAbsolute, join as join15, resolve as resolve10 } from "node:path";
4517
4986
  var HOST_CONFIG_MODE = 420;
4518
4987
  var HOST_CONFIG_FILE = "config.json";
4519
4988
  var WORKSPACES_SUBDIR = "workspaces";
@@ -4525,7 +4994,7 @@ function hostStateHome(env, home) {
4525
4994
  return resolveUserStateHome(env, home);
4526
4995
  }
4527
4996
  function hostConfigPath(env, home) {
4528
- return join14(hostStateHome(env, home), HOST_CONFIG_FILE);
4997
+ return join15(hostStateHome(env, home), HOST_CONFIG_FILE);
4529
4998
  }
4530
4999
  function assertWorkspaceName(name) {
4531
5000
  if (!SANDBOX_NAME_PATTERN.test(name)) {
@@ -4535,14 +5004,14 @@ function assertWorkspaceName(name) {
4535
5004
  }
4536
5005
  }
4537
5006
  function workspacesRoot(env, home) {
4538
- return join14(hostStateHome(env, home), WORKSPACES_SUBDIR);
5007
+ return join15(hostStateHome(env, home), WORKSPACES_SUBDIR);
4539
5008
  }
4540
5009
  function workspaceRoot(name, env, home) {
4541
5010
  assertWorkspaceName(name);
4542
- return join14(workspacesRoot(env, home), name);
5011
+ return join15(workspacesRoot(env, home), name);
4543
5012
  }
4544
5013
  function defaultHarnessRoot(env, home) {
4545
- return join14(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
5014
+ return join15(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
4546
5015
  }
4547
5016
  function validateHostConfig(value) {
4548
5017
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -4619,7 +5088,7 @@ function readHostConfig(env, home) {
4619
5088
  try {
4620
5089
  parsed = JSON.parse(raw);
4621
5090
  } catch {
4622
- throw new Error(`${basename5(path4)} is not valid JSON: ${path4}`);
5091
+ throw new Error(`${basename7(path4)} is not valid JSON: ${path4}`);
4623
5092
  }
4624
5093
  return validateHostConfig(parsed);
4625
5094
  }
@@ -4642,15 +5111,15 @@ function writeHostConfig(config, env, home) {
4642
5111
  }
4643
5112
  }
4644
5113
  function resolveHarnessRoot(explicit, env, home) {
4645
- if (typeof explicit === "string" && explicit !== "") return resolve8(explicit);
5114
+ if (typeof explicit === "string" && explicit !== "") return resolve10(explicit);
4646
5115
  const configured = readHostConfig(env, home).harnessRoot;
4647
- if (typeof configured === "string" && configured !== "") return resolve8(configured);
5116
+ if (typeof configured === "string" && configured !== "") return resolve10(configured);
4648
5117
  return defaultHarnessRoot(env, home);
4649
5118
  }
4650
5119
  function recordHarnessRoot(root, env, home) {
4651
5120
  const config = readHostConfig(env, home);
4652
5121
  const recorded = config.harnessRoot;
4653
- if (typeof recorded === "string" && recorded !== "" && resolve8(recorded) === root) return;
5122
+ if (typeof recorded === "string" && recorded !== "" && resolve10(recorded) === root) return;
4654
5123
  writeHostConfig({ ...config, harnessRoot: root }, env, home);
4655
5124
  }
4656
5125
  function fieldError2(path4, requirement) {
@@ -4659,7 +5128,7 @@ function fieldError2(path4, requirement) {
4659
5128
 
4660
5129
  // src/lib/host-workspace.ts
4661
5130
  import { existsSync as existsSync19, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync4, readdirSync as readdirSync3, renameSync as renameSync5, rmSync as rmSync4, statSync as statSync5 } from "node:fs";
4662
- import { dirname as dirname9, join as join15, resolve as resolve9 } from "node:path";
5131
+ import { dirname as dirname9, join as join16, resolve as resolve11 } from "node:path";
4663
5132
  var AGRO_REPO_URL = "https://github.com/mifunedev/agro.git";
4664
5133
  var IGNORABLE_ENTRIES = [
4665
5134
  REGISTRY_SUBDIR,
@@ -4671,15 +5140,15 @@ function isDirectory3(path4) {
4671
5140
  return statSync5(path4, { throwIfNoEntry: false })?.isDirectory() === true;
4672
5141
  }
4673
5142
  function stateHomes(home) {
4674
- return STATE_HOME_DIRS.map((name) => join15(home, name));
5143
+ return STATE_HOME_DIRS.map((name) => join16(home, name));
4675
5144
  }
4676
5145
  function stateHomeRootRefusal(bin, root, home, verb = "harness") {
4677
- const target = resolve9(root);
5146
+ const target = resolve11(root);
4678
5147
  const enclosing = stateHomes(home).find((state) => target === state);
4679
5148
  if (enclosing === void 0) return void 0;
4680
5149
  return `${bin} ${verb}: the ${verb} root ${target} is the state home ${enclosing} itself.
4681
5150
  A workspace there collides with AGRO state and blocks every command run from ${home}.
4682
- Move it out \u2014 for example \`mv ${enclosing} ${join15(home, "agro")}\` \u2014 then re-run this command.
5151
+ Move it out \u2014 for example \`mv ${enclosing} ${join16(home, "agro")}\` \u2014 then re-run this command.
4683
5152
  `;
4684
5153
  }
4685
5154
  function isIgnorableEntry(name) {
@@ -4696,24 +5165,24 @@ function cloneInto(target, run, root, ref) {
4696
5165
  assertCloned(run("git", ["clone", ...branch, AGRO_REPO_URL, target], { stdio: "inherit" }), root);
4697
5166
  }
4698
5167
  function cloneThroughStaging(root, run, ref) {
4699
- const staging = mkdtempSync4(join15(dirname9(root), "agro-clone-"));
5168
+ const staging = mkdtempSync4(join16(dirname9(root), "agro-clone-"));
4700
5169
  try {
4701
- const checkout = join15(staging, "agro");
5170
+ const checkout = join16(staging, "agro");
4702
5171
  cloneInto(checkout, run, root, ref);
4703
5172
  for (const name of readdirSync3(checkout)) {
4704
- const destination = join15(root, name);
5173
+ const destination = join16(root, name);
4705
5174
  if (existsSync19(destination)) {
4706
5175
  throw new Error(`could not move ${name} into ${root} \u2014 the path already exists`);
4707
5176
  }
4708
- renameSync5(join15(checkout, name), destination);
5177
+ renameSync5(join16(checkout, name), destination);
4709
5178
  }
4710
5179
  } finally {
4711
5180
  rmSync4(staging, { recursive: true, force: true });
4712
5181
  }
4713
5182
  }
4714
5183
  function ensureHostWorkspace(path4, run = spawnRunner, ref) {
4715
- const root = resolve9(path4);
4716
- if (existsSync19(join15(root, ".git"))) return { root, action: "reused" };
5184
+ const root = resolve11(path4);
5185
+ if (existsSync19(join16(root, ".git"))) return { root, action: "reused" };
4717
5186
  const stats = statSync5(root, { throwIfNoEntry: false });
4718
5187
  if (stats === void 0) {
4719
5188
  mkdirSync10(dirname9(root), { recursive: true });
@@ -4735,11 +5204,11 @@ function ensureHostWorkspace(path4, run = spawnRunner, ref) {
4735
5204
  function listHostWorkspaces(env, home) {
4736
5205
  const root = workspacesRoot(env, home);
4737
5206
  if (!isDirectory3(root)) return [];
4738
- return readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => SANDBOX_NAME_PATTERN.test(name)).filter((name) => existsSync19(join15(root, name, ".git"))).sort();
5207
+ return readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => SANDBOX_NAME_PATTERN.test(name)).filter((name) => existsSync19(join16(root, name, ".git"))).sort();
4739
5208
  }
4740
5209
  function resolveExistingWorkspace(bin, verb, path4, env, home) {
4741
- const root = resolve9(path4);
4742
- if (existsSync19(join15(root, ".git"))) return { ok: true, root };
5210
+ const root = resolve11(path4);
5211
+ if (existsSync19(join16(root, ".git"))) return { ok: true, root };
4743
5212
  const existing = listHostWorkspaces(env, home);
4744
5213
  const inventory = existing.length === 0 ? "No host workspace exists yet.\n" : `Workspaces that exist: ${existing.join(", ")}
4745
5214
  `;
@@ -4776,7 +5245,7 @@ function targetFor(root, cwd, run, env) {
4776
5245
  });
4777
5246
  }
4778
5247
  function hostPrefix(home) {
4779
- return join16(home, ".local");
5248
+ return join17(home, ".local");
4780
5249
  }
4781
5250
  function pathEntries(env) {
4782
5251
  return (env.PATH ?? "").split(delimiter2).filter((part) => part !== "");
@@ -4823,7 +5292,7 @@ async function sandboxMarkerExists(target, entry) {
4823
5292
  return r.exitCode === 0;
4824
5293
  }
4825
5294
  function hostBinaryUnderPrefix(entry, prefix) {
4826
- return existsSync20(join16(harnessBinPath(prefix), entry.binary));
5295
+ return existsSync20(join17(harnessBinPath(prefix), entry.binary));
4827
5296
  }
4828
5297
  function stateOf(entry, installed, location) {
4829
5298
  return {
@@ -4844,7 +5313,7 @@ function probeableHost(env, home) {
4844
5313
  return void 0;
4845
5314
  }
4846
5315
  const prefix = hostPrefix(home);
4847
- if (!existsSync20(join16(root, ".git")) && !existsSync20(prefix)) return void 0;
5316
+ if (!existsSync20(join17(root, ".git")) && !existsSync20(prefix)) return void 0;
4848
5317
  return { root, prefix };
4849
5318
  }
4850
5319
  async function collectStates(root, cwd, run, home, env, only) {
@@ -5445,7 +5914,7 @@ async function runHarnessInstall(name, opts, io) {
5445
5914
  // src/commands/workspace.ts
5446
5915
  import { existsSync as existsSync21, rmSync as rmSync5 } from "node:fs";
5447
5916
  import { homedir as homedir4 } from "node:os";
5448
- import { join as join17, resolve as resolve10 } from "node:path";
5917
+ import { join as join18, resolve as resolve12 } from "node:path";
5449
5918
  function homeOf2(opts) {
5450
5919
  return (opts.homedir ?? homedir4)();
5451
5920
  }
@@ -5454,7 +5923,7 @@ function messageOf2(error) {
5454
5923
  }
5455
5924
  function recordedRoot(env, home) {
5456
5925
  const configured = readHostConfig(env, home).harnessRoot;
5457
- return typeof configured === "string" && configured !== "" ? resolve10(configured) : void 0;
5926
+ return typeof configured === "string" && configured !== "" ? resolve12(configured) : void 0;
5458
5927
  }
5459
5928
  function renderTable2(rows, io) {
5460
5929
  const header2 = ["WORKSPACE", "DEFAULT", "PATH"];
@@ -5479,7 +5948,7 @@ async function runWorkspaceCreate(name, opts, io) {
5479
5948
  let root;
5480
5949
  const chosen = name ?? DEFAULT_WORKSPACE_NAME;
5481
5950
  try {
5482
- root = opts.path !== void 0 ? resolve10(opts.path) : workspaceRoot(chosen, env, home);
5951
+ root = opts.path !== void 0 ? resolve12(opts.path) : workspaceRoot(chosen, env, home);
5483
5952
  } catch (err) {
5484
5953
  io.stderr(`${bin} workspace: ${messageOf2(err)}
5485
5954
  `);
@@ -5493,7 +5962,7 @@ async function runWorkspaceCreate(name, opts, io) {
5493
5962
  let action;
5494
5963
  const preexisting = existsSync21(root);
5495
5964
  try {
5496
- if (!opts.json && !existsSync21(join17(root, ".git"))) {
5965
+ if (!opts.json && !existsSync21(join18(root, ".git"))) {
5497
5966
  const at = opts.ref !== void 0 ? ` at ${opts.ref}` : "";
5498
5967
  io.stdout(`cloning ${AGRO_REPO_URL}${at} into ${root}\u2026
5499
5968
  `);
@@ -5538,7 +6007,7 @@ async function runWorkspaceList(opts, io) {
5538
6007
  const recorded = recordedRoot(env, home);
5539
6008
  const registry = workspacesRoot(env, home);
5540
6009
  rows = listHostWorkspaces(env, home).map((name) => {
5541
- const root = join17(registry, name);
6010
+ const root = join18(registry, name);
5542
6011
  return { name, root, default: recorded === root };
5543
6012
  });
5544
6013
  } catch (err) {
@@ -5565,7 +6034,7 @@ async function runWorkspaceList(opts, io) {
5565
6034
  // src/commands/tool.ts
5566
6035
  import { existsSync as existsSync22 } from "node:fs";
5567
6036
  import { homedir as homedir5, userInfo as userInfo2 } from "node:os";
5568
- import { delimiter as delimiter3, join as join18 } from "node:path";
6037
+ import { delimiter as delimiter3, join as join19 } from "node:path";
5569
6038
 
5570
6039
  // src/lib/tools/catalog.ts
5571
6040
  var TOOLS_DOC = "docs/installation.md";
@@ -5996,7 +6465,7 @@ function targetFor2(root, run, env) {
5996
6465
  });
5997
6466
  }
5998
6467
  function hostPrefix2(home) {
5999
- return join18(home, ".local");
6468
+ return join19(home, ".local");
6000
6469
  }
6001
6470
  function sandboxMarkerPath2(entry) {
6002
6471
  return `${SANDBOX_HARNESS_PREFIX}/share/agro/tools/${entry.id}.installed`;
@@ -6010,7 +6479,7 @@ async function sandboxMarkerExists2(target, entry) {
6010
6479
  return r.exitCode === 0;
6011
6480
  }
6012
6481
  function hostBinaryUnderPrefix2(entry, prefix) {
6013
- return existsSync22(join18(harnessBinPath(prefix), entry.binary));
6482
+ return existsSync22(join19(harnessBinPath(prefix), entry.binary));
6014
6483
  }
6015
6484
  function pathEntries2(env) {
6016
6485
  return (env.PATH ?? "").split(delimiter3).filter((part) => part !== "");
@@ -6090,7 +6559,7 @@ function probeableHost2(env, home) {
6090
6559
  return void 0;
6091
6560
  }
6092
6561
  const prefix = hostPrefix2(home);
6093
- if (!existsSync22(join18(root, ".git")) && !existsSync22(prefix)) return void 0;
6562
+ if (!existsSync22(join19(root, ".git")) && !existsSync22(prefix)) return void 0;
6094
6563
  return { root, prefix };
6095
6564
  }
6096
6565
  async function collectRows(root, run, home, env, only) {
@@ -6656,7 +7125,7 @@ var AGRO_COMMANDS = Object.freeze({
6656
7125
  });
6657
7126
 
6658
7127
  // src/cli.ts
6659
- var DEFAULT_SOURCE_CONTROL_DIR = resolve11(
7128
+ var DEFAULT_SOURCE_CONTROL_DIR = resolve13(
6660
7129
  dirname10(fileURLToPath(import.meta.url)),
6661
7130
  "../.."
6662
7131
  );
@@ -7501,18 +7970,18 @@ function parseGatewayArgs(rest) {
7501
7970
  return { ok: true, args: { help: false, passthrough: [...rest] } };
7502
7971
  }
7503
7972
  function bundledPayloadExists(bundled, exists = existsSync23) {
7504
- return exists(join19(bundled.sourceControlDir, "manifest.json"));
7973
+ return exists(join20(bundled.sourceControlDir, "manifest.json"));
7505
7974
  }
7506
7975
  function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
7507
7976
  const exists = bundled.exists ?? existsSync23;
7508
7977
  if (args.fromDir !== void 0) {
7509
- return { kind: "local", fromDir: resolve11(args.fromDir) };
7978
+ return { kind: "local", fromDir: resolve13(args.fromDir) };
7510
7979
  }
7511
7980
  if (args.fromRemote) {
7512
7981
  return { kind: "remote", ref: args.ref };
7513
7982
  }
7514
7983
  if (bundledPayloadExists(bundled, exists)) {
7515
- return { kind: "local", fromDir: resolve11(bundled.sourceControlDir, "..") };
7984
+ return { kind: "local", fromDir: resolve13(bundled.sourceControlDir, "..") };
7516
7985
  }
7517
7986
  return {
7518
7987
  kind: "remote",
@@ -7524,7 +7993,7 @@ function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
7524
7993
  function readPayloadVersion(checkoutDir) {
7525
7994
  try {
7526
7995
  const parsed = JSON.parse(
7527
- readFileSync13(join19(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
7996
+ readFileSync13(join20(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
7528
7997
  );
7529
7998
  if (parsed && typeof parsed.version === "string") return parsed.version;
7530
7999
  } catch {
@@ -7533,7 +8002,7 @@ function readPayloadVersion(checkoutDir) {
7533
8002
  }
7534
8003
  async function runWithRemoteSource(hooks, fn) {
7535
8004
  const fetch2 = hooks.fetch ?? fetchRemoteSource;
7536
- const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join19(tmpdir4(), "oh-remote-")));
8005
+ const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join20(tmpdir4(), "oh-remote-")));
7537
8006
  const rm = hooks.rm ?? ((dir) => rmSync6(dir, { recursive: true, force: true }));
7538
8007
  const out = hooks.stdout ?? ((s) => process.stdout.write(s));
7539
8008
  const checkoutDir = mkdtemp();
@@ -7899,7 +8368,19 @@ function lifecycleIo() {
7899
8368
  stderr: (s) => process.stderr.write(s)
7900
8369
  };
7901
8370
  }
7902
- main(process.argv.slice(2)).then(
8371
+ async function runCli(argv) {
8372
+ try {
8373
+ return await main(argv);
8374
+ } catch (err) {
8375
+ if (err instanceof RuntimeUnsupportedError) {
8376
+ process.stderr.write(`${err.message}
8377
+ `);
8378
+ return 1;
8379
+ }
8380
+ throw err;
8381
+ }
8382
+ }
8383
+ runCli(process.argv.slice(2)).then(
7903
8384
  (code) => process.exit(code),
7904
8385
  (err) => {
7905
8386
  const msg = err instanceof Error ? err.message : String(err);
@@ -7941,6 +8422,7 @@ export {
7941
8422
  printVendorHelp,
7942
8423
  printWorkspaceHelp,
7943
8424
  resolveVendorSource,
8425
+ runCli,
7944
8426
  runWithRemoteSource,
7945
8427
  runtimeLines
7946
8428
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mifune/agro",
3
- "version": "0.19.0",
3
+ "version": "0.20.0-rc.1",
4
4
  "description": "AGRO CLI — create and manage sandboxes for coding agents.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/mifunedev/agro#readme",