@mifune/agro 0.12.1 → 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 +193 -80
  2. package/dist/oh.js +193 -80
  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,16 +230,10 @@ function resolveRegistryHome(env = process.env, home = homedir(), warn) {
230
230
  if (configured !== void 0) return { path: resolve(configured), configured: true };
231
231
  return { path: home, configured: false };
232
232
  }
233
- function generationUserStateHome(generation, env, home, warn) {
233
+ function resolveAgroUserStateHome(env = process.env, home = homedir(), warn) {
234
234
  const configured = aliasedEnvValue(env, "HOME", warn);
235
235
  if (configured !== void 0) return resolve(configured);
236
- return join(home, GENERATIONS[generation].userStateDir);
237
- }
238
- function resolveAgroUserStateHome(env = process.env, home = homedir(), warn) {
239
- return generationUserStateHome("agro", env, home, warn);
240
- }
241
- function resolveLegacyUserStateHome(env = process.env, home = homedir(), warn) {
242
- return generationUserStateHome("legacy", env, home, warn);
236
+ return join(home, GENERATIONS.agro.userStateDir);
243
237
  }
244
238
  function resolveUserStateHome(env = process.env, home = homedir(), warn) {
245
239
  const configured = aliasedEnvValue(env, "HOME", warn);
@@ -1970,8 +1964,8 @@ function redact(token) {
1970
1964
  async function ask(question) {
1971
1965
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1972
1966
  try {
1973
- return await new Promise((resolve15) => {
1974
- rl.question(` ${question} `, (answer) => resolve15(answer.trim()));
1967
+ return await new Promise((resolve14) => {
1968
+ rl.question(` ${question} `, (answer) => resolve14(answer.trim()));
1975
1969
  });
1976
1970
  } finally {
1977
1971
  rl.close();
@@ -1983,7 +1977,7 @@ async function askSecret(question) {
1983
1977
  process.stdin.setRawMode(true);
1984
1978
  process.stdin.resume();
1985
1979
  const bytes = [];
1986
- return await new Promise((resolve15) => {
1980
+ return await new Promise((resolve14) => {
1987
1981
  const cleanup = () => {
1988
1982
  try {
1989
1983
  process.stdin.setRawMode(false);
@@ -2001,7 +1995,7 @@ async function askSecret(question) {
2001
1995
  } else if (byte === 13 || byte === 10) {
2002
1996
  cleanup();
2003
1997
  process.stdout.write("\n");
2004
- resolve15(Buffer.from(bytes).toString("utf8").trim());
1998
+ resolve14(Buffer.from(bytes).toString("utf8").trim());
2005
1999
  return;
2006
2000
  } else if (byte === 127 || byte === 8) {
2007
2001
  if (bytes.length > 0) {
@@ -2051,10 +2045,10 @@ async function askSecretPiped(question) {
2051
2045
  };
2052
2046
  process.on("SIGINT", onSigint);
2053
2047
  try {
2054
- return await new Promise((resolve15) => {
2048
+ return await new Promise((resolve14) => {
2055
2049
  rl.question(` ${question} `, (answer) => {
2056
2050
  process.stdout.write("\n");
2057
- resolve15(answer.trim());
2051
+ resolve14(answer.trim());
2058
2052
  });
2059
2053
  });
2060
2054
  } finally {
@@ -4012,7 +4006,7 @@ async function runSandboxList(opts, io) {
4012
4006
  // src/commands/harness.ts
4013
4007
  import { existsSync as existsSync18 } from "node:fs";
4014
4008
  import { homedir as homedir2 } from "node:os";
4015
- import { delimiter as delimiter2, join as join12, resolve as resolve12 } from "node:path";
4009
+ import { delimiter as delimiter2, join as join12 } from "node:path";
4016
4010
 
4017
4011
  // src/lib/docs.ts
4018
4012
  var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
@@ -4028,24 +4022,35 @@ function sourceDocsUrl(docsPath) {
4028
4022
  import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync11, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "node:fs";
4029
4023
  import { basename as basename6, dirname as dirname8, isAbsolute as isAbsolute2, join as join10, resolve as resolve10 } from "node:path";
4030
4024
  var HOST_CONFIG_MODE = 420;
4031
- function hostConfigPath(env = process.env, home) {
4032
- return join10(resolveAgroUserStateHome(env, home), 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);
4033
4030
  }
4034
- function legacyHostConfigPath(env = process.env, home) {
4035
- return join10(resolveLegacyUserStateHome(env, home), GENERATIONS.legacy.configFile);
4031
+ function hostConfigPath(env, home) {
4032
+ return join10(hostStateHome(env, home), HOST_CONFIG_FILE);
4036
4033
  }
4037
- function readableHostConfigPath(env, home) {
4038
- const path4 = hostConfigPath(env, home);
4039
- if (existsSync16(path4)) return path4;
4040
- const legacy = legacyHostConfigPath(env, home);
4041
- return existsSync16(legacy) ? legacy : path4;
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
+ }
4042
4040
  }
4043
- function defaultHarnessRoot(env = process.env, home) {
4044
- return resolveAgroUserStateHome(env, home);
4041
+ function workspacesRoot(env, home) {
4042
+ return join10(hostStateHome(env, home), WORKSPACES_SUBDIR);
4043
+ }
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);
4045
4050
  }
4046
4051
  function validateHostConfig(value) {
4047
4052
  if (!value || typeof value !== "object" || Array.isArray(value)) {
4048
- throw new Error(`${GENERATIONS.agro.configFile}: must contain a JSON object`);
4053
+ throw new Error(`${HOST_CONFIG_FILE}: must contain a JSON object`);
4049
4054
  }
4050
4055
  const record = value;
4051
4056
  if (record.version !== void 0 && record.version !== 1) {
@@ -4097,8 +4102,8 @@ function validateReceipts(value, field, noun) {
4097
4102
  }
4098
4103
  }
4099
4104
  }
4100
- function readHostConfig(env = process.env, home) {
4101
- const path4 = readableHostConfigPath(env, home);
4105
+ function readHostConfig(env, home) {
4106
+ const path4 = hostConfigPath(env, home);
4102
4107
  if (!existsSync16(path4)) return { version: 1 };
4103
4108
  let raw;
4104
4109
  try {
@@ -4115,7 +4120,7 @@ function readHostConfig(env = process.env, home) {
4115
4120
  }
4116
4121
  return validateHostConfig(parsed);
4117
4122
  }
4118
- function writeHostConfig(config, env = process.env, home) {
4123
+ function writeHostConfig(config, env, home) {
4119
4124
  const path4 = hostConfigPath(env, home);
4120
4125
  const validated = validateHostConfig({ ...config, version: 1 });
4121
4126
  const body = `${JSON.stringify(validated, null, 2)}
@@ -4125,7 +4130,6 @@ function writeHostConfig(config, env = process.env, home) {
4125
4130
  try {
4126
4131
  writeFileSync8(tmp, body, { mode: HOST_CONFIG_MODE, encoding: "utf8" });
4127
4132
  renameSync5(tmp, path4);
4128
- retireLegacyHostConfig(env, home, path4);
4129
4133
  } catch (error) {
4130
4134
  try {
4131
4135
  unlinkSync5(tmp);
@@ -4134,19 +4138,14 @@ function writeHostConfig(config, env = process.env, home) {
4134
4138
  throw error;
4135
4139
  }
4136
4140
  }
4137
- function resolveHarnessRoot(explicit, env = process.env, home) {
4141
+ function resolveHarnessRoot(explicit, env, home) {
4138
4142
  if (typeof explicit === "string" && explicit !== "") return resolve10(explicit);
4139
4143
  const configured = readHostConfig(env, home).harnessRoot;
4140
4144
  if (typeof configured === "string" && configured !== "") return resolve10(configured);
4141
4145
  return defaultHarnessRoot(env, home);
4142
4146
  }
4143
- function retireLegacyHostConfig(env, home, written) {
4144
- const legacy = legacyHostConfigPath(env, home);
4145
- if (legacy === written || !existsSync16(legacy)) return;
4146
- unlinkSync5(legacy);
4147
- }
4148
4147
  function fieldError2(path4, requirement) {
4149
- return new Error(`${GENERATIONS.agro.configFile}: ${path4} ${requirement}`);
4148
+ return new Error(`${HOST_CONFIG_FILE}: ${path4} ${requirement}`);
4150
4149
  }
4151
4150
 
4152
4151
  // src/lib/host-workspace.ts
@@ -4159,6 +4158,40 @@ var IGNORABLE_ENTRIES = [
4159
4158
  GENERATIONS.agro.configFile,
4160
4159
  GENERATIONS.legacy.configFile
4161
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
+ }
4162
4195
  function isIgnorableEntry(name) {
4163
4196
  return name.startsWith(".") || IGNORABLE_ENTRIES.includes(name);
4164
4197
  }
@@ -4229,7 +4262,8 @@ var HARNESS_CATALOG = [
4229
4262
  verifyArgv: ["claude", "--version"],
4230
4263
  uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "@anthropic-ai/claude-code"],
4231
4264
  docsPath: "docs/harnesses/claude-code.md",
4232
- kind: "installable"
4265
+ kind: "installable",
4266
+ bypassPermissionsFlag: "--permission-mode bypassPermissions"
4233
4267
  },
4234
4268
  {
4235
4269
  id: "codex",
@@ -4376,6 +4410,9 @@ function resolveVerifyArgv(entry, prefix) {
4376
4410
  function resolveUninstallArgv(entry, prefix) {
4377
4411
  return entry.uninstallArgv === null ? null : substitute(entry.uninstallArgv, prefix);
4378
4412
  }
4413
+ function harnessLaunchCommand(entry) {
4414
+ return entry.bypassPermissionsFlag === void 0 ? entry.binary : `${entry.binary} ${entry.bypassPermissionsFlag}`;
4415
+ }
4379
4416
  function harnessBinPath(prefix) {
4380
4417
  return `${prefix}/bin`;
4381
4418
  }
@@ -4443,7 +4480,7 @@ function stateOf(entry, installed, location) {
4443
4480
  function probeableHost(env, home) {
4444
4481
  let root;
4445
4482
  try {
4446
- root = resolveHarnessRoot(void 0, env);
4483
+ root = resolveHarnessRoot(void 0, env, home);
4447
4484
  } catch {
4448
4485
  return void 0;
4449
4486
  }
@@ -4578,7 +4615,13 @@ function isInteractive(opts) {
4578
4615
  async function installOnHost(entry, opts, io, run, status) {
4579
4616
  const bin = opts.bin;
4580
4617
  const env = opts.env ?? process.env;
4581
- 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
+ }
4582
4625
  if (!flagged && !isInteractive(opts)) {
4583
4626
  io.stderr(
4584
4627
  sandboxRefusal(bin, status) + `Or install on the host with \`${bin} harness install ${entry.id} --host\`.
@@ -4593,15 +4636,17 @@ async function installOnHost(entry, opts, io, run, status) {
4593
4636
  }
4594
4637
  let root;
4595
4638
  let recorded;
4639
+ let explicit;
4596
4640
  try {
4597
- recorded = readHostConfig(env).harnessRoot;
4598
- 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);
4599
4644
  } catch (err) {
4600
4645
  io.stderr(`${bin} harness: ${messageOf(err)}
4601
4646
  `);
4602
4647
  return 1;
4603
4648
  }
4604
- const sticky = opts.path === void 0 && recorded !== void 0 && recorded !== "";
4649
+ const sticky = explicit === void 0 && recorded !== void 0 && recorded !== "";
4605
4650
  if (!flagged) {
4606
4651
  const ask2 = io.ask ?? ask;
4607
4652
  const answer = (await ask2(
@@ -4612,12 +4657,23 @@ async function installOnHost(entry, opts, io, run, status) {
4612
4657
  return 1;
4613
4658
  }
4614
4659
  if (!sticky) {
4615
- const chosen = (await ask2(`Harness root [${root}]:`)).trim();
4616
- 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
+ }
4617
4668
  }
4618
4669
  }
4619
4670
  if (sticky) io.stdout(`using the recorded harness root ${root}
4620
4671
  `);
4672
+ const nested = stateHomeRootRefusal(bin, root, home);
4673
+ if (nested !== void 0) {
4674
+ io.stderr(nested);
4675
+ return 1;
4676
+ }
4621
4677
  try {
4622
4678
  if (!existsSync18(join12(root, ".git"))) io.stdout(`cloning ${AGRO_REPO_URL} into ${root}\u2026
4623
4679
  `);
@@ -4633,8 +4689,21 @@ async function installOnHost(entry, opts, io, run, status) {
4633
4689
  `);
4634
4690
  return 1;
4635
4691
  }
4636
- const prefix = hostPrefix(homeOf(opts));
4692
+ const prefix = hostPrefix(home);
4637
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
+ }
4638
4707
  const hermes = entry.id === "hermes";
4639
4708
  const installEnv = hermes ? {
4640
4709
  ...aliasedEnvPair("PROJECT_ROOT", root),
@@ -4678,14 +4747,15 @@ async function installOnHost(entry, opts, io, run, status) {
4678
4747
  workspaceRoot: root
4679
4748
  };
4680
4749
  try {
4681
- const config = readHostConfig(env);
4750
+ const config = readHostConfig(env, home);
4682
4751
  writeHostConfig(
4683
4752
  {
4684
4753
  ...config,
4685
4754
  harnessRoot: root,
4686
4755
  hostHarnesses: { ...config.hostHarnesses ?? {}, [entry.id]: receipt }
4687
4756
  },
4688
- env
4757
+ env,
4758
+ home
4689
4759
  );
4690
4760
  } catch (err) {
4691
4761
  io.stderr(`${bin} harness: could not record the install: ${messageOf(err)}
@@ -4698,7 +4768,7 @@ async function installOnHost(entry, opts, io, run, status) {
4698
4768
  io.stdout(`Add this line to your shell profile: export PATH="${harnessBinPath(prefix)}:$PATH"
4699
4769
  `);
4700
4770
  }
4701
- 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)}
4702
4772
  `);
4703
4773
  return 0;
4704
4774
  }
@@ -4742,7 +4812,13 @@ async function removeHarness(entry, target, prefix, user, opts, io) {
4742
4812
  async function uninstallOnHost(entry, opts, io, run) {
4743
4813
  const bin = opts.bin;
4744
4814
  const env = opts.env ?? process.env;
4745
- 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
+ }
4746
4822
  if (resolveUninstallArgv(entry, computed) === null) {
4747
4823
  io.stdout(`${entry.id}: nothing to remove \u2014 npx fetches it at each run
4748
4824
  `);
@@ -4751,13 +4827,18 @@ async function uninstallOnHost(entry, opts, io, run) {
4751
4827
  let config;
4752
4828
  let workspace;
4753
4829
  try {
4754
- config = readHostConfig(env);
4755
- workspace = resolveHarnessRoot(void 0, env);
4830
+ config = readHostConfig(env, home);
4831
+ workspace = resolveHarnessRoot(void 0, env, home);
4756
4832
  } catch (err) {
4757
4833
  io.stderr(`${bin} harness: ${messageOf(err)}
4758
4834
  `);
4759
4835
  return 1;
4760
4836
  }
4837
+ const nested = stateHomeRootRefusal(bin, workspace, home);
4838
+ if (nested !== void 0) {
4839
+ io.stderr(nested);
4840
+ return 1;
4841
+ }
4761
4842
  const receipt = config.hostHarnesses?.[entry.id];
4762
4843
  if (receipt === void 0 && opts.force !== true) {
4763
4844
  io.stderr(
@@ -4774,7 +4855,7 @@ Removing it could delete a harness you installed yourself. Re-run with \`--force
4774
4855
  const remaining = { ...config.hostHarnesses ?? {} };
4775
4856
  delete remaining[entry.id];
4776
4857
  try {
4777
- writeHostConfig({ ...config, hostHarnesses: remaining }, env);
4858
+ writeHostConfig({ ...config, hostHarnesses: remaining }, env, home);
4778
4859
  } catch (err) {
4779
4860
  io.stderr(`${bin} harness: could not clear the install record: ${messageOf(err)}
4780
4861
  `);
@@ -4859,7 +4940,7 @@ async function runHarnessInstall(name, opts, io) {
4859
4940
  // src/commands/tool.ts
4860
4941
  import { existsSync as existsSync19 } from "node:fs";
4861
4942
  import { homedir as homedir3 } from "node:os";
4862
- 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";
4863
4944
 
4864
4945
  // src/lib/tools/catalog.ts
4865
4946
  var TOOLS_DOC = "docs/installation.md";
@@ -5145,7 +5226,7 @@ function rowOf(entry, installed, version, location) {
5145
5226
  function probeableHost2(env, home) {
5146
5227
  let root;
5147
5228
  try {
5148
- root = resolveHarnessRoot(void 0, env);
5229
+ root = resolveHarnessRoot(void 0, env, home);
5149
5230
  } catch {
5150
5231
  return void 0;
5151
5232
  }
@@ -5293,6 +5374,7 @@ ${hostCapableToolIds().map((t) => ` ${bin} tool install ${t} --host`).join("\n"
5293
5374
  async function installOnHost2(entry, opts, io, run, status) {
5294
5375
  const bin = opts.bin;
5295
5376
  const env = opts.env ?? process.env;
5377
+ const home = homeOf2(opts);
5296
5378
  const flagged = opts.host === true || opts.path !== void 0;
5297
5379
  if (!entry.hostCapable) {
5298
5380
  io.stderr(sandboxRefusal2(bin, status));
@@ -5326,8 +5408,8 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5326
5408
  let root;
5327
5409
  let recorded;
5328
5410
  try {
5329
- recorded = readHostConfig(env).harnessRoot;
5330
- root = resolveHarnessRoot(opts.path, env);
5411
+ recorded = readHostConfig(env, home).harnessRoot;
5412
+ root = resolveHarnessRoot(opts.path, env, home);
5331
5413
  } catch (err) {
5332
5414
  io.stderr(`${bin} tool: ${messageOf2(err)}
5333
5415
  `);
@@ -5345,7 +5427,7 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5345
5427
  }
5346
5428
  if (!sticky) {
5347
5429
  const chosen = (await ask2(`Harness root [${root}]:`)).trim();
5348
- if (chosen !== "") root = resolve13(chosen);
5430
+ if (chosen !== "") root = resolve12(chosen);
5349
5431
  }
5350
5432
  }
5351
5433
  if (sticky) io.stdout(`using the recorded harness root ${root}
@@ -5365,7 +5447,7 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5365
5447
  `);
5366
5448
  return 1;
5367
5449
  }
5368
- const prefix = hostPrefix2(homeOf2(opts));
5450
+ const prefix = hostPrefix2(home);
5369
5451
  const installEnv = { NPM_USER_PREFIX: prefix };
5370
5452
  const target = hostTargetFor2(root, prefix, run, env);
5371
5453
  if (await probeInstalled2(target, entry, void 0, installEnv) === true) {
@@ -5394,14 +5476,15 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5394
5476
  workspaceRoot: root
5395
5477
  };
5396
5478
  try {
5397
- const config = readHostConfig(env);
5479
+ const config = readHostConfig(env, home);
5398
5480
  writeHostConfig(
5399
5481
  {
5400
5482
  ...config,
5401
5483
  harnessRoot: root,
5402
5484
  hostTools: { ...config.hostTools ?? {}, [entry.id]: receipt }
5403
5485
  },
5404
- env
5486
+ env,
5487
+ home
5405
5488
  );
5406
5489
  } catch (err) {
5407
5490
  io.stderr(`${bin} tool: could not record the install: ${messageOf2(err)}
@@ -5502,12 +5585,13 @@ async function removeTool(entry, target, prefix, user, opts, io) {
5502
5585
  async function uninstallOnHost2(entry, opts, io, run) {
5503
5586
  const bin = opts.bin;
5504
5587
  const env = opts.env ?? process.env;
5505
- const computed = hostPrefix2(homeOf2(opts));
5588
+ const home = homeOf2(opts);
5589
+ const computed = hostPrefix2(home);
5506
5590
  let config;
5507
5591
  let workspace;
5508
5592
  try {
5509
- config = readHostConfig(env);
5510
- workspace = resolveHarnessRoot(void 0, env);
5593
+ config = readHostConfig(env, home);
5594
+ workspace = resolveHarnessRoot(void 0, env, home);
5511
5595
  } catch (err) {
5512
5596
  io.stderr(`${bin} tool: ${messageOf2(err)}
5513
5597
  `);
@@ -5529,7 +5613,7 @@ Removing it could delete a tool you installed yourself. Re-run with \`--force\`
5529
5613
  const remaining = { ...config.hostTools ?? {} };
5530
5614
  delete remaining[entry.id];
5531
5615
  try {
5532
- writeHostConfig({ ...config, hostTools: remaining }, env);
5616
+ writeHostConfig({ ...config, hostTools: remaining }, env, home);
5533
5617
  } catch (err) {
5534
5618
  io.stderr(`${bin} tool: could not clear the install record: ${messageOf2(err)}
5535
5619
  `);
@@ -5624,8 +5708,8 @@ function fetchRemoteSource(opts) {
5624
5708
  }
5625
5709
 
5626
5710
  // src/cli.ts
5627
- var VERSION = true ? "0.12.1" : "0.0.0-dev";
5628
- var DEFAULT_SOURCE_OH_DIR = resolve14(
5711
+ var VERSION = true ? "0.12.2" : "0.0.0-dev";
5712
+ var DEFAULT_SOURCE_OH_DIR = resolve13(
5629
5713
  dirname10(fileURLToPath(import.meta.url)),
5630
5714
  "../.."
5631
5715
  );
@@ -5862,10 +5946,11 @@ Usage:
5862
5946
 
5863
5947
  \`install\` and \`uninstall\` act on the running sandbox. When no sandbox is
5864
5948
  reachable they act on the host. A host install clones the AGRO workspace into the
5865
- harness root, then installs the harness into \`~/.local\`. \`--path <dir>\` chooses
5866
- the harness root; a successful install records it as \`harnessRoot\` in the host
5867
- \`${stateNames(bin).configFile}\`. The default is \`~/.agro\`, and the install prefix is
5868
- always \`~/.local\`.
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\`.
5869
5954
 
5870
5955
  On the host, \`uninstall\` removes only the harness \`install\` recorded, from the
5871
5956
  prefix in that record. Without a record it refuses, and \`--force\` overrides. In
@@ -5874,7 +5959,8 @@ the sandbox it needs no record. See \`docs/harnesses/overview.md\`.
5874
5959
  Flags:
5875
5960
  --json Machine-readable output (list/status)
5876
5961
  --host Install on the host when the sandbox is not running (install)
5877
- --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)
5878
5964
  --force Remove without a recorded host install (uninstall)
5879
5965
 
5880
5966
  Harnesses:
@@ -5930,8 +6016,8 @@ only on these tools:
5930
6016
  ${hostCapableToolIds().map((t) => ` ${t}`).join("\n")}
5931
6017
 
5932
6018
  A host install clones the AGRO workspace into the harness root \u2014 \`--path <dir>\`,
5933
- then \`harnessRoot\` in the host \`${stateNames(bin).configFile}\`, then \`~/.agro\` \u2014 and installs into
5934
- \`~/.local\`. It records the install as \`hostTools\` in that same file. On the host,
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,
5935
6021
  \`uninstall\` removes only what \`install\` recorded, from the prefix in that record.
5936
6022
  Without a record it refuses, and \`--force\` overrides.
5937
6023
 
@@ -6331,11 +6417,16 @@ function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
6331
6417
  }
6332
6418
  const positionals = [];
6333
6419
  let expectPath = false;
6420
+ let expectWorkspace = false;
6334
6421
  for (const token of rest) {
6335
6422
  if (expectPath) {
6336
6423
  expectPath = false;
6337
6424
  args.path = token;
6338
6425
  args.host = true;
6426
+ } else if (expectWorkspace) {
6427
+ expectWorkspace = false;
6428
+ args.workspace = token;
6429
+ args.host = true;
6339
6430
  } else if (token === "--json") {
6340
6431
  args.json = true;
6341
6432
  } else if (token === "--host") {
@@ -6347,6 +6438,11 @@ function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
6347
6438
  } else if (token.startsWith("--path=")) {
6348
6439
  args.path = token.slice("--path=".length);
6349
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;
6350
6446
  } else if (token.startsWith("-")) {
6351
6447
  return { ok: false, error: `${bin} harness: unknown flag "${token}"` };
6352
6448
  } else {
@@ -6356,6 +6452,15 @@ function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
6356
6452
  if (expectPath || args.path === "") {
6357
6453
  return { ok: false, error: `${bin} harness: --path requires a directory` };
6358
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
+ }
6359
6464
  const [sub, name, ...extra] = positionals;
6360
6465
  if (sub !== "list" && sub !== "install" && sub !== "status" && sub !== "uninstall") {
6361
6466
  return {
@@ -6374,7 +6479,10 @@ function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
6374
6479
  return { ok: false, error: `${bin} harness list: unexpected argument "${name}"` };
6375
6480
  }
6376
6481
  if (sub !== "install" && args.host) {
6377
- 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
+ };
6378
6486
  }
6379
6487
  if (sub !== "uninstall" && args.force) {
6380
6488
  return { ok: false, error: `${bin} harness ${sub}: --force applies to uninstall only` };
@@ -6449,13 +6557,13 @@ function bundledPayloadExists(bundled, exists = existsSync20) {
6449
6557
  function resolveUpdateSource(args, bundled, bin = LEGACY_PRODUCT.bin) {
6450
6558
  const exists = bundled.exists ?? existsSync20;
6451
6559
  if (args.fromDir !== void 0) {
6452
- return { kind: "local", fromDir: resolve14(args.fromDir) };
6560
+ return { kind: "local", fromDir: resolve13(args.fromDir) };
6453
6561
  }
6454
6562
  if (args.fromRemote) {
6455
6563
  return { kind: "remote", ref: args.ref };
6456
6564
  }
6457
6565
  if (bundledPayloadExists(bundled, exists)) {
6458
- return { kind: "local", fromDir: resolve14(bundled.sourceOhDir, "..") };
6566
+ return { kind: "local", fromDir: resolve13(bundled.sourceOhDir, "..") };
6459
6567
  }
6460
6568
  return {
6461
6569
  kind: "remote",
@@ -6747,7 +6855,12 @@ async function main(argv) {
6747
6855
  }
6748
6856
  return await runHarnessInstall(
6749
6857
  a.name,
6750
- { 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
+ },
6751
6864
  io
6752
6865
  );
6753
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,16 +230,10 @@ function resolveRegistryHome(env = process.env, home = homedir(), warn) {
230
230
  if (configured !== void 0) return { path: resolve(configured), configured: true };
231
231
  return { path: home, configured: false };
232
232
  }
233
- function generationUserStateHome(generation, env, home, warn) {
233
+ function resolveAgroUserStateHome(env = process.env, home = homedir(), warn) {
234
234
  const configured = aliasedEnvValue(env, "HOME", warn);
235
235
  if (configured !== void 0) return resolve(configured);
236
- return join(home, GENERATIONS[generation].userStateDir);
237
- }
238
- function resolveAgroUserStateHome(env = process.env, home = homedir(), warn) {
239
- return generationUserStateHome("agro", env, home, warn);
240
- }
241
- function resolveLegacyUserStateHome(env = process.env, home = homedir(), warn) {
242
- return generationUserStateHome("legacy", env, home, warn);
236
+ return join(home, GENERATIONS.agro.userStateDir);
243
237
  }
244
238
  function resolveUserStateHome(env = process.env, home = homedir(), warn) {
245
239
  const configured = aliasedEnvValue(env, "HOME", warn);
@@ -1970,8 +1964,8 @@ function redact(token) {
1970
1964
  async function ask(question) {
1971
1965
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1972
1966
  try {
1973
- return await new Promise((resolve15) => {
1974
- rl.question(` ${question} `, (answer) => resolve15(answer.trim()));
1967
+ return await new Promise((resolve14) => {
1968
+ rl.question(` ${question} `, (answer) => resolve14(answer.trim()));
1975
1969
  });
1976
1970
  } finally {
1977
1971
  rl.close();
@@ -1983,7 +1977,7 @@ async function askSecret(question) {
1983
1977
  process.stdin.setRawMode(true);
1984
1978
  process.stdin.resume();
1985
1979
  const bytes = [];
1986
- return await new Promise((resolve15) => {
1980
+ return await new Promise((resolve14) => {
1987
1981
  const cleanup = () => {
1988
1982
  try {
1989
1983
  process.stdin.setRawMode(false);
@@ -2001,7 +1995,7 @@ async function askSecret(question) {
2001
1995
  } else if (byte === 13 || byte === 10) {
2002
1996
  cleanup();
2003
1997
  process.stdout.write("\n");
2004
- resolve15(Buffer.from(bytes).toString("utf8").trim());
1998
+ resolve14(Buffer.from(bytes).toString("utf8").trim());
2005
1999
  return;
2006
2000
  } else if (byte === 127 || byte === 8) {
2007
2001
  if (bytes.length > 0) {
@@ -2051,10 +2045,10 @@ async function askSecretPiped(question) {
2051
2045
  };
2052
2046
  process.on("SIGINT", onSigint);
2053
2047
  try {
2054
- return await new Promise((resolve15) => {
2048
+ return await new Promise((resolve14) => {
2055
2049
  rl.question(` ${question} `, (answer) => {
2056
2050
  process.stdout.write("\n");
2057
- resolve15(answer.trim());
2051
+ resolve14(answer.trim());
2058
2052
  });
2059
2053
  });
2060
2054
  } finally {
@@ -4012,7 +4006,7 @@ async function runSandboxList(opts, io) {
4012
4006
  // src/commands/harness.ts
4013
4007
  import { existsSync as existsSync18 } from "node:fs";
4014
4008
  import { homedir as homedir2 } from "node:os";
4015
- import { delimiter as delimiter2, join as join12, resolve as resolve12 } from "node:path";
4009
+ import { delimiter as delimiter2, join as join12 } from "node:path";
4016
4010
 
4017
4011
  // src/lib/docs.ts
4018
4012
  var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
@@ -4028,24 +4022,35 @@ function sourceDocsUrl(docsPath) {
4028
4022
  import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync11, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "node:fs";
4029
4023
  import { basename as basename6, dirname as dirname8, isAbsolute as isAbsolute2, join as join10, resolve as resolve10 } from "node:path";
4030
4024
  var HOST_CONFIG_MODE = 420;
4031
- function hostConfigPath(env = process.env, home) {
4032
- return join10(resolveAgroUserStateHome(env, home), 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);
4033
4030
  }
4034
- function legacyHostConfigPath(env = process.env, home) {
4035
- return join10(resolveLegacyUserStateHome(env, home), GENERATIONS.legacy.configFile);
4031
+ function hostConfigPath(env, home) {
4032
+ return join10(hostStateHome(env, home), HOST_CONFIG_FILE);
4036
4033
  }
4037
- function readableHostConfigPath(env, home) {
4038
- const path4 = hostConfigPath(env, home);
4039
- if (existsSync16(path4)) return path4;
4040
- const legacy = legacyHostConfigPath(env, home);
4041
- return existsSync16(legacy) ? legacy : path4;
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
+ }
4042
4040
  }
4043
- function defaultHarnessRoot(env = process.env, home) {
4044
- return resolveAgroUserStateHome(env, home);
4041
+ function workspacesRoot(env, home) {
4042
+ return join10(hostStateHome(env, home), WORKSPACES_SUBDIR);
4043
+ }
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);
4045
4050
  }
4046
4051
  function validateHostConfig(value) {
4047
4052
  if (!value || typeof value !== "object" || Array.isArray(value)) {
4048
- throw new Error(`${GENERATIONS.agro.configFile}: must contain a JSON object`);
4053
+ throw new Error(`${HOST_CONFIG_FILE}: must contain a JSON object`);
4049
4054
  }
4050
4055
  const record = value;
4051
4056
  if (record.version !== void 0 && record.version !== 1) {
@@ -4097,8 +4102,8 @@ function validateReceipts(value, field, noun) {
4097
4102
  }
4098
4103
  }
4099
4104
  }
4100
- function readHostConfig(env = process.env, home) {
4101
- const path4 = readableHostConfigPath(env, home);
4105
+ function readHostConfig(env, home) {
4106
+ const path4 = hostConfigPath(env, home);
4102
4107
  if (!existsSync16(path4)) return { version: 1 };
4103
4108
  let raw;
4104
4109
  try {
@@ -4115,7 +4120,7 @@ function readHostConfig(env = process.env, home) {
4115
4120
  }
4116
4121
  return validateHostConfig(parsed);
4117
4122
  }
4118
- function writeHostConfig(config, env = process.env, home) {
4123
+ function writeHostConfig(config, env, home) {
4119
4124
  const path4 = hostConfigPath(env, home);
4120
4125
  const validated = validateHostConfig({ ...config, version: 1 });
4121
4126
  const body = `${JSON.stringify(validated, null, 2)}
@@ -4125,7 +4130,6 @@ function writeHostConfig(config, env = process.env, home) {
4125
4130
  try {
4126
4131
  writeFileSync8(tmp, body, { mode: HOST_CONFIG_MODE, encoding: "utf8" });
4127
4132
  renameSync5(tmp, path4);
4128
- retireLegacyHostConfig(env, home, path4);
4129
4133
  } catch (error) {
4130
4134
  try {
4131
4135
  unlinkSync5(tmp);
@@ -4134,19 +4138,14 @@ function writeHostConfig(config, env = process.env, home) {
4134
4138
  throw error;
4135
4139
  }
4136
4140
  }
4137
- function resolveHarnessRoot(explicit, env = process.env, home) {
4141
+ function resolveHarnessRoot(explicit, env, home) {
4138
4142
  if (typeof explicit === "string" && explicit !== "") return resolve10(explicit);
4139
4143
  const configured = readHostConfig(env, home).harnessRoot;
4140
4144
  if (typeof configured === "string" && configured !== "") return resolve10(configured);
4141
4145
  return defaultHarnessRoot(env, home);
4142
4146
  }
4143
- function retireLegacyHostConfig(env, home, written) {
4144
- const legacy = legacyHostConfigPath(env, home);
4145
- if (legacy === written || !existsSync16(legacy)) return;
4146
- unlinkSync5(legacy);
4147
- }
4148
4147
  function fieldError2(path4, requirement) {
4149
- return new Error(`${GENERATIONS.agro.configFile}: ${path4} ${requirement}`);
4148
+ return new Error(`${HOST_CONFIG_FILE}: ${path4} ${requirement}`);
4150
4149
  }
4151
4150
 
4152
4151
  // src/lib/host-workspace.ts
@@ -4159,6 +4158,40 @@ var IGNORABLE_ENTRIES = [
4159
4158
  GENERATIONS.agro.configFile,
4160
4159
  GENERATIONS.legacy.configFile
4161
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
+ }
4162
4195
  function isIgnorableEntry(name) {
4163
4196
  return name.startsWith(".") || IGNORABLE_ENTRIES.includes(name);
4164
4197
  }
@@ -4229,7 +4262,8 @@ var HARNESS_CATALOG = [
4229
4262
  verifyArgv: ["claude", "--version"],
4230
4263
  uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "@anthropic-ai/claude-code"],
4231
4264
  docsPath: "docs/harnesses/claude-code.md",
4232
- kind: "installable"
4265
+ kind: "installable",
4266
+ bypassPermissionsFlag: "--permission-mode bypassPermissions"
4233
4267
  },
4234
4268
  {
4235
4269
  id: "codex",
@@ -4376,6 +4410,9 @@ function resolveVerifyArgv(entry, prefix) {
4376
4410
  function resolveUninstallArgv(entry, prefix) {
4377
4411
  return entry.uninstallArgv === null ? null : substitute(entry.uninstallArgv, prefix);
4378
4412
  }
4413
+ function harnessLaunchCommand(entry) {
4414
+ return entry.bypassPermissionsFlag === void 0 ? entry.binary : `${entry.binary} ${entry.bypassPermissionsFlag}`;
4415
+ }
4379
4416
  function harnessBinPath(prefix) {
4380
4417
  return `${prefix}/bin`;
4381
4418
  }
@@ -4443,7 +4480,7 @@ function stateOf(entry, installed, location) {
4443
4480
  function probeableHost(env, home) {
4444
4481
  let root;
4445
4482
  try {
4446
- root = resolveHarnessRoot(void 0, env);
4483
+ root = resolveHarnessRoot(void 0, env, home);
4447
4484
  } catch {
4448
4485
  return void 0;
4449
4486
  }
@@ -4578,7 +4615,13 @@ function isInteractive(opts) {
4578
4615
  async function installOnHost(entry, opts, io, run, status) {
4579
4616
  const bin = opts.bin;
4580
4617
  const env = opts.env ?? process.env;
4581
- 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
+ }
4582
4625
  if (!flagged && !isInteractive(opts)) {
4583
4626
  io.stderr(
4584
4627
  sandboxRefusal(bin, status) + `Or install on the host with \`${bin} harness install ${entry.id} --host\`.
@@ -4593,15 +4636,17 @@ async function installOnHost(entry, opts, io, run, status) {
4593
4636
  }
4594
4637
  let root;
4595
4638
  let recorded;
4639
+ let explicit;
4596
4640
  try {
4597
- recorded = readHostConfig(env).harnessRoot;
4598
- 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);
4599
4644
  } catch (err) {
4600
4645
  io.stderr(`${bin} harness: ${messageOf(err)}
4601
4646
  `);
4602
4647
  return 1;
4603
4648
  }
4604
- const sticky = opts.path === void 0 && recorded !== void 0 && recorded !== "";
4649
+ const sticky = explicit === void 0 && recorded !== void 0 && recorded !== "";
4605
4650
  if (!flagged) {
4606
4651
  const ask2 = io.ask ?? ask;
4607
4652
  const answer = (await ask2(
@@ -4612,12 +4657,23 @@ async function installOnHost(entry, opts, io, run, status) {
4612
4657
  return 1;
4613
4658
  }
4614
4659
  if (!sticky) {
4615
- const chosen = (await ask2(`Harness root [${root}]:`)).trim();
4616
- 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
+ }
4617
4668
  }
4618
4669
  }
4619
4670
  if (sticky) io.stdout(`using the recorded harness root ${root}
4620
4671
  `);
4672
+ const nested = stateHomeRootRefusal(bin, root, home);
4673
+ if (nested !== void 0) {
4674
+ io.stderr(nested);
4675
+ return 1;
4676
+ }
4621
4677
  try {
4622
4678
  if (!existsSync18(join12(root, ".git"))) io.stdout(`cloning ${AGRO_REPO_URL} into ${root}\u2026
4623
4679
  `);
@@ -4633,8 +4689,21 @@ async function installOnHost(entry, opts, io, run, status) {
4633
4689
  `);
4634
4690
  return 1;
4635
4691
  }
4636
- const prefix = hostPrefix(homeOf(opts));
4692
+ const prefix = hostPrefix(home);
4637
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
+ }
4638
4707
  const hermes = entry.id === "hermes";
4639
4708
  const installEnv = hermes ? {
4640
4709
  ...aliasedEnvPair("PROJECT_ROOT", root),
@@ -4678,14 +4747,15 @@ async function installOnHost(entry, opts, io, run, status) {
4678
4747
  workspaceRoot: root
4679
4748
  };
4680
4749
  try {
4681
- const config = readHostConfig(env);
4750
+ const config = readHostConfig(env, home);
4682
4751
  writeHostConfig(
4683
4752
  {
4684
4753
  ...config,
4685
4754
  harnessRoot: root,
4686
4755
  hostHarnesses: { ...config.hostHarnesses ?? {}, [entry.id]: receipt }
4687
4756
  },
4688
- env
4757
+ env,
4758
+ home
4689
4759
  );
4690
4760
  } catch (err) {
4691
4761
  io.stderr(`${bin} harness: could not record the install: ${messageOf(err)}
@@ -4698,7 +4768,7 @@ async function installOnHost(entry, opts, io, run, status) {
4698
4768
  io.stdout(`Add this line to your shell profile: export PATH="${harnessBinPath(prefix)}:$PATH"
4699
4769
  `);
4700
4770
  }
4701
- 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)}
4702
4772
  `);
4703
4773
  return 0;
4704
4774
  }
@@ -4742,7 +4812,13 @@ async function removeHarness(entry, target, prefix, user, opts, io) {
4742
4812
  async function uninstallOnHost(entry, opts, io, run) {
4743
4813
  const bin = opts.bin;
4744
4814
  const env = opts.env ?? process.env;
4745
- 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
+ }
4746
4822
  if (resolveUninstallArgv(entry, computed) === null) {
4747
4823
  io.stdout(`${entry.id}: nothing to remove \u2014 npx fetches it at each run
4748
4824
  `);
@@ -4751,13 +4827,18 @@ async function uninstallOnHost(entry, opts, io, run) {
4751
4827
  let config;
4752
4828
  let workspace;
4753
4829
  try {
4754
- config = readHostConfig(env);
4755
- workspace = resolveHarnessRoot(void 0, env);
4830
+ config = readHostConfig(env, home);
4831
+ workspace = resolveHarnessRoot(void 0, env, home);
4756
4832
  } catch (err) {
4757
4833
  io.stderr(`${bin} harness: ${messageOf(err)}
4758
4834
  `);
4759
4835
  return 1;
4760
4836
  }
4837
+ const nested = stateHomeRootRefusal(bin, workspace, home);
4838
+ if (nested !== void 0) {
4839
+ io.stderr(nested);
4840
+ return 1;
4841
+ }
4761
4842
  const receipt = config.hostHarnesses?.[entry.id];
4762
4843
  if (receipt === void 0 && opts.force !== true) {
4763
4844
  io.stderr(
@@ -4774,7 +4855,7 @@ Removing it could delete a harness you installed yourself. Re-run with \`--force
4774
4855
  const remaining = { ...config.hostHarnesses ?? {} };
4775
4856
  delete remaining[entry.id];
4776
4857
  try {
4777
- writeHostConfig({ ...config, hostHarnesses: remaining }, env);
4858
+ writeHostConfig({ ...config, hostHarnesses: remaining }, env, home);
4778
4859
  } catch (err) {
4779
4860
  io.stderr(`${bin} harness: could not clear the install record: ${messageOf(err)}
4780
4861
  `);
@@ -4859,7 +4940,7 @@ async function runHarnessInstall(name, opts, io) {
4859
4940
  // src/commands/tool.ts
4860
4941
  import { existsSync as existsSync19 } from "node:fs";
4861
4942
  import { homedir as homedir3 } from "node:os";
4862
- 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";
4863
4944
 
4864
4945
  // src/lib/tools/catalog.ts
4865
4946
  var TOOLS_DOC = "docs/installation.md";
@@ -5145,7 +5226,7 @@ function rowOf(entry, installed, version, location) {
5145
5226
  function probeableHost2(env, home) {
5146
5227
  let root;
5147
5228
  try {
5148
- root = resolveHarnessRoot(void 0, env);
5229
+ root = resolveHarnessRoot(void 0, env, home);
5149
5230
  } catch {
5150
5231
  return void 0;
5151
5232
  }
@@ -5293,6 +5374,7 @@ ${hostCapableToolIds().map((t) => ` ${bin} tool install ${t} --host`).join("\n"
5293
5374
  async function installOnHost2(entry, opts, io, run, status) {
5294
5375
  const bin = opts.bin;
5295
5376
  const env = opts.env ?? process.env;
5377
+ const home = homeOf2(opts);
5296
5378
  const flagged = opts.host === true || opts.path !== void 0;
5297
5379
  if (!entry.hostCapable) {
5298
5380
  io.stderr(sandboxRefusal2(bin, status));
@@ -5326,8 +5408,8 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5326
5408
  let root;
5327
5409
  let recorded;
5328
5410
  try {
5329
- recorded = readHostConfig(env).harnessRoot;
5330
- root = resolveHarnessRoot(opts.path, env);
5411
+ recorded = readHostConfig(env, home).harnessRoot;
5412
+ root = resolveHarnessRoot(opts.path, env, home);
5331
5413
  } catch (err) {
5332
5414
  io.stderr(`${bin} tool: ${messageOf2(err)}
5333
5415
  `);
@@ -5345,7 +5427,7 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5345
5427
  }
5346
5428
  if (!sticky) {
5347
5429
  const chosen = (await ask2(`Harness root [${root}]:`)).trim();
5348
- if (chosen !== "") root = resolve13(chosen);
5430
+ if (chosen !== "") root = resolve12(chosen);
5349
5431
  }
5350
5432
  }
5351
5433
  if (sticky) io.stdout(`using the recorded harness root ${root}
@@ -5365,7 +5447,7 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5365
5447
  `);
5366
5448
  return 1;
5367
5449
  }
5368
- const prefix = hostPrefix2(homeOf2(opts));
5450
+ const prefix = hostPrefix2(home);
5369
5451
  const installEnv = { NPM_USER_PREFIX: prefix };
5370
5452
  const target = hostTargetFor2(root, prefix, run, env);
5371
5453
  if (await probeInstalled2(target, entry, void 0, installEnv) === true) {
@@ -5394,14 +5476,15 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5394
5476
  workspaceRoot: root
5395
5477
  };
5396
5478
  try {
5397
- const config = readHostConfig(env);
5479
+ const config = readHostConfig(env, home);
5398
5480
  writeHostConfig(
5399
5481
  {
5400
5482
  ...config,
5401
5483
  harnessRoot: root,
5402
5484
  hostTools: { ...config.hostTools ?? {}, [entry.id]: receipt }
5403
5485
  },
5404
- env
5486
+ env,
5487
+ home
5405
5488
  );
5406
5489
  } catch (err) {
5407
5490
  io.stderr(`${bin} tool: could not record the install: ${messageOf2(err)}
@@ -5502,12 +5585,13 @@ async function removeTool(entry, target, prefix, user, opts, io) {
5502
5585
  async function uninstallOnHost2(entry, opts, io, run) {
5503
5586
  const bin = opts.bin;
5504
5587
  const env = opts.env ?? process.env;
5505
- const computed = hostPrefix2(homeOf2(opts));
5588
+ const home = homeOf2(opts);
5589
+ const computed = hostPrefix2(home);
5506
5590
  let config;
5507
5591
  let workspace;
5508
5592
  try {
5509
- config = readHostConfig(env);
5510
- workspace = resolveHarnessRoot(void 0, env);
5593
+ config = readHostConfig(env, home);
5594
+ workspace = resolveHarnessRoot(void 0, env, home);
5511
5595
  } catch (err) {
5512
5596
  io.stderr(`${bin} tool: ${messageOf2(err)}
5513
5597
  `);
@@ -5529,7 +5613,7 @@ Removing it could delete a tool you installed yourself. Re-run with \`--force\`
5529
5613
  const remaining = { ...config.hostTools ?? {} };
5530
5614
  delete remaining[entry.id];
5531
5615
  try {
5532
- writeHostConfig({ ...config, hostTools: remaining }, env);
5616
+ writeHostConfig({ ...config, hostTools: remaining }, env, home);
5533
5617
  } catch (err) {
5534
5618
  io.stderr(`${bin} tool: could not clear the install record: ${messageOf2(err)}
5535
5619
  `);
@@ -5624,8 +5708,8 @@ function fetchRemoteSource(opts) {
5624
5708
  }
5625
5709
 
5626
5710
  // src/cli.ts
5627
- var VERSION = true ? "0.12.1" : "0.0.0-dev";
5628
- var DEFAULT_SOURCE_OH_DIR = resolve14(
5711
+ var VERSION = true ? "0.12.2" : "0.0.0-dev";
5712
+ var DEFAULT_SOURCE_OH_DIR = resolve13(
5629
5713
  dirname10(fileURLToPath(import.meta.url)),
5630
5714
  "../.."
5631
5715
  );
@@ -5862,10 +5946,11 @@ Usage:
5862
5946
 
5863
5947
  \`install\` and \`uninstall\` act on the running sandbox. When no sandbox is
5864
5948
  reachable they act on the host. A host install clones the AGRO workspace into the
5865
- harness root, then installs the harness into \`~/.local\`. \`--path <dir>\` chooses
5866
- the harness root; a successful install records it as \`harnessRoot\` in the host
5867
- \`${stateNames(bin).configFile}\`. The default is \`~/.agro\`, and the install prefix is
5868
- always \`~/.local\`.
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\`.
5869
5954
 
5870
5955
  On the host, \`uninstall\` removes only the harness \`install\` recorded, from the
5871
5956
  prefix in that record. Without a record it refuses, and \`--force\` overrides. In
@@ -5874,7 +5959,8 @@ the sandbox it needs no record. See \`docs/harnesses/overview.md\`.
5874
5959
  Flags:
5875
5960
  --json Machine-readable output (list/status)
5876
5961
  --host Install on the host when the sandbox is not running (install)
5877
- --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)
5878
5964
  --force Remove without a recorded host install (uninstall)
5879
5965
 
5880
5966
  Harnesses:
@@ -5930,8 +6016,8 @@ only on these tools:
5930
6016
  ${hostCapableToolIds().map((t) => ` ${t}`).join("\n")}
5931
6017
 
5932
6018
  A host install clones the AGRO workspace into the harness root \u2014 \`--path <dir>\`,
5933
- then \`harnessRoot\` in the host \`${stateNames(bin).configFile}\`, then \`~/.agro\` \u2014 and installs into
5934
- \`~/.local\`. It records the install as \`hostTools\` in that same file. On the host,
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,
5935
6021
  \`uninstall\` removes only what \`install\` recorded, from the prefix in that record.
5936
6022
  Without a record it refuses, and \`--force\` overrides.
5937
6023
 
@@ -6331,11 +6417,16 @@ function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
6331
6417
  }
6332
6418
  const positionals = [];
6333
6419
  let expectPath = false;
6420
+ let expectWorkspace = false;
6334
6421
  for (const token of rest) {
6335
6422
  if (expectPath) {
6336
6423
  expectPath = false;
6337
6424
  args.path = token;
6338
6425
  args.host = true;
6426
+ } else if (expectWorkspace) {
6427
+ expectWorkspace = false;
6428
+ args.workspace = token;
6429
+ args.host = true;
6339
6430
  } else if (token === "--json") {
6340
6431
  args.json = true;
6341
6432
  } else if (token === "--host") {
@@ -6347,6 +6438,11 @@ function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
6347
6438
  } else if (token.startsWith("--path=")) {
6348
6439
  args.path = token.slice("--path=".length);
6349
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;
6350
6446
  } else if (token.startsWith("-")) {
6351
6447
  return { ok: false, error: `${bin} harness: unknown flag "${token}"` };
6352
6448
  } else {
@@ -6356,6 +6452,15 @@ function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
6356
6452
  if (expectPath || args.path === "") {
6357
6453
  return { ok: false, error: `${bin} harness: --path requires a directory` };
6358
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
+ }
6359
6464
  const [sub, name, ...extra] = positionals;
6360
6465
  if (sub !== "list" && sub !== "install" && sub !== "status" && sub !== "uninstall") {
6361
6466
  return {
@@ -6374,7 +6479,10 @@ function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
6374
6479
  return { ok: false, error: `${bin} harness list: unexpected argument "${name}"` };
6375
6480
  }
6376
6481
  if (sub !== "install" && args.host) {
6377
- 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
+ };
6378
6486
  }
6379
6487
  if (sub !== "uninstall" && args.force) {
6380
6488
  return { ok: false, error: `${bin} harness ${sub}: --force applies to uninstall only` };
@@ -6449,13 +6557,13 @@ function bundledPayloadExists(bundled, exists = existsSync20) {
6449
6557
  function resolveUpdateSource(args, bundled, bin = LEGACY_PRODUCT.bin) {
6450
6558
  const exists = bundled.exists ?? existsSync20;
6451
6559
  if (args.fromDir !== void 0) {
6452
- return { kind: "local", fromDir: resolve14(args.fromDir) };
6560
+ return { kind: "local", fromDir: resolve13(args.fromDir) };
6453
6561
  }
6454
6562
  if (args.fromRemote) {
6455
6563
  return { kind: "remote", ref: args.ref };
6456
6564
  }
6457
6565
  if (bundledPayloadExists(bundled, exists)) {
6458
- return { kind: "local", fromDir: resolve14(bundled.sourceOhDir, "..") };
6566
+ return { kind: "local", fromDir: resolve13(bundled.sourceOhDir, "..") };
6459
6567
  }
6460
6568
  return {
6461
6569
  kind: "remote",
@@ -6747,7 +6855,12 @@ async function main(argv) {
6747
6855
  }
6748
6856
  return await runHarnessInstall(
6749
6857
  a.name,
6750
- { 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
+ },
6751
6864
  io
6752
6865
  );
6753
6866
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mifune/agro",
3
- "version": "0.12.1",
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",