@mifune/agro 0.15.0-minimal.1 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/agro.js +371 -79
  2. package/package.json +1 -1
package/dist/agro.js CHANGED
@@ -7,8 +7,8 @@ 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 join17, resolve as resolve11 } from "node:path";
11
- import { existsSync as existsSync21, mkdtempSync as mkdtempSync5, readFileSync as readFileSync12, rmSync as rmSync5 } from "node:fs";
10
+ import { dirname as dirname10, join as join18, resolve as resolve11 } from "node:path";
11
+ import { existsSync as existsSync22, mkdtempSync as mkdtempSync5, readFileSync as readFileSync12, rmSync as rmSync6 } from "node:fs";
12
12
  import { tmpdir as tmpdir4 } from "node:os";
13
13
 
14
14
  // src/commands/update.ts
@@ -1479,6 +1479,11 @@ fi
1479
1479
  # shellcheck source=paths.sh
1480
1480
  . "$PATHS_SH"
1481
1481
 
1482
+ MIGRATOR="$SCRIPT_DIR/migrate-harness-yaml.sh"
1483
+ if [ -f "$REPO_DIR/harness.yaml" ] && [ -f "$MIGRATOR" ]; then
1484
+ sh "$MIGRATOR" "$REPO_DIR" >&2 || true
1485
+ fi
1486
+
1482
1487
  ENV_FILE="$REPO_DIR/.env"
1483
1488
  [ -f "$ENV_FILE" ] || ENV_FILE="$REPO_DIR/.devcontainer/.env"
1484
1489
 
@@ -2217,7 +2222,7 @@ function resolveExecutionTarget(opts) {
2217
2222
  }
2218
2223
 
2219
2224
  // src/lib/version.ts
2220
- var AGRO_VERSION = true ? "0.15.0-minimal.1" : "0.0.0-dev";
2225
+ var AGRO_VERSION = true ? "0.15.1" : "0.0.0-dev";
2221
2226
  var OFFICIAL_IMAGE = "ghcr.io/mifunedev/agro";
2222
2227
  var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z][a-z0-9]*\.[0-9]+)?$/;
2223
2228
  function parseReleaseVersion(value) {
@@ -3727,9 +3732,9 @@ async function runLangfuseSetup(opts, io) {
3727
3732
  }
3728
3733
 
3729
3734
  // src/commands/sandbox.ts
3730
- import { existsSync as existsSync15, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync3, rmSync as rmSync3, statSync as statSync4 } from "node:fs";
3735
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, rmSync as rmSync3, statSync as statSync4 } from "node:fs";
3731
3736
  import { tmpdir as tmpdir3 } from "node:os";
3732
- import { join as join11, resolve as resolve7 } from "node:path";
3737
+ import { join as join12, resolve as resolve7 } from "node:path";
3733
3738
 
3734
3739
  // src/lib/runtimes/catalog.ts
3735
3740
  var RUNTIME_CATALOG = Object.freeze([
@@ -3758,6 +3763,27 @@ function runtimeIds() {
3758
3763
  return RUNTIME_CATALOG.map((r) => r.id);
3759
3764
  }
3760
3765
 
3766
+ // src/lib/star-prompt.ts
3767
+ import { existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "node:fs";
3768
+ import { join as join11 } from "node:path";
3769
+ var STAR_PROMPT_LINE = "\u2B50 If AGRO helps, star https://github.com/mifunedev/agro";
3770
+ var MARKER = "star-prompt-shown";
3771
+ function maybePrintStarPrompt(io, env = process.env, isTTY = process.stdout.isTTY === true, stateHome = resolveUserStateHome(env)) {
3772
+ if (env.AGRO_NO_STAR_PROMPT === "1") return;
3773
+ if (env.CI !== void 0 && env.CI !== "") return;
3774
+ if (!isTTY) return;
3775
+ const marker = join11(stateHome, MARKER);
3776
+ if (existsSync15(marker)) return;
3777
+ io.stdout(`${STAR_PROMPT_LINE}
3778
+ `);
3779
+ try {
3780
+ mkdirSync7(stateHome, { recursive: true });
3781
+ writeFileSync8(marker, "");
3782
+ } catch {
3783
+ return;
3784
+ }
3785
+ }
3786
+
3761
3787
  // src/commands/sandbox.ts
3762
3788
  function hostTimezone() {
3763
3789
  const tz = process.env.TZ;
@@ -3774,27 +3800,27 @@ function gitIdentity(run, key) {
3774
3800
  return (result.stdout ?? "").trim();
3775
3801
  }
3776
3802
  function isBuildCapable(repo) {
3777
- return repo !== void 0 && existsSync15(join11(repo, ".devcontainer", "Dockerfile"));
3803
+ return repo !== void 0 && existsSync16(join12(repo, ".devcontainer", "Dockerfile"));
3778
3804
  }
3779
3805
  function prepareHomeMount(value) {
3780
3806
  const path4 = resolve7(value);
3781
- if (existsSync15(path4)) {
3807
+ if (existsSync16(path4)) {
3782
3808
  if (!statSync4(path4).isDirectory()) {
3783
3809
  throw new Error(`--home-mount path exists and is not a directory: ${path4}`);
3784
3810
  }
3785
3811
  return path4;
3786
3812
  }
3787
- mkdirSync7(path4, { recursive: true });
3813
+ mkdirSync8(path4, { recursive: true });
3788
3814
  return path4;
3789
3815
  }
3790
3816
  function readSeedConfig(repo) {
3791
3817
  if (repo === void 0) return void 0;
3792
3818
  const file = agroConfigPath(repo);
3793
- return existsSync15(file) ? readAgroConfig(file) : void 0;
3819
+ return existsSync16(file) ? readAgroConfig(file) : void 0;
3794
3820
  }
3795
3821
  function readEntryConfig(name) {
3796
3822
  const file = agroConfigPath(entryRoot(name));
3797
- return existsSync15(file) ? readAgroConfig(file) : void 0;
3823
+ return existsSync16(file) ? readAgroConfig(file) : void 0;
3798
3824
  }
3799
3825
  function assignSetting(target, path4, value) {
3800
3826
  const segments = path4.split(".");
@@ -3891,7 +3917,7 @@ async function runSandboxInstall(opts, io) {
3891
3917
  return 1;
3892
3918
  }
3893
3919
  const checkout = opts.checkout === void 0 ? void 0 : resolve7(opts.checkout);
3894
- if (checkout !== void 0 && !existsSync15(checkout)) {
3920
+ if (checkout !== void 0 && !existsSync16(checkout)) {
3895
3921
  io.stderr(`${opts.bin} sandbox install: --checkout directory does not exist: ${checkout}
3896
3922
  `);
3897
3923
  return 1;
@@ -3910,7 +3936,7 @@ async function runSandboxInstall(opts, io) {
3910
3936
  if (buildRequested && !isBuildCapable(configCheckout(config))) {
3911
3937
  const target = configCheckout(config) ?? resolve7(opts.checkout ?? ".");
3912
3938
  io.stderr(
3913
- `${opts.bin} sandbox install: image.mode is "build" but ${join11(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
3939
+ `${opts.bin} sandbox install: image.mode is "build" but ${join12(target, ".devcontainer", "Dockerfile")} does not exist \u2014 point --checkout <dir> at a harness checkout that has .devcontainer/Dockerfile, or set image.mode to "image" to run the prebuilt image
3914
3940
  `
3915
3941
  );
3916
3942
  return 1;
@@ -3954,7 +3980,7 @@ async function runSandboxInstall(opts, io) {
3954
3980
  ...useNoBuild ? { noBuild: true } : {}
3955
3981
  };
3956
3982
  if (opts.printArgv === true) {
3957
- const preview = mkdtempSync3(join11(tmpdir3(), "oh-sandbox-preview-"));
3983
+ const preview = mkdtempSync3(join12(tmpdir3(), "oh-sandbox-preview-"));
3958
3984
  try {
3959
3985
  writeAgroConfig(preview, config);
3960
3986
  materialize(preview, { ...configCheckout(config) !== void 0 ? { checkout: configCheckout(config) } : {} });
@@ -3964,12 +3990,15 @@ async function runSandboxInstall(opts, io) {
3964
3990
  }
3965
3991
  }
3966
3992
  const root = entryRoot(config.name);
3967
- mkdirSync7(root, { recursive: true });
3993
+ mkdirSync8(root, { recursive: true });
3968
3994
  writeAgroConfig(root, config);
3969
3995
  materialize(root, { ...configCheckout(config) !== void 0 ? { checkout: configCheckout(config) } : {} });
3970
3996
  const code = await runSandbox({ ...sandboxOpts, cwd: root }, io);
3971
- if (code === 0) io.stdout(`next: ${opts.bin} shell ${config.name}
3997
+ if (code === 0) {
3998
+ io.stdout(`next: ${opts.bin} shell ${config.name}
3972
3999
  `);
4000
+ maybePrintStarPrompt(io);
4001
+ }
3973
4002
  return code;
3974
4003
  }
3975
4004
  async function entryStatus(root, name, run) {
@@ -4021,9 +4050,9 @@ async function runSandboxList(opts, io) {
4021
4050
  }
4022
4051
 
4023
4052
  // src/commands/harness.ts
4024
- import { existsSync as existsSync18 } from "node:fs";
4053
+ import { existsSync as existsSync19 } from "node:fs";
4025
4054
  import { homedir as homedir3 } from "node:os";
4026
- import { delimiter as delimiter2, join as join14 } from "node:path";
4055
+ import { delimiter as delimiter2, join as join15 } from "node:path";
4027
4056
 
4028
4057
  // src/lib/docs.ts
4029
4058
  var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
@@ -4036,17 +4065,17 @@ function sourceDocsUrl(docsPath) {
4036
4065
  }
4037
4066
 
4038
4067
  // src/lib/host-config.ts
4039
- import { existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync11, renameSync as renameSync4, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "node:fs";
4040
- import { basename as basename5, dirname as dirname8, isAbsolute, join as join12, resolve as resolve8 } from "node:path";
4068
+ import { existsSync as existsSync17, mkdirSync as mkdirSync9, readFileSync as readFileSync11, renameSync as renameSync4, unlinkSync as unlinkSync5, writeFileSync as writeFileSync9 } from "node:fs";
4069
+ import { basename as basename5, dirname as dirname8, isAbsolute, join as join13, resolve as resolve8 } from "node:path";
4041
4070
  var HOST_CONFIG_MODE = 420;
4042
4071
  var HOST_CONFIG_FILE = "config.json";
4043
4072
  var WORKSPACES_SUBDIR = "workspaces";
4044
- var DEFAULT_WORKSPACE_NAME = "default";
4073
+ var DEFAULT_WORKSPACE_NAME = "harness";
4045
4074
  function hostStateHome(env, home) {
4046
4075
  return resolveUserStateHome(env, home);
4047
4076
  }
4048
4077
  function hostConfigPath(env, home) {
4049
- return join12(hostStateHome(env, home), HOST_CONFIG_FILE);
4078
+ return join13(hostStateHome(env, home), HOST_CONFIG_FILE);
4050
4079
  }
4051
4080
  function assertWorkspaceName(name) {
4052
4081
  if (!SANDBOX_NAME_PATTERN.test(name)) {
@@ -4056,14 +4085,14 @@ function assertWorkspaceName(name) {
4056
4085
  }
4057
4086
  }
4058
4087
  function workspacesRoot(env, home) {
4059
- return join12(hostStateHome(env, home), WORKSPACES_SUBDIR);
4088
+ return join13(hostStateHome(env, home), WORKSPACES_SUBDIR);
4060
4089
  }
4061
4090
  function workspaceRoot(name, env, home) {
4062
4091
  assertWorkspaceName(name);
4063
- return join12(workspacesRoot(env, home), name);
4092
+ return join13(workspacesRoot(env, home), name);
4064
4093
  }
4065
4094
  function defaultHarnessRoot(env, home) {
4066
- return join12(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
4095
+ return join13(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
4067
4096
  }
4068
4097
  function validateHostConfig(value) {
4069
4098
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -4121,7 +4150,7 @@ function validateReceipts(value, field, noun) {
4121
4150
  }
4122
4151
  function readHostConfig(env, home) {
4123
4152
  const path4 = hostConfigPath(env, home);
4124
- if (!existsSync16(path4)) return { version: 1 };
4153
+ if (!existsSync17(path4)) return { version: 1 };
4125
4154
  let raw;
4126
4155
  try {
4127
4156
  raw = readFileSync11(path4, "utf8");
@@ -4143,9 +4172,9 @@ function writeHostConfig(config, env, home) {
4143
4172
  const body = `${JSON.stringify(validated, null, 2)}
4144
4173
  `;
4145
4174
  const tmp = `${path4}.tmp.${process.pid}`;
4146
- mkdirSync8(dirname8(path4), { recursive: true });
4175
+ mkdirSync9(dirname8(path4), { recursive: true });
4147
4176
  try {
4148
- writeFileSync8(tmp, body, { mode: HOST_CONFIG_MODE, encoding: "utf8" });
4177
+ writeFileSync9(tmp, body, { mode: HOST_CONFIG_MODE, encoding: "utf8" });
4149
4178
  renameSync4(tmp, path4);
4150
4179
  } catch (error) {
4151
4180
  try {
@@ -4172,8 +4201,8 @@ function fieldError2(path4, requirement) {
4172
4201
  }
4173
4202
 
4174
4203
  // src/lib/host-workspace.ts
4175
- import { existsSync as existsSync17, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync4, readdirSync as readdirSync3, renameSync as renameSync5, rmSync as rmSync4, statSync as statSync5 } from "node:fs";
4176
- import { dirname as dirname9, join as join13, resolve as resolve9 } from "node:path";
4204
+ import { existsSync as existsSync18, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync4, readdirSync as readdirSync3, renameSync as renameSync5, rmSync as rmSync4, statSync as statSync5 } from "node:fs";
4205
+ import { dirname as dirname9, join as join14, resolve as resolve9 } from "node:path";
4177
4206
  var AGRO_REPO_URL = "https://github.com/mifunedev/agro.git";
4178
4207
  var IGNORABLE_ENTRIES = [
4179
4208
  REGISTRY_SUBDIR,
@@ -4185,7 +4214,7 @@ function isDirectory3(path4) {
4185
4214
  return statSync5(path4, { throwIfNoEntry: false })?.isDirectory() === true;
4186
4215
  }
4187
4216
  function stateHomes(home) {
4188
- return STATE_HOME_DIRS.map((name) => join13(home, name));
4217
+ return STATE_HOME_DIRS.map((name) => join14(home, name));
4189
4218
  }
4190
4219
  function stateHomeRootRefusal(bin, root, home, verb = "harness") {
4191
4220
  const target = resolve9(root);
@@ -4193,7 +4222,7 @@ function stateHomeRootRefusal(bin, root, home, verb = "harness") {
4193
4222
  if (enclosing === void 0) return void 0;
4194
4223
  return `${bin} ${verb}: the ${verb} root ${target} is the state home ${enclosing} itself.
4195
4224
  A workspace there collides with AGRO state and blocks every command run from ${home}.
4196
- Move it out \u2014 for example \`mv ${enclosing} ${join13(home, "agro")}\` \u2014 then re-run this command.
4225
+ Move it out \u2014 for example \`mv ${enclosing} ${join14(home, "agro")}\` \u2014 then re-run this command.
4197
4226
  `;
4198
4227
  }
4199
4228
  function isIgnorableEntry(name) {
@@ -4205,32 +4234,33 @@ function assertCloned(result, root) {
4205
4234
  throw new Error(`could not clone ${AGRO_REPO_URL} into ${root} (exit status ${String(status)})`);
4206
4235
  }
4207
4236
  }
4208
- function cloneInto(target, run, root) {
4209
- assertCloned(run("git", ["clone", AGRO_REPO_URL, target], { stdio: "inherit" }), root);
4237
+ function cloneInto(target, run, root, ref) {
4238
+ const branch = ref !== void 0 ? ["--branch", ref] : [];
4239
+ assertCloned(run("git", ["clone", ...branch, AGRO_REPO_URL, target], { stdio: "inherit" }), root);
4210
4240
  }
4211
- function cloneThroughStaging(root, run) {
4212
- const staging = mkdtempSync4(join13(dirname9(root), "agro-clone-"));
4241
+ function cloneThroughStaging(root, run, ref) {
4242
+ const staging = mkdtempSync4(join14(dirname9(root), "agro-clone-"));
4213
4243
  try {
4214
- const checkout = join13(staging, "agro");
4215
- cloneInto(checkout, run, root);
4244
+ const checkout = join14(staging, "agro");
4245
+ cloneInto(checkout, run, root, ref);
4216
4246
  for (const name of readdirSync3(checkout)) {
4217
- const destination = join13(root, name);
4218
- if (existsSync17(destination)) {
4247
+ const destination = join14(root, name);
4248
+ if (existsSync18(destination)) {
4219
4249
  throw new Error(`could not move ${name} into ${root} \u2014 the path already exists`);
4220
4250
  }
4221
- renameSync5(join13(checkout, name), destination);
4251
+ renameSync5(join14(checkout, name), destination);
4222
4252
  }
4223
4253
  } finally {
4224
4254
  rmSync4(staging, { recursive: true, force: true });
4225
4255
  }
4226
4256
  }
4227
- function ensureHostWorkspace(path4, run = spawnRunner) {
4257
+ function ensureHostWorkspace(path4, run = spawnRunner, ref) {
4228
4258
  const root = resolve9(path4);
4229
- if (existsSync17(join13(root, ".git"))) return { root, action: "reused" };
4259
+ if (existsSync18(join14(root, ".git"))) return { root, action: "reused" };
4230
4260
  const stats = statSync5(root, { throwIfNoEntry: false });
4231
4261
  if (stats === void 0) {
4232
- mkdirSync9(dirname9(root), { recursive: true });
4233
- cloneInto(root, run, root);
4262
+ mkdirSync10(dirname9(root), { recursive: true });
4263
+ cloneInto(root, run, root, ref);
4234
4264
  return { root, action: "cloned" };
4235
4265
  }
4236
4266
  if (!stats.isDirectory()) {
@@ -4242,17 +4272,17 @@ function ensureHostWorkspace(path4, run = spawnRunner) {
4242
4272
  `${root} holds files but no git checkout (${blocking.sort().join(", ")}) \u2014 choose an empty directory or an existing AGRO checkout`
4243
4273
  );
4244
4274
  }
4245
- cloneThroughStaging(root, run);
4275
+ cloneThroughStaging(root, run, ref);
4246
4276
  return { root, action: "cloned" };
4247
4277
  }
4248
4278
  function listHostWorkspaces(env, home) {
4249
4279
  const root = workspacesRoot(env, home);
4250
4280
  if (!isDirectory3(root)) return [];
4251
- return readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => SANDBOX_NAME_PATTERN.test(name)).filter((name) => existsSync17(join13(root, name, ".git"))).sort();
4281
+ return readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => SANDBOX_NAME_PATTERN.test(name)).filter((name) => existsSync18(join14(root, name, ".git"))).sort();
4252
4282
  }
4253
4283
  function resolveExistingWorkspace(bin, verb, path4, env, home) {
4254
4284
  const root = resolve9(path4);
4255
- if (existsSync17(join13(root, ".git"))) return { ok: true, root };
4285
+ if (existsSync18(join14(root, ".git"))) return { ok: true, root };
4256
4286
  const existing = listHostWorkspaces(env, home);
4257
4287
  const inventory = existing.length === 0 ? "No host workspace exists yet.\n" : `Workspaces that exist: ${existing.join(", ")}
4258
4288
  `;
@@ -4282,7 +4312,7 @@ function targetFor(root, run, env) {
4282
4312
  });
4283
4313
  }
4284
4314
  function hostPrefix(home) {
4285
- return join14(home, ".local");
4315
+ return join15(home, ".local");
4286
4316
  }
4287
4317
  function pathEntries(env) {
4288
4318
  return (env.PATH ?? "").split(delimiter2).filter((part) => part !== "");
@@ -4335,7 +4365,7 @@ function probeableHost(env, home) {
4335
4365
  return void 0;
4336
4366
  }
4337
4367
  const prefix = hostPrefix(home);
4338
- if (!existsSync18(join14(root, ".git")) && !existsSync18(prefix)) return void 0;
4368
+ if (!existsSync19(join15(root, ".git")) && !existsSync19(prefix)) return void 0;
4339
4369
  return { root, prefix };
4340
4370
  }
4341
4371
  async function collectStates(root, run, home, env, only) {
@@ -4431,6 +4461,19 @@ async function runHarnessStatus(name, opts, io) {
4431
4461
  function hermesTargetRoot(target) {
4432
4462
  return target.kind === "docker-compose" ? "/home/sandbox/harness" : target.workspace.targetRoot;
4433
4463
  }
4464
+ async function reconcileHermes(target, io, bin, user, root = hermesTargetRoot(target)) {
4465
+ const result = await target.exec({
4466
+ argv: remoteControlDirScript(root, "scripts/link-providers.sh", ["--init", "--hermes-only"]),
4467
+ env: agroEnvPair("PROJECT_ROOT", root),
4468
+ ...user ? { user } : {},
4469
+ stdio: "inherit"
4470
+ });
4471
+ if (result.exitCode !== 0) {
4472
+ io.stderr(`${bin} harness: Hermes integration failed (exit ${result.exitCode}); no installation success reported.
4473
+ `);
4474
+ }
4475
+ return result.exitCode;
4476
+ }
4434
4477
  function unreachableReason(status) {
4435
4478
  return runtimeIsAbsent(status) ? "No container runtime is on PATH." : `The sandbox is not running (${status}).`;
4436
4479
  }
@@ -4513,7 +4556,7 @@ async function installOnHost(entry, opts, io, run, status) {
4513
4556
  });
4514
4557
  if (linked.exitCode !== 0) {
4515
4558
  io.stderr(
4516
- `${bin} harness: could not link provider hooks in ${root} (exit ${linked.exitCode}); nothing was installed.
4559
+ `${bin} harness: could not link provider skills in ${root} (exit ${linked.exitCode}); nothing was installed.
4517
4560
  Run: bash ${root}/.agro/scripts/link-providers.sh --init
4518
4561
  `
4519
4562
  );
@@ -4524,6 +4567,10 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
4524
4567
  ...agroEnvPair("PROJECT_ROOT", root),
4525
4568
  HERMES_HOME: `${root}/.hermes`
4526
4569
  } : void 0;
4570
+ if (hermes) {
4571
+ const code = await reconcileHermes(target, io, bin, void 0, root);
4572
+ if (code !== 0) return code;
4573
+ }
4527
4574
  if (await probeInstalled(target, entry, prefix, void 0, installEnv) === true) {
4528
4575
  io.stdout(`${entry.id}: already installed (${entry.binary})
4529
4576
  `);
@@ -4554,6 +4601,8 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
4554
4601
  `);
4555
4602
  return 1;
4556
4603
  }
4604
+ const code = await reconcileHermes(target, io, bin, void 0, root);
4605
+ if (code !== 0) return code;
4557
4606
  }
4558
4607
  const receipt = {
4559
4608
  prefix,
@@ -4707,6 +4756,10 @@ async function runHarnessInstall(name, opts, io) {
4707
4756
  ...agroEnvPair("PROJECT_ROOT", hermesTargetRoot(target)),
4708
4757
  HERMES_HOME: `${hermesTargetRoot(target)}/.hermes`
4709
4758
  } : void 0;
4759
+ if (hermes) {
4760
+ const code = await reconcileHermes(target, io, opts.bin, "sandbox");
4761
+ if (code !== 0) return code;
4762
+ }
4710
4763
  const already = await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox", installEnv);
4711
4764
  if (already === true) {
4712
4765
  io.stdout(`${entry.id}: already installed (${entry.binary})
@@ -4736,6 +4789,8 @@ async function runHarnessInstall(name, opts, io) {
4736
4789
  `);
4737
4790
  return 1;
4738
4791
  }
4792
+ const code = await reconcileHermes(target, io, opts.bin, "sandbox");
4793
+ if (code !== 0) return code;
4739
4794
  }
4740
4795
  io.stdout(`${entry.id}: installed \u2014 see ${sourceDocsUrl(entry.docsPath)} for authentication
4741
4796
  `);
@@ -4743,9 +4798,9 @@ async function runHarnessInstall(name, opts, io) {
4743
4798
  }
4744
4799
 
4745
4800
  // src/commands/workspace.ts
4746
- import { existsSync as existsSync19 } from "node:fs";
4801
+ import { existsSync as existsSync20, rmSync as rmSync5 } from "node:fs";
4747
4802
  import { homedir as homedir4 } from "node:os";
4748
- import { join as join15, resolve as resolve10 } from "node:path";
4803
+ import { join as join16, resolve as resolve10 } from "node:path";
4749
4804
  function homeOf2(opts) {
4750
4805
  return (opts.homedir ?? homedir4)();
4751
4806
  }
@@ -4791,15 +4846,25 @@ async function runWorkspaceCreate(name, opts, io) {
4791
4846
  return 1;
4792
4847
  }
4793
4848
  let action;
4849
+ const preexisting = existsSync20(root);
4794
4850
  try {
4795
- if (!opts.json && !existsSync19(join15(root, ".git"))) {
4796
- io.stdout(`cloning ${AGRO_REPO_URL} into ${root}\u2026
4851
+ if (!opts.json && !existsSync20(join16(root, ".git"))) {
4852
+ const at = opts.ref !== void 0 ? ` at ${opts.ref}` : "";
4853
+ io.stdout(`cloning ${AGRO_REPO_URL}${at} into ${root}\u2026
4797
4854
  `);
4798
4855
  }
4799
- const created = ensureHostWorkspace(root, run);
4856
+ const created = ensureHostWorkspace(root, run, opts.ref);
4800
4857
  root = created.root;
4801
4858
  action = created.action;
4802
4859
  } catch (err) {
4860
+ if (opts.ref !== void 0) {
4861
+ if (!preexisting) rmSync5(root, { recursive: true, force: true });
4862
+ io.stderr(
4863
+ `${bin} workspace: could not clone ref "${opts.ref}" \u2014 the ref does not exist in ${AGRO_REPO_URL} or the clone failed (${messageOf2(err)})
4864
+ `
4865
+ );
4866
+ return 1;
4867
+ }
4803
4868
  io.stderr(`${bin} workspace: ${messageOf2(err)}
4804
4869
  `);
4805
4870
  return 1;
@@ -4828,7 +4893,7 @@ async function runWorkspaceList(opts, io) {
4828
4893
  const recorded = recordedRoot(env, home);
4829
4894
  const registry = workspacesRoot(env, home);
4830
4895
  rows = listHostWorkspaces(env, home).map((name) => {
4831
- const root = join15(registry, name);
4896
+ const root = join16(registry, name);
4832
4897
  return { name, root, default: recorded === root };
4833
4898
  });
4834
4899
  } catch (err) {
@@ -4853,9 +4918,9 @@ async function runWorkspaceList(opts, io) {
4853
4918
  }
4854
4919
 
4855
4920
  // src/commands/tool.ts
4856
- import { existsSync as existsSync20 } from "node:fs";
4857
- import { homedir as homedir5 } from "node:os";
4858
- import { delimiter as delimiter3, join as join16 } from "node:path";
4921
+ import { existsSync as existsSync21 } from "node:fs";
4922
+ import { homedir as homedir5, userInfo as userInfo2 } from "node:os";
4923
+ import { delimiter as delimiter3, join as join17 } from "node:path";
4859
4924
 
4860
4925
  // src/lib/tools/catalog.ts
4861
4926
  var TOOLS_DOC = "docs/installation.md";
@@ -5076,6 +5141,178 @@ install -d -m 0700 "$HOME/.tailscale"`
5076
5141
  `${HARNESS_PREFIX_TOKEN}/bin/tailscaled`
5077
5142
  ]),
5078
5143
  docsPath: TOOLS_DOC
5144
+ }),
5145
+ Object.freeze({
5146
+ id: "code-server",
5147
+ title: "code-server",
5148
+ kind: "installable",
5149
+ binary: "code-server",
5150
+ verifyArgv: Object.freeze(["bash", "-lc", "command -v code-server >/dev/null"]),
5151
+ versionArgv: Object.freeze(["code-server", "--version"]),
5152
+ installArgv: Object.freeze([
5153
+ "bash",
5154
+ "-lc",
5155
+ [
5156
+ "set -e",
5157
+ "version=4.129.0",
5158
+ 'arch="$(dpkg --print-architecture)"',
5159
+ 'case "$arch" in',
5160
+ " amd64) sha=889b09ff3a167a293f53cb68a5a7f38dbab6bd2b50d7a5951c757e56ba51a2b0 ;;",
5161
+ " arm64) sha=62f7886018923a18cc16112ccfbcd51aee80f8e0c1bb7abc48773d1fd32a7617 ;;",
5162
+ ' *) echo "no pinned code-server build for $arch" >&2; exit 1 ;;',
5163
+ "esac",
5164
+ 'prefix="${NPM_USER_PREFIX:-$HOME/.local}"',
5165
+ 'dest="$prefix/lib/code-server-$version"',
5166
+ 'tmp="$(mktemp -d)"',
5167
+ `trap 'rm -rf "$tmp"' EXIT`,
5168
+ 'curl -fsSL "https://github.com/coder/code-server/releases/download/v$version/code-server-$version-linux-$arch.tar.gz" -o "$tmp/code-server.tar.gz"',
5169
+ 'echo "$sha $tmp/code-server.tar.gz" | sha256sum -c -',
5170
+ 'install -d "$tmp/release" "$prefix/lib" "$prefix/bin"',
5171
+ 'tar -xzf "$tmp/code-server.tar.gz" -C "$tmp/release" --strip-components=1',
5172
+ 'rm -rf "$dest"',
5173
+ 'mv "$tmp/release" "$dest"',
5174
+ 'ln -sfn "$dest/bin/code-server" "$prefix/bin/code-server"',
5175
+ 'if ! "$prefix/bin/code-server" --version | grep -q "^$version "; then',
5176
+ ' echo "$prefix/bin/code-server does not report version $version" >&2',
5177
+ " exit 1",
5178
+ "fi"
5179
+ ].join("\n")
5180
+ ]),
5181
+ installUser: "sandbox",
5182
+ hostCapable: true,
5183
+ uninstallArgv: Object.freeze([
5184
+ "rm",
5185
+ "-rf",
5186
+ `${HARNESS_PREFIX_TOKEN}/bin/code-server`,
5187
+ `${HARNESS_PREFIX_TOKEN}/lib/code-server-4.129.0`
5188
+ ]),
5189
+ docsPath: TOOLS_DOC
5190
+ }),
5191
+ Object.freeze({
5192
+ id: "docker-engine",
5193
+ title: "Docker Engine + Compose",
5194
+ kind: "installable",
5195
+ binary: "docker",
5196
+ verifyArgv: Object.freeze([
5197
+ "bash",
5198
+ "-lc",
5199
+ 'command -v docker >/dev/null && docker compose version >/dev/null && systemctl is-enabled --quiet docker && id -nG "${SUDO_USER:-$(id -un)}" | tr " " "\\n" | grep -qx docker'
5200
+ ]),
5201
+ versionArgv: Object.freeze(["docker", "--version"]),
5202
+ hostInstallArgv: Object.freeze([
5203
+ "bash",
5204
+ "-lc",
5205
+ [
5206
+ "set -e",
5207
+ ". /etc/os-release",
5208
+ 'if [ "${ID:-}" != ubuntu ]; then',
5209
+ ' echo "docker-engine installs from Docker\'s Ubuntu repository; this host is ${ID:-unknown}" >&2',
5210
+ " exit 1",
5211
+ "fi",
5212
+ "fingerprint=9DC858229FC7DD38854AE2D88D81803C0EBFCD88",
5213
+ 'user="${SUDO_USER:-$(id -un)}"',
5214
+ "export DEBIAN_FRONTEND=noninteractive",
5215
+ 'tmp="$(mktemp -d)"',
5216
+ `trap 'rm -rf "$tmp"' EXIT`,
5217
+ "apt-get update",
5218
+ "apt-get install -y ca-certificates curl gnupg",
5219
+ "install -m 0755 -d /etc/apt/keyrings",
5220
+ 'curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o "$tmp/docker.asc"',
5221
+ `actual="$(gpg --homedir "$tmp" --show-keys --with-colons "$tmp/docker.asc" | awk -F: '$1 == "fpr" { print $10; exit }')"`,
5222
+ 'if [ "$actual" != "$fingerprint" ]; then',
5223
+ ' echo "Docker\'s repository key has fingerprint ${actual:-none}, expected $fingerprint" >&2',
5224
+ " exit 1",
5225
+ "fi",
5226
+ 'install -m 0644 "$tmp/docker.asc" /etc/apt/keyrings/docker.asc',
5227
+ 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu ${UBUNTU_CODENAME:-$VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list',
5228
+ "apt-get update",
5229
+ "apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin",
5230
+ 'usermod -aG docker "$user"',
5231
+ "systemctl enable --now docker",
5232
+ 'echo "$user is in the docker group. Log out and back in before you run docker without sudo."'
5233
+ ].join("\n")
5234
+ ]),
5235
+ hostInstallUser: "root",
5236
+ notInstallableReason: (bin) => `Docker Engine installs on the host only, with \`${bin} tool install docker-engine --host\`. To use Docker inside the sandbox, set \`access.dockerSocket\` to true in agro.json on the host and recreate the sandbox. That mounts the host Docker socket, which is effectively host root.`,
5237
+ hostCapable: true,
5238
+ uninstallArgv: null,
5239
+ docsPath: TOOLS_DOC
5240
+ }),
5241
+ Object.freeze({
5242
+ id: "desktop",
5243
+ title: "XFCE desktop over XRDP",
5244
+ kind: "installable",
5245
+ binary: "xrdp",
5246
+ verifyArgv: Object.freeze([
5247
+ "bash",
5248
+ "-lc",
5249
+ 'command -v xrdp >/dev/null && systemctl is-enabled --quiet xrdp && systemctl is-enabled --quiet tailscaled && test -f /etc/systemd/system/xrdp.service.d/agro-tailscale-only.conf && grep -qx xfce4-session "$HOME/.xsession"'
5250
+ ]),
5251
+ hostInstallArgv: Object.freeze([
5252
+ "bash",
5253
+ "-lc",
5254
+ [
5255
+ "set -e",
5256
+ ". /etc/os-release",
5257
+ 'if [ "${ID:-}" != ubuntu ]; then',
5258
+ ' echo "desktop installs Ubuntu packages; this host is ${ID:-unknown}" >&2',
5259
+ " exit 1",
5260
+ "fi",
5261
+ 'user="${SUDO_USER:-$(id -un)}"',
5262
+ 'home="$(getent passwd "$user" | cut -d: -f6)"',
5263
+ 'codename="${UBUNTU_CODENAME:-$VERSION_CODENAME}"',
5264
+ "export DEBIAN_FRONTEND=noninteractive",
5265
+ 'tmp="$(mktemp -d)"',
5266
+ `trap 'rm -rf "$tmp"' EXIT`,
5267
+ "apt-get update",
5268
+ "apt-get install -y ca-certificates curl gnupg nftables",
5269
+ "if ! systemctl cat tailscaled.service >/dev/null 2>&1; then",
5270
+ " fingerprint=2596A99EAAB33821893C0A79458CA832957F5868",
5271
+ ' curl -fsSL "https://pkgs.tailscale.com/stable/ubuntu/$codename.noarmor.gpg" -o "$tmp/tailscale.gpg"',
5272
+ ` actual="$(gpg --homedir "$tmp" --show-keys --with-colons "$tmp/tailscale.gpg" | awk -F: '$1 == "fpr" { print $10; exit }')"`,
5273
+ ' if [ "$actual" != "$fingerprint" ]; then',
5274
+ ' echo "Tailscale\'s repository key has fingerprint ${actual:-none}, expected $fingerprint" >&2',
5275
+ " exit 1",
5276
+ " fi",
5277
+ ' install -m 0644 "$tmp/tailscale.gpg" /usr/share/keyrings/tailscale-archive-keyring.gpg',
5278
+ ' echo "deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/ubuntu $codename main" > /etc/apt/sources.list.d/tailscale.list',
5279
+ " apt-get update",
5280
+ " apt-get install -y tailscale",
5281
+ "fi",
5282
+ "systemctl enable --now tailscaled",
5283
+ "install -d -m 0755 /etc/xrdp /etc/systemd/system/xrdp.service.d",
5284
+ "cat > /etc/xrdp/agro-tailscale-only.nft <<'EOF'",
5285
+ "table inet agro_xrdp",
5286
+ "delete table inet agro_xrdp",
5287
+ "table inet agro_xrdp {",
5288
+ " chain input {",
5289
+ " type filter hook input priority filter; policy accept;",
5290
+ ' iifname { "lo", "tailscale0" } accept',
5291
+ " tcp dport 3389 reject with tcp reset",
5292
+ " }",
5293
+ "}",
5294
+ "EOF",
5295
+ "nft -f /etc/xrdp/agro-tailscale-only.nft",
5296
+ `printf '[Service]\\nExecStartPre=%s -f /etc/xrdp/agro-tailscale-only.nft\\n' "$(command -v nft)" > /etc/systemd/system/xrdp.service.d/agro-tailscale-only.conf`,
5297
+ "systemctl daemon-reload",
5298
+ "apt-get install -y xfce4 xfce4-goodies xrdp xorgxrdp",
5299
+ `printf '%s\\n' xfce4-session > "$home/.xsession"`,
5300
+ 'chown "$user:$(id -gn "$user")" "$home/.xsession"',
5301
+ "adduser xrdp ssl-cert",
5302
+ "systemctl daemon-reload",
5303
+ "systemctl enable xrdp",
5304
+ "systemctl restart xrdp",
5305
+ "systemctl is-active --quiet xrdp",
5306
+ 'echo "The desktop is installed. Two steps remain:"',
5307
+ 'echo " sudo tailscale up"',
5308
+ 'echo " sudo passwd $user"'
5309
+ ].join("\n")
5310
+ ]),
5311
+ hostInstallUser: "root",
5312
+ notInstallableReason: (bin) => `The desktop installs on the host only, with \`${bin} tool install desktop --host\`. It installs Tailscale and serves XFCE over XRDP on TCP 3389 through Tailscale only.`,
5313
+ hostCapable: true,
5314
+ uninstallArgv: null,
5315
+ docsPath: TOOLS_DOC
5079
5316
  })
5080
5317
  ]);
5081
5318
  function findTool(id) {
@@ -5113,7 +5350,7 @@ function targetFor2(root, run, env) {
5113
5350
  });
5114
5351
  }
5115
5352
  function hostPrefix2(home) {
5116
- return join16(home, ".local");
5353
+ return join17(home, ".local");
5117
5354
  }
5118
5355
  function pathEntries2(env) {
5119
5356
  return (env.PATH ?? "").split(delimiter3).filter((part) => part !== "");
@@ -5138,6 +5375,10 @@ function homeOf3(opts) {
5138
5375
  function platformOf(opts) {
5139
5376
  return opts.platform ?? process.platform;
5140
5377
  }
5378
+ function isRootUser(opts) {
5379
+ const identity = opts.identity ?? (() => ({ name: userInfo2().username, uid: userInfo2().uid }));
5380
+ return identity().uid === 0;
5381
+ }
5141
5382
  function isInteractive2(opts) {
5142
5383
  return opts.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true);
5143
5384
  }
@@ -5176,6 +5417,7 @@ function rowOf(entry, installed, version, location) {
5176
5417
  version,
5177
5418
  installable: entry.installArgv !== void 0,
5178
5419
  hostCapable: entry.hostCapable,
5420
+ hostRoot: entry.hostInstallUser === "root",
5179
5421
  location,
5180
5422
  docs: sourceDocsUrl(entry.docsPath)
5181
5423
  };
@@ -5188,7 +5430,7 @@ function probeableHost2(env, home) {
5188
5430
  return void 0;
5189
5431
  }
5190
5432
  const prefix = hostPrefix2(home);
5191
- if (!existsSync20(join16(root, ".git")) && !existsSync20(prefix)) return void 0;
5433
+ if (!existsSync21(join17(root, ".git")) && !existsSync21(prefix)) return void 0;
5192
5434
  return { root, prefix };
5193
5435
  }
5194
5436
  async function collectRows(root, run, home, env, only) {
@@ -5223,13 +5465,20 @@ function cell2(value, absent) {
5223
5465
  function renderTable3(collected, io, bin) {
5224
5466
  const rows = collected.rows;
5225
5467
  const header2 = ["TOOL", "KIND", "INSTALLED"];
5226
- const body = rows.map((r) => [r.id, r.kind, cell2(r.installed, "?")]);
5468
+ const body = rows.map((r) => [
5469
+ r.id,
5470
+ r.hostRoot ? `${r.kind} (root)` : r.kind,
5471
+ cell2(r.installed, "?")
5472
+ ]);
5227
5473
  const widths = header2.map(
5228
5474
  (h, i) => Math.max(h.length, ...body.map((b) => b[i].length))
5229
5475
  );
5230
5476
  const line = (cols) => cols.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd() + "\n";
5231
5477
  io.stdout(line(header2));
5232
5478
  for (const row of body) io.stdout(line(row));
5479
+ if (rows.some((r) => r.hostRoot)) {
5480
+ io.stdout("\n(root) installs on the host as root through `sudo -n`.\n");
5481
+ }
5233
5482
  if (rows.some((r) => r.location === "host")) {
5234
5483
  io.stdout(
5235
5484
  `
@@ -5409,11 +5658,21 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5409
5658
  }
5410
5659
  return 0;
5411
5660
  }
5661
+ const sudo = entry.hostInstallUser === "root" && !isRootUser(opts);
5662
+ if (sudo && (await tryExec(target, ["sudo", "-n", "true"], void 0))?.exitCode !== 0) {
5663
+ io.stderr(
5664
+ `${bin} tool: ${entry.id} installs as root on the host and needs passwordless \`sudo\`.
5665
+ \`sudo -n true\` failed. Configure passwordless sudo for this user, or run as root.
5666
+ `
5667
+ );
5668
+ return 1;
5669
+ }
5412
5670
  if (!await confirmDownload(entry, opts, io, true)) return 1;
5413
5671
  io.stdout(`installing ${entry.title} on the host\u2026
5414
5672
  `);
5673
+ const argv = resolveToolInstallArgv(entry, true);
5415
5674
  const r = await target.exec({
5416
- argv: resolveToolInstallArgv(entry, true),
5675
+ argv: sudo ? ["sudo", "-n", "--", ...argv] : argv,
5417
5676
  stdio: "inherit",
5418
5677
  env: installEnv
5419
5678
  });
@@ -5445,6 +5704,11 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5445
5704
  `);
5446
5705
  return 1;
5447
5706
  }
5707
+ if (entry.hostInstallUser === "root") {
5708
+ io.stdout(`${entry.id} installed on the host \u2014 see ${sourceDocsUrl(entry.docsPath)}
5709
+ `);
5710
+ return 0;
5711
+ }
5448
5712
  io.stdout(`${entry.id}: installed at ${prefix} \u2014 see ${sourceDocsUrl(entry.docsPath)}
5449
5713
  `);
5450
5714
  if (!onPath2(prefix, env)) {
@@ -5460,7 +5724,8 @@ async function runToolInstall(name, opts, io) {
5460
5724
  const root = resolveProjectRoot(opts.cwd);
5461
5725
  const entry = findTool(name);
5462
5726
  if (!entry) return unknownTool(name, io, opts.bin);
5463
- if (entry.installArgv === void 0) {
5727
+ const hostOnly = entry.installArgv === void 0 && entry.hostInstallArgv !== void 0;
5728
+ const refuseContainer = () => {
5464
5729
  io.stderr(`${opts.bin} tool: ${entry.id} cannot be installed by this command.
5465
5730
 
5466
5731
  `);
@@ -5471,12 +5736,16 @@ async function runToolInstall(name, opts, io) {
5471
5736
  ${installableToolIds().map((t) => ` ${t}`).join("\n")}
5472
5737
  `);
5473
5738
  return 1;
5739
+ };
5740
+ if (entry.installArgv === void 0 && (!hostOnly || runningInsideSandbox(opts.env ?? process.env))) {
5741
+ return refuseContainer();
5474
5742
  }
5475
5743
  const target = targetFor2(root, run, opts.env);
5476
5744
  const status = await resolveTargetStatus(target);
5477
- if (!isReachable2(status)) {
5745
+ if (!isReachable2(status) || hostOnly && opts.host === true) {
5478
5746
  return await installOnHost2(entry, opts, io, run, status);
5479
5747
  }
5748
+ if (entry.installArgv === void 0) return refuseContainer();
5480
5749
  const already = await probeInstalled2(target, entry, "sandbox");
5481
5750
  if (already === true) {
5482
5751
  io.stdout(`${entry.id}: already installed (${entry.binary})
@@ -5926,8 +6195,8 @@ Usage:
5926
6195
 
5927
6196
  \`install\` and \`uninstall\` act on the running sandbox. When no sandbox is
5928
6197
  reachable they act on the host. A host install needs an existing AGRO workspace
5929
- and creates none \u2014 run \`${bin} workspace create <name>\` first. The default root is
5930
- the workspace registry entry \`~/.agro/workspaces/default\`. \`--workspace <name>\`
6198
+ and creates none \u2014 run \`${bin} workspace create\` first. The fallback root is
6199
+ the workspace registry entry \`~/.agro/workspaces/harness\`. \`--workspace <name>\`
5931
6200
  chooses another entry and \`--path <dir>\` a directory outside the registry. A
5932
6201
  successful install records the root as \`harnessRoot\` in \`~/.agro/config.json\`. The
5933
6202
  install prefix is always \`~/.local\`.
@@ -5951,13 +6220,14 @@ function printWorkspaceHelp(bin = AGRO_PRODUCT.bin) {
5951
6220
  process.stdout.write(`${bin} workspace \u2014 Create and list host AGRO workspaces
5952
6221
 
5953
6222
  Usage:
5954
- ${bin} workspace create [<name>] Clone the AGRO repository into a registry entry
6223
+ ${bin} workspace create [<name>] [--ref <ref>]
6224
+ Clone the AGRO repository into a registry entry
5955
6225
  ${bin} workspace list List every host workspace
5956
6226
 
5957
6227
  A host workspace is an AGRO checkout under \`~/.agro/workspaces/<name>\`. \`create\`
5958
6228
  clones ${AGRO_REPO_URL} into that entry, and reuses an existing checkout. The
5959
- default name is \`default\`. \`--path <dir>\` creates the workspace outside the
5960
- registry instead.
6229
+ implicit name is \`harness\`. Pass \`default\` to create that named workspace.
6230
+ \`--path <dir>\` creates the workspace outside the registry instead.
5961
6231
 
5962
6232
  \`create\` records no default. A host install selects its root with
5963
6233
  \`${bin} harness install <harness> --workspace <name>\`, and that install records the
@@ -5966,6 +6236,9 @@ root as \`harnessRoot\`. \`list\` marks the recorded root.
5966
6236
  Flags:
5967
6237
  --json Machine-readable output
5968
6238
  --path <dir> Create the workspace at a directory outside the registry
6239
+ --ref <ref> Clone this branch or tag instead of the default branch (create
6240
+ only). A missing ref exits 1 and leaves no target directory.
6241
+ An existing checkout is reused as is.
5969
6242
  `);
5970
6243
  }
5971
6244
  function printGatewayHelp(bin = AGRO_PRODUCT.bin) {
@@ -6017,8 +6290,8 @@ only on these tools:
6017
6290
  ${hostCapableToolIds().map((t) => ` ${t}`).join("\n")}
6018
6291
 
6019
6292
  A host install needs an existing AGRO workspace \u2014 \`--path <dir>\`, then
6020
- \`harnessRoot\` in \`~/.agro/config.json\`, then \`~/.agro/workspaces/default\`. It creates
6021
- no workspace: run \`${bin} workspace create <name>\` first. It installs into
6293
+ \`harnessRoot\` in \`~/.agro/config.json\`, then \`~/.agro/workspaces/harness\`. It creates
6294
+ no workspace: run \`${bin} workspace create\` first. It installs into
6022
6295
  \`~/.local\` and records the install as \`hostTools\` in that same file. On the host,
6023
6296
  \`uninstall\` removes only what \`install\` recorded, from the prefix in that record.
6024
6297
  Without a record it refuses, and \`--force\` overrides.
@@ -6469,16 +6742,24 @@ function parseWorkspaceArgs(rest, bin = AGRO_PRODUCT.bin) {
6469
6742
  }
6470
6743
  const positionals = [];
6471
6744
  let expectPath = false;
6745
+ let expectRef = false;
6472
6746
  for (const token of rest) {
6473
6747
  if (expectPath) {
6474
6748
  expectPath = false;
6475
6749
  args.path = token;
6750
+ } else if (expectRef) {
6751
+ expectRef = false;
6752
+ args.ref = token;
6476
6753
  } else if (token === "--json") {
6477
6754
  args.json = true;
6478
6755
  } else if (token === "--path") {
6479
6756
  expectPath = true;
6480
6757
  } else if (token.startsWith("--path=")) {
6481
6758
  args.path = token.slice("--path=".length);
6759
+ } else if (token === "--ref") {
6760
+ expectRef = true;
6761
+ } else if (token.startsWith("--ref=")) {
6762
+ args.ref = token.slice("--ref=".length);
6482
6763
  } else if (token.startsWith("-")) {
6483
6764
  return { ok: false, error: `${bin} workspace: unknown flag "${token}"` };
6484
6765
  } else {
@@ -6488,6 +6769,9 @@ function parseWorkspaceArgs(rest, bin = AGRO_PRODUCT.bin) {
6488
6769
  if (expectPath || args.path === "") {
6489
6770
  return { ok: false, error: `${bin} workspace: --path requires a directory` };
6490
6771
  }
6772
+ if (expectRef || args.ref === "") {
6773
+ return { ok: false, error: `${bin} workspace: --ref requires a branch or tag` };
6774
+ }
6491
6775
  const [sub, name, ...extra] = positionals;
6492
6776
  if (sub !== "create" && sub !== "list") {
6493
6777
  return {
@@ -6505,6 +6789,9 @@ function parseWorkspaceArgs(rest, bin = AGRO_PRODUCT.bin) {
6505
6789
  if (sub === "list" && args.path !== void 0) {
6506
6790
  return { ok: false, error: `${bin} workspace list: --path applies to create only` };
6507
6791
  }
6792
+ if (sub === "list" && args.ref !== void 0) {
6793
+ return { ok: false, error: `${bin} workspace list: --ref applies to create only` };
6794
+ }
6508
6795
  if (args.path !== void 0 && name !== void 0) {
6509
6796
  return {
6510
6797
  ok: false,
@@ -6661,11 +6948,11 @@ function parseGatewayArgs(rest) {
6661
6948
  }
6662
6949
  return { ok: true, args: { help: false, passthrough: [...rest] } };
6663
6950
  }
6664
- function bundledPayloadExists(bundled, exists = existsSync21) {
6665
- return exists(join17(bundled.sourceControlDir, "manifest.json"));
6951
+ function bundledPayloadExists(bundled, exists = existsSync22) {
6952
+ return exists(join18(bundled.sourceControlDir, "manifest.json"));
6666
6953
  }
6667
6954
  function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
6668
- const exists = bundled.exists ?? existsSync21;
6955
+ const exists = bundled.exists ?? existsSync22;
6669
6956
  if (args.fromDir !== void 0) {
6670
6957
  return { kind: "local", fromDir: resolve11(args.fromDir) };
6671
6958
  }
@@ -6685,7 +6972,7 @@ function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
6685
6972
  function readPayloadVersion(checkoutDir) {
6686
6973
  try {
6687
6974
  const parsed = JSON.parse(
6688
- readFileSync12(join17(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
6975
+ readFileSync12(join18(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
6689
6976
  );
6690
6977
  if (parsed && typeof parsed.version === "string") return parsed.version;
6691
6978
  } catch {
@@ -6694,8 +6981,8 @@ function readPayloadVersion(checkoutDir) {
6694
6981
  }
6695
6982
  async function runWithRemoteSource(hooks, fn) {
6696
6983
  const fetch2 = hooks.fetch ?? fetchRemoteSource;
6697
- const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join17(tmpdir4(), "oh-remote-")));
6698
- const rm = hooks.rm ?? ((dir) => rmSync5(dir, { recursive: true, force: true }));
6984
+ const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join18(tmpdir4(), "oh-remote-")));
6985
+ const rm = hooks.rm ?? ((dir) => rmSync6(dir, { recursive: true, force: true }));
6699
6986
  const out = hooks.stdout ?? ((s) => process.stdout.write(s));
6700
6987
  const checkoutDir = mkdtemp();
6701
6988
  try {
@@ -6957,7 +7244,12 @@ async function main(argv) {
6957
7244
  }
6958
7245
  return await runWorkspaceCreate(
6959
7246
  a.name,
6960
- { bin, json: a.json, ...a.path !== void 0 ? { path: a.path } : {} },
7247
+ {
7248
+ bin,
7249
+ json: a.json,
7250
+ ...a.path !== void 0 ? { path: a.path } : {},
7251
+ ...a.ref !== void 0 ? { ref: a.ref } : {}
7252
+ },
6961
7253
  io
6962
7254
  );
6963
7255
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mifune/agro",
3
- "version": "0.15.0-minimal.1",
3
+ "version": "0.15.1",
4
4
  "description": "AGRO CLI — create and manage sandboxes for coding agents.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/mifunedev/agro#readme",