@mifune/agro 0.18.1 → 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.
- package/README.md +1 -1
- package/dist/agro.js +754 -175
- package/package.json +2 -2
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
|
|
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 =
|
|
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((
|
|
1287
|
-
rl.question(` ${question} `, (answer) =>
|
|
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((
|
|
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
|
-
|
|
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((
|
|
1417
|
+
return await new Promise((resolve14) => {
|
|
1368
1418
|
rl.question(` ${question} `, (answer) => {
|
|
1369
1419
|
process.stdout.write("\n");
|
|
1370
|
-
|
|
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
|
|
1812
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2254
|
-
var AGRO_VERSION = true ? "0.18.1" : "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
|
|
2262
|
-
return
|
|
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(
|
|
2737
|
+
return mkdtempSync(join6(tmpdir(), COMPOSE_ENV_DIR_PREFIX));
|
|
2332
2738
|
}
|
|
2333
2739
|
function writeComposeEnv(root, dir) {
|
|
2334
|
-
const file =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 (
|
|
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:
|
|
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
|
|
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
|
|
3319
|
+
import { dirname as dirname5, join as join7 } from "node:path";
|
|
2891
3320
|
function appendGitignoreLines(root, lines) {
|
|
2892
|
-
const target =
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
3460
|
+
return join9(home, ".claude", "settings.json");
|
|
3032
3461
|
}
|
|
3033
3462
|
function piTracingConfigPath(home) {
|
|
3034
|
-
return
|
|
3463
|
+
return join9(home, ".pi", "agent", "langfuse.json");
|
|
3035
3464
|
}
|
|
3036
3465
|
function codexTracingConfigPath(home) {
|
|
3037
|
-
return
|
|
3466
|
+
return join9(home, ".codex", "langfuse.json");
|
|
3038
3467
|
}
|
|
3039
3468
|
var claudeCodeTracingWriter = (home, settings) => {
|
|
3040
3469
|
const path4 = claudeCodeSettingsPath(home);
|
|
@@ -3145,6 +3574,25 @@ var HARNESS_CATALOG = [
|
|
|
3145
3574
|
docsPath: "docs/harnesses/opencode.md",
|
|
3146
3575
|
kind: "installable"
|
|
3147
3576
|
},
|
|
3577
|
+
{
|
|
3578
|
+
id: "openclaw",
|
|
3579
|
+
title: "OpenClaw",
|
|
3580
|
+
binary: "openclaw",
|
|
3581
|
+
installArgv: [
|
|
3582
|
+
"npm",
|
|
3583
|
+
"--prefix",
|
|
3584
|
+
HARNESS_PREFIX_TOKEN,
|
|
3585
|
+
"install",
|
|
3586
|
+
"-g",
|
|
3587
|
+
"--allow-scripts=openclaw",
|
|
3588
|
+
"openclaw@2026.9.9"
|
|
3589
|
+
],
|
|
3590
|
+
installUser: "sandbox",
|
|
3591
|
+
verifyArgv: ["openclaw", "--version"],
|
|
3592
|
+
uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "openclaw"],
|
|
3593
|
+
docsPath: "docs/harnesses/openclaw.md",
|
|
3594
|
+
kind: "installable"
|
|
3595
|
+
},
|
|
3148
3596
|
{
|
|
3149
3597
|
id: "grok-build",
|
|
3150
3598
|
title: "Grok Build",
|
|
@@ -3259,7 +3707,7 @@ function harnessBinPath(prefix) {
|
|
|
3259
3707
|
}
|
|
3260
3708
|
|
|
3261
3709
|
// src/lib/tracing/providers/langfuse.ts
|
|
3262
|
-
import { join as
|
|
3710
|
+
import { join as join10 } from "node:path";
|
|
3263
3711
|
var LANGFUSE_FRAGMENT_KEYS = [
|
|
3264
3712
|
"LANGFUSE_PUBLIC_KEY",
|
|
3265
3713
|
"LANGFUSE_SECRET_KEY",
|
|
@@ -3267,7 +3715,7 @@ var LANGFUSE_FRAGMENT_KEYS = [
|
|
|
3267
3715
|
"LANGFUSE_TRACING_ENVIRONMENT"
|
|
3268
3716
|
];
|
|
3269
3717
|
function langfuseFragmentPath(home) {
|
|
3270
|
-
return
|
|
3718
|
+
return join10(home, ".config", "agro", "langfuse.env");
|
|
3271
3719
|
}
|
|
3272
3720
|
function loadLangfuseCredentials(root) {
|
|
3273
3721
|
const publicKey = readSecret(root, "LANGFUSE_PUBLIC_KEY");
|
|
@@ -3431,7 +3879,7 @@ function displayPath(home, path4) {
|
|
|
3431
3879
|
return rel.startsWith("..") ? path4 : `~/${rel}`;
|
|
3432
3880
|
}
|
|
3433
3881
|
function withTempHome(fn) {
|
|
3434
|
-
const dir = mkdtempSync2(
|
|
3882
|
+
const dir = mkdtempSync2(join11(tmpdir2(), "agro-langfuse-"));
|
|
3435
3883
|
try {
|
|
3436
3884
|
return fn(dir);
|
|
3437
3885
|
} finally {
|
|
@@ -3440,10 +3888,10 @@ function withTempHome(fn) {
|
|
|
3440
3888
|
}
|
|
3441
3889
|
function probeGeneratedFile(home, render) {
|
|
3442
3890
|
const rel = withTempHome((scratch) => relative(scratch, render(scratch).path));
|
|
3443
|
-
const path4 =
|
|
3891
|
+
const path4 = join11(home, rel);
|
|
3444
3892
|
if (!existsSync14(path4)) return { path: path4, state: "missing" };
|
|
3445
3893
|
const outcome = withTempHome((scratch) => {
|
|
3446
|
-
const copy =
|
|
3894
|
+
const copy = join11(scratch, rel);
|
|
3447
3895
|
mkdirSync6(dirname7(copy), { recursive: true });
|
|
3448
3896
|
copyFileSync2(path4, copy);
|
|
3449
3897
|
return render(scratch).outcome;
|
|
@@ -3506,7 +3954,7 @@ async function runLangfuseApply(opts, io) {
|
|
|
3506
3954
|
return renderAll(ctx, resolved, io, opts.bin, "apply");
|
|
3507
3955
|
}
|
|
3508
3956
|
function zshenvWarning(home) {
|
|
3509
|
-
const zshenv =
|
|
3957
|
+
const zshenv = join11(home, ".zshenv");
|
|
3510
3958
|
if (!existsSync14(zshenv)) return void 0;
|
|
3511
3959
|
if (readFileSync10(zshenv, "utf8").includes("langfuse.env")) return void 0;
|
|
3512
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
|
|
@@ -3787,45 +4235,18 @@ async function runLangfuseSetup(opts, io) {
|
|
|
3787
4235
|
// src/commands/sandbox.ts
|
|
3788
4236
|
import { existsSync as existsSync16, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, rmSync as rmSync3, statSync as statSync4 } from "node:fs";
|
|
3789
4237
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
3790
|
-
import { join as
|
|
3791
|
-
|
|
3792
|
-
// src/lib/runtimes/catalog.ts
|
|
3793
|
-
var RUNTIME_CATALOG = Object.freeze([
|
|
3794
|
-
Object.freeze({
|
|
3795
|
-
id: "docker",
|
|
3796
|
-
title: "Docker container",
|
|
3797
|
-
tier: "container",
|
|
3798
|
-
state: "active",
|
|
3799
|
-
provisionable: true,
|
|
3800
|
-
docsPath: "docs/runtimes/overview.md"
|
|
3801
|
-
}),
|
|
3802
|
-
Object.freeze({
|
|
3803
|
-
id: "microsandbox",
|
|
3804
|
-
title: "MicroSandbox",
|
|
3805
|
-
tier: "microvm",
|
|
3806
|
-
state: "planned",
|
|
3807
|
-
provisionable: false,
|
|
3808
|
-
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\`.`,
|
|
3809
|
-
docsPath: "docs/runtimes/microsandbox.md"
|
|
3810
|
-
})
|
|
3811
|
-
]);
|
|
3812
|
-
function findRuntime(id) {
|
|
3813
|
-
return RUNTIME_CATALOG.find((r) => r.id === id);
|
|
3814
|
-
}
|
|
3815
|
-
function runtimeIds() {
|
|
3816
|
-
return RUNTIME_CATALOG.map((r) => r.id);
|
|
3817
|
-
}
|
|
4238
|
+
import { join as join13, resolve as resolve9 } from "node:path";
|
|
3818
4239
|
|
|
3819
4240
|
// src/lib/star-prompt.ts
|
|
3820
4241
|
import { existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
3821
|
-
import { join as
|
|
4242
|
+
import { join as join12 } from "node:path";
|
|
3822
4243
|
var STAR_PROMPT_LINE = "\u2B50 If AGRO helps, star https://github.com/mifunedev/agro";
|
|
3823
4244
|
var MARKER = "star-prompt-shown";
|
|
3824
4245
|
function maybePrintStarPrompt(io, env = process.env, isTTY = process.stdout.isTTY === true, stateHome = resolveUserStateHome(env)) {
|
|
3825
4246
|
if (env.AGRO_NO_STAR_PROMPT === "1") return;
|
|
3826
4247
|
if (env.CI !== void 0 && env.CI !== "") return;
|
|
3827
4248
|
if (!isTTY) return;
|
|
3828
|
-
const marker =
|
|
4249
|
+
const marker = join12(stateHome, MARKER);
|
|
3829
4250
|
if (existsSync15(marker)) return;
|
|
3830
4251
|
io.stdout(`${STAR_PROMPT_LINE}
|
|
3831
4252
|
`);
|
|
@@ -3840,7 +4261,7 @@ function maybePrintStarPrompt(io, env = process.env, isTTY = process.stdout.isTT
|
|
|
3840
4261
|
// src/commands/sandbox-list.ts
|
|
3841
4262
|
async function entryStatus(root, name, run) {
|
|
3842
4263
|
try {
|
|
3843
|
-
const target = resolveExecutionTarget({ projectRoot: root, container: name, run });
|
|
4264
|
+
const target = resolveExecutionTarget({ projectRoot: root, container: entrySandboxName(root) ?? name, run });
|
|
3844
4265
|
return await target.status();
|
|
3845
4266
|
} catch {
|
|
3846
4267
|
return "unknown";
|
|
@@ -3902,10 +4323,10 @@ function gitIdentity(run, key) {
|
|
|
3902
4323
|
return (result.stdout ?? "").trim();
|
|
3903
4324
|
}
|
|
3904
4325
|
function isBuildCapable(repo) {
|
|
3905
|
-
return repo !== void 0 && existsSync16(
|
|
4326
|
+
return repo !== void 0 && existsSync16(join13(repo, ".devcontainer", "Dockerfile"));
|
|
3906
4327
|
}
|
|
3907
4328
|
function prepareHomeMount(value) {
|
|
3908
|
-
const path4 =
|
|
4329
|
+
const path4 = resolve9(value);
|
|
3909
4330
|
if (existsSync16(path4)) {
|
|
3910
4331
|
if (!statSync4(path4).isDirectory()) {
|
|
3911
4332
|
throw new Error(`--home-mount path exists and is not a directory: ${path4}`);
|
|
@@ -3949,18 +4370,18 @@ function mergeSettings(...sources) {
|
|
|
3949
4370
|
for (const source of present) overlaySettings(merged, source);
|
|
3950
4371
|
return merged;
|
|
3951
4372
|
}
|
|
3952
|
-
function
|
|
4373
|
+
function nonEmpty2(value) {
|
|
3953
4374
|
return value === void 0 || value === "" ? void 0 : value;
|
|
3954
4375
|
}
|
|
3955
|
-
function seedConfig(name, checkout, seed, run) {
|
|
4376
|
+
function seedConfig(name, checkout, seed, run, runtime = "docker") {
|
|
3956
4377
|
const config = overlaySettings(defaultAgroConfig(name), seed);
|
|
3957
4378
|
config.name = name;
|
|
3958
|
-
config.runtime =
|
|
4379
|
+
config.runtime = runtime;
|
|
3959
4380
|
if (checkout !== void 0) config.checkout = checkout;
|
|
3960
|
-
config.timezone =
|
|
4381
|
+
config.timezone = nonEmpty2(seed?.timezone) ?? hostTimezone();
|
|
3961
4382
|
config.git = {
|
|
3962
|
-
userName:
|
|
3963
|
-
userEmail:
|
|
4383
|
+
userName: nonEmpty2(seed?.git?.userName) ?? gitIdentity(run, "user.name"),
|
|
4384
|
+
userEmail: nonEmpty2(seed?.git?.userEmail) ?? gitIdentity(run, "user.email")
|
|
3964
4385
|
};
|
|
3965
4386
|
config.image = {
|
|
3966
4387
|
...config.image,
|
|
@@ -4001,7 +4422,7 @@ async function runWizard(config, io) {
|
|
|
4001
4422
|
"Host path for /home/sandbox (blank keeps the Docker-managed volume)",
|
|
4002
4423
|
config.storage?.homePath ?? ""
|
|
4003
4424
|
);
|
|
4004
|
-
if (homePath !== "") config.storage = { ...config.storage, homePath:
|
|
4425
|
+
if (homePath !== "") config.storage = { ...config.storage, homePath: resolve9(homePath) };
|
|
4005
4426
|
}
|
|
4006
4427
|
async function runSandboxInstall(opts, io) {
|
|
4007
4428
|
const run = opts.run ?? spawnRunner;
|
|
@@ -4018,7 +4439,8 @@ async function runSandboxInstall(opts, io) {
|
|
|
4018
4439
|
`);
|
|
4019
4440
|
return 1;
|
|
4020
4441
|
}
|
|
4021
|
-
|
|
4442
|
+
if (runtime.id === "openshell") return await installOpenShell(opts, io, run);
|
|
4443
|
+
const checkout = opts.checkout === void 0 ? void 0 : resolve9(opts.checkout);
|
|
4022
4444
|
if (checkout !== void 0 && !existsSync16(checkout)) {
|
|
4023
4445
|
io.stderr(`${opts.bin} sandbox install: --checkout directory does not exist: ${checkout}
|
|
4024
4446
|
`);
|
|
@@ -4036,9 +4458,9 @@ async function runSandboxInstall(opts, io) {
|
|
|
4036
4458
|
const config = seedConfig(name, checkout, mergeSettings(readEntryConfig(name), repoSeed), run);
|
|
4037
4459
|
const buildRequested = config.image?.mode === "build" && opts.noBuild !== true && opts.image !== true && opts.imageRef === void 0;
|
|
4038
4460
|
if (buildRequested && !isBuildCapable(configCheckout(config))) {
|
|
4039
|
-
const target = configCheckout(config) ??
|
|
4461
|
+
const target = configCheckout(config) ?? resolve9(opts.checkout ?? ".");
|
|
4040
4462
|
io.stderr(
|
|
4041
|
-
`${opts.bin} sandbox install: image.mode is "build" but ${
|
|
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
|
|
4042
4464
|
`
|
|
4043
4465
|
);
|
|
4044
4466
|
return 1;
|
|
@@ -4082,7 +4504,7 @@ async function runSandboxInstall(opts, io) {
|
|
|
4082
4504
|
...useNoBuild ? { noBuild: true } : {}
|
|
4083
4505
|
};
|
|
4084
4506
|
if (opts.printArgv === true) {
|
|
4085
|
-
const preview = mkdtempSync3(
|
|
4507
|
+
const preview = mkdtempSync3(join13(tmpdir3(), "oh-sandbox-preview-"));
|
|
4086
4508
|
try {
|
|
4087
4509
|
writeAgroConfig(preview, config);
|
|
4088
4510
|
materialize(preview, { ...configCheckout(config) !== void 0 ? { checkout: configCheckout(config) } : {} });
|
|
@@ -4103,16 +4525,77 @@ async function runSandboxInstall(opts, io) {
|
|
|
4103
4525
|
}
|
|
4104
4526
|
return code;
|
|
4105
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
|
+
}
|
|
4106
4589
|
|
|
4107
4590
|
// src/services/sandbox-upgrade.ts
|
|
4108
4591
|
import { closeSync as closeSync2, existsSync as existsSync17, openSync as openSync2, readFileSync as readFileSync11, unlinkSync as unlinkSync5, writeSync } from "node:fs";
|
|
4109
|
-
import { join as
|
|
4592
|
+
import { join as join14 } from "node:path";
|
|
4110
4593
|
function errorMessage(error) {
|
|
4111
4594
|
return error instanceof Error ? error.message : String(error);
|
|
4112
4595
|
}
|
|
4113
4596
|
function conflictingDotenvImage(root, imageRef) {
|
|
4114
|
-
const rootEnv =
|
|
4115
|
-
const fallbackEnv =
|
|
4597
|
+
const rootEnv = join14(root, ".env");
|
|
4598
|
+
const fallbackEnv = join14(root, ".devcontainer", ".env");
|
|
4116
4599
|
const file = existsSync17(rootEnv) ? rootEnv : fallbackEnv;
|
|
4117
4600
|
if (!existsSync17(file)) return void 0;
|
|
4118
4601
|
for (const line of readFileSync11(file, "utf8").split(/\r?\n/)) {
|
|
@@ -4150,7 +4633,8 @@ async function runSandboxUpgrade(opts, io) {
|
|
|
4150
4633
|
`);
|
|
4151
4634
|
return 1;
|
|
4152
4635
|
}
|
|
4153
|
-
|
|
4636
|
+
routeVerb(root, "upgrade", entrySandboxName(root) ?? opts.name);
|
|
4637
|
+
const lock = join14(root, ".sandbox-upgrade.lock");
|
|
4154
4638
|
let fd;
|
|
4155
4639
|
try {
|
|
4156
4640
|
fd = openSync2(lock, "wx", 384);
|
|
@@ -4265,6 +4749,10 @@ identity, SSH (with its host port), the host Docker socket, and the host path
|
|
|
4265
4749
|
for /home/sandbox. With --checkout it seeds those answers from that
|
|
4266
4750
|
checkout's ${stateNames(bin).configFile}.
|
|
4267
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
|
+
|
|
4268
4756
|
Flags:
|
|
4269
4757
|
--name <name> Registry entry name (default: the lowest free ${DEFAULT_NAME_PREFIX}<n>)
|
|
4270
4758
|
--checkout <dir> Bind this checkout into the sandbox and seed the defaults
|
|
@@ -4287,7 +4775,7 @@ Flags:
|
|
|
4287
4775
|
${stateNames(bin).configFile} image.ref > ${officialImageRef(AGRO_VERSION)}
|
|
4288
4776
|
--image=<ref> Run a custom image (implies --image); conflicts with --version
|
|
4289
4777
|
--no-build Suppress the local build and reuse an existing image
|
|
4290
|
-
--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
|
|
4291
4779
|
without writing an entry
|
|
4292
4780
|
--json Machine-readable output (list)
|
|
4293
4781
|
|
|
@@ -4480,7 +4968,7 @@ async function runSandboxCommand(rest, bin) {
|
|
|
4480
4968
|
// src/commands/harness.ts
|
|
4481
4969
|
import { existsSync as existsSync20 } from "node:fs";
|
|
4482
4970
|
import { homedir as homedir3 } from "node:os";
|
|
4483
|
-
import { delimiter as delimiter2, join as
|
|
4971
|
+
import { delimiter as delimiter2, join as join17 } from "node:path";
|
|
4484
4972
|
|
|
4485
4973
|
// src/lib/docs.ts
|
|
4486
4974
|
var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
|
|
@@ -4494,7 +4982,7 @@ function sourceDocsUrl(docsPath) {
|
|
|
4494
4982
|
|
|
4495
4983
|
// src/lib/host-config.ts
|
|
4496
4984
|
import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, renameSync as renameSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "node:fs";
|
|
4497
|
-
import { basename as
|
|
4985
|
+
import { basename as basename7, dirname as dirname8, isAbsolute, join as join15, resolve as resolve10 } from "node:path";
|
|
4498
4986
|
var HOST_CONFIG_MODE = 420;
|
|
4499
4987
|
var HOST_CONFIG_FILE = "config.json";
|
|
4500
4988
|
var WORKSPACES_SUBDIR = "workspaces";
|
|
@@ -4506,7 +4994,7 @@ function hostStateHome(env, home) {
|
|
|
4506
4994
|
return resolveUserStateHome(env, home);
|
|
4507
4995
|
}
|
|
4508
4996
|
function hostConfigPath(env, home) {
|
|
4509
|
-
return
|
|
4997
|
+
return join15(hostStateHome(env, home), HOST_CONFIG_FILE);
|
|
4510
4998
|
}
|
|
4511
4999
|
function assertWorkspaceName(name) {
|
|
4512
5000
|
if (!SANDBOX_NAME_PATTERN.test(name)) {
|
|
@@ -4516,14 +5004,14 @@ function assertWorkspaceName(name) {
|
|
|
4516
5004
|
}
|
|
4517
5005
|
}
|
|
4518
5006
|
function workspacesRoot(env, home) {
|
|
4519
|
-
return
|
|
5007
|
+
return join15(hostStateHome(env, home), WORKSPACES_SUBDIR);
|
|
4520
5008
|
}
|
|
4521
5009
|
function workspaceRoot(name, env, home) {
|
|
4522
5010
|
assertWorkspaceName(name);
|
|
4523
|
-
return
|
|
5011
|
+
return join15(workspacesRoot(env, home), name);
|
|
4524
5012
|
}
|
|
4525
5013
|
function defaultHarnessRoot(env, home) {
|
|
4526
|
-
return
|
|
5014
|
+
return join15(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
|
|
4527
5015
|
}
|
|
4528
5016
|
function validateHostConfig(value) {
|
|
4529
5017
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -4600,7 +5088,7 @@ function readHostConfig(env, home) {
|
|
|
4600
5088
|
try {
|
|
4601
5089
|
parsed = JSON.parse(raw);
|
|
4602
5090
|
} catch {
|
|
4603
|
-
throw new Error(`${
|
|
5091
|
+
throw new Error(`${basename7(path4)} is not valid JSON: ${path4}`);
|
|
4604
5092
|
}
|
|
4605
5093
|
return validateHostConfig(parsed);
|
|
4606
5094
|
}
|
|
@@ -4623,15 +5111,15 @@ function writeHostConfig(config, env, home) {
|
|
|
4623
5111
|
}
|
|
4624
5112
|
}
|
|
4625
5113
|
function resolveHarnessRoot(explicit, env, home) {
|
|
4626
|
-
if (typeof explicit === "string" && explicit !== "") return
|
|
5114
|
+
if (typeof explicit === "string" && explicit !== "") return resolve10(explicit);
|
|
4627
5115
|
const configured = readHostConfig(env, home).harnessRoot;
|
|
4628
|
-
if (typeof configured === "string" && configured !== "") return
|
|
5116
|
+
if (typeof configured === "string" && configured !== "") return resolve10(configured);
|
|
4629
5117
|
return defaultHarnessRoot(env, home);
|
|
4630
5118
|
}
|
|
4631
5119
|
function recordHarnessRoot(root, env, home) {
|
|
4632
5120
|
const config = readHostConfig(env, home);
|
|
4633
5121
|
const recorded = config.harnessRoot;
|
|
4634
|
-
if (typeof recorded === "string" && recorded !== "" &&
|
|
5122
|
+
if (typeof recorded === "string" && recorded !== "" && resolve10(recorded) === root) return;
|
|
4635
5123
|
writeHostConfig({ ...config, harnessRoot: root }, env, home);
|
|
4636
5124
|
}
|
|
4637
5125
|
function fieldError2(path4, requirement) {
|
|
@@ -4640,7 +5128,7 @@ function fieldError2(path4, requirement) {
|
|
|
4640
5128
|
|
|
4641
5129
|
// src/lib/host-workspace.ts
|
|
4642
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";
|
|
4643
|
-
import { dirname as dirname9, join as
|
|
5131
|
+
import { dirname as dirname9, join as join16, resolve as resolve11 } from "node:path";
|
|
4644
5132
|
var AGRO_REPO_URL = "https://github.com/mifunedev/agro.git";
|
|
4645
5133
|
var IGNORABLE_ENTRIES = [
|
|
4646
5134
|
REGISTRY_SUBDIR,
|
|
@@ -4652,15 +5140,15 @@ function isDirectory3(path4) {
|
|
|
4652
5140
|
return statSync5(path4, { throwIfNoEntry: false })?.isDirectory() === true;
|
|
4653
5141
|
}
|
|
4654
5142
|
function stateHomes(home) {
|
|
4655
|
-
return STATE_HOME_DIRS.map((name) =>
|
|
5143
|
+
return STATE_HOME_DIRS.map((name) => join16(home, name));
|
|
4656
5144
|
}
|
|
4657
5145
|
function stateHomeRootRefusal(bin, root, home, verb = "harness") {
|
|
4658
|
-
const target =
|
|
5146
|
+
const target = resolve11(root);
|
|
4659
5147
|
const enclosing = stateHomes(home).find((state) => target === state);
|
|
4660
5148
|
if (enclosing === void 0) return void 0;
|
|
4661
5149
|
return `${bin} ${verb}: the ${verb} root ${target} is the state home ${enclosing} itself.
|
|
4662
5150
|
A workspace there collides with AGRO state and blocks every command run from ${home}.
|
|
4663
|
-
Move it out \u2014 for example \`mv ${enclosing} ${
|
|
5151
|
+
Move it out \u2014 for example \`mv ${enclosing} ${join16(home, "agro")}\` \u2014 then re-run this command.
|
|
4664
5152
|
`;
|
|
4665
5153
|
}
|
|
4666
5154
|
function isIgnorableEntry(name) {
|
|
@@ -4677,24 +5165,24 @@ function cloneInto(target, run, root, ref) {
|
|
|
4677
5165
|
assertCloned(run("git", ["clone", ...branch, AGRO_REPO_URL, target], { stdio: "inherit" }), root);
|
|
4678
5166
|
}
|
|
4679
5167
|
function cloneThroughStaging(root, run, ref) {
|
|
4680
|
-
const staging = mkdtempSync4(
|
|
5168
|
+
const staging = mkdtempSync4(join16(dirname9(root), "agro-clone-"));
|
|
4681
5169
|
try {
|
|
4682
|
-
const checkout =
|
|
5170
|
+
const checkout = join16(staging, "agro");
|
|
4683
5171
|
cloneInto(checkout, run, root, ref);
|
|
4684
5172
|
for (const name of readdirSync3(checkout)) {
|
|
4685
|
-
const destination =
|
|
5173
|
+
const destination = join16(root, name);
|
|
4686
5174
|
if (existsSync19(destination)) {
|
|
4687
5175
|
throw new Error(`could not move ${name} into ${root} \u2014 the path already exists`);
|
|
4688
5176
|
}
|
|
4689
|
-
renameSync5(
|
|
5177
|
+
renameSync5(join16(checkout, name), destination);
|
|
4690
5178
|
}
|
|
4691
5179
|
} finally {
|
|
4692
5180
|
rmSync4(staging, { recursive: true, force: true });
|
|
4693
5181
|
}
|
|
4694
5182
|
}
|
|
4695
5183
|
function ensureHostWorkspace(path4, run = spawnRunner, ref) {
|
|
4696
|
-
const root =
|
|
4697
|
-
if (existsSync19(
|
|
5184
|
+
const root = resolve11(path4);
|
|
5185
|
+
if (existsSync19(join16(root, ".git"))) return { root, action: "reused" };
|
|
4698
5186
|
const stats = statSync5(root, { throwIfNoEntry: false });
|
|
4699
5187
|
if (stats === void 0) {
|
|
4700
5188
|
mkdirSync10(dirname9(root), { recursive: true });
|
|
@@ -4716,11 +5204,11 @@ function ensureHostWorkspace(path4, run = spawnRunner, ref) {
|
|
|
4716
5204
|
function listHostWorkspaces(env, home) {
|
|
4717
5205
|
const root = workspacesRoot(env, home);
|
|
4718
5206
|
if (!isDirectory3(root)) return [];
|
|
4719
|
-
return readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => SANDBOX_NAME_PATTERN.test(name)).filter((name) => existsSync19(
|
|
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();
|
|
4720
5208
|
}
|
|
4721
5209
|
function resolveExistingWorkspace(bin, verb, path4, env, home) {
|
|
4722
|
-
const root =
|
|
4723
|
-
if (existsSync19(
|
|
5210
|
+
const root = resolve11(path4);
|
|
5211
|
+
if (existsSync19(join16(root, ".git"))) return { ok: true, root };
|
|
4724
5212
|
const existing = listHostWorkspaces(env, home);
|
|
4725
5213
|
const inventory = existing.length === 0 ? "No host workspace exists yet.\n" : `Workspaces that exist: ${existing.join(", ")}
|
|
4726
5214
|
`;
|
|
@@ -4740,8 +5228,15 @@ var PROBE_TIMEOUT_MS = 15e3;
|
|
|
4740
5228
|
function isReachable(status) {
|
|
4741
5229
|
return status === "ready" || status === "starting";
|
|
4742
5230
|
}
|
|
4743
|
-
function
|
|
4744
|
-
|
|
5231
|
+
function registeredContainerName(cwd) {
|
|
5232
|
+
try {
|
|
5233
|
+
return configuredContainerName(resolveSandboxRoot(cwd === void 0 ? {} : { cwd }));
|
|
5234
|
+
} catch {
|
|
5235
|
+
return void 0;
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
function targetFor(root, cwd, run, env) {
|
|
5239
|
+
const name = configuredContainerName(root) ?? registeredContainerName(cwd) ?? DEFAULT_CONTAINER_NAME;
|
|
4745
5240
|
return resolveExecutionTarget({
|
|
4746
5241
|
projectRoot: root,
|
|
4747
5242
|
container: name,
|
|
@@ -4750,7 +5245,7 @@ function targetFor(root, run, env) {
|
|
|
4750
5245
|
});
|
|
4751
5246
|
}
|
|
4752
5247
|
function hostPrefix(home) {
|
|
4753
|
-
return
|
|
5248
|
+
return join17(home, ".local");
|
|
4754
5249
|
}
|
|
4755
5250
|
function pathEntries(env) {
|
|
4756
5251
|
return (env.PATH ?? "").split(delimiter2).filter((part) => part !== "");
|
|
@@ -4797,7 +5292,7 @@ async function sandboxMarkerExists(target, entry) {
|
|
|
4797
5292
|
return r.exitCode === 0;
|
|
4798
5293
|
}
|
|
4799
5294
|
function hostBinaryUnderPrefix(entry, prefix) {
|
|
4800
|
-
return existsSync20(
|
|
5295
|
+
return existsSync20(join17(harnessBinPath(prefix), entry.binary));
|
|
4801
5296
|
}
|
|
4802
5297
|
function stateOf(entry, installed, location) {
|
|
4803
5298
|
return {
|
|
@@ -4818,12 +5313,12 @@ function probeableHost(env, home) {
|
|
|
4818
5313
|
return void 0;
|
|
4819
5314
|
}
|
|
4820
5315
|
const prefix = hostPrefix(home);
|
|
4821
|
-
if (!existsSync20(
|
|
5316
|
+
if (!existsSync20(join17(root, ".git")) && !existsSync20(prefix)) return void 0;
|
|
4822
5317
|
return { root, prefix };
|
|
4823
5318
|
}
|
|
4824
|
-
async function collectStates(root, run, home, env, only) {
|
|
5319
|
+
async function collectStates(root, cwd, run, home, env, only) {
|
|
4825
5320
|
const entries = only ? [...only] : [...HARNESS_CATALOG];
|
|
4826
|
-
const target = targetFor(root, run, env);
|
|
5321
|
+
const target = targetFor(root, cwd, run, env);
|
|
4827
5322
|
const reachable = isReachable(await resolveTargetStatus(target));
|
|
4828
5323
|
if (reachable) {
|
|
4829
5324
|
const states2 = [];
|
|
@@ -4876,7 +5371,7 @@ INSTALLED is \`?\` \u2014 the sandbox is not running. Start it with \`${bin} san
|
|
|
4876
5371
|
async function runHarnessList(opts, io) {
|
|
4877
5372
|
const run = opts.run ?? spawnRunner;
|
|
4878
5373
|
const root = resolveProjectRoot(opts.cwd);
|
|
4879
|
-
const collected = await collectStates(root, run, homeOf(opts), opts.env);
|
|
5374
|
+
const collected = await collectStates(root, opts.cwd, run, homeOf(opts), opts.env);
|
|
4880
5375
|
if (opts.json) {
|
|
4881
5376
|
io.stdout(`${JSON.stringify(collected.states, null, 2)}
|
|
4882
5377
|
`);
|
|
@@ -4902,7 +5397,7 @@ async function runHarnessStatus(name, opts, io) {
|
|
|
4902
5397
|
only = findHarness(name);
|
|
4903
5398
|
if (!only) return unknownHarness(name, io, opts.bin);
|
|
4904
5399
|
}
|
|
4905
|
-
const collected = await collectStates(root, run, homeOf(opts), opts.env, only ? [only] : void 0);
|
|
5400
|
+
const collected = await collectStates(root, opts.cwd, run, homeOf(opts), opts.env, only ? [only] : void 0);
|
|
4906
5401
|
if (opts.json) {
|
|
4907
5402
|
io.stdout(`${JSON.stringify(only ? collected.states[0] : collected.states, null, 2)}
|
|
4908
5403
|
`);
|
|
@@ -4911,7 +5406,7 @@ async function runHarnessStatus(name, opts, io) {
|
|
|
4911
5406
|
}
|
|
4912
5407
|
return 0;
|
|
4913
5408
|
}
|
|
4914
|
-
function
|
|
5409
|
+
function sandboxTargetRoot(target) {
|
|
4915
5410
|
return target.kind === "docker-compose" ? "/home/sandbox/harness" : target.workspace.targetRoot;
|
|
4916
5411
|
}
|
|
4917
5412
|
function shellPath(path4) {
|
|
@@ -4924,7 +5419,7 @@ function hermesLaunch(io, root) {
|
|
|
4924
5419
|
function hermesEnv(root) {
|
|
4925
5420
|
return { ...agroEnvPair("PROJECT_ROOT", root), HERMES_HOME: `${root}/.hermes` };
|
|
4926
5421
|
}
|
|
4927
|
-
async function reconcileHermes(target, io, bin, user, root =
|
|
5422
|
+
async function reconcileHermes(target, io, bin, user, root = sandboxTargetRoot(target)) {
|
|
4928
5423
|
const result = await target.exec({
|
|
4929
5424
|
argv: remoteControlDirScript(root, "scripts/link-providers.sh", ["--init", "--hermes-only"]),
|
|
4930
5425
|
env: hermesEnv(root),
|
|
@@ -4937,7 +5432,7 @@ async function reconcileHermes(target, io, bin, user, root = hermesTargetRoot(ta
|
|
|
4937
5432
|
}
|
|
4938
5433
|
return result.exitCode;
|
|
4939
5434
|
}
|
|
4940
|
-
async function validateHermesHome(target, io, bin, user, inheritedHome, root =
|
|
5435
|
+
async function validateHermesHome(target, io, bin, user, inheritedHome, root = sandboxTargetRoot(target)) {
|
|
4941
5436
|
const result = await target.exec({
|
|
4942
5437
|
argv: remoteControlDirScript(root, "scripts/hermes-workspace.sh", [
|
|
4943
5438
|
"check",
|
|
@@ -4955,7 +5450,7 @@ async function validateHermesHome(target, io, bin, user, inheritedHome, root = h
|
|
|
4955
5450
|
}
|
|
4956
5451
|
return result.exitCode;
|
|
4957
5452
|
}
|
|
4958
|
-
async function configureHermes(target, io, bin, user, root =
|
|
5453
|
+
async function configureHermes(target, io, bin, user, root = sandboxTargetRoot(target)) {
|
|
4959
5454
|
const result = await target.exec({
|
|
4960
5455
|
argv: remoteControlDirScript(root, "scripts/hermes-workspace.sh", [
|
|
4961
5456
|
"configure",
|
|
@@ -4974,6 +5469,55 @@ async function configureHermes(target, io, bin, user, root = hermesTargetRoot(ta
|
|
|
4974
5469
|
}
|
|
4975
5470
|
return result.exitCode;
|
|
4976
5471
|
}
|
|
5472
|
+
function openclawStateDir(root) {
|
|
5473
|
+
return `${root}/.openclaw`;
|
|
5474
|
+
}
|
|
5475
|
+
function shellWord(value) {
|
|
5476
|
+
return /^[\w./-]+$/.test(value) ? value : shellPath(value);
|
|
5477
|
+
}
|
|
5478
|
+
function openclawLaunch(io, root) {
|
|
5479
|
+
io.stdout(`Run OpenClaw in the AGRO workspace: OPENCLAW_STATE_DIR=${shellWord(openclawStateDir(root))} openclaw
|
|
5480
|
+
`);
|
|
5481
|
+
}
|
|
5482
|
+
function openclawEnv(root) {
|
|
5483
|
+
return { ...agroEnvPair("PROJECT_ROOT", root), OPENCLAW_STATE_DIR: openclawStateDir(root) };
|
|
5484
|
+
}
|
|
5485
|
+
async function validateOpenclawState(target, io, bin, user, inheritedDir, root = sandboxTargetRoot(target)) {
|
|
5486
|
+
const result = await target.exec({
|
|
5487
|
+
argv: remoteControlDirScript(root, "scripts/openclaw-workspace.sh", [
|
|
5488
|
+
"check",
|
|
5489
|
+
root,
|
|
5490
|
+
openclawStateDir(root),
|
|
5491
|
+
inheritedDir ?? ""
|
|
5492
|
+
]),
|
|
5493
|
+
env: openclawEnv(root),
|
|
5494
|
+
...user ? { user } : {},
|
|
5495
|
+
stdio: "capture"
|
|
5496
|
+
});
|
|
5497
|
+
if (result.exitCode !== 0) {
|
|
5498
|
+
io.stderr(result.stderr || `${bin} harness: could not validate the OpenClaw state directory; select OPENCLAW_STATE_DIR=${shellWord(openclawStateDir(root))} before retrying.
|
|
5499
|
+
`);
|
|
5500
|
+
}
|
|
5501
|
+
return result.exitCode;
|
|
5502
|
+
}
|
|
5503
|
+
async function configureOpenclaw(target, io, bin, user, root = sandboxTargetRoot(target)) {
|
|
5504
|
+
const result = await target.exec({
|
|
5505
|
+
argv: remoteControlDirScript(root, "scripts/openclaw-workspace.sh", [
|
|
5506
|
+
"configure",
|
|
5507
|
+
root,
|
|
5508
|
+
openclawStateDir(root),
|
|
5509
|
+
"openclaw"
|
|
5510
|
+
]),
|
|
5511
|
+
env: openclawEnv(root),
|
|
5512
|
+
...user ? { user } : {},
|
|
5513
|
+
stdio: "inherit"
|
|
5514
|
+
});
|
|
5515
|
+
if (result.exitCode !== 0) {
|
|
5516
|
+
io.stderr(`${bin} harness: OpenClaw workspace configuration failed (exit ${result.exitCode}); no installation success reported.
|
|
5517
|
+
`);
|
|
5518
|
+
}
|
|
5519
|
+
return result.exitCode;
|
|
5520
|
+
}
|
|
4977
5521
|
function unreachableReason(status) {
|
|
4978
5522
|
return runtimeIsAbsent(status) ? "No container runtime is on PATH." : `The sandbox is not running (${status}).`;
|
|
4979
5523
|
}
|
|
@@ -5050,10 +5594,15 @@ async function installOnHost(entry, opts, io, run, status) {
|
|
|
5050
5594
|
const prefix = hostPrefix(home);
|
|
5051
5595
|
const target = hostTargetFor(root, prefix, run, env);
|
|
5052
5596
|
const hermes = entry.id === "hermes";
|
|
5597
|
+
const openclaw = entry.id === "openclaw";
|
|
5053
5598
|
if (hermes) {
|
|
5054
5599
|
const code = await validateHermesHome(target, io, bin, void 0, env.HERMES_HOME, root);
|
|
5055
5600
|
if (code !== 0) return code;
|
|
5056
5601
|
}
|
|
5602
|
+
if (openclaw) {
|
|
5603
|
+
const code = await validateOpenclawState(target, io, bin, void 0, env.OPENCLAW_STATE_DIR, root);
|
|
5604
|
+
if (code !== 0) return code;
|
|
5605
|
+
}
|
|
5057
5606
|
const linked = await target.exec({
|
|
5058
5607
|
argv: remoteControlDirScript(root, "scripts/link-providers.sh", ["--init"]),
|
|
5059
5608
|
env: agroEnvPair("PROJECT_ROOT", root),
|
|
@@ -5067,7 +5616,7 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
|
|
|
5067
5616
|
);
|
|
5068
5617
|
return 1;
|
|
5069
5618
|
}
|
|
5070
|
-
const installEnv = hermes ? hermesEnv(root) : void 0;
|
|
5619
|
+
const installEnv = hermes ? hermesEnv(root) : openclaw ? openclawEnv(root) : void 0;
|
|
5071
5620
|
if (hermes) {
|
|
5072
5621
|
const code = await reconcileHermes(target, io, bin, void 0, root);
|
|
5073
5622
|
if (code !== 0) return code;
|
|
@@ -5080,11 +5629,16 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
|
|
|
5080
5629
|
`);
|
|
5081
5630
|
return 1;
|
|
5082
5631
|
}
|
|
5083
|
-
|
|
5632
|
+
const present = await probeInstalled(target, entry, prefix, void 0, installEnv) === true;
|
|
5633
|
+
if (present && !hasReceipt && !hostBinaryUnderPrefix(entry, prefix)) {
|
|
5084
5634
|
if (hermes) {
|
|
5085
5635
|
const code = await configureHermes(target, io, bin, void 0, root);
|
|
5086
5636
|
if (code !== 0) return code;
|
|
5087
5637
|
}
|
|
5638
|
+
if (openclaw) {
|
|
5639
|
+
const code = await configureOpenclaw(target, io, bin, void 0, root);
|
|
5640
|
+
if (code !== 0) return code;
|
|
5641
|
+
}
|
|
5088
5642
|
io.stdout(`${entry.id}: already installed (${entry.binary})
|
|
5089
5643
|
`);
|
|
5090
5644
|
try {
|
|
@@ -5095,9 +5649,10 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
|
|
|
5095
5649
|
return 1;
|
|
5096
5650
|
}
|
|
5097
5651
|
if (hermes) hermesLaunch(io, root);
|
|
5652
|
+
if (openclaw) openclawLaunch(io, root);
|
|
5098
5653
|
return 0;
|
|
5099
5654
|
}
|
|
5100
|
-
io.stdout(
|
|
5655
|
+
io.stdout(`${present && hasReceipt ? "updating" : "installing"} ${entry.title} on the host\u2026
|
|
5101
5656
|
`);
|
|
5102
5657
|
const r = await target.exec({
|
|
5103
5658
|
argv: resolveInstallArgv(entry, prefix),
|
|
@@ -5120,6 +5675,10 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
|
|
|
5120
5675
|
const configured = await configureHermes(target, io, bin, void 0, root);
|
|
5121
5676
|
if (configured !== 0) return configured;
|
|
5122
5677
|
}
|
|
5678
|
+
if (openclaw) {
|
|
5679
|
+
const configured = await configureOpenclaw(target, io, bin, void 0, root);
|
|
5680
|
+
if (configured !== 0) return configured;
|
|
5681
|
+
}
|
|
5123
5682
|
const receipt = {
|
|
5124
5683
|
prefix,
|
|
5125
5684
|
binary: entry.binary,
|
|
@@ -5150,6 +5709,7 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
|
|
|
5150
5709
|
`);
|
|
5151
5710
|
}
|
|
5152
5711
|
if (hermes) hermesLaunch(io, root);
|
|
5712
|
+
else if (openclaw) openclawLaunch(io, root);
|
|
5153
5713
|
else io.stdout(`Run ${entry.title} in the AGRO workspace: cd ${root} && ${harnessLaunchCommand(entry)}
|
|
5154
5714
|
`);
|
|
5155
5715
|
return 0;
|
|
@@ -5248,7 +5808,7 @@ async function runHarnessUninstall(name, opts, io) {
|
|
|
5248
5808
|
const root = resolveProjectRoot(opts.cwd);
|
|
5249
5809
|
const entry = findHarness(name);
|
|
5250
5810
|
if (!entry) return unknownHarness(name, io, opts.bin);
|
|
5251
|
-
const target = targetFor(root, run, opts.env);
|
|
5811
|
+
const target = targetFor(root, opts.cwd, run, opts.env);
|
|
5252
5812
|
if (!isReachable(await resolveTargetStatus(target))) {
|
|
5253
5813
|
return await uninstallOnHost(entry, opts, io, run);
|
|
5254
5814
|
}
|
|
@@ -5272,40 +5832,41 @@ async function runHarnessInstall(name, opts, io) {
|
|
|
5272
5832
|
const env = opts.env ?? process.env;
|
|
5273
5833
|
const projectRoot = agroEnvValue(env, "PROJECT_ROOT");
|
|
5274
5834
|
const root = resolveProjectRoot(
|
|
5275
|
-
name === "hermes" && runningInsideSandbox(env) && projectRoot !== void 0 ? projectRoot : opts.cwd
|
|
5835
|
+
(name === "hermes" || name === "openclaw") && runningInsideSandbox(env) && projectRoot !== void 0 ? projectRoot : opts.cwd
|
|
5276
5836
|
);
|
|
5277
5837
|
const entry = findHarness(name);
|
|
5278
5838
|
if (!entry) return unknownHarness(name, io, opts.bin);
|
|
5279
|
-
const target = targetFor(root, run, opts.env);
|
|
5839
|
+
const target = targetFor(root, opts.cwd, run, opts.env);
|
|
5280
5840
|
const status = await resolveTargetStatus(target);
|
|
5281
5841
|
if (!isReachable(status)) {
|
|
5282
5842
|
return await installOnHost(entry, opts, io, run, status);
|
|
5283
5843
|
}
|
|
5284
5844
|
const hermes = entry.id === "hermes";
|
|
5285
|
-
const
|
|
5845
|
+
const openclaw = entry.id === "openclaw";
|
|
5846
|
+
const targetRoot = sandboxTargetRoot(target);
|
|
5847
|
+
const installEnv = hermes ? hermesEnv(targetRoot) : openclaw ? openclawEnv(targetRoot) : void 0;
|
|
5286
5848
|
if (hermes) {
|
|
5287
5849
|
const checked = await validateHermesHome(target, io, opts.bin, "sandbox", env.HERMES_HOME);
|
|
5288
5850
|
if (checked !== 0) return checked;
|
|
5289
5851
|
const code = await reconcileHermes(target, io, opts.bin, "sandbox");
|
|
5290
5852
|
if (code !== 0) return code;
|
|
5291
5853
|
}
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
if (
|
|
5295
|
-
const code = await configureHermes(target, io, opts.bin, "sandbox");
|
|
5296
|
-
if (code !== 0) return code;
|
|
5297
|
-
}
|
|
5298
|
-
io.stdout(`${entry.id}: already installed (${entry.binary})
|
|
5299
|
-
`);
|
|
5300
|
-
if (hermes) hermesLaunch(io, hermesTargetRoot(target));
|
|
5301
|
-
return 0;
|
|
5854
|
+
if (openclaw) {
|
|
5855
|
+
const checked = await validateOpenclawState(target, io, opts.bin, "sandbox", env.OPENCLAW_STATE_DIR);
|
|
5856
|
+
if (checked !== 0) return checked;
|
|
5302
5857
|
}
|
|
5858
|
+
const already = await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox", installEnv);
|
|
5303
5859
|
if (already === null) {
|
|
5304
5860
|
io.stderr("docker is required to install into the running sandbox but was not found on PATH\n");
|
|
5305
5861
|
return 1;
|
|
5306
5862
|
}
|
|
5307
|
-
|
|
5863
|
+
if (already && await sandboxMarkerExists(target, entry)) {
|
|
5864
|
+
io.stdout(`updating ${entry.title} in the sandbox\u2026
|
|
5308
5865
|
`);
|
|
5866
|
+
} else {
|
|
5867
|
+
io.stdout(`installing ${entry.title} into the sandbox\u2026
|
|
5868
|
+
`);
|
|
5869
|
+
}
|
|
5309
5870
|
const r = await target.exec({
|
|
5310
5871
|
argv: resolveInstallArgv(entry, SANDBOX_HARNESS_PREFIX),
|
|
5311
5872
|
user: entry.installUser,
|
|
@@ -5328,6 +5889,10 @@ async function runHarnessInstall(name, opts, io) {
|
|
|
5328
5889
|
const configured = await configureHermes(target, io, opts.bin, "sandbox");
|
|
5329
5890
|
if (configured !== 0) return configured;
|
|
5330
5891
|
}
|
|
5892
|
+
if (openclaw) {
|
|
5893
|
+
const configured = await configureOpenclaw(target, io, opts.bin, "sandbox");
|
|
5894
|
+
if (configured !== 0) return configured;
|
|
5895
|
+
}
|
|
5331
5896
|
const marker = sandboxMarkerPath(entry);
|
|
5332
5897
|
const marked = await target.exec({
|
|
5333
5898
|
argv: ["sh", "-c", `mkdir -p "$(dirname "$1")" && : > "$1"`, "sh", marker],
|
|
@@ -5341,14 +5906,15 @@ async function runHarnessInstall(name, opts, io) {
|
|
|
5341
5906
|
}
|
|
5342
5907
|
io.stdout(`${entry.id}: installed \u2014 see ${sourceDocsUrl(entry.docsPath)} for authentication
|
|
5343
5908
|
`);
|
|
5344
|
-
if (hermes) hermesLaunch(io,
|
|
5909
|
+
if (hermes) hermesLaunch(io, targetRoot);
|
|
5910
|
+
if (openclaw) openclawLaunch(io, targetRoot);
|
|
5345
5911
|
return 0;
|
|
5346
5912
|
}
|
|
5347
5913
|
|
|
5348
5914
|
// src/commands/workspace.ts
|
|
5349
5915
|
import { existsSync as existsSync21, rmSync as rmSync5 } from "node:fs";
|
|
5350
5916
|
import { homedir as homedir4 } from "node:os";
|
|
5351
|
-
import { join as
|
|
5917
|
+
import { join as join18, resolve as resolve12 } from "node:path";
|
|
5352
5918
|
function homeOf2(opts) {
|
|
5353
5919
|
return (opts.homedir ?? homedir4)();
|
|
5354
5920
|
}
|
|
@@ -5357,7 +5923,7 @@ function messageOf2(error) {
|
|
|
5357
5923
|
}
|
|
5358
5924
|
function recordedRoot(env, home) {
|
|
5359
5925
|
const configured = readHostConfig(env, home).harnessRoot;
|
|
5360
|
-
return typeof configured === "string" && configured !== "" ?
|
|
5926
|
+
return typeof configured === "string" && configured !== "" ? resolve12(configured) : void 0;
|
|
5361
5927
|
}
|
|
5362
5928
|
function renderTable2(rows, io) {
|
|
5363
5929
|
const header2 = ["WORKSPACE", "DEFAULT", "PATH"];
|
|
@@ -5382,7 +5948,7 @@ async function runWorkspaceCreate(name, opts, io) {
|
|
|
5382
5948
|
let root;
|
|
5383
5949
|
const chosen = name ?? DEFAULT_WORKSPACE_NAME;
|
|
5384
5950
|
try {
|
|
5385
|
-
root = opts.path !== void 0 ?
|
|
5951
|
+
root = opts.path !== void 0 ? resolve12(opts.path) : workspaceRoot(chosen, env, home);
|
|
5386
5952
|
} catch (err) {
|
|
5387
5953
|
io.stderr(`${bin} workspace: ${messageOf2(err)}
|
|
5388
5954
|
`);
|
|
@@ -5396,7 +5962,7 @@ async function runWorkspaceCreate(name, opts, io) {
|
|
|
5396
5962
|
let action;
|
|
5397
5963
|
const preexisting = existsSync21(root);
|
|
5398
5964
|
try {
|
|
5399
|
-
if (!opts.json && !existsSync21(
|
|
5965
|
+
if (!opts.json && !existsSync21(join18(root, ".git"))) {
|
|
5400
5966
|
const at = opts.ref !== void 0 ? ` at ${opts.ref}` : "";
|
|
5401
5967
|
io.stdout(`cloning ${AGRO_REPO_URL}${at} into ${root}\u2026
|
|
5402
5968
|
`);
|
|
@@ -5441,7 +6007,7 @@ async function runWorkspaceList(opts, io) {
|
|
|
5441
6007
|
const recorded = recordedRoot(env, home);
|
|
5442
6008
|
const registry = workspacesRoot(env, home);
|
|
5443
6009
|
rows = listHostWorkspaces(env, home).map((name) => {
|
|
5444
|
-
const root =
|
|
6010
|
+
const root = join18(registry, name);
|
|
5445
6011
|
return { name, root, default: recorded === root };
|
|
5446
6012
|
});
|
|
5447
6013
|
} catch (err) {
|
|
@@ -5468,7 +6034,7 @@ async function runWorkspaceList(opts, io) {
|
|
|
5468
6034
|
// src/commands/tool.ts
|
|
5469
6035
|
import { existsSync as existsSync22 } from "node:fs";
|
|
5470
6036
|
import { homedir as homedir5, userInfo as userInfo2 } from "node:os";
|
|
5471
|
-
import { delimiter as delimiter3, join as
|
|
6037
|
+
import { delimiter as delimiter3, join as join19 } from "node:path";
|
|
5472
6038
|
|
|
5473
6039
|
// src/lib/tools/catalog.ts
|
|
5474
6040
|
var TOOLS_DOC = "docs/installation.md";
|
|
@@ -5899,7 +6465,7 @@ function targetFor2(root, run, env) {
|
|
|
5899
6465
|
});
|
|
5900
6466
|
}
|
|
5901
6467
|
function hostPrefix2(home) {
|
|
5902
|
-
return
|
|
6468
|
+
return join19(home, ".local");
|
|
5903
6469
|
}
|
|
5904
6470
|
function sandboxMarkerPath2(entry) {
|
|
5905
6471
|
return `${SANDBOX_HARNESS_PREFIX}/share/agro/tools/${entry.id}.installed`;
|
|
@@ -5913,7 +6479,7 @@ async function sandboxMarkerExists2(target, entry) {
|
|
|
5913
6479
|
return r.exitCode === 0;
|
|
5914
6480
|
}
|
|
5915
6481
|
function hostBinaryUnderPrefix2(entry, prefix) {
|
|
5916
|
-
return existsSync22(
|
|
6482
|
+
return existsSync22(join19(harnessBinPath(prefix), entry.binary));
|
|
5917
6483
|
}
|
|
5918
6484
|
function pathEntries2(env) {
|
|
5919
6485
|
return (env.PATH ?? "").split(delimiter3).filter((part) => part !== "");
|
|
@@ -5993,7 +6559,7 @@ function probeableHost2(env, home) {
|
|
|
5993
6559
|
return void 0;
|
|
5994
6560
|
}
|
|
5995
6561
|
const prefix = hostPrefix2(home);
|
|
5996
|
-
if (!existsSync22(
|
|
6562
|
+
if (!existsSync22(join19(root, ".git")) && !existsSync22(prefix)) return void 0;
|
|
5997
6563
|
return { root, prefix };
|
|
5998
6564
|
}
|
|
5999
6565
|
async function collectRows(root, run, home, env, only) {
|
|
@@ -6559,7 +7125,7 @@ var AGRO_COMMANDS = Object.freeze({
|
|
|
6559
7125
|
});
|
|
6560
7126
|
|
|
6561
7127
|
// src/cli.ts
|
|
6562
|
-
var DEFAULT_SOURCE_CONTROL_DIR =
|
|
7128
|
+
var DEFAULT_SOURCE_CONTROL_DIR = resolve13(
|
|
6563
7129
|
dirname10(fileURLToPath(import.meta.url)),
|
|
6564
7130
|
"../.."
|
|
6565
7131
|
);
|
|
@@ -6605,7 +7171,7 @@ Usage:
|
|
|
6605
7171
|
${bin} workspace <args...> Create and list host AGRO workspaces (create|list)
|
|
6606
7172
|
${bin} harness <args...> Install and inspect agent CLI harnesses
|
|
6607
7173
|
${bin} tool <args...> Install and inspect sandbox tooling
|
|
6608
|
-
${bin} gateway <args...> Manage a messaging client session (pi|hermes)
|
|
7174
|
+
${bin} gateway <args...> Manage a messaging client session (pi|hermes|openclaw)
|
|
6609
7175
|
${bin} --version Print version
|
|
6610
7176
|
${bin} --help Show this help
|
|
6611
7177
|
|
|
@@ -6813,10 +7379,10 @@ function printGatewayHelp(bin = AGRO_PRODUCT.bin) {
|
|
|
6813
7379
|
process.stdout.write(`${bin} gateway \u2014 Manage a messaging client session (Slack bridge)
|
|
6814
7380
|
|
|
6815
7381
|
Usage:
|
|
6816
|
-
${bin} gateway <pi|hermes> [--attach] start the client session (--attach after)
|
|
6817
|
-
${bin} gateway <pi|hermes> --restart restart the session
|
|
6818
|
-
${bin} gateway <pi|hermes> --stop stop the session
|
|
6819
|
-
${bin} gateway status
|
|
7382
|
+
${bin} gateway <pi|hermes|openclaw> [--attach] start the client session (--attach after)
|
|
7383
|
+
${bin} gateway <pi|hermes|openclaw> --restart restart the session
|
|
7384
|
+
${bin} gateway <pi|hermes|openclaw> --stop stop the session
|
|
7385
|
+
${bin} gateway status show all sessions
|
|
6820
7386
|
|
|
6821
7387
|
Only a LEADING --help/-h is intercepted here; everything else passes through
|
|
6822
7388
|
verbatim to the vendored ${stateNames(bin).controlDir}/scripts/gateway.sh with ${stateNames(bin).envPrefix}PROJECT_ROOT set to
|
|
@@ -7404,18 +7970,18 @@ function parseGatewayArgs(rest) {
|
|
|
7404
7970
|
return { ok: true, args: { help: false, passthrough: [...rest] } };
|
|
7405
7971
|
}
|
|
7406
7972
|
function bundledPayloadExists(bundled, exists = existsSync23) {
|
|
7407
|
-
return exists(
|
|
7973
|
+
return exists(join20(bundled.sourceControlDir, "manifest.json"));
|
|
7408
7974
|
}
|
|
7409
7975
|
function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
|
|
7410
7976
|
const exists = bundled.exists ?? existsSync23;
|
|
7411
7977
|
if (args.fromDir !== void 0) {
|
|
7412
|
-
return { kind: "local", fromDir:
|
|
7978
|
+
return { kind: "local", fromDir: resolve13(args.fromDir) };
|
|
7413
7979
|
}
|
|
7414
7980
|
if (args.fromRemote) {
|
|
7415
7981
|
return { kind: "remote", ref: args.ref };
|
|
7416
7982
|
}
|
|
7417
7983
|
if (bundledPayloadExists(bundled, exists)) {
|
|
7418
|
-
return { kind: "local", fromDir:
|
|
7984
|
+
return { kind: "local", fromDir: resolve13(bundled.sourceControlDir, "..") };
|
|
7419
7985
|
}
|
|
7420
7986
|
return {
|
|
7421
7987
|
kind: "remote",
|
|
@@ -7427,7 +7993,7 @@ function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
|
|
|
7427
7993
|
function readPayloadVersion(checkoutDir) {
|
|
7428
7994
|
try {
|
|
7429
7995
|
const parsed = JSON.parse(
|
|
7430
|
-
readFileSync13(
|
|
7996
|
+
readFileSync13(join20(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
|
|
7431
7997
|
);
|
|
7432
7998
|
if (parsed && typeof parsed.version === "string") return parsed.version;
|
|
7433
7999
|
} catch {
|
|
@@ -7436,7 +8002,7 @@ function readPayloadVersion(checkoutDir) {
|
|
|
7436
8002
|
}
|
|
7437
8003
|
async function runWithRemoteSource(hooks, fn) {
|
|
7438
8004
|
const fetch2 = hooks.fetch ?? fetchRemoteSource;
|
|
7439
|
-
const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(
|
|
8005
|
+
const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join20(tmpdir4(), "oh-remote-")));
|
|
7440
8006
|
const rm = hooks.rm ?? ((dir) => rmSync6(dir, { recursive: true, force: true }));
|
|
7441
8007
|
const out = hooks.stdout ?? ((s) => process.stdout.write(s));
|
|
7442
8008
|
const checkoutDir = mkdtemp();
|
|
@@ -7802,7 +8368,19 @@ function lifecycleIo() {
|
|
|
7802
8368
|
stderr: (s) => process.stderr.write(s)
|
|
7803
8369
|
};
|
|
7804
8370
|
}
|
|
7805
|
-
|
|
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(
|
|
7806
8384
|
(code) => process.exit(code),
|
|
7807
8385
|
(err) => {
|
|
7808
8386
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -7844,6 +8422,7 @@ export {
|
|
|
7844
8422
|
printVendorHelp,
|
|
7845
8423
|
printWorkspaceHelp,
|
|
7846
8424
|
resolveVendorSource,
|
|
8425
|
+
runCli,
|
|
7847
8426
|
runWithRemoteSource,
|
|
7848
8427
|
runtimeLines
|
|
7849
8428
|
};
|