@mifune/agro 0.9.0 → 0.10.0

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 (5) hide show
  1. package/NOTICE +2 -2
  2. package/README.md +16 -16
  3. package/dist/agro.js +1132 -474
  4. package/dist/oh.js +1132 -474
  5. package/package.json +5 -5
package/dist/agro.js CHANGED
@@ -2,20 +2,246 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { fileURLToPath } from "node:url";
5
- import { dirname as dirname6, join as join10, resolve as resolve8 } from "node:path";
6
- import { existsSync as existsSync16, mkdtempSync as mkdtempSync3, readFileSync as readFileSync11, rmSync as rmSync3 } from "node:fs";
5
+ import { dirname as dirname8, join as join11, resolve as resolve10 } from "node:path";
6
+ import { existsSync as existsSync17, mkdtempSync as mkdtempSync3, readFileSync as readFileSync12, rmSync as rmSync3 } from "node:fs";
7
7
  import { tmpdir as tmpdir3 } from "node:os";
8
8
 
9
9
  // src/commands/update.ts
10
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
11
+ import path3 from "node:path";
12
+
13
+ // src/lib/compat.ts
10
14
  import {
11
- existsSync as existsSync2,
12
- statSync,
13
- readFileSync as readFileSync2
15
+ existsSync,
16
+ lstatSync,
17
+ readdirSync,
18
+ readFileSync,
19
+ readlinkSync,
20
+ statSync
14
21
  } from "node:fs";
15
- import path3 from "node:path";
22
+ import { homedir } from "node:os";
23
+ import { join, resolve } from "node:path";
24
+ var GENERATIONS = {
25
+ legacy: {
26
+ controlDir: ".oh",
27
+ configFile: "oh.json",
28
+ envPrefix: "OH_",
29
+ userStateDir: ".oh",
30
+ seedDir: "/opt/oh-seed"
31
+ },
32
+ agro: {
33
+ controlDir: ".agro",
34
+ configFile: "agro.json",
35
+ envPrefix: "AGRO_",
36
+ userStateDir: ".agro",
37
+ seedDir: "/opt/agro-seed"
38
+ }
39
+ };
40
+ var LEGACY_CHECKOUT_DIR = ".openharness";
41
+ var DEFAULT_GENERATION = "agro";
42
+ var DEFAULT_SANDBOX_NAME = "agro";
43
+ var CompatConflictError = class extends Error {
44
+ legacyPath;
45
+ agroPath;
46
+ differences;
47
+ constructor(legacyPath, agroPath, differences) {
48
+ super(
49
+ `${legacyPath} and ${agroPath} both exist and differ \u2014 resolve the conflict before continuing (keep exactly one, or make them identical); differences: ${differences.join("; ")}`
50
+ );
51
+ this.name = "CompatConflictError";
52
+ this.legacyPath = legacyPath;
53
+ this.agroPath = agroPath;
54
+ this.differences = differences;
55
+ }
56
+ };
57
+ function entryType(stats) {
58
+ if (stats.isSymbolicLink()) return "symlink";
59
+ if (stats.isDirectory()) return "directory";
60
+ if (stats.isFile()) return "file";
61
+ return "other";
62
+ }
63
+ function lstatOrUndefined(path4) {
64
+ return lstatSync(path4, { throwIfNoEntry: false });
65
+ }
66
+ function statOrUndefined(path4) {
67
+ try {
68
+ return statSync(path4, { throwIfNoEntry: false });
69
+ } catch {
70
+ return void 0;
71
+ }
72
+ }
73
+ function sameBytes(a, b) {
74
+ const left = readFileSync(a);
75
+ const right = readFileSync(b);
76
+ return left.equals(right);
77
+ }
78
+ function octal(mode) {
79
+ return (mode & 511).toString(8).padStart(3, "0");
80
+ }
81
+ function contentDifference(type, legacy, agro, left, right) {
82
+ switch (type) {
83
+ case "symlink":
84
+ return readlinkSync(legacy) !== readlinkSync(agro) ? "symlink target differs" : void 0;
85
+ case "file":
86
+ return left.size !== right.size || !sameBytes(legacy, agro) ? "content differs" : void 0;
87
+ case "other":
88
+ return "unsupported entry type";
89
+ default:
90
+ return void 0;
91
+ }
92
+ }
93
+ function entryDifferences(label, legacy, agro, left, right) {
94
+ const type = entryType(left);
95
+ if (type !== entryType(right)) return [`${label}: type ${type} vs ${entryType(right)}`];
96
+ const differences = [];
97
+ if ((left.mode & 511) !== (right.mode & 511)) {
98
+ differences.push(`${label}: mode ${octal(left.mode)} vs ${octal(right.mode)}`);
99
+ }
100
+ const content = contentDifference(type, legacy, agro, left, right);
101
+ if (content !== void 0) differences.push(`${label}: ${content}`);
102
+ return differences;
103
+ }
104
+ function compareTrees(legacy, agro, rel = "") {
105
+ const label = rel === "" ? "." : rel;
106
+ const left = lstatOrUndefined(legacy);
107
+ const right = lstatOrUndefined(agro);
108
+ if (left === void 0 && right === void 0) return [];
109
+ if (left === void 0) return [`${label}: only in ${agro}`];
110
+ if (right === void 0) return [`${label}: only in ${legacy}`];
111
+ const differences = entryDifferences(label, legacy, agro, left, right);
112
+ if (!left.isDirectory() || !right.isDirectory()) return differences;
113
+ const names = /* @__PURE__ */ new Set([...readdirSync(legacy), ...readdirSync(agro)]);
114
+ for (const name of [...names].sort()) {
115
+ const childRel = rel === "" ? name : `${rel}/${name}`;
116
+ differences.push(...compareTrees(join(legacy, name), join(agro, name), childRel));
117
+ }
118
+ return differences;
119
+ }
120
+ function resolvePair(legacyPath, agroPath, present) {
121
+ const legacyExists = present(legacyPath);
122
+ const agroExists = present(agroPath);
123
+ const base = { legacyPath, agroPath };
124
+ if (!legacyExists && !agroExists) {
125
+ const path4 = DEFAULT_GENERATION === "agro" ? agroPath : legacyPath;
126
+ return { ...base, kind: "absent", generation: DEFAULT_GENERATION, path: path4 };
127
+ }
128
+ if (legacyExists && !agroExists) {
129
+ return { ...base, kind: "legacy-only", generation: "legacy", path: legacyPath };
130
+ }
131
+ if (!legacyExists && agroExists) {
132
+ return { ...base, kind: "agro-only", generation: "agro", path: agroPath };
133
+ }
134
+ const differences = compareTrees(legacyPath, agroPath);
135
+ if (differences.length > 0) throw new CompatConflictError(legacyPath, agroPath, differences);
136
+ return { ...base, kind: "both-equivalent", generation: "agro", path: agroPath };
137
+ }
138
+ function isDirectoryAt(path4) {
139
+ return statOrUndefined(path4)?.isDirectory() === true;
140
+ }
141
+ function isFileAt(path4) {
142
+ return statOrUndefined(path4)?.isFile() === true;
143
+ }
144
+ function resolveControlDir(root) {
145
+ const dir = resolve(root);
146
+ return resolvePair(
147
+ join(dir, GENERATIONS.legacy.controlDir),
148
+ join(dir, GENERATIONS.agro.controlDir),
149
+ isDirectoryAt
150
+ );
151
+ }
152
+ function resolveConfigFile(root) {
153
+ const dir = resolve(root);
154
+ return resolvePair(
155
+ join(dir, GENERATIONS.legacy.configFile),
156
+ join(dir, GENERATIONS.agro.configFile),
157
+ isFileAt
158
+ );
159
+ }
160
+ function resolveProjectLayout(root) {
161
+ const dir = resolve(root);
162
+ const control = resolveControlDir(dir);
163
+ const config = resolveConfigFile(dir);
164
+ const generation = control.kind !== "absent" ? control.generation : config.kind !== "absent" ? config.generation : DEFAULT_GENERATION;
165
+ const names = GENERATIONS[generation];
166
+ return {
167
+ generation,
168
+ root: dir,
169
+ controlDir: join(dir, names.controlDir),
170
+ configFile: join(dir, names.configFile)
171
+ };
172
+ }
173
+ function controlDirCandidates() {
174
+ return [GENERATIONS.agro.controlDir, GENERATIONS.legacy.controlDir];
175
+ }
176
+ function remoteControlDirScript(root, rel, args) {
177
+ const candidates = controlDirCandidates().join(" ");
178
+ const script = `root="$1"; shift; rel="$1"; shift; for name in ${candidates}; do if [ -d "$root/$name" ]; then exec bash "$root/$name/$rel" "$@"; fi; done; printf 'no control plane (%s) under %s\\n' '${candidates}' "$root" >&2; exit 1`;
179
+ return ["bash", "-c", script, "control-dir", root, rel, ...args];
180
+ }
181
+ function envValue(env, key) {
182
+ const value = env[key];
183
+ return value !== void 0 && value !== "" ? value : void 0;
184
+ }
185
+ function resolveAliasedEnv(env, suffix) {
186
+ const agroKey = `${GENERATIONS.agro.envPrefix}${suffix}`;
187
+ const legacyKey = `${GENERATIONS.legacy.envPrefix}${suffix}`;
188
+ const agro = envValue(env, agroKey);
189
+ const legacy = envValue(env, legacyKey);
190
+ const conflict = agro !== void 0 && legacy !== void 0 && agro !== legacy;
191
+ if (agro !== void 0) {
192
+ return { key: suffix, agroKey, legacyKey, value: agro, source: "agro", conflict };
193
+ }
194
+ if (legacy !== void 0) {
195
+ return { key: suffix, agroKey, legacyKey, value: legacy, source: "legacy", conflict: false };
196
+ }
197
+ return { key: suffix, agroKey, legacyKey, value: void 0, source: "none", conflict: false };
198
+ }
199
+ function aliasConflictWarning(resolved2) {
200
+ if (!resolved2.conflict) return void 0;
201
+ return `compat: ${resolved2.agroKey} and ${resolved2.legacyKey} are both set and differ \u2014 using ${resolved2.agroKey}`;
202
+ }
203
+ function aliasedEnvPair(suffix, value) {
204
+ return {
205
+ [`${GENERATIONS.agro.envPrefix}${suffix}`]: value,
206
+ [`${GENERATIONS.legacy.envPrefix}${suffix}`]: value
207
+ };
208
+ }
209
+ function aliasedEnvValue(env, suffix, warn = (message) => process.stderr.write(`${message}
210
+ `)) {
211
+ const resolved2 = resolveAliasedEnv(env, suffix);
212
+ const warning = aliasConflictWarning(resolved2);
213
+ if (warning !== void 0) warn(warning);
214
+ return resolved2.value;
215
+ }
216
+ var REGISTRY_SUBDIR = "sandboxes";
217
+ function discoverUserState(home = homedir()) {
218
+ const probe = (name) => {
219
+ const path4 = join(home, name);
220
+ return { path: path4, exists: isDirectoryAt(path4) };
221
+ };
222
+ return {
223
+ agroHome: probe(GENERATIONS.agro.userStateDir),
224
+ legacyHome: probe(GENERATIONS.legacy.userStateDir),
225
+ legacyCheckout: probe(LEGACY_CHECKOUT_DIR)
226
+ };
227
+ }
228
+ function resolveRegistryHome(env = process.env, home = homedir(), warn) {
229
+ const configured = aliasedEnvValue(env, "HOME", warn);
230
+ if (configured !== void 0) return { path: resolve(configured), configured: true };
231
+ return { path: home, configured: false };
232
+ }
233
+ function resolveUserStateHome(env = process.env, home = homedir(), warn) {
234
+ const configured = aliasedEnvValue(env, "HOME", warn);
235
+ if (configured !== void 0) return resolve(configured);
236
+ const discovered = discoverUserState(home);
237
+ const legacyRegistry = join(discovered.legacyHome.path, REGISTRY_SUBDIR);
238
+ const agroRegistry = join(discovered.agroHome.path, REGISTRY_SUBDIR);
239
+ const resolution = resolvePair(legacyRegistry, agroRegistry, isDirectoryAt);
240
+ return resolution.generation === "agro" ? discovered.agroHome.path : discovered.legacyHome.path;
241
+ }
16
242
 
17
243
  // src/lib/manifest.ts
18
- import { readFileSync } from "node:fs";
244
+ import { readFileSync as readFileSync2 } from "node:fs";
19
245
  import path from "node:path";
20
246
  var REGEX_SPECIAL = /* @__PURE__ */ new Set([".", "+", "?", "^", "$", "{", "}", "(", ")", "|", "[", "]", "\\"]);
21
247
  function globToRegExp(glob) {
@@ -66,7 +292,7 @@ function rootPayloadDirs(manifest) {
66
292
  function loadManifest(fromOh) {
67
293
  try {
68
294
  const manifestPath = path.resolve(fromOh, "manifest.json");
69
- const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
295
+ const parsed = JSON.parse(readFileSync2(manifestPath, "utf8"));
70
296
  if (!parsed || !Array.isArray(parsed.include) || parsed.include.length === 0) {
71
297
  return null;
72
298
  }
@@ -82,30 +308,30 @@ function loadManifest(fromOh) {
82
308
 
83
309
  // src/lib/vendor.ts
84
310
  import {
85
- existsSync,
86
- lstatSync,
311
+ existsSync as existsSync2,
312
+ lstatSync as lstatSync2,
87
313
  mkdirSync,
88
- readdirSync,
314
+ readdirSync as readdirSync2,
89
315
  copyFileSync
90
316
  } from "node:fs";
91
317
  import path2 from "node:path";
92
- function assertDestInTarget(dest, targetOh, sep4) {
93
- if (dest === targetOh || dest.startsWith(targetOh + sep4)) {
318
+ function assertDestInTarget(dest, targetOh, sep5) {
319
+ if (dest === targetOh || dest.startsWith(targetOh + sep5)) {
94
320
  return;
95
321
  }
96
322
  throw new Error("oh: refusing to write outside target .oh: " + dest);
97
323
  }
98
- function assertDestInRoot(dest, targetRoot, sep4) {
99
- if (dest === targetRoot || dest.startsWith(targetRoot + sep4)) {
324
+ function assertDestInRoot(dest, targetRoot, sep5) {
325
+ if (dest === targetRoot || dest.startsWith(targetRoot + sep5)) {
100
326
  return;
101
327
  }
102
328
  throw new Error("oh: refusing to write outside target root: " + dest);
103
329
  }
104
330
  function walkFiles(root, dir, acc) {
105
- const entries = readdirSync(dir, { withFileTypes: true });
331
+ const entries = readdirSync2(dir, { withFileTypes: true });
106
332
  for (const entry of entries) {
107
333
  const abs = path2.resolve(dir, entry.name);
108
- const st = lstatSync(abs);
334
+ const st = lstatSync2(abs);
109
335
  if (st.isSymbolicLink()) {
110
336
  continue;
111
337
  }
@@ -142,7 +368,7 @@ function copyOhPayload(fromOh, targetOh, manifest, opts, report) {
142
368
  }
143
369
  const dest = path2.join(targetOh, rel);
144
370
  assertDestInTarget(dest, targetOh, path2.sep);
145
- const exists = existsSync(dest);
371
+ const exists = existsSync2(dest);
146
372
  if (exists && skipExisting && !force) {
147
373
  report?.("skip-exists", rel);
148
374
  skipped++;
@@ -168,7 +394,7 @@ function copyRootPayload(fromRoot, targetRoot, manifest, opts, report) {
168
394
  const relpaths = [];
169
395
  for (const dir of rootPayloadDirs(manifest)) {
170
396
  const abs = path2.join(fromRoot, dir);
171
- if (!existsSync(abs) || !lstatSync(abs).isDirectory()) {
397
+ if (!existsSync2(abs) || !lstatSync2(abs).isDirectory()) {
172
398
  continue;
173
399
  }
174
400
  walkFiles(fromRoot, abs, relpaths);
@@ -190,7 +416,7 @@ function copyRootPayload(fromRoot, targetRoot, manifest, opts, report) {
190
416
  }
191
417
  const dest = path2.join(targetRoot, rel);
192
418
  assertDestInRoot(dest, targetRoot, path2.sep);
193
- const exists = existsSync(dest);
419
+ const exists = existsSync2(dest);
194
420
  if (exists && skipExisting && !force) {
195
421
  report?.("skip-exists", rel);
196
422
  skipped++;
@@ -232,7 +458,7 @@ function compareVersions(a, b) {
232
458
  function readCliVersion(ohDir) {
233
459
  const pkgPath = path3.resolve(ohDir, "cli", "package.json");
234
460
  try {
235
- const parsed = JSON.parse(readFileSync2(pkgPath, "utf8"));
461
+ const parsed = JSON.parse(readFileSync3(pkgPath, "utf8"));
236
462
  if (parsed && typeof parsed.version === "string") {
237
463
  return parsed.version;
238
464
  }
@@ -243,23 +469,27 @@ function readCliVersion(ohDir) {
243
469
  async function runUpdate(opts, io) {
244
470
  const { targetDir, fromDir, force, dryRun } = opts;
245
471
  const dryPrefix = dryRun ? "[dry-run] " : "";
246
- const fromOh = path3.resolve(fromDir, ".oh");
247
- if (!existsSync2(fromOh) || !statSync(fromOh).isDirectory()) {
472
+ const source = resolveControlDir(fromDir);
473
+ if (source.kind === "absent") {
248
474
  io.stderr(
249
- "oh update: update source not found at " + fromOh + ". Pass --from <built-OpenHarness-checkout> or --from-remote [--ref <ref>].\n"
475
+ "oh update: update source not found at " + source.agroPath + " or " + source.legacyPath + ". Pass --from <built-AGRO-checkout> or --from-remote [--ref <ref>].\n"
250
476
  );
251
477
  return 1;
252
478
  }
253
- const targetOh = path3.resolve(targetDir, ".oh");
479
+ const fromOh = source.path;
480
+ const controlName = path3.basename(fromOh);
481
+ const target = resolveControlDir(targetDir);
482
+ const targetOh = target.kind === "absent" ? path3.resolve(targetDir, controlName) : target.path;
483
+ const targetName = path3.basename(targetOh);
254
484
  if (fromOh === targetOh) {
255
- io.stderr("oh update: source and target are the same .oh; nothing to update.\n");
485
+ io.stderr("oh update: source and target are the same " + targetName + "; nothing to update.\n");
256
486
  return 1;
257
487
  }
258
488
  const available = readCliVersion(fromOh);
259
489
  const current = readCliVersion(targetOh);
260
490
  const cmp = compareVersions(available, current);
261
491
  if (cmp > 0) {
262
- io.stdout(dryPrefix + "updating .oh: " + current + " -> " + available + "\n");
492
+ io.stdout(dryPrefix + "updating " + targetName + ": " + current + " -> " + available + "\n");
263
493
  } else if (cmp === 0) {
264
494
  if (!force) {
265
495
  io.stdout(dryPrefix + "oh update: already up to date (v" + current + ")\n");
@@ -274,13 +504,13 @@ async function runUpdate(opts, io) {
274
504
  return 1;
275
505
  }
276
506
  io.stdout(
277
- dryPrefix + "oh update: downgrading .oh: " + current + " -> " + available + " (--force)\n"
507
+ dryPrefix + "oh update: downgrading " + targetName + ": " + current + " -> " + available + " (--force)\n"
278
508
  );
279
509
  }
280
510
  const manifest = loadManifest(fromOh);
281
511
  if (manifest === null) {
282
512
  io.stdout(
283
- dryPrefix + "oh update: no .oh/manifest.json in source; overlaying all of .oh/ (legacy mode)\n"
513
+ dryPrefix + "oh update: no " + controlName + "/manifest.json in source; overlaying all of " + controlName + "/ (legacy mode)\n"
284
514
  );
285
515
  }
286
516
  let created = 0;
@@ -311,245 +541,52 @@ async function runUpdate(opts, io) {
311
541
  path3.resolve(fromDir),
312
542
  path3.resolve(targetDir),
313
543
  manifest,
314
- { force, dryRun },
315
- report
316
- );
317
- for (const dir of rootPayloadDirs(manifest ?? { include: [], exclude: [] })) {
318
- const stranded = path3.resolve(targetDir, ".oh", dir);
319
- if (existsSync2(stranded)) {
320
- io.stdout(
321
- dryPrefix + "oh update: " + path3.join(".oh", dir) + " still exists from a pre-move install and is no longer read; move any local edits into " + dir + "/ and delete it\n"
322
- );
323
- }
324
- }
325
- io.stdout(
326
- dryPrefix + "oh update: " + created + " created, " + overwritten + " overwritten, " + (skipped + rootResult.skipped) + " skipped\n"
327
- );
328
- return 0;
329
- }
330
-
331
- // src/commands/self-upgrade.ts
332
- import { spawnSync } from "node:child_process";
333
- import {
334
- accessSync,
335
- constants,
336
- readFileSync as readFileSync4,
337
- realpathSync,
338
- renameSync,
339
- statSync as statSync3,
340
- unlinkSync,
341
- writeFileSync
342
- } from "node:fs";
343
- import { basename as basename2, delimiter, dirname, extname, join as join2 } from "node:path";
344
-
345
- // src/lib/compat.ts
346
- import {
347
- existsSync as existsSync3,
348
- lstatSync as lstatSync2,
349
- readdirSync as readdirSync2,
350
- readFileSync as readFileSync3,
351
- readlinkSync,
352
- statSync as statSync2
353
- } from "node:fs";
354
- import { homedir } from "node:os";
355
- import { join, resolve } from "node:path";
356
- var GENERATIONS = {
357
- legacy: {
358
- controlDir: ".oh",
359
- configFile: "oh.json",
360
- envPrefix: "OH_",
361
- userStateDir: ".oh",
362
- seedDir: "/opt/oh-seed"
363
- },
364
- agro: {
365
- controlDir: ".agro",
366
- configFile: "agro.json",
367
- envPrefix: "AGRO_",
368
- userStateDir: ".agro",
369
- seedDir: "/opt/agro-seed"
370
- }
371
- };
372
- var LEGACY_CHECKOUT_DIR = ".openharness";
373
- var CompatConflictError = class extends Error {
374
- legacyPath;
375
- agroPath;
376
- differences;
377
- constructor(legacyPath, agroPath, differences) {
378
- super(
379
- `${legacyPath} and ${agroPath} both exist and differ \u2014 resolve the conflict before continuing (keep exactly one, or make them identical); differences: ${differences.join("; ")}`
380
- );
381
- this.name = "CompatConflictError";
382
- this.legacyPath = legacyPath;
383
- this.agroPath = agroPath;
384
- this.differences = differences;
385
- }
386
- };
387
- function entryType(stats) {
388
- if (stats.isSymbolicLink()) return "symlink";
389
- if (stats.isDirectory()) return "directory";
390
- if (stats.isFile()) return "file";
391
- return "other";
392
- }
393
- function lstatOrUndefined(path4) {
394
- return lstatSync2(path4, { throwIfNoEntry: false });
395
- }
396
- function statOrUndefined(path4) {
397
- try {
398
- return statSync2(path4, { throwIfNoEntry: false });
399
- } catch {
400
- return void 0;
401
- }
402
- }
403
- function sameBytes(a, b) {
404
- const left = readFileSync3(a);
405
- const right = readFileSync3(b);
406
- return left.equals(right);
407
- }
408
- function octal(mode) {
409
- return (mode & 511).toString(8).padStart(3, "0");
410
- }
411
- function contentDifference(type, legacy, agro, left, right) {
412
- switch (type) {
413
- case "symlink":
414
- return readlinkSync(legacy) !== readlinkSync(agro) ? "symlink target differs" : void 0;
415
- case "file":
416
- return left.size !== right.size || !sameBytes(legacy, agro) ? "content differs" : void 0;
417
- case "other":
418
- return "unsupported entry type";
419
- default:
420
- return void 0;
421
- }
422
- }
423
- function entryDifferences(label, legacy, agro, left, right) {
424
- const type = entryType(left);
425
- if (type !== entryType(right)) return [`${label}: type ${type} vs ${entryType(right)}`];
426
- const differences = [];
427
- if ((left.mode & 511) !== (right.mode & 511)) {
428
- differences.push(`${label}: mode ${octal(left.mode)} vs ${octal(right.mode)}`);
429
- }
430
- const content = contentDifference(type, legacy, agro, left, right);
431
- if (content !== void 0) differences.push(`${label}: ${content}`);
432
- return differences;
433
- }
434
- function compareTrees(legacy, agro, rel = "") {
435
- const label = rel === "" ? "." : rel;
436
- const left = lstatOrUndefined(legacy);
437
- const right = lstatOrUndefined(agro);
438
- if (left === void 0 && right === void 0) return [];
439
- if (left === void 0) return [`${label}: only in ${agro}`];
440
- if (right === void 0) return [`${label}: only in ${legacy}`];
441
- const differences = entryDifferences(label, legacy, agro, left, right);
442
- if (!left.isDirectory() || !right.isDirectory()) return differences;
443
- const names = /* @__PURE__ */ new Set([...readdirSync2(legacy), ...readdirSync2(agro)]);
444
- for (const name of [...names].sort()) {
445
- const childRel = rel === "" ? name : `${rel}/${name}`;
446
- differences.push(...compareTrees(join(legacy, name), join(agro, name), childRel));
447
- }
448
- return differences;
449
- }
450
- function resolvePair(legacyPath, agroPath, present) {
451
- const legacyExists = present(legacyPath);
452
- const agroExists = present(agroPath);
453
- const base = { legacyPath, agroPath };
454
- if (!legacyExists && !agroExists) {
455
- return { ...base, kind: "absent", generation: void 0, path: void 0 };
456
- }
457
- if (legacyExists && !agroExists) {
458
- return { ...base, kind: "legacy-only", generation: "legacy", path: legacyPath };
459
- }
460
- if (!legacyExists && agroExists) {
461
- return { ...base, kind: "agro-only", generation: "agro", path: agroPath };
462
- }
463
- const differences = compareTrees(legacyPath, agroPath);
464
- if (differences.length > 0) throw new CompatConflictError(legacyPath, agroPath, differences);
465
- return { ...base, kind: "both-equivalent", generation: "agro", path: agroPath };
466
- }
467
- function isDirectoryAt(path4) {
468
- return statOrUndefined(path4)?.isDirectory() === true;
469
- }
470
- function isFileAt(path4) {
471
- return statOrUndefined(path4)?.isFile() === true;
472
- }
473
- function resolveControlDir(root) {
474
- const dir = resolve(root);
475
- return resolvePair(
476
- join(dir, GENERATIONS.legacy.controlDir),
477
- join(dir, GENERATIONS.agro.controlDir),
478
- isDirectoryAt
479
- );
480
- }
481
- function resolveConfigFile(root) {
482
- const dir = resolve(root);
483
- return resolvePair(
484
- join(dir, GENERATIONS.legacy.configFile),
485
- join(dir, GENERATIONS.agro.configFile),
486
- isFileAt
544
+ { force, dryRun },
545
+ report
487
546
  );
488
- }
489
- function envValue(env, key) {
490
- const value = env[key];
491
- return value !== void 0 && value !== "" ? value : void 0;
492
- }
493
- function resolveAliasedEnv(env, suffix) {
494
- const agroKey = `${GENERATIONS.agro.envPrefix}${suffix}`;
495
- const legacyKey = `${GENERATIONS.legacy.envPrefix}${suffix}`;
496
- const agro = envValue(env, agroKey);
497
- const legacy = envValue(env, legacyKey);
498
- const conflict = agro !== void 0 && legacy !== void 0 && agro !== legacy;
499
- if (agro !== void 0) {
500
- return { key: suffix, agroKey, legacyKey, value: agro, source: "agro", conflict };
501
- }
502
- if (legacy !== void 0) {
503
- return { key: suffix, agroKey, legacyKey, value: legacy, source: "legacy", conflict: false };
547
+ for (const dir of rootPayloadDirs(manifest ?? { include: [], exclude: [] })) {
548
+ const stranded = path3.join(targetOh, dir);
549
+ if (existsSync3(stranded)) {
550
+ io.stdout(
551
+ dryPrefix + "oh update: " + path3.join(targetName, dir) + " still exists from a pre-move install and is no longer read; move any local edits into " + dir + "/ and delete it\n"
552
+ );
553
+ }
504
554
  }
505
- return { key: suffix, agroKey, legacyKey, value: void 0, source: "none", conflict: false };
506
- }
507
- function aliasConflictWarning(resolved) {
508
- if (!resolved.conflict) return void 0;
509
- return `compat: ${resolved.agroKey} and ${resolved.legacyKey} are both set and differ \u2014 using ${resolved.agroKey}`;
510
- }
511
- function aliasedEnvValue(env, suffix, warn = (message) => process.stderr.write(`${message}
512
- `)) {
513
- const resolved = resolveAliasedEnv(env, suffix);
514
- const warning = aliasConflictWarning(resolved);
515
- if (warning !== void 0) warn(warning);
516
- return resolved.value;
517
- }
518
- var REGISTRY_SUBDIR = "sandboxes";
519
- function discoverUserState(home = homedir()) {
520
- const probe = (name) => {
521
- const path4 = join(home, name);
522
- return { path: path4, exists: isDirectoryAt(path4) };
523
- };
524
- return {
525
- agroHome: probe(GENERATIONS.agro.userStateDir),
526
- legacyHome: probe(GENERATIONS.legacy.userStateDir),
527
- legacyCheckout: probe(LEGACY_CHECKOUT_DIR)
528
- };
529
- }
530
- function resolveUserStateHome(env = process.env, home = homedir(), warn) {
531
- const configured = aliasedEnvValue(env, "HOME", warn);
532
- if (configured !== void 0) return resolve(configured);
533
- const discovered = discoverUserState(home);
534
- const legacyRegistry = join(discovered.legacyHome.path, REGISTRY_SUBDIR);
535
- const agroRegistry = join(discovered.agroHome.path, REGISTRY_SUBDIR);
536
- const resolution = resolvePair(legacyRegistry, agroRegistry, isDirectoryAt);
537
- return resolution.generation === "agro" ? discovered.agroHome.path : discovered.legacyHome.path;
555
+ io.stdout(
556
+ dryPrefix + "oh update: " + created + " created, " + overwritten + " overwritten, " + (skipped + rootResult.skipped) + " skipped\n"
557
+ );
558
+ return 0;
538
559
  }
539
560
 
561
+ // src/commands/self-upgrade.ts
562
+ import { spawnSync } from "node:child_process";
563
+ import {
564
+ accessSync,
565
+ constants,
566
+ readFileSync as readFileSync4,
567
+ realpathSync,
568
+ renameSync,
569
+ statSync as statSync2,
570
+ unlinkSync,
571
+ writeFileSync
572
+ } from "node:fs";
573
+ import { basename as basename2, delimiter, dirname, extname, join as join2 } from "node:path";
574
+
540
575
  // src/lib/product.ts
541
576
  import { basename } from "node:path";
542
577
  var AGRO_PRODUCT = {
543
578
  name: "agro",
544
579
  bin: "agro",
545
580
  title: "AGRO CLI",
546
- packageName: "@mifune/agro"
581
+ packageName: "@mifune/agro",
582
+ generation: "agro"
547
583
  };
548
584
  var LEGACY_PRODUCT = {
549
585
  name: "oh",
550
586
  bin: "oh",
551
587
  title: "Open Harness CLI",
552
- packageName: "@mifune/openharness"
588
+ packageName: "@mifune/openharness",
589
+ generation: "legacy"
553
590
  };
554
591
  var FILE_EXTENSION = /\.[^.]*$/;
555
592
  function invokedName(argv1) {
@@ -559,9 +596,15 @@ function invokedName(argv1) {
559
596
  function resolveProduct(argv1) {
560
597
  return invokedName(argv1) === LEGACY_PRODUCT.bin ? LEGACY_PRODUCT : AGRO_PRODUCT;
561
598
  }
599
+ function productFor(bin) {
600
+ return bin === LEGACY_PRODUCT.bin ? LEGACY_PRODUCT : AGRO_PRODUCT;
601
+ }
602
+ function stateNames(bin) {
603
+ return GENERATIONS[productFor(bin).generation];
604
+ }
562
605
 
563
606
  // src/commands/self-upgrade.ts
564
- var DEFAULT_ARTIFACT_URL = "https://github.com/mifunedev/openharness/releases/latest/download/agro.js";
607
+ var DEFAULT_ARTIFACT_URL = "https://github.com/mifunedev/agro/releases/latest/download/agro.js";
565
608
  var PREFIX = `${AGRO_PRODUCT.bin} update`;
566
609
  var PACKAGE = AGRO_PRODUCT.packageName;
567
610
  var IMAGE_ROOT = "/opt/oh/";
@@ -609,9 +652,9 @@ function kindOfTarget(target, deps) {
609
652
  function classifyInstallation(argv1, deps) {
610
653
  const invoked = argv1 ?? "";
611
654
  if (invoked === "") return { kind: "unknown", target: "", invoked, reason: "no executable path (argv[1] is empty)" };
612
- const resolved = resolveTarget(invoked, deps);
613
- if (resolved.reason !== void 0) return { kind: "unknown", target: resolved.target, invoked, reason: resolved.reason };
614
- return { ...kindOfTarget(resolved.target, deps), target: resolved.target, invoked };
655
+ const resolved2 = resolveTarget(invoked, deps);
656
+ if (resolved2.reason !== void 0) return { kind: "unknown", target: resolved2.target, invoked, reason: resolved2.reason };
657
+ return { ...kindOfTarget(resolved2.target, deps), target: resolved2.target, invoked };
615
658
  }
616
659
  function parseSemver(input) {
617
660
  const m = SEMVER.exec(input.trim());
@@ -667,7 +710,7 @@ function refuseUnsupported(installation) {
667
710
  `${at} \u2014 the sandbox image ships this CLI; pull a newer image on the host (oh/agro stop, then agro sandbox install docker --name <name>)`
668
711
  );
669
712
  case "source":
670
- return new Error(`${at} \u2014 rebuild from the checkout: npm --prefix .oh/cli run build`);
713
+ return new Error(`${at} \u2014 rebuild from the checkout: npm --prefix .agro/cli run build`);
671
714
  case "legacy-package":
672
715
  return new Error(`${at} \u2014 install the canonical package: npm install -g ${PACKAGE}`);
673
716
  default:
@@ -679,13 +722,13 @@ function refuseUnsupported(installation) {
679
722
  function assertOnlyAgroOnPath(installation, deps) {
680
723
  const found = deps.which(AGRO_PRODUCT.bin);
681
724
  if (found === void 0) return;
682
- let resolved;
725
+ let resolved2;
683
726
  try {
684
- resolved = deps.realpath(found);
727
+ resolved2 = deps.realpath(found);
685
728
  } catch {
686
- resolved = found;
729
+ resolved2 = found;
687
730
  }
688
- if (resolved !== installation.target) {
731
+ if (resolved2 !== installation.target) {
689
732
  throw new Error(
690
733
  `another ${AGRO_PRODUCT.bin} is earlier on PATH (${found}); upgrading ${installation.target} would not change what runs \u2014 remove or reorder it first`
691
734
  );
@@ -863,14 +906,14 @@ async function runSelfUpgrade(opts, deps, io) {
863
906
  }
864
907
  function whichOnPath(name, env, platform) {
865
908
  const win32 = platform === "win32";
866
- const sep4 = win32 ? ";" : delimiter;
867
- const dirs = (env.PATH ?? "").split(sep4).filter((d) => d !== "");
909
+ const sep5 = win32 ? ";" : delimiter;
910
+ const dirs = (env.PATH ?? "").split(sep5).filter((d) => d !== "");
868
911
  const exts = win32 ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")] : [""];
869
912
  for (const dir of dirs) {
870
913
  for (const ext of exts) {
871
914
  const candidate = join2(dir, `${name}${ext}`);
872
915
  try {
873
- if (statSync3(candidate).isFile()) {
916
+ if (statSync2(candidate).isFile()) {
874
917
  if (!win32) accessSync(candidate, constants.X_OK);
875
918
  return candidate;
876
919
  }
@@ -897,7 +940,7 @@ function defaultDeps(currentVersion) {
897
940
  return {
898
941
  env: process.env,
899
942
  realpath: (path4) => realpathSync(path4),
900
- stat: (path4) => statSync3(path4),
943
+ stat: (path4) => statSync2(path4),
901
944
  access: (path4, mode) => accessSync(path4, mode),
902
945
  readFile: (path4) => readFileSync4(path4),
903
946
  writeFile: (path4, data, mode) => writeFileSync(path4, data, { mode }),
@@ -912,10 +955,560 @@ function defaultDeps(currentVersion) {
912
955
  };
913
956
  }
914
957
 
958
+ // src/commands/migrate.ts
959
+ import { lstatSync as lstatSync4 } from "node:fs";
960
+ import { dirname as dirname3, join as join3, resolve as resolve3 } from "node:path";
961
+
962
+ // src/lib/migrate.ts
963
+ import {
964
+ closeSync,
965
+ existsSync as existsSync4,
966
+ fsyncSync,
967
+ lstatSync as lstatSync3,
968
+ mkdirSync as mkdirSync2,
969
+ openSync,
970
+ readFileSync as readFileSync5,
971
+ readlinkSync as readlinkSync2,
972
+ realpathSync as realpathSync2,
973
+ renameSync as renameSync2,
974
+ symlinkSync,
975
+ unlinkSync as unlinkSync2,
976
+ writeFileSync as writeFileSync2
977
+ } from "node:fs";
978
+ import { dirname as dirname2, isAbsolute, relative, resolve as resolve2, sep } from "node:path";
979
+ var MIGRATION_PLAN_VERSION = 1;
980
+ var RETIRED_SUFFIX = ".migrated";
981
+ var LOCK_FILE = ".agro-migrate.lock";
982
+ function snapshot(path4) {
983
+ const stats = lstatSync3(path4, { throwIfNoEntry: false });
984
+ if (stats === void 0) return void 0;
985
+ return {
986
+ type: stats.isSymbolicLink() ? "symlink" : stats.isDirectory() ? "directory" : "file",
987
+ mode: stats.mode & 511,
988
+ ino: stats.ino,
989
+ mtimeMs: stats.mtimeMs,
990
+ size: stats.size
991
+ };
992
+ }
993
+ function presentAs(path4, kind) {
994
+ let stats;
995
+ try {
996
+ stats = lstatSync3(path4, { throwIfNoEntry: false });
997
+ } catch {
998
+ return false;
999
+ }
1000
+ if (stats === void 0) return false;
1001
+ if (stats.isSymbolicLink()) return true;
1002
+ return kind === "dir" ? stats.isDirectory() : stats.isFile();
1003
+ }
1004
+ function nearestExistingAncestor(path4) {
1005
+ let cursor = path4;
1006
+ while (!existsSync4(cursor)) {
1007
+ const parent = dirname2(cursor);
1008
+ if (parent === cursor) return cursor;
1009
+ cursor = parent;
1010
+ }
1011
+ return cursor;
1012
+ }
1013
+ function insideRoot(root, candidate) {
1014
+ const realRoot = realpathSync2(root);
1015
+ const anchor = nearestExistingAncestor(dirname2(candidate));
1016
+ const realAnchor = realpathSync2(anchor);
1017
+ const rel = relative(realRoot, realAnchor);
1018
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
1019
+ }
1020
+ function assertInsideRoot(root, path4, label) {
1021
+ if (!isAbsolute(path4)) throw new Error(`migration ${label} must be absolute: ${path4}`);
1022
+ const absolute = resolve2(path4);
1023
+ if (!insideRoot(root, absolute)) {
1024
+ throw new Error(`refusing migration ${label} outside ${root}: ${absolute}`);
1025
+ }
1026
+ return absolute;
1027
+ }
1028
+ function sameSnapshot(a, b) {
1029
+ return b !== void 0 && a.type === b.type && a.mode === b.mode && a.ino === b.ino && a.mtimeMs === b.mtimeMs && a.size === b.size;
1030
+ }
1031
+ function planPair(root, pair) {
1032
+ const legacy = assertInsideRoot(root, pair.legacy, "source");
1033
+ const agro = assertInsideRoot(root, pair.agro, "destination");
1034
+ const legacyPresent = presentAs(legacy, pair.kind);
1035
+ const agroPresent = presentAs(agro, pair.kind);
1036
+ if (!legacyPresent) {
1037
+ return { kind: "noop", path: agro, reason: agroPresent ? "already migrated" : "absent in both generations" };
1038
+ }
1039
+ if (!agroPresent) {
1040
+ if (lstatSync3(agro, { throwIfNoEntry: false }) !== void 0) {
1041
+ return { path: agro, reason: `destination exists but is not a ${pair.kind}` };
1042
+ }
1043
+ return {
1044
+ kind: "rename",
1045
+ from: legacy,
1046
+ to: agro,
1047
+ snapshot: snapshot(legacy),
1048
+ createsParent: !existsSync4(dirname2(agro))
1049
+ };
1050
+ }
1051
+ const differences = compareTrees(legacy, agro);
1052
+ if (differences.length > 0) {
1053
+ return { path: agro, reason: "both generations exist and differ", differences };
1054
+ }
1055
+ const retired = `${legacy}${RETIRED_SUFFIX}`;
1056
+ if (lstatSync3(retired, { throwIfNoEntry: false }) !== void 0) {
1057
+ return { path: retired, reason: "retired copy already exists" };
1058
+ }
1059
+ return { kind: "retire", from: legacy, to: retired, snapshot: snapshot(legacy) };
1060
+ }
1061
+ function planRewrite(root, rewrite) {
1062
+ const path4 = assertInsideRoot(root, rewrite.path, "rewrite target");
1063
+ const current = snapshot(path4);
1064
+ if (current === void 0 || current.type !== "file") {
1065
+ return { kind: "noop", path: path4, reason: "rewrite target is not a regular file" };
1066
+ }
1067
+ const content = readFileSync5(path4, "utf8");
1068
+ const changed = rewrite.replacements.some((r) => r.from !== "" && content.includes(r.from));
1069
+ if (!changed) return { kind: "noop", path: path4, reason: "rewrite already applied" };
1070
+ return { kind: "rewrite", path: path4, replacements: rewrite.replacements, snapshot: current };
1071
+ }
1072
+ function planRelink(root, relink) {
1073
+ const path4 = assertInsideRoot(root, relink.path, "relink target");
1074
+ const current = snapshot(path4);
1075
+ if (current === void 0) return { kind: "noop", path: path4, reason: "link absent" };
1076
+ if (current.type !== "symlink") return { kind: "noop", path: path4, reason: "not a symlink" };
1077
+ const target = readlinkSync2(path4);
1078
+ if (target === relink.to) return { kind: "noop", path: path4, reason: "link already points at the AGRO target" };
1079
+ if (target !== relink.from) return { kind: "noop", path: path4, reason: `link points at ${target}, not ${relink.from}` };
1080
+ return { kind: "relink", path: path4, from: relink.from, to: relink.to, snapshot: current };
1081
+ }
1082
+ function resolved(path4) {
1083
+ try {
1084
+ return realpathSync2(path4);
1085
+ } catch {
1086
+ return void 0;
1087
+ }
1088
+ }
1089
+ function resolvesToPack(path4, packs) {
1090
+ const target = resolved(path4);
1091
+ if (target === void 0) return false;
1092
+ return packs.some((pack) => resolved(pack) === target);
1093
+ }
1094
+ function replacementSurvives(retire) {
1095
+ const current = snapshot(retire.replacement);
1096
+ if (current === void 0 || current.type !== "symlink") return false;
1097
+ if (!retire.replacementTargets.includes(readlinkSync2(retire.replacement))) return false;
1098
+ return resolvesToPack(retire.replacement, retire.packs);
1099
+ }
1100
+ function preserveRetiredLink(root, retire, reason) {
1101
+ const step = planRelink(root, { path: retire.path, ...retire.preserve });
1102
+ return step.kind === "relink" ? step : { kind: "noop", path: resolve2(retire.path), reason };
1103
+ }
1104
+ function planRetireLink(root, retire) {
1105
+ const path4 = assertInsideRoot(root, retire.path, "retired link");
1106
+ const current = snapshot(path4);
1107
+ if (current === void 0) return { kind: "noop", path: path4, reason: "link absent" };
1108
+ if (current.type !== "symlink") return { kind: "noop", path: path4, reason: "not a symlink" };
1109
+ if (!resolvesToPack(path4, retire.packs)) {
1110
+ return preserveRetiredLink(root, retire, "link does not resolve to the Open Harness skill pack");
1111
+ }
1112
+ if (!replacementSurvives(retire)) {
1113
+ return preserveRetiredLink(
1114
+ root,
1115
+ retire,
1116
+ `${retire.replacement} is not an independent skill-pack link that survives the migration`
1117
+ );
1118
+ }
1119
+ const retired = `${path4}${RETIRED_SUFFIX}`;
1120
+ if (lstatSync3(retired, { throwIfNoEntry: false }) !== void 0) {
1121
+ return { path: retired, reason: "retired copy already exists" };
1122
+ }
1123
+ return { kind: "retire", from: path4, to: retired, snapshot: current };
1124
+ }
1125
+ function isStep(entry) {
1126
+ return "kind" in entry;
1127
+ }
1128
+ function planMigration(spec) {
1129
+ const root = resolve2(spec.root);
1130
+ if (!existsSync4(root)) throw new Error(`migration root does not exist: ${root}`);
1131
+ const steps = [];
1132
+ const conflicts = [];
1133
+ for (const pair of spec.pairs) {
1134
+ const planned = planPair(root, pair);
1135
+ if (isStep(planned)) steps.push(planned);
1136
+ else conflicts.push(planned);
1137
+ }
1138
+ for (const retired of spec.retireLinks ?? []) {
1139
+ const planned = planRetireLink(root, retired);
1140
+ if (isStep(planned)) steps.push(planned);
1141
+ else conflicts.push(planned);
1142
+ }
1143
+ for (const rewrite of spec.rewrites ?? []) steps.push(planRewrite(root, rewrite));
1144
+ for (const relink of spec.relinks ?? []) steps.push(planRelink(root, relink));
1145
+ const mutating = steps.some((step) => step.kind !== "noop");
1146
+ const status = conflicts.length > 0 ? "conflict" : mutating ? "ready" : "noop";
1147
+ return { version: MIGRATION_PLAN_VERSION, root, status, steps, conflicts };
1148
+ }
1149
+ function acquireLock(root) {
1150
+ const lock = resolve2(root, LOCK_FILE);
1151
+ let fd;
1152
+ try {
1153
+ fd = openSync(lock, "wx", 384);
1154
+ } catch (error) {
1155
+ const code = error.code;
1156
+ if (code === "EEXIST") return void 0;
1157
+ throw error;
1158
+ }
1159
+ writeFileSync2(fd, `${process.pid}
1160
+ `);
1161
+ closeSync(fd);
1162
+ return () => {
1163
+ try {
1164
+ unlinkSync2(lock);
1165
+ } catch {
1166
+ }
1167
+ };
1168
+ }
1169
+ function revalidateInPlace(step) {
1170
+ if (!sameSnapshot(step.snapshot, snapshot(step.path))) return `${step.path} changed since the plan was made`;
1171
+ if (step.kind === "relink" && readlinkSync2(step.path) !== step.from) {
1172
+ return `${step.path} changed since the plan was made`;
1173
+ }
1174
+ return void 0;
1175
+ }
1176
+ function revalidate(step) {
1177
+ if (step.kind === "noop") return void 0;
1178
+ if (step.kind === "rewrite" || step.kind === "relink") return revalidateInPlace(step);
1179
+ if (!sameSnapshot(step.snapshot, snapshot(step.from))) {
1180
+ return `${step.from} changed since the plan was made`;
1181
+ }
1182
+ if (lstatSync3(step.to, { throwIfNoEntry: false }) !== void 0) {
1183
+ return `${step.to} appeared since the plan was made`;
1184
+ }
1185
+ return void 0;
1186
+ }
1187
+ function applyRewrite(step) {
1188
+ let content = readFileSync5(step.path, "utf8");
1189
+ for (const { from, to } of step.replacements) {
1190
+ if (from === "") continue;
1191
+ content = content.split(from).join(to);
1192
+ }
1193
+ const tmp = `${step.path}.tmp.${process.pid}`;
1194
+ const fd = openSync(tmp, "w", step.snapshot.mode);
1195
+ try {
1196
+ writeFileSync2(fd, content, "utf8");
1197
+ fsyncSync(fd);
1198
+ } finally {
1199
+ closeSync(fd);
1200
+ }
1201
+ renameSync2(tmp, step.path);
1202
+ }
1203
+ function applyRelink(step) {
1204
+ const tmp = `${step.path}.tmp.${process.pid}`;
1205
+ symlinkSync(step.to, tmp);
1206
+ try {
1207
+ renameSync2(tmp, step.path);
1208
+ } catch (error) {
1209
+ unlinkSync2(tmp);
1210
+ throw error;
1211
+ }
1212
+ }
1213
+ function pendingResults(plan) {
1214
+ return plan.steps.map((step) => ({ ...step, outcome: step.kind === "noop" ? "skipped" : "pending" }));
1215
+ }
1216
+ function refusal(plan) {
1217
+ if (plan.version !== MIGRATION_PLAN_VERSION) return `unsupported plan version ${plan.version}`;
1218
+ if (plan.status === "conflict") {
1219
+ return plan.conflicts.map((c) => `${c.path}: ${c.reason}`).join("; ");
1220
+ }
1221
+ return void 0;
1222
+ }
1223
+ function revalidatePlan(root, plan) {
1224
+ for (const step of plan.steps) {
1225
+ const stale = revalidate(step);
1226
+ if (stale !== void 0) return stale;
1227
+ if (step.kind === "rename" || step.kind === "retire") {
1228
+ assertInsideRoot(root, step.from, "source");
1229
+ assertInsideRoot(root, step.to, "destination");
1230
+ }
1231
+ }
1232
+ return void 0;
1233
+ }
1234
+ function applyStep(step) {
1235
+ if (step.kind === "noop") return;
1236
+ if (step.kind === "rewrite") {
1237
+ applyRewrite(step);
1238
+ return;
1239
+ }
1240
+ if (step.kind === "relink") {
1241
+ applyRelink(step);
1242
+ return;
1243
+ }
1244
+ if (step.kind === "rename" && step.createsParent) mkdirSync2(dirname2(step.to), { recursive: true });
1245
+ renameSync2(step.from, step.to);
1246
+ }
1247
+ function applySteps(root, plan) {
1248
+ const results = pendingResults(plan);
1249
+ for (let i = 0; i < plan.steps.length; i += 1) {
1250
+ const step = plan.steps[i];
1251
+ if (step.kind === "noop") continue;
1252
+ try {
1253
+ applyStep(step);
1254
+ results[i] = { ...step, outcome: "done" };
1255
+ } catch (error) {
1256
+ results[i] = { ...step, outcome: "failed", error: error instanceof Error ? error.message : String(error) };
1257
+ return {
1258
+ status: "failed",
1259
+ root,
1260
+ reason: `step ${i + 1} failed; completed steps are recoverable by reversing them in order`,
1261
+ steps: results
1262
+ };
1263
+ }
1264
+ }
1265
+ return { status: "applied", root, steps: results };
1266
+ }
1267
+ function applyMigration(plan) {
1268
+ const root = resolve2(plan.root);
1269
+ const refused = refusal(plan);
1270
+ if (refused !== void 0) return { status: "refused", root, reason: refused, steps: pendingResults(plan) };
1271
+ if (plan.status === "noop") return { status: "noop", root, steps: pendingResults(plan) };
1272
+ const release = acquireLock(root);
1273
+ if (release === void 0) {
1274
+ return {
1275
+ status: "refused",
1276
+ root,
1277
+ reason: `another migration holds ${resolve2(root, LOCK_FILE)}`,
1278
+ steps: pendingResults(plan)
1279
+ };
1280
+ }
1281
+ try {
1282
+ const stale = revalidatePlan(root, plan);
1283
+ if (stale !== void 0) return { status: "refused", root, reason: stale, steps: pendingResults(plan) };
1284
+ return applySteps(root, plan);
1285
+ } finally {
1286
+ release();
1287
+ }
1288
+ }
1289
+ var PROVIDER_LINKS = [
1290
+ { link: ".claude/skills", target: "skills" },
1291
+ { link: ".claude/hooks", target: "hooks" },
1292
+ { link: ".agents/skills", target: "skills" }
1293
+ ];
1294
+ var CANONICAL_SKILL_PACKS = [".oh/skills", ".agro/skills"];
1295
+ var SURVIVING_SKILL_LINK_TARGETS = ["../.oh/skills", "../.agro/skills"];
1296
+ var RETIRED_PROVIDER_LINKS = [
1297
+ {
1298
+ link: ".pi/skills",
1299
+ replacement: ".agents/skills",
1300
+ preserve: { from: "../.oh/skills", to: "../.agro/skills" }
1301
+ },
1302
+ {
1303
+ link: ".codex/skills",
1304
+ replacement: ".agents/skills",
1305
+ preserve: { from: "../.oh/skills", to: "../.agro/skills" }
1306
+ }
1307
+ ];
1308
+ function providerRelinks(root) {
1309
+ const dir = resolve2(root);
1310
+ return PROVIDER_LINKS.map(({ link, target }) => ({
1311
+ path: resolve2(dir, ...link.split("/")),
1312
+ from: `../.oh/${target}`,
1313
+ to: `../.agro/${target}`
1314
+ }));
1315
+ }
1316
+ function retiredProviderLinks(root) {
1317
+ const dir = resolve2(root);
1318
+ const packs = CANONICAL_SKILL_PACKS.map((pack) => resolve2(dir, ...pack.split("/")));
1319
+ return RETIRED_PROVIDER_LINKS.map(({ link, replacement, preserve }) => ({
1320
+ path: resolve2(dir, ...link.split("/")),
1321
+ packs,
1322
+ replacement: resolve2(dir, ...replacement.split("/")),
1323
+ replacementTargets: [...SURVIVING_SKILL_LINK_TARGETS],
1324
+ preserve
1325
+ }));
1326
+ }
1327
+ function projectMigrationSpec(root) {
1328
+ const dir = resolve2(root);
1329
+ return {
1330
+ root: dir,
1331
+ pairs: [
1332
+ { legacy: `${dir}${sep}.oh`, agro: `${dir}${sep}.agro`, kind: "dir" },
1333
+ { legacy: `${dir}${sep}oh.json`, agro: `${dir}${sep}agro.json`, kind: "file" }
1334
+ ],
1335
+ retireLinks: retiredProviderLinks(dir),
1336
+ relinks: providerRelinks(dir)
1337
+ };
1338
+ }
1339
+ function userStateMigrationSpec(home) {
1340
+ const dir = resolve2(home);
1341
+ return {
1342
+ root: dir,
1343
+ pairs: [
1344
+ {
1345
+ legacy: `${dir}${sep}.oh${sep}sandboxes`,
1346
+ agro: `${dir}${sep}.agro${sep}sandboxes`,
1347
+ kind: "dir"
1348
+ }
1349
+ ]
1350
+ };
1351
+ }
1352
+
1353
+ // src/commands/migrate.ts
1354
+ var ROOT_MARKERS = [".oh", ".agro", "oh.json", "agro.json"];
1355
+ var HELP_FLAGS = ["-h", "--help", "help"];
1356
+ function defaultMigrateDeps() {
1357
+ const registry = resolveRegistryHome();
1358
+ return {
1359
+ cwd: () => process.cwd(),
1360
+ home: () => registry.path,
1361
+ homeConfigured: () => registry.configured
1362
+ };
1363
+ }
1364
+ function parseMigrateArgs(argv, bin) {
1365
+ const args = { bin, help: false, check: false, home: false, json: false };
1366
+ for (const arg of argv) {
1367
+ if (HELP_FLAGS.includes(arg)) return { ok: true, args: { ...args, help: true } };
1368
+ if (arg === "--check") args.check = true;
1369
+ else if (arg === "--home") args.home = true;
1370
+ else if (arg === "--json") args.json = true;
1371
+ else return { ok: false, error: `${bin} migrate: unexpected argument "${arg}"`, showHelp: true };
1372
+ }
1373
+ return { ok: true, args };
1374
+ }
1375
+ function migrateHelpText(bin) {
1376
+ return `${bin} migrate \u2014 Move a legacy .oh/ project or ~/.oh registry to AGRO names
1377
+
1378
+ Usage:
1379
+ ${bin} migrate [--check] [--home] [--json]
1380
+
1381
+ Project mode (default) finds the nearest ancestor of the current directory that
1382
+ holds .oh/, .agro/, oh.json or agro.json and renames .oh/ -> .agro/ and
1383
+ oh.json -> agro.json wholesale. Provider links (.claude/skills, .claude/hooks,
1384
+ .agents/skills) that point at ../.oh/... are re-pointed at ../.agro/....
1385
+ Retired links (.pi/skills, .codex/skills) move to <path>.migrated only when each
1386
+ resolves to the Open Harness skill pack and .agents/skills independently links
1387
+ to that pack. Otherwise each retired link stays in place, re-pointed at
1388
+ ../.agro/skills when it named the legacy pack. Other links stay unchanged.
1389
+
1390
+ Flags:
1391
+ --check Print the plan and change nothing.
1392
+ --home Migrate the sandbox registry ~/.oh/sandboxes -> ~/.agro/sandboxes instead.
1393
+ --json Emit the plan (--check) or {plan, result} as JSON on stdout.
1394
+
1395
+ Byte-identical legacy and AGRO copies retire the legacy one to <name>.migrated;
1396
+ divergent copies are refused and nothing is merged or deleted. ~/.openharness,
1397
+ .env and .git are never touched.
1398
+
1399
+ Exit codes: 0 applied or nothing to do, 2 refused (conflict or lock), 1 failure.
1400
+ `;
1401
+ }
1402
+ function printMigrateHelp(bin) {
1403
+ process.stdout.write(migrateHelpText(bin));
1404
+ }
1405
+ function hasMarker(dir) {
1406
+ return ROOT_MARKERS.some((marker) => lstatSync4(join3(dir, marker), { throwIfNoEntry: false }) !== void 0);
1407
+ }
1408
+ function findProjectRoot(cwd) {
1409
+ const start = resolve3(cwd);
1410
+ let cursor = start;
1411
+ while (true) {
1412
+ if (hasMarker(cursor)) return cursor;
1413
+ const parent = dirname3(cursor);
1414
+ if (parent === cursor) return start;
1415
+ cursor = parent;
1416
+ }
1417
+ }
1418
+ function specFor(args, deps) {
1419
+ return args.home ? userStateMigrationSpec(deps.home()) : projectMigrationSpec(findProjectRoot(deps.cwd()));
1420
+ }
1421
+ function describeStep(step) {
1422
+ switch (step.kind) {
1423
+ case "rename":
1424
+ return `rename ${step.from} -> ${step.to}`;
1425
+ case "retire":
1426
+ return `retire ${step.from} -> ${step.to}`;
1427
+ case "rewrite":
1428
+ return `rewrite ${step.path} (${step.replacements.length} replacement${step.replacements.length === 1 ? "" : "s"})`;
1429
+ case "relink":
1430
+ return `relink ${step.path}: ${step.from} -> ${step.to}`;
1431
+ case "noop":
1432
+ return `noop ${step.path} (${step.reason})`;
1433
+ }
1434
+ }
1435
+ function describeConflict(conflict) {
1436
+ const lines = [` ${conflict.path}: ${conflict.reason}`];
1437
+ for (const difference of conflict.differences ?? []) lines.push(` - ${difference}`);
1438
+ return lines;
1439
+ }
1440
+ function planSummary(bin, plan) {
1441
+ const lines = [`${bin} migrate: plan for ${plan.root}`];
1442
+ for (const step of plan.steps) lines.push(` ${describeStep(step)}`);
1443
+ if (plan.conflicts.length > 0) {
1444
+ lines.push("conflicts:");
1445
+ for (const conflict of plan.conflicts) lines.push(...describeConflict(conflict));
1446
+ }
1447
+ lines.push(`status: ${plan.status}`);
1448
+ return `${lines.join("\n")}
1449
+ `;
1450
+ }
1451
+ function describeOutcome(step) {
1452
+ const suffix = step.error === void 0 ? "" : ` \u2014 ${step.error}`;
1453
+ return ` ${step.outcome.padEnd(7)} ${describeStep(step)}${suffix}`;
1454
+ }
1455
+ function resultSummary(bin, result) {
1456
+ const lines = [`${bin} migrate: ${result.status}${result.reason === void 0 ? "" : ` \u2014 ${result.reason}`}`];
1457
+ for (const step of result.steps) lines.push(describeOutcome(step));
1458
+ return `${lines.join("\n")}
1459
+ `;
1460
+ }
1461
+ function planExitCode(plan) {
1462
+ return plan.status === "conflict" ? 2 : 0;
1463
+ }
1464
+ function resultExitCode(result) {
1465
+ if (result.status === "applied" || result.status === "noop") return 0;
1466
+ return result.status === "refused" ? 2 : 1;
1467
+ }
1468
+ function emitJson(io, value) {
1469
+ io.stdout(`${JSON.stringify(value, null, 2)}
1470
+ `);
1471
+ }
1472
+ function check(args, plan, io) {
1473
+ if (args.json) emitJson(io, plan);
1474
+ else io.stdout(planSummary(args.bin, plan));
1475
+ return planExitCode(plan);
1476
+ }
1477
+ function apply(args, plan, io) {
1478
+ const result = applyMigration(plan);
1479
+ if (args.json) {
1480
+ emitJson(io, { plan, result });
1481
+ } else {
1482
+ io.stdout(planSummary(args.bin, plan));
1483
+ io.stdout(resultSummary(args.bin, result));
1484
+ }
1485
+ return resultExitCode(result);
1486
+ }
1487
+ function runMigrate(args, io, deps = defaultMigrateDeps()) {
1488
+ try {
1489
+ if (args.home && deps.homeConfigured?.() === true) {
1490
+ io.stdout(
1491
+ `${args.bin} migrate: the registry home is set explicitly to ${deps.home()}, so it holds no .oh or .agro directory to rename
1492
+ `
1493
+ );
1494
+ io.stdout(`${args.bin} migrate: noop
1495
+ `);
1496
+ return 0;
1497
+ }
1498
+ const plan = planMigration(specFor(args, deps));
1499
+ return args.check ? check(args, plan, io) : apply(args, plan, io);
1500
+ } catch (error) {
1501
+ const message = error instanceof Error ? error.message : String(error);
1502
+ io.stderr(`${args.bin} migrate: ${message}
1503
+ `);
1504
+ return 1;
1505
+ }
1506
+ }
1507
+
915
1508
  // src/commands/cloud.ts
916
1509
  import { readFile } from "node:fs/promises";
917
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "node:fs";
918
- import { join as join3 } from "node:path";
1510
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "node:fs";
1511
+ import { join as join4 } from "node:path";
919
1512
 
920
1513
  // src/lib/prompt.ts
921
1514
  import { createInterface } from "node:readline";
@@ -944,8 +1537,8 @@ function redact(token) {
944
1537
  async function ask(question) {
945
1538
  const rl = createInterface({ input: process.stdin, output: process.stdout });
946
1539
  try {
947
- return await new Promise((resolve9) => {
948
- rl.question(` ${question} `, (answer) => resolve9(answer.trim()));
1540
+ return await new Promise((resolve11) => {
1541
+ rl.question(` ${question} `, (answer) => resolve11(answer.trim()));
949
1542
  });
950
1543
  } finally {
951
1544
  rl.close();
@@ -957,7 +1550,7 @@ async function askSecret(question) {
957
1550
  process.stdin.setRawMode(true);
958
1551
  process.stdin.resume();
959
1552
  const bytes = [];
960
- return await new Promise((resolve9) => {
1553
+ return await new Promise((resolve11) => {
961
1554
  const cleanup = () => {
962
1555
  try {
963
1556
  process.stdin.setRawMode(false);
@@ -975,7 +1568,7 @@ async function askSecret(question) {
975
1568
  } else if (byte === 13 || byte === 10) {
976
1569
  cleanup();
977
1570
  process.stdout.write("\n");
978
- resolve9(Buffer.from(bytes).toString("utf8").trim());
1571
+ resolve11(Buffer.from(bytes).toString("utf8").trim());
979
1572
  return;
980
1573
  } else if (byte === 127 || byte === 8) {
981
1574
  if (bytes.length > 0) {
@@ -1025,10 +1618,10 @@ async function askSecretPiped(question) {
1025
1618
  };
1026
1619
  process.on("SIGINT", onSigint);
1027
1620
  try {
1028
- return await new Promise((resolve9) => {
1621
+ return await new Promise((resolve11) => {
1029
1622
  rl.question(` ${question} `, (answer) => {
1030
1623
  process.stdout.write("\n");
1031
- resolve9(answer.trim());
1624
+ resolve11(answer.trim());
1032
1625
  });
1033
1626
  });
1034
1627
  } finally {
@@ -1044,14 +1637,14 @@ async function confirm(question, defaultYes = true) {
1044
1637
  }
1045
1638
 
1046
1639
  // src/lib/oh-config.ts
1047
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
1048
- import { basename as basename3, dirname as dirname2, resolve as resolve3 } from "node:path";
1640
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "node:fs";
1641
+ import { basename as basename3, dirname as dirname4, resolve as resolve5 } from "node:path";
1049
1642
 
1050
1643
  // src/lib/env-file.ts
1051
- import { existsSync as existsSync4 } from "node:fs";
1052
- import { resolve as resolve2, sep } from "node:path";
1644
+ import { existsSync as existsSync5 } from "node:fs";
1645
+ import { resolve as resolve4, sep as sep2 } from "node:path";
1053
1646
  function assertInRoot(dest, root) {
1054
- if (!(dest === root || dest.startsWith(root + sep))) {
1647
+ if (!(dest === root || dest.startsWith(root + sep2))) {
1055
1648
  throw new Error(`refusing to write outside the project root: ${dest}`);
1056
1649
  }
1057
1650
  }
@@ -1075,12 +1668,12 @@ function setEnvValue(root, key, value) {
1075
1668
  }
1076
1669
  function setConfigField(root, path4, value) {
1077
1670
  const file = ohConfigPath(root);
1078
- assertInRoot(file, resolve2(root));
1671
+ assertInRoot(file, resolve4(root));
1079
1672
  const config = readOhConfig(file);
1080
1673
  const before = getOhConfigValue(config, path4);
1081
1674
  const next = setOhConfigValue(config, path4, value);
1082
1675
  const after = getOhConfigValue(next, path4);
1083
- if (existsSync4(file) && JSON.stringify(before) === JSON.stringify(after)) {
1676
+ if (existsSync5(file) && JSON.stringify(before) === JSON.stringify(after)) {
1084
1677
  return "already-set";
1085
1678
  }
1086
1679
  writeOhConfig(root, next);
@@ -1147,8 +1740,7 @@ var LANGFUSE_PRIVACY_PRESETS = [
1147
1740
  ];
1148
1741
  var SANDBOX_RUNTIMES = ["docker"];
1149
1742
  function ohConfigPath(root) {
1150
- const resolved = resolveConfigFile(root);
1151
- return resolved.generation === "agro" ? resolve3(root, GENERATIONS.agro.configFile) : resolve3(root, OH_CONFIG_FILE);
1743
+ return resolveProjectLayout(root).configFile;
1152
1744
  }
1153
1745
  function defaultOhConfig(name) {
1154
1746
  return {
@@ -1173,10 +1765,10 @@ function defaultOhConfig(name) {
1173
1765
  };
1174
1766
  }
1175
1767
  function readOhConfig(path4) {
1176
- if (!existsSync5(path4)) return defaultOhConfig(basename3(dirname2(resolve3(path4))));
1768
+ if (!existsSync6(path4)) return defaultOhConfig(basename3(dirname4(resolve5(path4))));
1177
1769
  let raw;
1178
1770
  try {
1179
- raw = readFileSync5(path4, "utf8");
1771
+ raw = readFileSync6(path4, "utf8");
1180
1772
  } catch (error) {
1181
1773
  const detail = error instanceof Error ? error.message : String(error);
1182
1774
  throw new Error(`could not read ${path4}: ${detail}`);
@@ -1185,24 +1777,24 @@ function readOhConfig(path4) {
1185
1777
  try {
1186
1778
  parsed = JSON.parse(raw);
1187
1779
  } catch {
1188
- throw new Error(`${OH_CONFIG_FILE} is not valid JSON: ${path4}`);
1780
+ throw new Error(`${basename3(path4)} is not valid JSON: ${path4}`);
1189
1781
  }
1190
1782
  return validateOhConfig(parsed);
1191
1783
  }
1192
1784
  function writeOhConfig(root, config) {
1193
1785
  const path4 = ohConfigPath(root);
1194
- assertInRoot(path4, resolve3(root));
1786
+ assertInRoot(path4, resolve5(root));
1195
1787
  const validated = validateOhConfig({ ...config, version: 1 });
1196
1788
  const body = `${JSON.stringify(validated, null, 2)}
1197
1789
  `;
1198
1790
  const tmp = `${path4}.tmp.${process.pid}`;
1199
- mkdirSync2(dirname2(path4), { recursive: true });
1791
+ mkdirSync3(dirname4(path4), { recursive: true });
1200
1792
  try {
1201
- writeFileSync2(tmp, body, { mode: OH_CONFIG_MODE, encoding: "utf8" });
1202
- renameSync2(tmp, path4);
1793
+ writeFileSync3(tmp, body, { mode: OH_CONFIG_MODE, encoding: "utf8" });
1794
+ renameSync3(tmp, path4);
1203
1795
  } catch (error) {
1204
1796
  try {
1205
- unlinkSync2(tmp);
1797
+ unlinkSync3(tmp);
1206
1798
  } catch {
1207
1799
  }
1208
1800
  throw error;
@@ -1402,12 +1994,12 @@ function coerceFieldValue(field, raw) {
1402
1994
  }
1403
1995
 
1404
1996
  // src/lib/project.ts
1405
- import { dirname as dirname3, resolve as resolve4 } from "node:path";
1997
+ import { dirname as dirname5, resolve as resolve6 } from "node:path";
1406
1998
  function resolveProjectRoot(startDir = process.cwd()) {
1407
- let dir = resolve4(startDir);
1999
+ let dir = resolve6(startDir);
1408
2000
  for (; ; ) {
1409
2001
  if (resolveControlDir(dir).kind !== "absent") return dir;
1410
- const parent = dirname3(dir);
2002
+ const parent = dirname5(dir);
1411
2003
  if (parent === dir) {
1412
2004
  throw new Error("not an OpenHarness-equipped repo \u2014 run `oh update` first");
1413
2005
  }
@@ -1416,23 +2008,23 @@ function resolveProjectRoot(startDir = process.cwd()) {
1416
2008
  }
1417
2009
 
1418
2010
  // src/lib/secrets.ts
1419
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
1420
- import { resolve as resolve5 } from "node:path";
2011
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "node:fs";
2012
+ import { resolve as resolve7 } from "node:path";
1421
2013
 
1422
2014
  // src/lib/env.ts
1423
2015
  import {
1424
- readFileSync as readFileSync6,
1425
- writeFileSync as writeFileSync3,
1426
- existsSync as existsSync6,
1427
- fsyncSync,
1428
- renameSync as renameSync3,
1429
- unlinkSync as unlinkSync3,
1430
- openSync,
1431
- closeSync
2016
+ readFileSync as readFileSync7,
2017
+ writeFileSync as writeFileSync4,
2018
+ existsSync as existsSync7,
2019
+ fsyncSync as fsyncSync2,
2020
+ renameSync as renameSync4,
2021
+ unlinkSync as unlinkSync4,
2022
+ openSync as openSync2,
2023
+ closeSync as closeSync2
1432
2024
  } from "node:fs";
1433
2025
  function loadEnvInto(envPath, env) {
1434
- if (!existsSync6(envPath)) return;
1435
- const content = readFileSync6(envPath, "utf8");
2026
+ if (!existsSync7(envPath)) return;
2027
+ const content = readFileSync7(envPath, "utf8");
1436
2028
  for (const rawLine of content.split("\n")) {
1437
2029
  const line = rawLine.trimEnd();
1438
2030
  if (line === "" || /^\s*#/.test(line)) continue;
@@ -1448,21 +2040,21 @@ function writeEnvFile(envPath, content) {
1448
2040
  const tmpPath = `${envPath}.tmp.${process.pid}`;
1449
2041
  let fd;
1450
2042
  try {
1451
- fd = openSync(tmpPath, "w", 384);
1452
- writeFileSync3(tmpPath, content, "utf8");
1453
- fsyncSync(fd);
1454
- closeSync(fd);
2043
+ fd = openSync2(tmpPath, "w", 384);
2044
+ writeFileSync4(tmpPath, content, "utf8");
2045
+ fsyncSync2(fd);
2046
+ closeSync2(fd);
1455
2047
  fd = void 0;
1456
- renameSync3(tmpPath, envPath);
2048
+ renameSync4(tmpPath, envPath);
1457
2049
  } catch (err) {
1458
2050
  if (fd !== void 0) {
1459
2051
  try {
1460
- closeSync(fd);
2052
+ closeSync2(fd);
1461
2053
  } catch {
1462
2054
  }
1463
2055
  }
1464
2056
  try {
1465
- unlinkSync3(tmpPath);
2057
+ unlinkSync4(tmpPath);
1466
2058
  } catch {
1467
2059
  }
1468
2060
  throw err;
@@ -1484,7 +2076,7 @@ var SECRET_KEYS = [
1484
2076
  ];
1485
2077
  var ALLOWED = new Set(SECRET_KEYS);
1486
2078
  function secretsFilePath(root) {
1487
- return resolve5(root, SECRETS_FILE);
2079
+ return resolve7(root, SECRETS_FILE);
1488
2080
  }
1489
2081
  function isSecretKey(key) {
1490
2082
  return ALLOWED.has(key);
@@ -1498,7 +2090,7 @@ function assertSecretKey(key) {
1498
2090
  function readSecret(root, key) {
1499
2091
  assertSecretKey(key);
1500
2092
  const file = secretsFilePath(root);
1501
- if (!existsSync7(file)) return void 0;
2093
+ if (!existsSync8(file)) return void 0;
1502
2094
  const env = {};
1503
2095
  loadEnvInto(file, env);
1504
2096
  const value = env[key]?.trim();
@@ -1510,14 +2102,14 @@ function setSecret(root, key, value) {
1510
2102
  throw new Error(`${key} value must not contain a newline`);
1511
2103
  }
1512
2104
  const file = secretsFilePath(root);
1513
- assertInRoot(file, resolve5(root));
1514
- const original = existsSync7(file) ? readFileSync7(file, "utf8") : "";
2105
+ assertInRoot(file, resolve7(root));
2106
+ const original = existsSync8(file) ? readFileSync8(file, "utf8") : "";
1515
2107
  const { content } = setKeyInEnv(original, key, value);
1516
2108
  writeEnvFile(file, content);
1517
2109
  }
1518
2110
  function listSecretKeys(root) {
1519
2111
  const file = secretsFilePath(root);
1520
- if (!existsSync7(file)) return [];
2112
+ if (!existsSync8(file)) return [];
1521
2113
  const env = {};
1522
2114
  loadEnvInto(file, env);
1523
2115
  return SECRET_KEYS.filter((key) => {
@@ -1827,12 +2419,12 @@ function requireRoot(io) {
1827
2419
  function legacyCloudConfigPath(env) {
1828
2420
  const home = env.HOME ?? env.USERPROFILE;
1829
2421
  if (!home) return void 0;
1830
- return join3(home, ".config", "openharness", "cloud.json");
2422
+ return join4(home, ".config", "openharness", "cloud.json");
1831
2423
  }
1832
2424
  function readLegacyCloudConfig(path4) {
1833
2425
  let parsed;
1834
2426
  try {
1835
- parsed = JSON.parse(readFileSync8(path4, "utf8"));
2427
+ parsed = JSON.parse(readFileSync9(path4, "utf8"));
1836
2428
  } catch {
1837
2429
  return void 0;
1838
2430
  }
@@ -1845,7 +2437,7 @@ function readLegacyCloudConfig(path4) {
1845
2437
  }
1846
2438
  function migrateLegacyCloudConfig(root, io, env) {
1847
2439
  const path4 = legacyCloudConfigPath(env);
1848
- if (!path4 || !existsSync8(path4)) return;
2440
+ if (!path4 || !existsSync9(path4)) return;
1849
2441
  const config = readOhConfig(ohConfigPath(root));
1850
2442
  if (config.cloud?.apiUrl !== void 0) return;
1851
2443
  if (readSecret(root, PROVISION_KEY_SECRET) !== void 0) return;
@@ -1863,7 +2455,7 @@ function migrateLegacyCloudConfig(root, io, env) {
1863
2455
  `);
1864
2456
  if (legacy.apiUrl) io.stdout(` cloud.apiUrl -> ${ohConfigPath(root)}
1865
2457
  `);
1866
- if (legacy.provisionKey) io.stdout(` ${PROVISION_KEY_SECRET} -> ${join3(root, ".env")}
2458
+ if (legacy.provisionKey) io.stdout(` ${PROVISION_KEY_SECRET} -> ${join4(root, ".env")}
1867
2459
  `);
1868
2460
  io.stdout(`${path4} is no longer read; delete it when you no longer need the copy.
1869
2461
  `);
@@ -1890,7 +2482,7 @@ async function configureCloud(args, io, env) {
1890
2482
  assertNoExtraArgs(args);
1891
2483
  io.stdout(`Config: ${ohConfigPath(root)}
1892
2484
  `);
1893
- io.stdout(`Secrets: ${join3(root, ".env")}
2485
+ io.stdout(`Secrets: ${join4(root, ".env")}
1894
2486
  `);
1895
2487
  io.stdout(`API URL: ${current.apiUrl ?? DEFAULT_API_URL}
1896
2488
  `);
@@ -1912,7 +2504,7 @@ async function configureCloud(args, io, env) {
1912
2504
  setSecret(root, PROVISION_KEY_SECRET, provisionKey);
1913
2505
  io.stdout(`Saved cloud.apiUrl to ${ohConfigPath(root)}
1914
2506
  `);
1915
- io.stdout(`Saved ${PROVISION_KEY_SECRET} to ${join3(root, ".env")}
2507
+ io.stdout(`Saved ${PROVISION_KEY_SECRET} to ${join4(root, ".env")}
1916
2508
  `);
1917
2509
  io.stdout("Provision credential: configured\n");
1918
2510
  }
@@ -1950,7 +2542,7 @@ async function executeCloud(argv, io) {
1950
2542
  const result = await dispatch(args, {
1951
2543
  request,
1952
2544
  readFile: io.readFile ?? readFile,
1953
- sleep: io.sleep ?? ((milliseconds) => new Promise((resolve9) => setTimeout(resolve9, milliseconds))),
2545
+ sleep: io.sleep ?? ((milliseconds) => new Promise((resolve11) => setTimeout(resolve11, milliseconds))),
1954
2546
  write
1955
2547
  });
1956
2548
  if (result === void 0) return;
@@ -1979,8 +2571,8 @@ async function runCloud(argv, io) {
1979
2571
 
1980
2572
  // src/lib/execution/runner.ts
1981
2573
  import { spawnSync as spawnSync2 } from "node:child_process";
1982
- import { existsSync as existsSync9 } from "node:fs";
1983
- import { join as join4 } from "node:path";
2574
+ import { existsSync as existsSync10 } from "node:fs";
2575
+ import { basename as basename4, join as join5 } from "node:path";
1984
2576
  var spawnRunner = (cmd, args, opts) => {
1985
2577
  const common = {
1986
2578
  env: opts.env,
@@ -2018,32 +2610,33 @@ function assertSpawned(r, what) {
2018
2610
  }
2019
2611
  }
2020
2612
  function requireLifecycleScript(root, rel) {
2021
- const script = join4(root, ".oh", "scripts", rel);
2022
- if (!existsSync9(script)) {
2613
+ const { controlDir } = resolveProjectLayout(root);
2614
+ const script = join5(controlDir, "scripts", rel);
2615
+ if (!existsSync10(script)) {
2023
2616
  throw new Error(
2024
- `missing lifecycle script ${script} \u2014 the vendored .oh/ payload looks incomplete; run \`oh update\` to re-vendor it`
2617
+ `missing lifecycle script ${script} \u2014 the vendored ${basename4(controlDir)}/ payload looks incomplete; run \`oh update\` to re-vendor it`
2025
2618
  );
2026
2619
  }
2027
2620
  return script;
2028
2621
  }
2029
2622
 
2030
2623
  // src/lib/registry.ts
2031
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, readdirSync as readdirSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
2032
- import { dirname as dirname4, join as join5, resolve as resolve6, sep as sep2 } from "node:path";
2624
+ import { existsSync as existsSync11, mkdirSync as mkdirSync4, readdirSync as readdirSync3, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
2625
+ import { dirname as dirname6, join as join6, resolve as resolve8, sep as sep3 } from "node:path";
2033
2626
 
2034
- // oh-asset:/home/runner/work/openharness/openharness/.devcontainer/docker-compose.yml
2035
- var docker_compose_default = 'name: ${SANDBOX_NAME:-openharness}\n\nservices:\n sandbox:\n image: ${OH_SANDBOX_IMAGE:-sandbox-${SANDBOX_NAME:-openharness}}\n pull_policy: ${OH_PULL_POLICY:-missing}\n container_name: ${SANDBOX_NAME:-openharness}\n build:\n context: ${OH_REPO_DIR:-..}\n dockerfile: .devcontainer/Dockerfile\n volumes:\n - ${OH_HOME_MOUNT:-workspace}:/home/sandbox\n - ${OH_REPO_DIR:-..}:/home/sandbox/harness\n extra_hosts:\n - "host.docker.internal:host-gateway"\n environment:\n - SANDBOX_NAME=${SANDBOX_NAME:-openharness}\n - SANDBOX_PASSWORD=${SANDBOX_PASSWORD:-test1234}\n - TZ=${TZ:-America/Los_Angeles}\n - CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=true\n - CC_SAFETY_NET_STRICT=1\n - CC_SAFETY_NET_WORKTREE=1\n - GIT_USER_NAME=${GIT_USER_NAME:-}\n - GIT_USER_EMAIL=${GIT_USER_EMAIL:-}\n - GH_TOKEN=${GH_TOKEN:-}\n stdin_open: true\n tty: true\n cgroup: private\n cap_add:\n - SYS_ADMIN\n security_opt:\n - apparmor=unconfined\n tmpfs:\n - /run\n - /run/lock\n - /sys/fs\n healthcheck:\n test: ["CMD", "bash", "/home/sandbox/harness/.oh/scripts/sandbox-healthcheck.sh"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 600s\n restart: unless-stopped\n\nvolumes:\n workspace:\n';
2627
+ // oh-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.yml
2628
+ var docker_compose_default = 'name: ${SANDBOX_NAME:-agro}\n\nservices:\n sandbox:\n image: ${AGRO_SANDBOX_IMAGE:-${OH_SANDBOX_IMAGE:-sandbox-${SANDBOX_NAME:-agro}}}\n pull_policy: ${AGRO_PULL_POLICY:-${OH_PULL_POLICY:-missing}}\n container_name: ${SANDBOX_NAME:-agro}\n build:\n context: ${AGRO_REPO_DIR:-${OH_REPO_DIR:-..}}\n dockerfile: .devcontainer/Dockerfile\n volumes:\n - ${AGRO_HOME_MOUNT:-${OH_HOME_MOUNT:-workspace}}:/home/sandbox\n - ${AGRO_REPO_DIR:-${OH_REPO_DIR:-..}}:/home/sandbox/harness\n extra_hosts:\n - "host.docker.internal:host-gateway"\n environment:\n - SANDBOX_NAME=${SANDBOX_NAME:-agro}\n - SANDBOX_PASSWORD=${SANDBOX_PASSWORD:-test1234}\n - TZ=${TZ:-America/Los_Angeles}\n - CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=true\n - CC_SAFETY_NET_STRICT=1\n - CC_SAFETY_NET_WORKTREE=1\n - GIT_USER_NAME=${GIT_USER_NAME:-}\n - GIT_USER_EMAIL=${GIT_USER_EMAIL:-}\n - GH_TOKEN=${GH_TOKEN:-}\n stdin_open: true\n tty: true\n cgroup: private\n cap_add:\n - SYS_ADMIN\n security_opt:\n - apparmor=unconfined\n tmpfs:\n - /run\n - /run/lock\n - /sys/fs\n healthcheck:\n test: ["CMD", "bash", "-c", "if [ -d /home/sandbox/harness/.agro ]; then exec bash /home/sandbox/harness/.agro/scripts/sandbox-healthcheck.sh; fi; exec bash /home/sandbox/harness/.oh/scripts/sandbox-healthcheck.sh"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 600s\n restart: unless-stopped\n\nvolumes:\n workspace:\n';
2036
2629
 
2037
- // oh-asset:/home/runner/work/openharness/openharness/.devcontainer/docker-compose.image-only.yml
2038
- var docker_compose_image_only_default = 'name: ${SANDBOX_NAME:-openharness}\n\nservices:\n sandbox:\n image: ${OH_SANDBOX_IMAGE:-ghcr.io/mifunedev/openharness:latest}\n pull_policy: ${OH_PULL_POLICY:-always}\n container_name: ${SANDBOX_NAME:-openharness}\n volumes:\n - ${OH_HOME_MOUNT:-workspace}:/home/sandbox\n extra_hosts:\n - "host.docker.internal:host-gateway"\n environment:\n - SANDBOX_NAME=${SANDBOX_NAME:-openharness}\n - SANDBOX_PASSWORD=${SANDBOX_PASSWORD:-test1234}\n - TZ=${TZ:-America/Los_Angeles}\n - CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=true\n - CC_SAFETY_NET_STRICT=1\n - CC_SAFETY_NET_WORKTREE=1\n - GIT_USER_NAME=${GIT_USER_NAME:-}\n - GIT_USER_EMAIL=${GIT_USER_EMAIL:-}\n - GH_TOKEN=${GH_TOKEN:-}\n stdin_open: true\n tty: true\n cgroup: private\n cap_add:\n - SYS_ADMIN\n security_opt:\n - apparmor=unconfined\n tmpfs:\n - /run\n - /run/lock\n - /sys/fs\n healthcheck:\n test: ["CMD", "bash", "/home/sandbox/harness/.oh/scripts/sandbox-healthcheck.sh"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 600s\n restart: unless-stopped\n\nvolumes:\n workspace:\n';
2630
+ // oh-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.image-only.yml
2631
+ var docker_compose_image_only_default = 'name: ${SANDBOX_NAME:-agro}\n\nservices:\n sandbox:\n image: ${AGRO_SANDBOX_IMAGE:-${OH_SANDBOX_IMAGE:-ghcr.io/mifunedev/agro:latest}}\n pull_policy: ${AGRO_PULL_POLICY:-${OH_PULL_POLICY:-always}}\n container_name: ${SANDBOX_NAME:-agro}\n volumes:\n - ${AGRO_HOME_MOUNT:-${OH_HOME_MOUNT:-workspace}}:/home/sandbox\n extra_hosts:\n - "host.docker.internal:host-gateway"\n environment:\n - SANDBOX_NAME=${SANDBOX_NAME:-agro}\n - SANDBOX_PASSWORD=${SANDBOX_PASSWORD:-test1234}\n - TZ=${TZ:-America/Los_Angeles}\n - CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=true\n - CC_SAFETY_NET_STRICT=1\n - CC_SAFETY_NET_WORKTREE=1\n - GIT_USER_NAME=${GIT_USER_NAME:-}\n - GIT_USER_EMAIL=${GIT_USER_EMAIL:-}\n - GH_TOKEN=${GH_TOKEN:-}\n stdin_open: true\n tty: true\n cgroup: private\n cap_add:\n - SYS_ADMIN\n security_opt:\n - apparmor=unconfined\n tmpfs:\n - /run\n - /run/lock\n - /sys/fs\n healthcheck:\n test: ["CMD", "bash", "-c", "if [ -d /home/sandbox/harness/.agro ]; then exec bash /home/sandbox/harness/.agro/scripts/sandbox-healthcheck.sh; fi; exec bash /home/sandbox/harness/.oh/scripts/sandbox-healthcheck.sh"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 600s\n restart: unless-stopped\n\nvolumes:\n workspace:\n';
2039
2632
 
2040
- // oh-asset:/home/runner/work/openharness/openharness/.devcontainer/docker-compose.ssh.yml
2633
+ // oh-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.ssh.yml
2041
2634
  var docker_compose_ssh_default = 'services:\n sandbox:\n ports:\n - "127.0.0.1:${SANDBOX_SSH_PORT:-2222}:22"\n';
2042
2635
 
2043
- // oh-asset:/home/runner/work/openharness/openharness/.devcontainer/docker-compose.docker-sock.yml
2636
+ // oh-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.docker-sock.yml
2044
2637
  var docker_compose_docker_sock_default = "services:\n sandbox:\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n";
2045
2638
 
2046
- // oh-asset:/home/runner/work/openharness/openharness/.oh/scripts/docker-compose.sh
2639
+ // oh-asset:/home/runner/work/agro/agro/.agro/scripts/docker-compose.sh
2047
2640
  var docker_compose_default2 = `#!/usr/bin/env bash
2048
2641
 
2049
2642
  set -euo pipefail
@@ -2059,10 +2652,10 @@ Usage: scripts/docker-compose.sh [--repo-dir DIR] [--extra-env-file FILE]
2059
2652
  [--print-argv] <docker-compose-args...>
2060
2653
 
2061
2654
  Builds the harness docker compose argv from the repository dotenv and the
2062
- composeOverrides[] list in oh.json, then executes \`docker compose ...\` with the
2655
+ composeOverrides[] list in agro.json, then executes \`docker compose ...\` with the
2063
2656
  provided args.
2064
2657
  --extra-env-file adds a lower-precedence --env-file ahead of the dotenv; \`oh\`
2065
- renders the non-secret settings of oh.json into that file.
2658
+ renders the non-secret settings of agro.json into that file.
2066
2659
  --print-argv prints one argv entry per line instead of executing; useful for
2067
2660
  safe diagnostics and tests.
2068
2661
  EOF
@@ -2102,7 +2695,7 @@ done
2102
2695
 
2103
2696
  COMPAT="$SCRIPT_DIR/compat.sh"
2104
2697
  if [ ! -f "$COMPAT" ]; then
2105
- printf 'error: %s is missing \u2014 the vendored .oh/scripts payload is incomplete; run \`oh update\`\\n' "$COMPAT" >&2
2698
+ printf 'error: %s is missing \u2014 the vendored .agro/scripts payload is incomplete; run \`oh update\`\\n' "$COMPAT" >&2
2106
2699
  exit 2
2107
2700
  fi
2108
2701
  # shellcheck source=compat.sh
@@ -2166,7 +2759,7 @@ fi
2166
2759
 
2167
2760
  CONFIG_JSON="$(compat_selected_path compat_config_file "$REPO_DIR")" || exit 2
2168
2761
 
2169
- if [ -z "$EXTRA_ENV_FILE" ] && [ -n "$CONFIG_JSON" ]; then
2762
+ if [ -z "$EXTRA_ENV_FILE" ] && [ -f "$CONFIG_JSON" ]; then
2170
2763
  printf 'note: non-secret config comes from %s via \`oh\`; this direct run uses only %s and the compose-file defaults\\n' \\
2171
2764
  "\${CONFIG_JSON#"$REPO_DIR"/}" "\${ENV_FILE#"$REPO_DIR"/}" >&2
2172
2765
  fi
@@ -2199,7 +2792,7 @@ if truthy "$ssh_value"; then
2199
2792
  port_check="$SCRIPT_DIR/check-host-port.sh"
2200
2793
  if [ -x "$port_check" ] || [ -f "$port_check" ]; then
2201
2794
  sandbox_name=\${SANDBOX_NAME:-$(read_env_value SANDBOX_NAME)}
2202
- [ -n "$sandbox_name" ] || sandbox_name=openharness
2795
+ sandbox_name="$(compat_sandbox_name "$sandbox_name")"
2203
2796
  own_port=0
2204
2797
  if command -v docker >/dev/null 2>&1; then
2205
2798
  docker ps --format '{{.Names}}\\t{{.Ports}}' 2>/dev/null \\
@@ -2219,7 +2812,7 @@ if truthy "$ssh_value"; then
2219
2812
  fi
2220
2813
  fi
2221
2814
 
2222
- [ -n "$CONFIG_JSON" ] || CONFIG_JSON="$REPO_DIR/.oh/config.json"
2815
+ [ -f "$CONFIG_JSON" ] || CONFIG_JSON="$REPO_DIR/.agro/config.json"
2223
2816
  [ -f "$CONFIG_JSON" ] || CONFIG_JSON="$REPO_DIR/config.json"
2224
2817
  if command -v jq >/dev/null 2>&1 && [ -f "$CONFIG_JSON" ]; then
2225
2818
  while IFS= read -r override; do
@@ -2235,11 +2828,11 @@ fi
2235
2828
  exec docker compose "\${args[@]}" "$@"
2236
2829
  `;
2237
2830
 
2238
- // oh-asset:/home/runner/work/openharness/openharness/.oh/scripts/compat.sh
2831
+ // oh-asset:/home/runner/work/agro/agro/.agro/scripts/compat.sh
2239
2832
  var compat_default = `#!/usr/bin/env bash
2240
2833
  # Boot-safe dual-generation resolution: bash + coreutils only, no node, no jq.
2241
- # The TypeScript contract is .oh/cli/src/lib/compat.ts; both consume the same
2242
- # vectors in .oh/cli/src/lib/__tests__/fixtures/compat-vectors.json.
2834
+ # The TypeScript contract is .agro/cli/src/lib/compat.ts; both consume the same
2835
+ # vectors in .agro/cli/src/lib/__tests__/fixtures/compat-vectors.json.
2243
2836
 
2244
2837
  COMPAT_LEGACY_CONTROL_DIR=".oh"
2245
2838
  COMPAT_AGRO_CONTROL_DIR=".agro"
@@ -2247,6 +2840,8 @@ COMPAT_LEGACY_CONFIG_FILE="oh.json"
2247
2840
  COMPAT_AGRO_CONFIG_FILE="agro.json"
2248
2841
  COMPAT_LEGACY_SEED_DIR="/opt/oh-seed"
2249
2842
  COMPAT_AGRO_SEED_DIR="/opt/agro-seed"
2843
+ COMPAT_DEFAULT_GENERATION=agro
2844
+ COMPAT_DEFAULT_SANDBOX_NAME=agro
2250
2845
  COMPAT_CONFLICT_STATUS=3
2251
2846
 
2252
2847
  compat_tree_manifest() {
@@ -2294,6 +2889,14 @@ compat_tree_diff() {
2294
2889
  return "$rc"
2295
2890
  }
2296
2891
 
2892
+ compat_sandbox_name() {
2893
+ if [ -n "\${1:-}" ]; then
2894
+ printf '%s\\n' "$1"
2895
+ else
2896
+ printf '%s\\n' "$COMPAT_DEFAULT_SANDBOX_NAME"
2897
+ fi
2898
+ }
2899
+
2297
2900
  compat_present() {
2298
2901
  case "$1" in
2299
2902
  dir) [ -d "$2" ] ;;
@@ -2308,7 +2911,11 @@ compat_resolve_pair() {
2308
2911
  compat_present "$entry_kind" "$legacy" || legacy_present=0
2309
2912
  compat_present "$entry_kind" "$agro" || agro_present=0
2310
2913
  if [ "$legacy_present" = 0 ] && [ "$agro_present" = 0 ]; then
2311
- printf 'absent\\t\\n'
2914
+ if [ "$COMPAT_DEFAULT_GENERATION" = agro ]; then
2915
+ printf 'absent\\t%s\\n' "$agro"
2916
+ else
2917
+ printf 'absent\\t%s\\n' "$legacy"
2918
+ fi
2312
2919
  return 0
2313
2920
  fi
2314
2921
  if [ "$legacy_present" = 1 ] && [ "$agro_present" = 0 ]; then
@@ -2374,10 +2981,10 @@ compat_seed_src() {
2374
2981
  printf '%s\\n' "$configured"
2375
2982
  return 0
2376
2983
  fi
2377
- if [ -d "$prefix$COMPAT_AGRO_SEED_DIR" ] && [ ! -d "$prefix$COMPAT_LEGACY_SEED_DIR" ]; then
2378
- printf '%s\\n' "$prefix$COMPAT_AGRO_SEED_DIR"
2379
- else
2984
+ if [ -d "$prefix$COMPAT_LEGACY_SEED_DIR" ]; then
2380
2985
  printf '%s\\n' "$prefix$COMPAT_LEGACY_SEED_DIR"
2986
+ else
2987
+ printf '%s\\n' "$prefix$COMPAT_AGRO_SEED_DIR"
2381
2988
  fi
2382
2989
  }
2383
2990
 
@@ -2385,12 +2992,11 @@ compat_marker_file() {
2385
2992
  local root="$1" line dir
2386
2993
  line="$(compat_control_dir "$root")" || return $?
2387
2994
  dir="\${line#* }"
2388
- [ -n "$dir" ] || return 1
2389
2995
  printf '%s\\n' "$dir/.image-seeded"
2390
2996
  }
2391
2997
  `;
2392
2998
 
2393
- // oh-asset:/home/runner/work/openharness/openharness/.oh/scripts/check-host-port.sh
2999
+ // oh-asset:/home/runner/work/agro/agro/.agro/scripts/check-host-port.sh
2394
3000
  var check_host_port_default = `#!/usr/bin/env bash
2395
3001
 
2396
3002
  set -eu
@@ -2476,12 +3082,12 @@ exit 0
2476
3082
 
2477
3083
  // src/lib/registry.ts
2478
3084
  var SANDBOX_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
2479
- var DEFAULT_NAME_PREFIX = "oh-sbx-";
3085
+ var DEFAULT_NAME_PREFIX = "agro-sbx-";
2480
3086
  function ohHome() {
2481
3087
  return resolveUserStateHome(process.env);
2482
3088
  }
2483
3089
  function registryRoot() {
2484
- return join5(ohHome(), "sandboxes");
3090
+ return join6(ohHome(), "sandboxes");
2485
3091
  }
2486
3092
  function assertSandboxName(name) {
2487
3093
  if (!SANDBOX_NAME_PATTERN.test(name)) {
@@ -2492,20 +3098,20 @@ function assertSandboxName(name) {
2492
3098
  }
2493
3099
  function entryRoot(name) {
2494
3100
  assertSandboxName(name);
2495
- return join5(registryRoot(), name);
3101
+ return join6(registryRoot(), name);
2496
3102
  }
2497
3103
  function isDirectory(path4) {
2498
- return statSync4(path4, { throwIfNoEntry: false })?.isDirectory() === true;
3104
+ return statSync3(path4, { throwIfNoEntry: false })?.isDirectory() === true;
2499
3105
  }
2500
3106
  function listEntries() {
2501
3107
  const root = registryRoot();
2502
3108
  if (!isDirectory(root)) return [];
2503
- return readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => SANDBOX_NAME_PATTERN.test(name)).filter((name) => existsSync10(ohConfigPath(join5(root, name)))).sort();
3109
+ return readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => SANDBOX_NAME_PATTERN.test(name)).filter((name) => existsSync11(ohConfigPath(join6(root, name)))).sort();
2504
3110
  }
2505
3111
  function entryRepo(name) {
2506
3112
  const config = readOhConfig(ohConfigPath(entryRoot(name)));
2507
3113
  const repo = config.repo;
2508
- return typeof repo === "string" && repo !== "" ? resolve6(repo) : void 0;
3114
+ return typeof repo === "string" && repo !== "" ? resolve8(repo) : void 0;
2509
3115
  }
2510
3116
  function runningContainerNames(run) {
2511
3117
  const names = /* @__PURE__ */ new Set();
@@ -2531,26 +3137,27 @@ function nextDefaultName(run = spawnRunner) {
2531
3137
  }
2532
3138
  }
2533
3139
  function materialize(root, opts = {}) {
2534
- const entry = resolve6(root);
3140
+ const entry = resolve8(root);
3141
+ const scripts = join6(resolveProjectLayout(entry).controlDir, "scripts");
2535
3142
  const files = [
2536
3143
  {
2537
- rel: ".devcontainer/docker-compose.yml",
3144
+ dest: join6(entry, ".devcontainer", "docker-compose.yml"),
2538
3145
  body: opts.repo === void 0 ? docker_compose_image_only_default : docker_compose_default,
2539
3146
  mode: 420
2540
3147
  },
2541
- { rel: ".devcontainer/docker-compose.ssh.yml", body: docker_compose_ssh_default, mode: 420 },
2542
- { rel: ".devcontainer/docker-compose.docker-sock.yml", body: docker_compose_docker_sock_default, mode: 420 },
2543
- { rel: ".oh/scripts/docker-compose.sh", body: docker_compose_default2, mode: 493 },
2544
- { rel: ".oh/scripts/compat.sh", body: compat_default, mode: 420 },
2545
- { rel: ".oh/scripts/check-host-port.sh", body: check_host_port_default, mode: 493 }
3148
+ { dest: join6(entry, ".devcontainer", "docker-compose.ssh.yml"), body: docker_compose_ssh_default, mode: 420 },
3149
+ { dest: join6(entry, ".devcontainer", "docker-compose.docker-sock.yml"), body: docker_compose_docker_sock_default, mode: 420 },
3150
+ { dest: join6(scripts, "docker-compose.sh"), body: docker_compose_default2, mode: 493 },
3151
+ { dest: join6(scripts, "compat.sh"), body: compat_default, mode: 420 },
3152
+ { dest: join6(scripts, "check-host-port.sh"), body: check_host_port_default, mode: 493 }
2546
3153
  ];
2547
3154
  for (const file of files) {
2548
- const dest = resolve6(entry, file.rel);
2549
- if (!dest.startsWith(entry + sep2)) {
3155
+ const dest = resolve8(file.dest);
3156
+ if (!dest.startsWith(entry + sep3)) {
2550
3157
  throw new Error(`refusing to materialize outside the sandbox entry: ${dest}`);
2551
3158
  }
2552
- mkdirSync3(dirname4(dest), { recursive: true });
2553
- writeFileSync4(dest, file.body, { mode: file.mode, encoding: "utf8" });
3159
+ mkdirSync4(dirname6(dest), { recursive: true });
3160
+ writeFileSync5(dest, file.body, { mode: file.mode, encoding: "utf8" });
2554
3161
  }
2555
3162
  }
2556
3163
  function resolveSandboxRoot(opts = {}) {
@@ -2565,10 +3172,10 @@ function resolveSandboxRoot(opts = {}) {
2565
3172
  }
2566
3173
  const names = listEntries();
2567
3174
  if (names.length === 1) return entryRoot(names[0]);
2568
- const cwd = resolve6(opts.cwd ?? process.cwd());
3175
+ const cwd = resolve8(opts.cwd ?? process.cwd());
2569
3176
  for (const name of names) {
2570
3177
  const repo = entryRepo(name);
2571
- if (repo !== void 0 && (cwd === repo || cwd.startsWith(repo + sep2))) {
3178
+ if (repo !== void 0 && (cwd === repo || cwd.startsWith(repo + sep3))) {
2572
3179
  return entryRoot(name);
2573
3180
  }
2574
3181
  }
@@ -2814,15 +3421,15 @@ function sameRemote(url, owner, name) {
2814
3421
  }
2815
3422
 
2816
3423
  // src/commands/secret.ts
2817
- import { existsSync as existsSync12 } from "node:fs";
2818
- import { join as join7 } from "node:path";
3424
+ import { existsSync as existsSync13 } from "node:fs";
3425
+ import { join as join8 } from "node:path";
2819
3426
 
2820
3427
  // src/lib/gitignore.ts
2821
- import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
2822
- import { dirname as dirname5, join as join6 } from "node:path";
3428
+ import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
3429
+ import { dirname as dirname7, join as join7 } from "node:path";
2823
3430
  function appendGitignoreLines(root, lines) {
2824
- const target = join6(root, ".gitignore");
2825
- const existing = existsSync11(target) ? readFileSync9(target, "utf8") : "";
3431
+ const target = join7(root, ".gitignore");
3432
+ const existing = existsSync12(target) ? readFileSync10(target, "utf8") : "";
2826
3433
  const present = new Set(existing.split("\n").map((line) => line.trimEnd()));
2827
3434
  const added = [];
2828
3435
  for (const line of lines) {
@@ -2836,8 +3443,8 @@ function appendGitignoreLines(root, lines) {
2836
3443
  if (output.length > 0 && !output.endsWith("\n")) output += "\n";
2837
3444
  output += `${added.join("\n")}
2838
3445
  `;
2839
- mkdirSync4(dirname5(target), { recursive: true });
2840
- writeFileSync5(target, output, "utf8");
3446
+ mkdirSync5(dirname7(target), { recursive: true });
3447
+ writeFileSync6(target, output, "utf8");
2841
3448
  return added;
2842
3449
  }
2843
3450
 
@@ -2846,7 +3453,7 @@ function secretsRoot(opts) {
2846
3453
  return opts.sandbox === void 0 ? resolveProjectRoot(opts.cwd) : resolveSandboxRoot({ name: opts.sandbox });
2847
3454
  }
2848
3455
  function ignoreSecretsFile(root) {
2849
- if (!existsSync12(join7(root, ".git"))) return;
3456
+ if (!existsSync13(join8(root, ".git"))) return;
2850
3457
  appendGitignoreLines(root, [".env"]);
2851
3458
  }
2852
3459
  function secretKeyList() {
@@ -2901,14 +3508,14 @@ async function runSecretList(opts, io) {
2901
3508
  }
2902
3509
 
2903
3510
  // src/commands/lifecycle.ts
2904
- import { existsSync as existsSync14, mkdtempSync, readFileSync as readFileSync10, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
3511
+ import { existsSync as existsSync15, mkdtempSync, readFileSync as readFileSync11, rmSync, writeFileSync as writeFileSync7 } from "node:fs";
2905
3512
  import { tmpdir } from "node:os";
2906
- import { basename as basename4, join as join8, sep as sep3 } from "node:path";
3513
+ import { basename as basename5, join as join9, sep as sep4 } from "node:path";
2907
3514
 
2908
3515
  // src/lib/execution/detect.ts
2909
- import { existsSync as existsSync13 } from "node:fs";
3516
+ import { existsSync as existsSync14 } from "node:fs";
2910
3517
  var SANDBOX_MARKER_FILE = "/.dockerenv";
2911
- function runningInsideSandbox(env = process.env, fileExists = existsSync13) {
3518
+ function runningInsideSandbox(env = process.env, fileExists = existsSync14) {
2912
3519
  const override = resolveAliasedEnv(env, "EXECUTION_TARGET").value;
2913
3520
  if (override === "local") return true;
2914
3521
  if (override === "docker-compose") return false;
@@ -3151,6 +3758,7 @@ var RETIRED_KEYS = [
3151
3758
  "PROJECTS_DIR",
3152
3759
  "CRONS_DIR",
3153
3760
  "OH_PROJECT_ROOT",
3761
+ "AGRO_PROJECT_ROOT",
3154
3762
  "INSTALL_DEEPAGENTS",
3155
3763
  "INSTALL_OPENCODE",
3156
3764
  "INSTALL_GROK_BUILD",
@@ -3174,24 +3782,24 @@ function renderComposeVars(config) {
3174
3782
  };
3175
3783
  put("SANDBOX_NAME", config.name);
3176
3784
  put("TZ", config.timezone);
3177
- put("OH_HOME_MOUNT", config.storage?.homePath);
3178
- put("OH_REPO_DIR", config.repo);
3785
+ put("AGRO_HOME_MOUNT", config.storage?.homePath);
3786
+ put("AGRO_REPO_DIR", config.repo);
3179
3787
  put("GIT_USER_NAME", config.git?.userName);
3180
3788
  put("GIT_USER_EMAIL", config.git?.userEmail);
3181
3789
  put("DOCKER_SOCKET", config.access?.dockerSocket);
3182
3790
  put("SANDBOX_SSH", config.access?.ssh);
3183
3791
  put("SANDBOX_SSH_PORT", config.access?.sshPort);
3184
- put("OH_SANDBOX_IMAGE", config.image?.ref);
3185
- put("OH_PULL_POLICY", config.image?.pullPolicy);
3792
+ put("AGRO_SANDBOX_IMAGE", config.image?.ref);
3793
+ put("AGRO_PULL_POLICY", config.image?.pullPolicy);
3186
3794
  for (const { key, value } of out) {
3187
3795
  if (isSecretKey(key)) {
3188
- throw new Error(`refusing to render secret ${key} from oh.json`);
3796
+ throw new Error(`refusing to render secret ${key} from the sandbox config`);
3189
3797
  }
3190
3798
  if (RETIRED_KEYS.includes(key)) {
3191
3799
  throw new Error(`refusing to render retired variable ${key}`);
3192
3800
  }
3193
3801
  if (/[\r\n]/.test(value)) {
3194
- throw new Error(`oh.json value for ${key} must not contain a newline`);
3802
+ throw new Error(`config value for ${key} must not contain a newline`);
3195
3803
  }
3196
3804
  }
3197
3805
  return out;
@@ -3205,18 +3813,18 @@ function renderComposeEnv(config) {
3205
3813
  // src/commands/lifecycle.ts
3206
3814
  var COMPOSE_ENV_DIR_PREFIX = "oh-compose-env-";
3207
3815
  function composeEnvDir() {
3208
- return mkdtempSync(join8(tmpdir(), COMPOSE_ENV_DIR_PREFIX));
3816
+ return mkdtempSync(join9(tmpdir(), COMPOSE_ENV_DIR_PREFIX));
3209
3817
  }
3210
3818
  function writeComposeEnv(root, dir) {
3211
- const file = join8(dir, "compose.env");
3212
- writeFileSync6(file, renderComposeEnv(readOhConfig(ohConfigPath(root))), {
3819
+ const file = join9(dir, "compose.env");
3820
+ writeFileSync7(file, renderComposeEnv(readOhConfig(ohConfigPath(root))), {
3213
3821
  mode: 384,
3214
3822
  encoding: "utf8"
3215
3823
  });
3216
3824
  return file;
3217
3825
  }
3218
3826
  function withComposeEnvFile(root, fn) {
3219
- if (!existsSync14(ohConfigPath(root))) return fn([]);
3827
+ if (!existsSync15(ohConfigPath(root))) return fn([]);
3220
3828
  const dir = composeEnvDir();
3221
3829
  try {
3222
3830
  return fn(["--extra-env-file", writeComposeEnv(root, dir)]);
@@ -3225,7 +3833,7 @@ function withComposeEnvFile(root, fn) {
3225
3833
  }
3226
3834
  }
3227
3835
  async function withComposeEnvFileAsync(root, fn) {
3228
- if (!existsSync14(ohConfigPath(root))) return await fn([]);
3836
+ if (!existsSync15(ohConfigPath(root))) return await fn([]);
3229
3837
  const dir = composeEnvDir();
3230
3838
  try {
3231
3839
  return await fn(["--extra-env-file", writeComposeEnv(root, dir)]);
@@ -3235,17 +3843,17 @@ async function withComposeEnvFileAsync(root, fn) {
3235
3843
  }
3236
3844
  function sandboxRoot(opts) {
3237
3845
  const root = resolveSandboxRoot({ name: opts.name, cwd: opts.cwd });
3238
- if (existsSync14(ohConfigPath(root))) {
3846
+ if (existsSync15(ohConfigPath(root))) {
3239
3847
  const repo = configuredString(root, "repo");
3240
3848
  materialize(root, repo === void 0 ? {} : { repo });
3241
3849
  }
3242
3850
  return root;
3243
3851
  }
3244
- var DEFAULT_CONTAINER_NAME = "openharness";
3245
- var DEFAULT_SANDBOX_IMAGE = "ghcr.io/mifunedev/openharness:latest";
3852
+ var DEFAULT_CONTAINER_NAME = DEFAULT_SANDBOX_NAME;
3853
+ var DEFAULT_SANDBOX_IMAGE = "ghcr.io/mifunedev/agro:latest";
3246
3854
  function configuredField(root, path4) {
3247
3855
  const file = ohConfigPath(root);
3248
- if (!existsSync14(file)) return void 0;
3856
+ if (!existsSync15(file)) return void 0;
3249
3857
  try {
3250
3858
  return getOhConfigValue(readOhConfig(file), path4);
3251
3859
  } catch {
@@ -3267,8 +3875,8 @@ async function maybePromptDockerSocket(root, io) {
3267
3875
  if (dockerSocketConfigured(root)) return;
3268
3876
  const interactive = process.stdin.isTTY === true || io.ask !== void 0;
3269
3877
  if (!interactive) return;
3270
- const envDir = join8(root, ".devcontainer");
3271
- if (!existsSync14(envDir)) return;
3878
+ const envDir = join9(root, ".devcontainer");
3879
+ if (!existsSync15(envDir)) return;
3272
3880
  const askFn = io.ask ?? ask;
3273
3881
  const answer = (await askFn(
3274
3882
  "Mount host Docker socket into the sandbox? (effectively host root \u2014 enable only if the agent must drive Docker) [y/N]"
@@ -3288,7 +3896,7 @@ async function runSandbox(opts, io) {
3288
3896
  const useImage = opts.image === true || opts.imageRef !== void 0;
3289
3897
  const useNoBuild = useImage || opts.noBuild === true;
3290
3898
  const imageRef = useImage ? opts.imageRef ?? configuredImage(root) ?? DEFAULT_SANDBOX_IMAGE : void 0;
3291
- const env = imageRef === void 0 ? void 0 : { ...process.env, OH_SANDBOX_IMAGE: imageRef };
3899
+ const env = imageRef === void 0 ? void 0 : { ...process.env, ...aliasedEnvPair("SANDBOX_IMAGE", imageRef) };
3292
3900
  if (opts.printArgv === true) {
3293
3901
  const script = requireLifecycleScript(root, "docker-compose.sh");
3294
3902
  return withComposeEnvFile(root, (extraArgs) => {
@@ -3405,7 +4013,7 @@ function runComposeConfig(opts, extra = []) {
3405
4013
  function namedVolumes(root) {
3406
4014
  let text;
3407
4015
  try {
3408
- text = readFileSync10(join8(root, ".devcontainer", "docker-compose.yml"), "utf8");
4016
+ text = readFileSync11(join9(root, ".devcontainer", "docker-compose.yml"), "utf8");
3409
4017
  } catch {
3410
4018
  return [];
3411
4019
  }
@@ -3470,8 +4078,8 @@ stay there. Delete that directory yourself if you also want them gone.
3470
4078
  return 1;
3471
4079
  }
3472
4080
  }
3473
- const code = runComposeVerb("destroy", { ...opts, name: basename4(root) });
3474
- if (code === 0 && root.startsWith(registryRoot() + sep3)) {
4081
+ const code = runComposeVerb("destroy", { ...opts, name: basename5(root) });
4082
+ if (code === 0 && root.startsWith(registryRoot() + sep4)) {
3475
4083
  rmSync(root, { recursive: true, force: true });
3476
4084
  io.stdout(`removed the sandbox entry ${root}
3477
4085
  `);
@@ -3484,16 +4092,16 @@ function runGateway(args, opts) {
3484
4092
  const script = requireLifecycleScript(root, "gateway.sh");
3485
4093
  const r = run("bash", [script, ...args], {
3486
4094
  stdio: "inherit",
3487
- env: { ...process.env, OH_PROJECT_ROOT: root }
4095
+ env: { ...process.env, ...aliasedEnvPair("PROJECT_ROOT", root) }
3488
4096
  });
3489
4097
  assertSpawned(r, `bash ${script}`);
3490
4098
  return r.status ?? 1;
3491
4099
  }
3492
4100
 
3493
4101
  // src/commands/sandbox.ts
3494
- import { existsSync as existsSync15, mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "node:fs";
4102
+ import { existsSync as existsSync16, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "node:fs";
3495
4103
  import { tmpdir as tmpdir2 } from "node:os";
3496
- import { join as join9, resolve as resolve7 } from "node:path";
4104
+ import { join as join10, resolve as resolve9 } from "node:path";
3497
4105
 
3498
4106
  // src/lib/runtimes/catalog.ts
3499
4107
  var RUNTIME_CATALOG = Object.freeze([
@@ -3540,23 +4148,53 @@ function gitIdentity(run, key) {
3540
4148
  function readSeedConfig(repo) {
3541
4149
  if (repo === void 0) return void 0;
3542
4150
  const file = ohConfigPath(repo);
3543
- return existsSync15(file) ? readOhConfig(file) : void 0;
4151
+ return existsSync16(file) ? readOhConfig(file) : void 0;
4152
+ }
4153
+ function readEntryConfig(name) {
4154
+ const file = ohConfigPath(entryRoot(name));
4155
+ return existsSync16(file) ? readOhConfig(file) : void 0;
4156
+ }
4157
+ function assignSetting(target, path4, value) {
4158
+ const segments = path4.split(".");
4159
+ let cursor = target;
4160
+ for (const segment of segments.slice(0, -1)) {
4161
+ const section = cursor[segment];
4162
+ if (!section || typeof section !== "object" || Array.isArray(section)) cursor[segment] = {};
4163
+ cursor = cursor[segment];
4164
+ }
4165
+ cursor[segments[segments.length - 1]] = value;
4166
+ }
4167
+ function overlaySettings(target, source) {
4168
+ if (source === void 0) return target;
4169
+ for (const field of OH_CONFIG_FIELDS) {
4170
+ const value = getOhConfigValue(source, field.path);
4171
+ if (value !== void 0) assignSetting(target, field.path, value);
4172
+ }
4173
+ return target;
4174
+ }
4175
+ function mergeSettings(...sources) {
4176
+ const present = sources.filter((source) => source !== void 0);
4177
+ if (present.length === 0) return void 0;
4178
+ const merged = { version: 1 };
4179
+ for (const source of present) overlaySettings(merged, source);
4180
+ return merged;
4181
+ }
4182
+ function nonEmpty(value) {
4183
+ return value === void 0 || value === "" ? void 0 : value;
3544
4184
  }
3545
4185
  function seedConfig(name, repo, seed, run) {
3546
- const config = defaultOhConfig(name);
4186
+ const config = overlaySettings(defaultOhConfig(name), seed);
4187
+ config.name = name;
3547
4188
  config.runtime = "docker";
3548
4189
  if (repo !== void 0) config.repo = repo;
3549
- config.timezone = seed?.timezone ?? hostTimezone();
4190
+ config.timezone = nonEmpty(seed?.timezone) ?? hostTimezone();
3550
4191
  config.git = {
3551
- userName: seed?.git?.userName ?? gitIdentity(run, "user.name"),
3552
- userEmail: seed?.git?.userEmail ?? gitIdentity(run, "user.email")
4192
+ userName: nonEmpty(seed?.git?.userName) ?? gitIdentity(run, "user.name"),
4193
+ userEmail: nonEmpty(seed?.git?.userEmail) ?? gitIdentity(run, "user.email")
3553
4194
  };
3554
- if (seed?.storage?.homePath !== void 0) config.storage = { homePath: seed.storage.homePath };
3555
- config.access = { ...config.access, ...seed?.access ?? {} };
3556
4195
  config.image = {
3557
4196
  ...config.image,
3558
- ...seed?.image ?? {},
3559
- mode: seed?.image?.mode ?? (repo === void 0 ? "image" : "build")
4197
+ mode: seed?.image?.mode ?? (config.repo === void 0 ? "image" : "build")
3560
4198
  };
3561
4199
  return config;
3562
4200
  }
@@ -3612,14 +4250,14 @@ async function runSandboxInstall(opts, io) {
3612
4250
  `);
3613
4251
  return 1;
3614
4252
  }
3615
- const repo = opts.repo === void 0 ? void 0 : resolve7(opts.repo);
3616
- if (repo !== void 0 && !existsSync15(repo)) {
4253
+ const repo = opts.repo === void 0 ? void 0 : resolve9(opts.repo);
4254
+ if (repo !== void 0 && !existsSync16(repo)) {
3617
4255
  io.stderr(`oh sandbox install: --repo directory does not exist: ${repo}
3618
4256
  `);
3619
4257
  return 1;
3620
4258
  }
3621
- const seed = readSeedConfig(repo);
3622
- const name = opts.name ?? seed?.name ?? nextDefaultName(run);
4259
+ const repoSeed = readSeedConfig(repo);
4260
+ const name = opts.name ?? repoSeed?.name ?? nextDefaultName(run);
3623
4261
  try {
3624
4262
  assertSandboxName(name);
3625
4263
  } catch (error) {
@@ -3627,7 +4265,7 @@ async function runSandboxInstall(opts, io) {
3627
4265
  `);
3628
4266
  return 1;
3629
4267
  }
3630
- const config = seedConfig(name, repo, seed, run);
4268
+ const config = seedConfig(name, repo, mergeSettings(readEntryConfig(name), repoSeed), run);
3631
4269
  const interactive = opts.yes !== true && (process.stdin.isTTY === true || io.ask !== void 0);
3632
4270
  if (interactive) {
3633
4271
  header("Configure the sandbox (press Enter to accept the shown default)");
@@ -3644,7 +4282,7 @@ async function runSandboxInstall(opts, io) {
3644
4282
  ...useNoBuild ? { noBuild: true } : {}
3645
4283
  };
3646
4284
  if (opts.printArgv === true) {
3647
- const preview = mkdtempSync2(join9(tmpdir2(), "oh-sandbox-preview-"));
4285
+ const preview = mkdtempSync2(join10(tmpdir2(), "oh-sandbox-preview-"));
3648
4286
  try {
3649
4287
  writeOhConfig(preview, config);
3650
4288
  materialize(preview, { ...config.repo !== void 0 ? { repo: config.repo } : {} });
@@ -3654,7 +4292,7 @@ async function runSandboxInstall(opts, io) {
3654
4292
  }
3655
4293
  }
3656
4294
  const root = entryRoot(config.name);
3657
- mkdirSync5(root, { recursive: true });
4295
+ mkdirSync6(root, { recursive: true });
3658
4296
  writeOhConfig(root, config);
3659
4297
  materialize(root, { ...config.repo !== void 0 ? { repo: config.repo } : {} });
3660
4298
  const code = await runSandbox({ ...sandboxOpts, cwd: root }, io);
@@ -3709,7 +4347,7 @@ async function runSandboxList(opts, io) {
3709
4347
  }
3710
4348
 
3711
4349
  // src/lib/docs.ts
3712
- var SOURCE_DOCS_BASE = "https://github.com/mifunedev/openharness/blob/main/";
4350
+ var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
3713
4351
  function sourceDocsUrl(docsPath) {
3714
4352
  const segments = docsPath.split("/");
3715
4353
  if (segments[0] !== "docs" || segments.length < 2 || segments.some((segment) => segment === "" || segment === "." || segment === "..") || docsPath.includes("\\") || docsPath.includes("?") || docsPath.includes("#")) {
@@ -3961,8 +4599,8 @@ function hermesTargetRoot(target) {
3961
4599
  async function reconcileHermes(target, io) {
3962
4600
  const root = hermesTargetRoot(target);
3963
4601
  const result = await target.exec({
3964
- argv: ["bash", `${root}/.oh/scripts/link-providers.sh`, "--init", "--hermes-only"],
3965
- env: { OH_PROJECT_ROOT: root },
4602
+ argv: remoteControlDirScript(root, "scripts/link-providers.sh", ["--init", "--hermes-only"]),
4603
+ env: aliasedEnvPair("PROJECT_ROOT", root),
3966
4604
  user: "sandbox",
3967
4605
  stdio: "inherit"
3968
4606
  });
@@ -4002,7 +4640,7 @@ Start it with \`oh sandbox\`, then re-run this command.
4002
4640
  }
4003
4641
  const hermes = entry.id === "hermes";
4004
4642
  const installEnv = hermes ? {
4005
- OH_PROJECT_ROOT: hermesTargetRoot(target),
4643
+ ...aliasedEnvPair("PROJECT_ROOT", hermesTargetRoot(target)),
4006
4644
  HERMES_HOME: `${hermesTargetRoot(target)}/.hermes`
4007
4645
  } : void 0;
4008
4646
  if (hermes) {
@@ -4419,7 +5057,7 @@ Start it with \`oh sandbox\`, then re-run this command.
4419
5057
 
4420
5058
  // src/lib/remote.ts
4421
5059
  import { spawnSync as spawnSync3 } from "node:child_process";
4422
- var DEFAULT_REPO_URL = "https://github.com/mifunedev/openharness";
5060
+ var DEFAULT_REPO_URL = "https://github.com/mifunedev/agro";
4423
5061
  var DEFAULT_CLONE_TIMEOUT_MS = 12e4;
4424
5062
  var spawnRunner2 = (cmd, args, opts) => {
4425
5063
  const r = spawnSync3(cmd, args, {
@@ -4436,7 +5074,7 @@ var spawnRunner2 = (cmd, args, opts) => {
4436
5074
  };
4437
5075
  };
4438
5076
  function fallbackHint() {
4439
- return "use --from <dir> to point at a local OpenHarness checkout instead";
5077
+ return "use --from <dir> to point at a local AGRO checkout instead";
4440
5078
  }
4441
5079
  function fetchRemoteSource(opts) {
4442
5080
  const repoUrl2 = opts.repoUrl ?? DEFAULT_REPO_URL;
@@ -4481,9 +5119,9 @@ function fetchRemoteSource(opts) {
4481
5119
  }
4482
5120
 
4483
5121
  // src/cli.ts
4484
- var VERSION = true ? "0.9.0" : "0.0.0-dev";
4485
- var DEFAULT_SOURCE_OH_DIR = resolve8(
4486
- dirname6(fileURLToPath(import.meta.url)),
5122
+ var VERSION = true ? "0.10.0" : "0.0.0-dev";
5123
+ var DEFAULT_SOURCE_OH_DIR = resolve10(
5124
+ dirname8(fileURLToPath(import.meta.url)),
4487
5125
  "../.."
4488
5126
  );
4489
5127
  var INTEGRATIONS = {};
@@ -4506,14 +5144,16 @@ function compatibilityNote(product) {
4506
5144
  }
4507
5145
  function printOhHelp(product = LEGACY_PRODUCT) {
4508
5146
  const { bin, title } = product;
5147
+ const updateSummary = bin === AGRO_PRODUCT.bin ? `Upgrade the installed ${bin} CLI` : `Vendor or upgrade the ${stateNames(bin).controlDir}/ control plane`;
4509
5148
  process.stdout.write(`${bin} \u2014 ${title} (v${VERSION})
4510
5149
 
4511
5150
  Usage:
4512
5151
  ${bin} sandbox <args...> Create and list sandboxes (install|list)
4513
5152
  ${bin} shell [name] Open a zsh shell in the running sandbox container
4514
- ${bin} config <args...> Read and write oh.json (show|set), or run a wizard
5153
+ ${bin} config <args...> Read and write ${stateNames(bin).configFile} (show|set), or run a wizard
4515
5154
  ${bin} secret <args...> Read and write the gitignored root .env (set|list)
4516
- ${bin} update Vendor or upgrade the .oh/ control plane
5155
+ ${bin} update ${updateSummary}
5156
+ ${bin} migrate Move a legacy .oh/ project or ~/.oh registry to AGRO names (--check|--home)
4517
5157
  ${bin} stop [name] Stop the sandbox, preserving volumes
4518
5158
  ${bin} restart [name] Restart the sandbox service
4519
5159
  ${bin} logs [name] Tail sandbox logs (follows)
@@ -4532,7 +5172,7 @@ ${integrationLines()}
4532
5172
  ${compatibilityNote(product)}`);
4533
5173
  }
4534
5174
  function printConfigHelp(bin = LEGACY_PRODUCT.bin) {
4535
- process.stdout.write(`${bin} config \u2014 Read and write oh.json, the tracked non-secret settings
5175
+ process.stdout.write(`${bin} config \u2014 Read and write ${stateNames(bin).configFile}, the tracked non-secret settings
4536
5176
 
4537
5177
  Usage:
4538
5178
  ${bin} config show [--sandbox <name>]
@@ -4541,7 +5181,7 @@ Usage:
4541
5181
  ${bin} config <integration> Run an integration wizard
4542
5182
  ${bin} config <integration> --help
4543
5183
 
4544
- oh.json holds every non-secret setting and is tracked by git. Credentials live
5184
+ ${stateNames(bin).configFile} holds every non-secret setting and is tracked by git. Credentials live
4545
5185
  in the gitignored root .env \u2014 write those with \`${bin} secret set <KEY>\`. Apply a
4546
5186
  change with \`${bin} stop <name> && ${bin} sandbox install docker --name <name>\`. Field
4547
5187
  reference:
@@ -4566,7 +5206,7 @@ the registry entry \`${bin} sandbox list\` names.
4566
5206
 
4567
5207
  The value is never read from the command line \u2014 an argument would land in your
4568
5208
  shell history. \`${bin} secret list\` never prints a raw value. .env is mode 0600 and
4569
- gitignored; every non-secret setting belongs in oh.json (\`${bin} config set\`).
5209
+ gitignored; every non-secret setting belongs in ${stateNames(bin).configFile} (\`${bin} config set\`).
4570
5210
 
4571
5211
  Keys:
4572
5212
  ${secretKeyList()}
@@ -4579,7 +5219,7 @@ Usage:
4579
5219
  ${bin} update [--dry-run]
4580
5220
 
4581
5221
  Upgrades exactly one thing: the ${bin} executable that is running. It writes no
4582
- project file \u2014 no .oh/, no oh.json, no .env \u2014 and it never asks for sudo.
5222
+ project file \u2014 no ${stateNames(bin).controlDir}/, no ${stateNames(bin).configFile}, no .env \u2014 and it never asks for sudo.
4583
5223
 
4584
5224
  The upgrade follows whichever mechanism installed this executable:
4585
5225
  npm-managed realpath under node_modules/${AGRO_PRODUCT.packageName}/: reads the registry
@@ -4603,7 +5243,7 @@ Flags:
4603
5243
  --dry-run Report the installation kind, target, and versions without
4604
5244
  changing anything.
4605
5245
 
4606
- Project payload vendoring (.oh/ + crons/) is \`oh update\` during the
5246
+ Project payload vendoring (${stateNames(bin).controlDir}/ + crons/) is \`oh update\` during the
4607
5247
  compatibility window; its --from, --from-remote, --ref and --force flags are
4608
5248
  not accepted here.
4609
5249
  `);
@@ -4613,22 +5253,22 @@ function printUpdateHelp(bin = LEGACY_PRODUCT.bin) {
4613
5253
  printSelfUpgradeHelp(bin);
4614
5254
  return;
4615
5255
  }
4616
- process.stdout.write(`${bin} update \u2014 Vendor or upgrade the .oh/ control plane
5256
+ process.stdout.write(`${bin} update \u2014 Vendor or upgrade the ${stateNames(bin).controlDir}/ control plane
4617
5257
 
4618
5258
  Usage:
4619
5259
  ${bin} update [--from <dir> | --from-remote [--ref <ref>]] [--dry-run] [--force]
4620
5260
 
4621
- Writes ONLY the .oh/ control plane and crons/ (skills, scripts, CLI) into the
5261
+ Writes ONLY the ${stateNames(bin).controlDir}/ control plane and crons/ (skills, scripts, CLI) into the
4622
5262
  current directory. An empty directory is equipped from scratch; everything else
4623
- in your project is left untouched \u2014 it writes no oh.json, .env, AGENTS.md,
5263
+ in your project is left untouched \u2014 it writes no ${stateNames(bin).configFile}, .env, AGENTS.md,
4624
5264
  .gitignore or .devcontainer/.
4625
5265
 
4626
5266
  Payload source precedence: --from <dir> > --from-remote > the CLI's own bundled
4627
- .oh/ payload > a remote fetch announced on one line.
5267
+ ${stateNames(bin).controlDir}/ payload > a remote fetch announced on one line.
4628
5268
 
4629
5269
  Flags:
4630
- --from <dir> A built OpenHarness checkout to vendor from.
4631
- --from-remote Fetch the source checkout from the public OpenHarness repo
5270
+ --from <dir> A built AGRO checkout to vendor from.
5271
+ --from-remote Fetch the source checkout from the public AGRO repo
4632
5272
  instead (shallow git clone into a temp dir, removed after
4633
5273
  the run). Conflicts with --from.
4634
5274
  --ref <ref> Branch or tag for --from-remote (default: the clone's
@@ -4651,7 +5291,7 @@ Usage:
4651
5291
  [--image[=<ref>]] [--no-build] [--print-argv]
4652
5292
  ${bin} sandbox list [--json]
4653
5293
 
4654
- \`install\` writes a sandbox entry under \${OH_HOME:-~/.oh}/sandboxes/<name>/,
5294
+ \`install\` writes a sandbox entry under \${${stateNames(bin).envPrefix}HOME:-~/${stateNames(bin).userStateDir}}/sandboxes/<name>/,
4655
5295
  materialises the compose files and the compose wrapper into it, then starts the
4656
5296
  container. It needs no project checkout and runs from any directory. Without
4657
5297
  --repo the sandbox runs the prebuilt image; with --repo <dir> that checkout is
@@ -4659,16 +5299,16 @@ bound at /home/sandbox/harness and can be built locally.
4659
5299
 
4660
5300
  On a terminal without --yes it asks for the sandbox name, the timezone, the git
4661
5301
  identity, SSH (with its host port), and the host Docker socket. With --repo it
4662
- seeds those answers from that checkout's oh.json.
5302
+ seeds those answers from that checkout's ${stateNames(bin).configFile}.
4663
5303
 
4664
5304
  Flags:
4665
- --name <name> Registry entry name (default: the lowest free oh-sbx-<n>)
5305
+ --name <name> Registry entry name (default: the lowest free ${DEFAULT_NAME_PREFIX}<n>)
4666
5306
  --repo <dir> Bind this checkout into the sandbox and seed the defaults
4667
- from its oh.json
5307
+ from its ${stateNames(bin).configFile}
4668
5308
  --yes Non-interactive: keep every default and ask nothing
4669
5309
  --image[=<ref>] Run the prebuilt image instead of building (implies
4670
5310
  --no-build). Ref resolves last-wins: --image=<ref> >
4671
- oh.json image.ref > ghcr.io/mifunedev/openharness:latest.
5311
+ ${stateNames(bin).configFile} image.ref > ${DEFAULT_SANDBOX_IMAGE}.
4672
5312
  --no-build Suppress the local build and reuse an existing image
4673
5313
  --print-argv Print the docker compose argv that would run, then exit
4674
5314
  without writing an entry
@@ -4702,7 +5342,7 @@ Usage:
4702
5342
  ${bin} harness status [name] Show installed state
4703
5343
 
4704
5344
  \`install\` is the only door: it probes the running sandbox, installs the harness
4705
- into the persistent home volume, and reports. It reads and writes no \`oh.json\`
5345
+ into the persistent home volume, and reports. It reads and writes no \`${stateNames(bin).configFile}\`
4706
5346
  field, and it never rebuilds or restarts the sandbox. It requires a running
4707
5347
  sandbox \u2014 start one with \`${bin} sandbox\` first.
4708
5348
 
@@ -4723,7 +5363,7 @@ Usage:
4723
5363
  ${bin} gateway status show both sessions
4724
5364
 
4725
5365
  Only a LEADING --help/-h is intercepted here; everything else passes through
4726
- verbatim to the vendored .oh/scripts/gateway.sh with OH_PROJECT_ROOT set to
5366
+ verbatim to the vendored ${stateNames(bin).controlDir}/scripts/gateway.sh with ${stateNames(bin).envPrefix}PROJECT_ROOT set to
4727
5367
  the equipped project root. Exits with the script's exit code.
4728
5368
  `);
4729
5369
  }
@@ -4751,7 +5391,7 @@ Most tools are baked into the image and are report-only; \`install\` works on:
4751
5391
  ${installableToolIds().map((t) => ` ${t}`).join("\n")}
4752
5392
 
4753
5393
  \`install\` is the only door: it probes the running sandbox, installs the tool
4754
- into the persistent home volume, and reports. It reads and writes no \`oh.json\`
5394
+ into the persistent home volume, and reports. It reads and writes no \`${stateNames(bin).configFile}\`
4755
5395
  field, and it never rebuilds or restarts the sandbox. A large download is
4756
5396
  confirmed first, and a non-interactive run without --yes installs nothing.
4757
5397
 
@@ -4776,7 +5416,7 @@ function printComposeVerbHelp(verb, bin = LEGACY_PRODUCT.bin) {
4776
5416
  Usage:
4777
5417
  ${bin} ${verb} [name] [-- <extra docker compose args>]
4778
5418
 
4779
- Runs .oh/scripts/docker-compose.sh inside the sandbox entry, the single
5419
+ Runs ${stateNames(bin).controlDir}/scripts/docker-compose.sh inside the sandbox entry, the single
4780
5420
  implementation. \`${bin}\` is the only lifecycle door. [name] is a sandbox entry from
4781
5421
  \`${bin} sandbox list\`; with exactly one registered sandbox it can be omitted.
4782
5422
 
@@ -4789,7 +5429,7 @@ function printDestroyHelp(bin = LEGACY_PRODUCT.bin) {
4789
5429
  Usage:
4790
5430
  ${bin} destroy [name] [--yes]
4791
5431
 
4792
- Runs .oh/scripts/docker-compose.sh with \`down -v\`. This is the one destructive
5432
+ Runs ${stateNames(bin).controlDir}/scripts/docker-compose.sh with \`down -v\`. This is the one destructive
4793
5433
  lifecycle verb: \`-v\` deletes the named
4794
5434
  volumes, and those volumes hold every agent CLI login, the gh CLI token, and
4795
5435
  the SSH keys. Use \`${bin} stop\` when you only want the containers gone.
@@ -4812,11 +5452,11 @@ Usage:
4812
5452
  ${bin} compose config [-- <extra docker compose args>]
4813
5453
 
4814
5454
  Subcommands:
4815
- config Print the compose configuration .oh/scripts/docker-compose.sh
4816
- resolves from .devcontainer/.env and .oh/config.json
5455
+ config Print the compose configuration ${stateNames(bin).controlDir}/scripts/docker-compose.sh
5456
+ resolves from .devcontainer/.env and ${stateNames(bin).controlDir}/config.json
4817
5457
 
4818
5458
  Namespaced under \`${bin} compose\` because \`${bin} config <integration>\` already means
4819
- "run an integration wizard", and \`${bin} config show/set\` reads and writes oh.json.
5459
+ "run an integration wizard", and \`${bin} config show/set\` reads and writes ${stateNames(bin).configFile}.
4820
5460
 
4821
5461
  See ${sourceDocsUrl("docs/lifecycle-commands.md")} for every verb.
4822
5462
  `);
@@ -4962,14 +5602,14 @@ function parseComposeArgs(rest, bin = LEGACY_PRODUCT.bin) {
4962
5602
  args.subcommand = "config";
4963
5603
  const tail = rest.slice(1);
4964
5604
  if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
4965
- const sep4 = tail.indexOf("--");
4966
- if (sep4 === -1 && tail.length > 0) {
5605
+ const sep5 = tail.indexOf("--");
5606
+ if (sep5 === -1 && tail.length > 0) {
4967
5607
  return {
4968
5608
  ok: false,
4969
5609
  error: `${bin} compose config: unexpected argument "${tail[0]}" \u2014 pass extra docker compose args after \`--\``
4970
5610
  };
4971
5611
  }
4972
- if (sep4 !== -1) args.passthrough = tail.slice(sep4 + 1);
5612
+ if (sep5 !== -1) args.passthrough = tail.slice(sep5 + 1);
4973
5613
  return { ok: true, args };
4974
5614
  }
4975
5615
  var PAYLOAD_UPDATE_FLAGS = ["--from", "--from-remote", "--ref", "--force"];
@@ -5203,19 +5843,19 @@ function parseGatewayArgs(rest) {
5203
5843
  }
5204
5844
  return { ok: true, args: { help: false, passthrough: [...rest] } };
5205
5845
  }
5206
- function bundledPayloadExists(bundled, exists = existsSync16) {
5207
- return exists(join10(bundled.sourceOhDir, "manifest.json"));
5846
+ function bundledPayloadExists(bundled, exists = existsSync17) {
5847
+ return exists(join11(bundled.sourceOhDir, "manifest.json"));
5208
5848
  }
5209
5849
  function resolveUpdateSource(args, bundled, bin = LEGACY_PRODUCT.bin) {
5210
- const exists = bundled.exists ?? existsSync16;
5850
+ const exists = bundled.exists ?? existsSync17;
5211
5851
  if (args.fromDir !== void 0) {
5212
- return { kind: "local", fromDir: resolve8(args.fromDir) };
5852
+ return { kind: "local", fromDir: resolve10(args.fromDir) };
5213
5853
  }
5214
5854
  if (args.fromRemote) {
5215
5855
  return { kind: "remote", ref: args.ref };
5216
5856
  }
5217
5857
  if (bundledPayloadExists(bundled, exists)) {
5218
- return { kind: "local", fromDir: resolve8(bundled.sourceOhDir, "..") };
5858
+ return { kind: "local", fromDir: resolve10(bundled.sourceOhDir, "..") };
5219
5859
  }
5220
5860
  return {
5221
5861
  kind: "remote",
@@ -5227,7 +5867,7 @@ function resolveUpdateSource(args, bundled, bin = LEGACY_PRODUCT.bin) {
5227
5867
  function readPayloadVersion(checkoutDir) {
5228
5868
  try {
5229
5869
  const parsed = JSON.parse(
5230
- readFileSync11(join10(checkoutDir, ".oh", "cli", "package.json"), "utf8")
5870
+ readFileSync12(join11(resolveControlDir(checkoutDir).path, "cli", "package.json"), "utf8")
5231
5871
  );
5232
5872
  if (parsed && typeof parsed.version === "string") return parsed.version;
5233
5873
  } catch {
@@ -5236,7 +5876,7 @@ function readPayloadVersion(checkoutDir) {
5236
5876
  }
5237
5877
  async function runWithRemoteSource(hooks, fn) {
5238
5878
  const fetch2 = hooks.fetch ?? fetchRemoteSource;
5239
- const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync3(join10(tmpdir3(), "oh-remote-")));
5879
+ const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync3(join11(tmpdir3(), "oh-remote-")));
5240
5880
  const rm = hooks.rm ?? ((dir) => rmSync3(dir, { recursive: true, force: true }));
5241
5881
  const out = hooks.stdout ?? ((s) => process.stdout.write(s));
5242
5882
  const checkoutDir = mkdtemp();
@@ -5321,6 +5961,24 @@ async function main(argv) {
5321
5961
  if (a.verb === "list") return await runSecretList(scope, io);
5322
5962
  return await runSecretSet(a.key, scope, io);
5323
5963
  }
5964
+ if (first === "migrate") {
5965
+ const parsed = parseMigrateArgs(argv.slice(1), bin);
5966
+ if (!parsed.ok) {
5967
+ process.stderr.write(`${parsed.error}
5968
+ `);
5969
+ if (parsed.showHelp) printMigrateHelp(bin);
5970
+ return 1;
5971
+ }
5972
+ if (parsed.args.help) {
5973
+ printMigrateHelp(bin);
5974
+ return 0;
5975
+ }
5976
+ const io = {
5977
+ stdout: (s) => process.stdout.write(s),
5978
+ stderr: (s) => process.stderr.write(s)
5979
+ };
5980
+ return runMigrate(parsed.args, io);
5981
+ }
5324
5982
  if (first === "update") {
5325
5983
  const parsed = parseUpdateArgs(argv.slice(1), bin);
5326
5984
  if (!parsed.ok) {
@@ -5437,9 +6095,9 @@ async function main(argv) {
5437
6095
  printComposeVerbHelp(verb, bin);
5438
6096
  return 0;
5439
6097
  }
5440
- const sep4 = rest.indexOf("--");
5441
- const head = sep4 === -1 ? rest : rest.slice(0, sep4);
5442
- const extra = sep4 === -1 ? [] : rest.slice(sep4 + 1);
6098
+ const sep5 = rest.indexOf("--");
6099
+ const head = sep5 === -1 ? rest : rest.slice(0, sep5);
6100
+ const extra = sep5 === -1 ? [] : rest.slice(sep5 + 1);
5443
6101
  if (head.length > 0 && head[0].startsWith("-")) {
5444
6102
  process.stderr.write(`${bin} ${verb}: unknown flag "${head[0]}"
5445
6103
  `);