@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.
- package/NOTICE +2 -2
- package/README.md +16 -16
- package/dist/agro.js +1132 -474
- package/dist/oh.js +1132 -474
- 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
|
|
6
|
-
import { existsSync as
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
existsSync,
|
|
16
|
+
lstatSync,
|
|
17
|
+
readdirSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
readlinkSync,
|
|
20
|
+
statSync
|
|
14
21
|
} from "node:fs";
|
|
15
|
-
import
|
|
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(
|
|
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,
|
|
93
|
-
if (dest === targetOh || dest.startsWith(targetOh +
|
|
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,
|
|
99
|
-
if (dest === targetRoot || dest.startsWith(targetRoot +
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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(
|
|
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
|
|
247
|
-
if (
|
|
472
|
+
const source = resolveControlDir(fromDir);
|
|
473
|
+
if (source.kind === "absent") {
|
|
248
474
|
io.stderr(
|
|
249
|
-
"oh update: update source not found at " +
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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/
|
|
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
|
|
613
|
-
if (
|
|
614
|
-
return { ...kindOfTarget(
|
|
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 .
|
|
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
|
|
725
|
+
let resolved2;
|
|
683
726
|
try {
|
|
684
|
-
|
|
727
|
+
resolved2 = deps.realpath(found);
|
|
685
728
|
} catch {
|
|
686
|
-
|
|
729
|
+
resolved2 = found;
|
|
687
730
|
}
|
|
688
|
-
if (
|
|
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
|
|
867
|
-
const dirs = (env.PATH ?? "").split(
|
|
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 (
|
|
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) =>
|
|
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
|
|
918
|
-
import { join as
|
|
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((
|
|
948
|
-
rl.question(` ${question} `, (answer) =>
|
|
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((
|
|
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
|
-
|
|
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((
|
|
1621
|
+
return await new Promise((resolve11) => {
|
|
1029
1622
|
rl.question(` ${question} `, (answer) => {
|
|
1030
1623
|
process.stdout.write("\n");
|
|
1031
|
-
|
|
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
|
|
1048
|
-
import { basename as basename3, dirname as
|
|
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
|
|
1052
|
-
import { resolve as
|
|
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 +
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
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 (!
|
|
1768
|
+
if (!existsSync6(path4)) return defaultOhConfig(basename3(dirname4(resolve5(path4))));
|
|
1177
1769
|
let raw;
|
|
1178
1770
|
try {
|
|
1179
|
-
raw =
|
|
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(`${
|
|
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,
|
|
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
|
-
|
|
1791
|
+
mkdirSync3(dirname4(path4), { recursive: true });
|
|
1200
1792
|
try {
|
|
1201
|
-
|
|
1202
|
-
|
|
1793
|
+
writeFileSync3(tmp, body, { mode: OH_CONFIG_MODE, encoding: "utf8" });
|
|
1794
|
+
renameSync3(tmp, path4);
|
|
1203
1795
|
} catch (error) {
|
|
1204
1796
|
try {
|
|
1205
|
-
|
|
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
|
|
1997
|
+
import { dirname as dirname5, resolve as resolve6 } from "node:path";
|
|
1406
1998
|
function resolveProjectRoot(startDir = process.cwd()) {
|
|
1407
|
-
let dir =
|
|
1999
|
+
let dir = resolve6(startDir);
|
|
1408
2000
|
for (; ; ) {
|
|
1409
2001
|
if (resolveControlDir(dir).kind !== "absent") return dir;
|
|
1410
|
-
const parent =
|
|
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
|
|
1420
|
-
import { resolve as
|
|
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
|
|
1425
|
-
writeFileSync as
|
|
1426
|
-
existsSync as
|
|
1427
|
-
fsyncSync,
|
|
1428
|
-
renameSync as
|
|
1429
|
-
unlinkSync as
|
|
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 (!
|
|
1435
|
-
const content =
|
|
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 =
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
2043
|
+
fd = openSync2(tmpPath, "w", 384);
|
|
2044
|
+
writeFileSync4(tmpPath, content, "utf8");
|
|
2045
|
+
fsyncSync2(fd);
|
|
2046
|
+
closeSync2(fd);
|
|
1455
2047
|
fd = void 0;
|
|
1456
|
-
|
|
2048
|
+
renameSync4(tmpPath, envPath);
|
|
1457
2049
|
} catch (err) {
|
|
1458
2050
|
if (fd !== void 0) {
|
|
1459
2051
|
try {
|
|
1460
|
-
|
|
2052
|
+
closeSync2(fd);
|
|
1461
2053
|
} catch {
|
|
1462
2054
|
}
|
|
1463
2055
|
}
|
|
1464
2056
|
try {
|
|
1465
|
-
|
|
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
|
|
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 (!
|
|
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,
|
|
1514
|
-
const original =
|
|
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 (!
|
|
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
|
|
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(
|
|
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 || !
|
|
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} -> ${
|
|
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: ${
|
|
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 ${
|
|
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((
|
|
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
|
|
1983
|
-
import { join as
|
|
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
|
|
2022
|
-
|
|
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
|
|
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
|
|
2032
|
-
import { dirname as
|
|
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/
|
|
2035
|
-
var docker_compose_default = 'name: ${SANDBOX_NAME:-
|
|
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/
|
|
2038
|
-
var docker_compose_image_only_default = 'name: ${SANDBOX_NAME:-
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
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
|
|
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 .
|
|
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" ] && [ -
|
|
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
|
-
|
|
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
|
-
[ -
|
|
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/
|
|
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 .
|
|
2242
|
-
# vectors in .
|
|
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
|
-
|
|
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$
|
|
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/
|
|
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 = "
|
|
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
|
|
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
|
|
3101
|
+
return join6(registryRoot(), name);
|
|
2496
3102
|
}
|
|
2497
3103
|
function isDirectory(path4) {
|
|
2498
|
-
return
|
|
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) =>
|
|
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 !== "" ?
|
|
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 =
|
|
3140
|
+
const entry = resolve8(root);
|
|
3141
|
+
const scripts = join6(resolveProjectLayout(entry).controlDir, "scripts");
|
|
2535
3142
|
const files = [
|
|
2536
3143
|
{
|
|
2537
|
-
|
|
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
|
-
{
|
|
2542
|
-
{
|
|
2543
|
-
{
|
|
2544
|
-
{
|
|
2545
|
-
{
|
|
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 =
|
|
2549
|
-
if (!dest.startsWith(entry +
|
|
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
|
-
|
|
2553
|
-
|
|
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 =
|
|
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 +
|
|
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
|
|
2818
|
-
import { join as
|
|
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
|
|
2822
|
-
import { dirname as
|
|
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 =
|
|
2825
|
-
const existing =
|
|
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
|
-
|
|
2840
|
-
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
|
3516
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
2910
3517
|
var SANDBOX_MARKER_FILE = "/.dockerenv";
|
|
2911
|
-
function runningInsideSandbox(env = process.env, fileExists =
|
|
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("
|
|
3178
|
-
put("
|
|
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("
|
|
3185
|
-
put("
|
|
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
|
|
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(`
|
|
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(
|
|
3816
|
+
return mkdtempSync(join9(tmpdir(), COMPOSE_ENV_DIR_PREFIX));
|
|
3209
3817
|
}
|
|
3210
3818
|
function writeComposeEnv(root, dir) {
|
|
3211
|
-
const file =
|
|
3212
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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 =
|
|
3245
|
-
var DEFAULT_SANDBOX_IMAGE = "ghcr.io/mifunedev/
|
|
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 (!
|
|
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 =
|
|
3271
|
-
if (!
|
|
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,
|
|
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 =
|
|
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:
|
|
3474
|
-
if (code === 0 && root.startsWith(registryRoot() +
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 :
|
|
3616
|
-
if (repo !== void 0 && !
|
|
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
|
|
3622
|
-
const name = opts.name ??
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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/
|
|
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:
|
|
3965
|
-
env:
|
|
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
|
-
|
|
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/
|
|
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
|
|
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.
|
|
4485
|
-
var DEFAULT_SOURCE_OH_DIR =
|
|
4486
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 .
|
|
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 (.
|
|
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 .
|
|
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 .
|
|
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
|
|
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
|
-
.
|
|
5267
|
+
${stateNames(bin).controlDir}/ payload > a remote fetch announced on one line.
|
|
4628
5268
|
|
|
4629
5269
|
Flags:
|
|
4630
|
-
--from <dir> A built
|
|
4631
|
-
--from-remote Fetch the source checkout from the public
|
|
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 \${
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 .
|
|
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
|
|
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 .
|
|
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 .
|
|
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 .
|
|
4816
|
-
resolves from .devcontainer/.env and .
|
|
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
|
|
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
|
|
4966
|
-
if (
|
|
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 (
|
|
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 =
|
|
5207
|
-
return exists(
|
|
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 ??
|
|
5850
|
+
const exists = bundled.exists ?? existsSync17;
|
|
5211
5851
|
if (args.fromDir !== void 0) {
|
|
5212
|
-
return { kind: "local", 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:
|
|
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
|
-
|
|
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(
|
|
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
|
|
5441
|
-
const head =
|
|
5442
|
-
const extra =
|
|
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
|
`);
|