@mifune/agro 0.11.0 → 0.12.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/dist/agro.js +1083 -151
- package/dist/oh.js +1083 -151
- package/package.json +1 -1
package/dist/agro.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { dirname as
|
|
6
|
-
import { existsSync as
|
|
5
|
+
import { dirname as dirname10, join as join14, resolve as resolve14 } from "node:path";
|
|
6
|
+
import { existsSync as existsSync20, mkdtempSync as mkdtempSync4, readFileSync as readFileSync12, rmSync as rmSync4 } from "node:fs";
|
|
7
7
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
8
8
|
|
|
9
9
|
// src/commands/update.ts
|
|
@@ -230,6 +230,17 @@ function resolveRegistryHome(env = process.env, home = homedir(), warn) {
|
|
|
230
230
|
if (configured !== void 0) return { path: resolve(configured), configured: true };
|
|
231
231
|
return { path: home, configured: false };
|
|
232
232
|
}
|
|
233
|
+
function generationUserStateHome(generation, env, home, warn) {
|
|
234
|
+
const configured = aliasedEnvValue(env, "HOME", warn);
|
|
235
|
+
if (configured !== void 0) return resolve(configured);
|
|
236
|
+
return join(home, GENERATIONS[generation].userStateDir);
|
|
237
|
+
}
|
|
238
|
+
function resolveAgroUserStateHome(env = process.env, home = homedir(), warn) {
|
|
239
|
+
return generationUserStateHome("agro", env, home, warn);
|
|
240
|
+
}
|
|
241
|
+
function resolveLegacyUserStateHome(env = process.env, home = homedir(), warn) {
|
|
242
|
+
return generationUserStateHome("legacy", env, home, warn);
|
|
243
|
+
}
|
|
233
244
|
function resolveUserStateHome(env = process.env, home = homedir(), warn) {
|
|
234
245
|
const configured = aliasedEnvValue(env, "HOME", warn);
|
|
235
246
|
if (configured !== void 0) return resolve(configured);
|
|
@@ -1959,8 +1970,8 @@ function redact(token) {
|
|
|
1959
1970
|
async function ask(question) {
|
|
1960
1971
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1961
1972
|
try {
|
|
1962
|
-
return await new Promise((
|
|
1963
|
-
rl.question(` ${question} `, (answer) =>
|
|
1973
|
+
return await new Promise((resolve15) => {
|
|
1974
|
+
rl.question(` ${question} `, (answer) => resolve15(answer.trim()));
|
|
1964
1975
|
});
|
|
1965
1976
|
} finally {
|
|
1966
1977
|
rl.close();
|
|
@@ -1972,7 +1983,7 @@ async function askSecret(question) {
|
|
|
1972
1983
|
process.stdin.setRawMode(true);
|
|
1973
1984
|
process.stdin.resume();
|
|
1974
1985
|
const bytes = [];
|
|
1975
|
-
return await new Promise((
|
|
1986
|
+
return await new Promise((resolve15) => {
|
|
1976
1987
|
const cleanup = () => {
|
|
1977
1988
|
try {
|
|
1978
1989
|
process.stdin.setRawMode(false);
|
|
@@ -1990,7 +2001,7 @@ async function askSecret(question) {
|
|
|
1990
2001
|
} else if (byte === 13 || byte === 10) {
|
|
1991
2002
|
cleanup();
|
|
1992
2003
|
process.stdout.write("\n");
|
|
1993
|
-
|
|
2004
|
+
resolve15(Buffer.from(bytes).toString("utf8").trim());
|
|
1994
2005
|
return;
|
|
1995
2006
|
} else if (byte === 127 || byte === 8) {
|
|
1996
2007
|
if (bytes.length > 0) {
|
|
@@ -2040,10 +2051,10 @@ async function askSecretPiped(question) {
|
|
|
2040
2051
|
};
|
|
2041
2052
|
process.on("SIGINT", onSigint);
|
|
2042
2053
|
try {
|
|
2043
|
-
return await new Promise((
|
|
2054
|
+
return await new Promise((resolve15) => {
|
|
2044
2055
|
rl.question(` ${question} `, (answer) => {
|
|
2045
2056
|
process.stdout.write("\n");
|
|
2046
|
-
|
|
2057
|
+
resolve15(answer.trim());
|
|
2047
2058
|
});
|
|
2048
2059
|
});
|
|
2049
2060
|
} finally {
|
|
@@ -2976,6 +2987,20 @@ function defaultIdentity() {
|
|
|
2976
2987
|
return { name: info.username, uid: info.uid };
|
|
2977
2988
|
}
|
|
2978
2989
|
|
|
2990
|
+
// src/lib/execution/status.ts
|
|
2991
|
+
var RUNTIME_ABSENT_STATUS = "no container runtime";
|
|
2992
|
+
async function resolveTargetStatus(target) {
|
|
2993
|
+
try {
|
|
2994
|
+
return await target.status();
|
|
2995
|
+
} catch (err) {
|
|
2996
|
+
if (err instanceof ExecutionSpawnError) return RUNTIME_ABSENT_STATUS;
|
|
2997
|
+
throw err;
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
function runtimeIsAbsent(status) {
|
|
3001
|
+
return status === RUNTIME_ABSENT_STATUS;
|
|
3002
|
+
}
|
|
3003
|
+
|
|
2979
3004
|
// src/lib/execution/index.ts
|
|
2980
3005
|
function resolveExecutionTarget(opts) {
|
|
2981
3006
|
if (runningInsideSandbox(opts.env ?? process.env)) {
|
|
@@ -3984,6 +4009,11 @@ async function runSandboxList(opts, io) {
|
|
|
3984
4009
|
return 0;
|
|
3985
4010
|
}
|
|
3986
4011
|
|
|
4012
|
+
// src/commands/harness.ts
|
|
4013
|
+
import { existsSync as existsSync18 } from "node:fs";
|
|
4014
|
+
import { homedir as homedir2 } from "node:os";
|
|
4015
|
+
import { delimiter as delimiter2, join as join12, resolve as resolve12 } from "node:path";
|
|
4016
|
+
|
|
3987
4017
|
// src/lib/docs.ts
|
|
3988
4018
|
var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
|
|
3989
4019
|
function sourceDocsUrl(docsPath) {
|
|
@@ -3994,7 +4024,194 @@ function sourceDocsUrl(docsPath) {
|
|
|
3994
4024
|
return new URL(segments.map(encodeURIComponent).join("/"), SOURCE_DOCS_BASE).toString();
|
|
3995
4025
|
}
|
|
3996
4026
|
|
|
4027
|
+
// src/lib/host-config.ts
|
|
4028
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync11, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
4029
|
+
import { basename as basename6, dirname as dirname8, isAbsolute as isAbsolute2, join as join10, resolve as resolve10 } from "node:path";
|
|
4030
|
+
var HOST_CONFIG_MODE = 420;
|
|
4031
|
+
function hostConfigPath(env = process.env, home) {
|
|
4032
|
+
return join10(resolveAgroUserStateHome(env, home), GENERATIONS.agro.configFile);
|
|
4033
|
+
}
|
|
4034
|
+
function legacyHostConfigPath(env = process.env, home) {
|
|
4035
|
+
return join10(resolveLegacyUserStateHome(env, home), GENERATIONS.legacy.configFile);
|
|
4036
|
+
}
|
|
4037
|
+
function readableHostConfigPath(env, home) {
|
|
4038
|
+
const path4 = hostConfigPath(env, home);
|
|
4039
|
+
if (existsSync16(path4)) return path4;
|
|
4040
|
+
const legacy = legacyHostConfigPath(env, home);
|
|
4041
|
+
return existsSync16(legacy) ? legacy : path4;
|
|
4042
|
+
}
|
|
4043
|
+
function defaultHarnessRoot(env = process.env, home) {
|
|
4044
|
+
return resolveAgroUserStateHome(env, home);
|
|
4045
|
+
}
|
|
4046
|
+
function validateHostConfig(value) {
|
|
4047
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4048
|
+
throw new Error(`${GENERATIONS.agro.configFile}: must contain a JSON object`);
|
|
4049
|
+
}
|
|
4050
|
+
const record = value;
|
|
4051
|
+
if (record.version !== void 0 && record.version !== 1) {
|
|
4052
|
+
throw fieldError2("version", "must be 1");
|
|
4053
|
+
}
|
|
4054
|
+
const harnessRoot = record.harnessRoot;
|
|
4055
|
+
if (harnessRoot !== void 0) {
|
|
4056
|
+
if (typeof harnessRoot !== "string" || harnessRoot === "") {
|
|
4057
|
+
throw fieldError2("harnessRoot", "must be a non-empty string");
|
|
4058
|
+
}
|
|
4059
|
+
if (!isAbsolute2(harnessRoot)) {
|
|
4060
|
+
throw fieldError2("harnessRoot", "must be an absolute path");
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
const hostHarnesses = record.hostHarnesses;
|
|
4064
|
+
if (hostHarnesses !== void 0) validateReceipts(hostHarnesses, "hostHarnesses", "harness");
|
|
4065
|
+
const hostTools = record.hostTools;
|
|
4066
|
+
if (hostTools !== void 0) validateReceipts(hostTools, "hostTools", "tool");
|
|
4067
|
+
return { ...record, version: 1 };
|
|
4068
|
+
}
|
|
4069
|
+
function absoluteField(value, path4) {
|
|
4070
|
+
if (typeof value !== "string" || value === "") {
|
|
4071
|
+
throw fieldError2(path4, "must be a non-empty string");
|
|
4072
|
+
}
|
|
4073
|
+
if (!isAbsolute2(value)) throw fieldError2(path4, "must be an absolute path");
|
|
4074
|
+
return value;
|
|
4075
|
+
}
|
|
4076
|
+
function validateReceipts(value, field, noun) {
|
|
4077
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4078
|
+
throw fieldError2(field, `must be a JSON object keyed by ${noun} id`);
|
|
4079
|
+
}
|
|
4080
|
+
for (const [id, receipt] of Object.entries(value)) {
|
|
4081
|
+
if (id === "") throw fieldError2(field, `must use a non-empty ${noun} id as each key`);
|
|
4082
|
+
const at = `${field}.${id}`;
|
|
4083
|
+
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
|
|
4084
|
+
throw fieldError2(at, "must be a JSON object");
|
|
4085
|
+
}
|
|
4086
|
+
const entry = receipt;
|
|
4087
|
+
absoluteField(entry.prefix, `${at}.prefix`);
|
|
4088
|
+
absoluteField(entry.binPath, `${at}.binPath`);
|
|
4089
|
+
if (typeof entry.binary !== "string" || entry.binary === "") {
|
|
4090
|
+
throw fieldError2(`${at}.binary`, "must be a non-empty string");
|
|
4091
|
+
}
|
|
4092
|
+
if (typeof entry.installedAt !== "string" || entry.installedAt === "") {
|
|
4093
|
+
throw fieldError2(`${at}.installedAt`, "must be a non-empty string");
|
|
4094
|
+
}
|
|
4095
|
+
if (entry.workspaceRoot !== void 0) {
|
|
4096
|
+
absoluteField(entry.workspaceRoot, `${at}.workspaceRoot`);
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
function readHostConfig(env = process.env, home) {
|
|
4101
|
+
const path4 = readableHostConfigPath(env, home);
|
|
4102
|
+
if (!existsSync16(path4)) return { version: 1 };
|
|
4103
|
+
let raw;
|
|
4104
|
+
try {
|
|
4105
|
+
raw = readFileSync11(path4, "utf8");
|
|
4106
|
+
} catch (error) {
|
|
4107
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4108
|
+
throw new Error(`could not read ${path4}: ${detail}`);
|
|
4109
|
+
}
|
|
4110
|
+
let parsed;
|
|
4111
|
+
try {
|
|
4112
|
+
parsed = JSON.parse(raw);
|
|
4113
|
+
} catch {
|
|
4114
|
+
throw new Error(`${basename6(path4)} is not valid JSON: ${path4}`);
|
|
4115
|
+
}
|
|
4116
|
+
return validateHostConfig(parsed);
|
|
4117
|
+
}
|
|
4118
|
+
function writeHostConfig(config, env = process.env, home) {
|
|
4119
|
+
const path4 = hostConfigPath(env, home);
|
|
4120
|
+
const validated = validateHostConfig({ ...config, version: 1 });
|
|
4121
|
+
const body = `${JSON.stringify(validated, null, 2)}
|
|
4122
|
+
`;
|
|
4123
|
+
const tmp = `${path4}.tmp.${process.pid}`;
|
|
4124
|
+
mkdirSync7(dirname8(path4), { recursive: true });
|
|
4125
|
+
try {
|
|
4126
|
+
writeFileSync8(tmp, body, { mode: HOST_CONFIG_MODE, encoding: "utf8" });
|
|
4127
|
+
renameSync5(tmp, path4);
|
|
4128
|
+
retireLegacyHostConfig(env, home, path4);
|
|
4129
|
+
} catch (error) {
|
|
4130
|
+
try {
|
|
4131
|
+
unlinkSync5(tmp);
|
|
4132
|
+
} catch {
|
|
4133
|
+
}
|
|
4134
|
+
throw error;
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
function resolveHarnessRoot(explicit, env = process.env, home) {
|
|
4138
|
+
if (typeof explicit === "string" && explicit !== "") return resolve10(explicit);
|
|
4139
|
+
const configured = readHostConfig(env, home).harnessRoot;
|
|
4140
|
+
if (typeof configured === "string" && configured !== "") return resolve10(configured);
|
|
4141
|
+
return defaultHarnessRoot(env, home);
|
|
4142
|
+
}
|
|
4143
|
+
function retireLegacyHostConfig(env, home, written) {
|
|
4144
|
+
const legacy = legacyHostConfigPath(env, home);
|
|
4145
|
+
if (legacy === written || !existsSync16(legacy)) return;
|
|
4146
|
+
unlinkSync5(legacy);
|
|
4147
|
+
}
|
|
4148
|
+
function fieldError2(path4, requirement) {
|
|
4149
|
+
return new Error(`${GENERATIONS.agro.configFile}: ${path4} ${requirement}`);
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
// src/lib/host-workspace.ts
|
|
4153
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, readdirSync as readdirSync4, renameSync as renameSync6, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
|
|
4154
|
+
import { dirname as dirname9, join as join11, resolve as resolve11 } from "node:path";
|
|
4155
|
+
var AGRO_REPO_URL = "https://github.com/mifunedev/agro.git";
|
|
4156
|
+
var IGNORABLE_ENTRIES = [
|
|
4157
|
+
REGISTRY_SUBDIR,
|
|
4158
|
+
"escalate",
|
|
4159
|
+
GENERATIONS.agro.configFile,
|
|
4160
|
+
GENERATIONS.legacy.configFile
|
|
4161
|
+
];
|
|
4162
|
+
function isIgnorableEntry(name) {
|
|
4163
|
+
return name.startsWith(".") || IGNORABLE_ENTRIES.includes(name);
|
|
4164
|
+
}
|
|
4165
|
+
function assertCloned(result, root) {
|
|
4166
|
+
const status = result.error ? result.error.code ?? "spawn failed" : result.status;
|
|
4167
|
+
if (result.error || result.status !== 0) {
|
|
4168
|
+
throw new Error(`could not clone ${AGRO_REPO_URL} into ${root} (exit status ${String(status)})`);
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
function cloneInto(target, run, root) {
|
|
4172
|
+
assertCloned(run("git", ["clone", AGRO_REPO_URL, target], { stdio: "inherit" }), root);
|
|
4173
|
+
}
|
|
4174
|
+
function cloneThroughStaging(root, run) {
|
|
4175
|
+
const staging = mkdtempSync3(join11(dirname9(root), "agro-clone-"));
|
|
4176
|
+
try {
|
|
4177
|
+
const checkout = join11(staging, "agro");
|
|
4178
|
+
cloneInto(checkout, run, root);
|
|
4179
|
+
for (const name of readdirSync4(checkout)) {
|
|
4180
|
+
const destination = join11(root, name);
|
|
4181
|
+
if (existsSync17(destination)) {
|
|
4182
|
+
throw new Error(`could not move ${name} into ${root} \u2014 the path already exists`);
|
|
4183
|
+
}
|
|
4184
|
+
renameSync6(join11(checkout, name), destination);
|
|
4185
|
+
}
|
|
4186
|
+
} finally {
|
|
4187
|
+
rmSync3(staging, { recursive: true, force: true });
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
function ensureHostWorkspace(path4, run = spawnRunner) {
|
|
4191
|
+
const root = resolve11(path4);
|
|
4192
|
+
if (existsSync17(join11(root, ".git"))) return { root, action: "reused" };
|
|
4193
|
+
const stats = statSync5(root, { throwIfNoEntry: false });
|
|
4194
|
+
if (stats === void 0) {
|
|
4195
|
+
mkdirSync8(dirname9(root), { recursive: true });
|
|
4196
|
+
cloneInto(root, run, root);
|
|
4197
|
+
return { root, action: "cloned" };
|
|
4198
|
+
}
|
|
4199
|
+
if (!stats.isDirectory()) {
|
|
4200
|
+
throw new Error(`${root} exists and is not a directory \u2014 choose another harness root`);
|
|
4201
|
+
}
|
|
4202
|
+
const blocking = readdirSync4(root).filter((name) => !isIgnorableEntry(name));
|
|
4203
|
+
if (blocking.length > 0) {
|
|
4204
|
+
throw new Error(
|
|
4205
|
+
`${root} holds files but no git checkout (${blocking.sort().join(", ")}) \u2014 choose an empty directory or an existing AGRO checkout`
|
|
4206
|
+
);
|
|
4207
|
+
}
|
|
4208
|
+
cloneThroughStaging(root, run);
|
|
4209
|
+
return { root, action: "cloned" };
|
|
4210
|
+
}
|
|
4211
|
+
|
|
3997
4212
|
// src/lib/harnesses/catalog.ts
|
|
4213
|
+
var SANDBOX_HARNESS_PREFIX = "/home/sandbox/.local";
|
|
4214
|
+
var HARNESS_PREFIX_TOKEN = "{{prefix}}";
|
|
3998
4215
|
var HARNESS_CATALOG = [
|
|
3999
4216
|
{
|
|
4000
4217
|
id: "claude-code",
|
|
@@ -4003,13 +4220,14 @@ var HARNESS_CATALOG = [
|
|
|
4003
4220
|
installArgv: [
|
|
4004
4221
|
"npm",
|
|
4005
4222
|
"--prefix",
|
|
4006
|
-
|
|
4223
|
+
HARNESS_PREFIX_TOKEN,
|
|
4007
4224
|
"install",
|
|
4008
4225
|
"-g",
|
|
4009
4226
|
"@anthropic-ai/claude-code"
|
|
4010
4227
|
],
|
|
4011
4228
|
installUser: "sandbox",
|
|
4012
4229
|
verifyArgv: ["claude", "--version"],
|
|
4230
|
+
uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "@anthropic-ai/claude-code"],
|
|
4013
4231
|
docsPath: "docs/harnesses/claude-code.md",
|
|
4014
4232
|
kind: "installable"
|
|
4015
4233
|
},
|
|
@@ -4020,13 +4238,14 @@ var HARNESS_CATALOG = [
|
|
|
4020
4238
|
installArgv: [
|
|
4021
4239
|
"npm",
|
|
4022
4240
|
"--prefix",
|
|
4023
|
-
|
|
4241
|
+
HARNESS_PREFIX_TOKEN,
|
|
4024
4242
|
"install",
|
|
4025
4243
|
"-g",
|
|
4026
4244
|
"@openai/codex"
|
|
4027
4245
|
],
|
|
4028
4246
|
installUser: "sandbox",
|
|
4029
4247
|
verifyArgv: ["codex", "--version"],
|
|
4248
|
+
uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "@openai/codex"],
|
|
4030
4249
|
docsPath: "docs/harnesses/codex.md",
|
|
4031
4250
|
kind: "installable"
|
|
4032
4251
|
},
|
|
@@ -4037,7 +4256,7 @@ var HARNESS_CATALOG = [
|
|
|
4037
4256
|
installArgv: [
|
|
4038
4257
|
"npm",
|
|
4039
4258
|
"--prefix",
|
|
4040
|
-
|
|
4259
|
+
HARNESS_PREFIX_TOKEN,
|
|
4041
4260
|
"install",
|
|
4042
4261
|
"-g",
|
|
4043
4262
|
"--ignore-scripts",
|
|
@@ -4045,6 +4264,7 @@ var HARNESS_CATALOG = [
|
|
|
4045
4264
|
],
|
|
4046
4265
|
installUser: "sandbox",
|
|
4047
4266
|
verifyArgv: ["pi", "--version"],
|
|
4267
|
+
uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "@earendil-works/pi-coding-agent"],
|
|
4048
4268
|
docsPath: "docs/harnesses/pi.md",
|
|
4049
4269
|
kind: "installable"
|
|
4050
4270
|
},
|
|
@@ -4055,13 +4275,14 @@ var HARNESS_CATALOG = [
|
|
|
4055
4275
|
installArgv: [
|
|
4056
4276
|
"npm",
|
|
4057
4277
|
"--prefix",
|
|
4058
|
-
|
|
4278
|
+
HARNESS_PREFIX_TOKEN,
|
|
4059
4279
|
"install",
|
|
4060
4280
|
"-g",
|
|
4061
4281
|
"opencode-ai"
|
|
4062
4282
|
],
|
|
4063
4283
|
installUser: "sandbox",
|
|
4064
4284
|
verifyArgv: ["opencode", "--version"],
|
|
4285
|
+
uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "opencode-ai"],
|
|
4065
4286
|
docsPath: "docs/harnesses/opencode.md",
|
|
4066
4287
|
kind: "installable"
|
|
4067
4288
|
},
|
|
@@ -4072,10 +4293,11 @@ var HARNESS_CATALOG = [
|
|
|
4072
4293
|
installArgv: [
|
|
4073
4294
|
"bash",
|
|
4074
4295
|
"-lc",
|
|
4075
|
-
|
|
4296
|
+
`curl -fsSL https://x.ai/cli/install.sh | GROK_BIN_DIR="${HARNESS_PREFIX_TOKEN}/bin" bash -s 0.2.39 && rm -f "${HARNESS_PREFIX_TOKEN}/bin/agent"`
|
|
4076
4297
|
],
|
|
4077
4298
|
installUser: "sandbox",
|
|
4078
4299
|
verifyArgv: ["grok", "--version"],
|
|
4300
|
+
uninstallArgv: ["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/grok`],
|
|
4079
4301
|
docsPath: "docs/harnesses/grok-build.md",
|
|
4080
4302
|
kind: "installable"
|
|
4081
4303
|
},
|
|
@@ -4086,10 +4308,11 @@ var HARNESS_CATALOG = [
|
|
|
4086
4308
|
installArgv: [
|
|
4087
4309
|
"bash",
|
|
4088
4310
|
"-lc",
|
|
4089
|
-
`curl -fsSL https://hermes-agent.nousresearch.com/install.sh | HERMES_INSTALL_DIR="$
|
|
4311
|
+
`curl -fsSL https://hermes-agent.nousresearch.com/install.sh | HERMES_INSTALL_DIR="${HARNESS_PREFIX_TOKEN}/lib/hermes-agent" bash -s -- --skip-setup --skip-browser && uv pip install --python "${HARNESS_PREFIX_TOKEN}/lib/hermes-agent/venv/bin/python" 'hermes-agent[slack,teams,web,pty]'`
|
|
4090
4312
|
],
|
|
4091
4313
|
installUser: "sandbox",
|
|
4092
4314
|
verifyArgv: ["hermes", "--version"],
|
|
4315
|
+
uninstallArgv: ["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/hermes`, `${HARNESS_PREFIX_TOKEN}/lib/hermes-agent`],
|
|
4093
4316
|
docsPath: "docs/harnesses/hermes.md",
|
|
4094
4317
|
kind: "installable"
|
|
4095
4318
|
},
|
|
@@ -4100,10 +4323,11 @@ var HARNESS_CATALOG = [
|
|
|
4100
4323
|
installArgv: [
|
|
4101
4324
|
"bash",
|
|
4102
4325
|
"-lc",
|
|
4103
|
-
|
|
4326
|
+
`set -o pipefail; curl -fsSL https://dev.meta.ai/install.sh | MUSE_INSTALL_DIR="${HARNESS_PREFIX_TOKEN}/bin" MUSE_NO_MODIFY_PATH=1 MUSE_LOGIN=0 bash`
|
|
4104
4327
|
],
|
|
4105
4328
|
installUser: "sandbox",
|
|
4106
4329
|
verifyArgv: ["muse", "--version"],
|
|
4330
|
+
uninstallArgv: ["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/muse`],
|
|
4107
4331
|
docsPath: "docs/harnesses/muse-code.md",
|
|
4108
4332
|
kind: "installable"
|
|
4109
4333
|
},
|
|
@@ -4114,10 +4338,11 @@ var HARNESS_CATALOG = [
|
|
|
4114
4338
|
installArgv: [
|
|
4115
4339
|
"bash",
|
|
4116
4340
|
"-lc",
|
|
4117
|
-
|
|
4341
|
+
`set -o pipefail; curl -fsSL https://antigravity.google/cli/install.sh | bash -s -- --dir "${HARNESS_PREFIX_TOKEN}/bin"`
|
|
4118
4342
|
],
|
|
4119
4343
|
installUser: "sandbox",
|
|
4120
4344
|
verifyArgv: ["agy", "--version"],
|
|
4345
|
+
uninstallArgv: ["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/agy`],
|
|
4121
4346
|
docsPath: "docs/harnesses/antigravity-cli.md",
|
|
4122
4347
|
kind: "installable"
|
|
4123
4348
|
},
|
|
@@ -4128,6 +4353,7 @@ var HARNESS_CATALOG = [
|
|
|
4128
4353
|
installArgv: ["npx", "--yes", "t3", "--version"],
|
|
4129
4354
|
installUser: "sandbox",
|
|
4130
4355
|
verifyArgv: ["npx", "--no-install", "t3", "--version"],
|
|
4356
|
+
uninstallArgv: null,
|
|
4131
4357
|
docsPath: "docs/harnesses/t3code.md",
|
|
4132
4358
|
kind: "on-demand"
|
|
4133
4359
|
}
|
|
@@ -4138,6 +4364,21 @@ function findHarness(id) {
|
|
|
4138
4364
|
function harnessIds() {
|
|
4139
4365
|
return HARNESS_CATALOG.map((h) => h.id);
|
|
4140
4366
|
}
|
|
4367
|
+
function substitute(argv, prefix) {
|
|
4368
|
+
return argv.map((part) => part.split(HARNESS_PREFIX_TOKEN).join(prefix));
|
|
4369
|
+
}
|
|
4370
|
+
function resolveInstallArgv(entry, prefix) {
|
|
4371
|
+
return substitute(entry.installArgv, prefix);
|
|
4372
|
+
}
|
|
4373
|
+
function resolveVerifyArgv(entry, prefix) {
|
|
4374
|
+
return substitute(entry.verifyArgv, prefix);
|
|
4375
|
+
}
|
|
4376
|
+
function resolveUninstallArgv(entry, prefix) {
|
|
4377
|
+
return entry.uninstallArgv === null ? null : substitute(entry.uninstallArgv, prefix);
|
|
4378
|
+
}
|
|
4379
|
+
function harnessBinPath(prefix) {
|
|
4380
|
+
return `${prefix}/bin`;
|
|
4381
|
+
}
|
|
4141
4382
|
|
|
4142
4383
|
// src/commands/harness.ts
|
|
4143
4384
|
var PROBE_TIMEOUT_MS = 15e3;
|
|
@@ -4153,11 +4394,31 @@ function targetFor(root, run, env) {
|
|
|
4153
4394
|
...env ? { env } : {}
|
|
4154
4395
|
});
|
|
4155
4396
|
}
|
|
4156
|
-
|
|
4397
|
+
function hostPrefix(home) {
|
|
4398
|
+
return join12(home, ".local");
|
|
4399
|
+
}
|
|
4400
|
+
function pathEntries(env) {
|
|
4401
|
+
return (env.PATH ?? "").split(delimiter2).filter((part) => part !== "");
|
|
4402
|
+
}
|
|
4403
|
+
function onPath(prefix, env) {
|
|
4404
|
+
return pathEntries(env).includes(harnessBinPath(prefix));
|
|
4405
|
+
}
|
|
4406
|
+
function hostTargetFor(root, prefix, run, env) {
|
|
4407
|
+
const path4 = [harnessBinPath(prefix), ...pathEntries(env)].join(delimiter2);
|
|
4408
|
+
return new LocalExecutionTarget({
|
|
4409
|
+
projectRoot: root,
|
|
4410
|
+
run,
|
|
4411
|
+
env: { ...env, PATH: path4 }
|
|
4412
|
+
});
|
|
4413
|
+
}
|
|
4414
|
+
function messageOf(error) {
|
|
4415
|
+
return error instanceof Error ? error.message : String(error);
|
|
4416
|
+
}
|
|
4417
|
+
async function probeInstalled(target, entry, prefix, user, env) {
|
|
4157
4418
|
try {
|
|
4158
4419
|
const r = await target.exec({
|
|
4159
|
-
argv:
|
|
4160
|
-
user:
|
|
4420
|
+
argv: resolveVerifyArgv(entry, prefix),
|
|
4421
|
+
...user ? { user } : {},
|
|
4161
4422
|
stdio: "capture",
|
|
4162
4423
|
timeoutMs: PROBE_TIMEOUT_MS,
|
|
4163
4424
|
...env ? { env } : {}
|
|
@@ -4168,33 +4429,58 @@ async function probeInstalled(target, entry, env) {
|
|
|
4168
4429
|
throw err;
|
|
4169
4430
|
}
|
|
4170
4431
|
}
|
|
4171
|
-
|
|
4432
|
+
function stateOf(entry, installed, location) {
|
|
4433
|
+
return {
|
|
4434
|
+
id: entry.id,
|
|
4435
|
+
title: entry.title,
|
|
4436
|
+
binary: entry.binary,
|
|
4437
|
+
kind: entry.kind,
|
|
4438
|
+
installed,
|
|
4439
|
+
location,
|
|
4440
|
+
docs: sourceDocsUrl(entry.docsPath)
|
|
4441
|
+
};
|
|
4442
|
+
}
|
|
4443
|
+
function probeableHost(env, home) {
|
|
4444
|
+
let root;
|
|
4445
|
+
try {
|
|
4446
|
+
root = resolveHarnessRoot(void 0, env);
|
|
4447
|
+
} catch {
|
|
4448
|
+
return void 0;
|
|
4449
|
+
}
|
|
4450
|
+
const prefix = hostPrefix(home);
|
|
4451
|
+
if (!existsSync18(join12(root, ".git")) && !existsSync18(prefix)) return void 0;
|
|
4452
|
+
return { root, prefix };
|
|
4453
|
+
}
|
|
4454
|
+
async function collectStates(root, run, home, env, only) {
|
|
4172
4455
|
const entries = only ? [...only] : [...HARNESS_CATALOG];
|
|
4173
4456
|
const target = targetFor(root, run, env);
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4457
|
+
const reachable = isReachable(await resolveTargetStatus(target));
|
|
4458
|
+
if (reachable) {
|
|
4459
|
+
const states2 = [];
|
|
4460
|
+
for (const entry of entries) {
|
|
4461
|
+
const installed = await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox");
|
|
4462
|
+
states2.push(stateOf(entry, installed, installed === null ? "unknown" : "sandbox"));
|
|
4463
|
+
}
|
|
4464
|
+
return { states: states2 };
|
|
4465
|
+
}
|
|
4466
|
+
const host = probeableHost(env ?? process.env, home);
|
|
4467
|
+
if (host === void 0) {
|
|
4468
|
+
return { states: entries.map((entry) => stateOf(entry, null, "unknown")) };
|
|
4179
4469
|
}
|
|
4470
|
+
const hostTarget = hostTargetFor(host.root, host.prefix, run, env ?? process.env);
|
|
4180
4471
|
const states = [];
|
|
4181
4472
|
for (const entry of entries) {
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
title: entry.title,
|
|
4185
|
-
binary: entry.binary,
|
|
4186
|
-
kind: entry.kind,
|
|
4187
|
-
installed: reachable ? await probeInstalled(target, entry) : null,
|
|
4188
|
-
docs: sourceDocsUrl(entry.docsPath)
|
|
4189
|
-
});
|
|
4473
|
+
const installed = await probeInstalled(hostTarget, entry, host.prefix, void 0);
|
|
4474
|
+
states.push(stateOf(entry, installed, installed === null ? "unknown" : "host"));
|
|
4190
4475
|
}
|
|
4191
|
-
return states;
|
|
4476
|
+
return { states, hostPrefix: host.prefix };
|
|
4192
4477
|
}
|
|
4193
4478
|
function cell(value, absent) {
|
|
4194
4479
|
if (value === null) return absent;
|
|
4195
4480
|
return value ? "yes" : "no";
|
|
4196
4481
|
}
|
|
4197
|
-
function renderTable(
|
|
4482
|
+
function renderTable(collected, io, bin) {
|
|
4483
|
+
const states = collected.states;
|
|
4198
4484
|
const header2 = ["HARNESS", "KIND", "INSTALLED"];
|
|
4199
4485
|
const rows = states.map((s) => [s.id, s.kind, cell(s.installed, "?")]);
|
|
4200
4486
|
const widths = header2.map(
|
|
@@ -4203,6 +4489,14 @@ function renderTable(states, io, bin) {
|
|
|
4203
4489
|
const line = (cols) => cols.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd() + "\n";
|
|
4204
4490
|
io.stdout(line(header2));
|
|
4205
4491
|
for (const row of rows) io.stdout(line(row));
|
|
4492
|
+
if (states.some((s) => s.location === "host")) {
|
|
4493
|
+
io.stdout(
|
|
4494
|
+
`
|
|
4495
|
+
INSTALLED reports the host prefix ${collected.hostPrefix} \u2014 the sandbox is not running.
|
|
4496
|
+
`
|
|
4497
|
+
);
|
|
4498
|
+
return;
|
|
4499
|
+
}
|
|
4206
4500
|
if (states.some((s) => s.installed === null)) {
|
|
4207
4501
|
io.stdout(`
|
|
4208
4502
|
INSTALLED is \`?\` \u2014 the sandbox is not running. Start it with \`${bin} sandbox\`.
|
|
@@ -4212,12 +4506,12 @@ INSTALLED is \`?\` \u2014 the sandbox is not running. Start it with \`${bin} san
|
|
|
4212
4506
|
async function runHarnessList(opts, io) {
|
|
4213
4507
|
const run = opts.run ?? spawnRunner;
|
|
4214
4508
|
const root = resolveProjectRoot(opts.cwd);
|
|
4215
|
-
const
|
|
4509
|
+
const collected = await collectStates(root, run, homeOf(opts), opts.env);
|
|
4216
4510
|
if (opts.json) {
|
|
4217
|
-
io.stdout(`${JSON.stringify(states, null, 2)}
|
|
4511
|
+
io.stdout(`${JSON.stringify(collected.states, null, 2)}
|
|
4218
4512
|
`);
|
|
4219
4513
|
} else {
|
|
4220
|
-
renderTable(
|
|
4514
|
+
renderTable(collected, io, opts.bin);
|
|
4221
4515
|
}
|
|
4222
4516
|
return 0;
|
|
4223
4517
|
}
|
|
@@ -4238,24 +4532,23 @@ async function runHarnessStatus(name, opts, io) {
|
|
|
4238
4532
|
only = findHarness(name);
|
|
4239
4533
|
if (!only) return unknownHarness(name, io, opts.bin);
|
|
4240
4534
|
}
|
|
4241
|
-
const
|
|
4535
|
+
const collected = await collectStates(root, run, homeOf(opts), opts.env, only ? [only] : void 0);
|
|
4242
4536
|
if (opts.json) {
|
|
4243
|
-
io.stdout(`${JSON.stringify(only ? states[0] : states, null, 2)}
|
|
4537
|
+
io.stdout(`${JSON.stringify(only ? collected.states[0] : collected.states, null, 2)}
|
|
4244
4538
|
`);
|
|
4245
4539
|
} else {
|
|
4246
|
-
renderTable(
|
|
4540
|
+
renderTable(collected, io, opts.bin);
|
|
4247
4541
|
}
|
|
4248
4542
|
return 0;
|
|
4249
4543
|
}
|
|
4250
4544
|
function hermesTargetRoot(target) {
|
|
4251
4545
|
return target.kind === "docker-compose" ? "/home/sandbox/harness" : target.workspace.targetRoot;
|
|
4252
4546
|
}
|
|
4253
|
-
async function reconcileHermes(target, io, bin) {
|
|
4254
|
-
const root = hermesTargetRoot(target);
|
|
4547
|
+
async function reconcileHermes(target, io, bin, user, root = hermesTargetRoot(target)) {
|
|
4255
4548
|
const result = await target.exec({
|
|
4256
4549
|
argv: remoteControlDirScript(root, "scripts/link-providers.sh", ["--init", "--hermes-only"]),
|
|
4257
4550
|
env: aliasedEnvPair("PROJECT_ROOT", root),
|
|
4258
|
-
user:
|
|
4551
|
+
...user ? { user } : {},
|
|
4259
4552
|
stdio: "inherit"
|
|
4260
4553
|
});
|
|
4261
4554
|
if (result.exitCode !== 0) {
|
|
@@ -4264,6 +4557,245 @@ async function reconcileHermes(target, io, bin) {
|
|
|
4264
4557
|
}
|
|
4265
4558
|
return result.exitCode;
|
|
4266
4559
|
}
|
|
4560
|
+
function unreachableReason(status) {
|
|
4561
|
+
return runtimeIsAbsent(status) ? "No container runtime is on PATH." : `The sandbox is not running (${status}).`;
|
|
4562
|
+
}
|
|
4563
|
+
function sandboxRefusal(bin, status) {
|
|
4564
|
+
if (runtimeIsAbsent(status)) {
|
|
4565
|
+
return `${bin} harness: no container runtime is on PATH.
|
|
4566
|
+
`;
|
|
4567
|
+
}
|
|
4568
|
+
return `${bin} harness: the sandbox is not running (${status}).
|
|
4569
|
+
Start it with \`${bin} sandbox\`, then re-run this command.
|
|
4570
|
+
`;
|
|
4571
|
+
}
|
|
4572
|
+
function homeOf(opts) {
|
|
4573
|
+
return (opts.homedir ?? homedir2)();
|
|
4574
|
+
}
|
|
4575
|
+
function isInteractive(opts) {
|
|
4576
|
+
return opts.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true);
|
|
4577
|
+
}
|
|
4578
|
+
async function installOnHost(entry, opts, io, run, status) {
|
|
4579
|
+
const bin = opts.bin;
|
|
4580
|
+
const env = opts.env ?? process.env;
|
|
4581
|
+
const flagged = opts.host === true || opts.path !== void 0;
|
|
4582
|
+
if (!flagged && !isInteractive(opts)) {
|
|
4583
|
+
io.stderr(
|
|
4584
|
+
sandboxRefusal(bin, status) + `Or install on the host with \`${bin} harness install ${entry.id} --host\`.
|
|
4585
|
+
`
|
|
4586
|
+
);
|
|
4587
|
+
return 1;
|
|
4588
|
+
}
|
|
4589
|
+
if (entry.kind === "on-demand") {
|
|
4590
|
+
io.stdout(`${entry.id}: no installation is needed \u2014 npx fetches it at each run
|
|
4591
|
+
`);
|
|
4592
|
+
return 0;
|
|
4593
|
+
}
|
|
4594
|
+
let root;
|
|
4595
|
+
let recorded;
|
|
4596
|
+
try {
|
|
4597
|
+
recorded = readHostConfig(env).harnessRoot;
|
|
4598
|
+
root = resolveHarnessRoot(opts.path, env);
|
|
4599
|
+
} catch (err) {
|
|
4600
|
+
io.stderr(`${bin} harness: ${messageOf(err)}
|
|
4601
|
+
`);
|
|
4602
|
+
return 1;
|
|
4603
|
+
}
|
|
4604
|
+
const sticky = opts.path === void 0 && recorded !== void 0 && recorded !== "";
|
|
4605
|
+
if (!flagged) {
|
|
4606
|
+
const ask2 = io.ask ?? ask;
|
|
4607
|
+
const answer = (await ask2(
|
|
4608
|
+
`${unreachableReason(status)} Install ${entry.title} on the host? [y/N]`
|
|
4609
|
+
)).trim().toLowerCase();
|
|
4610
|
+
if (!/^y/.test(answer)) {
|
|
4611
|
+
io.stderr(sandboxRefusal(bin, status));
|
|
4612
|
+
return 1;
|
|
4613
|
+
}
|
|
4614
|
+
if (!sticky) {
|
|
4615
|
+
const chosen = (await ask2(`Harness root [${root}]:`)).trim();
|
|
4616
|
+
if (chosen !== "") root = resolve12(chosen);
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
if (sticky) io.stdout(`using the recorded harness root ${root}
|
|
4620
|
+
`);
|
|
4621
|
+
try {
|
|
4622
|
+
if (!existsSync18(join12(root, ".git"))) io.stdout(`cloning ${AGRO_REPO_URL} into ${root}\u2026
|
|
4623
|
+
`);
|
|
4624
|
+
const workspace = ensureHostWorkspace(root, run);
|
|
4625
|
+
root = workspace.root;
|
|
4626
|
+
io.stdout(
|
|
4627
|
+
workspace.action === "cloned" ? `host workspace cloned into ${root}
|
|
4628
|
+
` : `host workspace reused at ${root}
|
|
4629
|
+
`
|
|
4630
|
+
);
|
|
4631
|
+
} catch (err) {
|
|
4632
|
+
io.stderr(`${bin} harness: ${messageOf(err)}
|
|
4633
|
+
`);
|
|
4634
|
+
return 1;
|
|
4635
|
+
}
|
|
4636
|
+
const prefix = hostPrefix(homeOf(opts));
|
|
4637
|
+
const target = hostTargetFor(root, prefix, run, env);
|
|
4638
|
+
const hermes = entry.id === "hermes";
|
|
4639
|
+
const installEnv = hermes ? {
|
|
4640
|
+
...aliasedEnvPair("PROJECT_ROOT", root),
|
|
4641
|
+
HERMES_HOME: `${root}/.hermes`
|
|
4642
|
+
} : void 0;
|
|
4643
|
+
if (hermes) {
|
|
4644
|
+
const code = await reconcileHermes(target, io, bin, void 0, root);
|
|
4645
|
+
if (code !== 0) return code;
|
|
4646
|
+
}
|
|
4647
|
+
if (await probeInstalled(target, entry, prefix, void 0, installEnv) === true) {
|
|
4648
|
+
io.stdout(`${entry.id}: already installed (${entry.binary})
|
|
4649
|
+
`);
|
|
4650
|
+
return 0;
|
|
4651
|
+
}
|
|
4652
|
+
io.stdout(`installing ${entry.title} on the host\u2026
|
|
4653
|
+
`);
|
|
4654
|
+
const r = await target.exec({
|
|
4655
|
+
argv: resolveInstallArgv(entry, prefix),
|
|
4656
|
+
stdio: "inherit",
|
|
4657
|
+
...installEnv ? { env: installEnv } : {}
|
|
4658
|
+
});
|
|
4659
|
+
if (r.exitCode !== 0) {
|
|
4660
|
+
io.stderr(`${bin} harness: installing ${entry.id} failed (exit ${r.exitCode}).
|
|
4661
|
+
`);
|
|
4662
|
+
return r.exitCode;
|
|
4663
|
+
}
|
|
4664
|
+
if (hermes) {
|
|
4665
|
+
if (await probeInstalled(target, entry, prefix, void 0, installEnv) !== true) {
|
|
4666
|
+
io.stderr(`${bin} harness: Hermes installation finished but executable verification failed.
|
|
4667
|
+
`);
|
|
4668
|
+
return 1;
|
|
4669
|
+
}
|
|
4670
|
+
const code = await reconcileHermes(target, io, bin, void 0, root);
|
|
4671
|
+
if (code !== 0) return code;
|
|
4672
|
+
}
|
|
4673
|
+
const receipt = {
|
|
4674
|
+
prefix,
|
|
4675
|
+
binary: entry.binary,
|
|
4676
|
+
binPath: harnessBinPath(prefix),
|
|
4677
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4678
|
+
workspaceRoot: root
|
|
4679
|
+
};
|
|
4680
|
+
try {
|
|
4681
|
+
const config = readHostConfig(env);
|
|
4682
|
+
writeHostConfig(
|
|
4683
|
+
{
|
|
4684
|
+
...config,
|
|
4685
|
+
harnessRoot: root,
|
|
4686
|
+
hostHarnesses: { ...config.hostHarnesses ?? {}, [entry.id]: receipt }
|
|
4687
|
+
},
|
|
4688
|
+
env
|
|
4689
|
+
);
|
|
4690
|
+
} catch (err) {
|
|
4691
|
+
io.stderr(`${bin} harness: could not record the install: ${messageOf(err)}
|
|
4692
|
+
`);
|
|
4693
|
+
return 1;
|
|
4694
|
+
}
|
|
4695
|
+
io.stdout(`${entry.id}: installed at ${prefix} \u2014 see ${sourceDocsUrl(entry.docsPath)} for authentication
|
|
4696
|
+
`);
|
|
4697
|
+
if (!onPath(prefix, env)) {
|
|
4698
|
+
io.stdout(`Add this line to your shell profile: export PATH="${harnessBinPath(prefix)}:$PATH"
|
|
4699
|
+
`);
|
|
4700
|
+
}
|
|
4701
|
+
io.stdout(`Run ${entry.binary} from the AGRO workspace: cd ${root}
|
|
4702
|
+
`);
|
|
4703
|
+
return 0;
|
|
4704
|
+
}
|
|
4705
|
+
async function removeHarness(entry, target, prefix, user, opts, io) {
|
|
4706
|
+
const argv = resolveUninstallArgv(entry, prefix);
|
|
4707
|
+
if (argv === null) {
|
|
4708
|
+
io.stdout(`${entry.id}: nothing to remove \u2014 npx fetches it at each run
|
|
4709
|
+
`);
|
|
4710
|
+
return { code: 0, dropReceipt: false };
|
|
4711
|
+
}
|
|
4712
|
+
if (await probeInstalled(target, entry, prefix, user) !== true) {
|
|
4713
|
+
io.stdout(`${entry.id}: not installed (${entry.binary})
|
|
4714
|
+
`);
|
|
4715
|
+
return { code: 0, dropReceipt: true };
|
|
4716
|
+
}
|
|
4717
|
+
if (isInteractive(opts)) {
|
|
4718
|
+
const ask2 = io.ask ?? ask;
|
|
4719
|
+
const answer = (await ask2(`Remove ${entry.title} from ${prefix}? [y/N]`)).trim().toLowerCase();
|
|
4720
|
+
if (!/^y/.test(answer)) {
|
|
4721
|
+
io.stderr(`${opts.bin} harness: removed nothing.
|
|
4722
|
+
`);
|
|
4723
|
+
return { code: 1, dropReceipt: false };
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
io.stdout(`removing ${entry.title} from ${prefix}\u2026
|
|
4727
|
+
`);
|
|
4728
|
+
const r = await target.exec({
|
|
4729
|
+
argv,
|
|
4730
|
+
...user ? { user } : {},
|
|
4731
|
+
stdio: "inherit"
|
|
4732
|
+
});
|
|
4733
|
+
if (r.exitCode !== 0) {
|
|
4734
|
+
io.stderr(`${opts.bin} harness: removing ${entry.id} failed (exit ${r.exitCode}).
|
|
4735
|
+
`);
|
|
4736
|
+
return { code: r.exitCode, dropReceipt: false };
|
|
4737
|
+
}
|
|
4738
|
+
io.stdout(`${entry.id}: removed from ${prefix}
|
|
4739
|
+
`);
|
|
4740
|
+
return { code: 0, dropReceipt: true };
|
|
4741
|
+
}
|
|
4742
|
+
async function uninstallOnHost(entry, opts, io, run) {
|
|
4743
|
+
const bin = opts.bin;
|
|
4744
|
+
const env = opts.env ?? process.env;
|
|
4745
|
+
const computed = hostPrefix(homeOf(opts));
|
|
4746
|
+
if (resolveUninstallArgv(entry, computed) === null) {
|
|
4747
|
+
io.stdout(`${entry.id}: nothing to remove \u2014 npx fetches it at each run
|
|
4748
|
+
`);
|
|
4749
|
+
return 0;
|
|
4750
|
+
}
|
|
4751
|
+
let config;
|
|
4752
|
+
let workspace;
|
|
4753
|
+
try {
|
|
4754
|
+
config = readHostConfig(env);
|
|
4755
|
+
workspace = resolveHarnessRoot(void 0, env);
|
|
4756
|
+
} catch (err) {
|
|
4757
|
+
io.stderr(`${bin} harness: ${messageOf(err)}
|
|
4758
|
+
`);
|
|
4759
|
+
return 1;
|
|
4760
|
+
}
|
|
4761
|
+
const receipt = config.hostHarnesses?.[entry.id];
|
|
4762
|
+
if (receipt === void 0 && opts.force !== true) {
|
|
4763
|
+
io.stderr(
|
|
4764
|
+
`${bin} harness: no record of installing ${entry.id} on this host.
|
|
4765
|
+
Removing it could delete a harness you installed yourself. Re-run with \`--force\` to remove it from ${computed}.
|
|
4766
|
+
`
|
|
4767
|
+
);
|
|
4768
|
+
return 1;
|
|
4769
|
+
}
|
|
4770
|
+
const prefix = receipt?.prefix ?? computed;
|
|
4771
|
+
const target = hostTargetFor(receipt?.workspaceRoot ?? workspace, prefix, run, env);
|
|
4772
|
+
const outcome = await removeHarness(entry, target, prefix, void 0, opts, io);
|
|
4773
|
+
if (outcome.dropReceipt && receipt !== void 0) {
|
|
4774
|
+
const remaining = { ...config.hostHarnesses ?? {} };
|
|
4775
|
+
delete remaining[entry.id];
|
|
4776
|
+
try {
|
|
4777
|
+
writeHostConfig({ ...config, hostHarnesses: remaining }, env);
|
|
4778
|
+
} catch (err) {
|
|
4779
|
+
io.stderr(`${bin} harness: could not clear the install record: ${messageOf(err)}
|
|
4780
|
+
`);
|
|
4781
|
+
return 1;
|
|
4782
|
+
}
|
|
4783
|
+
io.stdout(`${entry.id}: cleared the host install record
|
|
4784
|
+
`);
|
|
4785
|
+
}
|
|
4786
|
+
return outcome.code;
|
|
4787
|
+
}
|
|
4788
|
+
async function runHarnessUninstall(name, opts, io) {
|
|
4789
|
+
const run = opts.run ?? spawnRunner;
|
|
4790
|
+
const root = resolveProjectRoot(opts.cwd);
|
|
4791
|
+
const entry = findHarness(name);
|
|
4792
|
+
if (!entry) return unknownHarness(name, io, opts.bin);
|
|
4793
|
+
const target = targetFor(root, run, opts.env);
|
|
4794
|
+
if (!isReachable(await resolveTargetStatus(target))) {
|
|
4795
|
+
return await uninstallOnHost(entry, opts, io, run);
|
|
4796
|
+
}
|
|
4797
|
+
return (await removeHarness(entry, target, SANDBOX_HARNESS_PREFIX, "sandbox", opts, io)).code;
|
|
4798
|
+
}
|
|
4267
4799
|
async function runHarnessInstall(name, opts, io) {
|
|
4268
4800
|
const run = opts.run ?? spawnRunner;
|
|
4269
4801
|
const env = opts.env ?? process.env;
|
|
@@ -4274,23 +4806,9 @@ async function runHarnessInstall(name, opts, io) {
|
|
|
4274
4806
|
const entry = findHarness(name);
|
|
4275
4807
|
if (!entry) return unknownHarness(name, io, opts.bin);
|
|
4276
4808
|
const target = targetFor(root, run, opts.env);
|
|
4277
|
-
|
|
4278
|
-
try {
|
|
4279
|
-
status = await target.status();
|
|
4280
|
-
} catch (err) {
|
|
4281
|
-
if (err instanceof ExecutionSpawnError && err.code === "ENOENT") {
|
|
4282
|
-
io.stderr("docker is required to install into the running sandbox but was not found on PATH\n");
|
|
4283
|
-
return 1;
|
|
4284
|
-
}
|
|
4285
|
-
throw err;
|
|
4286
|
-
}
|
|
4809
|
+
const status = await resolveTargetStatus(target);
|
|
4287
4810
|
if (!isReachable(status)) {
|
|
4288
|
-
io
|
|
4289
|
-
`${opts.bin} harness: the sandbox is not running (${status}).
|
|
4290
|
-
Start it with \`${opts.bin} sandbox\`, then re-run this command.
|
|
4291
|
-
`
|
|
4292
|
-
);
|
|
4293
|
-
return 1;
|
|
4811
|
+
return await installOnHost(entry, opts, io, run, status);
|
|
4294
4812
|
}
|
|
4295
4813
|
const hermes = entry.id === "hermes";
|
|
4296
4814
|
const installEnv = hermes ? {
|
|
@@ -4298,10 +4816,10 @@ Start it with \`${opts.bin} sandbox\`, then re-run this command.
|
|
|
4298
4816
|
HERMES_HOME: `${hermesTargetRoot(target)}/.hermes`
|
|
4299
4817
|
} : void 0;
|
|
4300
4818
|
if (hermes) {
|
|
4301
|
-
const code = await reconcileHermes(target, io, opts.bin);
|
|
4819
|
+
const code = await reconcileHermes(target, io, opts.bin, "sandbox");
|
|
4302
4820
|
if (code !== 0) return code;
|
|
4303
4821
|
}
|
|
4304
|
-
const already = await probeInstalled(target, entry, installEnv);
|
|
4822
|
+
const already = await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox", installEnv);
|
|
4305
4823
|
if (already === true) {
|
|
4306
4824
|
io.stdout(`${entry.id}: already installed (${entry.binary})
|
|
4307
4825
|
`);
|
|
@@ -4314,7 +4832,7 @@ Start it with \`${opts.bin} sandbox\`, then re-run this command.
|
|
|
4314
4832
|
io.stdout(`installing ${entry.title} into the sandbox\u2026
|
|
4315
4833
|
`);
|
|
4316
4834
|
const r = await target.exec({
|
|
4317
|
-
argv:
|
|
4835
|
+
argv: resolveInstallArgv(entry, SANDBOX_HARNESS_PREFIX),
|
|
4318
4836
|
user: entry.installUser,
|
|
4319
4837
|
stdio: "inherit",
|
|
4320
4838
|
...installEnv ? { env: installEnv } : {}
|
|
@@ -4325,12 +4843,12 @@ Start it with \`${opts.bin} sandbox\`, then re-run this command.
|
|
|
4325
4843
|
return r.exitCode;
|
|
4326
4844
|
}
|
|
4327
4845
|
if (hermes) {
|
|
4328
|
-
if (await probeInstalled(target, entry, installEnv) !== true) {
|
|
4846
|
+
if (await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox", installEnv) !== true) {
|
|
4329
4847
|
io.stderr(`${opts.bin} harness: Hermes installation finished but executable verification failed.
|
|
4330
4848
|
`);
|
|
4331
4849
|
return 1;
|
|
4332
4850
|
}
|
|
4333
|
-
const code = await reconcileHermes(target, io, opts.bin);
|
|
4851
|
+
const code = await reconcileHermes(target, io, opts.bin, "sandbox");
|
|
4334
4852
|
if (code !== 0) return code;
|
|
4335
4853
|
}
|
|
4336
4854
|
io.stdout(`${entry.id}: installed \u2014 see ${sourceDocsUrl(entry.docsPath)} for authentication
|
|
@@ -4338,8 +4856,14 @@ Start it with \`${opts.bin} sandbox\`, then re-run this command.
|
|
|
4338
4856
|
return 0;
|
|
4339
4857
|
}
|
|
4340
4858
|
|
|
4859
|
+
// src/commands/tool.ts
|
|
4860
|
+
import { existsSync as existsSync19 } from "node:fs";
|
|
4861
|
+
import { homedir as homedir3 } from "node:os";
|
|
4862
|
+
import { delimiter as delimiter3, join as join13, resolve as resolve13 } from "node:path";
|
|
4863
|
+
|
|
4341
4864
|
// src/lib/tools/catalog.ts
|
|
4342
4865
|
var TOOLS_DOC = "docs/installation.md";
|
|
4866
|
+
var TOOL_HOST_PLATFORM = "linux";
|
|
4343
4867
|
var TOOL_CATALOG = Object.freeze([
|
|
4344
4868
|
Object.freeze({
|
|
4345
4869
|
id: "agent-browser",
|
|
@@ -4354,6 +4878,9 @@ var TOOL_CATALOG = Object.freeze([
|
|
|
4354
4878
|
]),
|
|
4355
4879
|
installUser: "sandbox",
|
|
4356
4880
|
downloadSize: "~1 GB",
|
|
4881
|
+
hostCapable: false,
|
|
4882
|
+
notHostCapableReason: () => "agent-browser runs `agent-browser install --with-deps`, which installs system packages with the operating system package manager. AGRO installs it into the sandbox only, where that stays contained.",
|
|
4883
|
+
uninstallArgv: Object.freeze(["bash", "-lc", "pnpm remove -g agent-browser"]),
|
|
4357
4884
|
docsPath: TOOLS_DOC
|
|
4358
4885
|
}),
|
|
4359
4886
|
Object.freeze({
|
|
@@ -4385,6 +4912,8 @@ var TOOL_CATALOG = Object.freeze([
|
|
|
4385
4912
|
].join("\n")
|
|
4386
4913
|
]),
|
|
4387
4914
|
installUser: "sandbox",
|
|
4915
|
+
hostCapable: true,
|
|
4916
|
+
uninstallArgv: Object.freeze(["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/herdr`]),
|
|
4388
4917
|
docsPath: TOOLS_DOC
|
|
4389
4918
|
}),
|
|
4390
4919
|
Object.freeze({
|
|
@@ -4417,6 +4946,8 @@ var TOOL_CATALOG = Object.freeze([
|
|
|
4417
4946
|
].join("\n")
|
|
4418
4947
|
]),
|
|
4419
4948
|
installUser: "sandbox",
|
|
4949
|
+
hostCapable: true,
|
|
4950
|
+
uninstallArgv: Object.freeze(["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/cloudflared`]),
|
|
4420
4951
|
docsPath: TOOLS_DOC
|
|
4421
4952
|
}),
|
|
4422
4953
|
Object.freeze({
|
|
@@ -4442,6 +4973,13 @@ var TOOL_CATALOG = Object.freeze([
|
|
|
4442
4973
|
].join("\n")
|
|
4443
4974
|
]),
|
|
4444
4975
|
installUser: "sandbox",
|
|
4976
|
+
hostCapable: true,
|
|
4977
|
+
uninstallArgv: Object.freeze([
|
|
4978
|
+
"rm",
|
|
4979
|
+
"-rf",
|
|
4980
|
+
`${HARNESS_PREFIX_TOKEN}/bin/msb`,
|
|
4981
|
+
`${HARNESS_PREFIX_TOKEN}/microsandbox`
|
|
4982
|
+
]),
|
|
4445
4983
|
docsPath: "docs/runtimes/microsandbox.md"
|
|
4446
4984
|
}),
|
|
4447
4985
|
Object.freeze({
|
|
@@ -4452,6 +4990,8 @@ var TOOL_CATALOG = Object.freeze([
|
|
|
4452
4990
|
verifyArgv: Object.freeze(["bash", "-lc", "command -v docker >/dev/null"]),
|
|
4453
4991
|
versionArgv: Object.freeze(["docker", "--version"]),
|
|
4454
4992
|
notInstallableReason: (bin) => `The Docker CLI is installed in the base image. Note that the CLI being present says nothing about whether a daemon is reachable \u2014 \`${bin} ps <name>\` answers that.`,
|
|
4993
|
+
hostCapable: false,
|
|
4994
|
+
uninstallArgv: null,
|
|
4455
4995
|
docsPath: TOOLS_DOC
|
|
4456
4996
|
}),
|
|
4457
4997
|
Object.freeze({
|
|
@@ -4462,6 +5002,8 @@ var TOOL_CATALOG = Object.freeze([
|
|
|
4462
5002
|
verifyArgv: Object.freeze(["bash", "-lc", "command -v gh >/dev/null"]),
|
|
4463
5003
|
versionArgv: Object.freeze(["gh", "--version"]),
|
|
4464
5004
|
notInstallableReason: () => "The GitHub CLI is installed in the base image. Run `gh auth login` inside the sandbox to authenticate it.",
|
|
5005
|
+
hostCapable: false,
|
|
5006
|
+
uninstallArgv: null,
|
|
4465
5007
|
docsPath: TOOLS_DOC
|
|
4466
5008
|
}),
|
|
4467
5009
|
Object.freeze({
|
|
@@ -4493,6 +5035,13 @@ install -m 0755 "$tmp/tailscale_1.102.3_$arch/tailscaled" "$prefix/bin/tailscale
|
|
|
4493
5035
|
install -d -m 0700 "$HOME/.tailscale"`
|
|
4494
5036
|
]),
|
|
4495
5037
|
installUser: "sandbox",
|
|
5038
|
+
hostCapable: true,
|
|
5039
|
+
uninstallArgv: Object.freeze([
|
|
5040
|
+
"rm",
|
|
5041
|
+
"-rf",
|
|
5042
|
+
`${HARNESS_PREFIX_TOKEN}/bin/tailscale`,
|
|
5043
|
+
`${HARNESS_PREFIX_TOKEN}/bin/tailscaled`
|
|
5044
|
+
]),
|
|
4496
5045
|
docsPath: TOOLS_DOC
|
|
4497
5046
|
})
|
|
4498
5047
|
]);
|
|
@@ -4505,6 +5054,12 @@ function toolIds() {
|
|
|
4505
5054
|
function installableToolIds() {
|
|
4506
5055
|
return TOOL_CATALOG.filter((t) => t.installArgv !== void 0).map((t) => t.id);
|
|
4507
5056
|
}
|
|
5057
|
+
function hostCapableToolIds() {
|
|
5058
|
+
return TOOL_CATALOG.filter((t) => t.hostCapable).map((t) => t.id);
|
|
5059
|
+
}
|
|
5060
|
+
function resolveToolUninstallArgv(entry, prefix) {
|
|
5061
|
+
return entry.uninstallArgv === null ? null : entry.uninstallArgv.map((part) => part.split(HARNESS_PREFIX_TOKEN).join(prefix));
|
|
5062
|
+
}
|
|
4508
5063
|
|
|
4509
5064
|
// src/commands/tool.ts
|
|
4510
5065
|
function isReachable2(status) {
|
|
@@ -4519,56 +5074,116 @@ function targetFor2(root, run, env) {
|
|
|
4519
5074
|
...env ? { env } : {}
|
|
4520
5075
|
});
|
|
4521
5076
|
}
|
|
4522
|
-
|
|
5077
|
+
function hostPrefix2(home) {
|
|
5078
|
+
return join13(home, ".local");
|
|
5079
|
+
}
|
|
5080
|
+
function pathEntries2(env) {
|
|
5081
|
+
return (env.PATH ?? "").split(delimiter3).filter((part) => part !== "");
|
|
5082
|
+
}
|
|
5083
|
+
function onPath2(prefix, env) {
|
|
5084
|
+
return pathEntries2(env).includes(harnessBinPath(prefix));
|
|
5085
|
+
}
|
|
5086
|
+
function hostTargetFor2(root, prefix, run, env) {
|
|
5087
|
+
const path4 = [harnessBinPath(prefix), ...pathEntries2(env)].join(delimiter3);
|
|
5088
|
+
return new LocalExecutionTarget({
|
|
5089
|
+
projectRoot: root,
|
|
5090
|
+
run,
|
|
5091
|
+
env: { ...env, PATH: path4 }
|
|
5092
|
+
});
|
|
5093
|
+
}
|
|
5094
|
+
function messageOf2(error) {
|
|
5095
|
+
return error instanceof Error ? error.message : String(error);
|
|
5096
|
+
}
|
|
5097
|
+
function homeOf2(opts) {
|
|
5098
|
+
return (opts.homedir ?? homedir3)();
|
|
5099
|
+
}
|
|
5100
|
+
function platformOf(opts) {
|
|
5101
|
+
return opts.platform ?? process.platform;
|
|
5102
|
+
}
|
|
5103
|
+
function isInteractive2(opts) {
|
|
5104
|
+
return opts.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true);
|
|
5105
|
+
}
|
|
5106
|
+
async function tryExec(target, argv, user, env) {
|
|
4523
5107
|
try {
|
|
4524
|
-
const r = await target.exec({
|
|
5108
|
+
const r = await target.exec({
|
|
5109
|
+
argv: [...argv],
|
|
5110
|
+
...user ? { user } : {},
|
|
5111
|
+
stdio: "capture",
|
|
5112
|
+
...env ? { env } : {}
|
|
5113
|
+
});
|
|
4525
5114
|
return { exitCode: r.exitCode, stdout: r.stdout };
|
|
4526
5115
|
} catch (err) {
|
|
4527
5116
|
if (err instanceof ExecutionSpawnError) return null;
|
|
4528
5117
|
throw err;
|
|
4529
5118
|
}
|
|
4530
5119
|
}
|
|
4531
|
-
async function probeInstalled2(target, entry) {
|
|
4532
|
-
const r = await tryExec(target, entry.verifyArgv,
|
|
5120
|
+
async function probeInstalled2(target, entry, user, env) {
|
|
5121
|
+
const r = await tryExec(target, entry.verifyArgv, user, env);
|
|
4533
5122
|
return r === null ? null : r.exitCode === 0;
|
|
4534
5123
|
}
|
|
4535
|
-
async function probeVersion(target, entry) {
|
|
5124
|
+
async function probeVersion(target, entry, user) {
|
|
4536
5125
|
if (entry.versionArgv === void 0) return null;
|
|
4537
|
-
const r = await tryExec(target, entry.versionArgv,
|
|
5126
|
+
const r = await tryExec(target, entry.versionArgv, user);
|
|
4538
5127
|
if (r === null || r.exitCode !== 0) return null;
|
|
4539
5128
|
const first = r.stdout.trim().split("\n")[0] ?? "";
|
|
4540
5129
|
return first === "" ? null : first;
|
|
4541
5130
|
}
|
|
4542
|
-
|
|
5131
|
+
function rowOf(entry, installed, version, location) {
|
|
5132
|
+
return {
|
|
5133
|
+
id: entry.id,
|
|
5134
|
+
title: entry.title,
|
|
5135
|
+
binary: entry.binary,
|
|
5136
|
+
kind: entry.kind,
|
|
5137
|
+
installed,
|
|
5138
|
+
version,
|
|
5139
|
+
installable: entry.installArgv !== void 0,
|
|
5140
|
+
hostCapable: entry.hostCapable,
|
|
5141
|
+
location,
|
|
5142
|
+
docs: sourceDocsUrl(entry.docsPath)
|
|
5143
|
+
};
|
|
5144
|
+
}
|
|
5145
|
+
function probeableHost2(env, home) {
|
|
5146
|
+
let root;
|
|
5147
|
+
try {
|
|
5148
|
+
root = resolveHarnessRoot(void 0, env);
|
|
5149
|
+
} catch {
|
|
5150
|
+
return void 0;
|
|
5151
|
+
}
|
|
5152
|
+
const prefix = hostPrefix2(home);
|
|
5153
|
+
if (!existsSync19(join13(root, ".git")) && !existsSync19(prefix)) return void 0;
|
|
5154
|
+
return { root, prefix };
|
|
5155
|
+
}
|
|
5156
|
+
async function collectRows(root, run, home, env, only) {
|
|
4543
5157
|
const entries = only ? [...only] : [...TOOL_CATALOG];
|
|
4544
5158
|
const target = targetFor2(root, run, env);
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
5159
|
+
if (isReachable2(await resolveTargetStatus(target))) {
|
|
5160
|
+
const rows2 = [];
|
|
5161
|
+
for (const entry of entries) {
|
|
5162
|
+
const installed = await probeInstalled2(target, entry, "sandbox");
|
|
5163
|
+
const version = installed === true ? await probeVersion(target, entry, "sandbox") : null;
|
|
5164
|
+
rows2.push(rowOf(entry, installed, version, installed === null ? "unknown" : "sandbox"));
|
|
5165
|
+
}
|
|
5166
|
+
return { rows: rows2 };
|
|
4550
5167
|
}
|
|
5168
|
+
const host = probeableHost2(env ?? process.env, home);
|
|
5169
|
+
if (host === void 0) {
|
|
5170
|
+
return { rows: entries.map((entry) => rowOf(entry, null, null, "unknown")) };
|
|
5171
|
+
}
|
|
5172
|
+
const hostTarget = hostTargetFor2(host.root, host.prefix, run, env ?? process.env);
|
|
4551
5173
|
const rows = [];
|
|
4552
5174
|
for (const entry of entries) {
|
|
4553
|
-
const installed =
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
title: entry.title,
|
|
4557
|
-
binary: entry.binary,
|
|
4558
|
-
kind: entry.kind,
|
|
4559
|
-
installed,
|
|
4560
|
-
version: reachable && installed === true ? await probeVersion(target, entry) : null,
|
|
4561
|
-
installable: entry.installArgv !== void 0,
|
|
4562
|
-
docs: sourceDocsUrl(entry.docsPath)
|
|
4563
|
-
});
|
|
5175
|
+
const installed = await probeInstalled2(hostTarget, entry, void 0);
|
|
5176
|
+
const version = installed === true ? await probeVersion(hostTarget, entry, void 0) : null;
|
|
5177
|
+
rows.push(rowOf(entry, installed, version, installed === null ? "unknown" : "host"));
|
|
4564
5178
|
}
|
|
4565
|
-
return rows;
|
|
5179
|
+
return { rows, hostPrefix: host.prefix };
|
|
4566
5180
|
}
|
|
4567
5181
|
function cell2(value, absent) {
|
|
4568
5182
|
if (value === null) return absent;
|
|
4569
5183
|
return value ? "yes" : "no";
|
|
4570
5184
|
}
|
|
4571
|
-
function renderTable2(
|
|
5185
|
+
function renderTable2(collected, io, bin) {
|
|
5186
|
+
const rows = collected.rows;
|
|
4572
5187
|
const header2 = ["TOOL", "KIND", "INSTALLED"];
|
|
4573
5188
|
const body = rows.map((r) => [r.id, r.kind, cell2(r.installed, "?")]);
|
|
4574
5189
|
const widths = header2.map(
|
|
@@ -4577,21 +5192,31 @@ function renderTable2(rows, io, bin) {
|
|
|
4577
5192
|
const line = (cols) => cols.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd() + "\n";
|
|
4578
5193
|
io.stdout(line(header2));
|
|
4579
5194
|
for (const row of body) io.stdout(line(row));
|
|
5195
|
+
if (rows.some((r) => r.location === "host")) {
|
|
5196
|
+
io.stdout(
|
|
5197
|
+
`
|
|
5198
|
+
INSTALLED reports the host prefix ${collected.hostPrefix} \u2014 the sandbox is not running.
|
|
5199
|
+
`
|
|
5200
|
+
);
|
|
5201
|
+
return;
|
|
5202
|
+
}
|
|
4580
5203
|
if (rows.some((r) => r.installed === null)) {
|
|
4581
5204
|
io.stdout(`
|
|
4582
5205
|
INSTALLED is \`?\` \u2014 the sandbox is not running. Start it with \`${bin} sandbox\`.
|
|
4583
5206
|
`);
|
|
4584
5207
|
}
|
|
4585
5208
|
}
|
|
4586
|
-
function renderDetail(
|
|
4587
|
-
renderTable2(
|
|
4588
|
-
for (const r of rows) {
|
|
5209
|
+
function renderDetail(collected, io, bin) {
|
|
5210
|
+
renderTable2(collected, io, bin);
|
|
5211
|
+
for (const r of collected.rows) {
|
|
4589
5212
|
io.stdout(`
|
|
4590
5213
|
${r.id} \u2014 ${r.title}
|
|
4591
5214
|
`);
|
|
4592
5215
|
io.stdout(` version: ${r.version ?? "\u2014"}
|
|
4593
5216
|
`);
|
|
4594
5217
|
io.stdout(` installable: ${r.installable ? "yes" : "no"}
|
|
5218
|
+
`);
|
|
5219
|
+
io.stdout(` on the host: ${r.hostCapable ? "yes" : "no"}
|
|
4595
5220
|
`);
|
|
4596
5221
|
io.stdout(` see ${r.docs}
|
|
4597
5222
|
`);
|
|
@@ -4600,12 +5225,12 @@ ${r.id} \u2014 ${r.title}
|
|
|
4600
5225
|
async function runToolList(opts, io) {
|
|
4601
5226
|
const run = opts.run ?? spawnRunner;
|
|
4602
5227
|
const root = resolveProjectRoot(opts.cwd);
|
|
4603
|
-
const
|
|
5228
|
+
const collected = await collectRows(root, run, homeOf2(opts), opts.env);
|
|
4604
5229
|
if (opts.json) {
|
|
4605
|
-
io.stdout(`${JSON.stringify(rows, null, 2)}
|
|
5230
|
+
io.stdout(`${JSON.stringify(collected.rows, null, 2)}
|
|
4606
5231
|
`);
|
|
4607
5232
|
} else {
|
|
4608
|
-
renderTable2(
|
|
5233
|
+
renderTable2(collected, io, opts.bin);
|
|
4609
5234
|
}
|
|
4610
5235
|
return 0;
|
|
4611
5236
|
}
|
|
@@ -4626,12 +5251,12 @@ async function runToolStatus(name, opts, io) {
|
|
|
4626
5251
|
only = findTool(name);
|
|
4627
5252
|
if (!only) return unknownTool(name, io, opts.bin);
|
|
4628
5253
|
}
|
|
4629
|
-
const
|
|
5254
|
+
const collected = await collectRows(root, run, homeOf2(opts), opts.env, only ? [only] : void 0);
|
|
4630
5255
|
if (opts.json) {
|
|
4631
|
-
io.stdout(`${JSON.stringify(only ? rows[0] : rows, null, 2)}
|
|
5256
|
+
io.stdout(`${JSON.stringify(only ? collected.rows[0] : collected.rows, null, 2)}
|
|
4632
5257
|
`);
|
|
4633
5258
|
} else {
|
|
4634
|
-
renderDetail(
|
|
5259
|
+
renderDetail(collected, io, opts.bin);
|
|
4635
5260
|
}
|
|
4636
5261
|
return 0;
|
|
4637
5262
|
}
|
|
@@ -4648,6 +5273,151 @@ Re-run with --yes to accept the download.
|
|
|
4648
5273
|
);
|
|
4649
5274
|
return false;
|
|
4650
5275
|
}
|
|
5276
|
+
function unreachableReason2(status) {
|
|
5277
|
+
return runtimeIsAbsent(status) ? "No container runtime is on PATH." : `The sandbox is not running (${status}).`;
|
|
5278
|
+
}
|
|
5279
|
+
function sandboxRefusal2(bin, status) {
|
|
5280
|
+
if (runtimeIsAbsent(status)) {
|
|
5281
|
+
return `${bin} tool: no container runtime is on PATH.
|
|
5282
|
+
`;
|
|
5283
|
+
}
|
|
5284
|
+
return `${bin} tool: the sandbox is not running (${status}).
|
|
5285
|
+
Start it with \`${bin} sandbox\`, then re-run this command.
|
|
5286
|
+
`;
|
|
5287
|
+
}
|
|
5288
|
+
function hostCapableList(bin) {
|
|
5289
|
+
return `Tools that install on the host:
|
|
5290
|
+
${hostCapableToolIds().map((t) => ` ${bin} tool install ${t} --host`).join("\n")}
|
|
5291
|
+
`;
|
|
5292
|
+
}
|
|
5293
|
+
async function installOnHost2(entry, opts, io, run, status) {
|
|
5294
|
+
const bin = opts.bin;
|
|
5295
|
+
const env = opts.env ?? process.env;
|
|
5296
|
+
const flagged = opts.host === true || opts.path !== void 0;
|
|
5297
|
+
if (!entry.hostCapable) {
|
|
5298
|
+
io.stderr(sandboxRefusal2(bin, status));
|
|
5299
|
+
io.stderr(`
|
|
5300
|
+
${bin} tool: ${entry.id} cannot be installed on the host.
|
|
5301
|
+
`);
|
|
5302
|
+
io.stderr(`${(entry.notHostCapableReason ?? entry.notInstallableReason)?.(bin) ?? ""}
|
|
5303
|
+
|
|
5304
|
+
`);
|
|
5305
|
+
io.stderr(hostCapableList(bin));
|
|
5306
|
+
return 1;
|
|
5307
|
+
}
|
|
5308
|
+
const platform = platformOf(opts);
|
|
5309
|
+
if (platform !== TOOL_HOST_PLATFORM) {
|
|
5310
|
+
io.stderr(sandboxRefusal2(bin, status));
|
|
5311
|
+
io.stderr(
|
|
5312
|
+
`
|
|
5313
|
+
${bin} tool: a host install needs ${TOOL_HOST_PLATFORM}; this host is ${platform}.
|
|
5314
|
+
Every tool installer in the catalog is Debian-specific. Install ${entry.id} with your own package manager, or start the sandbox.
|
|
5315
|
+
`
|
|
5316
|
+
);
|
|
5317
|
+
return 1;
|
|
5318
|
+
}
|
|
5319
|
+
if (!flagged && !isInteractive2(opts)) {
|
|
5320
|
+
io.stderr(
|
|
5321
|
+
sandboxRefusal2(bin, status) + `Or install on the host with \`${bin} tool install ${entry.id} --host\`.
|
|
5322
|
+
`
|
|
5323
|
+
);
|
|
5324
|
+
return 1;
|
|
5325
|
+
}
|
|
5326
|
+
let root;
|
|
5327
|
+
let recorded;
|
|
5328
|
+
try {
|
|
5329
|
+
recorded = readHostConfig(env).harnessRoot;
|
|
5330
|
+
root = resolveHarnessRoot(opts.path, env);
|
|
5331
|
+
} catch (err) {
|
|
5332
|
+
io.stderr(`${bin} tool: ${messageOf2(err)}
|
|
5333
|
+
`);
|
|
5334
|
+
return 1;
|
|
5335
|
+
}
|
|
5336
|
+
const sticky = opts.path === void 0 && recorded !== void 0 && recorded !== "";
|
|
5337
|
+
if (!flagged) {
|
|
5338
|
+
const ask2 = io.ask ?? ask;
|
|
5339
|
+
const answer = (await ask2(
|
|
5340
|
+
`${unreachableReason2(status)} Install ${entry.title} on the host? [y/N]`
|
|
5341
|
+
)).trim().toLowerCase();
|
|
5342
|
+
if (!/^y/.test(answer)) {
|
|
5343
|
+
io.stderr(sandboxRefusal2(bin, status));
|
|
5344
|
+
return 1;
|
|
5345
|
+
}
|
|
5346
|
+
if (!sticky) {
|
|
5347
|
+
const chosen = (await ask2(`Harness root [${root}]:`)).trim();
|
|
5348
|
+
if (chosen !== "") root = resolve13(chosen);
|
|
5349
|
+
}
|
|
5350
|
+
}
|
|
5351
|
+
if (sticky) io.stdout(`using the recorded harness root ${root}
|
|
5352
|
+
`);
|
|
5353
|
+
try {
|
|
5354
|
+
if (!existsSync19(join13(root, ".git"))) io.stdout(`cloning ${AGRO_REPO_URL} into ${root}\u2026
|
|
5355
|
+
`);
|
|
5356
|
+
const workspace = ensureHostWorkspace(root, run);
|
|
5357
|
+
root = workspace.root;
|
|
5358
|
+
io.stdout(
|
|
5359
|
+
workspace.action === "cloned" ? `host workspace cloned into ${root}
|
|
5360
|
+
` : `host workspace reused at ${root}
|
|
5361
|
+
`
|
|
5362
|
+
);
|
|
5363
|
+
} catch (err) {
|
|
5364
|
+
io.stderr(`${bin} tool: ${messageOf2(err)}
|
|
5365
|
+
`);
|
|
5366
|
+
return 1;
|
|
5367
|
+
}
|
|
5368
|
+
const prefix = hostPrefix2(homeOf2(opts));
|
|
5369
|
+
const installEnv = { NPM_USER_PREFIX: prefix };
|
|
5370
|
+
const target = hostTargetFor2(root, prefix, run, env);
|
|
5371
|
+
if (await probeInstalled2(target, entry, void 0, installEnv) === true) {
|
|
5372
|
+
io.stdout(`${entry.id}: already installed (${entry.binary})
|
|
5373
|
+
`);
|
|
5374
|
+
return 0;
|
|
5375
|
+
}
|
|
5376
|
+
if (!await confirmDownload(entry, opts, io)) return 1;
|
|
5377
|
+
io.stdout(`installing ${entry.title} on the host\u2026
|
|
5378
|
+
`);
|
|
5379
|
+
const r = await target.exec({
|
|
5380
|
+
argv: [...entry.installArgv],
|
|
5381
|
+
stdio: "inherit",
|
|
5382
|
+
env: installEnv
|
|
5383
|
+
});
|
|
5384
|
+
if (r.exitCode !== 0) {
|
|
5385
|
+
io.stderr(`${bin} tool: installing ${entry.id} failed (exit ${r.exitCode}).
|
|
5386
|
+
`);
|
|
5387
|
+
return r.exitCode;
|
|
5388
|
+
}
|
|
5389
|
+
const receipt = {
|
|
5390
|
+
prefix,
|
|
5391
|
+
binary: entry.binary,
|
|
5392
|
+
binPath: harnessBinPath(prefix),
|
|
5393
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5394
|
+
workspaceRoot: root
|
|
5395
|
+
};
|
|
5396
|
+
try {
|
|
5397
|
+
const config = readHostConfig(env);
|
|
5398
|
+
writeHostConfig(
|
|
5399
|
+
{
|
|
5400
|
+
...config,
|
|
5401
|
+
harnessRoot: root,
|
|
5402
|
+
hostTools: { ...config.hostTools ?? {}, [entry.id]: receipt }
|
|
5403
|
+
},
|
|
5404
|
+
env
|
|
5405
|
+
);
|
|
5406
|
+
} catch (err) {
|
|
5407
|
+
io.stderr(`${bin} tool: could not record the install: ${messageOf2(err)}
|
|
5408
|
+
`);
|
|
5409
|
+
return 1;
|
|
5410
|
+
}
|
|
5411
|
+
io.stdout(`${entry.id}: installed at ${prefix} \u2014 see ${sourceDocsUrl(entry.docsPath)}
|
|
5412
|
+
`);
|
|
5413
|
+
if (!onPath2(prefix, env)) {
|
|
5414
|
+
io.stdout(`Add this line to your shell profile: export PATH="${harnessBinPath(prefix)}:$PATH"
|
|
5415
|
+
`);
|
|
5416
|
+
}
|
|
5417
|
+
io.stdout(`Run ${entry.binary} from the AGRO workspace: cd ${root}
|
|
5418
|
+
`);
|
|
5419
|
+
return 0;
|
|
5420
|
+
}
|
|
4651
5421
|
async function runToolInstall(name, opts, io) {
|
|
4652
5422
|
const run = opts.run ?? spawnRunner;
|
|
4653
5423
|
const root = resolveProjectRoot(opts.cwd);
|
|
@@ -4666,25 +5436,11 @@ ${installableToolIds().map((t) => ` ${t}`).join("\n")}
|
|
|
4666
5436
|
return 1;
|
|
4667
5437
|
}
|
|
4668
5438
|
const target = targetFor2(root, run, opts.env);
|
|
4669
|
-
|
|
4670
|
-
try {
|
|
4671
|
-
status = await target.status();
|
|
4672
|
-
} catch (err) {
|
|
4673
|
-
if (err instanceof ExecutionSpawnError && err.code === "ENOENT") {
|
|
4674
|
-
io.stderr("docker is required to install into the running sandbox but was not found on PATH\n");
|
|
4675
|
-
return 1;
|
|
4676
|
-
}
|
|
4677
|
-
throw err;
|
|
4678
|
-
}
|
|
5439
|
+
const status = await resolveTargetStatus(target);
|
|
4679
5440
|
if (!isReachable2(status)) {
|
|
4680
|
-
io
|
|
4681
|
-
`${opts.bin} tool: the sandbox is not running (${status}).
|
|
4682
|
-
Start it with \`${opts.bin} sandbox\`, then re-run this command.
|
|
4683
|
-
`
|
|
4684
|
-
);
|
|
4685
|
-
return 1;
|
|
5441
|
+
return await installOnHost2(entry, opts, io, run, status);
|
|
4686
5442
|
}
|
|
4687
|
-
const already = await probeInstalled2(target, entry);
|
|
5443
|
+
const already = await probeInstalled2(target, entry, "sandbox");
|
|
4688
5444
|
if (already === true) {
|
|
4689
5445
|
io.stdout(`${entry.id}: already installed (${entry.binary})
|
|
4690
5446
|
`);
|
|
@@ -4711,6 +5467,98 @@ Start it with \`${opts.bin} sandbox\`, then re-run this command.
|
|
|
4711
5467
|
`);
|
|
4712
5468
|
return 0;
|
|
4713
5469
|
}
|
|
5470
|
+
async function removeTool(entry, target, prefix, user, opts, io) {
|
|
5471
|
+
const argv = resolveToolUninstallArgv(entry, prefix);
|
|
5472
|
+
if (await probeInstalled2(target, entry, user) !== true) {
|
|
5473
|
+
io.stdout(`${entry.id}: not installed (${entry.binary})
|
|
5474
|
+
`);
|
|
5475
|
+
return { code: 0, dropReceipt: true };
|
|
5476
|
+
}
|
|
5477
|
+
if (isInteractive2(opts)) {
|
|
5478
|
+
const ask2 = io.ask ?? ask;
|
|
5479
|
+
const answer = (await ask2(`Remove ${entry.title} from ${prefix}? [y/N]`)).trim().toLowerCase();
|
|
5480
|
+
if (!/^y/.test(answer)) {
|
|
5481
|
+
io.stderr(`${opts.bin} tool: removed nothing.
|
|
5482
|
+
`);
|
|
5483
|
+
return { code: 1, dropReceipt: false };
|
|
5484
|
+
}
|
|
5485
|
+
}
|
|
5486
|
+
io.stdout(`removing ${entry.title} from ${prefix}\u2026
|
|
5487
|
+
`);
|
|
5488
|
+
const r = await target.exec({
|
|
5489
|
+
argv,
|
|
5490
|
+
...user ? { user } : {},
|
|
5491
|
+
stdio: "inherit"
|
|
5492
|
+
});
|
|
5493
|
+
if (r.exitCode !== 0) {
|
|
5494
|
+
io.stderr(`${opts.bin} tool: removing ${entry.id} failed (exit ${r.exitCode}).
|
|
5495
|
+
`);
|
|
5496
|
+
return { code: r.exitCode, dropReceipt: false };
|
|
5497
|
+
}
|
|
5498
|
+
io.stdout(`${entry.id}: removed from ${prefix}
|
|
5499
|
+
`);
|
|
5500
|
+
return { code: 0, dropReceipt: true };
|
|
5501
|
+
}
|
|
5502
|
+
async function uninstallOnHost2(entry, opts, io, run) {
|
|
5503
|
+
const bin = opts.bin;
|
|
5504
|
+
const env = opts.env ?? process.env;
|
|
5505
|
+
const computed = hostPrefix2(homeOf2(opts));
|
|
5506
|
+
let config;
|
|
5507
|
+
let workspace;
|
|
5508
|
+
try {
|
|
5509
|
+
config = readHostConfig(env);
|
|
5510
|
+
workspace = resolveHarnessRoot(void 0, env);
|
|
5511
|
+
} catch (err) {
|
|
5512
|
+
io.stderr(`${bin} tool: ${messageOf2(err)}
|
|
5513
|
+
`);
|
|
5514
|
+
return 1;
|
|
5515
|
+
}
|
|
5516
|
+
const receipt = config.hostTools?.[entry.id];
|
|
5517
|
+
if (receipt === void 0 && opts.force !== true) {
|
|
5518
|
+
io.stderr(
|
|
5519
|
+
`${bin} tool: no record of installing ${entry.id} on this host.
|
|
5520
|
+
Removing it could delete a tool you installed yourself. Re-run with \`--force\` to remove it from ${computed}.
|
|
5521
|
+
`
|
|
5522
|
+
);
|
|
5523
|
+
return 1;
|
|
5524
|
+
}
|
|
5525
|
+
const prefix = receipt?.prefix ?? computed;
|
|
5526
|
+
const target = hostTargetFor2(receipt?.workspaceRoot ?? workspace, prefix, run, env);
|
|
5527
|
+
const outcome = await removeTool(entry, target, prefix, void 0, opts, io);
|
|
5528
|
+
if (outcome.dropReceipt && receipt !== void 0) {
|
|
5529
|
+
const remaining = { ...config.hostTools ?? {} };
|
|
5530
|
+
delete remaining[entry.id];
|
|
5531
|
+
try {
|
|
5532
|
+
writeHostConfig({ ...config, hostTools: remaining }, env);
|
|
5533
|
+
} catch (err) {
|
|
5534
|
+
io.stderr(`${bin} tool: could not clear the install record: ${messageOf2(err)}
|
|
5535
|
+
`);
|
|
5536
|
+
return 1;
|
|
5537
|
+
}
|
|
5538
|
+
io.stdout(`${entry.id}: cleared the host install record
|
|
5539
|
+
`);
|
|
5540
|
+
}
|
|
5541
|
+
return outcome.code;
|
|
5542
|
+
}
|
|
5543
|
+
async function runToolUninstall(name, opts, io) {
|
|
5544
|
+
const run = opts.run ?? spawnRunner;
|
|
5545
|
+
const root = resolveProjectRoot(opts.cwd);
|
|
5546
|
+
const entry = findTool(name);
|
|
5547
|
+
if (!entry) return unknownTool(name, io, opts.bin);
|
|
5548
|
+
if (entry.uninstallArgv === null) {
|
|
5549
|
+
io.stderr(`${opts.bin} tool: ${entry.id} cannot be removed by this command.
|
|
5550
|
+
|
|
5551
|
+
`);
|
|
5552
|
+
io.stderr(`${entry.notInstallableReason?.(opts.bin) ?? ""}
|
|
5553
|
+
`);
|
|
5554
|
+
return 1;
|
|
5555
|
+
}
|
|
5556
|
+
const target = targetFor2(root, run, opts.env);
|
|
5557
|
+
if (!isReachable2(await resolveTargetStatus(target))) {
|
|
5558
|
+
return await uninstallOnHost2(entry, opts, io, run);
|
|
5559
|
+
}
|
|
5560
|
+
return (await removeTool(entry, target, SANDBOX_HARNESS_PREFIX, "sandbox", opts, io)).code;
|
|
5561
|
+
}
|
|
4714
5562
|
|
|
4715
5563
|
// src/lib/remote.ts
|
|
4716
5564
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
@@ -4776,9 +5624,9 @@ function fetchRemoteSource(opts) {
|
|
|
4776
5624
|
}
|
|
4777
5625
|
|
|
4778
5626
|
// src/cli.ts
|
|
4779
|
-
var VERSION = true ? "0.
|
|
4780
|
-
var DEFAULT_SOURCE_OH_DIR =
|
|
4781
|
-
|
|
5627
|
+
var VERSION = true ? "0.12.1" : "0.0.0-dev";
|
|
5628
|
+
var DEFAULT_SOURCE_OH_DIR = resolve14(
|
|
5629
|
+
dirname10(fileURLToPath(import.meta.url)),
|
|
4782
5630
|
"../.."
|
|
4783
5631
|
);
|
|
4784
5632
|
var INTEGRATIONS = {};
|
|
@@ -5008,16 +5856,26 @@ function printHarnessHelp(bin = LEGACY_PRODUCT.bin) {
|
|
|
5008
5856
|
|
|
5009
5857
|
Usage:
|
|
5010
5858
|
${bin} harness list List known harnesses and their state
|
|
5011
|
-
${bin} harness install <name> Install a harness into the sandbox
|
|
5859
|
+
${bin} harness install <name> Install a harness into the sandbox or the host
|
|
5860
|
+
${bin} harness uninstall <name> Remove a harness the same way it was installed
|
|
5012
5861
|
${bin} harness status [name] Show installed state
|
|
5013
5862
|
|
|
5014
|
-
\`install\`
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5863
|
+
\`install\` and \`uninstall\` act on the running sandbox. When no sandbox is
|
|
5864
|
+
reachable they act on the host. A host install clones the AGRO workspace into the
|
|
5865
|
+
harness root, then installs the harness into \`~/.local\`. \`--path <dir>\` chooses
|
|
5866
|
+
the harness root; a successful install records it as \`harnessRoot\` in the host
|
|
5867
|
+
\`${stateNames(bin).configFile}\`. The default is \`~/.agro\`, and the install prefix is
|
|
5868
|
+
always \`~/.local\`.
|
|
5869
|
+
|
|
5870
|
+
On the host, \`uninstall\` removes only the harness \`install\` recorded, from the
|
|
5871
|
+
prefix in that record. Without a record it refuses, and \`--force\` overrides. In
|
|
5872
|
+
the sandbox it needs no record. See \`docs/harnesses/overview.md\`.
|
|
5018
5873
|
|
|
5019
5874
|
Flags:
|
|
5020
5875
|
--json Machine-readable output (list/status)
|
|
5876
|
+
--host Install on the host when the sandbox is not running (install)
|
|
5877
|
+
--path <dir> Harness root for the workspace clone; implies --host (install)
|
|
5878
|
+
--force Remove without a recorded host install (uninstall)
|
|
5021
5879
|
|
|
5022
5880
|
Harnesses:
|
|
5023
5881
|
${harnessIds().map((h) => ` ${h}`).join("\n")}
|
|
@@ -5055,19 +5913,34 @@ tunnel client, the GitHub CLI, an isolation runtime's own binary.
|
|
|
5055
5913
|
Usage:
|
|
5056
5914
|
${bin} tool list List known tools and their state
|
|
5057
5915
|
${bin} tool status [name] Show installed state and version
|
|
5058
|
-
${bin} tool install <name> Install a tool into the sandbox
|
|
5916
|
+
${bin} tool install <name> Install a tool into the sandbox or the host
|
|
5917
|
+
${bin} tool uninstall <name> Remove a tool the same way it was installed
|
|
5059
5918
|
|
|
5060
5919
|
Most tools are baked into the image and are report-only; \`install\` works on:
|
|
5061
5920
|
${installableToolIds().map((t) => ` ${t}`).join("\n")}
|
|
5062
5921
|
|
|
5063
5922
|
\`install\` is the only door: it probes the running sandbox, installs the tool
|
|
5064
|
-
into the persistent home volume, and reports. It
|
|
5065
|
-
|
|
5066
|
-
|
|
5923
|
+
into the persistent home volume, and reports. It never rebuilds or restarts the
|
|
5924
|
+
sandbox. A large download is confirmed first, and a non-interactive run without
|
|
5925
|
+
--yes installs nothing.
|
|
5926
|
+
|
|
5927
|
+
When no sandbox is reachable, \`install\` and \`uninstall\` act on the host. A host
|
|
5928
|
+
install needs Linux, because every installer is Debian-specific, and it works
|
|
5929
|
+
only on these tools:
|
|
5930
|
+
${hostCapableToolIds().map((t) => ` ${t}`).join("\n")}
|
|
5931
|
+
|
|
5932
|
+
A host install clones the AGRO workspace into the harness root \u2014 \`--path <dir>\`,
|
|
5933
|
+
then \`harnessRoot\` in the host \`${stateNames(bin).configFile}\`, then \`~/.agro\` \u2014 and installs into
|
|
5934
|
+
\`~/.local\`. It records the install as \`hostTools\` in that same file. On the host,
|
|
5935
|
+
\`uninstall\` removes only what \`install\` recorded, from the prefix in that record.
|
|
5936
|
+
Without a record it refuses, and \`--force\` overrides.
|
|
5067
5937
|
|
|
5068
5938
|
Flags:
|
|
5069
5939
|
--yes Accept a large download without prompting
|
|
5070
5940
|
--json Machine-readable output (list/status)
|
|
5941
|
+
--host Install on the host when the sandbox is not running (install)
|
|
5942
|
+
--path <dir> Harness root for the workspace clone; implies --host (install)
|
|
5943
|
+
--force Remove without a recorded host install (uninstall)
|
|
5071
5944
|
|
|
5072
5945
|
Tools:
|
|
5073
5946
|
${toolIds().map((t) => ` ${t}`).join("\n")}
|
|
@@ -5449,72 +6322,117 @@ function parseShellArgs(rest, bin = LEGACY_PRODUCT.bin) {
|
|
|
5449
6322
|
function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
|
|
5450
6323
|
const args = {
|
|
5451
6324
|
help: false,
|
|
5452
|
-
json: false
|
|
6325
|
+
json: false,
|
|
6326
|
+
host: false,
|
|
6327
|
+
force: false
|
|
5453
6328
|
};
|
|
5454
6329
|
if (rest.length === 0 || isHelpFlag(rest[0])) {
|
|
5455
6330
|
return { ok: true, args: { ...args, help: true } };
|
|
5456
6331
|
}
|
|
5457
6332
|
const positionals = [];
|
|
6333
|
+
let expectPath = false;
|
|
5458
6334
|
for (const token of rest) {
|
|
5459
|
-
if (
|
|
6335
|
+
if (expectPath) {
|
|
6336
|
+
expectPath = false;
|
|
6337
|
+
args.path = token;
|
|
6338
|
+
args.host = true;
|
|
6339
|
+
} else if (token === "--json") {
|
|
5460
6340
|
args.json = true;
|
|
6341
|
+
} else if (token === "--host") {
|
|
6342
|
+
args.host = true;
|
|
6343
|
+
} else if (token === "--force") {
|
|
6344
|
+
args.force = true;
|
|
6345
|
+
} else if (token === "--path") {
|
|
6346
|
+
expectPath = true;
|
|
6347
|
+
} else if (token.startsWith("--path=")) {
|
|
6348
|
+
args.path = token.slice("--path=".length);
|
|
6349
|
+
args.host = true;
|
|
5461
6350
|
} else if (token.startsWith("-")) {
|
|
5462
6351
|
return { ok: false, error: `${bin} harness: unknown flag "${token}"` };
|
|
5463
6352
|
} else {
|
|
5464
6353
|
positionals.push(token);
|
|
5465
6354
|
}
|
|
5466
6355
|
}
|
|
6356
|
+
if (expectPath || args.path === "") {
|
|
6357
|
+
return { ok: false, error: `${bin} harness: --path requires a directory` };
|
|
6358
|
+
}
|
|
5467
6359
|
const [sub, name, ...extra] = positionals;
|
|
5468
|
-
if (sub !== "list" && sub !== "install" && sub !== "status") {
|
|
6360
|
+
if (sub !== "list" && sub !== "install" && sub !== "status" && sub !== "uninstall") {
|
|
5469
6361
|
return {
|
|
5470
6362
|
ok: false,
|
|
5471
|
-
error: `${bin} harness: unknown subcommand "${sub}" \u2014 expected list, install, or status`,
|
|
6363
|
+
error: `${bin} harness: unknown subcommand "${sub}" \u2014 expected list, install, uninstall, or status`,
|
|
5472
6364
|
showHelp: true
|
|
5473
6365
|
};
|
|
5474
6366
|
}
|
|
5475
6367
|
if (extra.length > 0) {
|
|
5476
6368
|
return { ok: false, error: `${bin} harness: unexpected argument "${extra[0]}"` };
|
|
5477
6369
|
}
|
|
5478
|
-
if (sub === "install" && name === void 0) {
|
|
5479
|
-
return { ok: false, error: `${bin} harness
|
|
6370
|
+
if ((sub === "install" || sub === "uninstall") && name === void 0) {
|
|
6371
|
+
return { ok: false, error: `${bin} harness ${sub}: a harness name is required`, showHelp: true };
|
|
5480
6372
|
}
|
|
5481
6373
|
if (sub === "list" && name !== void 0) {
|
|
5482
6374
|
return { ok: false, error: `${bin} harness list: unexpected argument "${name}"` };
|
|
5483
6375
|
}
|
|
6376
|
+
if (sub !== "install" && args.host) {
|
|
6377
|
+
return { ok: false, error: `${bin} harness ${sub}: --host and --path apply to install only` };
|
|
6378
|
+
}
|
|
6379
|
+
if (sub !== "uninstall" && args.force) {
|
|
6380
|
+
return { ok: false, error: `${bin} harness ${sub}: --force applies to uninstall only` };
|
|
6381
|
+
}
|
|
5484
6382
|
args.subcommand = sub;
|
|
5485
6383
|
if (name !== void 0) args.name = name;
|
|
5486
6384
|
return { ok: true, args };
|
|
5487
6385
|
}
|
|
5488
6386
|
function parseToolArgs(rest, bin = LEGACY_PRODUCT.bin) {
|
|
5489
|
-
const args = { help: false, yes: false, json: false };
|
|
6387
|
+
const args = { help: false, yes: false, json: false, host: false, force: false };
|
|
5490
6388
|
if (rest.length === 0 || isHelpFlag(rest[0])) {
|
|
5491
6389
|
return { ok: true, args: { ...args, help: true } };
|
|
5492
6390
|
}
|
|
5493
6391
|
const positionals = [];
|
|
6392
|
+
let expectPath = false;
|
|
5494
6393
|
for (const token of rest) {
|
|
5495
|
-
if (
|
|
6394
|
+
if (expectPath) {
|
|
6395
|
+
expectPath = false;
|
|
6396
|
+
args.path = token;
|
|
6397
|
+
args.host = true;
|
|
6398
|
+
} else if (token === "--yes" || token === "-y") args.yes = true;
|
|
5496
6399
|
else if (token === "--json") args.json = true;
|
|
5497
|
-
else if (token
|
|
6400
|
+
else if (token === "--host") args.host = true;
|
|
6401
|
+
else if (token === "--force") args.force = true;
|
|
6402
|
+
else if (token === "--path") expectPath = true;
|
|
6403
|
+
else if (token.startsWith("--path=")) {
|
|
6404
|
+
args.path = token.slice("--path=".length);
|
|
6405
|
+
args.host = true;
|
|
6406
|
+
} else if (token.startsWith("-")) {
|
|
5498
6407
|
return { ok: false, error: `${bin} tool: unknown flag "${token}"` };
|
|
5499
6408
|
} else positionals.push(token);
|
|
5500
6409
|
}
|
|
6410
|
+
if (expectPath || args.path === "") {
|
|
6411
|
+
return { ok: false, error: `${bin} tool: --path requires a directory` };
|
|
6412
|
+
}
|
|
5501
6413
|
const [sub, name, ...extra] = positionals;
|
|
5502
|
-
if (sub !== "list" && sub !== "install" && sub !== "status") {
|
|
6414
|
+
if (sub !== "list" && sub !== "install" && sub !== "status" && sub !== "uninstall") {
|
|
5503
6415
|
return {
|
|
5504
6416
|
ok: false,
|
|
5505
|
-
error: `${bin} tool: unknown subcommand "${sub}" \u2014 expected list, install, or status`,
|
|
6417
|
+
error: `${bin} tool: unknown subcommand "${sub}" \u2014 expected list, install, uninstall, or status`,
|
|
5506
6418
|
showHelp: true
|
|
5507
6419
|
};
|
|
5508
6420
|
}
|
|
5509
6421
|
if (extra.length > 0) {
|
|
5510
6422
|
return { ok: false, error: `${bin} tool: unexpected argument "${extra[0]}"` };
|
|
5511
6423
|
}
|
|
5512
|
-
if (sub === "install" && name === void 0) {
|
|
5513
|
-
return { ok: false, error: `${bin} tool
|
|
6424
|
+
if ((sub === "install" || sub === "uninstall") && name === void 0) {
|
|
6425
|
+
return { ok: false, error: `${bin} tool ${sub}: a tool name is required`, showHelp: true };
|
|
5514
6426
|
}
|
|
5515
6427
|
if (sub === "list" && name !== void 0) {
|
|
5516
6428
|
return { ok: false, error: `${bin} tool list: unexpected argument "${name}"` };
|
|
5517
6429
|
}
|
|
6430
|
+
if (sub !== "install" && args.host) {
|
|
6431
|
+
return { ok: false, error: `${bin} tool ${sub}: --host and --path apply to install only` };
|
|
6432
|
+
}
|
|
6433
|
+
if (sub !== "uninstall" && args.force) {
|
|
6434
|
+
return { ok: false, error: `${bin} tool ${sub}: --force applies to uninstall only` };
|
|
6435
|
+
}
|
|
5518
6436
|
args.subcommand = sub;
|
|
5519
6437
|
if (name !== void 0) args.name = name;
|
|
5520
6438
|
return { ok: true, args };
|
|
@@ -5525,19 +6443,19 @@ function parseGatewayArgs(rest) {
|
|
|
5525
6443
|
}
|
|
5526
6444
|
return { ok: true, args: { help: false, passthrough: [...rest] } };
|
|
5527
6445
|
}
|
|
5528
|
-
function bundledPayloadExists(bundled, exists =
|
|
5529
|
-
return exists(
|
|
6446
|
+
function bundledPayloadExists(bundled, exists = existsSync20) {
|
|
6447
|
+
return exists(join14(bundled.sourceOhDir, "manifest.json"));
|
|
5530
6448
|
}
|
|
5531
6449
|
function resolveUpdateSource(args, bundled, bin = LEGACY_PRODUCT.bin) {
|
|
5532
|
-
const exists = bundled.exists ??
|
|
6450
|
+
const exists = bundled.exists ?? existsSync20;
|
|
5533
6451
|
if (args.fromDir !== void 0) {
|
|
5534
|
-
return { kind: "local", fromDir:
|
|
6452
|
+
return { kind: "local", fromDir: resolve14(args.fromDir) };
|
|
5535
6453
|
}
|
|
5536
6454
|
if (args.fromRemote) {
|
|
5537
6455
|
return { kind: "remote", ref: args.ref };
|
|
5538
6456
|
}
|
|
5539
6457
|
if (bundledPayloadExists(bundled, exists)) {
|
|
5540
|
-
return { kind: "local", fromDir:
|
|
6458
|
+
return { kind: "local", fromDir: resolve14(bundled.sourceOhDir, "..") };
|
|
5541
6459
|
}
|
|
5542
6460
|
return {
|
|
5543
6461
|
kind: "remote",
|
|
@@ -5549,7 +6467,7 @@ function resolveUpdateSource(args, bundled, bin = LEGACY_PRODUCT.bin) {
|
|
|
5549
6467
|
function readPayloadVersion(checkoutDir) {
|
|
5550
6468
|
try {
|
|
5551
6469
|
const parsed = JSON.parse(
|
|
5552
|
-
|
|
6470
|
+
readFileSync12(join14(resolveControlDir(checkoutDir).path, "cli", "package.json"), "utf8")
|
|
5553
6471
|
);
|
|
5554
6472
|
if (parsed && typeof parsed.version === "string") return parsed.version;
|
|
5555
6473
|
} catch {
|
|
@@ -5558,8 +6476,8 @@ function readPayloadVersion(checkoutDir) {
|
|
|
5558
6476
|
}
|
|
5559
6477
|
async function runWithRemoteSource(hooks, fn) {
|
|
5560
6478
|
const fetch2 = hooks.fetch ?? fetchRemoteSource;
|
|
5561
|
-
const mkdtemp = hooks.mkdtemp ?? (() =>
|
|
5562
|
-
const rm = hooks.rm ?? ((dir) =>
|
|
6479
|
+
const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync4(join14(tmpdir3(), "oh-remote-")));
|
|
6480
|
+
const rm = hooks.rm ?? ((dir) => rmSync4(dir, { recursive: true, force: true }));
|
|
5563
6481
|
const out = hooks.stdout ?? ((s) => process.stdout.write(s));
|
|
5564
6482
|
const checkoutDir = mkdtemp();
|
|
5565
6483
|
try {
|
|
@@ -5824,7 +6742,14 @@ async function main(argv) {
|
|
|
5824
6742
|
if (a.subcommand === "status") {
|
|
5825
6743
|
return await runHarnessStatus(a.name, { bin, json: a.json }, io);
|
|
5826
6744
|
}
|
|
5827
|
-
|
|
6745
|
+
if (a.subcommand === "uninstall") {
|
|
6746
|
+
return await runHarnessUninstall(a.name, { bin, force: a.force }, io);
|
|
6747
|
+
}
|
|
6748
|
+
return await runHarnessInstall(
|
|
6749
|
+
a.name,
|
|
6750
|
+
{ bin, host: a.host, ...a.path !== void 0 ? { path: a.path } : {} },
|
|
6751
|
+
io
|
|
6752
|
+
);
|
|
5828
6753
|
}
|
|
5829
6754
|
if (first === "tool") {
|
|
5830
6755
|
const parsed = parseToolArgs(argv.slice(1), bin);
|
|
@@ -5849,7 +6774,14 @@ async function main(argv) {
|
|
|
5849
6774
|
if (a.subcommand === "status") {
|
|
5850
6775
|
return await runToolStatus(a.name, { bin, json: a.json }, io);
|
|
5851
6776
|
}
|
|
5852
|
-
|
|
6777
|
+
if (a.subcommand === "uninstall") {
|
|
6778
|
+
return await runToolUninstall(a.name, { bin, force: a.force }, io);
|
|
6779
|
+
}
|
|
6780
|
+
return await runToolInstall(
|
|
6781
|
+
a.name,
|
|
6782
|
+
{ bin, yes: a.yes, host: a.host, ...a.path !== void 0 ? { path: a.path } : {} },
|
|
6783
|
+
io
|
|
6784
|
+
);
|
|
5853
6785
|
}
|
|
5854
6786
|
if (first === "gateway") {
|
|
5855
6787
|
const parsed = parseGatewayArgs(argv.slice(1));
|