@mifune/agro 0.12.0 → 0.12.2

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 (3) hide show
  1. package/dist/agro.js +200 -65
  2. package/dist/oh.js +200 -65
  3. package/package.json +1 -1
package/dist/agro.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { fileURLToPath } from "node:url";
5
- import { dirname as dirname10, join as join14, resolve as resolve14 } from "node:path";
5
+ import { dirname as dirname10, join as join14, resolve as resolve13 } from "node:path";
6
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
 
@@ -230,6 +230,11 @@ 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 resolveAgroUserStateHome(env = process.env, home = homedir(), warn) {
234
+ const configured = aliasedEnvValue(env, "HOME", warn);
235
+ if (configured !== void 0) return resolve(configured);
236
+ return join(home, GENERATIONS.agro.userStateDir);
237
+ }
233
238
  function resolveUserStateHome(env = process.env, home = homedir(), warn) {
234
239
  const configured = aliasedEnvValue(env, "HOME", warn);
235
240
  if (configured !== void 0) return resolve(configured);
@@ -1959,8 +1964,8 @@ function redact(token) {
1959
1964
  async function ask(question) {
1960
1965
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1961
1966
  try {
1962
- return await new Promise((resolve15) => {
1963
- rl.question(` ${question} `, (answer) => resolve15(answer.trim()));
1967
+ return await new Promise((resolve14) => {
1968
+ rl.question(` ${question} `, (answer) => resolve14(answer.trim()));
1964
1969
  });
1965
1970
  } finally {
1966
1971
  rl.close();
@@ -1972,7 +1977,7 @@ async function askSecret(question) {
1972
1977
  process.stdin.setRawMode(true);
1973
1978
  process.stdin.resume();
1974
1979
  const bytes = [];
1975
- return await new Promise((resolve15) => {
1980
+ return await new Promise((resolve14) => {
1976
1981
  const cleanup = () => {
1977
1982
  try {
1978
1983
  process.stdin.setRawMode(false);
@@ -1990,7 +1995,7 @@ async function askSecret(question) {
1990
1995
  } else if (byte === 13 || byte === 10) {
1991
1996
  cleanup();
1992
1997
  process.stdout.write("\n");
1993
- resolve15(Buffer.from(bytes).toString("utf8").trim());
1998
+ resolve14(Buffer.from(bytes).toString("utf8").trim());
1994
1999
  return;
1995
2000
  } else if (byte === 127 || byte === 8) {
1996
2001
  if (bytes.length > 0) {
@@ -2040,10 +2045,10 @@ async function askSecretPiped(question) {
2040
2045
  };
2041
2046
  process.on("SIGINT", onSigint);
2042
2047
  try {
2043
- return await new Promise((resolve15) => {
2048
+ return await new Promise((resolve14) => {
2044
2049
  rl.question(` ${question} `, (answer) => {
2045
2050
  process.stdout.write("\n");
2046
- resolve15(answer.trim());
2051
+ resolve14(answer.trim());
2047
2052
  });
2048
2053
  });
2049
2054
  } finally {
@@ -4001,7 +4006,7 @@ async function runSandboxList(opts, io) {
4001
4006
  // src/commands/harness.ts
4002
4007
  import { existsSync as existsSync18 } from "node:fs";
4003
4008
  import { homedir as homedir2 } from "node:os";
4004
- import { delimiter as delimiter2, join as join12, resolve as resolve12 } from "node:path";
4009
+ import { delimiter as delimiter2, join as join12 } from "node:path";
4005
4010
 
4006
4011
  // src/lib/docs.ts
4007
4012
  var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
@@ -4017,19 +4022,35 @@ function sourceDocsUrl(docsPath) {
4017
4022
  import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync11, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "node:fs";
4018
4023
  import { basename as basename6, dirname as dirname8, isAbsolute as isAbsolute2, join as join10, resolve as resolve10 } from "node:path";
4019
4024
  var HOST_CONFIG_MODE = 420;
4020
- function hostConfigFileName(stateHome) {
4021
- return basename6(stateHome) === GENERATIONS.legacy.userStateDir ? GENERATIONS.legacy.configFile : GENERATIONS.agro.configFile;
4025
+ var HOST_CONFIG_FILE = "config.json";
4026
+ var WORKSPACES_SUBDIR = "workspaces";
4027
+ var DEFAULT_WORKSPACE_NAME = "default";
4028
+ function hostStateHome(env, home) {
4029
+ return resolveAgroUserStateHome(env, home);
4030
+ }
4031
+ function hostConfigPath(env, home) {
4032
+ return join10(hostStateHome(env, home), HOST_CONFIG_FILE);
4033
+ }
4034
+ function assertWorkspaceName(name) {
4035
+ if (!SANDBOX_NAME_PATTERN.test(name)) {
4036
+ throw new Error(
4037
+ `invalid workspace name "${name}" \u2014 use lowercase letters, digits and dashes, starting with a letter or digit`
4038
+ );
4039
+ }
4022
4040
  }
4023
- function hostConfigPath(env = process.env) {
4024
- const stateHome = resolveUserStateHome(env);
4025
- return join10(stateHome, hostConfigFileName(stateHome));
4041
+ function workspacesRoot(env, home) {
4042
+ return join10(hostStateHome(env, home), WORKSPACES_SUBDIR);
4026
4043
  }
4027
- function defaultHarnessRoot(env = process.env) {
4028
- return resolveUserStateHome(env);
4044
+ function workspaceRoot(name, env, home) {
4045
+ assertWorkspaceName(name);
4046
+ return join10(workspacesRoot(env, home), name);
4047
+ }
4048
+ function defaultHarnessRoot(env, home) {
4049
+ return join10(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
4029
4050
  }
4030
4051
  function validateHostConfig(value) {
4031
4052
  if (!value || typeof value !== "object" || Array.isArray(value)) {
4032
- throw new Error(`${GENERATIONS.agro.configFile}: must contain a JSON object`);
4053
+ throw new Error(`${HOST_CONFIG_FILE}: must contain a JSON object`);
4033
4054
  }
4034
4055
  const record = value;
4035
4056
  if (record.version !== void 0 && record.version !== 1) {
@@ -4081,8 +4102,8 @@ function validateReceipts(value, field, noun) {
4081
4102
  }
4082
4103
  }
4083
4104
  }
4084
- function readHostConfig(env = process.env) {
4085
- const path4 = hostConfigPath(env);
4105
+ function readHostConfig(env, home) {
4106
+ const path4 = hostConfigPath(env, home);
4086
4107
  if (!existsSync16(path4)) return { version: 1 };
4087
4108
  let raw;
4088
4109
  try {
@@ -4099,8 +4120,8 @@ function readHostConfig(env = process.env) {
4099
4120
  }
4100
4121
  return validateHostConfig(parsed);
4101
4122
  }
4102
- function writeHostConfig(config, env = process.env) {
4103
- const path4 = hostConfigPath(env);
4123
+ function writeHostConfig(config, env, home) {
4124
+ const path4 = hostConfigPath(env, home);
4104
4125
  const validated = validateHostConfig({ ...config, version: 1 });
4105
4126
  const body = `${JSON.stringify(validated, null, 2)}
4106
4127
  `;
@@ -4117,14 +4138,14 @@ function writeHostConfig(config, env = process.env) {
4117
4138
  throw error;
4118
4139
  }
4119
4140
  }
4120
- function resolveHarnessRoot(explicit, env = process.env) {
4141
+ function resolveHarnessRoot(explicit, env, home) {
4121
4142
  if (typeof explicit === "string" && explicit !== "") return resolve10(explicit);
4122
- const configured = readHostConfig(env).harnessRoot;
4143
+ const configured = readHostConfig(env, home).harnessRoot;
4123
4144
  if (typeof configured === "string" && configured !== "") return resolve10(configured);
4124
- return defaultHarnessRoot(env);
4145
+ return defaultHarnessRoot(env, home);
4125
4146
  }
4126
4147
  function fieldError2(path4, requirement) {
4127
- return new Error(`${GENERATIONS.agro.configFile}: ${path4} ${requirement}`);
4148
+ return new Error(`${HOST_CONFIG_FILE}: ${path4} ${requirement}`);
4128
4149
  }
4129
4150
 
4130
4151
  // src/lib/host-workspace.ts
@@ -4137,6 +4158,40 @@ var IGNORABLE_ENTRIES = [
4137
4158
  GENERATIONS.agro.configFile,
4138
4159
  GENERATIONS.legacy.configFile
4139
4160
  ];
4161
+ var STATE_HOME_DIRS = [
4162
+ GENERATIONS.legacy.userStateDir,
4163
+ GENERATIONS.agro.userStateDir
4164
+ ];
4165
+ function isDirectory2(path4) {
4166
+ return statSync5(path4, { throwIfNoEntry: false })?.isDirectory() === true;
4167
+ }
4168
+ function stateHomes(home) {
4169
+ return STATE_HOME_DIRS.map((name) => join11(home, name));
4170
+ }
4171
+ function stateHomeRefusal(bin, home) {
4172
+ const [legacy, agro] = stateHomes(home);
4173
+ if (isDirectory2(legacy) && isDirectory2(agro)) {
4174
+ return `${bin} harness: the host state home is split \u2014 ${legacy} and ${agro} both exist.
4175
+ Merge them with \`${bin} migrate --home\`, then re-run this command.
4176
+ `;
4177
+ }
4178
+ if (isDirectory2(legacy)) {
4179
+ return `${bin} harness: the host state home is the legacy ${legacy}, and host state now lives in ${agro}.
4180
+ Creating ${agro} beside it would split the two.
4181
+ Migrate first with \`${bin} migrate --home\`, then re-run this command.
4182
+ `;
4183
+ }
4184
+ return void 0;
4185
+ }
4186
+ function stateHomeRootRefusal(bin, root, home) {
4187
+ const target = resolve11(root);
4188
+ const enclosing = stateHomes(home).find((state) => target === state);
4189
+ if (enclosing === void 0) return void 0;
4190
+ return `${bin} harness: the harness root ${target} is the state home ${enclosing} itself.
4191
+ A workspace there collides with AGRO state and blocks every command run from ${home}.
4192
+ Move it out \u2014 for example \`mv ${enclosing} ${join11(home, "agro")}\` \u2014 then re-run this command.
4193
+ `;
4194
+ }
4140
4195
  function isIgnorableEntry(name) {
4141
4196
  return name.startsWith(".") || IGNORABLE_ENTRIES.includes(name);
4142
4197
  }
@@ -4207,7 +4262,8 @@ var HARNESS_CATALOG = [
4207
4262
  verifyArgv: ["claude", "--version"],
4208
4263
  uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "@anthropic-ai/claude-code"],
4209
4264
  docsPath: "docs/harnesses/claude-code.md",
4210
- kind: "installable"
4265
+ kind: "installable",
4266
+ bypassPermissionsFlag: "--permission-mode bypassPermissions"
4211
4267
  },
4212
4268
  {
4213
4269
  id: "codex",
@@ -4354,6 +4410,9 @@ function resolveVerifyArgv(entry, prefix) {
4354
4410
  function resolveUninstallArgv(entry, prefix) {
4355
4411
  return entry.uninstallArgv === null ? null : substitute(entry.uninstallArgv, prefix);
4356
4412
  }
4413
+ function harnessLaunchCommand(entry) {
4414
+ return entry.bypassPermissionsFlag === void 0 ? entry.binary : `${entry.binary} ${entry.bypassPermissionsFlag}`;
4415
+ }
4357
4416
  function harnessBinPath(prefix) {
4358
4417
  return `${prefix}/bin`;
4359
4418
  }
@@ -4421,7 +4480,7 @@ function stateOf(entry, installed, location) {
4421
4480
  function probeableHost(env, home) {
4422
4481
  let root;
4423
4482
  try {
4424
- root = resolveHarnessRoot(void 0, env);
4483
+ root = resolveHarnessRoot(void 0, env, home);
4425
4484
  } catch {
4426
4485
  return void 0;
4427
4486
  }
@@ -4556,7 +4615,13 @@ function isInteractive(opts) {
4556
4615
  async function installOnHost(entry, opts, io, run, status) {
4557
4616
  const bin = opts.bin;
4558
4617
  const env = opts.env ?? process.env;
4559
- const flagged = opts.host === true || opts.path !== void 0;
4618
+ const home = homeOf(opts);
4619
+ const flagged = opts.host === true || opts.path !== void 0 || opts.workspace !== void 0;
4620
+ const generation = stateHomeRefusal(bin, home);
4621
+ if (generation !== void 0) {
4622
+ io.stderr(generation);
4623
+ return 1;
4624
+ }
4560
4625
  if (!flagged && !isInteractive(opts)) {
4561
4626
  io.stderr(
4562
4627
  sandboxRefusal(bin, status) + `Or install on the host with \`${bin} harness install ${entry.id} --host\`.
@@ -4571,15 +4636,17 @@ async function installOnHost(entry, opts, io, run, status) {
4571
4636
  }
4572
4637
  let root;
4573
4638
  let recorded;
4639
+ let explicit;
4574
4640
  try {
4575
- recorded = readHostConfig(env).harnessRoot;
4576
- root = resolveHarnessRoot(opts.path, env);
4641
+ explicit = opts.workspace !== void 0 ? workspaceRoot(opts.workspace, env, home) : opts.path;
4642
+ recorded = readHostConfig(env, home).harnessRoot;
4643
+ root = resolveHarnessRoot(explicit, env, home);
4577
4644
  } catch (err) {
4578
4645
  io.stderr(`${bin} harness: ${messageOf(err)}
4579
4646
  `);
4580
4647
  return 1;
4581
4648
  }
4582
- const sticky = opts.path === void 0 && recorded !== void 0 && recorded !== "";
4649
+ const sticky = explicit === void 0 && recorded !== void 0 && recorded !== "";
4583
4650
  if (!flagged) {
4584
4651
  const ask2 = io.ask ?? ask;
4585
4652
  const answer = (await ask2(
@@ -4590,12 +4657,23 @@ async function installOnHost(entry, opts, io, run, status) {
4590
4657
  return 1;
4591
4658
  }
4592
4659
  if (!sticky) {
4593
- const chosen = (await ask2(`Harness root [${root}]:`)).trim();
4594
- if (chosen !== "") root = resolve12(chosen);
4660
+ const chosen = (await ask2(`Workspace name [${DEFAULT_WORKSPACE_NAME}]:`)).trim();
4661
+ try {
4662
+ root = workspaceRoot(chosen === "" ? DEFAULT_WORKSPACE_NAME : chosen, env, home);
4663
+ } catch (err) {
4664
+ io.stderr(`${bin} harness: ${messageOf(err)}
4665
+ `);
4666
+ return 1;
4667
+ }
4595
4668
  }
4596
4669
  }
4597
4670
  if (sticky) io.stdout(`using the recorded harness root ${root}
4598
4671
  `);
4672
+ const nested = stateHomeRootRefusal(bin, root, home);
4673
+ if (nested !== void 0) {
4674
+ io.stderr(nested);
4675
+ return 1;
4676
+ }
4599
4677
  try {
4600
4678
  if (!existsSync18(join12(root, ".git"))) io.stdout(`cloning ${AGRO_REPO_URL} into ${root}\u2026
4601
4679
  `);
@@ -4611,8 +4689,21 @@ async function installOnHost(entry, opts, io, run, status) {
4611
4689
  `);
4612
4690
  return 1;
4613
4691
  }
4614
- const prefix = hostPrefix(homeOf(opts));
4692
+ const prefix = hostPrefix(home);
4615
4693
  const target = hostTargetFor(root, prefix, run, env);
4694
+ const linked = await target.exec({
4695
+ argv: remoteControlDirScript(root, "scripts/link-providers.sh", ["--init"]),
4696
+ env: aliasedEnvPair("PROJECT_ROOT", root),
4697
+ stdio: "inherit"
4698
+ });
4699
+ if (linked.exitCode !== 0) {
4700
+ io.stderr(
4701
+ `${bin} harness: could not link provider skills in ${root} (exit ${linked.exitCode}); nothing was installed.
4702
+ Run: bash ${root}/.agro/scripts/link-providers.sh --init
4703
+ `
4704
+ );
4705
+ return 1;
4706
+ }
4616
4707
  const hermes = entry.id === "hermes";
4617
4708
  const installEnv = hermes ? {
4618
4709
  ...aliasedEnvPair("PROJECT_ROOT", root),
@@ -4656,14 +4747,15 @@ async function installOnHost(entry, opts, io, run, status) {
4656
4747
  workspaceRoot: root
4657
4748
  };
4658
4749
  try {
4659
- const config = readHostConfig(env);
4750
+ const config = readHostConfig(env, home);
4660
4751
  writeHostConfig(
4661
4752
  {
4662
4753
  ...config,
4663
4754
  harnessRoot: root,
4664
4755
  hostHarnesses: { ...config.hostHarnesses ?? {}, [entry.id]: receipt }
4665
4756
  },
4666
- env
4757
+ env,
4758
+ home
4667
4759
  );
4668
4760
  } catch (err) {
4669
4761
  io.stderr(`${bin} harness: could not record the install: ${messageOf(err)}
@@ -4676,7 +4768,7 @@ async function installOnHost(entry, opts, io, run, status) {
4676
4768
  io.stdout(`Add this line to your shell profile: export PATH="${harnessBinPath(prefix)}:$PATH"
4677
4769
  `);
4678
4770
  }
4679
- io.stdout(`Run ${entry.binary} from the AGRO workspace: cd ${root}
4771
+ io.stdout(`Run ${entry.title} in the AGRO workspace: cd ${root} && ${harnessLaunchCommand(entry)}
4680
4772
  `);
4681
4773
  return 0;
4682
4774
  }
@@ -4720,7 +4812,13 @@ async function removeHarness(entry, target, prefix, user, opts, io) {
4720
4812
  async function uninstallOnHost(entry, opts, io, run) {
4721
4813
  const bin = opts.bin;
4722
4814
  const env = opts.env ?? process.env;
4723
- const computed = hostPrefix(homeOf(opts));
4815
+ const home = homeOf(opts);
4816
+ const computed = hostPrefix(home);
4817
+ const generation = stateHomeRefusal(bin, home);
4818
+ if (generation !== void 0) {
4819
+ io.stderr(generation);
4820
+ return 1;
4821
+ }
4724
4822
  if (resolveUninstallArgv(entry, computed) === null) {
4725
4823
  io.stdout(`${entry.id}: nothing to remove \u2014 npx fetches it at each run
4726
4824
  `);
@@ -4729,13 +4827,18 @@ async function uninstallOnHost(entry, opts, io, run) {
4729
4827
  let config;
4730
4828
  let workspace;
4731
4829
  try {
4732
- config = readHostConfig(env);
4733
- workspace = resolveHarnessRoot(void 0, env);
4830
+ config = readHostConfig(env, home);
4831
+ workspace = resolveHarnessRoot(void 0, env, home);
4734
4832
  } catch (err) {
4735
4833
  io.stderr(`${bin} harness: ${messageOf(err)}
4736
4834
  `);
4737
4835
  return 1;
4738
4836
  }
4837
+ const nested = stateHomeRootRefusal(bin, workspace, home);
4838
+ if (nested !== void 0) {
4839
+ io.stderr(nested);
4840
+ return 1;
4841
+ }
4739
4842
  const receipt = config.hostHarnesses?.[entry.id];
4740
4843
  if (receipt === void 0 && opts.force !== true) {
4741
4844
  io.stderr(
@@ -4752,7 +4855,7 @@ Removing it could delete a harness you installed yourself. Re-run with \`--force
4752
4855
  const remaining = { ...config.hostHarnesses ?? {} };
4753
4856
  delete remaining[entry.id];
4754
4857
  try {
4755
- writeHostConfig({ ...config, hostHarnesses: remaining }, env);
4858
+ writeHostConfig({ ...config, hostHarnesses: remaining }, env, home);
4756
4859
  } catch (err) {
4757
4860
  io.stderr(`${bin} harness: could not clear the install record: ${messageOf(err)}
4758
4861
  `);
@@ -4837,7 +4940,7 @@ async function runHarnessInstall(name, opts, io) {
4837
4940
  // src/commands/tool.ts
4838
4941
  import { existsSync as existsSync19 } from "node:fs";
4839
4942
  import { homedir as homedir3 } from "node:os";
4840
- import { delimiter as delimiter3, join as join13, resolve as resolve13 } from "node:path";
4943
+ import { delimiter as delimiter3, join as join13, resolve as resolve12 } from "node:path";
4841
4944
 
4842
4945
  // src/lib/tools/catalog.ts
4843
4946
  var TOOLS_DOC = "docs/installation.md";
@@ -5123,7 +5226,7 @@ function rowOf(entry, installed, version, location) {
5123
5226
  function probeableHost2(env, home) {
5124
5227
  let root;
5125
5228
  try {
5126
- root = resolveHarnessRoot(void 0, env);
5229
+ root = resolveHarnessRoot(void 0, env, home);
5127
5230
  } catch {
5128
5231
  return void 0;
5129
5232
  }
@@ -5271,6 +5374,7 @@ ${hostCapableToolIds().map((t) => ` ${bin} tool install ${t} --host`).join("\n"
5271
5374
  async function installOnHost2(entry, opts, io, run, status) {
5272
5375
  const bin = opts.bin;
5273
5376
  const env = opts.env ?? process.env;
5377
+ const home = homeOf2(opts);
5274
5378
  const flagged = opts.host === true || opts.path !== void 0;
5275
5379
  if (!entry.hostCapable) {
5276
5380
  io.stderr(sandboxRefusal2(bin, status));
@@ -5304,8 +5408,8 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5304
5408
  let root;
5305
5409
  let recorded;
5306
5410
  try {
5307
- recorded = readHostConfig(env).harnessRoot;
5308
- root = resolveHarnessRoot(opts.path, env);
5411
+ recorded = readHostConfig(env, home).harnessRoot;
5412
+ root = resolveHarnessRoot(opts.path, env, home);
5309
5413
  } catch (err) {
5310
5414
  io.stderr(`${bin} tool: ${messageOf2(err)}
5311
5415
  `);
@@ -5323,7 +5427,7 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5323
5427
  }
5324
5428
  if (!sticky) {
5325
5429
  const chosen = (await ask2(`Harness root [${root}]:`)).trim();
5326
- if (chosen !== "") root = resolve13(chosen);
5430
+ if (chosen !== "") root = resolve12(chosen);
5327
5431
  }
5328
5432
  }
5329
5433
  if (sticky) io.stdout(`using the recorded harness root ${root}
@@ -5343,7 +5447,7 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5343
5447
  `);
5344
5448
  return 1;
5345
5449
  }
5346
- const prefix = hostPrefix2(homeOf2(opts));
5450
+ const prefix = hostPrefix2(home);
5347
5451
  const installEnv = { NPM_USER_PREFIX: prefix };
5348
5452
  const target = hostTargetFor2(root, prefix, run, env);
5349
5453
  if (await probeInstalled2(target, entry, void 0, installEnv) === true) {
@@ -5372,14 +5476,15 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5372
5476
  workspaceRoot: root
5373
5477
  };
5374
5478
  try {
5375
- const config = readHostConfig(env);
5479
+ const config = readHostConfig(env, home);
5376
5480
  writeHostConfig(
5377
5481
  {
5378
5482
  ...config,
5379
5483
  harnessRoot: root,
5380
5484
  hostTools: { ...config.hostTools ?? {}, [entry.id]: receipt }
5381
5485
  },
5382
- env
5486
+ env,
5487
+ home
5383
5488
  );
5384
5489
  } catch (err) {
5385
5490
  io.stderr(`${bin} tool: could not record the install: ${messageOf2(err)}
@@ -5480,12 +5585,13 @@ async function removeTool(entry, target, prefix, user, opts, io) {
5480
5585
  async function uninstallOnHost2(entry, opts, io, run) {
5481
5586
  const bin = opts.bin;
5482
5587
  const env = opts.env ?? process.env;
5483
- const computed = hostPrefix2(homeOf2(opts));
5588
+ const home = homeOf2(opts);
5589
+ const computed = hostPrefix2(home);
5484
5590
  let config;
5485
5591
  let workspace;
5486
5592
  try {
5487
- config = readHostConfig(env);
5488
- workspace = resolveHarnessRoot(void 0, env);
5593
+ config = readHostConfig(env, home);
5594
+ workspace = resolveHarnessRoot(void 0, env, home);
5489
5595
  } catch (err) {
5490
5596
  io.stderr(`${bin} tool: ${messageOf2(err)}
5491
5597
  `);
@@ -5507,7 +5613,7 @@ Removing it could delete a tool you installed yourself. Re-run with \`--force\`
5507
5613
  const remaining = { ...config.hostTools ?? {} };
5508
5614
  delete remaining[entry.id];
5509
5615
  try {
5510
- writeHostConfig({ ...config, hostTools: remaining }, env);
5616
+ writeHostConfig({ ...config, hostTools: remaining }, env, home);
5511
5617
  } catch (err) {
5512
5618
  io.stderr(`${bin} tool: could not clear the install record: ${messageOf2(err)}
5513
5619
  `);
@@ -5602,8 +5708,8 @@ function fetchRemoteSource(opts) {
5602
5708
  }
5603
5709
 
5604
5710
  // src/cli.ts
5605
- var VERSION = true ? "0.12.0" : "0.0.0-dev";
5606
- var DEFAULT_SOURCE_OH_DIR = resolve14(
5711
+ var VERSION = true ? "0.12.2" : "0.0.0-dev";
5712
+ var DEFAULT_SOURCE_OH_DIR = resolve13(
5607
5713
  dirname10(fileURLToPath(import.meta.url)),
5608
5714
  "../.."
5609
5715
  );
@@ -5840,10 +5946,11 @@ Usage:
5840
5946
 
5841
5947
  \`install\` and \`uninstall\` act on the running sandbox. When no sandbox is
5842
5948
  reachable they act on the host. A host install clones the AGRO workspace into the
5843
- harness root, then installs the harness into \`~/.local\`. \`--path <dir>\` chooses
5844
- the harness root; a successful install records it as \`harnessRoot\` in the host
5845
- \`${stateNames(bin).configFile}\`. The default is \`~/.agro\`, and the install prefix is
5846
- always \`~/.local\`.
5949
+ harness root, then installs the harness into \`~/.local\`. The default root is the
5950
+ workspace registry entry \`~/.agro/workspaces/default\`; an interactive run asks for
5951
+ the workspace name. \`--workspace <name>\` chooses that entry and \`--path <dir>\` a
5952
+ directory outside the registry. A successful install records the root as
5953
+ \`harnessRoot\` in \`~/.agro/config.json\`. The install prefix is always \`~/.local\`.
5847
5954
 
5848
5955
  On the host, \`uninstall\` removes only the harness \`install\` recorded, from the
5849
5956
  prefix in that record. Without a record it refuses, and \`--force\` overrides. In
@@ -5852,7 +5959,8 @@ the sandbox it needs no record. See \`docs/harnesses/overview.md\`.
5852
5959
  Flags:
5853
5960
  --json Machine-readable output (list/status)
5854
5961
  --host Install on the host when the sandbox is not running (install)
5855
- --path <dir> Harness root for the workspace clone; implies --host (install)
5962
+ --workspace <name> Workspace registry entry to clone into; implies --host (install)
5963
+ --path <dir> Harness root outside the registry; implies --host (install)
5856
5964
  --force Remove without a recorded host install (uninstall)
5857
5965
 
5858
5966
  Harnesses:
@@ -5908,8 +6016,8 @@ only on these tools:
5908
6016
  ${hostCapableToolIds().map((t) => ` ${t}`).join("\n")}
5909
6017
 
5910
6018
  A host install clones the AGRO workspace into the harness root \u2014 \`--path <dir>\`,
5911
- then \`harnessRoot\` in the host \`${stateNames(bin).configFile}\`, then \`~/.agro\` \u2014 and installs into
5912
- \`~/.local\`. It records the install as \`hostTools\` in that same file. On the host,
6019
+ then \`harnessRoot\` in \`~/.agro/config.json\`, then \`~/.agro/workspaces/default\` \u2014 and
6020
+ installs into \`~/.local\`. It records the install as \`hostTools\` in that same file. On the host,
5913
6021
  \`uninstall\` removes only what \`install\` recorded, from the prefix in that record.
5914
6022
  Without a record it refuses, and \`--force\` overrides.
5915
6023
 
@@ -6309,11 +6417,16 @@ function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
6309
6417
  }
6310
6418
  const positionals = [];
6311
6419
  let expectPath = false;
6420
+ let expectWorkspace = false;
6312
6421
  for (const token of rest) {
6313
6422
  if (expectPath) {
6314
6423
  expectPath = false;
6315
6424
  args.path = token;
6316
6425
  args.host = true;
6426
+ } else if (expectWorkspace) {
6427
+ expectWorkspace = false;
6428
+ args.workspace = token;
6429
+ args.host = true;
6317
6430
  } else if (token === "--json") {
6318
6431
  args.json = true;
6319
6432
  } else if (token === "--host") {
@@ -6325,6 +6438,11 @@ function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
6325
6438
  } else if (token.startsWith("--path=")) {
6326
6439
  args.path = token.slice("--path=".length);
6327
6440
  args.host = true;
6441
+ } else if (token === "--workspace") {
6442
+ expectWorkspace = true;
6443
+ } else if (token.startsWith("--workspace=")) {
6444
+ args.workspace = token.slice("--workspace=".length);
6445
+ args.host = true;
6328
6446
  } else if (token.startsWith("-")) {
6329
6447
  return { ok: false, error: `${bin} harness: unknown flag "${token}"` };
6330
6448
  } else {
@@ -6334,6 +6452,15 @@ function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
6334
6452
  if (expectPath || args.path === "") {
6335
6453
  return { ok: false, error: `${bin} harness: --path requires a directory` };
6336
6454
  }
6455
+ if (expectWorkspace || args.workspace === "") {
6456
+ return { ok: false, error: `${bin} harness: --workspace requires a name` };
6457
+ }
6458
+ if (args.path !== void 0 && args.workspace !== void 0) {
6459
+ return {
6460
+ ok: false,
6461
+ error: `${bin} harness: --workspace names a registry entry and --path names a directory \u2014 pass one, not both`
6462
+ };
6463
+ }
6337
6464
  const [sub, name, ...extra] = positionals;
6338
6465
  if (sub !== "list" && sub !== "install" && sub !== "status" && sub !== "uninstall") {
6339
6466
  return {
@@ -6352,7 +6479,10 @@ function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
6352
6479
  return { ok: false, error: `${bin} harness list: unexpected argument "${name}"` };
6353
6480
  }
6354
6481
  if (sub !== "install" && args.host) {
6355
- return { ok: false, error: `${bin} harness ${sub}: --host and --path apply to install only` };
6482
+ return {
6483
+ ok: false,
6484
+ error: `${bin} harness ${sub}: --host, --workspace and --path apply to install only`
6485
+ };
6356
6486
  }
6357
6487
  if (sub !== "uninstall" && args.force) {
6358
6488
  return { ok: false, error: `${bin} harness ${sub}: --force applies to uninstall only` };
@@ -6427,13 +6557,13 @@ function bundledPayloadExists(bundled, exists = existsSync20) {
6427
6557
  function resolveUpdateSource(args, bundled, bin = LEGACY_PRODUCT.bin) {
6428
6558
  const exists = bundled.exists ?? existsSync20;
6429
6559
  if (args.fromDir !== void 0) {
6430
- return { kind: "local", fromDir: resolve14(args.fromDir) };
6560
+ return { kind: "local", fromDir: resolve13(args.fromDir) };
6431
6561
  }
6432
6562
  if (args.fromRemote) {
6433
6563
  return { kind: "remote", ref: args.ref };
6434
6564
  }
6435
6565
  if (bundledPayloadExists(bundled, exists)) {
6436
- return { kind: "local", fromDir: resolve14(bundled.sourceOhDir, "..") };
6566
+ return { kind: "local", fromDir: resolve13(bundled.sourceOhDir, "..") };
6437
6567
  }
6438
6568
  return {
6439
6569
  kind: "remote",
@@ -6725,7 +6855,12 @@ async function main(argv) {
6725
6855
  }
6726
6856
  return await runHarnessInstall(
6727
6857
  a.name,
6728
- { bin, host: a.host, ...a.path !== void 0 ? { path: a.path } : {} },
6858
+ {
6859
+ bin,
6860
+ host: a.host,
6861
+ ...a.path !== void 0 ? { path: a.path } : {},
6862
+ ...a.workspace !== void 0 ? { workspace: a.workspace } : {}
6863
+ },
6729
6864
  io
6730
6865
  );
6731
6866
  }
package/dist/oh.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { fileURLToPath } from "node:url";
5
- import { dirname as dirname10, join as join14, resolve as resolve14 } from "node:path";
5
+ import { dirname as dirname10, join as join14, resolve as resolve13 } from "node:path";
6
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
 
@@ -230,6 +230,11 @@ 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 resolveAgroUserStateHome(env = process.env, home = homedir(), warn) {
234
+ const configured = aliasedEnvValue(env, "HOME", warn);
235
+ if (configured !== void 0) return resolve(configured);
236
+ return join(home, GENERATIONS.agro.userStateDir);
237
+ }
233
238
  function resolveUserStateHome(env = process.env, home = homedir(), warn) {
234
239
  const configured = aliasedEnvValue(env, "HOME", warn);
235
240
  if (configured !== void 0) return resolve(configured);
@@ -1959,8 +1964,8 @@ function redact(token) {
1959
1964
  async function ask(question) {
1960
1965
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1961
1966
  try {
1962
- return await new Promise((resolve15) => {
1963
- rl.question(` ${question} `, (answer) => resolve15(answer.trim()));
1967
+ return await new Promise((resolve14) => {
1968
+ rl.question(` ${question} `, (answer) => resolve14(answer.trim()));
1964
1969
  });
1965
1970
  } finally {
1966
1971
  rl.close();
@@ -1972,7 +1977,7 @@ async function askSecret(question) {
1972
1977
  process.stdin.setRawMode(true);
1973
1978
  process.stdin.resume();
1974
1979
  const bytes = [];
1975
- return await new Promise((resolve15) => {
1980
+ return await new Promise((resolve14) => {
1976
1981
  const cleanup = () => {
1977
1982
  try {
1978
1983
  process.stdin.setRawMode(false);
@@ -1990,7 +1995,7 @@ async function askSecret(question) {
1990
1995
  } else if (byte === 13 || byte === 10) {
1991
1996
  cleanup();
1992
1997
  process.stdout.write("\n");
1993
- resolve15(Buffer.from(bytes).toString("utf8").trim());
1998
+ resolve14(Buffer.from(bytes).toString("utf8").trim());
1994
1999
  return;
1995
2000
  } else if (byte === 127 || byte === 8) {
1996
2001
  if (bytes.length > 0) {
@@ -2040,10 +2045,10 @@ async function askSecretPiped(question) {
2040
2045
  };
2041
2046
  process.on("SIGINT", onSigint);
2042
2047
  try {
2043
- return await new Promise((resolve15) => {
2048
+ return await new Promise((resolve14) => {
2044
2049
  rl.question(` ${question} `, (answer) => {
2045
2050
  process.stdout.write("\n");
2046
- resolve15(answer.trim());
2051
+ resolve14(answer.trim());
2047
2052
  });
2048
2053
  });
2049
2054
  } finally {
@@ -4001,7 +4006,7 @@ async function runSandboxList(opts, io) {
4001
4006
  // src/commands/harness.ts
4002
4007
  import { existsSync as existsSync18 } from "node:fs";
4003
4008
  import { homedir as homedir2 } from "node:os";
4004
- import { delimiter as delimiter2, join as join12, resolve as resolve12 } from "node:path";
4009
+ import { delimiter as delimiter2, join as join12 } from "node:path";
4005
4010
 
4006
4011
  // src/lib/docs.ts
4007
4012
  var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
@@ -4017,19 +4022,35 @@ function sourceDocsUrl(docsPath) {
4017
4022
  import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync11, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "node:fs";
4018
4023
  import { basename as basename6, dirname as dirname8, isAbsolute as isAbsolute2, join as join10, resolve as resolve10 } from "node:path";
4019
4024
  var HOST_CONFIG_MODE = 420;
4020
- function hostConfigFileName(stateHome) {
4021
- return basename6(stateHome) === GENERATIONS.legacy.userStateDir ? GENERATIONS.legacy.configFile : GENERATIONS.agro.configFile;
4025
+ var HOST_CONFIG_FILE = "config.json";
4026
+ var WORKSPACES_SUBDIR = "workspaces";
4027
+ var DEFAULT_WORKSPACE_NAME = "default";
4028
+ function hostStateHome(env, home) {
4029
+ return resolveAgroUserStateHome(env, home);
4030
+ }
4031
+ function hostConfigPath(env, home) {
4032
+ return join10(hostStateHome(env, home), HOST_CONFIG_FILE);
4033
+ }
4034
+ function assertWorkspaceName(name) {
4035
+ if (!SANDBOX_NAME_PATTERN.test(name)) {
4036
+ throw new Error(
4037
+ `invalid workspace name "${name}" \u2014 use lowercase letters, digits and dashes, starting with a letter or digit`
4038
+ );
4039
+ }
4022
4040
  }
4023
- function hostConfigPath(env = process.env) {
4024
- const stateHome = resolveUserStateHome(env);
4025
- return join10(stateHome, hostConfigFileName(stateHome));
4041
+ function workspacesRoot(env, home) {
4042
+ return join10(hostStateHome(env, home), WORKSPACES_SUBDIR);
4026
4043
  }
4027
- function defaultHarnessRoot(env = process.env) {
4028
- return resolveUserStateHome(env);
4044
+ function workspaceRoot(name, env, home) {
4045
+ assertWorkspaceName(name);
4046
+ return join10(workspacesRoot(env, home), name);
4047
+ }
4048
+ function defaultHarnessRoot(env, home) {
4049
+ return join10(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
4029
4050
  }
4030
4051
  function validateHostConfig(value) {
4031
4052
  if (!value || typeof value !== "object" || Array.isArray(value)) {
4032
- throw new Error(`${GENERATIONS.agro.configFile}: must contain a JSON object`);
4053
+ throw new Error(`${HOST_CONFIG_FILE}: must contain a JSON object`);
4033
4054
  }
4034
4055
  const record = value;
4035
4056
  if (record.version !== void 0 && record.version !== 1) {
@@ -4081,8 +4102,8 @@ function validateReceipts(value, field, noun) {
4081
4102
  }
4082
4103
  }
4083
4104
  }
4084
- function readHostConfig(env = process.env) {
4085
- const path4 = hostConfigPath(env);
4105
+ function readHostConfig(env, home) {
4106
+ const path4 = hostConfigPath(env, home);
4086
4107
  if (!existsSync16(path4)) return { version: 1 };
4087
4108
  let raw;
4088
4109
  try {
@@ -4099,8 +4120,8 @@ function readHostConfig(env = process.env) {
4099
4120
  }
4100
4121
  return validateHostConfig(parsed);
4101
4122
  }
4102
- function writeHostConfig(config, env = process.env) {
4103
- const path4 = hostConfigPath(env);
4123
+ function writeHostConfig(config, env, home) {
4124
+ const path4 = hostConfigPath(env, home);
4104
4125
  const validated = validateHostConfig({ ...config, version: 1 });
4105
4126
  const body = `${JSON.stringify(validated, null, 2)}
4106
4127
  `;
@@ -4117,14 +4138,14 @@ function writeHostConfig(config, env = process.env) {
4117
4138
  throw error;
4118
4139
  }
4119
4140
  }
4120
- function resolveHarnessRoot(explicit, env = process.env) {
4141
+ function resolveHarnessRoot(explicit, env, home) {
4121
4142
  if (typeof explicit === "string" && explicit !== "") return resolve10(explicit);
4122
- const configured = readHostConfig(env).harnessRoot;
4143
+ const configured = readHostConfig(env, home).harnessRoot;
4123
4144
  if (typeof configured === "string" && configured !== "") return resolve10(configured);
4124
- return defaultHarnessRoot(env);
4145
+ return defaultHarnessRoot(env, home);
4125
4146
  }
4126
4147
  function fieldError2(path4, requirement) {
4127
- return new Error(`${GENERATIONS.agro.configFile}: ${path4} ${requirement}`);
4148
+ return new Error(`${HOST_CONFIG_FILE}: ${path4} ${requirement}`);
4128
4149
  }
4129
4150
 
4130
4151
  // src/lib/host-workspace.ts
@@ -4137,6 +4158,40 @@ var IGNORABLE_ENTRIES = [
4137
4158
  GENERATIONS.agro.configFile,
4138
4159
  GENERATIONS.legacy.configFile
4139
4160
  ];
4161
+ var STATE_HOME_DIRS = [
4162
+ GENERATIONS.legacy.userStateDir,
4163
+ GENERATIONS.agro.userStateDir
4164
+ ];
4165
+ function isDirectory2(path4) {
4166
+ return statSync5(path4, { throwIfNoEntry: false })?.isDirectory() === true;
4167
+ }
4168
+ function stateHomes(home) {
4169
+ return STATE_HOME_DIRS.map((name) => join11(home, name));
4170
+ }
4171
+ function stateHomeRefusal(bin, home) {
4172
+ const [legacy, agro] = stateHomes(home);
4173
+ if (isDirectory2(legacy) && isDirectory2(agro)) {
4174
+ return `${bin} harness: the host state home is split \u2014 ${legacy} and ${agro} both exist.
4175
+ Merge them with \`${bin} migrate --home\`, then re-run this command.
4176
+ `;
4177
+ }
4178
+ if (isDirectory2(legacy)) {
4179
+ return `${bin} harness: the host state home is the legacy ${legacy}, and host state now lives in ${agro}.
4180
+ Creating ${agro} beside it would split the two.
4181
+ Migrate first with \`${bin} migrate --home\`, then re-run this command.
4182
+ `;
4183
+ }
4184
+ return void 0;
4185
+ }
4186
+ function stateHomeRootRefusal(bin, root, home) {
4187
+ const target = resolve11(root);
4188
+ const enclosing = stateHomes(home).find((state) => target === state);
4189
+ if (enclosing === void 0) return void 0;
4190
+ return `${bin} harness: the harness root ${target} is the state home ${enclosing} itself.
4191
+ A workspace there collides with AGRO state and blocks every command run from ${home}.
4192
+ Move it out \u2014 for example \`mv ${enclosing} ${join11(home, "agro")}\` \u2014 then re-run this command.
4193
+ `;
4194
+ }
4140
4195
  function isIgnorableEntry(name) {
4141
4196
  return name.startsWith(".") || IGNORABLE_ENTRIES.includes(name);
4142
4197
  }
@@ -4207,7 +4262,8 @@ var HARNESS_CATALOG = [
4207
4262
  verifyArgv: ["claude", "--version"],
4208
4263
  uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "@anthropic-ai/claude-code"],
4209
4264
  docsPath: "docs/harnesses/claude-code.md",
4210
- kind: "installable"
4265
+ kind: "installable",
4266
+ bypassPermissionsFlag: "--permission-mode bypassPermissions"
4211
4267
  },
4212
4268
  {
4213
4269
  id: "codex",
@@ -4354,6 +4410,9 @@ function resolveVerifyArgv(entry, prefix) {
4354
4410
  function resolveUninstallArgv(entry, prefix) {
4355
4411
  return entry.uninstallArgv === null ? null : substitute(entry.uninstallArgv, prefix);
4356
4412
  }
4413
+ function harnessLaunchCommand(entry) {
4414
+ return entry.bypassPermissionsFlag === void 0 ? entry.binary : `${entry.binary} ${entry.bypassPermissionsFlag}`;
4415
+ }
4357
4416
  function harnessBinPath(prefix) {
4358
4417
  return `${prefix}/bin`;
4359
4418
  }
@@ -4421,7 +4480,7 @@ function stateOf(entry, installed, location) {
4421
4480
  function probeableHost(env, home) {
4422
4481
  let root;
4423
4482
  try {
4424
- root = resolveHarnessRoot(void 0, env);
4483
+ root = resolveHarnessRoot(void 0, env, home);
4425
4484
  } catch {
4426
4485
  return void 0;
4427
4486
  }
@@ -4556,7 +4615,13 @@ function isInteractive(opts) {
4556
4615
  async function installOnHost(entry, opts, io, run, status) {
4557
4616
  const bin = opts.bin;
4558
4617
  const env = opts.env ?? process.env;
4559
- const flagged = opts.host === true || opts.path !== void 0;
4618
+ const home = homeOf(opts);
4619
+ const flagged = opts.host === true || opts.path !== void 0 || opts.workspace !== void 0;
4620
+ const generation = stateHomeRefusal(bin, home);
4621
+ if (generation !== void 0) {
4622
+ io.stderr(generation);
4623
+ return 1;
4624
+ }
4560
4625
  if (!flagged && !isInteractive(opts)) {
4561
4626
  io.stderr(
4562
4627
  sandboxRefusal(bin, status) + `Or install on the host with \`${bin} harness install ${entry.id} --host\`.
@@ -4571,15 +4636,17 @@ async function installOnHost(entry, opts, io, run, status) {
4571
4636
  }
4572
4637
  let root;
4573
4638
  let recorded;
4639
+ let explicit;
4574
4640
  try {
4575
- recorded = readHostConfig(env).harnessRoot;
4576
- root = resolveHarnessRoot(opts.path, env);
4641
+ explicit = opts.workspace !== void 0 ? workspaceRoot(opts.workspace, env, home) : opts.path;
4642
+ recorded = readHostConfig(env, home).harnessRoot;
4643
+ root = resolveHarnessRoot(explicit, env, home);
4577
4644
  } catch (err) {
4578
4645
  io.stderr(`${bin} harness: ${messageOf(err)}
4579
4646
  `);
4580
4647
  return 1;
4581
4648
  }
4582
- const sticky = opts.path === void 0 && recorded !== void 0 && recorded !== "";
4649
+ const sticky = explicit === void 0 && recorded !== void 0 && recorded !== "";
4583
4650
  if (!flagged) {
4584
4651
  const ask2 = io.ask ?? ask;
4585
4652
  const answer = (await ask2(
@@ -4590,12 +4657,23 @@ async function installOnHost(entry, opts, io, run, status) {
4590
4657
  return 1;
4591
4658
  }
4592
4659
  if (!sticky) {
4593
- const chosen = (await ask2(`Harness root [${root}]:`)).trim();
4594
- if (chosen !== "") root = resolve12(chosen);
4660
+ const chosen = (await ask2(`Workspace name [${DEFAULT_WORKSPACE_NAME}]:`)).trim();
4661
+ try {
4662
+ root = workspaceRoot(chosen === "" ? DEFAULT_WORKSPACE_NAME : chosen, env, home);
4663
+ } catch (err) {
4664
+ io.stderr(`${bin} harness: ${messageOf(err)}
4665
+ `);
4666
+ return 1;
4667
+ }
4595
4668
  }
4596
4669
  }
4597
4670
  if (sticky) io.stdout(`using the recorded harness root ${root}
4598
4671
  `);
4672
+ const nested = stateHomeRootRefusal(bin, root, home);
4673
+ if (nested !== void 0) {
4674
+ io.stderr(nested);
4675
+ return 1;
4676
+ }
4599
4677
  try {
4600
4678
  if (!existsSync18(join12(root, ".git"))) io.stdout(`cloning ${AGRO_REPO_URL} into ${root}\u2026
4601
4679
  `);
@@ -4611,8 +4689,21 @@ async function installOnHost(entry, opts, io, run, status) {
4611
4689
  `);
4612
4690
  return 1;
4613
4691
  }
4614
- const prefix = hostPrefix(homeOf(opts));
4692
+ const prefix = hostPrefix(home);
4615
4693
  const target = hostTargetFor(root, prefix, run, env);
4694
+ const linked = await target.exec({
4695
+ argv: remoteControlDirScript(root, "scripts/link-providers.sh", ["--init"]),
4696
+ env: aliasedEnvPair("PROJECT_ROOT", root),
4697
+ stdio: "inherit"
4698
+ });
4699
+ if (linked.exitCode !== 0) {
4700
+ io.stderr(
4701
+ `${bin} harness: could not link provider skills in ${root} (exit ${linked.exitCode}); nothing was installed.
4702
+ Run: bash ${root}/.agro/scripts/link-providers.sh --init
4703
+ `
4704
+ );
4705
+ return 1;
4706
+ }
4616
4707
  const hermes = entry.id === "hermes";
4617
4708
  const installEnv = hermes ? {
4618
4709
  ...aliasedEnvPair("PROJECT_ROOT", root),
@@ -4656,14 +4747,15 @@ async function installOnHost(entry, opts, io, run, status) {
4656
4747
  workspaceRoot: root
4657
4748
  };
4658
4749
  try {
4659
- const config = readHostConfig(env);
4750
+ const config = readHostConfig(env, home);
4660
4751
  writeHostConfig(
4661
4752
  {
4662
4753
  ...config,
4663
4754
  harnessRoot: root,
4664
4755
  hostHarnesses: { ...config.hostHarnesses ?? {}, [entry.id]: receipt }
4665
4756
  },
4666
- env
4757
+ env,
4758
+ home
4667
4759
  );
4668
4760
  } catch (err) {
4669
4761
  io.stderr(`${bin} harness: could not record the install: ${messageOf(err)}
@@ -4676,7 +4768,7 @@ async function installOnHost(entry, opts, io, run, status) {
4676
4768
  io.stdout(`Add this line to your shell profile: export PATH="${harnessBinPath(prefix)}:$PATH"
4677
4769
  `);
4678
4770
  }
4679
- io.stdout(`Run ${entry.binary} from the AGRO workspace: cd ${root}
4771
+ io.stdout(`Run ${entry.title} in the AGRO workspace: cd ${root} && ${harnessLaunchCommand(entry)}
4680
4772
  `);
4681
4773
  return 0;
4682
4774
  }
@@ -4720,7 +4812,13 @@ async function removeHarness(entry, target, prefix, user, opts, io) {
4720
4812
  async function uninstallOnHost(entry, opts, io, run) {
4721
4813
  const bin = opts.bin;
4722
4814
  const env = opts.env ?? process.env;
4723
- const computed = hostPrefix(homeOf(opts));
4815
+ const home = homeOf(opts);
4816
+ const computed = hostPrefix(home);
4817
+ const generation = stateHomeRefusal(bin, home);
4818
+ if (generation !== void 0) {
4819
+ io.stderr(generation);
4820
+ return 1;
4821
+ }
4724
4822
  if (resolveUninstallArgv(entry, computed) === null) {
4725
4823
  io.stdout(`${entry.id}: nothing to remove \u2014 npx fetches it at each run
4726
4824
  `);
@@ -4729,13 +4827,18 @@ async function uninstallOnHost(entry, opts, io, run) {
4729
4827
  let config;
4730
4828
  let workspace;
4731
4829
  try {
4732
- config = readHostConfig(env);
4733
- workspace = resolveHarnessRoot(void 0, env);
4830
+ config = readHostConfig(env, home);
4831
+ workspace = resolveHarnessRoot(void 0, env, home);
4734
4832
  } catch (err) {
4735
4833
  io.stderr(`${bin} harness: ${messageOf(err)}
4736
4834
  `);
4737
4835
  return 1;
4738
4836
  }
4837
+ const nested = stateHomeRootRefusal(bin, workspace, home);
4838
+ if (nested !== void 0) {
4839
+ io.stderr(nested);
4840
+ return 1;
4841
+ }
4739
4842
  const receipt = config.hostHarnesses?.[entry.id];
4740
4843
  if (receipt === void 0 && opts.force !== true) {
4741
4844
  io.stderr(
@@ -4752,7 +4855,7 @@ Removing it could delete a harness you installed yourself. Re-run with \`--force
4752
4855
  const remaining = { ...config.hostHarnesses ?? {} };
4753
4856
  delete remaining[entry.id];
4754
4857
  try {
4755
- writeHostConfig({ ...config, hostHarnesses: remaining }, env);
4858
+ writeHostConfig({ ...config, hostHarnesses: remaining }, env, home);
4756
4859
  } catch (err) {
4757
4860
  io.stderr(`${bin} harness: could not clear the install record: ${messageOf(err)}
4758
4861
  `);
@@ -4837,7 +4940,7 @@ async function runHarnessInstall(name, opts, io) {
4837
4940
  // src/commands/tool.ts
4838
4941
  import { existsSync as existsSync19 } from "node:fs";
4839
4942
  import { homedir as homedir3 } from "node:os";
4840
- import { delimiter as delimiter3, join as join13, resolve as resolve13 } from "node:path";
4943
+ import { delimiter as delimiter3, join as join13, resolve as resolve12 } from "node:path";
4841
4944
 
4842
4945
  // src/lib/tools/catalog.ts
4843
4946
  var TOOLS_DOC = "docs/installation.md";
@@ -5123,7 +5226,7 @@ function rowOf(entry, installed, version, location) {
5123
5226
  function probeableHost2(env, home) {
5124
5227
  let root;
5125
5228
  try {
5126
- root = resolveHarnessRoot(void 0, env);
5229
+ root = resolveHarnessRoot(void 0, env, home);
5127
5230
  } catch {
5128
5231
  return void 0;
5129
5232
  }
@@ -5271,6 +5374,7 @@ ${hostCapableToolIds().map((t) => ` ${bin} tool install ${t} --host`).join("\n"
5271
5374
  async function installOnHost2(entry, opts, io, run, status) {
5272
5375
  const bin = opts.bin;
5273
5376
  const env = opts.env ?? process.env;
5377
+ const home = homeOf2(opts);
5274
5378
  const flagged = opts.host === true || opts.path !== void 0;
5275
5379
  if (!entry.hostCapable) {
5276
5380
  io.stderr(sandboxRefusal2(bin, status));
@@ -5304,8 +5408,8 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5304
5408
  let root;
5305
5409
  let recorded;
5306
5410
  try {
5307
- recorded = readHostConfig(env).harnessRoot;
5308
- root = resolveHarnessRoot(opts.path, env);
5411
+ recorded = readHostConfig(env, home).harnessRoot;
5412
+ root = resolveHarnessRoot(opts.path, env, home);
5309
5413
  } catch (err) {
5310
5414
  io.stderr(`${bin} tool: ${messageOf2(err)}
5311
5415
  `);
@@ -5323,7 +5427,7 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5323
5427
  }
5324
5428
  if (!sticky) {
5325
5429
  const chosen = (await ask2(`Harness root [${root}]:`)).trim();
5326
- if (chosen !== "") root = resolve13(chosen);
5430
+ if (chosen !== "") root = resolve12(chosen);
5327
5431
  }
5328
5432
  }
5329
5433
  if (sticky) io.stdout(`using the recorded harness root ${root}
@@ -5343,7 +5447,7 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5343
5447
  `);
5344
5448
  return 1;
5345
5449
  }
5346
- const prefix = hostPrefix2(homeOf2(opts));
5450
+ const prefix = hostPrefix2(home);
5347
5451
  const installEnv = { NPM_USER_PREFIX: prefix };
5348
5452
  const target = hostTargetFor2(root, prefix, run, env);
5349
5453
  if (await probeInstalled2(target, entry, void 0, installEnv) === true) {
@@ -5372,14 +5476,15 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5372
5476
  workspaceRoot: root
5373
5477
  };
5374
5478
  try {
5375
- const config = readHostConfig(env);
5479
+ const config = readHostConfig(env, home);
5376
5480
  writeHostConfig(
5377
5481
  {
5378
5482
  ...config,
5379
5483
  harnessRoot: root,
5380
5484
  hostTools: { ...config.hostTools ?? {}, [entry.id]: receipt }
5381
5485
  },
5382
- env
5486
+ env,
5487
+ home
5383
5488
  );
5384
5489
  } catch (err) {
5385
5490
  io.stderr(`${bin} tool: could not record the install: ${messageOf2(err)}
@@ -5480,12 +5585,13 @@ async function removeTool(entry, target, prefix, user, opts, io) {
5480
5585
  async function uninstallOnHost2(entry, opts, io, run) {
5481
5586
  const bin = opts.bin;
5482
5587
  const env = opts.env ?? process.env;
5483
- const computed = hostPrefix2(homeOf2(opts));
5588
+ const home = homeOf2(opts);
5589
+ const computed = hostPrefix2(home);
5484
5590
  let config;
5485
5591
  let workspace;
5486
5592
  try {
5487
- config = readHostConfig(env);
5488
- workspace = resolveHarnessRoot(void 0, env);
5593
+ config = readHostConfig(env, home);
5594
+ workspace = resolveHarnessRoot(void 0, env, home);
5489
5595
  } catch (err) {
5490
5596
  io.stderr(`${bin} tool: ${messageOf2(err)}
5491
5597
  `);
@@ -5507,7 +5613,7 @@ Removing it could delete a tool you installed yourself. Re-run with \`--force\`
5507
5613
  const remaining = { ...config.hostTools ?? {} };
5508
5614
  delete remaining[entry.id];
5509
5615
  try {
5510
- writeHostConfig({ ...config, hostTools: remaining }, env);
5616
+ writeHostConfig({ ...config, hostTools: remaining }, env, home);
5511
5617
  } catch (err) {
5512
5618
  io.stderr(`${bin} tool: could not clear the install record: ${messageOf2(err)}
5513
5619
  `);
@@ -5602,8 +5708,8 @@ function fetchRemoteSource(opts) {
5602
5708
  }
5603
5709
 
5604
5710
  // src/cli.ts
5605
- var VERSION = true ? "0.12.0" : "0.0.0-dev";
5606
- var DEFAULT_SOURCE_OH_DIR = resolve14(
5711
+ var VERSION = true ? "0.12.2" : "0.0.0-dev";
5712
+ var DEFAULT_SOURCE_OH_DIR = resolve13(
5607
5713
  dirname10(fileURLToPath(import.meta.url)),
5608
5714
  "../.."
5609
5715
  );
@@ -5840,10 +5946,11 @@ Usage:
5840
5946
 
5841
5947
  \`install\` and \`uninstall\` act on the running sandbox. When no sandbox is
5842
5948
  reachable they act on the host. A host install clones the AGRO workspace into the
5843
- harness root, then installs the harness into \`~/.local\`. \`--path <dir>\` chooses
5844
- the harness root; a successful install records it as \`harnessRoot\` in the host
5845
- \`${stateNames(bin).configFile}\`. The default is \`~/.agro\`, and the install prefix is
5846
- always \`~/.local\`.
5949
+ harness root, then installs the harness into \`~/.local\`. The default root is the
5950
+ workspace registry entry \`~/.agro/workspaces/default\`; an interactive run asks for
5951
+ the workspace name. \`--workspace <name>\` chooses that entry and \`--path <dir>\` a
5952
+ directory outside the registry. A successful install records the root as
5953
+ \`harnessRoot\` in \`~/.agro/config.json\`. The install prefix is always \`~/.local\`.
5847
5954
 
5848
5955
  On the host, \`uninstall\` removes only the harness \`install\` recorded, from the
5849
5956
  prefix in that record. Without a record it refuses, and \`--force\` overrides. In
@@ -5852,7 +5959,8 @@ the sandbox it needs no record. See \`docs/harnesses/overview.md\`.
5852
5959
  Flags:
5853
5960
  --json Machine-readable output (list/status)
5854
5961
  --host Install on the host when the sandbox is not running (install)
5855
- --path <dir> Harness root for the workspace clone; implies --host (install)
5962
+ --workspace <name> Workspace registry entry to clone into; implies --host (install)
5963
+ --path <dir> Harness root outside the registry; implies --host (install)
5856
5964
  --force Remove without a recorded host install (uninstall)
5857
5965
 
5858
5966
  Harnesses:
@@ -5908,8 +6016,8 @@ only on these tools:
5908
6016
  ${hostCapableToolIds().map((t) => ` ${t}`).join("\n")}
5909
6017
 
5910
6018
  A host install clones the AGRO workspace into the harness root \u2014 \`--path <dir>\`,
5911
- then \`harnessRoot\` in the host \`${stateNames(bin).configFile}\`, then \`~/.agro\` \u2014 and installs into
5912
- \`~/.local\`. It records the install as \`hostTools\` in that same file. On the host,
6019
+ then \`harnessRoot\` in \`~/.agro/config.json\`, then \`~/.agro/workspaces/default\` \u2014 and
6020
+ installs into \`~/.local\`. It records the install as \`hostTools\` in that same file. On the host,
5913
6021
  \`uninstall\` removes only what \`install\` recorded, from the prefix in that record.
5914
6022
  Without a record it refuses, and \`--force\` overrides.
5915
6023
 
@@ -6309,11 +6417,16 @@ function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
6309
6417
  }
6310
6418
  const positionals = [];
6311
6419
  let expectPath = false;
6420
+ let expectWorkspace = false;
6312
6421
  for (const token of rest) {
6313
6422
  if (expectPath) {
6314
6423
  expectPath = false;
6315
6424
  args.path = token;
6316
6425
  args.host = true;
6426
+ } else if (expectWorkspace) {
6427
+ expectWorkspace = false;
6428
+ args.workspace = token;
6429
+ args.host = true;
6317
6430
  } else if (token === "--json") {
6318
6431
  args.json = true;
6319
6432
  } else if (token === "--host") {
@@ -6325,6 +6438,11 @@ function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
6325
6438
  } else if (token.startsWith("--path=")) {
6326
6439
  args.path = token.slice("--path=".length);
6327
6440
  args.host = true;
6441
+ } else if (token === "--workspace") {
6442
+ expectWorkspace = true;
6443
+ } else if (token.startsWith("--workspace=")) {
6444
+ args.workspace = token.slice("--workspace=".length);
6445
+ args.host = true;
6328
6446
  } else if (token.startsWith("-")) {
6329
6447
  return { ok: false, error: `${bin} harness: unknown flag "${token}"` };
6330
6448
  } else {
@@ -6334,6 +6452,15 @@ function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
6334
6452
  if (expectPath || args.path === "") {
6335
6453
  return { ok: false, error: `${bin} harness: --path requires a directory` };
6336
6454
  }
6455
+ if (expectWorkspace || args.workspace === "") {
6456
+ return { ok: false, error: `${bin} harness: --workspace requires a name` };
6457
+ }
6458
+ if (args.path !== void 0 && args.workspace !== void 0) {
6459
+ return {
6460
+ ok: false,
6461
+ error: `${bin} harness: --workspace names a registry entry and --path names a directory \u2014 pass one, not both`
6462
+ };
6463
+ }
6337
6464
  const [sub, name, ...extra] = positionals;
6338
6465
  if (sub !== "list" && sub !== "install" && sub !== "status" && sub !== "uninstall") {
6339
6466
  return {
@@ -6352,7 +6479,10 @@ function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
6352
6479
  return { ok: false, error: `${bin} harness list: unexpected argument "${name}"` };
6353
6480
  }
6354
6481
  if (sub !== "install" && args.host) {
6355
- return { ok: false, error: `${bin} harness ${sub}: --host and --path apply to install only` };
6482
+ return {
6483
+ ok: false,
6484
+ error: `${bin} harness ${sub}: --host, --workspace and --path apply to install only`
6485
+ };
6356
6486
  }
6357
6487
  if (sub !== "uninstall" && args.force) {
6358
6488
  return { ok: false, error: `${bin} harness ${sub}: --force applies to uninstall only` };
@@ -6427,13 +6557,13 @@ function bundledPayloadExists(bundled, exists = existsSync20) {
6427
6557
  function resolveUpdateSource(args, bundled, bin = LEGACY_PRODUCT.bin) {
6428
6558
  const exists = bundled.exists ?? existsSync20;
6429
6559
  if (args.fromDir !== void 0) {
6430
- return { kind: "local", fromDir: resolve14(args.fromDir) };
6560
+ return { kind: "local", fromDir: resolve13(args.fromDir) };
6431
6561
  }
6432
6562
  if (args.fromRemote) {
6433
6563
  return { kind: "remote", ref: args.ref };
6434
6564
  }
6435
6565
  if (bundledPayloadExists(bundled, exists)) {
6436
- return { kind: "local", fromDir: resolve14(bundled.sourceOhDir, "..") };
6566
+ return { kind: "local", fromDir: resolve13(bundled.sourceOhDir, "..") };
6437
6567
  }
6438
6568
  return {
6439
6569
  kind: "remote",
@@ -6725,7 +6855,12 @@ async function main(argv) {
6725
6855
  }
6726
6856
  return await runHarnessInstall(
6727
6857
  a.name,
6728
- { bin, host: a.host, ...a.path !== void 0 ? { path: a.path } : {} },
6858
+ {
6859
+ bin,
6860
+ host: a.host,
6861
+ ...a.path !== void 0 ? { path: a.path } : {},
6862
+ ...a.workspace !== void 0 ? { workspace: a.workspace } : {}
6863
+ },
6729
6864
  io
6730
6865
  );
6731
6866
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mifune/agro",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
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",