@mifune/agro 0.9.0 → 0.11.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 +25 -51
- package/dist/agro.js +2023 -1682
- package/dist/oh.js +2023 -1682
- package/package.json +6 -6
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
|
|
5
|
+
import { dirname as dirname8, join as join10, resolve as resolve10 } from "node:path";
|
|
6
6
|
import { existsSync as existsSync16, mkdtempSync as mkdtempSync3, readFileSync as readFileSync11, 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,66 @@ 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
|
-
|
|
93
|
-
|
|
318
|
+
|
|
319
|
+
// src/lib/product.ts
|
|
320
|
+
import { basename } from "node:path";
|
|
321
|
+
var AGRO_PRODUCT = {
|
|
322
|
+
name: "agro",
|
|
323
|
+
bin: "agro",
|
|
324
|
+
title: "AGRO CLI",
|
|
325
|
+
packageName: "@mifune/agro",
|
|
326
|
+
generation: "agro"
|
|
327
|
+
};
|
|
328
|
+
var LEGACY_PRODUCT = {
|
|
329
|
+
name: "oh",
|
|
330
|
+
bin: "oh",
|
|
331
|
+
title: "Open Harness CLI",
|
|
332
|
+
packageName: "@mifune/openharness",
|
|
333
|
+
generation: "legacy"
|
|
334
|
+
};
|
|
335
|
+
var FILE_EXTENSION = /\.[^.]*$/;
|
|
336
|
+
function invokedName(argv1) {
|
|
337
|
+
if (argv1 === void 0) return "";
|
|
338
|
+
return basename(argv1.replace(/\\/g, "/")).replace(FILE_EXTENSION, "");
|
|
339
|
+
}
|
|
340
|
+
function resolveProduct(argv1) {
|
|
341
|
+
return invokedName(argv1) === LEGACY_PRODUCT.bin ? LEGACY_PRODUCT : AGRO_PRODUCT;
|
|
342
|
+
}
|
|
343
|
+
function activeBin() {
|
|
344
|
+
return resolveProduct(process.argv[1]).bin;
|
|
345
|
+
}
|
|
346
|
+
function productFor(bin) {
|
|
347
|
+
return bin === LEGACY_PRODUCT.bin ? LEGACY_PRODUCT : AGRO_PRODUCT;
|
|
348
|
+
}
|
|
349
|
+
function stateNames(bin) {
|
|
350
|
+
return GENERATIONS[productFor(bin).generation];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/lib/vendor.ts
|
|
354
|
+
function assertDestInTarget(dest, targetOh, sep5) {
|
|
355
|
+
if (dest === targetOh || dest.startsWith(targetOh + sep5)) {
|
|
94
356
|
return;
|
|
95
357
|
}
|
|
96
|
-
throw new Error(
|
|
358
|
+
throw new Error(`${activeBin()}: refusing to write outside target .oh: ` + dest);
|
|
97
359
|
}
|
|
98
|
-
function assertDestInRoot(dest, targetRoot,
|
|
99
|
-
if (dest === targetRoot || dest.startsWith(targetRoot +
|
|
360
|
+
function assertDestInRoot(dest, targetRoot, sep5) {
|
|
361
|
+
if (dest === targetRoot || dest.startsWith(targetRoot + sep5)) {
|
|
100
362
|
return;
|
|
101
363
|
}
|
|
102
|
-
throw new Error(
|
|
364
|
+
throw new Error(`${activeBin()}: refusing to write outside target root: ` + dest);
|
|
103
365
|
}
|
|
104
366
|
function walkFiles(root, dir, acc) {
|
|
105
|
-
const entries =
|
|
367
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
106
368
|
for (const entry of entries) {
|
|
107
369
|
const abs = path2.resolve(dir, entry.name);
|
|
108
|
-
const st =
|
|
370
|
+
const st = lstatSync2(abs);
|
|
109
371
|
if (st.isSymbolicLink()) {
|
|
110
372
|
continue;
|
|
111
373
|
}
|
|
@@ -142,7 +404,7 @@ function copyOhPayload(fromOh, targetOh, manifest, opts, report) {
|
|
|
142
404
|
}
|
|
143
405
|
const dest = path2.join(targetOh, rel);
|
|
144
406
|
assertDestInTarget(dest, targetOh, path2.sep);
|
|
145
|
-
const exists =
|
|
407
|
+
const exists = existsSync2(dest);
|
|
146
408
|
if (exists && skipExisting && !force) {
|
|
147
409
|
report?.("skip-exists", rel);
|
|
148
410
|
skipped++;
|
|
@@ -168,7 +430,7 @@ function copyRootPayload(fromRoot, targetRoot, manifest, opts, report) {
|
|
|
168
430
|
const relpaths = [];
|
|
169
431
|
for (const dir of rootPayloadDirs(manifest)) {
|
|
170
432
|
const abs = path2.join(fromRoot, dir);
|
|
171
|
-
if (!
|
|
433
|
+
if (!existsSync2(abs) || !lstatSync2(abs).isDirectory()) {
|
|
172
434
|
continue;
|
|
173
435
|
}
|
|
174
436
|
walkFiles(fromRoot, abs, relpaths);
|
|
@@ -190,7 +452,7 @@ function copyRootPayload(fromRoot, targetRoot, manifest, opts, report) {
|
|
|
190
452
|
}
|
|
191
453
|
const dest = path2.join(targetRoot, rel);
|
|
192
454
|
assertDestInRoot(dest, targetRoot, path2.sep);
|
|
193
|
-
const exists =
|
|
455
|
+
const exists = existsSync2(dest);
|
|
194
456
|
if (exists && skipExisting && !force) {
|
|
195
457
|
report?.("skip-exists", rel);
|
|
196
458
|
skipped++;
|
|
@@ -232,7 +494,7 @@ function compareVersions(a, b) {
|
|
|
232
494
|
function readCliVersion(ohDir) {
|
|
233
495
|
const pkgPath = path3.resolve(ohDir, "cli", "package.json");
|
|
234
496
|
try {
|
|
235
|
-
const parsed = JSON.parse(
|
|
497
|
+
const parsed = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
236
498
|
if (parsed && typeof parsed.version === "string") {
|
|
237
499
|
return parsed.version;
|
|
238
500
|
}
|
|
@@ -241,46 +503,50 @@ function readCliVersion(ohDir) {
|
|
|
241
503
|
return "0.0.0";
|
|
242
504
|
}
|
|
243
505
|
async function runUpdate(opts, io) {
|
|
244
|
-
const { targetDir, fromDir, force, dryRun } = opts;
|
|
506
|
+
const { bin, targetDir, fromDir, force, dryRun } = opts;
|
|
245
507
|
const dryPrefix = dryRun ? "[dry-run] " : "";
|
|
246
|
-
const
|
|
247
|
-
if (
|
|
508
|
+
const source = resolveControlDir(fromDir);
|
|
509
|
+
if (source.kind === "absent") {
|
|
248
510
|
io.stderr(
|
|
249
|
-
"
|
|
511
|
+
bin + " update: update source not found at " + source.agroPath + " or " + source.legacyPath + ". Pass --from <built-AGRO-checkout> or --from-remote [--ref <ref>].\n"
|
|
250
512
|
);
|
|
251
513
|
return 1;
|
|
252
514
|
}
|
|
253
|
-
const
|
|
515
|
+
const fromOh = source.path;
|
|
516
|
+
const controlName = path3.basename(fromOh);
|
|
517
|
+
const target = resolveControlDir(targetDir);
|
|
518
|
+
const targetOh = target.kind === "absent" ? path3.resolve(targetDir, controlName) : target.path;
|
|
519
|
+
const targetName = path3.basename(targetOh);
|
|
254
520
|
if (fromOh === targetOh) {
|
|
255
|
-
io.stderr("
|
|
521
|
+
io.stderr(bin + " update: source and target are the same " + targetName + "; nothing to update.\n");
|
|
256
522
|
return 1;
|
|
257
523
|
}
|
|
258
524
|
const available = readCliVersion(fromOh);
|
|
259
525
|
const current = readCliVersion(targetOh);
|
|
260
526
|
const cmp = compareVersions(available, current);
|
|
261
527
|
if (cmp > 0) {
|
|
262
|
-
io.stdout(dryPrefix + "updating
|
|
528
|
+
io.stdout(dryPrefix + "updating " + targetName + ": " + current + " -> " + available + "\n");
|
|
263
529
|
} else if (cmp === 0) {
|
|
264
530
|
if (!force) {
|
|
265
|
-
io.stdout(dryPrefix + "
|
|
531
|
+
io.stdout(dryPrefix + bin + " update: already up to date (v" + current + ")\n");
|
|
266
532
|
return 0;
|
|
267
533
|
}
|
|
268
|
-
io.stdout(dryPrefix + "
|
|
534
|
+
io.stdout(dryPrefix + bin + " update: re-overlay (v" + current + ", --force)\n");
|
|
269
535
|
} else {
|
|
270
536
|
if (!force) {
|
|
271
537
|
io.stderr(
|
|
272
|
-
"
|
|
538
|
+
bin + " update: refusing downgrade (current v" + current + " > source v" + available + "); pass --force to override\n"
|
|
273
539
|
);
|
|
274
540
|
return 1;
|
|
275
541
|
}
|
|
276
542
|
io.stdout(
|
|
277
|
-
dryPrefix + "
|
|
543
|
+
dryPrefix + bin + " update: downgrading " + targetName + ": " + current + " -> " + available + " (--force)\n"
|
|
278
544
|
);
|
|
279
545
|
}
|
|
280
546
|
const manifest = loadManifest(fromOh);
|
|
281
547
|
if (manifest === null) {
|
|
282
548
|
io.stdout(
|
|
283
|
-
dryPrefix + "
|
|
549
|
+
dryPrefix + bin + " update: no " + controlName + "/manifest.json in source; overlaying all of " + controlName + "/ (legacy mode)\n"
|
|
284
550
|
);
|
|
285
551
|
}
|
|
286
552
|
let created = 0;
|
|
@@ -315,15 +581,15 @@ async function runUpdate(opts, io) {
|
|
|
315
581
|
report
|
|
316
582
|
);
|
|
317
583
|
for (const dir of rootPayloadDirs(manifest ?? { include: [], exclude: [] })) {
|
|
318
|
-
const stranded = path3.
|
|
319
|
-
if (
|
|
584
|
+
const stranded = path3.join(targetOh, dir);
|
|
585
|
+
if (existsSync3(stranded)) {
|
|
320
586
|
io.stdout(
|
|
321
|
-
dryPrefix + "
|
|
587
|
+
dryPrefix + bin + " 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"
|
|
322
588
|
);
|
|
323
589
|
}
|
|
324
590
|
}
|
|
325
591
|
io.stdout(
|
|
326
|
-
dryPrefix + "
|
|
592
|
+
dryPrefix + bin + " update: " + created + " created, " + overwritten + " overwritten, " + (skipped + rootResult.skipped) + " skipped\n"
|
|
327
593
|
);
|
|
328
594
|
return 0;
|
|
329
595
|
}
|
|
@@ -336,232 +602,12 @@ import {
|
|
|
336
602
|
readFileSync as readFileSync4,
|
|
337
603
|
realpathSync,
|
|
338
604
|
renameSync,
|
|
339
|
-
statSync as
|
|
605
|
+
statSync as statSync2,
|
|
340
606
|
unlinkSync,
|
|
341
607
|
writeFileSync
|
|
342
608
|
} from "node:fs";
|
|
343
609
|
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
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
function envValue(env, key) {
|
|
490
|
-
const value = env[key];
|
|
491
|
-
return value !== void 0 && value !== "" ? value : void 0;
|
|
492
|
-
}
|
|
493
|
-
function resolveAliasedEnv(env, suffix) {
|
|
494
|
-
const agroKey = `${GENERATIONS.agro.envPrefix}${suffix}`;
|
|
495
|
-
const legacyKey = `${GENERATIONS.legacy.envPrefix}${suffix}`;
|
|
496
|
-
const agro = envValue(env, agroKey);
|
|
497
|
-
const legacy = envValue(env, legacyKey);
|
|
498
|
-
const conflict = agro !== void 0 && legacy !== void 0 && agro !== legacy;
|
|
499
|
-
if (agro !== void 0) {
|
|
500
|
-
return { key: suffix, agroKey, legacyKey, value: agro, source: "agro", conflict };
|
|
501
|
-
}
|
|
502
|
-
if (legacy !== void 0) {
|
|
503
|
-
return { key: suffix, agroKey, legacyKey, value: legacy, source: "legacy", conflict: false };
|
|
504
|
-
}
|
|
505
|
-
return { key: suffix, agroKey, legacyKey, value: void 0, source: "none", conflict: false };
|
|
506
|
-
}
|
|
507
|
-
function aliasConflictWarning(resolved) {
|
|
508
|
-
if (!resolved.conflict) return void 0;
|
|
509
|
-
return `compat: ${resolved.agroKey} and ${resolved.legacyKey} are both set and differ \u2014 using ${resolved.agroKey}`;
|
|
510
|
-
}
|
|
511
|
-
function aliasedEnvValue(env, suffix, warn = (message) => process.stderr.write(`${message}
|
|
512
|
-
`)) {
|
|
513
|
-
const resolved = resolveAliasedEnv(env, suffix);
|
|
514
|
-
const warning = aliasConflictWarning(resolved);
|
|
515
|
-
if (warning !== void 0) warn(warning);
|
|
516
|
-
return resolved.value;
|
|
517
|
-
}
|
|
518
|
-
var REGISTRY_SUBDIR = "sandboxes";
|
|
519
|
-
function discoverUserState(home = homedir()) {
|
|
520
|
-
const probe = (name) => {
|
|
521
|
-
const path4 = join(home, name);
|
|
522
|
-
return { path: path4, exists: isDirectoryAt(path4) };
|
|
523
|
-
};
|
|
524
|
-
return {
|
|
525
|
-
agroHome: probe(GENERATIONS.agro.userStateDir),
|
|
526
|
-
legacyHome: probe(GENERATIONS.legacy.userStateDir),
|
|
527
|
-
legacyCheckout: probe(LEGACY_CHECKOUT_DIR)
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
function resolveUserStateHome(env = process.env, home = homedir(), warn) {
|
|
531
|
-
const configured = aliasedEnvValue(env, "HOME", warn);
|
|
532
|
-
if (configured !== void 0) return resolve(configured);
|
|
533
|
-
const discovered = discoverUserState(home);
|
|
534
|
-
const legacyRegistry = join(discovered.legacyHome.path, REGISTRY_SUBDIR);
|
|
535
|
-
const agroRegistry = join(discovered.agroHome.path, REGISTRY_SUBDIR);
|
|
536
|
-
const resolution = resolvePair(legacyRegistry, agroRegistry, isDirectoryAt);
|
|
537
|
-
return resolution.generation === "agro" ? discovered.agroHome.path : discovered.legacyHome.path;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// src/lib/product.ts
|
|
541
|
-
import { basename } from "node:path";
|
|
542
|
-
var AGRO_PRODUCT = {
|
|
543
|
-
name: "agro",
|
|
544
|
-
bin: "agro",
|
|
545
|
-
title: "AGRO CLI",
|
|
546
|
-
packageName: "@mifune/agro"
|
|
547
|
-
};
|
|
548
|
-
var LEGACY_PRODUCT = {
|
|
549
|
-
name: "oh",
|
|
550
|
-
bin: "oh",
|
|
551
|
-
title: "Open Harness CLI",
|
|
552
|
-
packageName: "@mifune/openharness"
|
|
553
|
-
};
|
|
554
|
-
var FILE_EXTENSION = /\.[^.]*$/;
|
|
555
|
-
function invokedName(argv1) {
|
|
556
|
-
if (argv1 === void 0) return "";
|
|
557
|
-
return basename(argv1.replace(/\\/g, "/")).replace(FILE_EXTENSION, "");
|
|
558
|
-
}
|
|
559
|
-
function resolveProduct(argv1) {
|
|
560
|
-
return invokedName(argv1) === LEGACY_PRODUCT.bin ? LEGACY_PRODUCT : AGRO_PRODUCT;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// src/commands/self-upgrade.ts
|
|
564
|
-
var DEFAULT_ARTIFACT_URL = "https://github.com/mifunedev/openharness/releases/latest/download/agro.js";
|
|
610
|
+
var DEFAULT_ARTIFACT_URL = "https://github.com/mifunedev/agro/releases/latest/download/agro.js";
|
|
565
611
|
var PREFIX = `${AGRO_PRODUCT.bin} update`;
|
|
566
612
|
var PACKAGE = AGRO_PRODUCT.packageName;
|
|
567
613
|
var IMAGE_ROOT = "/opt/oh/";
|
|
@@ -609,9 +655,9 @@ function kindOfTarget(target, deps) {
|
|
|
609
655
|
function classifyInstallation(argv1, deps) {
|
|
610
656
|
const invoked = argv1 ?? "";
|
|
611
657
|
if (invoked === "") return { kind: "unknown", target: "", invoked, reason: "no executable path (argv[1] is empty)" };
|
|
612
|
-
const
|
|
613
|
-
if (
|
|
614
|
-
return { ...kindOfTarget(
|
|
658
|
+
const resolved2 = resolveTarget(invoked, deps);
|
|
659
|
+
if (resolved2.reason !== void 0) return { kind: "unknown", target: resolved2.target, invoked, reason: resolved2.reason };
|
|
660
|
+
return { ...kindOfTarget(resolved2.target, deps), target: resolved2.target, invoked };
|
|
615
661
|
}
|
|
616
662
|
function parseSemver(input) {
|
|
617
663
|
const m = SEMVER.exec(input.trim());
|
|
@@ -658,16 +704,16 @@ function compareSemver(a, b) {
|
|
|
658
704
|
function versionOf(result) {
|
|
659
705
|
return result.status === 0 ? result.stdout.trim() : "";
|
|
660
706
|
}
|
|
661
|
-
function refuseUnsupported(installation) {
|
|
707
|
+
function refuseUnsupported(installation, bin) {
|
|
662
708
|
const { kind, target, reason } = installation;
|
|
663
709
|
const at = `${kind} installation at ${target}`;
|
|
664
710
|
switch (kind) {
|
|
665
711
|
case "image":
|
|
666
712
|
return new Error(
|
|
667
|
-
`${at} \u2014 the sandbox image ships this CLI; pull a newer image on the host (
|
|
713
|
+
`${at} \u2014 the sandbox image ships this CLI; pull a newer image on the host (${bin} stop, then ${bin} sandbox install docker --name <name>)`
|
|
668
714
|
);
|
|
669
715
|
case "source":
|
|
670
|
-
return new Error(`${at} \u2014 rebuild from the checkout: npm --prefix .
|
|
716
|
+
return new Error(`${at} \u2014 rebuild from the checkout: npm --prefix .agro/cli run build`);
|
|
671
717
|
case "legacy-package":
|
|
672
718
|
return new Error(`${at} \u2014 install the canonical package: npm install -g ${PACKAGE}`);
|
|
673
719
|
default:
|
|
@@ -679,13 +725,13 @@ function refuseUnsupported(installation) {
|
|
|
679
725
|
function assertOnlyAgroOnPath(installation, deps) {
|
|
680
726
|
const found = deps.which(AGRO_PRODUCT.bin);
|
|
681
727
|
if (found === void 0) return;
|
|
682
|
-
let
|
|
728
|
+
let resolved2;
|
|
683
729
|
try {
|
|
684
|
-
|
|
730
|
+
resolved2 = deps.realpath(found);
|
|
685
731
|
} catch {
|
|
686
|
-
|
|
732
|
+
resolved2 = found;
|
|
687
733
|
}
|
|
688
|
-
if (
|
|
734
|
+
if (resolved2 !== installation.target) {
|
|
689
735
|
throw new Error(
|
|
690
736
|
`another ${AGRO_PRODUCT.bin} is earlier on PATH (${found}); upgrading ${installation.target} would not change what runs \u2014 remove or reorder it first`
|
|
691
737
|
);
|
|
@@ -847,7 +893,7 @@ async function runSelfUpgrade(opts, deps, io) {
|
|
|
847
893
|
try {
|
|
848
894
|
const installation = classifyInstallation(opts.argv1, deps);
|
|
849
895
|
if (installation.kind !== "npm" && installation.kind !== "standalone") {
|
|
850
|
-
throw refuseUnsupported(installation);
|
|
896
|
+
throw refuseUnsupported(installation, resolveProduct(opts.argv1).bin);
|
|
851
897
|
}
|
|
852
898
|
io.stdout(`${PREFIX}: ${installation.kind} installation at ${installation.target}
|
|
853
899
|
`);
|
|
@@ -863,14 +909,14 @@ async function runSelfUpgrade(opts, deps, io) {
|
|
|
863
909
|
}
|
|
864
910
|
function whichOnPath(name, env, platform) {
|
|
865
911
|
const win32 = platform === "win32";
|
|
866
|
-
const
|
|
867
|
-
const dirs = (env.PATH ?? "").split(
|
|
912
|
+
const sep5 = win32 ? ";" : delimiter;
|
|
913
|
+
const dirs = (env.PATH ?? "").split(sep5).filter((d) => d !== "");
|
|
868
914
|
const exts = win32 ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")] : [""];
|
|
869
915
|
for (const dir of dirs) {
|
|
870
916
|
for (const ext of exts) {
|
|
871
917
|
const candidate = join2(dir, `${name}${ext}`);
|
|
872
918
|
try {
|
|
873
|
-
if (
|
|
919
|
+
if (statSync2(candidate).isFile()) {
|
|
874
920
|
if (!win32) accessSync(candidate, constants.X_OK);
|
|
875
921
|
return candidate;
|
|
876
922
|
}
|
|
@@ -897,7 +943,7 @@ function defaultDeps(currentVersion) {
|
|
|
897
943
|
return {
|
|
898
944
|
env: process.env,
|
|
899
945
|
realpath: (path4) => realpathSync(path4),
|
|
900
|
-
stat: (path4) =>
|
|
946
|
+
stat: (path4) => statSync2(path4),
|
|
901
947
|
access: (path4, mode) => accessSync(path4, mode),
|
|
902
948
|
readFile: (path4) => readFileSync4(path4),
|
|
903
949
|
writeFile: (path4, data, mode) => writeFileSync(path4, data, { mode }),
|
|
@@ -912,212 +958,614 @@ function defaultDeps(currentVersion) {
|
|
|
912
958
|
};
|
|
913
959
|
}
|
|
914
960
|
|
|
915
|
-
// src/commands/
|
|
916
|
-
import {
|
|
917
|
-
import {
|
|
918
|
-
import { join as join3 } from "node:path";
|
|
919
|
-
|
|
920
|
-
// src/lib/prompt.ts
|
|
921
|
-
import { createInterface } from "node:readline";
|
|
922
|
-
var COLORS = {
|
|
923
|
-
reset: "\x1B[0m",
|
|
924
|
-
bold: "\x1B[1m",
|
|
925
|
-
dim: "\x1B[2m",
|
|
926
|
-
green: "\x1B[32m",
|
|
927
|
-
yellow: "\x1B[33m",
|
|
928
|
-
red: "\x1B[31m",
|
|
929
|
-
cyan: "\x1B[36m"
|
|
930
|
-
};
|
|
931
|
-
function header(msg) {
|
|
932
|
-
process.stdout.write(`
|
|
933
|
-
${COLORS.bold}${msg}${COLORS.reset}
|
|
961
|
+
// src/commands/migrate.ts
|
|
962
|
+
import { lstatSync as lstatSync4 } from "node:fs";
|
|
963
|
+
import { dirname as dirname3, join as join3, resolve as resolve3 } from "node:path";
|
|
934
964
|
|
|
965
|
+
// src/lib/migrate.ts
|
|
966
|
+
import {
|
|
967
|
+
closeSync,
|
|
968
|
+
existsSync as existsSync4,
|
|
969
|
+
fsyncSync,
|
|
970
|
+
lstatSync as lstatSync3,
|
|
971
|
+
mkdirSync as mkdirSync2,
|
|
972
|
+
openSync,
|
|
973
|
+
readFileSync as readFileSync5,
|
|
974
|
+
readlinkSync as readlinkSync2,
|
|
975
|
+
realpathSync as realpathSync2,
|
|
976
|
+
renameSync as renameSync2,
|
|
977
|
+
symlinkSync,
|
|
978
|
+
unlinkSync as unlinkSync2,
|
|
979
|
+
writeFileSync as writeFileSync2
|
|
980
|
+
} from "node:fs";
|
|
981
|
+
import { dirname as dirname2, isAbsolute, relative, resolve as resolve2, sep } from "node:path";
|
|
982
|
+
var MIGRATION_PLAN_VERSION = 1;
|
|
983
|
+
var RETIRED_SUFFIX = ".migrated";
|
|
984
|
+
var LOCK_FILE = ".agro-migrate.lock";
|
|
985
|
+
function snapshot(path4) {
|
|
986
|
+
const stats = lstatSync3(path4, { throwIfNoEntry: false });
|
|
987
|
+
if (stats === void 0) return void 0;
|
|
988
|
+
return {
|
|
989
|
+
type: stats.isSymbolicLink() ? "symlink" : stats.isDirectory() ? "directory" : "file",
|
|
990
|
+
mode: stats.mode & 511,
|
|
991
|
+
ino: stats.ino,
|
|
992
|
+
mtimeMs: stats.mtimeMs,
|
|
993
|
+
size: stats.size
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
function presentAs(path4, kind) {
|
|
997
|
+
let stats;
|
|
998
|
+
try {
|
|
999
|
+
stats = lstatSync3(path4, { throwIfNoEntry: false });
|
|
1000
|
+
} catch {
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
if (stats === void 0) return false;
|
|
1004
|
+
if (stats.isSymbolicLink()) return true;
|
|
1005
|
+
return kind === "dir" ? stats.isDirectory() : stats.isFile();
|
|
1006
|
+
}
|
|
1007
|
+
function nearestExistingAncestor(path4) {
|
|
1008
|
+
let cursor = path4;
|
|
1009
|
+
while (!existsSync4(cursor)) {
|
|
1010
|
+
const parent = dirname2(cursor);
|
|
1011
|
+
if (parent === cursor) return cursor;
|
|
1012
|
+
cursor = parent;
|
|
1013
|
+
}
|
|
1014
|
+
return cursor;
|
|
1015
|
+
}
|
|
1016
|
+
function insideRoot(root, candidate) {
|
|
1017
|
+
const realRoot = realpathSync2(root);
|
|
1018
|
+
const anchor = nearestExistingAncestor(dirname2(candidate));
|
|
1019
|
+
const realAnchor = realpathSync2(anchor);
|
|
1020
|
+
const rel = relative(realRoot, realAnchor);
|
|
1021
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
1022
|
+
}
|
|
1023
|
+
function assertInsideRoot(root, path4, label) {
|
|
1024
|
+
if (!isAbsolute(path4)) throw new Error(`migration ${label} must be absolute: ${path4}`);
|
|
1025
|
+
const absolute = resolve2(path4);
|
|
1026
|
+
if (!insideRoot(root, absolute)) {
|
|
1027
|
+
throw new Error(`refusing migration ${label} outside ${root}: ${absolute}`);
|
|
1028
|
+
}
|
|
1029
|
+
return absolute;
|
|
1030
|
+
}
|
|
1031
|
+
function sameSnapshot(a, b) {
|
|
1032
|
+
return b !== void 0 && a.type === b.type && a.mode === b.mode && a.ino === b.ino && a.mtimeMs === b.mtimeMs && a.size === b.size;
|
|
1033
|
+
}
|
|
1034
|
+
function planPair(root, pair) {
|
|
1035
|
+
const legacy = assertInsideRoot(root, pair.legacy, "source");
|
|
1036
|
+
const agro = assertInsideRoot(root, pair.agro, "destination");
|
|
1037
|
+
const legacyPresent = presentAs(legacy, pair.kind);
|
|
1038
|
+
const agroPresent = presentAs(agro, pair.kind);
|
|
1039
|
+
if (!legacyPresent) {
|
|
1040
|
+
return { kind: "noop", path: agro, reason: agroPresent ? "already migrated" : "absent in both generations" };
|
|
1041
|
+
}
|
|
1042
|
+
if (!agroPresent) {
|
|
1043
|
+
if (lstatSync3(agro, { throwIfNoEntry: false }) !== void 0) {
|
|
1044
|
+
return { path: agro, reason: `destination exists but is not a ${pair.kind}` };
|
|
1045
|
+
}
|
|
1046
|
+
return {
|
|
1047
|
+
kind: "rename",
|
|
1048
|
+
from: legacy,
|
|
1049
|
+
to: agro,
|
|
1050
|
+
snapshot: snapshot(legacy),
|
|
1051
|
+
createsParent: !existsSync4(dirname2(agro))
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
const differences = compareTrees(legacy, agro);
|
|
1055
|
+
if (differences.length > 0) {
|
|
1056
|
+
return { path: agro, reason: "both generations exist and differ", differences };
|
|
1057
|
+
}
|
|
1058
|
+
const retired = `${legacy}${RETIRED_SUFFIX}`;
|
|
1059
|
+
if (lstatSync3(retired, { throwIfNoEntry: false }) !== void 0) {
|
|
1060
|
+
return { path: retired, reason: "retired copy already exists" };
|
|
1061
|
+
}
|
|
1062
|
+
return { kind: "retire", from: legacy, to: retired, snapshot: snapshot(legacy) };
|
|
1063
|
+
}
|
|
1064
|
+
function planRewrite(root, rewrite) {
|
|
1065
|
+
const path4 = assertInsideRoot(root, rewrite.path, "rewrite target");
|
|
1066
|
+
const current = snapshot(path4);
|
|
1067
|
+
if (current === void 0 || current.type !== "file") {
|
|
1068
|
+
return { kind: "noop", path: path4, reason: "rewrite target is not a regular file" };
|
|
1069
|
+
}
|
|
1070
|
+
const content = readFileSync5(path4, "utf8");
|
|
1071
|
+
const changed = rewrite.replacements.some((r) => r.from !== "" && content.includes(r.from));
|
|
1072
|
+
if (!changed) return { kind: "noop", path: path4, reason: "rewrite already applied" };
|
|
1073
|
+
return { kind: "rewrite", path: path4, replacements: rewrite.replacements, snapshot: current };
|
|
1074
|
+
}
|
|
1075
|
+
function planRelink(root, relink) {
|
|
1076
|
+
const path4 = assertInsideRoot(root, relink.path, "relink target");
|
|
1077
|
+
const current = snapshot(path4);
|
|
1078
|
+
if (current === void 0) return { kind: "noop", path: path4, reason: "link absent" };
|
|
1079
|
+
if (current.type !== "symlink") return { kind: "noop", path: path4, reason: "not a symlink" };
|
|
1080
|
+
const target = readlinkSync2(path4);
|
|
1081
|
+
if (target === relink.to) return { kind: "noop", path: path4, reason: "link already points at the AGRO target" };
|
|
1082
|
+
if (target !== relink.from) return { kind: "noop", path: path4, reason: `link points at ${target}, not ${relink.from}` };
|
|
1083
|
+
return { kind: "relink", path: path4, from: relink.from, to: relink.to, snapshot: current };
|
|
1084
|
+
}
|
|
1085
|
+
function resolved(path4) {
|
|
1086
|
+
try {
|
|
1087
|
+
return realpathSync2(path4);
|
|
1088
|
+
} catch {
|
|
1089
|
+
return void 0;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function resolvesToPack(path4, packs) {
|
|
1093
|
+
const target = resolved(path4);
|
|
1094
|
+
if (target === void 0) return false;
|
|
1095
|
+
return packs.some((pack) => resolved(pack) === target);
|
|
1096
|
+
}
|
|
1097
|
+
function replacementSurvives(retire) {
|
|
1098
|
+
const current = snapshot(retire.replacement);
|
|
1099
|
+
if (current === void 0 || current.type !== "symlink") return false;
|
|
1100
|
+
if (!retire.replacementTargets.includes(readlinkSync2(retire.replacement))) return false;
|
|
1101
|
+
return resolvesToPack(retire.replacement, retire.packs);
|
|
1102
|
+
}
|
|
1103
|
+
function preserveRetiredLink(root, retire, reason) {
|
|
1104
|
+
const step = planRelink(root, { path: retire.path, ...retire.preserve });
|
|
1105
|
+
return step.kind === "relink" ? step : { kind: "noop", path: resolve2(retire.path), reason };
|
|
1106
|
+
}
|
|
1107
|
+
function planRetireLink(root, retire) {
|
|
1108
|
+
const path4 = assertInsideRoot(root, retire.path, "retired link");
|
|
1109
|
+
const current = snapshot(path4);
|
|
1110
|
+
if (current === void 0) return { kind: "noop", path: path4, reason: "link absent" };
|
|
1111
|
+
if (current.type !== "symlink") return { kind: "noop", path: path4, reason: "not a symlink" };
|
|
1112
|
+
if (!resolvesToPack(path4, retire.packs)) {
|
|
1113
|
+
return preserveRetiredLink(root, retire, "link does not resolve to the Open Harness skill pack");
|
|
1114
|
+
}
|
|
1115
|
+
if (!replacementSurvives(retire)) {
|
|
1116
|
+
return preserveRetiredLink(
|
|
1117
|
+
root,
|
|
1118
|
+
retire,
|
|
1119
|
+
`${retire.replacement} is not an independent skill-pack link that survives the migration`
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
const retired = `${path4}${RETIRED_SUFFIX}`;
|
|
1123
|
+
if (lstatSync3(retired, { throwIfNoEntry: false }) !== void 0) {
|
|
1124
|
+
return { path: retired, reason: "retired copy already exists" };
|
|
1125
|
+
}
|
|
1126
|
+
return { kind: "retire", from: path4, to: retired, snapshot: current };
|
|
1127
|
+
}
|
|
1128
|
+
function isStep(entry) {
|
|
1129
|
+
return "kind" in entry;
|
|
1130
|
+
}
|
|
1131
|
+
function planMigration(spec) {
|
|
1132
|
+
const root = resolve2(spec.root);
|
|
1133
|
+
if (!existsSync4(root)) throw new Error(`migration root does not exist: ${root}`);
|
|
1134
|
+
const steps = [];
|
|
1135
|
+
const conflicts = [];
|
|
1136
|
+
for (const pair of spec.pairs) {
|
|
1137
|
+
const planned = planPair(root, pair);
|
|
1138
|
+
if (isStep(planned)) steps.push(planned);
|
|
1139
|
+
else conflicts.push(planned);
|
|
1140
|
+
}
|
|
1141
|
+
for (const retired of spec.retireLinks ?? []) {
|
|
1142
|
+
const planned = planRetireLink(root, retired);
|
|
1143
|
+
if (isStep(planned)) steps.push(planned);
|
|
1144
|
+
else conflicts.push(planned);
|
|
1145
|
+
}
|
|
1146
|
+
for (const rewrite of spec.rewrites ?? []) steps.push(planRewrite(root, rewrite));
|
|
1147
|
+
for (const relink of spec.relinks ?? []) steps.push(planRelink(root, relink));
|
|
1148
|
+
const mutating = steps.some((step) => step.kind !== "noop");
|
|
1149
|
+
const status = conflicts.length > 0 ? "conflict" : mutating ? "ready" : "noop";
|
|
1150
|
+
return { version: MIGRATION_PLAN_VERSION, root, status, steps, conflicts };
|
|
1151
|
+
}
|
|
1152
|
+
function acquireLock(root) {
|
|
1153
|
+
const lock = resolve2(root, LOCK_FILE);
|
|
1154
|
+
let fd;
|
|
1155
|
+
try {
|
|
1156
|
+
fd = openSync(lock, "wx", 384);
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
const code = error.code;
|
|
1159
|
+
if (code === "EEXIST") return void 0;
|
|
1160
|
+
throw error;
|
|
1161
|
+
}
|
|
1162
|
+
writeFileSync2(fd, `${process.pid}
|
|
935
1163
|
`);
|
|
1164
|
+
closeSync(fd);
|
|
1165
|
+
return () => {
|
|
1166
|
+
try {
|
|
1167
|
+
unlinkSync2(lock);
|
|
1168
|
+
} catch {
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
936
1171
|
}
|
|
937
|
-
function
|
|
938
|
-
return `${
|
|
1172
|
+
function revalidateInPlace(step) {
|
|
1173
|
+
if (!sameSnapshot(step.snapshot, snapshot(step.path))) return `${step.path} changed since the plan was made`;
|
|
1174
|
+
if (step.kind === "relink" && readlinkSync2(step.path) !== step.from) {
|
|
1175
|
+
return `${step.path} changed since the plan was made`;
|
|
1176
|
+
}
|
|
1177
|
+
return void 0;
|
|
939
1178
|
}
|
|
940
|
-
function
|
|
941
|
-
if (
|
|
942
|
-
|
|
1179
|
+
function revalidate(step) {
|
|
1180
|
+
if (step.kind === "noop") return void 0;
|
|
1181
|
+
if (step.kind === "rewrite" || step.kind === "relink") return revalidateInPlace(step);
|
|
1182
|
+
if (!sameSnapshot(step.snapshot, snapshot(step.from))) {
|
|
1183
|
+
return `${step.from} changed since the plan was made`;
|
|
1184
|
+
}
|
|
1185
|
+
if (lstatSync3(step.to, { throwIfNoEntry: false }) !== void 0) {
|
|
1186
|
+
return `${step.to} appeared since the plan was made`;
|
|
1187
|
+
}
|
|
1188
|
+
return void 0;
|
|
943
1189
|
}
|
|
944
|
-
|
|
945
|
-
|
|
1190
|
+
function applyRewrite(step) {
|
|
1191
|
+
let content = readFileSync5(step.path, "utf8");
|
|
1192
|
+
for (const { from, to } of step.replacements) {
|
|
1193
|
+
if (from === "") continue;
|
|
1194
|
+
content = content.split(from).join(to);
|
|
1195
|
+
}
|
|
1196
|
+
const tmp = `${step.path}.tmp.${process.pid}`;
|
|
1197
|
+
const fd = openSync(tmp, "w", step.snapshot.mode);
|
|
946
1198
|
try {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
});
|
|
1199
|
+
writeFileSync2(fd, content, "utf8");
|
|
1200
|
+
fsyncSync(fd);
|
|
950
1201
|
} finally {
|
|
951
|
-
|
|
1202
|
+
closeSync(fd);
|
|
952
1203
|
}
|
|
1204
|
+
renameSync2(tmp, step.path);
|
|
953
1205
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
process.stdin.setRawMode(false);
|
|
964
|
-
} catch {
|
|
965
|
-
}
|
|
966
|
-
process.stdin.pause();
|
|
967
|
-
process.stdin.removeListener("data", onData);
|
|
968
|
-
};
|
|
969
|
-
const onData = (data) => {
|
|
970
|
-
for (const byte of data) {
|
|
971
|
-
if (byte === 3) {
|
|
972
|
-
cleanup();
|
|
973
|
-
process.stdout.write("\n");
|
|
974
|
-
process.exit(130);
|
|
975
|
-
} else if (byte === 13 || byte === 10) {
|
|
976
|
-
cleanup();
|
|
977
|
-
process.stdout.write("\n");
|
|
978
|
-
resolve9(Buffer.from(bytes).toString("utf8").trim());
|
|
979
|
-
return;
|
|
980
|
-
} else if (byte === 127 || byte === 8) {
|
|
981
|
-
if (bytes.length > 0) {
|
|
982
|
-
bytes.pop();
|
|
983
|
-
process.stdout.write("\b \b");
|
|
984
|
-
}
|
|
985
|
-
} else if (byte === 21) {
|
|
986
|
-
while (bytes.length > 0) {
|
|
987
|
-
bytes.pop();
|
|
988
|
-
process.stdout.write("\b \b");
|
|
989
|
-
}
|
|
990
|
-
} else if (byte >= 32) {
|
|
991
|
-
bytes.push(byte);
|
|
992
|
-
process.stdout.write("\u25CF");
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
};
|
|
996
|
-
process.stdin.on("data", onData);
|
|
997
|
-
});
|
|
1206
|
+
function applyRelink(step) {
|
|
1207
|
+
const tmp = `${step.path}.tmp.${process.pid}`;
|
|
1208
|
+
symlinkSync(step.to, tmp);
|
|
1209
|
+
try {
|
|
1210
|
+
renameSync2(tmp, step.path);
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
unlinkSync2(tmp);
|
|
1213
|
+
throw error;
|
|
1214
|
+
}
|
|
998
1215
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1216
|
+
function pendingResults(plan) {
|
|
1217
|
+
return plan.steps.map((step) => ({ ...step, outcome: step.kind === "noop" ? "skipped" : "pending" }));
|
|
1218
|
+
}
|
|
1219
|
+
function refusal(plan) {
|
|
1220
|
+
if (plan.version !== MIGRATION_PLAN_VERSION) return `unsupported plan version ${plan.version}`;
|
|
1221
|
+
if (plan.status === "conflict") {
|
|
1222
|
+
return plan.conflicts.map((c) => `${c.path}: ${c.reason}`).join("; ");
|
|
1223
|
+
}
|
|
1224
|
+
return void 0;
|
|
1225
|
+
}
|
|
1226
|
+
function revalidatePlan(root, plan) {
|
|
1227
|
+
for (const step of plan.steps) {
|
|
1228
|
+
const stale = revalidate(step);
|
|
1229
|
+
if (stale !== void 0) return stale;
|
|
1230
|
+
if (step.kind === "rename" || step.kind === "retire") {
|
|
1231
|
+
assertInsideRoot(root, step.from, "source");
|
|
1232
|
+
assertInsideRoot(root, step.to, "destination");
|
|
1007
1233
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1234
|
+
}
|
|
1235
|
+
return void 0;
|
|
1236
|
+
}
|
|
1237
|
+
function applyStep(step) {
|
|
1238
|
+
if (step.kind === "noop") return;
|
|
1239
|
+
if (step.kind === "rewrite") {
|
|
1240
|
+
applyRewrite(step);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
if (step.kind === "relink") {
|
|
1244
|
+
applyRelink(step);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
if (step.kind === "rename" && step.createsParent) mkdirSync2(dirname2(step.to), { recursive: true });
|
|
1248
|
+
renameSync2(step.from, step.to);
|
|
1249
|
+
}
|
|
1250
|
+
function applySteps(root, plan) {
|
|
1251
|
+
const results = pendingResults(plan);
|
|
1252
|
+
for (let i = 0; i < plan.steps.length; i += 1) {
|
|
1253
|
+
const step = plan.steps[i];
|
|
1254
|
+
if (step.kind === "noop") continue;
|
|
1016
1255
|
try {
|
|
1017
|
-
|
|
1018
|
-
|
|
1256
|
+
applyStep(step);
|
|
1257
|
+
results[i] = { ...step, outcome: "done" };
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
results[i] = { ...step, outcome: "failed", error: error instanceof Error ? error.message : String(error) };
|
|
1260
|
+
return {
|
|
1261
|
+
status: "failed",
|
|
1262
|
+
root,
|
|
1263
|
+
reason: `step ${i + 1} failed; completed steps are recoverable by reversing them in order`,
|
|
1264
|
+
steps: results
|
|
1265
|
+
};
|
|
1019
1266
|
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1267
|
+
}
|
|
1268
|
+
return { status: "applied", root, steps: results };
|
|
1269
|
+
}
|
|
1270
|
+
function applyMigration(plan) {
|
|
1271
|
+
const root = resolve2(plan.root);
|
|
1272
|
+
const refused = refusal(plan);
|
|
1273
|
+
if (refused !== void 0) return { status: "refused", root, reason: refused, steps: pendingResults(plan) };
|
|
1274
|
+
if (plan.status === "noop") return { status: "noop", root, steps: pendingResults(plan) };
|
|
1275
|
+
const release = acquireLock(root);
|
|
1276
|
+
if (release === void 0) {
|
|
1277
|
+
return {
|
|
1278
|
+
status: "refused",
|
|
1279
|
+
root,
|
|
1280
|
+
reason: `another migration holds ${resolve2(root, LOCK_FILE)}`,
|
|
1281
|
+
steps: pendingResults(plan)
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1027
1284
|
try {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
resolve9(answer.trim());
|
|
1032
|
-
});
|
|
1033
|
-
});
|
|
1285
|
+
const stale = revalidatePlan(root, plan);
|
|
1286
|
+
if (stale !== void 0) return { status: "refused", root, reason: stale, steps: pendingResults(plan) };
|
|
1287
|
+
return applySteps(root, plan);
|
|
1034
1288
|
} finally {
|
|
1035
|
-
|
|
1036
|
-
restore();
|
|
1289
|
+
release();
|
|
1037
1290
|
}
|
|
1038
1291
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1292
|
+
var PROVIDER_LINKS = [
|
|
1293
|
+
{ link: ".claude/skills", target: "skills" },
|
|
1294
|
+
{ link: ".claude/hooks", target: "hooks" },
|
|
1295
|
+
{ link: ".agents/skills", target: "skills" }
|
|
1296
|
+
];
|
|
1297
|
+
var CANONICAL_SKILL_PACKS = [".oh/skills", ".agro/skills"];
|
|
1298
|
+
var SURVIVING_SKILL_LINK_TARGETS = ["../.oh/skills", "../.agro/skills"];
|
|
1299
|
+
var RETIRED_PROVIDER_LINKS = [
|
|
1300
|
+
{
|
|
1301
|
+
link: ".pi/skills",
|
|
1302
|
+
replacement: ".agents/skills",
|
|
1303
|
+
preserve: { from: "../.oh/skills", to: "../.agro/skills" }
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
link: ".codex/skills",
|
|
1307
|
+
replacement: ".agents/skills",
|
|
1308
|
+
preserve: { from: "../.oh/skills", to: "../.agro/skills" }
|
|
1309
|
+
}
|
|
1310
|
+
];
|
|
1311
|
+
function providerRelinks(root) {
|
|
1312
|
+
const dir = resolve2(root);
|
|
1313
|
+
return PROVIDER_LINKS.map(({ link, target }) => ({
|
|
1314
|
+
path: resolve2(dir, ...link.split("/")),
|
|
1315
|
+
from: `../.oh/${target}`,
|
|
1316
|
+
to: `../.agro/${target}`
|
|
1317
|
+
}));
|
|
1318
|
+
}
|
|
1319
|
+
function retiredProviderLinks(root) {
|
|
1320
|
+
const dir = resolve2(root);
|
|
1321
|
+
const packs = CANONICAL_SKILL_PACKS.map((pack) => resolve2(dir, ...pack.split("/")));
|
|
1322
|
+
return RETIRED_PROVIDER_LINKS.map(({ link, replacement, preserve }) => ({
|
|
1323
|
+
path: resolve2(dir, ...link.split("/")),
|
|
1324
|
+
packs,
|
|
1325
|
+
replacement: resolve2(dir, ...replacement.split("/")),
|
|
1326
|
+
replacementTargets: [...SURVIVING_SKILL_LINK_TARGETS],
|
|
1327
|
+
preserve
|
|
1328
|
+
}));
|
|
1329
|
+
}
|
|
1330
|
+
function projectMigrationSpec(root) {
|
|
1331
|
+
const dir = resolve2(root);
|
|
1332
|
+
return {
|
|
1333
|
+
root: dir,
|
|
1334
|
+
pairs: [
|
|
1335
|
+
{ legacy: `${dir}${sep}.oh`, agro: `${dir}${sep}.agro`, kind: "dir" },
|
|
1336
|
+
{ legacy: `${dir}${sep}oh.json`, agro: `${dir}${sep}agro.json`, kind: "file" }
|
|
1337
|
+
],
|
|
1338
|
+
retireLinks: retiredProviderLinks(dir),
|
|
1339
|
+
relinks: providerRelinks(dir)
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
function userStateMigrationSpec(home) {
|
|
1343
|
+
const dir = resolve2(home);
|
|
1344
|
+
return {
|
|
1345
|
+
root: dir,
|
|
1346
|
+
pairs: [
|
|
1347
|
+
{
|
|
1348
|
+
legacy: `${dir}${sep}.oh${sep}sandboxes`,
|
|
1349
|
+
agro: `${dir}${sep}.agro${sep}sandboxes`,
|
|
1350
|
+
kind: "dir"
|
|
1351
|
+
}
|
|
1352
|
+
]
|
|
1353
|
+
};
|
|
1044
1354
|
}
|
|
1045
1355
|
|
|
1046
|
-
// src/
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
}
|
|
1356
|
+
// src/commands/migrate.ts
|
|
1357
|
+
var ROOT_MARKERS = [".oh", ".agro", "oh.json", "agro.json"];
|
|
1358
|
+
var HELP_FLAGS = ["-h", "--help", "help"];
|
|
1359
|
+
function defaultMigrateDeps() {
|
|
1360
|
+
const registry = resolveRegistryHome();
|
|
1361
|
+
return {
|
|
1362
|
+
cwd: () => process.cwd(),
|
|
1363
|
+
home: () => registry.path,
|
|
1364
|
+
homeConfigured: () => registry.configured
|
|
1365
|
+
};
|
|
1057
1366
|
}
|
|
1058
|
-
function
|
|
1059
|
-
|
|
1060
|
-
|
|
1367
|
+
function parseMigrateArgs(argv, bin) {
|
|
1368
|
+
const args = { bin, help: false, check: false, home: false, json: false };
|
|
1369
|
+
for (const arg of argv) {
|
|
1370
|
+
if (HELP_FLAGS.includes(arg)) return { ok: true, args: { ...args, help: true } };
|
|
1371
|
+
if (arg === "--check") args.check = true;
|
|
1372
|
+
else if (arg === "--home") args.home = true;
|
|
1373
|
+
else if (arg === "--json") args.json = true;
|
|
1374
|
+
else return { ok: false, error: `${bin} migrate: unexpected argument "${arg}"`, showHelp: true };
|
|
1061
1375
|
}
|
|
1062
|
-
return
|
|
1376
|
+
return { ok: true, args };
|
|
1063
1377
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1378
|
+
function migrateHelpText(bin) {
|
|
1379
|
+
return `${bin} migrate \u2014 Move a legacy .oh/ project or ~/.oh registry to AGRO names
|
|
1380
|
+
|
|
1381
|
+
Usage:
|
|
1382
|
+
${bin} migrate [--check] [--home] [--json]
|
|
1383
|
+
|
|
1384
|
+
Project mode (default) finds the nearest ancestor of the current directory that
|
|
1385
|
+
holds .oh/, .agro/, oh.json or agro.json and renames .oh/ -> .agro/ and
|
|
1386
|
+
oh.json -> agro.json wholesale. Provider links (.claude/skills, .claude/hooks,
|
|
1387
|
+
.agents/skills) that point at ../.oh/... are re-pointed at ../.agro/....
|
|
1388
|
+
Retired links (.pi/skills, .codex/skills) move to <path>.migrated only when each
|
|
1389
|
+
resolves to the Open Harness skill pack and .agents/skills independently links
|
|
1390
|
+
to that pack. Otherwise each retired link stays in place, re-pointed at
|
|
1391
|
+
../.agro/skills when it named the legacy pack. Other links stay unchanged.
|
|
1392
|
+
|
|
1393
|
+
Flags:
|
|
1394
|
+
--check Print the plan and change nothing.
|
|
1395
|
+
--home Migrate the sandbox registry ~/.oh/sandboxes -> ~/.agro/sandboxes instead.
|
|
1396
|
+
--json Emit the plan (--check) or {plan, result} as JSON on stdout.
|
|
1397
|
+
|
|
1398
|
+
Byte-identical legacy and AGRO copies retire the legacy one to <name>.migrated;
|
|
1399
|
+
divergent copies are refused and nothing is merged or deleted. ~/.openharness,
|
|
1400
|
+
.env and .git are never touched.
|
|
1401
|
+
|
|
1402
|
+
Exit codes: 0 applied or nothing to do, 2 refused (conflict or lock), 1 failure.
|
|
1403
|
+
`;
|
|
1075
1404
|
}
|
|
1076
|
-
function
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1405
|
+
function printMigrateHelp(bin) {
|
|
1406
|
+
process.stdout.write(migrateHelpText(bin));
|
|
1407
|
+
}
|
|
1408
|
+
function hasMarker(dir) {
|
|
1409
|
+
return ROOT_MARKERS.some((marker) => lstatSync4(join3(dir, marker), { throwIfNoEntry: false }) !== void 0);
|
|
1410
|
+
}
|
|
1411
|
+
function findProjectRoot(cwd) {
|
|
1412
|
+
const start = resolve3(cwd);
|
|
1413
|
+
let cursor = start;
|
|
1414
|
+
while (true) {
|
|
1415
|
+
if (hasMarker(cursor)) return cursor;
|
|
1416
|
+
const parent = dirname3(cursor);
|
|
1417
|
+
if (parent === cursor) return start;
|
|
1418
|
+
cursor = parent;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function specFor(args, deps) {
|
|
1422
|
+
return args.home ? userStateMigrationSpec(deps.home()) : projectMigrationSpec(findProjectRoot(deps.cwd()));
|
|
1423
|
+
}
|
|
1424
|
+
function describeStep(step) {
|
|
1425
|
+
switch (step.kind) {
|
|
1426
|
+
case "rename":
|
|
1427
|
+
return `rename ${step.from} -> ${step.to}`;
|
|
1428
|
+
case "retire":
|
|
1429
|
+
return `retire ${step.from} -> ${step.to}`;
|
|
1430
|
+
case "rewrite":
|
|
1431
|
+
return `rewrite ${step.path} (${step.replacements.length} replacement${step.replacements.length === 1 ? "" : "s"})`;
|
|
1432
|
+
case "relink":
|
|
1433
|
+
return `relink ${step.path}: ${step.from} -> ${step.to}`;
|
|
1434
|
+
case "noop":
|
|
1435
|
+
return `noop ${step.path} (${step.reason})`;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
function describeConflict(conflict) {
|
|
1439
|
+
const lines = [` ${conflict.path}: ${conflict.reason}`];
|
|
1440
|
+
for (const difference of conflict.differences ?? []) lines.push(` - ${difference}`);
|
|
1441
|
+
return lines;
|
|
1442
|
+
}
|
|
1443
|
+
function planSummary(bin, plan) {
|
|
1444
|
+
const lines = [`${bin} migrate: plan for ${plan.root}`];
|
|
1445
|
+
for (const step of plan.steps) lines.push(` ${describeStep(step)}`);
|
|
1446
|
+
if (plan.conflicts.length > 0) {
|
|
1447
|
+
lines.push("conflicts:");
|
|
1448
|
+
for (const conflict of plan.conflicts) lines.push(...describeConflict(conflict));
|
|
1449
|
+
}
|
|
1450
|
+
lines.push(`status: ${plan.status}`);
|
|
1451
|
+
return `${lines.join("\n")}
|
|
1452
|
+
`;
|
|
1088
1453
|
}
|
|
1089
|
-
function
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1454
|
+
function describeOutcome(step) {
|
|
1455
|
+
const suffix = step.error === void 0 ? "" : ` \u2014 ${step.error}`;
|
|
1456
|
+
return ` ${step.outcome.padEnd(7)} ${describeStep(step)}${suffix}`;
|
|
1457
|
+
}
|
|
1458
|
+
function resultSummary(bin, result) {
|
|
1459
|
+
const lines = [`${bin} migrate: ${result.status}${result.reason === void 0 ? "" : ` \u2014 ${result.reason}`}`];
|
|
1460
|
+
for (const step of result.steps) lines.push(describeOutcome(step));
|
|
1461
|
+
return `${lines.join("\n")}
|
|
1462
|
+
`;
|
|
1463
|
+
}
|
|
1464
|
+
function planExitCode(plan) {
|
|
1465
|
+
return plan.status === "conflict" ? 2 : 0;
|
|
1466
|
+
}
|
|
1467
|
+
function resultExitCode(result) {
|
|
1468
|
+
if (result.status === "applied" || result.status === "noop") return 0;
|
|
1469
|
+
return result.status === "refused" ? 2 : 1;
|
|
1470
|
+
}
|
|
1471
|
+
function emitJson(io, value) {
|
|
1472
|
+
io.stdout(`${JSON.stringify(value, null, 2)}
|
|
1473
|
+
`);
|
|
1474
|
+
}
|
|
1475
|
+
function check(args, plan, io) {
|
|
1476
|
+
if (args.json) emitJson(io, plan);
|
|
1477
|
+
else io.stdout(planSummary(args.bin, plan));
|
|
1478
|
+
return planExitCode(plan);
|
|
1479
|
+
}
|
|
1480
|
+
function apply(args, plan, io) {
|
|
1481
|
+
const result = applyMigration(plan);
|
|
1482
|
+
if (args.json) {
|
|
1483
|
+
emitJson(io, { plan, result });
|
|
1484
|
+
} else {
|
|
1485
|
+
io.stdout(planSummary(args.bin, plan));
|
|
1486
|
+
io.stdout(resultSummary(args.bin, result));
|
|
1100
1487
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1488
|
+
return resultExitCode(result);
|
|
1489
|
+
}
|
|
1490
|
+
function runMigrate(args, io, deps = defaultMigrateDeps()) {
|
|
1491
|
+
try {
|
|
1492
|
+
if (args.home && deps.homeConfigured?.() === true) {
|
|
1493
|
+
io.stdout(
|
|
1494
|
+
`${args.bin} migrate: the registry home is set explicitly to ${deps.home()}, so it holds no .oh or .agro directory to rename
|
|
1495
|
+
`
|
|
1496
|
+
);
|
|
1497
|
+
io.stdout(`${args.bin} migrate: noop
|
|
1498
|
+
`);
|
|
1499
|
+
return 0;
|
|
1105
1500
|
}
|
|
1501
|
+
const plan = planMigration(specFor(args, deps));
|
|
1502
|
+
return args.check ? check(args, plan, io) : apply(args, plan, io);
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1505
|
+
io.stderr(`${args.bin} migrate: ${message}
|
|
1506
|
+
`);
|
|
1507
|
+
return 1;
|
|
1106
1508
|
}
|
|
1107
|
-
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// src/lib/execution/runner.ts
|
|
1512
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1513
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
1514
|
+
import { basename as basename3, join as join4 } from "node:path";
|
|
1515
|
+
var spawnRunner = (cmd, args, opts) => {
|
|
1516
|
+
const common = {
|
|
1517
|
+
env: opts.env,
|
|
1518
|
+
...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
|
|
1519
|
+
...opts.timeoutMs !== void 0 ? { timeout: opts.timeoutMs } : {}
|
|
1520
|
+
};
|
|
1521
|
+
const r = opts.stdio === "capture" ? spawnSync2(cmd, args, { ...common, stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" }) : spawnSync2(cmd, args, { ...common, stdio: "inherit" });
|
|
1522
|
+
const err = r.error;
|
|
1108
1523
|
return {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
outcome: "added"
|
|
1524
|
+
status: r.status,
|
|
1525
|
+
error: err ? { code: err.code, message: err.message } : void 0,
|
|
1526
|
+
stdout: typeof r.stdout === "string" ? r.stdout : void 0,
|
|
1527
|
+
stderr: typeof r.stderr === "string" ? r.stderr : void 0
|
|
1114
1528
|
};
|
|
1529
|
+
};
|
|
1530
|
+
var ExecutionSpawnError = class extends Error {
|
|
1531
|
+
code;
|
|
1532
|
+
constructor(what, error) {
|
|
1533
|
+
super(`failed to run ${what}${error?.message ? ` (${error.message})` : ""}`);
|
|
1534
|
+
this.name = "ExecutionSpawnError";
|
|
1535
|
+
this.code = error?.code;
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
var ExecutionExitError = class extends Error {
|
|
1539
|
+
exitCode;
|
|
1540
|
+
constructor(what, exitCode) {
|
|
1541
|
+
super(`${what} exited ${exitCode}`);
|
|
1542
|
+
this.name = "ExecutionExitError";
|
|
1543
|
+
this.exitCode = exitCode;
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
function assertSpawned(r, what) {
|
|
1547
|
+
if (r.error) {
|
|
1548
|
+
throw new ExecutionSpawnError(what, r.error);
|
|
1549
|
+
}
|
|
1115
1550
|
}
|
|
1116
|
-
function
|
|
1117
|
-
|
|
1551
|
+
function requireLifecycleScript(root, rel) {
|
|
1552
|
+
const { controlDir } = resolveProjectLayout(root);
|
|
1553
|
+
const script = join4(controlDir, "scripts", rel);
|
|
1554
|
+
if (!existsSync5(script)) {
|
|
1555
|
+
throw new Error(
|
|
1556
|
+
`missing lifecycle script ${script} \u2014 the vendored ${basename3(controlDir)}/ payload looks incomplete; run \`${activeBin()} update\` to re-vendor it`
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
return script;
|
|
1118
1560
|
}
|
|
1119
1561
|
|
|
1562
|
+
// src/lib/env-file.ts
|
|
1563
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
1564
|
+
import { resolve as resolve5, sep as sep2 } from "node:path";
|
|
1565
|
+
|
|
1120
1566
|
// src/lib/oh-config.ts
|
|
1567
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1568
|
+
import { basename as basename4, dirname as dirname4, resolve as resolve4 } from "node:path";
|
|
1121
1569
|
var OH_CONFIG_FILE = GENERATIONS.legacy.configFile;
|
|
1122
1570
|
var OH_CONFIG_MODE = 420;
|
|
1123
1571
|
var RESERVED_HOME_PATHS = [
|
|
@@ -1146,9 +1594,12 @@ var LANGFUSE_PRIVACY_PRESETS = [
|
|
|
1146
1594
|
"full-debug"
|
|
1147
1595
|
];
|
|
1148
1596
|
var SANDBOX_RUNTIMES = ["docker"];
|
|
1597
|
+
function configCheckout(config) {
|
|
1598
|
+
const value = config.checkout ?? config.repo;
|
|
1599
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
1600
|
+
}
|
|
1149
1601
|
function ohConfigPath(root) {
|
|
1150
|
-
|
|
1151
|
-
return resolved.generation === "agro" ? resolve3(root, GENERATIONS.agro.configFile) : resolve3(root, OH_CONFIG_FILE);
|
|
1602
|
+
return resolveProjectLayout(root).configFile;
|
|
1152
1603
|
}
|
|
1153
1604
|
function defaultOhConfig(name) {
|
|
1154
1605
|
return {
|
|
@@ -1167,16 +1618,15 @@ function defaultOhConfig(name) {
|
|
|
1167
1618
|
cron: { agentBin: "claude" },
|
|
1168
1619
|
build: { skipPnpmInstall: false },
|
|
1169
1620
|
image: { mode: "build", pullPolicy: "missing" },
|
|
1170
|
-
cloud: {},
|
|
1171
1621
|
langfuse: {},
|
|
1172
1622
|
composeOverrides: []
|
|
1173
1623
|
};
|
|
1174
1624
|
}
|
|
1175
1625
|
function readOhConfig(path4) {
|
|
1176
|
-
if (!
|
|
1626
|
+
if (!existsSync6(path4)) return defaultOhConfig(basename4(dirname4(resolve4(path4))));
|
|
1177
1627
|
let raw;
|
|
1178
1628
|
try {
|
|
1179
|
-
raw =
|
|
1629
|
+
raw = readFileSync6(path4, "utf8");
|
|
1180
1630
|
} catch (error) {
|
|
1181
1631
|
const detail = error instanceof Error ? error.message : String(error);
|
|
1182
1632
|
throw new Error(`could not read ${path4}: ${detail}`);
|
|
@@ -1185,24 +1635,24 @@ function readOhConfig(path4) {
|
|
|
1185
1635
|
try {
|
|
1186
1636
|
parsed = JSON.parse(raw);
|
|
1187
1637
|
} catch {
|
|
1188
|
-
throw new Error(`${
|
|
1638
|
+
throw new Error(`${basename4(path4)} is not valid JSON: ${path4}`);
|
|
1189
1639
|
}
|
|
1190
1640
|
return validateOhConfig(parsed);
|
|
1191
1641
|
}
|
|
1192
1642
|
function writeOhConfig(root, config) {
|
|
1193
1643
|
const path4 = ohConfigPath(root);
|
|
1194
|
-
assertInRoot(path4,
|
|
1644
|
+
assertInRoot(path4, resolve4(root));
|
|
1195
1645
|
const validated = validateOhConfig({ ...config, version: 1 });
|
|
1196
1646
|
const body = `${JSON.stringify(validated, null, 2)}
|
|
1197
1647
|
`;
|
|
1198
1648
|
const tmp = `${path4}.tmp.${process.pid}`;
|
|
1199
|
-
|
|
1649
|
+
mkdirSync3(dirname4(path4), { recursive: true });
|
|
1200
1650
|
try {
|
|
1201
|
-
|
|
1202
|
-
|
|
1651
|
+
writeFileSync3(tmp, body, { mode: OH_CONFIG_MODE, encoding: "utf8" });
|
|
1652
|
+
renameSync3(tmp, path4);
|
|
1203
1653
|
} catch (error) {
|
|
1204
1654
|
try {
|
|
1205
|
-
|
|
1655
|
+
unlinkSync3(tmp);
|
|
1206
1656
|
} catch {
|
|
1207
1657
|
}
|
|
1208
1658
|
throw error;
|
|
@@ -1216,6 +1666,7 @@ function validateOhConfig(value) {
|
|
|
1216
1666
|
expectString(record, "name");
|
|
1217
1667
|
expectEnum(record, "runtime", "", SANDBOX_RUNTIMES);
|
|
1218
1668
|
expectString(record, "repo");
|
|
1669
|
+
expectString(record, "checkout");
|
|
1219
1670
|
expectString(record, "timezone");
|
|
1220
1671
|
const storage = expectSection(record, "storage");
|
|
1221
1672
|
if (storage) {
|
|
@@ -1262,8 +1713,6 @@ function validateOhConfig(value) {
|
|
|
1262
1713
|
expectEnum(image, "mode", "image.", ["build", "image"]);
|
|
1263
1714
|
expectEnum(image, "pullPolicy", "image.", ["missing", "always", "never"]);
|
|
1264
1715
|
}
|
|
1265
|
-
const cloud = expectSection(record, "cloud");
|
|
1266
|
-
if (cloud) expectString(cloud, "apiUrl", "cloud.");
|
|
1267
1716
|
const langfuse = expectSection(record, "langfuse");
|
|
1268
1717
|
if (langfuse) {
|
|
1269
1718
|
expectString(langfuse, "baseUrl", "langfuse.");
|
|
@@ -1324,6 +1773,7 @@ var OH_CONFIG_FIELDS = [
|
|
|
1324
1773
|
{ path: "name", type: "string" },
|
|
1325
1774
|
{ path: "runtime", type: "enum", values: SANDBOX_RUNTIMES },
|
|
1326
1775
|
{ path: "repo", type: "string" },
|
|
1776
|
+
{ path: "checkout", type: "string" },
|
|
1327
1777
|
{ path: "timezone", type: "string" },
|
|
1328
1778
|
{ path: "git.userName", type: "string" },
|
|
1329
1779
|
{ path: "git.userEmail", type: "string" },
|
|
@@ -1340,7 +1790,6 @@ var OH_CONFIG_FIELDS = [
|
|
|
1340
1790
|
{ path: "image.mode", type: "enum", values: ["build", "image"] },
|
|
1341
1791
|
{ path: "image.pullPolicy", type: "enum", values: ["missing", "always", "never"] },
|
|
1342
1792
|
{ path: "storage.homePath", type: "string" },
|
|
1343
|
-
{ path: "cloud.apiUrl", type: "string" },
|
|
1344
1793
|
{ path: "langfuse.baseUrl", type: "string" },
|
|
1345
1794
|
{ path: "langfuse.privacyPreset", type: "enum", values: LANGFUSE_PRIVACY_PRESETS },
|
|
1346
1795
|
{ path: "composeOverrides", type: "list" }
|
|
@@ -1401,649 +1850,231 @@ function coerceFieldValue(field, raw) {
|
|
|
1401
1850
|
return raw;
|
|
1402
1851
|
}
|
|
1403
1852
|
|
|
1404
|
-
// src/lib/
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
for (; ; ) {
|
|
1409
|
-
if (resolveControlDir(dir).kind !== "absent") return dir;
|
|
1410
|
-
const parent = dirname3(dir);
|
|
1411
|
-
if (parent === dir) {
|
|
1412
|
-
throw new Error("not an OpenHarness-equipped repo \u2014 run `oh update` first");
|
|
1413
|
-
}
|
|
1414
|
-
dir = parent;
|
|
1853
|
+
// src/lib/env-file.ts
|
|
1854
|
+
function assertInRoot(dest, root) {
|
|
1855
|
+
if (!(dest === root || dest.startsWith(root + sep2))) {
|
|
1856
|
+
throw new Error(`refusing to write outside the project root: ${dest}`);
|
|
1415
1857
|
}
|
|
1416
1858
|
}
|
|
1417
|
-
|
|
1418
|
-
// src/lib/secrets.ts
|
|
1419
|
-
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
|
|
1420
|
-
import { resolve as resolve5 } from "node:path";
|
|
1421
|
-
|
|
1422
|
-
// src/lib/env.ts
|
|
1423
|
-
import {
|
|
1424
|
-
readFileSync as readFileSync6,
|
|
1425
|
-
writeFileSync as writeFileSync3,
|
|
1426
|
-
existsSync as existsSync6,
|
|
1427
|
-
fsyncSync,
|
|
1428
|
-
renameSync as renameSync3,
|
|
1429
|
-
unlinkSync as unlinkSync3,
|
|
1430
|
-
openSync,
|
|
1431
|
-
closeSync
|
|
1432
|
-
} from "node:fs";
|
|
1433
|
-
function loadEnvInto(envPath, env) {
|
|
1434
|
-
if (!existsSync6(envPath)) return;
|
|
1435
|
-
const content = readFileSync6(envPath, "utf8");
|
|
1436
|
-
for (const rawLine of content.split("\n")) {
|
|
1437
|
-
const line = rawLine.trimEnd();
|
|
1438
|
-
if (line === "" || /^\s*#/.test(line)) continue;
|
|
1439
|
-
const eqIdx = line.indexOf("=");
|
|
1440
|
-
if (eqIdx < 1) continue;
|
|
1441
|
-
const key = line.slice(0, eqIdx).trim();
|
|
1442
|
-
const value = line.slice(eqIdx + 1);
|
|
1443
|
-
if (key === "") continue;
|
|
1444
|
-
if (env[key] === void 0) env[key] = value;
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
function writeEnvFile(envPath, content) {
|
|
1448
|
-
const tmpPath = `${envPath}.tmp.${process.pid}`;
|
|
1449
|
-
let fd;
|
|
1450
|
-
try {
|
|
1451
|
-
fd = openSync(tmpPath, "w", 384);
|
|
1452
|
-
writeFileSync3(tmpPath, content, "utf8");
|
|
1453
|
-
fsyncSync(fd);
|
|
1454
|
-
closeSync(fd);
|
|
1455
|
-
fd = void 0;
|
|
1456
|
-
renameSync3(tmpPath, envPath);
|
|
1457
|
-
} catch (err) {
|
|
1458
|
-
if (fd !== void 0) {
|
|
1459
|
-
try {
|
|
1460
|
-
closeSync(fd);
|
|
1461
|
-
} catch {
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
try {
|
|
1465
|
-
unlinkSync3(tmpPath);
|
|
1466
|
-
} catch {
|
|
1467
|
-
}
|
|
1468
|
-
throw err;
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
// src/lib/secrets.ts
|
|
1473
|
-
var SECRETS_FILE = ".env";
|
|
1474
|
-
var SECRET_KEYS = [
|
|
1475
|
-
"GH_TOKEN",
|
|
1476
|
-
"XAI_API_KEY",
|
|
1477
|
-
"META_API_KEY",
|
|
1478
|
-
"SANDBOX_PASSWORD",
|
|
1479
|
-
"PI_SLACK_APP_TOKEN",
|
|
1480
|
-
"PI_SLACK_BOT_TOKEN",
|
|
1481
|
-
"LANGFUSE_PUBLIC_KEY",
|
|
1482
|
-
"LANGFUSE_SECRET_KEY",
|
|
1483
|
-
"OH_CLOUD_PROVISION_KEY"
|
|
1484
|
-
];
|
|
1485
|
-
var ALLOWED = new Set(SECRET_KEYS);
|
|
1486
|
-
function secretsFilePath(root) {
|
|
1487
|
-
return resolve5(root, SECRETS_FILE);
|
|
1488
|
-
}
|
|
1489
|
-
function isSecretKey(key) {
|
|
1490
|
-
return ALLOWED.has(key);
|
|
1491
|
-
}
|
|
1492
|
-
function assertSecretKey(key) {
|
|
1493
|
-
if (ALLOWED.has(key)) return;
|
|
1494
|
-
throw new Error(
|
|
1495
|
-
`${key} is not a secret \u2014 non-secret settings live in oh.json; use \`oh config set\` instead`
|
|
1496
|
-
);
|
|
1497
|
-
}
|
|
1498
|
-
function readSecret(root, key) {
|
|
1499
|
-
assertSecretKey(key);
|
|
1500
|
-
const file = secretsFilePath(root);
|
|
1501
|
-
if (!existsSync7(file)) return void 0;
|
|
1502
|
-
const env = {};
|
|
1503
|
-
loadEnvInto(file, env);
|
|
1504
|
-
const value = env[key]?.trim();
|
|
1505
|
-
return value === void 0 || value === "" ? void 0 : stripQuotes2(value);
|
|
1506
|
-
}
|
|
1507
|
-
function setSecret(root, key, value) {
|
|
1508
|
-
assertSecretKey(key);
|
|
1509
|
-
if (/[\r\n]/.test(value)) {
|
|
1510
|
-
throw new Error(`${key} value must not contain a newline`);
|
|
1511
|
-
}
|
|
1512
|
-
const file = secretsFilePath(root);
|
|
1513
|
-
assertInRoot(file, resolve5(root));
|
|
1514
|
-
const original = existsSync7(file) ? readFileSync7(file, "utf8") : "";
|
|
1515
|
-
const { content } = setKeyInEnv(original, key, value);
|
|
1516
|
-
writeEnvFile(file, content);
|
|
1517
|
-
}
|
|
1518
|
-
function listSecretKeys(root) {
|
|
1519
|
-
const file = secretsFilePath(root);
|
|
1520
|
-
if (!existsSync7(file)) return [];
|
|
1521
|
-
const env = {};
|
|
1522
|
-
loadEnvInto(file, env);
|
|
1523
|
-
return SECRET_KEYS.filter((key) => {
|
|
1524
|
-
const value = env[key]?.trim();
|
|
1525
|
-
return value !== void 0 && value !== "";
|
|
1526
|
-
});
|
|
1527
|
-
}
|
|
1528
|
-
function stripQuotes2(s) {
|
|
1859
|
+
function stripQuotes(s) {
|
|
1529
1860
|
if (s.length >= 2 && (s[0] === '"' && s.endsWith('"') || s[0] === "'" && s.endsWith("'"))) {
|
|
1530
1861
|
return s.slice(1, -1);
|
|
1531
1862
|
}
|
|
1532
1863
|
return s;
|
|
1533
1864
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
var DEFAULT_API_URL = "http://127.0.0.1:3000";
|
|
1537
|
-
var DEFAULT_TERMINAL_STATUSES = ["running", "failed", "destroyed"];
|
|
1538
|
-
var PROVISION_KEY_SECRET = "OH_CLOUD_PROVISION_KEY";
|
|
1539
|
-
var CLOUD_HELP = `oh cloud \u2014 Manage OpenHarness Cloud nodes
|
|
1540
|
-
|
|
1541
|
-
Usage:
|
|
1542
|
-
oh cloud config [--api-url <url>] [--provision-key <key>]
|
|
1543
|
-
oh cloud config show
|
|
1544
|
-
oh cloud [global options] nodes list
|
|
1545
|
-
oh cloud [global options] nodes get <node-id>
|
|
1546
|
-
oh cloud [global options] nodes create --ssh-key-id <id> [--name <name>]
|
|
1547
|
-
oh cloud [global options] nodes events <node-id>
|
|
1548
|
-
oh cloud [global options] nodes watch <node-id> [--interval <seconds>] [--until <statuses>]
|
|
1549
|
-
oh cloud [global options] nodes destroy <node-id>
|
|
1550
|
-
oh cloud [global options] nodes restart <node-id>
|
|
1551
|
-
oh cloud [global options] nodes rebuild <node-id>
|
|
1552
|
-
oh cloud [global options] ssh-keys list
|
|
1553
|
-
oh cloud [global options] ssh-keys create --name <name> (--public-key <key> | --public-key-file <path>)
|
|
1554
|
-
|
|
1555
|
-
Global options:
|
|
1556
|
-
--api-url <url> API base URL (default: OH_CLOUD_API_URL, OH_API_URL,
|
|
1557
|
-
oh.json cloud.apiUrl, or http://127.0.0.1:3000)
|
|
1558
|
-
--provision-key <key> Admin key (default: OH_CLOUD_PROVISION_KEY in the
|
|
1559
|
-
environment or in the root .env)
|
|
1560
|
-
--output <field> Print one top-level response field instead of JSON
|
|
1561
|
-
-h, --help Show this help
|
|
1562
|
-
|
|
1563
|
-
Configuration:
|
|
1564
|
-
oh cloud config writes cloud.apiUrl to the repository oh.json and securely
|
|
1565
|
-
prompts for the current provisioner key, which it stores as
|
|
1566
|
-
${PROVISION_KEY_SECRET} in the gitignored root .env. This is the temporary
|
|
1567
|
-
credential model until OpenHarness Cloud issues user API tokens.
|
|
1568
|
-
|
|
1569
|
-
Both settings are repository-local, so oh cloud runs inside an
|
|
1570
|
-
OpenHarness-equipped repo. Outside one, pass --api-url and --provision-key.
|
|
1571
|
-
|
|
1572
|
-
Environment:
|
|
1573
|
-
OH_CLOUD_API_URL Override the OpenHarness Cloud API base URL
|
|
1574
|
-
OH_CLOUD_PROVISION_KEY Override the stored key (OH_PROVISION_KEY and
|
|
1575
|
-
PROVISION_KEY are also accepted)
|
|
1576
|
-
|
|
1577
|
-
Examples:
|
|
1578
|
-
oh cloud config
|
|
1579
|
-
oh cloud ssh-keys create --name laptop --public-key-file ~/.ssh/openharness_node.pub
|
|
1580
|
-
oh cloud nodes create --name smoke-1 --ssh-key-id <ssh-key-id>
|
|
1581
|
-
oh cloud nodes watch <node-id>
|
|
1582
|
-
`;
|
|
1583
|
-
var CloudCliError = class extends Error {
|
|
1584
|
-
status;
|
|
1585
|
-
payload;
|
|
1586
|
-
constructor(message, options = {}) {
|
|
1587
|
-
super(message);
|
|
1588
|
-
this.name = "CloudCliError";
|
|
1589
|
-
this.status = options.status;
|
|
1590
|
-
this.payload = options.payload;
|
|
1591
|
-
}
|
|
1865
|
+
var CONFIG_FIELD_BY_ENV_KEY = {
|
|
1866
|
+
DOCKER_SOCKET: "access.dockerSocket"
|
|
1592
1867
|
};
|
|
1593
|
-
function
|
|
1594
|
-
const
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
const value = args[index + 1];
|
|
1599
|
-
if (value === void 0 || value.startsWith("-")) {
|
|
1600
|
-
throw new CloudCliError(`${candidate} requires a value`);
|
|
1601
|
-
}
|
|
1602
|
-
args.splice(index, 2);
|
|
1603
|
-
return value;
|
|
1604
|
-
}
|
|
1605
|
-
return void 0;
|
|
1606
|
-
}
|
|
1607
|
-
function requireOption(value, name) {
|
|
1608
|
-
if (!value) throw new CloudCliError(`${name} is required`);
|
|
1609
|
-
return value;
|
|
1610
|
-
}
|
|
1611
|
-
function requirePositional(value, name) {
|
|
1612
|
-
if (!value || value.startsWith("-")) throw new CloudCliError(`${name} is required`);
|
|
1613
|
-
return value;
|
|
1614
|
-
}
|
|
1615
|
-
function assertNoExtraArgs(args) {
|
|
1616
|
-
if (args.length > 0) {
|
|
1617
|
-
throw new CloudCliError(
|
|
1618
|
-
`unexpected argument${args.length === 1 ? "" : "s"}: ${args.join(" ")}`
|
|
1868
|
+
function setEnvValue(root, key, value, bin) {
|
|
1869
|
+
const path4 = CONFIG_FIELD_BY_ENV_KEY[key];
|
|
1870
|
+
if (path4 === void 0) {
|
|
1871
|
+
throw new Error(
|
|
1872
|
+
`${key} is not a settable oh.json field \u2014 run \`${bin} config set\` for a non-secret, \`${bin} secret set\` for a credential`
|
|
1619
1873
|
);
|
|
1620
1874
|
}
|
|
1875
|
+
return setConfigField(root, path4, value);
|
|
1621
1876
|
}
|
|
1622
|
-
function
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
if (
|
|
1630
|
-
|
|
1877
|
+
function setConfigField(root, path4, value) {
|
|
1878
|
+
const file = ohConfigPath(root);
|
|
1879
|
+
assertInRoot(file, resolve5(root));
|
|
1880
|
+
const config = readOhConfig(file);
|
|
1881
|
+
const before = getOhConfigValue(config, path4);
|
|
1882
|
+
const next = setOhConfigValue(config, path4, value);
|
|
1883
|
+
const after = getOhConfigValue(next, path4);
|
|
1884
|
+
if (existsSync7(file) && JSON.stringify(before) === JSON.stringify(after)) {
|
|
1885
|
+
return "already-set";
|
|
1631
1886
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
function formatJson(value) {
|
|
1635
|
-
return `${JSON.stringify(value, null, 2)}
|
|
1636
|
-
`;
|
|
1637
|
-
}
|
|
1638
|
-
function bodyPreview(body) {
|
|
1639
|
-
const compact = body.replace(/\s+/g, " ").trim();
|
|
1640
|
-
return compact.length > 180 ? `${compact.slice(0, 177)}...` : compact;
|
|
1641
|
-
}
|
|
1642
|
-
function responseKind(contentType) {
|
|
1643
|
-
return contentType?.split(";", 1)[0]?.trim() || "non-JSON content";
|
|
1887
|
+
writeOhConfig(root, next);
|
|
1888
|
+
return before === void 0 ? "added" : "updated";
|
|
1644
1889
|
}
|
|
1645
|
-
function
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1890
|
+
function setKeyInEnv(content, key, value) {
|
|
1891
|
+
const lines = content.split("\n");
|
|
1892
|
+
const live = new RegExp(`^${escapeRegExp(key)}=(.*)$`);
|
|
1893
|
+
const commented = new RegExp(`^[ \\t]*#[ \\t]*${escapeRegExp(key)}=(.*)$`);
|
|
1894
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1895
|
+
const m = live.exec(lines[i]);
|
|
1896
|
+
if (m) {
|
|
1897
|
+
if (stripQuotes(m[1].trim()) === value) return { content, outcome: "already-set" };
|
|
1898
|
+
lines[i] = `${key}=${value}`;
|
|
1899
|
+
return { content: lines.join("\n"), outcome: "updated" };
|
|
1652
1900
|
}
|
|
1653
1901
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
}
|
|
1659
|
-
async function parseResponse(response) {
|
|
1660
|
-
const text = await response.text();
|
|
1661
|
-
const contentType = response.headers.get("content-type");
|
|
1662
|
-
if (!text) return { payload: null, isJson: true, contentType };
|
|
1663
|
-
try {
|
|
1664
|
-
return { payload: JSON.parse(text), isJson: true, contentType };
|
|
1665
|
-
} catch {
|
|
1666
|
-
return { payload: text, isJson: false, contentType };
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
function createCloudApiClient(options) {
|
|
1670
|
-
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1671
|
-
if (typeof fetchImpl !== "function") {
|
|
1672
|
-
throw new CloudCliError(
|
|
1673
|
-
"the cloud CLI requires Node.js 20 or newer (global fetch is unavailable)"
|
|
1674
|
-
);
|
|
1675
|
-
}
|
|
1676
|
-
return async (method, pathname, body) => {
|
|
1677
|
-
let response;
|
|
1678
|
-
try {
|
|
1679
|
-
response = await fetchImpl(`${options.apiUrl}${pathname}`, {
|
|
1680
|
-
method,
|
|
1681
|
-
headers: {
|
|
1682
|
-
"x-provision-key": options.provisionKey,
|
|
1683
|
-
...body === void 0 ? {} : { "content-type": "application/json" }
|
|
1684
|
-
},
|
|
1685
|
-
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
1686
|
-
});
|
|
1687
|
-
} catch (error) {
|
|
1688
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
1689
|
-
throw new CloudCliError(`could not reach ${options.apiUrl}: ${detail}`);
|
|
1690
|
-
}
|
|
1691
|
-
const parsed = await parseResponse(response);
|
|
1692
|
-
if (!response.ok) {
|
|
1693
|
-
throw new CloudCliError(
|
|
1694
|
-
formatApiFailure(response.status, parsed.payload, parsed.contentType),
|
|
1695
|
-
{
|
|
1696
|
-
status: response.status,
|
|
1697
|
-
payload: parsed.payload
|
|
1698
|
-
}
|
|
1699
|
-
);
|
|
1700
|
-
}
|
|
1701
|
-
if (!parsed.isJson) {
|
|
1702
|
-
throw new CloudCliError(
|
|
1703
|
-
`API returned ${response.status} ${responseKind(parsed.contentType)}; expected JSON (${bodyPreview(parsed.payload)})`,
|
|
1704
|
-
{ status: response.status, payload: parsed.payload }
|
|
1705
|
-
);
|
|
1902
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1903
|
+
if (commented.test(lines[i])) {
|
|
1904
|
+
lines[i] = `${key}=${value}`;
|
|
1905
|
+
return { content: lines.join("\n"), outcome: "uncommented" };
|
|
1706
1906
|
}
|
|
1707
|
-
|
|
1907
|
+
}
|
|
1908
|
+
const body = content.replace(/\n+$/, "");
|
|
1909
|
+
return {
|
|
1910
|
+
content: body === "" ? `${key}=${value}
|
|
1911
|
+
` : `${body}
|
|
1912
|
+
${key}=${value}
|
|
1913
|
+
`,
|
|
1914
|
+
outcome: "added"
|
|
1708
1915
|
};
|
|
1709
1916
|
}
|
|
1710
|
-
function
|
|
1711
|
-
return
|
|
1712
|
-
}
|
|
1713
|
-
async function readPublicKey(args, readFileImpl) {
|
|
1714
|
-
const inline = takeOption(args, "--public-key");
|
|
1715
|
-
const file = takeOption(args, "--public-key-file");
|
|
1716
|
-
if (inline && file) {
|
|
1717
|
-
throw new CloudCliError("use only one of --public-key or --public-key-file");
|
|
1718
|
-
}
|
|
1719
|
-
if (!inline && !file) {
|
|
1720
|
-
throw new CloudCliError("--public-key or --public-key-file is required");
|
|
1721
|
-
}
|
|
1722
|
-
if (inline) return inline.trim();
|
|
1723
|
-
try {
|
|
1724
|
-
const value = (await readFileImpl(file, "utf8")).trim();
|
|
1725
|
-
if (!value) throw new CloudCliError(`SSH public key file is empty: ${file}`);
|
|
1726
|
-
return value;
|
|
1727
|
-
} catch (error) {
|
|
1728
|
-
if (error instanceof CloudCliError) throw error;
|
|
1729
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
1730
|
-
throw new CloudCliError(`could not read SSH public key file ${file}: ${detail}`);
|
|
1731
|
-
}
|
|
1917
|
+
function escapeRegExp(s) {
|
|
1918
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1732
1919
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
}
|
|
1744
|
-
if (action === "create") {
|
|
1745
|
-
const name = requireOption(takeOption(args, "--name"), "--name");
|
|
1746
|
-
const publicKey = await readPublicKey(args, options.readFile);
|
|
1747
|
-
assertNoExtraArgs(args);
|
|
1748
|
-
return options.request("POST", "/api/ssh-keys", { name, publicKey });
|
|
1749
|
-
}
|
|
1750
|
-
throw new CloudCliError(`unknown ssh-keys action: ${action}`);
|
|
1751
|
-
}
|
|
1752
|
-
if (resource !== "nodes") throw new CloudCliError(`unknown resource: ${resource}`);
|
|
1753
|
-
if (action === "list") {
|
|
1754
|
-
assertNoExtraArgs(args);
|
|
1755
|
-
return options.request("GET", "/api/nodes");
|
|
1756
|
-
}
|
|
1757
|
-
if (action === "create") {
|
|
1758
|
-
const name = takeOption(args, "--name");
|
|
1759
|
-
const sshKeyId = requireOption(takeOption(args, "--ssh-key-id"), "--ssh-key-id");
|
|
1760
|
-
assertNoExtraArgs(args);
|
|
1761
|
-
return options.request("POST", "/api/nodes", {
|
|
1762
|
-
...name ? { name } : {},
|
|
1763
|
-
sshKeyId
|
|
1764
|
-
});
|
|
1765
|
-
}
|
|
1766
|
-
const nodeIdActions = ["get", "events", "destroy", "restart", "rebuild", "watch"];
|
|
1767
|
-
if (!nodeIdActions.includes(action)) {
|
|
1768
|
-
throw new CloudCliError(`unknown nodes action: ${action}`);
|
|
1769
|
-
}
|
|
1770
|
-
const nodeId = requirePositional(args.shift(), "<node-id>");
|
|
1771
|
-
if (action === "get") {
|
|
1772
|
-
assertNoExtraArgs(args);
|
|
1773
|
-
return options.request("GET", nodePath(nodeId));
|
|
1774
|
-
}
|
|
1775
|
-
if (action === "events") {
|
|
1776
|
-
assertNoExtraArgs(args);
|
|
1777
|
-
return options.request("GET", nodePath(nodeId, "/events"));
|
|
1778
|
-
}
|
|
1779
|
-
if (action === "destroy") {
|
|
1780
|
-
assertNoExtraArgs(args);
|
|
1781
|
-
return options.request("DELETE", nodePath(nodeId));
|
|
1782
|
-
}
|
|
1783
|
-
if (action === "restart" || action === "rebuild") {
|
|
1784
|
-
assertNoExtraArgs(args);
|
|
1785
|
-
return options.request("POST", nodePath(nodeId, `/${action}`));
|
|
1786
|
-
}
|
|
1787
|
-
if (action === "watch") {
|
|
1788
|
-
const intervalValue = takeOption(args, "--interval") ?? "5";
|
|
1789
|
-
const intervalSeconds = Number(intervalValue);
|
|
1790
|
-
if (!Number.isFinite(intervalSeconds) || intervalSeconds < 0) {
|
|
1791
|
-
throw new CloudCliError(`--interval must be a non-negative number: ${intervalValue}`);
|
|
1792
|
-
}
|
|
1793
|
-
const untilValue = takeOption(args, "--until");
|
|
1794
|
-
const terminalStatuses = new Set(
|
|
1795
|
-
(untilValue ? untilValue.split(",") : DEFAULT_TERMINAL_STATUSES).map((status) => status.trim()).filter(Boolean)
|
|
1796
|
-
);
|
|
1797
|
-
if (terminalStatuses.size === 0) {
|
|
1798
|
-
throw new CloudCliError("--until must contain at least one status");
|
|
1799
|
-
}
|
|
1800
|
-
assertNoExtraArgs(args);
|
|
1801
|
-
let lastStatus;
|
|
1802
|
-
while (true) {
|
|
1803
|
-
const node = await options.request("GET", nodePath(nodeId));
|
|
1804
|
-
if (!node || typeof node !== "object" || typeof node.status !== "string") {
|
|
1805
|
-
throw new CloudCliError("node response did not include a status");
|
|
1806
|
-
}
|
|
1807
|
-
const status = node.status;
|
|
1808
|
-
if (status !== lastStatus) {
|
|
1809
|
-
options.write(node);
|
|
1810
|
-
lastStatus = status;
|
|
1811
|
-
}
|
|
1812
|
-
if (terminalStatuses.has(status)) return void 0;
|
|
1813
|
-
await options.sleep(intervalSeconds * 1e3);
|
|
1920
|
+
|
|
1921
|
+
// src/lib/project.ts
|
|
1922
|
+
import { dirname as dirname5, resolve as resolve6 } from "node:path";
|
|
1923
|
+
function resolveProjectRoot(startDir = process.cwd()) {
|
|
1924
|
+
let dir = resolve6(startDir);
|
|
1925
|
+
for (; ; ) {
|
|
1926
|
+
if (resolveControlDir(dir).kind !== "absent") return dir;
|
|
1927
|
+
const parent = dirname5(dir);
|
|
1928
|
+
if (parent === dir) {
|
|
1929
|
+
throw new Error(`not an OpenHarness-equipped repo \u2014 run \`${activeBin()} update\` first`);
|
|
1814
1930
|
}
|
|
1931
|
+
dir = parent;
|
|
1815
1932
|
}
|
|
1816
|
-
throw new CloudCliError(`unknown nodes action: ${action}`);
|
|
1817
|
-
}
|
|
1818
|
-
var OUTSIDE_REPO_HINT = "; pass --api-url and --provision-key to run `oh cloud` outside a repo";
|
|
1819
|
-
function requireRoot(io) {
|
|
1820
|
-
try {
|
|
1821
|
-
return resolveProjectRoot(io.cwd);
|
|
1822
|
-
} catch (error) {
|
|
1823
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
1824
|
-
throw new CloudCliError(`${detail}${OUTSIDE_REPO_HINT}`);
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
function legacyCloudConfigPath(env) {
|
|
1828
|
-
const home = env.HOME ?? env.USERPROFILE;
|
|
1829
|
-
if (!home) return void 0;
|
|
1830
|
-
return join3(home, ".config", "openharness", "cloud.json");
|
|
1831
|
-
}
|
|
1832
|
-
function readLegacyCloudConfig(path4) {
|
|
1833
|
-
let parsed;
|
|
1834
|
-
try {
|
|
1835
|
-
parsed = JSON.parse(readFileSync8(path4, "utf8"));
|
|
1836
|
-
} catch {
|
|
1837
|
-
return void 0;
|
|
1838
|
-
}
|
|
1839
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
1840
|
-
const record = parsed;
|
|
1841
|
-
return {
|
|
1842
|
-
...typeof record.apiUrl === "string" ? { apiUrl: record.apiUrl } : {},
|
|
1843
|
-
...typeof record.provisionKey === "string" ? { provisionKey: record.provisionKey } : {}
|
|
1844
|
-
};
|
|
1845
1933
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
io.stdout(`Migrated OpenHarness Cloud settings out of ${path4}
|
|
1863
|
-
`);
|
|
1864
|
-
if (legacy.apiUrl) io.stdout(` cloud.apiUrl -> ${ohConfigPath(root)}
|
|
1865
|
-
`);
|
|
1866
|
-
if (legacy.provisionKey) io.stdout(` ${PROVISION_KEY_SECRET} -> ${join3(root, ".env")}
|
|
1867
|
-
`);
|
|
1868
|
-
io.stdout(`${path4} is no longer read; delete it when you no longer need the copy.
|
|
1934
|
+
|
|
1935
|
+
// src/lib/prompt.ts
|
|
1936
|
+
import { createInterface } from "node:readline";
|
|
1937
|
+
var COLORS = {
|
|
1938
|
+
reset: "\x1B[0m",
|
|
1939
|
+
bold: "\x1B[1m",
|
|
1940
|
+
dim: "\x1B[2m",
|
|
1941
|
+
green: "\x1B[32m",
|
|
1942
|
+
yellow: "\x1B[33m",
|
|
1943
|
+
red: "\x1B[31m",
|
|
1944
|
+
cyan: "\x1B[36m"
|
|
1945
|
+
};
|
|
1946
|
+
function header(msg) {
|
|
1947
|
+
process.stdout.write(`
|
|
1948
|
+
${COLORS.bold}${msg}${COLORS.reset}
|
|
1949
|
+
|
|
1869
1950
|
`);
|
|
1870
1951
|
}
|
|
1871
|
-
function
|
|
1872
|
-
|
|
1873
|
-
const config = readOhConfig(ohConfigPath(root));
|
|
1874
|
-
return {
|
|
1875
|
-
...config.cloud?.apiUrl ? { apiUrl: config.cloud.apiUrl } : {},
|
|
1876
|
-
...(() => {
|
|
1877
|
-
const key = readSecret(root, PROVISION_KEY_SECRET);
|
|
1878
|
-
return key ? { provisionKey: key } : {};
|
|
1879
|
-
})()
|
|
1880
|
-
};
|
|
1952
|
+
function bold(s) {
|
|
1953
|
+
return `${COLORS.bold}${s}${COLORS.reset}`;
|
|
1881
1954
|
}
|
|
1882
|
-
function
|
|
1883
|
-
|
|
1955
|
+
function redact(token) {
|
|
1956
|
+
if (token.length <= 8) return "****";
|
|
1957
|
+
return token.slice(0, 5) + "*".repeat(token.length - 5);
|
|
1884
1958
|
}
|
|
1885
|
-
async function
|
|
1886
|
-
const
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
io.stdout(`Secrets: ${join3(root, ".env")}
|
|
1894
|
-
`);
|
|
1895
|
-
io.stdout(`API URL: ${current.apiUrl ?? DEFAULT_API_URL}
|
|
1896
|
-
`);
|
|
1897
|
-
io.stdout(
|
|
1898
|
-
`Provision credential: ${current.provisionKey ? redact(current.provisionKey) : "not configured"}
|
|
1899
|
-
`
|
|
1900
|
-
);
|
|
1901
|
-
return;
|
|
1959
|
+
async function ask(question) {
|
|
1960
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1961
|
+
try {
|
|
1962
|
+
return await new Promise((resolve11) => {
|
|
1963
|
+
rl.question(` ${question} `, (answer) => resolve11(answer.trim()));
|
|
1964
|
+
});
|
|
1965
|
+
} finally {
|
|
1966
|
+
rl.close();
|
|
1902
1967
|
}
|
|
1903
|
-
const apiUrl = normalizeApiUrl(
|
|
1904
|
-
takeOption(args, "--api-url") ?? current.apiUrl ?? env.OH_CLOUD_API_URL ?? env.OH_API_URL ?? DEFAULT_API_URL
|
|
1905
|
-
);
|
|
1906
|
-
const providedKey = takeOption(args, "--provision-key");
|
|
1907
|
-
assertNoExtraArgs(args);
|
|
1908
|
-
const provisionKey = (providedKey ?? await (io.askSecret ?? askSecret)("OpenHarness Cloud provisioner key:")).trim();
|
|
1909
|
-
if (!provisionKey) throw new CloudCliError("provisioner key cannot be empty");
|
|
1910
|
-
const config = readOhConfig(ohConfigPath(root));
|
|
1911
|
-
writeOhConfig(root, { ...config, cloud: { ...config.cloud, apiUrl } });
|
|
1912
|
-
setSecret(root, PROVISION_KEY_SECRET, provisionKey);
|
|
1913
|
-
io.stdout(`Saved cloud.apiUrl to ${ohConfigPath(root)}
|
|
1914
|
-
`);
|
|
1915
|
-
io.stdout(`Saved ${PROVISION_KEY_SECRET} to ${join3(root, ".env")}
|
|
1916
|
-
`);
|
|
1917
|
-
io.stdout("Provision credential: configured\n");
|
|
1918
1968
|
}
|
|
1919
|
-
async function
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
`);
|
|
1964
|
-
return;
|
|
1965
|
-
}
|
|
1966
|
-
write(result);
|
|
1967
|
-
}
|
|
1968
|
-
async function runCloud(argv, io) {
|
|
1969
|
-
try {
|
|
1970
|
-
await executeCloud(argv, io);
|
|
1971
|
-
return 0;
|
|
1972
|
-
} catch (error) {
|
|
1973
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1974
|
-
io.stderr(`oh cloud: ${message}
|
|
1975
|
-
`);
|
|
1976
|
-
return 1;
|
|
1977
|
-
}
|
|
1969
|
+
async function askSecret(question) {
|
|
1970
|
+
if (!process.stdin.isTTY) return askSecretPiped(question);
|
|
1971
|
+
process.stdout.write(` ${question} `);
|
|
1972
|
+
process.stdin.setRawMode(true);
|
|
1973
|
+
process.stdin.resume();
|
|
1974
|
+
const bytes = [];
|
|
1975
|
+
return await new Promise((resolve11) => {
|
|
1976
|
+
const cleanup = () => {
|
|
1977
|
+
try {
|
|
1978
|
+
process.stdin.setRawMode(false);
|
|
1979
|
+
} catch {
|
|
1980
|
+
}
|
|
1981
|
+
process.stdin.pause();
|
|
1982
|
+
process.stdin.removeListener("data", onData);
|
|
1983
|
+
};
|
|
1984
|
+
const onData = (data) => {
|
|
1985
|
+
for (const byte of data) {
|
|
1986
|
+
if (byte === 3) {
|
|
1987
|
+
cleanup();
|
|
1988
|
+
process.stdout.write("\n");
|
|
1989
|
+
process.exit(130);
|
|
1990
|
+
} else if (byte === 13 || byte === 10) {
|
|
1991
|
+
cleanup();
|
|
1992
|
+
process.stdout.write("\n");
|
|
1993
|
+
resolve11(Buffer.from(bytes).toString("utf8").trim());
|
|
1994
|
+
return;
|
|
1995
|
+
} else if (byte === 127 || byte === 8) {
|
|
1996
|
+
if (bytes.length > 0) {
|
|
1997
|
+
bytes.pop();
|
|
1998
|
+
process.stdout.write("\b \b");
|
|
1999
|
+
}
|
|
2000
|
+
} else if (byte === 21) {
|
|
2001
|
+
while (bytes.length > 0) {
|
|
2002
|
+
bytes.pop();
|
|
2003
|
+
process.stdout.write("\b \b");
|
|
2004
|
+
}
|
|
2005
|
+
} else if (byte >= 32) {
|
|
2006
|
+
bytes.push(byte);
|
|
2007
|
+
process.stdout.write("\u25CF");
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
process.stdin.on("data", onData);
|
|
2012
|
+
});
|
|
1978
2013
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2014
|
+
async function askSecretPiped(question) {
|
|
2015
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2016
|
+
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
2017
|
+
let prompted = false;
|
|
2018
|
+
const patched = (chunk) => {
|
|
2019
|
+
if (!prompted) {
|
|
2020
|
+
prompted = true;
|
|
2021
|
+
return stdoutWrite(chunk);
|
|
2022
|
+
}
|
|
2023
|
+
return true;
|
|
1989
2024
|
};
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
2025
|
+
process.stdout.write = patched;
|
|
2026
|
+
let restored = false;
|
|
2027
|
+
const restore = () => {
|
|
2028
|
+
if (restored) return;
|
|
2029
|
+
restored = true;
|
|
2030
|
+
process.stdout.write = stdoutWrite;
|
|
2031
|
+
try {
|
|
2032
|
+
rl.close();
|
|
2033
|
+
} catch {
|
|
2034
|
+
}
|
|
1997
2035
|
};
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
};
|
|
2015
|
-
function assertSpawned(r, what) {
|
|
2016
|
-
if (r.error) {
|
|
2017
|
-
throw new ExecutionSpawnError(what, r.error);
|
|
2036
|
+
const onSigint = () => {
|
|
2037
|
+
restore();
|
|
2038
|
+
process.stdout.write("\n");
|
|
2039
|
+
process.exit(130);
|
|
2040
|
+
};
|
|
2041
|
+
process.on("SIGINT", onSigint);
|
|
2042
|
+
try {
|
|
2043
|
+
return await new Promise((resolve11) => {
|
|
2044
|
+
rl.question(` ${question} `, (answer) => {
|
|
2045
|
+
process.stdout.write("\n");
|
|
2046
|
+
resolve11(answer.trim());
|
|
2047
|
+
});
|
|
2048
|
+
});
|
|
2049
|
+
} finally {
|
|
2050
|
+
process.removeListener("SIGINT", onSigint);
|
|
2051
|
+
restore();
|
|
2018
2052
|
}
|
|
2019
2053
|
}
|
|
2020
|
-
function
|
|
2021
|
-
const
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
);
|
|
2026
|
-
}
|
|
2027
|
-
return script;
|
|
2054
|
+
async function confirm(question, defaultYes = true) {
|
|
2055
|
+
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
2056
|
+
const answer = (await ask(`${question} ${suffix}`)).toLowerCase();
|
|
2057
|
+
if (answer === "") return defaultYes;
|
|
2058
|
+
return /^y/.test(answer);
|
|
2028
2059
|
}
|
|
2029
2060
|
|
|
2030
2061
|
// src/lib/registry.ts
|
|
2031
|
-
import { existsSync as
|
|
2032
|
-
import { dirname as
|
|
2062
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2063
|
+
import { dirname as dirname6, join as join5, resolve as resolve7, sep as sep3 } from "node:path";
|
|
2033
2064
|
|
|
2034
|
-
// oh-asset:/home/runner/work/
|
|
2035
|
-
var docker_compose_default = 'name: ${SANDBOX_NAME:-
|
|
2065
|
+
// oh-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.yml
|
|
2066
|
+
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
2067
|
|
|
2037
|
-
// oh-asset:/home/runner/work/
|
|
2038
|
-
var docker_compose_image_only_default = 'name: ${SANDBOX_NAME:-
|
|
2068
|
+
// oh-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.image-only.yml
|
|
2069
|
+
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
2070
|
|
|
2040
|
-
// oh-asset:/home/runner/work/
|
|
2071
|
+
// oh-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.ssh.yml
|
|
2041
2072
|
var docker_compose_ssh_default = 'services:\n sandbox:\n ports:\n - "127.0.0.1:${SANDBOX_SSH_PORT:-2222}:22"\n';
|
|
2042
2073
|
|
|
2043
|
-
// oh-asset:/home/runner/work/
|
|
2074
|
+
// oh-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.docker-sock.yml
|
|
2044
2075
|
var docker_compose_docker_sock_default = "services:\n sandbox:\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n";
|
|
2045
2076
|
|
|
2046
|
-
// oh-asset:/home/runner/work/
|
|
2077
|
+
// oh-asset:/home/runner/work/agro/agro/.agro/scripts/docker-compose.sh
|
|
2047
2078
|
var docker_compose_default2 = `#!/usr/bin/env bash
|
|
2048
2079
|
|
|
2049
2080
|
set -euo pipefail
|
|
@@ -2059,10 +2090,10 @@ Usage: scripts/docker-compose.sh [--repo-dir DIR] [--extra-env-file FILE]
|
|
|
2059
2090
|
[--print-argv] <docker-compose-args...>
|
|
2060
2091
|
|
|
2061
2092
|
Builds the harness docker compose argv from the repository dotenv and the
|
|
2062
|
-
composeOverrides[] list in
|
|
2093
|
+
composeOverrides[] list in agro.json, then executes \`docker compose ...\` with the
|
|
2063
2094
|
provided args.
|
|
2064
2095
|
--extra-env-file adds a lower-precedence --env-file ahead of the dotenv; \`oh\`
|
|
2065
|
-
renders the non-secret settings of
|
|
2096
|
+
renders the non-secret settings of agro.json into that file.
|
|
2066
2097
|
--print-argv prints one argv entry per line instead of executing; useful for
|
|
2067
2098
|
safe diagnostics and tests.
|
|
2068
2099
|
EOF
|
|
@@ -2102,7 +2133,7 @@ done
|
|
|
2102
2133
|
|
|
2103
2134
|
COMPAT="$SCRIPT_DIR/compat.sh"
|
|
2104
2135
|
if [ ! -f "$COMPAT" ]; then
|
|
2105
|
-
printf 'error: %s is missing \u2014 the vendored .
|
|
2136
|
+
printf 'error: %s is missing \u2014 the vendored .agro/scripts payload is incomplete; run \`oh update\`\\n' "$COMPAT" >&2
|
|
2106
2137
|
exit 2
|
|
2107
2138
|
fi
|
|
2108
2139
|
# shellcheck source=compat.sh
|
|
@@ -2166,7 +2197,7 @@ fi
|
|
|
2166
2197
|
|
|
2167
2198
|
CONFIG_JSON="$(compat_selected_path compat_config_file "$REPO_DIR")" || exit 2
|
|
2168
2199
|
|
|
2169
|
-
if [ -z "$EXTRA_ENV_FILE" ] && [ -
|
|
2200
|
+
if [ -z "$EXTRA_ENV_FILE" ] && [ -f "$CONFIG_JSON" ]; then
|
|
2170
2201
|
printf 'note: non-secret config comes from %s via \`oh\`; this direct run uses only %s and the compose-file defaults\\n' \\
|
|
2171
2202
|
"\${CONFIG_JSON#"$REPO_DIR"/}" "\${ENV_FILE#"$REPO_DIR"/}" >&2
|
|
2172
2203
|
fi
|
|
@@ -2199,7 +2230,7 @@ if truthy "$ssh_value"; then
|
|
|
2199
2230
|
port_check="$SCRIPT_DIR/check-host-port.sh"
|
|
2200
2231
|
if [ -x "$port_check" ] || [ -f "$port_check" ]; then
|
|
2201
2232
|
sandbox_name=\${SANDBOX_NAME:-$(read_env_value SANDBOX_NAME)}
|
|
2202
|
-
|
|
2233
|
+
sandbox_name="$(compat_sandbox_name "$sandbox_name")"
|
|
2203
2234
|
own_port=0
|
|
2204
2235
|
if command -v docker >/dev/null 2>&1; then
|
|
2205
2236
|
docker ps --format '{{.Names}}\\t{{.Ports}}' 2>/dev/null \\
|
|
@@ -2219,7 +2250,7 @@ if truthy "$ssh_value"; then
|
|
|
2219
2250
|
fi
|
|
2220
2251
|
fi
|
|
2221
2252
|
|
|
2222
|
-
[ -
|
|
2253
|
+
[ -f "$CONFIG_JSON" ] || CONFIG_JSON="$REPO_DIR/.agro/config.json"
|
|
2223
2254
|
[ -f "$CONFIG_JSON" ] || CONFIG_JSON="$REPO_DIR/config.json"
|
|
2224
2255
|
if command -v jq >/dev/null 2>&1 && [ -f "$CONFIG_JSON" ]; then
|
|
2225
2256
|
while IFS= read -r override; do
|
|
@@ -2235,11 +2266,11 @@ fi
|
|
|
2235
2266
|
exec docker compose "\${args[@]}" "$@"
|
|
2236
2267
|
`;
|
|
2237
2268
|
|
|
2238
|
-
// oh-asset:/home/runner/work/
|
|
2269
|
+
// oh-asset:/home/runner/work/agro/agro/.agro/scripts/compat.sh
|
|
2239
2270
|
var compat_default = `#!/usr/bin/env bash
|
|
2240
2271
|
# Boot-safe dual-generation resolution: bash + coreutils only, no node, no jq.
|
|
2241
|
-
# The TypeScript contract is .
|
|
2242
|
-
# vectors in .
|
|
2272
|
+
# The TypeScript contract is .agro/cli/src/lib/compat.ts; both consume the same
|
|
2273
|
+
# vectors in .agro/cli/src/lib/__tests__/fixtures/compat-vectors.json.
|
|
2243
2274
|
|
|
2244
2275
|
COMPAT_LEGACY_CONTROL_DIR=".oh"
|
|
2245
2276
|
COMPAT_AGRO_CONTROL_DIR=".agro"
|
|
@@ -2247,6 +2278,8 @@ COMPAT_LEGACY_CONFIG_FILE="oh.json"
|
|
|
2247
2278
|
COMPAT_AGRO_CONFIG_FILE="agro.json"
|
|
2248
2279
|
COMPAT_LEGACY_SEED_DIR="/opt/oh-seed"
|
|
2249
2280
|
COMPAT_AGRO_SEED_DIR="/opt/agro-seed"
|
|
2281
|
+
COMPAT_DEFAULT_GENERATION=agro
|
|
2282
|
+
COMPAT_DEFAULT_SANDBOX_NAME=agro
|
|
2250
2283
|
COMPAT_CONFLICT_STATUS=3
|
|
2251
2284
|
|
|
2252
2285
|
compat_tree_manifest() {
|
|
@@ -2294,6 +2327,14 @@ compat_tree_diff() {
|
|
|
2294
2327
|
return "$rc"
|
|
2295
2328
|
}
|
|
2296
2329
|
|
|
2330
|
+
compat_sandbox_name() {
|
|
2331
|
+
if [ -n "\${1:-}" ]; then
|
|
2332
|
+
printf '%s\\n' "$1"
|
|
2333
|
+
else
|
|
2334
|
+
printf '%s\\n' "$COMPAT_DEFAULT_SANDBOX_NAME"
|
|
2335
|
+
fi
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2297
2338
|
compat_present() {
|
|
2298
2339
|
case "$1" in
|
|
2299
2340
|
dir) [ -d "$2" ] ;;
|
|
@@ -2308,7 +2349,11 @@ compat_resolve_pair() {
|
|
|
2308
2349
|
compat_present "$entry_kind" "$legacy" || legacy_present=0
|
|
2309
2350
|
compat_present "$entry_kind" "$agro" || agro_present=0
|
|
2310
2351
|
if [ "$legacy_present" = 0 ] && [ "$agro_present" = 0 ]; then
|
|
2311
|
-
|
|
2352
|
+
if [ "$COMPAT_DEFAULT_GENERATION" = agro ]; then
|
|
2353
|
+
printf 'absent\\t%s\\n' "$agro"
|
|
2354
|
+
else
|
|
2355
|
+
printf 'absent\\t%s\\n' "$legacy"
|
|
2356
|
+
fi
|
|
2312
2357
|
return 0
|
|
2313
2358
|
fi
|
|
2314
2359
|
if [ "$legacy_present" = 1 ] && [ "$agro_present" = 0 ]; then
|
|
@@ -2374,10 +2419,10 @@ compat_seed_src() {
|
|
|
2374
2419
|
printf '%s\\n' "$configured"
|
|
2375
2420
|
return 0
|
|
2376
2421
|
fi
|
|
2377
|
-
if [ -d "$prefix$
|
|
2378
|
-
printf '%s\\n' "$prefix$COMPAT_AGRO_SEED_DIR"
|
|
2379
|
-
else
|
|
2422
|
+
if [ -d "$prefix$COMPAT_LEGACY_SEED_DIR" ]; then
|
|
2380
2423
|
printf '%s\\n' "$prefix$COMPAT_LEGACY_SEED_DIR"
|
|
2424
|
+
else
|
|
2425
|
+
printf '%s\\n' "$prefix$COMPAT_AGRO_SEED_DIR"
|
|
2381
2426
|
fi
|
|
2382
2427
|
}
|
|
2383
2428
|
|
|
@@ -2385,12 +2430,11 @@ compat_marker_file() {
|
|
|
2385
2430
|
local root="$1" line dir
|
|
2386
2431
|
line="$(compat_control_dir "$root")" || return $?
|
|
2387
2432
|
dir="\${line#* }"
|
|
2388
|
-
[ -n "$dir" ] || return 1
|
|
2389
2433
|
printf '%s\\n' "$dir/.image-seeded"
|
|
2390
2434
|
}
|
|
2391
2435
|
`;
|
|
2392
2436
|
|
|
2393
|
-
// oh-asset:/home/runner/work/
|
|
2437
|
+
// oh-asset:/home/runner/work/agro/agro/.agro/scripts/check-host-port.sh
|
|
2394
2438
|
var check_host_port_default = `#!/usr/bin/env bash
|
|
2395
2439
|
|
|
2396
2440
|
set -eu
|
|
@@ -2476,7 +2520,7 @@ exit 0
|
|
|
2476
2520
|
|
|
2477
2521
|
// src/lib/registry.ts
|
|
2478
2522
|
var SANDBOX_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
2479
|
-
var DEFAULT_NAME_PREFIX = "
|
|
2523
|
+
var DEFAULT_NAME_PREFIX = "agro-sbx-";
|
|
2480
2524
|
function ohHome() {
|
|
2481
2525
|
return resolveUserStateHome(process.env);
|
|
2482
2526
|
}
|
|
@@ -2495,17 +2539,17 @@ function entryRoot(name) {
|
|
|
2495
2539
|
return join5(registryRoot(), name);
|
|
2496
2540
|
}
|
|
2497
2541
|
function isDirectory(path4) {
|
|
2498
|
-
return
|
|
2542
|
+
return statSync3(path4, { throwIfNoEntry: false })?.isDirectory() === true;
|
|
2499
2543
|
}
|
|
2500
2544
|
function listEntries() {
|
|
2501
2545
|
const root = registryRoot();
|
|
2502
2546
|
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) =>
|
|
2547
|
+
return readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => SANDBOX_NAME_PATTERN.test(name)).filter((name) => existsSync8(ohConfigPath(join5(root, name)))).sort();
|
|
2504
2548
|
}
|
|
2505
2549
|
function entryRepo(name) {
|
|
2506
2550
|
const config = readOhConfig(ohConfigPath(entryRoot(name)));
|
|
2507
|
-
const
|
|
2508
|
-
return
|
|
2551
|
+
const checkout = configCheckout(config);
|
|
2552
|
+
return checkout === void 0 ? void 0 : resolve7(checkout);
|
|
2509
2553
|
}
|
|
2510
2554
|
function runningContainerNames(run) {
|
|
2511
2555
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -2531,25 +2575,26 @@ function nextDefaultName(run = spawnRunner) {
|
|
|
2531
2575
|
}
|
|
2532
2576
|
}
|
|
2533
2577
|
function materialize(root, opts = {}) {
|
|
2534
|
-
const entry =
|
|
2578
|
+
const entry = resolve7(root);
|
|
2579
|
+
const scripts = join5(resolveProjectLayout(entry).controlDir, "scripts");
|
|
2535
2580
|
const files = [
|
|
2536
2581
|
{
|
|
2537
|
-
|
|
2538
|
-
body: opts.repo === void 0 ? docker_compose_image_only_default : docker_compose_default,
|
|
2582
|
+
dest: join5(entry, ".devcontainer", "docker-compose.yml"),
|
|
2583
|
+
body: (opts.checkout ?? opts.repo) === void 0 ? docker_compose_image_only_default : docker_compose_default,
|
|
2539
2584
|
mode: 420
|
|
2540
2585
|
},
|
|
2541
|
-
{
|
|
2542
|
-
{
|
|
2543
|
-
{
|
|
2544
|
-
{
|
|
2545
|
-
{
|
|
2586
|
+
{ dest: join5(entry, ".devcontainer", "docker-compose.ssh.yml"), body: docker_compose_ssh_default, mode: 420 },
|
|
2587
|
+
{ dest: join5(entry, ".devcontainer", "docker-compose.docker-sock.yml"), body: docker_compose_docker_sock_default, mode: 420 },
|
|
2588
|
+
{ dest: join5(scripts, "docker-compose.sh"), body: docker_compose_default2, mode: 493 },
|
|
2589
|
+
{ dest: join5(scripts, "compat.sh"), body: compat_default, mode: 420 },
|
|
2590
|
+
{ dest: join5(scripts, "check-host-port.sh"), body: check_host_port_default, mode: 493 }
|
|
2546
2591
|
];
|
|
2547
2592
|
for (const file of files) {
|
|
2548
|
-
const dest =
|
|
2549
|
-
if (!dest.startsWith(entry +
|
|
2593
|
+
const dest = resolve7(file.dest);
|
|
2594
|
+
if (!dest.startsWith(entry + sep3)) {
|
|
2550
2595
|
throw new Error(`refusing to materialize outside the sandbox entry: ${dest}`);
|
|
2551
2596
|
}
|
|
2552
|
-
|
|
2597
|
+
mkdirSync4(dirname6(dest), { recursive: true });
|
|
2553
2598
|
writeFileSync4(dest, file.body, { mode: file.mode, encoding: "utf8" });
|
|
2554
2599
|
}
|
|
2555
2600
|
}
|
|
@@ -2558,23 +2603,23 @@ function resolveSandboxRoot(opts = {}) {
|
|
|
2558
2603
|
const root = entryRoot(opts.name);
|
|
2559
2604
|
if (!isDirectory(root)) {
|
|
2560
2605
|
throw new Error(
|
|
2561
|
-
`no sandbox named \`${opts.name}\` in ${registryRoot()} \u2014 create one with
|
|
2606
|
+
`no sandbox named \`${opts.name}\` in ${registryRoot()} \u2014 create one with \`${activeBin()} sandbox install docker --name ${opts.name}\``
|
|
2562
2607
|
);
|
|
2563
2608
|
}
|
|
2564
2609
|
return root;
|
|
2565
2610
|
}
|
|
2566
2611
|
const names = listEntries();
|
|
2567
2612
|
if (names.length === 1) return entryRoot(names[0]);
|
|
2568
|
-
const cwd =
|
|
2613
|
+
const cwd = resolve7(opts.cwd ?? process.cwd());
|
|
2569
2614
|
for (const name of names) {
|
|
2570
2615
|
const repo = entryRepo(name);
|
|
2571
|
-
if (repo !== void 0 && (cwd === repo || cwd.startsWith(repo +
|
|
2616
|
+
if (repo !== void 0 && (cwd === repo || cwd.startsWith(repo + sep3))) {
|
|
2572
2617
|
return entryRoot(name);
|
|
2573
2618
|
}
|
|
2574
2619
|
}
|
|
2575
2620
|
if (names.length === 0) {
|
|
2576
2621
|
throw new Error(
|
|
2577
|
-
`no sandbox is registered in ${registryRoot()} \u2014 create one with
|
|
2622
|
+
`no sandbox is registered in ${registryRoot()} \u2014 create one with \`${activeBin()} sandbox install docker\``
|
|
2578
2623
|
);
|
|
2579
2624
|
}
|
|
2580
2625
|
throw new Error(
|
|
@@ -2582,333 +2627,131 @@ function resolveSandboxRoot(opts = {}) {
|
|
|
2582
2627
|
);
|
|
2583
2628
|
}
|
|
2584
2629
|
|
|
2585
|
-
// src/
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
}
|
|
2589
|
-
function configFieldList() {
|
|
2590
|
-
return ohConfigFieldPaths().map((path4) => ` ${path4}`).join("\n");
|
|
2591
|
-
}
|
|
2592
|
-
async function runConfigShow(opts, io) {
|
|
2593
|
-
const root = configRoot(opts);
|
|
2594
|
-
const config = readOhConfig(ohConfigPath(root));
|
|
2595
|
-
io.stdout(`${JSON.stringify(config, null, 2)}
|
|
2596
|
-
`);
|
|
2597
|
-
return 0;
|
|
2598
|
-
}
|
|
2599
|
-
async function runConfigSet(key, value, opts, io) {
|
|
2600
|
-
if (isSecretKey(key) || isSecretKey(key.toUpperCase())) {
|
|
2601
|
-
io.stderr(
|
|
2602
|
-
`oh config set: ${key.toUpperCase()} is a secret \u2014 oh.json is tracked by git.
|
|
2603
|
-
Set it with \`oh secret set ${key.toUpperCase()}\` instead.
|
|
2604
|
-
`
|
|
2605
|
-
);
|
|
2606
|
-
return 1;
|
|
2607
|
-
}
|
|
2608
|
-
if (!findOhConfigField(key)) {
|
|
2609
|
-
io.stderr(`oh config set: unknown field "${key}"
|
|
2630
|
+
// src/lib/secrets.ts
|
|
2631
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
|
|
2632
|
+
import { resolve as resolve8 } from "node:path";
|
|
2610
2633
|
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2634
|
+
// src/lib/env.ts
|
|
2635
|
+
import {
|
|
2636
|
+
readFileSync as readFileSync7,
|
|
2637
|
+
writeFileSync as writeFileSync5,
|
|
2638
|
+
existsSync as existsSync9,
|
|
2639
|
+
fsyncSync as fsyncSync2,
|
|
2640
|
+
renameSync as renameSync4,
|
|
2641
|
+
unlinkSync as unlinkSync4,
|
|
2642
|
+
openSync as openSync2,
|
|
2643
|
+
closeSync as closeSync2
|
|
2644
|
+
} from "node:fs";
|
|
2645
|
+
function loadEnvInto(envPath, env) {
|
|
2646
|
+
if (!existsSync9(envPath)) return;
|
|
2647
|
+
const content = readFileSync7(envPath, "utf8");
|
|
2648
|
+
for (const rawLine of content.split("\n")) {
|
|
2649
|
+
const line = rawLine.trimEnd();
|
|
2650
|
+
if (line === "" || /^\s*#/.test(line)) continue;
|
|
2651
|
+
const eqIdx = line.indexOf("=");
|
|
2652
|
+
if (eqIdx < 1) continue;
|
|
2653
|
+
const key = line.slice(0, eqIdx).trim();
|
|
2654
|
+
const value = line.slice(eqIdx + 1);
|
|
2655
|
+
if (key === "") continue;
|
|
2656
|
+
if (env[key] === void 0) env[key] = value;
|
|
2615
2657
|
}
|
|
2616
|
-
|
|
2617
|
-
|
|
2658
|
+
}
|
|
2659
|
+
function writeEnvFile(envPath, content) {
|
|
2660
|
+
const tmpPath = `${envPath}.tmp.${process.pid}`;
|
|
2661
|
+
let fd;
|
|
2618
2662
|
try {
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2663
|
+
fd = openSync2(tmpPath, "w", 384);
|
|
2664
|
+
writeFileSync5(tmpPath, content, "utf8");
|
|
2665
|
+
fsyncSync2(fd);
|
|
2666
|
+
closeSync2(fd);
|
|
2667
|
+
fd = void 0;
|
|
2668
|
+
renameSync4(tmpPath, envPath);
|
|
2669
|
+
} catch (err) {
|
|
2670
|
+
if (fd !== void 0) {
|
|
2671
|
+
try {
|
|
2672
|
+
closeSync2(fd);
|
|
2673
|
+
} catch {
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
try {
|
|
2677
|
+
unlinkSync4(tmpPath);
|
|
2678
|
+
} catch {
|
|
2679
|
+
}
|
|
2680
|
+
throw err;
|
|
2624
2681
|
}
|
|
2625
|
-
io.stdout(
|
|
2626
|
-
outcome === "already-set" ? `oh.json: ${key} already ${value}
|
|
2627
|
-
` : `oh.json: set ${key}=${value} (${outcome})
|
|
2628
|
-
`
|
|
2629
|
-
);
|
|
2630
|
-
return 0;
|
|
2631
2682
|
}
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2683
|
+
|
|
2684
|
+
// src/lib/secrets.ts
|
|
2685
|
+
var SECRETS_FILE = ".env";
|
|
2686
|
+
var SECRET_KEYS = [
|
|
2687
|
+
"GH_TOKEN",
|
|
2688
|
+
"XAI_API_KEY",
|
|
2689
|
+
"META_API_KEY",
|
|
2690
|
+
"SANDBOX_PASSWORD",
|
|
2691
|
+
"PI_SLACK_APP_TOKEN",
|
|
2692
|
+
"PI_SLACK_BOT_TOKEN",
|
|
2693
|
+
"LANGFUSE_PUBLIC_KEY",
|
|
2694
|
+
"LANGFUSE_SECRET_KEY"
|
|
2695
|
+
];
|
|
2696
|
+
var ALLOWED = new Set(SECRET_KEYS);
|
|
2697
|
+
function secretsFilePath(root) {
|
|
2698
|
+
return resolve8(root, SECRETS_FILE);
|
|
2635
2699
|
}
|
|
2636
|
-
function
|
|
2637
|
-
return
|
|
2700
|
+
function isSecretKey(key) {
|
|
2701
|
+
return ALLOWED.has(key);
|
|
2638
2702
|
}
|
|
2639
|
-
function
|
|
2640
|
-
return
|
|
2641
|
-
|
|
2642
|
-
{
|
|
2643
|
-
|
|
2644
|
-
{ cmd: "git", args: ["push", "-u", "origin", "HEAD"] }
|
|
2645
|
-
];
|
|
2703
|
+
function assertSecretKey(key) {
|
|
2704
|
+
if (ALLOWED.has(key)) return;
|
|
2705
|
+
throw new Error(
|
|
2706
|
+
`${key} is not a secret \u2014 non-secret settings live in oh.json; use \`${activeBin()} config set\` instead`
|
|
2707
|
+
);
|
|
2646
2708
|
}
|
|
2647
|
-
function
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2709
|
+
function readSecret(root, key) {
|
|
2710
|
+
assertSecretKey(key);
|
|
2711
|
+
const file = secretsFilePath(root);
|
|
2712
|
+
if (!existsSync10(file)) return void 0;
|
|
2713
|
+
const env = {};
|
|
2714
|
+
loadEnvInto(file, env);
|
|
2715
|
+
const value = env[key]?.trim();
|
|
2716
|
+
return value === void 0 || value === "" ? void 0 : stripQuotes2(value);
|
|
2655
2717
|
}
|
|
2656
|
-
function
|
|
2657
|
-
|
|
2658
|
-
if (value
|
|
2659
|
-
|
|
2660
|
-
return `invalid ${kind} "${value}" (use letters, digits, ".", "_" or "-")`;
|
|
2661
|
-
}
|
|
2662
|
-
return void 0;
|
|
2663
|
-
}
|
|
2664
|
-
async function askOn(io, question) {
|
|
2665
|
-
const askFn = io.ask ?? ask;
|
|
2666
|
-
return (await askFn(question)).trim();
|
|
2667
|
-
}
|
|
2668
|
-
async function confirmOn(io, question, defaultYes) {
|
|
2669
|
-
const answer = (await askOn(io, `${question} ${defaultYes ? "[Y/n]" : "[y/N]"}`)).toLowerCase();
|
|
2670
|
-
if (answer === "") return defaultYes;
|
|
2671
|
-
return /^y/.test(answer);
|
|
2672
|
-
}
|
|
2673
|
-
async function askVisibility(io) {
|
|
2674
|
-
io.stdout(" Repository visibility\n 1) private (recommended)\n 2) public\n");
|
|
2675
|
-
for (; ; ) {
|
|
2676
|
-
const answer = await askOn(io, "Choose [1-2] (default 1):");
|
|
2677
|
-
if (answer === "" || answer === "1") return "private";
|
|
2678
|
-
if (answer === "2") return "public";
|
|
2679
|
-
io.stderr(" Invalid choice. Pick 1 or 2.\n");
|
|
2680
|
-
}
|
|
2681
|
-
}
|
|
2682
|
-
async function runConfigRepo(opts, io) {
|
|
2683
|
-
const run = opts.run ?? spawnRunner;
|
|
2684
|
-
const isTTY = io.isTTY ?? process.stdin.isTTY === true;
|
|
2685
|
-
if (!isTTY) {
|
|
2686
|
-
io.stderr(
|
|
2687
|
-
"oh config repo: creating a repo on your GitHub account needs an interactive terminal; re-run it from a TTY.\n"
|
|
2688
|
-
);
|
|
2689
|
-
return 1;
|
|
2690
|
-
}
|
|
2691
|
-
io.stdout(`
|
|
2692
|
-
${bold("Version this harness in your own repo")}
|
|
2693
|
-
`);
|
|
2694
|
-
io.stdout(
|
|
2695
|
-
` Creates a repo on your GitHub account, keeps the upstream you cloned from
|
|
2696
|
-
as the "${UPSTREAM_REMOTE}" remote, and pushes this branch to yours.
|
|
2697
|
-
`
|
|
2698
|
-
);
|
|
2699
|
-
if (!await confirmOn(io, "Version this harness in your own repo?", false)) {
|
|
2700
|
-
printManual(
|
|
2701
|
-
io,
|
|
2702
|
-
manualSteps("<your-user>", "<repo>", "private"),
|
|
2703
|
-
"Skipped \u2014 nothing was created and no remote was touched. To do it later, run `oh config repo`, or by hand:"
|
|
2704
|
-
);
|
|
2705
|
-
return 0;
|
|
2706
|
-
}
|
|
2707
|
-
const owner = await askOn(io, "GitHub owner (user or org):");
|
|
2708
|
-
const ownerError = validateSegment("GitHub owner", owner);
|
|
2709
|
-
if (ownerError) {
|
|
2710
|
-
io.stderr(`oh config repo: ${ownerError}
|
|
2711
|
-
`);
|
|
2712
|
-
return 1;
|
|
2713
|
-
}
|
|
2714
|
-
const name = await askOn(io, "Repository name [openharness]:");
|
|
2715
|
-
const repoName = name === "" ? "openharness" : name;
|
|
2716
|
-
const nameError = validateSegment("repository name", repoName);
|
|
2717
|
-
if (nameError) {
|
|
2718
|
-
io.stderr(`oh config repo: ${nameError}
|
|
2719
|
-
`);
|
|
2720
|
-
return 1;
|
|
2721
|
-
}
|
|
2722
|
-
const visibility = await askVisibility(io);
|
|
2723
|
-
const url = repoUrl(owner, repoName);
|
|
2724
|
-
const manual = manualSteps(owner, repoName, visibility);
|
|
2725
|
-
let root;
|
|
2726
|
-
try {
|
|
2727
|
-
root = resolveProjectRoot(opts.cwd);
|
|
2728
|
-
} catch (error) {
|
|
2729
|
-
io.stderr(`oh config repo: ${error instanceof Error ? error.message : String(error)}
|
|
2730
|
-
`);
|
|
2731
|
-
printManual(io, manual, "Nothing was changed. Finish this by hand from your checkout:");
|
|
2732
|
-
return 1;
|
|
2733
|
-
}
|
|
2734
|
-
const capture2 = { stdio: "capture", cwd: root };
|
|
2735
|
-
const auth = run("gh", ["auth", "status"], capture2);
|
|
2736
|
-
if (auth.error?.code === "ENOENT") {
|
|
2737
|
-
printManual(io, manual, "gh is not on PATH \u2014 finish this by hand:");
|
|
2738
|
-
return 0;
|
|
2739
|
-
}
|
|
2740
|
-
assertSpawned(auth, "gh auth status");
|
|
2741
|
-
if (auth.status !== 0) {
|
|
2742
|
-
printManual(
|
|
2743
|
-
io,
|
|
2744
|
-
manual,
|
|
2745
|
-
"gh is not authenticated (`gh auth login`) \u2014 finish this by hand once it is:"
|
|
2746
|
-
);
|
|
2747
|
-
return 0;
|
|
2748
|
-
}
|
|
2749
|
-
const remotesResult = run("git", ["remote"], capture2);
|
|
2750
|
-
assertSpawned(remotesResult, "git remote");
|
|
2751
|
-
if (remotesResult.status !== 0) {
|
|
2752
|
-
printManual(io, manual, "`git remote` failed \u2014 is this a git repo? Finish this by hand:");
|
|
2753
|
-
return 0;
|
|
2754
|
-
}
|
|
2755
|
-
const remotes = (remotesResult.stdout ?? "").split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
2756
|
-
let originUrl = "";
|
|
2757
|
-
if (remotes.includes("origin")) {
|
|
2758
|
-
const shown = run("git", ["remote", "get-url", "origin"], capture2);
|
|
2759
|
-
assertSpawned(shown, "git remote get-url origin");
|
|
2760
|
-
originUrl = (shown.stdout ?? "").trim();
|
|
2761
|
-
}
|
|
2762
|
-
const originIsTarget = originUrl !== "" && sameRemote(originUrl, owner, repoName);
|
|
2763
|
-
if (!originIsTarget && remotes.includes("origin") && remotes.includes(UPSTREAM_REMOTE)) {
|
|
2764
|
-
io.stderr(
|
|
2765
|
-
`oh config repo: origin already points at ${originUrl} and a "${UPSTREAM_REMOTE}" remote already exists \u2014 nothing was changed. Sort the remotes out by hand:
|
|
2766
|
-
`
|
|
2767
|
-
);
|
|
2768
|
-
printManual(io, manual.slice(2), "Remaining steps:");
|
|
2769
|
-
return 1;
|
|
2770
|
-
}
|
|
2771
|
-
const steps = [];
|
|
2772
|
-
const exists = run("gh", ["repo", "view", `${owner}/${repoName}`], capture2);
|
|
2773
|
-
assertSpawned(exists, "gh repo view");
|
|
2774
|
-
if (exists.status === 0) {
|
|
2775
|
-
io.stdout(` ${owner}/${repoName} already exists \u2014 skipping creation.
|
|
2776
|
-
`);
|
|
2777
|
-
} else {
|
|
2778
|
-
steps.push(manual[0]);
|
|
2779
|
-
}
|
|
2780
|
-
if (originIsTarget) {
|
|
2781
|
-
io.stdout(" origin already points at your repo \u2014 leaving the remotes alone.\n");
|
|
2782
|
-
} else {
|
|
2783
|
-
if (remotes.includes("origin")) steps.push(manual[1]);
|
|
2784
|
-
steps.push(manual[2]);
|
|
2785
|
-
}
|
|
2786
|
-
steps.push(manual[3]);
|
|
2787
|
-
for (let i = 0; i < steps.length; i++) {
|
|
2788
|
-
const step = steps[i];
|
|
2789
|
-
io.stdout(` $ ${shellLine(step)}
|
|
2790
|
-
`);
|
|
2791
|
-
const result = run(step.cmd, step.args, { stdio: "inherit", cwd: root });
|
|
2792
|
-
if (result.error || result.status !== 0) {
|
|
2793
|
-
const detail = result.error?.message ?? `exit ${result.status}`;
|
|
2794
|
-
io.stderr(`oh config repo: ${shellLine(step)} failed (${detail}) \u2014 stopping here.
|
|
2795
|
-
`);
|
|
2796
|
-
const remaining = steps.slice(i + 1);
|
|
2797
|
-
if (remaining.length > 0) {
|
|
2798
|
-
printManual(io, remaining, "Not run. Finish by hand once the failure is fixed:");
|
|
2799
|
-
}
|
|
2800
|
-
return 1;
|
|
2801
|
-
}
|
|
2802
|
-
}
|
|
2803
|
-
io.stdout(
|
|
2804
|
-
`
|
|
2805
|
-
Done \u2014 origin is ${url}${remotes.includes("origin") && !originIsTarget ? `, upstream kept as "${UPSTREAM_REMOTE}"` : ""}.
|
|
2806
|
-
`
|
|
2807
|
-
);
|
|
2808
|
-
return 0;
|
|
2809
|
-
}
|
|
2810
|
-
function sameRemote(url, owner, name) {
|
|
2811
|
-
const normalized = url.replace(/\.git$/, "").toLowerCase();
|
|
2812
|
-
const slug = `${owner}/${name}`.toLowerCase();
|
|
2813
|
-
return normalized.endsWith(`:${slug}`) || normalized.endsWith(`/${slug}`);
|
|
2814
|
-
}
|
|
2815
|
-
|
|
2816
|
-
// src/commands/secret.ts
|
|
2817
|
-
import { existsSync as existsSync12 } from "node:fs";
|
|
2818
|
-
import { join as join7 } from "node:path";
|
|
2819
|
-
|
|
2820
|
-
// src/lib/gitignore.ts
|
|
2821
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2822
|
-
import { dirname as dirname5, join as join6 } from "node:path";
|
|
2823
|
-
function appendGitignoreLines(root, lines) {
|
|
2824
|
-
const target = join6(root, ".gitignore");
|
|
2825
|
-
const existing = existsSync11(target) ? readFileSync9(target, "utf8") : "";
|
|
2826
|
-
const present = new Set(existing.split("\n").map((line) => line.trimEnd()));
|
|
2827
|
-
const added = [];
|
|
2828
|
-
for (const line of lines) {
|
|
2829
|
-
const key = line.trimEnd();
|
|
2830
|
-
if (key === "" || present.has(key)) continue;
|
|
2831
|
-
present.add(key);
|
|
2832
|
-
added.push(line);
|
|
2718
|
+
function setSecret(root, key, value) {
|
|
2719
|
+
assertSecretKey(key);
|
|
2720
|
+
if (/[\r\n]/.test(value)) {
|
|
2721
|
+
throw new Error(`${key} value must not contain a newline`);
|
|
2833
2722
|
}
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
mkdirSync4(dirname5(target), { recursive: true });
|
|
2840
|
-
writeFileSync5(target, output, "utf8");
|
|
2841
|
-
return added;
|
|
2842
|
-
}
|
|
2843
|
-
|
|
2844
|
-
// src/commands/secret.ts
|
|
2845
|
-
function secretsRoot(opts) {
|
|
2846
|
-
return opts.sandbox === void 0 ? resolveProjectRoot(opts.cwd) : resolveSandboxRoot({ name: opts.sandbox });
|
|
2847
|
-
}
|
|
2848
|
-
function ignoreSecretsFile(root) {
|
|
2849
|
-
if (!existsSync12(join7(root, ".git"))) return;
|
|
2850
|
-
appendGitignoreLines(root, [".env"]);
|
|
2851
|
-
}
|
|
2852
|
-
function secretKeyList() {
|
|
2853
|
-
return SECRET_KEYS.map((key) => ` ${key}`).join("\n");
|
|
2723
|
+
const file = secretsFilePath(root);
|
|
2724
|
+
assertInRoot(file, resolve8(root));
|
|
2725
|
+
const original = existsSync10(file) ? readFileSync8(file, "utf8") : "";
|
|
2726
|
+
const { content } = setKeyInEnv(original, key, value);
|
|
2727
|
+
writeEnvFile(file, content);
|
|
2854
2728
|
}
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
);
|
|
2865
|
-
return 1;
|
|
2866
|
-
}
|
|
2867
|
-
const root = secretsRoot(opts);
|
|
2868
|
-
const askSecret2 = io.askSecret ?? askSecret;
|
|
2869
|
-
const value = (await askSecret2(`Value for ${bold(key)} (input hidden):`)).trim();
|
|
2870
|
-
if (value === "") {
|
|
2871
|
-
io.stderr(`oh secret set: no value entered \u2014 ${key} unchanged
|
|
2872
|
-
`);
|
|
2873
|
-
return 1;
|
|
2874
|
-
}
|
|
2875
|
-
try {
|
|
2876
|
-
setSecret(root, key, value);
|
|
2877
|
-
ignoreSecretsFile(root);
|
|
2878
|
-
} catch (error) {
|
|
2879
|
-
io.stderr(`oh secret set: ${error instanceof Error ? error.message : String(error)}
|
|
2880
|
-
`);
|
|
2881
|
-
return 1;
|
|
2882
|
-
}
|
|
2883
|
-
io.stdout(`.env: set ${key}=${redact(value)}
|
|
2884
|
-
`);
|
|
2885
|
-
return 0;
|
|
2729
|
+
function listSecretKeys(root) {
|
|
2730
|
+
const file = secretsFilePath(root);
|
|
2731
|
+
if (!existsSync10(file)) return [];
|
|
2732
|
+
const env = {};
|
|
2733
|
+
loadEnvInto(file, env);
|
|
2734
|
+
return SECRET_KEYS.filter((key) => {
|
|
2735
|
+
const value = env[key]?.trim();
|
|
2736
|
+
return value !== void 0 && value !== "";
|
|
2737
|
+
});
|
|
2886
2738
|
}
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
if (keys.length === 0) {
|
|
2891
|
-
io.stdout("no secrets set \u2014 write one with `oh secret set <KEY>`\n");
|
|
2892
|
-
return 0;
|
|
2893
|
-
}
|
|
2894
|
-
const width = Math.max(...keys.map((key) => key.length));
|
|
2895
|
-
for (const key of keys) {
|
|
2896
|
-
const value = readSecret(root, key);
|
|
2897
|
-
io.stdout(`${key.padEnd(width)} ${value === void 0 ? "\u2014" : redact(value)}
|
|
2898
|
-
`);
|
|
2739
|
+
function stripQuotes2(s) {
|
|
2740
|
+
if (s.length >= 2 && (s[0] === '"' && s.endsWith('"') || s[0] === "'" && s.endsWith("'"))) {
|
|
2741
|
+
return s.slice(1, -1);
|
|
2899
2742
|
}
|
|
2900
|
-
return
|
|
2743
|
+
return s;
|
|
2901
2744
|
}
|
|
2902
2745
|
|
|
2903
2746
|
// src/commands/lifecycle.ts
|
|
2904
|
-
import { existsSync as
|
|
2747
|
+
import { existsSync as existsSync12, mkdtempSync, readFileSync as readFileSync9, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2905
2748
|
import { tmpdir } from "node:os";
|
|
2906
|
-
import { basename as
|
|
2749
|
+
import { basename as basename5, join as join6, sep as sep4 } from "node:path";
|
|
2907
2750
|
|
|
2908
2751
|
// src/lib/execution/detect.ts
|
|
2909
|
-
import { existsSync as
|
|
2752
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
2910
2753
|
var SANDBOX_MARKER_FILE = "/.dockerenv";
|
|
2911
|
-
function runningInsideSandbox(env = process.env, fileExists =
|
|
2754
|
+
function runningInsideSandbox(env = process.env, fileExists = existsSync11) {
|
|
2912
2755
|
const override = resolveAliasedEnv(env, "EXECUTION_TARGET").value;
|
|
2913
2756
|
if (override === "local") return true;
|
|
2914
2757
|
if (override === "docker-compose") return false;
|
|
@@ -3065,10 +2908,10 @@ var LocalExecutionTarget = class {
|
|
|
3065
2908
|
this.workspace = { hostRoot: opts.projectRoot, targetRoot: opts.projectRoot };
|
|
3066
2909
|
}
|
|
3067
2910
|
async provision() {
|
|
3068
|
-
throw new HostOnlyError(
|
|
2911
|
+
throw new HostOnlyError(`\`${activeBin()} sandbox\``);
|
|
3069
2912
|
}
|
|
3070
2913
|
async destroy() {
|
|
3071
|
-
throw new HostOnlyError(
|
|
2914
|
+
throw new HostOnlyError(`\`${activeBin()} destroy\``);
|
|
3072
2915
|
}
|
|
3073
2916
|
async status() {
|
|
3074
2917
|
return "ready";
|
|
@@ -3151,6 +2994,7 @@ var RETIRED_KEYS = [
|
|
|
3151
2994
|
"PROJECTS_DIR",
|
|
3152
2995
|
"CRONS_DIR",
|
|
3153
2996
|
"OH_PROJECT_ROOT",
|
|
2997
|
+
"AGRO_PROJECT_ROOT",
|
|
3154
2998
|
"INSTALL_DEEPAGENTS",
|
|
3155
2999
|
"INSTALL_OPENCODE",
|
|
3156
3000
|
"INSTALL_GROK_BUILD",
|
|
@@ -3174,24 +3018,24 @@ function renderComposeVars(config) {
|
|
|
3174
3018
|
};
|
|
3175
3019
|
put("SANDBOX_NAME", config.name);
|
|
3176
3020
|
put("TZ", config.timezone);
|
|
3177
|
-
put("
|
|
3178
|
-
put("
|
|
3021
|
+
put("AGRO_HOME_MOUNT", config.storage?.homePath);
|
|
3022
|
+
put("AGRO_REPO_DIR", configCheckout(config));
|
|
3179
3023
|
put("GIT_USER_NAME", config.git?.userName);
|
|
3180
3024
|
put("GIT_USER_EMAIL", config.git?.userEmail);
|
|
3181
3025
|
put("DOCKER_SOCKET", config.access?.dockerSocket);
|
|
3182
3026
|
put("SANDBOX_SSH", config.access?.ssh);
|
|
3183
3027
|
put("SANDBOX_SSH_PORT", config.access?.sshPort);
|
|
3184
|
-
put("
|
|
3185
|
-
put("
|
|
3028
|
+
put("AGRO_SANDBOX_IMAGE", config.image?.ref);
|
|
3029
|
+
put("AGRO_PULL_POLICY", config.image?.pullPolicy);
|
|
3186
3030
|
for (const { key, value } of out) {
|
|
3187
3031
|
if (isSecretKey(key)) {
|
|
3188
|
-
throw new Error(`refusing to render secret ${key} from
|
|
3032
|
+
throw new Error(`refusing to render secret ${key} from the sandbox config`);
|
|
3189
3033
|
}
|
|
3190
3034
|
if (RETIRED_KEYS.includes(key)) {
|
|
3191
3035
|
throw new Error(`refusing to render retired variable ${key}`);
|
|
3192
3036
|
}
|
|
3193
3037
|
if (/[\r\n]/.test(value)) {
|
|
3194
|
-
throw new Error(`
|
|
3038
|
+
throw new Error(`config value for ${key} must not contain a newline`);
|
|
3195
3039
|
}
|
|
3196
3040
|
}
|
|
3197
3041
|
return out;
|
|
@@ -3205,10 +3049,10 @@ function renderComposeEnv(config) {
|
|
|
3205
3049
|
// src/commands/lifecycle.ts
|
|
3206
3050
|
var COMPOSE_ENV_DIR_PREFIX = "oh-compose-env-";
|
|
3207
3051
|
function composeEnvDir() {
|
|
3208
|
-
return mkdtempSync(
|
|
3052
|
+
return mkdtempSync(join6(tmpdir(), COMPOSE_ENV_DIR_PREFIX));
|
|
3209
3053
|
}
|
|
3210
3054
|
function writeComposeEnv(root, dir) {
|
|
3211
|
-
const file =
|
|
3055
|
+
const file = join6(dir, "compose.env");
|
|
3212
3056
|
writeFileSync6(file, renderComposeEnv(readOhConfig(ohConfigPath(root))), {
|
|
3213
3057
|
mode: 384,
|
|
3214
3058
|
encoding: "utf8"
|
|
@@ -3216,7 +3060,7 @@ function writeComposeEnv(root, dir) {
|
|
|
3216
3060
|
return file;
|
|
3217
3061
|
}
|
|
3218
3062
|
function withComposeEnvFile(root, fn) {
|
|
3219
|
-
if (!
|
|
3063
|
+
if (!existsSync12(ohConfigPath(root))) return fn([]);
|
|
3220
3064
|
const dir = composeEnvDir();
|
|
3221
3065
|
try {
|
|
3222
3066
|
return fn(["--extra-env-file", writeComposeEnv(root, dir)]);
|
|
@@ -3225,7 +3069,7 @@ function withComposeEnvFile(root, fn) {
|
|
|
3225
3069
|
}
|
|
3226
3070
|
}
|
|
3227
3071
|
async function withComposeEnvFileAsync(root, fn) {
|
|
3228
|
-
if (!
|
|
3072
|
+
if (!existsSync12(ohConfigPath(root))) return await fn([]);
|
|
3229
3073
|
const dir = composeEnvDir();
|
|
3230
3074
|
try {
|
|
3231
3075
|
return await fn(["--extra-env-file", writeComposeEnv(root, dir)]);
|
|
@@ -3235,17 +3079,17 @@ async function withComposeEnvFileAsync(root, fn) {
|
|
|
3235
3079
|
}
|
|
3236
3080
|
function sandboxRoot(opts) {
|
|
3237
3081
|
const root = resolveSandboxRoot({ name: opts.name, cwd: opts.cwd });
|
|
3238
|
-
if (
|
|
3082
|
+
if (existsSync12(ohConfigPath(root))) {
|
|
3239
3083
|
const repo = configuredString(root, "repo");
|
|
3240
3084
|
materialize(root, repo === void 0 ? {} : { repo });
|
|
3241
3085
|
}
|
|
3242
3086
|
return root;
|
|
3243
3087
|
}
|
|
3244
|
-
var DEFAULT_CONTAINER_NAME =
|
|
3245
|
-
var DEFAULT_SANDBOX_IMAGE = "ghcr.io/mifunedev/
|
|
3088
|
+
var DEFAULT_CONTAINER_NAME = DEFAULT_SANDBOX_NAME;
|
|
3089
|
+
var DEFAULT_SANDBOX_IMAGE = "ghcr.io/mifunedev/agro:latest";
|
|
3246
3090
|
function configuredField(root, path4) {
|
|
3247
3091
|
const file = ohConfigPath(root);
|
|
3248
|
-
if (!
|
|
3092
|
+
if (!existsSync12(file)) return void 0;
|
|
3249
3093
|
try {
|
|
3250
3094
|
return getOhConfigValue(readOhConfig(file), path4);
|
|
3251
3095
|
} catch {
|
|
@@ -3263,18 +3107,18 @@ function fromProcessEnv(key) {
|
|
|
3263
3107
|
function dockerSocketConfigured(root) {
|
|
3264
3108
|
return configuredField(root, "access.dockerSocket") !== void 0;
|
|
3265
3109
|
}
|
|
3266
|
-
async function maybePromptDockerSocket(root, io) {
|
|
3110
|
+
async function maybePromptDockerSocket(root, io, bin) {
|
|
3267
3111
|
if (dockerSocketConfigured(root)) return;
|
|
3268
3112
|
const interactive = process.stdin.isTTY === true || io.ask !== void 0;
|
|
3269
3113
|
if (!interactive) return;
|
|
3270
|
-
const envDir =
|
|
3271
|
-
if (!
|
|
3114
|
+
const envDir = join6(root, ".devcontainer");
|
|
3115
|
+
if (!existsSync12(envDir)) return;
|
|
3272
3116
|
const askFn = io.ask ?? ask;
|
|
3273
3117
|
const answer = (await askFn(
|
|
3274
3118
|
"Mount host Docker socket into the sandbox? (effectively host root \u2014 enable only if the agent must drive Docker) [y/N]"
|
|
3275
3119
|
)).trim().toLowerCase();
|
|
3276
3120
|
const enabled = answer === "y" || answer === "yes";
|
|
3277
|
-
setEnvValue(root, "DOCKER_SOCKET", enabled ? "true" : "false");
|
|
3121
|
+
setEnvValue(root, "DOCKER_SOCKET", enabled ? "true" : "false", bin);
|
|
3278
3122
|
io.stdout(
|
|
3279
3123
|
enabled ? "DOCKER_SOCKET=true \u2014 host Docker socket will be mounted\n" : "DOCKER_SOCKET=false \u2014 host Docker socket stays unmounted\n"
|
|
3280
3124
|
);
|
|
@@ -3288,7 +3132,7 @@ async function runSandbox(opts, io) {
|
|
|
3288
3132
|
const useImage = opts.image === true || opts.imageRef !== void 0;
|
|
3289
3133
|
const useNoBuild = useImage || opts.noBuild === true;
|
|
3290
3134
|
const imageRef = useImage ? opts.imageRef ?? configuredImage(root) ?? DEFAULT_SANDBOX_IMAGE : void 0;
|
|
3291
|
-
const env = imageRef === void 0 ? void 0 : { ...process.env,
|
|
3135
|
+
const env = imageRef === void 0 ? void 0 : { ...process.env, ...aliasedEnvPair("SANDBOX_IMAGE", imageRef) };
|
|
3292
3136
|
if (opts.printArgv === true) {
|
|
3293
3137
|
const script = requireLifecycleScript(root, "docker-compose.sh");
|
|
3294
3138
|
return withComposeEnvFile(root, (extraArgs) => {
|
|
@@ -3311,11 +3155,11 @@ async function runSandbox(opts, io) {
|
|
|
3311
3155
|
});
|
|
3312
3156
|
}
|
|
3313
3157
|
if (runningInsideSandbox()) {
|
|
3314
|
-
io.stderr(`${new HostOnlyError(
|
|
3158
|
+
io.stderr(`${new HostOnlyError(`\`${opts.bin} sandbox\``).message}
|
|
3315
3159
|
`);
|
|
3316
3160
|
return 1;
|
|
3317
3161
|
}
|
|
3318
|
-
await maybePromptDockerSocket(root, io);
|
|
3162
|
+
await maybePromptDockerSocket(root, io, opts.bin);
|
|
3319
3163
|
requireLifecycleScript(root, "docker-compose.sh");
|
|
3320
3164
|
if (imageRef !== void 0) {
|
|
3321
3165
|
io.stdout(`image mode: ${imageRef} (skipping local build)
|
|
@@ -3358,142 +3202,487 @@ function runShell(opts, io) {
|
|
|
3358
3202
|
code = target.attach({ argv: ["zsh"], user: "sandbox" });
|
|
3359
3203
|
} catch (err) {
|
|
3360
3204
|
if (err instanceof ExecutionSpawnError && err.code === "ENOENT") {
|
|
3361
|
-
throw new Error(
|
|
3205
|
+
throw new Error(`docker is required for \`${opts.bin} shell\` but was not found on PATH`);
|
|
3206
|
+
}
|
|
3207
|
+
throw err;
|
|
3208
|
+
}
|
|
3209
|
+
if (code !== 0) {
|
|
3210
|
+
io.stderr(
|
|
3211
|
+
`container \`${name}\` not running? start it with \`${opts.bin} sandbox install docker\`
|
|
3212
|
+
`
|
|
3213
|
+
);
|
|
3214
|
+
}
|
|
3215
|
+
return code;
|
|
3216
|
+
}
|
|
3217
|
+
var COMPOSE_VERBS = Object.freeze({
|
|
3218
|
+
stop: Object.freeze(["stop"]),
|
|
3219
|
+
restart: Object.freeze(["restart"]),
|
|
3220
|
+
logs: Object.freeze(["logs", "-f"]),
|
|
3221
|
+
ps: Object.freeze(["ps"]),
|
|
3222
|
+
destroy: Object.freeze(["down", "-v"])
|
|
3223
|
+
});
|
|
3224
|
+
function composeVerbs() {
|
|
3225
|
+
return Object.keys(COMPOSE_VERBS);
|
|
3226
|
+
}
|
|
3227
|
+
function runComposeVerb(verb, opts, extra = []) {
|
|
3228
|
+
const run = opts.run ?? spawnRunner;
|
|
3229
|
+
const root = sandboxRoot(opts);
|
|
3230
|
+
const script = requireLifecycleScript(root, "docker-compose.sh");
|
|
3231
|
+
return withComposeEnvFile(root, (extraArgs) => {
|
|
3232
|
+
const r = run("bash", [script, ...extraArgs, ...COMPOSE_VERBS[verb], ...extra], {
|
|
3233
|
+
stdio: "inherit"
|
|
3234
|
+
});
|
|
3235
|
+
assertSpawned(r, `bash ${script} ${verb}`);
|
|
3236
|
+
return r.status ?? 1;
|
|
3237
|
+
});
|
|
3238
|
+
}
|
|
3239
|
+
function runComposeConfig(opts, extra = []) {
|
|
3240
|
+
const run = opts.run ?? spawnRunner;
|
|
3241
|
+
const root = sandboxRoot(opts);
|
|
3242
|
+
const script = requireLifecycleScript(root, "docker-compose.sh");
|
|
3243
|
+
return withComposeEnvFile(root, (extraArgs) => {
|
|
3244
|
+
const r = run("bash", [script, ...extraArgs, "config", ...extra], { stdio: "inherit" });
|
|
3245
|
+
assertSpawned(r, `bash ${script} config`);
|
|
3246
|
+
return r.status ?? 1;
|
|
3247
|
+
});
|
|
3248
|
+
}
|
|
3249
|
+
function namedVolumes(root) {
|
|
3250
|
+
let text;
|
|
3251
|
+
try {
|
|
3252
|
+
text = readFileSync9(join6(root, ".devcontainer", "docker-compose.yml"), "utf8");
|
|
3253
|
+
} catch {
|
|
3254
|
+
return [];
|
|
3255
|
+
}
|
|
3256
|
+
const lines = text.split("\n");
|
|
3257
|
+
const start = lines.findIndex((line) => /^volumes:\s*$/.test(line));
|
|
3258
|
+
if (start === -1) return [];
|
|
3259
|
+
const names = [];
|
|
3260
|
+
for (const line of lines.slice(start + 1)) {
|
|
3261
|
+
if (/^\S/.test(line)) break;
|
|
3262
|
+
const match = /^ {2}([A-Za-z0-9][A-Za-z0-9_.-]*):\s*$/.exec(line);
|
|
3263
|
+
if (match) names.push(match[1]);
|
|
3264
|
+
}
|
|
3265
|
+
return names;
|
|
3266
|
+
}
|
|
3267
|
+
function destroyConfirmationPhrase(root) {
|
|
3268
|
+
return configuredContainerName(root) ?? DEFAULT_CONTAINER_NAME;
|
|
3269
|
+
}
|
|
3270
|
+
async function runDestroy(opts, io) {
|
|
3271
|
+
const root = sandboxRoot(opts);
|
|
3272
|
+
const name = destroyConfirmationPhrase(root);
|
|
3273
|
+
if (opts.yes !== true) {
|
|
3274
|
+
const interactive = process.stdin.isTTY === true || io.ask !== void 0;
|
|
3275
|
+
if (!interactive) {
|
|
3276
|
+
io.stderr(
|
|
3277
|
+
`${opts.bin} destroy: refusing to destroy \`${name}\` without a terminal \u2014 re-run with --yes to confirm non-interactively
|
|
3278
|
+
`
|
|
3279
|
+
);
|
|
3280
|
+
return 1;
|
|
3281
|
+
}
|
|
3282
|
+
const homePath = readOhConfig(ohConfigPath(root)).storage?.homePath;
|
|
3283
|
+
io.stdout(`
|
|
3284
|
+
${bold(`${opts.bin} destroy \u2014 ${name}`)}
|
|
3285
|
+
|
|
3286
|
+
`);
|
|
3287
|
+
if (homePath !== void 0 && homePath !== "") {
|
|
3288
|
+
io.stdout("`docker compose down -v` removes the containers.\n");
|
|
3289
|
+
io.stdout(
|
|
3290
|
+
`This sandbox keeps its home on a host bind at ${homePath}, which \`down -v\`
|
|
3291
|
+
does not touch \u2014 every agent CLI login, the gh CLI token, and the SSH keys
|
|
3292
|
+
stay there. Delete that directory yourself if you also want them gone.
|
|
3293
|
+
|
|
3294
|
+
`
|
|
3295
|
+
);
|
|
3296
|
+
} else {
|
|
3297
|
+
const volumes = namedVolumes(root).map((volume) => `${name}_${volume}`);
|
|
3298
|
+
io.stdout("`docker compose down -v` removes the containers and deletes\n");
|
|
3299
|
+
io.stdout(
|
|
3300
|
+
volumes.length > 0 ? `these named volumes with everything in them:
|
|
3301
|
+
|
|
3302
|
+
${volumes.join("\n ")}
|
|
3303
|
+
|
|
3304
|
+
` : "every named volume this project owns, with everything in them.\n\n"
|
|
3305
|
+
);
|
|
3306
|
+
io.stdout(
|
|
3307
|
+
"That is the provider authentication those volumes hold \u2014 every agent CLI\nlogin, the gh CLI token, and the SSH keys. Sign-in starts over.\n\n"
|
|
3308
|
+
);
|
|
3309
|
+
}
|
|
3310
|
+
const askFn = io.ask ?? ask;
|
|
3311
|
+
const answer = (await askFn(`Type the sandbox name \`${name}\` to destroy it:`)).trim();
|
|
3312
|
+
if (answer !== name) {
|
|
3313
|
+
io.stderr(`${opts.bin} destroy: aborted \u2014 nothing was removed
|
|
3314
|
+
`);
|
|
3315
|
+
return 1;
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
const code = runComposeVerb("destroy", { ...opts, name: basename5(root) });
|
|
3319
|
+
if (code === 0 && root.startsWith(registryRoot() + sep4)) {
|
|
3320
|
+
rmSync(root, { recursive: true, force: true });
|
|
3321
|
+
io.stdout(`removed the sandbox entry ${root}
|
|
3322
|
+
`);
|
|
3323
|
+
}
|
|
3324
|
+
return code;
|
|
3325
|
+
}
|
|
3326
|
+
function runGateway(args, opts) {
|
|
3327
|
+
const run = opts.run ?? spawnRunner;
|
|
3328
|
+
const root = resolveProjectRoot(opts.cwd);
|
|
3329
|
+
const script = requireLifecycleScript(root, "gateway.sh");
|
|
3330
|
+
const r = run("bash", [script, ...args], {
|
|
3331
|
+
stdio: "inherit",
|
|
3332
|
+
env: { ...process.env, ...aliasedEnvPair("PROJECT_ROOT", root) }
|
|
3333
|
+
});
|
|
3334
|
+
assertSpawned(r, `bash ${script}`);
|
|
3335
|
+
return r.status ?? 1;
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
// src/commands/config.ts
|
|
3339
|
+
var HOME_PATH_FIELD = "storage.homePath";
|
|
3340
|
+
function currentHomePath(root) {
|
|
3341
|
+
return readOhConfig(ohConfigPath(root)).storage?.homePath;
|
|
3342
|
+
}
|
|
3343
|
+
function existingNamedVolume(root, run) {
|
|
3344
|
+
const project = destroyConfirmationPhrase(root);
|
|
3345
|
+
for (const volume of namedVolumes(root).map((name) => `${project}_${name}`)) {
|
|
3346
|
+
const result = run("docker", ["volume", "inspect", volume], { stdio: "capture" });
|
|
3347
|
+
if (result.error === void 0 && result.status === 0) return volume;
|
|
3348
|
+
}
|
|
3349
|
+
return void 0;
|
|
3350
|
+
}
|
|
3351
|
+
function configRoot(opts) {
|
|
3352
|
+
return opts.sandbox === void 0 ? resolveProjectRoot(opts.cwd) : resolveSandboxRoot({ name: opts.sandbox });
|
|
3353
|
+
}
|
|
3354
|
+
function configFieldList() {
|
|
3355
|
+
return ohConfigFieldPaths().map((path4) => ` ${path4}`).join("\n");
|
|
3356
|
+
}
|
|
3357
|
+
async function runConfigShow(opts, io) {
|
|
3358
|
+
const root = configRoot(opts);
|
|
3359
|
+
const config = readOhConfig(ohConfigPath(root));
|
|
3360
|
+
io.stdout(`${JSON.stringify(config, null, 2)}
|
|
3361
|
+
`);
|
|
3362
|
+
return 0;
|
|
3363
|
+
}
|
|
3364
|
+
async function runConfigSet(key, value, opts, io) {
|
|
3365
|
+
if (isSecretKey(key) || isSecretKey(key.toUpperCase())) {
|
|
3366
|
+
io.stderr(
|
|
3367
|
+
`${opts.bin} config set: ${key.toUpperCase()} is a secret \u2014 oh.json is tracked by git.
|
|
3368
|
+
Set it with \`${opts.bin} secret set ${key.toUpperCase()}\` instead.
|
|
3369
|
+
`
|
|
3370
|
+
);
|
|
3371
|
+
return 1;
|
|
3372
|
+
}
|
|
3373
|
+
if (!findOhConfigField(key)) {
|
|
3374
|
+
io.stderr(`${opts.bin} config set: unknown field "${key}"
|
|
3375
|
+
|
|
3376
|
+
Fields:
|
|
3377
|
+
${configFieldList()}
|
|
3378
|
+
`);
|
|
3379
|
+
return 1;
|
|
3380
|
+
}
|
|
3381
|
+
const root = configRoot(opts);
|
|
3382
|
+
if (key === HOME_PATH_FIELD && opts.force !== true && currentHomePath(root) !== value) {
|
|
3383
|
+
const volume = existingNamedVolume(root, opts.run ?? spawnRunner);
|
|
3384
|
+
if (volume !== void 0) {
|
|
3385
|
+
io.stderr(
|
|
3386
|
+
`${opts.bin} config set: ${HOME_PATH_FIELD} refused \u2014 the named volume ${volume} already exists.
|
|
3387
|
+
The next start would mount ${value} at /home/sandbox instead, and every file already in ${volume} would be orphaned there, invisible to the sandbox.
|
|
3388
|
+
Pass --force to change it anyway.
|
|
3389
|
+
`
|
|
3390
|
+
);
|
|
3391
|
+
return 1;
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
let outcome;
|
|
3395
|
+
try {
|
|
3396
|
+
outcome = setConfigField(root, key, value);
|
|
3397
|
+
} catch (error) {
|
|
3398
|
+
io.stderr(`${opts.bin} config set: ${error instanceof Error ? error.message : String(error)}
|
|
3399
|
+
`);
|
|
3400
|
+
return 1;
|
|
3401
|
+
}
|
|
3402
|
+
io.stdout(
|
|
3403
|
+
outcome === "already-set" ? `oh.json: ${key} already ${value}
|
|
3404
|
+
` : `oh.json: set ${key}=${value} (${outcome})
|
|
3405
|
+
`
|
|
3406
|
+
);
|
|
3407
|
+
return 0;
|
|
3408
|
+
}
|
|
3409
|
+
var UPSTREAM_REMOTE = "openharness";
|
|
3410
|
+
function repoUrl(owner, name) {
|
|
3411
|
+
return `git@github.com:${owner}/${name}.git`;
|
|
3412
|
+
}
|
|
3413
|
+
function shellLine(step) {
|
|
3414
|
+
return `${step.cmd} ${step.args.join(" ")}`;
|
|
3415
|
+
}
|
|
3416
|
+
function manualSteps(owner, name, visibility) {
|
|
3417
|
+
return [
|
|
3418
|
+
{ cmd: "gh", args: ["repo", "create", `${owner}/${name}`, `--${visibility}`] },
|
|
3419
|
+
{ cmd: "git", args: ["remote", "rename", "origin", UPSTREAM_REMOTE] },
|
|
3420
|
+
{ cmd: "git", args: ["remote", "add", "origin", repoUrl(owner, name)] },
|
|
3421
|
+
{ cmd: "git", args: ["push", "-u", "origin", "HEAD"] }
|
|
3422
|
+
];
|
|
3423
|
+
}
|
|
3424
|
+
function printManual(io, steps, lead) {
|
|
3425
|
+
io.stdout(`
|
|
3426
|
+
${lead}
|
|
3427
|
+
|
|
3428
|
+
`);
|
|
3429
|
+
for (const step of steps) io.stdout(` ${shellLine(step)}
|
|
3430
|
+
`);
|
|
3431
|
+
io.stdout("\n");
|
|
3432
|
+
}
|
|
3433
|
+
function validateSegment(kind, value) {
|
|
3434
|
+
if (value === "") return `${kind} is required`;
|
|
3435
|
+
if (value.startsWith("-")) return `invalid ${kind} "${value}" (must not start with "-")`;
|
|
3436
|
+
if (!/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
3437
|
+
return `invalid ${kind} "${value}" (use letters, digits, ".", "_" or "-")`;
|
|
3438
|
+
}
|
|
3439
|
+
return void 0;
|
|
3440
|
+
}
|
|
3441
|
+
async function askOn(io, question) {
|
|
3442
|
+
const askFn = io.ask ?? ask;
|
|
3443
|
+
return (await askFn(question)).trim();
|
|
3444
|
+
}
|
|
3445
|
+
async function confirmOn(io, question, defaultYes) {
|
|
3446
|
+
const answer = (await askOn(io, `${question} ${defaultYes ? "[Y/n]" : "[y/N]"}`)).toLowerCase();
|
|
3447
|
+
if (answer === "") return defaultYes;
|
|
3448
|
+
return /^y/.test(answer);
|
|
3449
|
+
}
|
|
3450
|
+
async function askVisibility(io) {
|
|
3451
|
+
io.stdout(" Repository visibility\n 1) private (recommended)\n 2) public\n");
|
|
3452
|
+
for (; ; ) {
|
|
3453
|
+
const answer = await askOn(io, "Choose [1-2] (default 1):");
|
|
3454
|
+
if (answer === "" || answer === "1") return "private";
|
|
3455
|
+
if (answer === "2") return "public";
|
|
3456
|
+
io.stderr(" Invalid choice. Pick 1 or 2.\n");
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
async function runConfigRepo(opts, io) {
|
|
3460
|
+
const run = opts.run ?? spawnRunner;
|
|
3461
|
+
const isTTY = io.isTTY ?? process.stdin.isTTY === true;
|
|
3462
|
+
if (!isTTY) {
|
|
3463
|
+
io.stderr(
|
|
3464
|
+
`${opts.bin} config repo: creating a repo on your GitHub account needs an interactive terminal; re-run it from a TTY.
|
|
3465
|
+
`
|
|
3466
|
+
);
|
|
3467
|
+
return 1;
|
|
3468
|
+
}
|
|
3469
|
+
io.stdout(`
|
|
3470
|
+
${bold("Version this harness in your own repo")}
|
|
3471
|
+
`);
|
|
3472
|
+
io.stdout(
|
|
3473
|
+
` Creates a repo on your GitHub account, keeps the upstream you cloned from
|
|
3474
|
+
as the "${UPSTREAM_REMOTE}" remote, and pushes this branch to yours.
|
|
3475
|
+
`
|
|
3476
|
+
);
|
|
3477
|
+
if (!await confirmOn(io, "Version this harness in your own repo?", false)) {
|
|
3478
|
+
printManual(
|
|
3479
|
+
io,
|
|
3480
|
+
manualSteps("<your-user>", "<repo>", "private"),
|
|
3481
|
+
`Skipped \u2014 nothing was created and no remote was touched. To do it later, run \`${opts.bin} config repo\`, or by hand:`
|
|
3482
|
+
);
|
|
3483
|
+
return 0;
|
|
3484
|
+
}
|
|
3485
|
+
const owner = await askOn(io, "GitHub owner (user or org):");
|
|
3486
|
+
const ownerError = validateSegment("GitHub owner", owner);
|
|
3487
|
+
if (ownerError) {
|
|
3488
|
+
io.stderr(`${opts.bin} config repo: ${ownerError}
|
|
3489
|
+
`);
|
|
3490
|
+
return 1;
|
|
3491
|
+
}
|
|
3492
|
+
const name = await askOn(io, "Repository name [openharness]:");
|
|
3493
|
+
const repoName = name === "" ? "openharness" : name;
|
|
3494
|
+
const nameError = validateSegment("repository name", repoName);
|
|
3495
|
+
if (nameError) {
|
|
3496
|
+
io.stderr(`${opts.bin} config repo: ${nameError}
|
|
3497
|
+
`);
|
|
3498
|
+
return 1;
|
|
3499
|
+
}
|
|
3500
|
+
const visibility = await askVisibility(io);
|
|
3501
|
+
const url = repoUrl(owner, repoName);
|
|
3502
|
+
const manual = manualSteps(owner, repoName, visibility);
|
|
3503
|
+
let root;
|
|
3504
|
+
try {
|
|
3505
|
+
root = resolveProjectRoot(opts.cwd);
|
|
3506
|
+
} catch (error) {
|
|
3507
|
+
io.stderr(`${opts.bin} config repo: ${error instanceof Error ? error.message : String(error)}
|
|
3508
|
+
`);
|
|
3509
|
+
printManual(io, manual, "Nothing was changed. Finish this by hand from your checkout:");
|
|
3510
|
+
return 1;
|
|
3511
|
+
}
|
|
3512
|
+
const capture2 = { stdio: "capture", cwd: root };
|
|
3513
|
+
const auth = run("gh", ["auth", "status"], capture2);
|
|
3514
|
+
if (auth.error?.code === "ENOENT") {
|
|
3515
|
+
printManual(io, manual, "gh is not on PATH \u2014 finish this by hand:");
|
|
3516
|
+
return 0;
|
|
3517
|
+
}
|
|
3518
|
+
assertSpawned(auth, "gh auth status");
|
|
3519
|
+
if (auth.status !== 0) {
|
|
3520
|
+
printManual(
|
|
3521
|
+
io,
|
|
3522
|
+
manual,
|
|
3523
|
+
"gh is not authenticated (`gh auth login`) \u2014 finish this by hand once it is:"
|
|
3524
|
+
);
|
|
3525
|
+
return 0;
|
|
3526
|
+
}
|
|
3527
|
+
const remotesResult = run("git", ["remote"], capture2);
|
|
3528
|
+
assertSpawned(remotesResult, "git remote");
|
|
3529
|
+
if (remotesResult.status !== 0) {
|
|
3530
|
+
printManual(io, manual, "`git remote` failed \u2014 is this a git repo? Finish this by hand:");
|
|
3531
|
+
return 0;
|
|
3532
|
+
}
|
|
3533
|
+
const remotes = (remotesResult.stdout ?? "").split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
3534
|
+
let originUrl = "";
|
|
3535
|
+
if (remotes.includes("origin")) {
|
|
3536
|
+
const shown = run("git", ["remote", "get-url", "origin"], capture2);
|
|
3537
|
+
assertSpawned(shown, "git remote get-url origin");
|
|
3538
|
+
originUrl = (shown.stdout ?? "").trim();
|
|
3539
|
+
}
|
|
3540
|
+
const originIsTarget = originUrl !== "" && sameRemote(originUrl, owner, repoName);
|
|
3541
|
+
if (!originIsTarget && remotes.includes("origin") && remotes.includes(UPSTREAM_REMOTE)) {
|
|
3542
|
+
io.stderr(
|
|
3543
|
+
`${opts.bin} config repo: origin already points at ${originUrl} and a "${UPSTREAM_REMOTE}" remote already exists \u2014 nothing was changed. Sort the remotes out by hand:
|
|
3544
|
+
`
|
|
3545
|
+
);
|
|
3546
|
+
printManual(io, manual.slice(2), "Remaining steps:");
|
|
3547
|
+
return 1;
|
|
3548
|
+
}
|
|
3549
|
+
const steps = [];
|
|
3550
|
+
const exists = run("gh", ["repo", "view", `${owner}/${repoName}`], capture2);
|
|
3551
|
+
assertSpawned(exists, "gh repo view");
|
|
3552
|
+
if (exists.status === 0) {
|
|
3553
|
+
io.stdout(` ${owner}/${repoName} already exists \u2014 skipping creation.
|
|
3554
|
+
`);
|
|
3555
|
+
} else {
|
|
3556
|
+
steps.push(manual[0]);
|
|
3557
|
+
}
|
|
3558
|
+
if (originIsTarget) {
|
|
3559
|
+
io.stdout(" origin already points at your repo \u2014 leaving the remotes alone.\n");
|
|
3560
|
+
} else {
|
|
3561
|
+
if (remotes.includes("origin")) steps.push(manual[1]);
|
|
3562
|
+
steps.push(manual[2]);
|
|
3563
|
+
}
|
|
3564
|
+
steps.push(manual[3]);
|
|
3565
|
+
for (let i = 0; i < steps.length; i++) {
|
|
3566
|
+
const step = steps[i];
|
|
3567
|
+
io.stdout(` $ ${shellLine(step)}
|
|
3568
|
+
`);
|
|
3569
|
+
const result = run(step.cmd, step.args, { stdio: "inherit", cwd: root });
|
|
3570
|
+
if (result.error || result.status !== 0) {
|
|
3571
|
+
const detail = result.error?.message ?? `exit ${result.status}`;
|
|
3572
|
+
io.stderr(`${opts.bin} config repo: ${shellLine(step)} failed (${detail}) \u2014 stopping here.
|
|
3573
|
+
`);
|
|
3574
|
+
const remaining = steps.slice(i + 1);
|
|
3575
|
+
if (remaining.length > 0) {
|
|
3576
|
+
printManual(io, remaining, "Not run. Finish by hand once the failure is fixed:");
|
|
3577
|
+
}
|
|
3578
|
+
return 1;
|
|
3362
3579
|
}
|
|
3363
|
-
throw err;
|
|
3364
3580
|
}
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3581
|
+
io.stdout(
|
|
3582
|
+
`
|
|
3583
|
+
Done \u2014 origin is ${url}${remotes.includes("origin") && !originIsTarget ? `, upstream kept as "${UPSTREAM_REMOTE}"` : ""}.
|
|
3368
3584
|
`
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
return code;
|
|
3585
|
+
);
|
|
3586
|
+
return 0;
|
|
3372
3587
|
}
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
ps: Object.freeze(["ps"]),
|
|
3378
|
-
destroy: Object.freeze(["down", "-v"])
|
|
3379
|
-
});
|
|
3380
|
-
function composeVerbs() {
|
|
3381
|
-
return Object.keys(COMPOSE_VERBS);
|
|
3588
|
+
function sameRemote(url, owner, name) {
|
|
3589
|
+
const normalized = url.replace(/\.git$/, "").toLowerCase();
|
|
3590
|
+
const slug = `${owner}/${name}`.toLowerCase();
|
|
3591
|
+
return normalized.endsWith(`:${slug}`) || normalized.endsWith(`/${slug}`);
|
|
3382
3592
|
}
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3593
|
+
|
|
3594
|
+
// src/commands/secret.ts
|
|
3595
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
3596
|
+
import { join as join8 } from "node:path";
|
|
3597
|
+
|
|
3598
|
+
// src/lib/gitignore.ts
|
|
3599
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "node:fs";
|
|
3600
|
+
import { dirname as dirname7, join as join7 } from "node:path";
|
|
3601
|
+
function appendGitignoreLines(root, lines) {
|
|
3602
|
+
const target = join7(root, ".gitignore");
|
|
3603
|
+
const existing = existsSync13(target) ? readFileSync10(target, "utf8") : "";
|
|
3604
|
+
const present = new Set(existing.split("\n").map((line) => line.trimEnd()));
|
|
3605
|
+
const added = [];
|
|
3606
|
+
for (const line of lines) {
|
|
3607
|
+
const key = line.trimEnd();
|
|
3608
|
+
if (key === "" || present.has(key)) continue;
|
|
3609
|
+
present.add(key);
|
|
3610
|
+
added.push(line);
|
|
3611
|
+
}
|
|
3612
|
+
if (added.length === 0) return added;
|
|
3613
|
+
let output = existing;
|
|
3614
|
+
if (output.length > 0 && !output.endsWith("\n")) output += "\n";
|
|
3615
|
+
output += `${added.join("\n")}
|
|
3616
|
+
`;
|
|
3617
|
+
mkdirSync5(dirname7(target), { recursive: true });
|
|
3618
|
+
writeFileSync7(target, output, "utf8");
|
|
3619
|
+
return added;
|
|
3394
3620
|
}
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
return withComposeEnvFile(root, (extraArgs) => {
|
|
3400
|
-
const r = run("bash", [script, ...extraArgs, "config", ...extra], { stdio: "inherit" });
|
|
3401
|
-
assertSpawned(r, `bash ${script} config`);
|
|
3402
|
-
return r.status ?? 1;
|
|
3403
|
-
});
|
|
3621
|
+
|
|
3622
|
+
// src/commands/secret.ts
|
|
3623
|
+
function secretsRoot(opts) {
|
|
3624
|
+
return opts.sandbox === void 0 ? resolveProjectRoot(opts.cwd) : resolveSandboxRoot({ name: opts.sandbox });
|
|
3404
3625
|
}
|
|
3405
|
-
function
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
text = readFileSync10(join8(root, ".devcontainer", "docker-compose.yml"), "utf8");
|
|
3409
|
-
} catch {
|
|
3410
|
-
return [];
|
|
3411
|
-
}
|
|
3412
|
-
const lines = text.split("\n");
|
|
3413
|
-
const start = lines.findIndex((line) => /^volumes:\s*$/.test(line));
|
|
3414
|
-
if (start === -1) return [];
|
|
3415
|
-
const names = [];
|
|
3416
|
-
for (const line of lines.slice(start + 1)) {
|
|
3417
|
-
if (/^\S/.test(line)) break;
|
|
3418
|
-
const match = /^ {2}([A-Za-z0-9][A-Za-z0-9_.-]*):\s*$/.exec(line);
|
|
3419
|
-
if (match) names.push(match[1]);
|
|
3420
|
-
}
|
|
3421
|
-
return names;
|
|
3626
|
+
function ignoreSecretsFile(root) {
|
|
3627
|
+
if (!existsSync14(join8(root, ".git"))) return;
|
|
3628
|
+
appendGitignoreLines(root, [".env"]);
|
|
3422
3629
|
}
|
|
3423
|
-
function
|
|
3424
|
-
return
|
|
3630
|
+
function secretKeyList() {
|
|
3631
|
+
return SECRET_KEYS.map((key) => ` ${key}`).join("\n");
|
|
3425
3632
|
}
|
|
3426
|
-
async function
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
if (!interactive) {
|
|
3432
|
-
io.stderr(
|
|
3433
|
-
`oh destroy: refusing to destroy \`${name}\` without a terminal \u2014 re-run with --yes to confirm non-interactively
|
|
3434
|
-
`
|
|
3435
|
-
);
|
|
3436
|
-
return 1;
|
|
3437
|
-
}
|
|
3438
|
-
const homePath = readOhConfig(ohConfigPath(root)).storage?.homePath;
|
|
3439
|
-
io.stdout(`
|
|
3440
|
-
${bold(`oh destroy \u2014 ${name}`)}
|
|
3441
|
-
|
|
3442
|
-
`);
|
|
3443
|
-
if (homePath !== void 0 && homePath !== "") {
|
|
3444
|
-
io.stdout("`docker compose down -v` removes the containers.\n");
|
|
3445
|
-
io.stdout(
|
|
3446
|
-
`This sandbox keeps its home on a host bind at ${homePath}, which \`down -v\`
|
|
3447
|
-
does not touch \u2014 every agent CLI login, the gh CLI token, and the SSH keys
|
|
3448
|
-
stay there. Delete that directory yourself if you also want them gone.
|
|
3633
|
+
async function runSecretSet(key, opts, io) {
|
|
3634
|
+
if (!isSecretKey(key)) {
|
|
3635
|
+
io.stderr(
|
|
3636
|
+
`${opts.bin} secret set: ${key} is not a secret \u2014 non-secret settings live in oh.json.
|
|
3637
|
+
Set it with \`${opts.bin} config set ${key}\` instead.
|
|
3449
3638
|
|
|
3639
|
+
Keys:
|
|
3640
|
+
${secretKeyList()}
|
|
3450
3641
|
`
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
const volumes = namedVolumes(root).map((volume) => `${name}_${volume}`);
|
|
3454
|
-
io.stdout("`docker compose down -v` removes the containers and deletes\n");
|
|
3455
|
-
io.stdout(
|
|
3456
|
-
volumes.length > 0 ? `these named volumes with everything in them:
|
|
3457
|
-
|
|
3458
|
-
${volumes.join("\n ")}
|
|
3459
|
-
|
|
3460
|
-
` : "every named volume this project owns, with everything in them.\n\n"
|
|
3461
|
-
);
|
|
3462
|
-
io.stdout(
|
|
3463
|
-
"That is the provider authentication those volumes hold \u2014 every agent CLI\nlogin, the gh CLI token, and the SSH keys. Sign-in starts over.\n\n"
|
|
3464
|
-
);
|
|
3465
|
-
}
|
|
3466
|
-
const askFn = io.ask ?? ask;
|
|
3467
|
-
const answer = (await askFn(`Type the sandbox name \`${name}\` to destroy it:`)).trim();
|
|
3468
|
-
if (answer !== name) {
|
|
3469
|
-
io.stderr("oh destroy: aborted \u2014 nothing was removed\n");
|
|
3470
|
-
return 1;
|
|
3471
|
-
}
|
|
3642
|
+
);
|
|
3643
|
+
return 1;
|
|
3472
3644
|
}
|
|
3473
|
-
const
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3645
|
+
const root = secretsRoot(opts);
|
|
3646
|
+
const askSecret2 = io.askSecret ?? askSecret;
|
|
3647
|
+
const value = (await askSecret2(`Value for ${bold(key)} (input hidden):`)).trim();
|
|
3648
|
+
if (value === "") {
|
|
3649
|
+
io.stderr(`${opts.bin} secret set: no value entered \u2014 ${key} unchanged
|
|
3477
3650
|
`);
|
|
3651
|
+
return 1;
|
|
3478
3652
|
}
|
|
3479
|
-
|
|
3653
|
+
try {
|
|
3654
|
+
setSecret(root, key, value);
|
|
3655
|
+
ignoreSecretsFile(root);
|
|
3656
|
+
} catch (error) {
|
|
3657
|
+
io.stderr(`${opts.bin} secret set: ${error instanceof Error ? error.message : String(error)}
|
|
3658
|
+
`);
|
|
3659
|
+
return 1;
|
|
3660
|
+
}
|
|
3661
|
+
io.stdout(`.env: set ${key}=${redact(value)}
|
|
3662
|
+
`);
|
|
3663
|
+
return 0;
|
|
3480
3664
|
}
|
|
3481
|
-
function
|
|
3482
|
-
const
|
|
3483
|
-
const
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
}
|
|
3489
|
-
|
|
3490
|
-
|
|
3665
|
+
async function runSecretList(opts, io) {
|
|
3666
|
+
const root = secretsRoot(opts);
|
|
3667
|
+
const keys = listSecretKeys(root);
|
|
3668
|
+
if (keys.length === 0) {
|
|
3669
|
+
io.stdout(`no secrets set \u2014 write one with \`${opts.bin} secret set <KEY>\`
|
|
3670
|
+
`);
|
|
3671
|
+
return 0;
|
|
3672
|
+
}
|
|
3673
|
+
const width = Math.max(...keys.map((key) => key.length));
|
|
3674
|
+
for (const key of keys) {
|
|
3675
|
+
const value = readSecret(root, key);
|
|
3676
|
+
io.stdout(`${key.padEnd(width)} ${value === void 0 ? "\u2014" : redact(value)}
|
|
3677
|
+
`);
|
|
3678
|
+
}
|
|
3679
|
+
return 0;
|
|
3491
3680
|
}
|
|
3492
3681
|
|
|
3493
3682
|
// src/commands/sandbox.ts
|
|
3494
|
-
import { existsSync as existsSync15, mkdirSync as
|
|
3683
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, rmSync as rmSync2, statSync as statSync4 } from "node:fs";
|
|
3495
3684
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
3496
|
-
import { join as join9, resolve as
|
|
3685
|
+
import { join as join9, resolve as resolve9 } from "node:path";
|
|
3497
3686
|
|
|
3498
3687
|
// src/lib/runtimes/catalog.ts
|
|
3499
3688
|
var RUNTIME_CATALOG = Object.freeze([
|
|
@@ -3511,7 +3700,7 @@ var RUNTIME_CATALOG = Object.freeze([
|
|
|
3511
3700
|
tier: "microvm",
|
|
3512
3701
|
state: "planned",
|
|
3513
3702
|
provisionable: false,
|
|
3514
|
-
notProvisionableReason:
|
|
3703
|
+
notProvisionableReason: (bin) => `microsandbox is not a provisionable runtime yet; see docs/rfcs/rfc-runtime-support.md. Inside a sandbox run \`${bin} tool install microsandbox\`.`,
|
|
3515
3704
|
docsPath: "docs/runtimes/microsandbox.md"
|
|
3516
3705
|
})
|
|
3517
3706
|
]);
|
|
@@ -3537,26 +3726,70 @@ function gitIdentity(run, key) {
|
|
|
3537
3726
|
if (result.error || result.status !== 0) return "";
|
|
3538
3727
|
return (result.stdout ?? "").trim();
|
|
3539
3728
|
}
|
|
3729
|
+
function isBuildCapable(repo) {
|
|
3730
|
+
return repo !== void 0 && existsSync15(join9(repo, ".devcontainer", "Dockerfile"));
|
|
3731
|
+
}
|
|
3732
|
+
function prepareHomeMount(value) {
|
|
3733
|
+
const path4 = resolve9(value);
|
|
3734
|
+
if (existsSync15(path4)) {
|
|
3735
|
+
if (!statSync4(path4).isDirectory()) {
|
|
3736
|
+
throw new Error(`--home-mount path exists and is not a directory: ${path4}`);
|
|
3737
|
+
}
|
|
3738
|
+
return path4;
|
|
3739
|
+
}
|
|
3740
|
+
mkdirSync6(path4, { recursive: true });
|
|
3741
|
+
return path4;
|
|
3742
|
+
}
|
|
3540
3743
|
function readSeedConfig(repo) {
|
|
3541
3744
|
if (repo === void 0) return void 0;
|
|
3542
3745
|
const file = ohConfigPath(repo);
|
|
3543
3746
|
return existsSync15(file) ? readOhConfig(file) : void 0;
|
|
3544
3747
|
}
|
|
3545
|
-
function
|
|
3546
|
-
const
|
|
3748
|
+
function readEntryConfig(name) {
|
|
3749
|
+
const file = ohConfigPath(entryRoot(name));
|
|
3750
|
+
return existsSync15(file) ? readOhConfig(file) : void 0;
|
|
3751
|
+
}
|
|
3752
|
+
function assignSetting(target, path4, value) {
|
|
3753
|
+
const segments = path4.split(".");
|
|
3754
|
+
let cursor = target;
|
|
3755
|
+
for (const segment of segments.slice(0, -1)) {
|
|
3756
|
+
const section = cursor[segment];
|
|
3757
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) cursor[segment] = {};
|
|
3758
|
+
cursor = cursor[segment];
|
|
3759
|
+
}
|
|
3760
|
+
cursor[segments[segments.length - 1]] = value;
|
|
3761
|
+
}
|
|
3762
|
+
function overlaySettings(target, source) {
|
|
3763
|
+
if (source === void 0) return target;
|
|
3764
|
+
for (const field of OH_CONFIG_FIELDS) {
|
|
3765
|
+
const value = getOhConfigValue(source, field.path);
|
|
3766
|
+
if (value !== void 0) assignSetting(target, field.path, value);
|
|
3767
|
+
}
|
|
3768
|
+
return target;
|
|
3769
|
+
}
|
|
3770
|
+
function mergeSettings(...sources) {
|
|
3771
|
+
const present = sources.filter((source) => source !== void 0);
|
|
3772
|
+
if (present.length === 0) return void 0;
|
|
3773
|
+
const merged = { version: 1 };
|
|
3774
|
+
for (const source of present) overlaySettings(merged, source);
|
|
3775
|
+
return merged;
|
|
3776
|
+
}
|
|
3777
|
+
function nonEmpty(value) {
|
|
3778
|
+
return value === void 0 || value === "" ? void 0 : value;
|
|
3779
|
+
}
|
|
3780
|
+
function seedConfig(name, checkout, seed, run) {
|
|
3781
|
+
const config = overlaySettings(defaultOhConfig(name), seed);
|
|
3782
|
+
config.name = name;
|
|
3547
3783
|
config.runtime = "docker";
|
|
3548
|
-
if (
|
|
3549
|
-
config.timezone = seed?.timezone ?? hostTimezone();
|
|
3784
|
+
if (checkout !== void 0) config.checkout = checkout;
|
|
3785
|
+
config.timezone = nonEmpty(seed?.timezone) ?? hostTimezone();
|
|
3550
3786
|
config.git = {
|
|
3551
|
-
userName: seed?.git?.userName ?? gitIdentity(run, "user.name"),
|
|
3552
|
-
userEmail: seed?.git?.userEmail ?? gitIdentity(run, "user.email")
|
|
3787
|
+
userName: nonEmpty(seed?.git?.userName) ?? gitIdentity(run, "user.name"),
|
|
3788
|
+
userEmail: nonEmpty(seed?.git?.userEmail) ?? gitIdentity(run, "user.email")
|
|
3553
3789
|
};
|
|
3554
|
-
if (seed?.storage?.homePath !== void 0) config.storage = { homePath: seed.storage.homePath };
|
|
3555
|
-
config.access = { ...config.access, ...seed?.access ?? {} };
|
|
3556
3790
|
config.image = {
|
|
3557
3791
|
...config.image,
|
|
3558
|
-
|
|
3559
|
-
mode: seed?.image?.mode ?? (repo === void 0 ? "image" : "build")
|
|
3792
|
+
mode: seed?.image?.mode ?? (isBuildCapable(configCheckout(config)) ? "build" : "image")
|
|
3560
3793
|
};
|
|
3561
3794
|
return config;
|
|
3562
3795
|
}
|
|
@@ -3596,48 +3829,89 @@ async function runWizard(config, io) {
|
|
|
3596
3829
|
config.access?.dockerSocket === true
|
|
3597
3830
|
);
|
|
3598
3831
|
config.access = access;
|
|
3832
|
+
const homePath = await askDefaulted(
|
|
3833
|
+
ask2,
|
|
3834
|
+
"Host path for /home/sandbox (blank keeps the Docker-managed volume)",
|
|
3835
|
+
config.storage?.homePath ?? ""
|
|
3836
|
+
);
|
|
3837
|
+
if (homePath !== "") config.storage = { ...config.storage, homePath: resolve9(homePath) };
|
|
3599
3838
|
}
|
|
3600
3839
|
async function runSandboxInstall(opts, io) {
|
|
3601
3840
|
const run = opts.run ?? spawnRunner;
|
|
3602
3841
|
const runtime = findRuntime(opts.runtime);
|
|
3603
3842
|
if (runtime === void 0) {
|
|
3604
3843
|
io.stderr(
|
|
3605
|
-
|
|
3844
|
+
`${opts.bin} sandbox install: unknown runtime "${opts.runtime}" \u2014 known runtimes: ${runtimeIds().join(", ")}
|
|
3606
3845
|
`
|
|
3607
3846
|
);
|
|
3608
3847
|
return 1;
|
|
3609
3848
|
}
|
|
3610
3849
|
if (!runtime.provisionable) {
|
|
3611
|
-
io.stderr(
|
|
3850
|
+
io.stderr(`${opts.bin} sandbox install: ${runtime.notProvisionableReason?.(opts.bin) ?? ""}
|
|
3612
3851
|
`);
|
|
3613
3852
|
return 1;
|
|
3614
3853
|
}
|
|
3615
|
-
const
|
|
3616
|
-
if (
|
|
3617
|
-
io.stderr(
|
|
3854
|
+
const checkout = opts.checkout === void 0 ? void 0 : resolve9(opts.checkout);
|
|
3855
|
+
if (checkout !== void 0 && !existsSync15(checkout)) {
|
|
3856
|
+
io.stderr(`${opts.bin} sandbox install: --checkout directory does not exist: ${checkout}
|
|
3618
3857
|
`);
|
|
3619
3858
|
return 1;
|
|
3620
3859
|
}
|
|
3621
|
-
const
|
|
3622
|
-
const name = opts.name ??
|
|
3860
|
+
const repoSeed = readSeedConfig(checkout);
|
|
3861
|
+
const name = opts.name ?? repoSeed?.name ?? nextDefaultName(run);
|
|
3623
3862
|
try {
|
|
3624
3863
|
assertSandboxName(name);
|
|
3625
3864
|
} catch (error) {
|
|
3626
|
-
io.stderr(
|
|
3865
|
+
io.stderr(`${opts.bin} sandbox install: ${error instanceof Error ? error.message : String(error)}
|
|
3627
3866
|
`);
|
|
3628
3867
|
return 1;
|
|
3629
3868
|
}
|
|
3630
|
-
const config = seedConfig(name,
|
|
3869
|
+
const config = seedConfig(name, checkout, mergeSettings(readEntryConfig(name), repoSeed), run);
|
|
3870
|
+
const buildRequested = config.image?.mode === "build" && opts.noBuild !== true && opts.image !== true && opts.imageRef === void 0;
|
|
3871
|
+
if (buildRequested && !isBuildCapable(configCheckout(config))) {
|
|
3872
|
+
const target = configCheckout(config) ?? resolve9(opts.checkout ?? ".");
|
|
3873
|
+
io.stderr(
|
|
3874
|
+
`${opts.bin} sandbox install: image.mode is "build" but ${join9(target, ".devcontainer", "Dockerfile")} does not exist \u2014 point --checkout <dir> at a harness checkout that has .devcontainer/Dockerfile, or set image.mode to "image" to run the prebuilt image
|
|
3875
|
+
`
|
|
3876
|
+
);
|
|
3877
|
+
return 1;
|
|
3878
|
+
}
|
|
3879
|
+
if (opts.homeMount !== void 0) {
|
|
3880
|
+
try {
|
|
3881
|
+
config.storage = { ...config.storage, homePath: prepareHomeMount(opts.homeMount) };
|
|
3882
|
+
} catch (error) {
|
|
3883
|
+
io.stderr(`${opts.bin} sandbox install: ${error instanceof Error ? error.message : String(error)}
|
|
3884
|
+
`);
|
|
3885
|
+
return 1;
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3631
3888
|
const interactive = opts.yes !== true && (process.stdin.isTTY === true || io.ask !== void 0);
|
|
3889
|
+
const homePathBeforeWizard = config.storage?.homePath;
|
|
3632
3890
|
if (interactive) {
|
|
3633
3891
|
header("Configure the sandbox (press Enter to accept the shown default)");
|
|
3634
3892
|
await runWizard(config, io);
|
|
3893
|
+
const chosen = config.storage?.homePath;
|
|
3894
|
+
if (chosen !== void 0 && chosen !== homePathBeforeWizard) {
|
|
3895
|
+
try {
|
|
3896
|
+
config.storage = { ...config.storage, homePath: prepareHomeMount(chosen) };
|
|
3897
|
+
} catch (error) {
|
|
3898
|
+
io.stderr(
|
|
3899
|
+
`${opts.bin} sandbox install: ${error instanceof Error ? error.message : String(error)}
|
|
3900
|
+
`
|
|
3901
|
+
);
|
|
3902
|
+
return 1;
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3635
3905
|
}
|
|
3636
3906
|
if (opts.imageRef !== void 0) {
|
|
3637
3907
|
config.image = { ...config.image, ref: opts.imageRef, mode: "image" };
|
|
3638
3908
|
}
|
|
3639
|
-
|
|
3909
|
+
if (configCheckout(config) !== void 0 && config.image?.mode === "image" && nonEmpty(config.image?.ref) === void 0) {
|
|
3910
|
+
config.image = { ...config.image, ref: DEFAULT_SANDBOX_IMAGE };
|
|
3911
|
+
}
|
|
3912
|
+
const useNoBuild = opts.noBuild === true || !(configCheckout(config) !== void 0 && config.image?.mode === "build");
|
|
3640
3913
|
const sandboxOpts = {
|
|
3914
|
+
bin: opts.bin,
|
|
3641
3915
|
run,
|
|
3642
3916
|
...opts.image === true ? { image: true } : {},
|
|
3643
3917
|
...opts.imageRef !== void 0 ? { imageRef: opts.imageRef } : {},
|
|
@@ -3647,18 +3921,18 @@ async function runSandboxInstall(opts, io) {
|
|
|
3647
3921
|
const preview = mkdtempSync2(join9(tmpdir2(), "oh-sandbox-preview-"));
|
|
3648
3922
|
try {
|
|
3649
3923
|
writeOhConfig(preview, config);
|
|
3650
|
-
materialize(preview, { ...config
|
|
3924
|
+
materialize(preview, { ...configCheckout(config) !== void 0 ? { checkout: configCheckout(config) } : {} });
|
|
3651
3925
|
return await runSandbox({ ...sandboxOpts, cwd: preview, printArgv: true }, io);
|
|
3652
3926
|
} finally {
|
|
3653
3927
|
rmSync2(preview, { recursive: true, force: true });
|
|
3654
3928
|
}
|
|
3655
3929
|
}
|
|
3656
3930
|
const root = entryRoot(config.name);
|
|
3657
|
-
|
|
3931
|
+
mkdirSync6(root, { recursive: true });
|
|
3658
3932
|
writeOhConfig(root, config);
|
|
3659
|
-
materialize(root, { ...config
|
|
3933
|
+
materialize(root, { ...configCheckout(config) !== void 0 ? { checkout: configCheckout(config) } : {} });
|
|
3660
3934
|
const code = await runSandbox({ ...sandboxOpts, cwd: root }, io);
|
|
3661
|
-
if (code === 0) io.stdout(`next:
|
|
3935
|
+
if (code === 0) io.stdout(`next: ${opts.bin} shell ${config.name}
|
|
3662
3936
|
`);
|
|
3663
3937
|
return code;
|
|
3664
3938
|
}
|
|
@@ -3676,10 +3950,12 @@ async function runSandboxList(opts, io) {
|
|
|
3676
3950
|
for (const name of listEntries()) {
|
|
3677
3951
|
const root = entryRoot(name);
|
|
3678
3952
|
const config = readOhConfig(ohConfigPath(root));
|
|
3953
|
+
const checkout = configCheckout(config) ?? "-";
|
|
3679
3954
|
rows.push({
|
|
3680
3955
|
name,
|
|
3681
3956
|
runtime: config.runtime ?? "docker",
|
|
3682
|
-
|
|
3957
|
+
checkout,
|
|
3958
|
+
repo: checkout,
|
|
3683
3959
|
status: await entryStatus(root, name, run)
|
|
3684
3960
|
});
|
|
3685
3961
|
}
|
|
@@ -3690,7 +3966,7 @@ async function runSandboxList(opts, io) {
|
|
|
3690
3966
|
}
|
|
3691
3967
|
if (rows.length === 0) {
|
|
3692
3968
|
io.stdout(
|
|
3693
|
-
`no sandbox is registered in ${registryRoot()} \u2014 create one with
|
|
3969
|
+
`no sandbox is registered in ${registryRoot()} \u2014 create one with \`${opts.bin} sandbox install docker\`
|
|
3694
3970
|
`
|
|
3695
3971
|
);
|
|
3696
3972
|
return 0;
|
|
@@ -3701,7 +3977,7 @@ async function runSandboxList(opts, io) {
|
|
|
3701
3977
|
const statusWidth = width((row) => row.status);
|
|
3702
3978
|
for (const row of rows) {
|
|
3703
3979
|
io.stdout(
|
|
3704
|
-
`${row.name.padEnd(nameWidth)} ${row.runtime.padEnd(runtimeWidth)} ${row.status.padEnd(statusWidth)} ${row.
|
|
3980
|
+
`${row.name.padEnd(nameWidth)} ${row.runtime.padEnd(runtimeWidth)} ${row.status.padEnd(statusWidth)} ${row.checkout}
|
|
3705
3981
|
`
|
|
3706
3982
|
);
|
|
3707
3983
|
}
|
|
@@ -3709,7 +3985,7 @@ async function runSandboxList(opts, io) {
|
|
|
3709
3985
|
}
|
|
3710
3986
|
|
|
3711
3987
|
// src/lib/docs.ts
|
|
3712
|
-
var SOURCE_DOCS_BASE = "https://github.com/mifunedev/
|
|
3988
|
+
var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
|
|
3713
3989
|
function sourceDocsUrl(docsPath) {
|
|
3714
3990
|
const segments = docsPath.split("/");
|
|
3715
3991
|
if (segments[0] !== "docs" || segments.length < 2 || segments.some((segment) => segment === "" || segment === "." || segment === "..") || docsPath.includes("\\") || docsPath.includes("?") || docsPath.includes("#")) {
|
|
@@ -3831,6 +4107,20 @@ var HARNESS_CATALOG = [
|
|
|
3831
4107
|
docsPath: "docs/harnesses/muse-code.md",
|
|
3832
4108
|
kind: "installable"
|
|
3833
4109
|
},
|
|
4110
|
+
{
|
|
4111
|
+
id: "antigravity-cli",
|
|
4112
|
+
title: "Antigravity CLI",
|
|
4113
|
+
binary: "agy",
|
|
4114
|
+
installArgv: [
|
|
4115
|
+
"bash",
|
|
4116
|
+
"-lc",
|
|
4117
|
+
'set -o pipefail; curl -fsSL https://antigravity.google/cli/install.sh | bash -s -- --dir "$HOME/.local/bin"'
|
|
4118
|
+
],
|
|
4119
|
+
installUser: "sandbox",
|
|
4120
|
+
verifyArgv: ["agy", "--version"],
|
|
4121
|
+
docsPath: "docs/harnesses/antigravity-cli.md",
|
|
4122
|
+
kind: "installable"
|
|
4123
|
+
},
|
|
3834
4124
|
{
|
|
3835
4125
|
id: "t3code",
|
|
3836
4126
|
title: "T3 Code",
|
|
@@ -3904,7 +4194,7 @@ function cell(value, absent) {
|
|
|
3904
4194
|
if (value === null) return absent;
|
|
3905
4195
|
return value ? "yes" : "no";
|
|
3906
4196
|
}
|
|
3907
|
-
function renderTable(states, io) {
|
|
4197
|
+
function renderTable(states, io, bin) {
|
|
3908
4198
|
const header2 = ["HARNESS", "KIND", "INSTALLED"];
|
|
3909
4199
|
const rows = states.map((s) => [s.id, s.kind, cell(s.installed, "?")]);
|
|
3910
4200
|
const widths = header2.map(
|
|
@@ -3914,7 +4204,9 @@ function renderTable(states, io) {
|
|
|
3914
4204
|
io.stdout(line(header2));
|
|
3915
4205
|
for (const row of rows) io.stdout(line(row));
|
|
3916
4206
|
if (states.some((s) => s.installed === null)) {
|
|
3917
|
-
io.stdout(
|
|
4207
|
+
io.stdout(`
|
|
4208
|
+
INSTALLED is \`?\` \u2014 the sandbox is not running. Start it with \`${bin} sandbox\`.
|
|
4209
|
+
`);
|
|
3918
4210
|
}
|
|
3919
4211
|
}
|
|
3920
4212
|
async function runHarnessList(opts, io) {
|
|
@@ -3925,12 +4217,12 @@ async function runHarnessList(opts, io) {
|
|
|
3925
4217
|
io.stdout(`${JSON.stringify(states, null, 2)}
|
|
3926
4218
|
`);
|
|
3927
4219
|
} else {
|
|
3928
|
-
renderTable(states, io);
|
|
4220
|
+
renderTable(states, io, opts.bin);
|
|
3929
4221
|
}
|
|
3930
4222
|
return 0;
|
|
3931
4223
|
}
|
|
3932
|
-
function unknownHarness(name, io) {
|
|
3933
|
-
io.stderr(
|
|
4224
|
+
function unknownHarness(name, io, bin) {
|
|
4225
|
+
io.stderr(`${bin} harness: unknown harness "${name}"
|
|
3934
4226
|
|
|
3935
4227
|
`);
|
|
3936
4228
|
io.stderr(`Known harnesses:
|
|
@@ -3944,30 +4236,30 @@ async function runHarnessStatus(name, opts, io) {
|
|
|
3944
4236
|
let only;
|
|
3945
4237
|
if (name !== void 0) {
|
|
3946
4238
|
only = findHarness(name);
|
|
3947
|
-
if (!only) return unknownHarness(name, io);
|
|
4239
|
+
if (!only) return unknownHarness(name, io, opts.bin);
|
|
3948
4240
|
}
|
|
3949
4241
|
const states = await collectStates(root, run, opts.env, only ? [only] : void 0);
|
|
3950
4242
|
if (opts.json) {
|
|
3951
4243
|
io.stdout(`${JSON.stringify(only ? states[0] : states, null, 2)}
|
|
3952
4244
|
`);
|
|
3953
4245
|
} else {
|
|
3954
|
-
renderTable(states, io);
|
|
4246
|
+
renderTable(states, io, opts.bin);
|
|
3955
4247
|
}
|
|
3956
4248
|
return 0;
|
|
3957
4249
|
}
|
|
3958
4250
|
function hermesTargetRoot(target) {
|
|
3959
4251
|
return target.kind === "docker-compose" ? "/home/sandbox/harness" : target.workspace.targetRoot;
|
|
3960
4252
|
}
|
|
3961
|
-
async function reconcileHermes(target, io) {
|
|
4253
|
+
async function reconcileHermes(target, io, bin) {
|
|
3962
4254
|
const root = hermesTargetRoot(target);
|
|
3963
4255
|
const result = await target.exec({
|
|
3964
|
-
argv:
|
|
3965
|
-
env:
|
|
4256
|
+
argv: remoteControlDirScript(root, "scripts/link-providers.sh", ["--init", "--hermes-only"]),
|
|
4257
|
+
env: aliasedEnvPair("PROJECT_ROOT", root),
|
|
3966
4258
|
user: "sandbox",
|
|
3967
4259
|
stdio: "inherit"
|
|
3968
4260
|
});
|
|
3969
4261
|
if (result.exitCode !== 0) {
|
|
3970
|
-
io.stderr(
|
|
4262
|
+
io.stderr(`${bin} harness: Hermes integration failed (exit ${result.exitCode}); no installation success reported.
|
|
3971
4263
|
`);
|
|
3972
4264
|
}
|
|
3973
4265
|
return result.exitCode;
|
|
@@ -3980,7 +4272,7 @@ async function runHarnessInstall(name, opts, io) {
|
|
|
3980
4272
|
name === "hermes" && runningInsideSandbox(env) && projectRoot !== void 0 ? projectRoot : opts.cwd
|
|
3981
4273
|
);
|
|
3982
4274
|
const entry = findHarness(name);
|
|
3983
|
-
if (!entry) return unknownHarness(name, io);
|
|
4275
|
+
if (!entry) return unknownHarness(name, io, opts.bin);
|
|
3984
4276
|
const target = targetFor(root, run, opts.env);
|
|
3985
4277
|
let status;
|
|
3986
4278
|
try {
|
|
@@ -3994,19 +4286,19 @@ async function runHarnessInstall(name, opts, io) {
|
|
|
3994
4286
|
}
|
|
3995
4287
|
if (!isReachable(status)) {
|
|
3996
4288
|
io.stderr(
|
|
3997
|
-
|
|
3998
|
-
Start it with
|
|
4289
|
+
`${opts.bin} harness: the sandbox is not running (${status}).
|
|
4290
|
+
Start it with \`${opts.bin} sandbox\`, then re-run this command.
|
|
3999
4291
|
`
|
|
4000
4292
|
);
|
|
4001
4293
|
return 1;
|
|
4002
4294
|
}
|
|
4003
4295
|
const hermes = entry.id === "hermes";
|
|
4004
4296
|
const installEnv = hermes ? {
|
|
4005
|
-
|
|
4297
|
+
...aliasedEnvPair("PROJECT_ROOT", hermesTargetRoot(target)),
|
|
4006
4298
|
HERMES_HOME: `${hermesTargetRoot(target)}/.hermes`
|
|
4007
4299
|
} : void 0;
|
|
4008
4300
|
if (hermes) {
|
|
4009
|
-
const code = await reconcileHermes(target, io);
|
|
4301
|
+
const code = await reconcileHermes(target, io, opts.bin);
|
|
4010
4302
|
if (code !== 0) return code;
|
|
4011
4303
|
}
|
|
4012
4304
|
const already = await probeInstalled(target, entry, installEnv);
|
|
@@ -4028,16 +4320,17 @@ Start it with \`oh sandbox\`, then re-run this command.
|
|
|
4028
4320
|
...installEnv ? { env: installEnv } : {}
|
|
4029
4321
|
});
|
|
4030
4322
|
if (r.exitCode !== 0) {
|
|
4031
|
-
io.stderr(
|
|
4323
|
+
io.stderr(`${opts.bin} harness: installing ${entry.id} failed (exit ${r.exitCode}).
|
|
4032
4324
|
`);
|
|
4033
4325
|
return r.exitCode;
|
|
4034
4326
|
}
|
|
4035
4327
|
if (hermes) {
|
|
4036
4328
|
if (await probeInstalled(target, entry, installEnv) !== true) {
|
|
4037
|
-
io.stderr(
|
|
4329
|
+
io.stderr(`${opts.bin} harness: Hermes installation finished but executable verification failed.
|
|
4330
|
+
`);
|
|
4038
4331
|
return 1;
|
|
4039
4332
|
}
|
|
4040
|
-
const code = await reconcileHermes(target, io);
|
|
4333
|
+
const code = await reconcileHermes(target, io, opts.bin);
|
|
4041
4334
|
if (code !== 0) return code;
|
|
4042
4335
|
}
|
|
4043
4336
|
io.stdout(`${entry.id}: installed \u2014 see ${sourceDocsUrl(entry.docsPath)} for authentication
|
|
@@ -4158,7 +4451,7 @@ var TOOL_CATALOG = Object.freeze([
|
|
|
4158
4451
|
binary: "docker",
|
|
4159
4452
|
verifyArgv: Object.freeze(["bash", "-lc", "command -v docker >/dev/null"]),
|
|
4160
4453
|
versionArgv: Object.freeze(["docker", "--version"]),
|
|
4161
|
-
notInstallableReason:
|
|
4454
|
+
notInstallableReason: (bin) => `The Docker CLI is installed in the base image. Note that the CLI being present says nothing about whether a daemon is reachable \u2014 \`${bin} ps <name>\` answers that.`,
|
|
4162
4455
|
docsPath: TOOLS_DOC
|
|
4163
4456
|
}),
|
|
4164
4457
|
Object.freeze({
|
|
@@ -4168,7 +4461,7 @@ var TOOL_CATALOG = Object.freeze([
|
|
|
4168
4461
|
binary: "gh",
|
|
4169
4462
|
verifyArgv: Object.freeze(["bash", "-lc", "command -v gh >/dev/null"]),
|
|
4170
4463
|
versionArgv: Object.freeze(["gh", "--version"]),
|
|
4171
|
-
notInstallableReason: "The GitHub CLI is installed in the base image. Run `gh auth login` inside the sandbox to authenticate it.",
|
|
4464
|
+
notInstallableReason: () => "The GitHub CLI is installed in the base image. Run `gh auth login` inside the sandbox to authenticate it.",
|
|
4172
4465
|
docsPath: TOOLS_DOC
|
|
4173
4466
|
}),
|
|
4174
4467
|
Object.freeze({
|
|
@@ -4275,7 +4568,7 @@ function cell2(value, absent) {
|
|
|
4275
4568
|
if (value === null) return absent;
|
|
4276
4569
|
return value ? "yes" : "no";
|
|
4277
4570
|
}
|
|
4278
|
-
function renderTable2(rows, io) {
|
|
4571
|
+
function renderTable2(rows, io, bin) {
|
|
4279
4572
|
const header2 = ["TOOL", "KIND", "INSTALLED"];
|
|
4280
4573
|
const body = rows.map((r) => [r.id, r.kind, cell2(r.installed, "?")]);
|
|
4281
4574
|
const widths = header2.map(
|
|
@@ -4285,11 +4578,13 @@ function renderTable2(rows, io) {
|
|
|
4285
4578
|
io.stdout(line(header2));
|
|
4286
4579
|
for (const row of body) io.stdout(line(row));
|
|
4287
4580
|
if (rows.some((r) => r.installed === null)) {
|
|
4288
|
-
io.stdout(
|
|
4581
|
+
io.stdout(`
|
|
4582
|
+
INSTALLED is \`?\` \u2014 the sandbox is not running. Start it with \`${bin} sandbox\`.
|
|
4583
|
+
`);
|
|
4289
4584
|
}
|
|
4290
4585
|
}
|
|
4291
|
-
function renderDetail(rows, io) {
|
|
4292
|
-
renderTable2(rows, io);
|
|
4586
|
+
function renderDetail(rows, io, bin) {
|
|
4587
|
+
renderTable2(rows, io, bin);
|
|
4293
4588
|
for (const r of rows) {
|
|
4294
4589
|
io.stdout(`
|
|
4295
4590
|
${r.id} \u2014 ${r.title}
|
|
@@ -4310,12 +4605,12 @@ async function runToolList(opts, io) {
|
|
|
4310
4605
|
io.stdout(`${JSON.stringify(rows, null, 2)}
|
|
4311
4606
|
`);
|
|
4312
4607
|
} else {
|
|
4313
|
-
renderTable2(rows, io);
|
|
4608
|
+
renderTable2(rows, io, opts.bin);
|
|
4314
4609
|
}
|
|
4315
4610
|
return 0;
|
|
4316
4611
|
}
|
|
4317
|
-
function unknownTool(name, io) {
|
|
4318
|
-
io.stderr(
|
|
4612
|
+
function unknownTool(name, io, bin) {
|
|
4613
|
+
io.stderr(`${bin} tool: unknown tool "${name}"
|
|
4319
4614
|
|
|
4320
4615
|
`);
|
|
4321
4616
|
io.stderr(`Known tools:
|
|
@@ -4329,14 +4624,14 @@ async function runToolStatus(name, opts, io) {
|
|
|
4329
4624
|
let only;
|
|
4330
4625
|
if (name !== void 0) {
|
|
4331
4626
|
only = findTool(name);
|
|
4332
|
-
if (!only) return unknownTool(name, io);
|
|
4627
|
+
if (!only) return unknownTool(name, io, opts.bin);
|
|
4333
4628
|
}
|
|
4334
4629
|
const rows = await collectRows(root, run, opts.env, only ? [only] : void 0);
|
|
4335
4630
|
if (opts.json) {
|
|
4336
4631
|
io.stdout(`${JSON.stringify(only ? rows[0] : rows, null, 2)}
|
|
4337
4632
|
`);
|
|
4338
4633
|
} else {
|
|
4339
|
-
renderDetail(rows, io);
|
|
4634
|
+
renderDetail(rows, io, opts.bin);
|
|
4340
4635
|
}
|
|
4341
4636
|
return 0;
|
|
4342
4637
|
}
|
|
@@ -4357,12 +4652,12 @@ async function runToolInstall(name, opts, io) {
|
|
|
4357
4652
|
const run = opts.run ?? spawnRunner;
|
|
4358
4653
|
const root = resolveProjectRoot(opts.cwd);
|
|
4359
4654
|
const entry = findTool(name);
|
|
4360
|
-
if (!entry) return unknownTool(name, io);
|
|
4655
|
+
if (!entry) return unknownTool(name, io, opts.bin);
|
|
4361
4656
|
if (entry.installArgv === void 0) {
|
|
4362
|
-
io.stderr(
|
|
4657
|
+
io.stderr(`${opts.bin} tool: ${entry.id} cannot be installed by this command.
|
|
4363
4658
|
|
|
4364
4659
|
`);
|
|
4365
|
-
io.stderr(`${entry.notInstallableReason ?? ""}
|
|
4660
|
+
io.stderr(`${entry.notInstallableReason?.(opts.bin) ?? ""}
|
|
4366
4661
|
|
|
4367
4662
|
`);
|
|
4368
4663
|
io.stderr(`Installable tools:
|
|
@@ -4383,8 +4678,8 @@ ${installableToolIds().map((t) => ` ${t}`).join("\n")}
|
|
|
4383
4678
|
}
|
|
4384
4679
|
if (!isReachable2(status)) {
|
|
4385
4680
|
io.stderr(
|
|
4386
|
-
|
|
4387
|
-
Start it with
|
|
4681
|
+
`${opts.bin} tool: the sandbox is not running (${status}).
|
|
4682
|
+
Start it with \`${opts.bin} sandbox\`, then re-run this command.
|
|
4388
4683
|
`
|
|
4389
4684
|
);
|
|
4390
4685
|
return 1;
|
|
@@ -4408,7 +4703,7 @@ Start it with \`oh sandbox\`, then re-run this command.
|
|
|
4408
4703
|
stdio: "inherit"
|
|
4409
4704
|
});
|
|
4410
4705
|
if (r.exitCode !== 0) {
|
|
4411
|
-
io.stderr(
|
|
4706
|
+
io.stderr(`${opts.bin} tool: installing ${entry.id} failed (exit ${r.exitCode}).
|
|
4412
4707
|
`);
|
|
4413
4708
|
return r.exitCode;
|
|
4414
4709
|
}
|
|
@@ -4419,7 +4714,7 @@ Start it with \`oh sandbox\`, then re-run this command.
|
|
|
4419
4714
|
|
|
4420
4715
|
// src/lib/remote.ts
|
|
4421
4716
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
4422
|
-
var DEFAULT_REPO_URL = "https://github.com/mifunedev/
|
|
4717
|
+
var DEFAULT_REPO_URL = "https://github.com/mifunedev/agro";
|
|
4423
4718
|
var DEFAULT_CLONE_TIMEOUT_MS = 12e4;
|
|
4424
4719
|
var spawnRunner2 = (cmd, args, opts) => {
|
|
4425
4720
|
const r = spawnSync3(cmd, args, {
|
|
@@ -4436,7 +4731,7 @@ var spawnRunner2 = (cmd, args, opts) => {
|
|
|
4436
4731
|
};
|
|
4437
4732
|
};
|
|
4438
4733
|
function fallbackHint() {
|
|
4439
|
-
return "use --from <dir> to point at a local
|
|
4734
|
+
return "use --from <dir> to point at a local AGRO checkout instead";
|
|
4440
4735
|
}
|
|
4441
4736
|
function fetchRemoteSource(opts) {
|
|
4442
4737
|
const repoUrl2 = opts.repoUrl ?? DEFAULT_REPO_URL;
|
|
@@ -4481,9 +4776,9 @@ function fetchRemoteSource(opts) {
|
|
|
4481
4776
|
}
|
|
4482
4777
|
|
|
4483
4778
|
// src/cli.ts
|
|
4484
|
-
var VERSION = true ? "0.
|
|
4485
|
-
var DEFAULT_SOURCE_OH_DIR =
|
|
4486
|
-
|
|
4779
|
+
var VERSION = true ? "0.11.0" : "0.0.0-dev";
|
|
4780
|
+
var DEFAULT_SOURCE_OH_DIR = resolve10(
|
|
4781
|
+
dirname8(fileURLToPath(import.meta.url)),
|
|
4487
4782
|
"../.."
|
|
4488
4783
|
);
|
|
4489
4784
|
var INTEGRATIONS = {};
|
|
@@ -4506,14 +4801,16 @@ function compatibilityNote(product) {
|
|
|
4506
4801
|
}
|
|
4507
4802
|
function printOhHelp(product = LEGACY_PRODUCT) {
|
|
4508
4803
|
const { bin, title } = product;
|
|
4804
|
+
const updateSummary = bin === AGRO_PRODUCT.bin ? `Upgrade the installed ${bin} CLI` : `Vendor or upgrade the ${stateNames(bin).controlDir}/ control plane`;
|
|
4509
4805
|
process.stdout.write(`${bin} \u2014 ${title} (v${VERSION})
|
|
4510
4806
|
|
|
4511
4807
|
Usage:
|
|
4512
4808
|
${bin} sandbox <args...> Create and list sandboxes (install|list)
|
|
4513
4809
|
${bin} shell [name] Open a zsh shell in the running sandbox container
|
|
4514
|
-
${bin} config <args...> Read and write
|
|
4810
|
+
${bin} config <args...> Read and write ${stateNames(bin).configFile} (show|set), or run a wizard
|
|
4515
4811
|
${bin} secret <args...> Read and write the gitignored root .env (set|list)
|
|
4516
|
-
${bin} update
|
|
4812
|
+
${bin} update ${updateSummary}
|
|
4813
|
+
${bin} migrate Move a legacy .oh/ project or ~/.oh registry to AGRO names (--check|--home)
|
|
4517
4814
|
${bin} stop [name] Stop the sandbox, preserving volumes
|
|
4518
4815
|
${bin} restart [name] Restart the sandbox service
|
|
4519
4816
|
${bin} logs [name] Tail sandbox logs (follows)
|
|
@@ -4523,7 +4820,6 @@ Usage:
|
|
|
4523
4820
|
${bin} harness <args...> Install and inspect agent CLI harnesses
|
|
4524
4821
|
${bin} tool <args...> Install and inspect sandbox tooling
|
|
4525
4822
|
${bin} gateway <args...> Manage a messaging client session (pi|hermes)
|
|
4526
|
-
${bin} cloud <args...> Manage OpenHarness Cloud nodes
|
|
4527
4823
|
${bin} --version Print version
|
|
4528
4824
|
${bin} --help Show this help
|
|
4529
4825
|
|
|
@@ -4532,21 +4828,25 @@ ${integrationLines()}
|
|
|
4532
4828
|
${compatibilityNote(product)}`);
|
|
4533
4829
|
}
|
|
4534
4830
|
function printConfigHelp(bin = LEGACY_PRODUCT.bin) {
|
|
4535
|
-
process.stdout.write(`${bin} config \u2014 Read and write
|
|
4831
|
+
process.stdout.write(`${bin} config \u2014 Read and write ${stateNames(bin).configFile}, the tracked non-secret settings
|
|
4536
4832
|
|
|
4537
4833
|
Usage:
|
|
4538
4834
|
${bin} config show [--sandbox <name>]
|
|
4539
|
-
${bin} config set <field> <value> [--sandbox <name>]
|
|
4835
|
+
${bin} config set <field> <value> [--sandbox <name>] [--force]
|
|
4540
4836
|
${bin} config repo Create your own GitHub repo and point origin at it
|
|
4541
4837
|
${bin} config <integration> Run an integration wizard
|
|
4542
4838
|
${bin} config <integration> --help
|
|
4543
4839
|
|
|
4544
|
-
|
|
4840
|
+
${stateNames(bin).configFile} holds every non-secret setting and is tracked by git. Credentials live
|
|
4545
4841
|
in the gitignored root .env \u2014 write those with \`${bin} secret set <KEY>\`. Apply a
|
|
4546
4842
|
change with \`${bin} stop <name> && ${bin} sandbox install docker --name <name>\`. Field
|
|
4547
4843
|
reference:
|
|
4548
4844
|
${sourceDocsUrl("docs/configuration.md")}
|
|
4549
4845
|
|
|
4846
|
+
\`${bin} config set storage.homePath <dir>\` refuses the change when the sandbox's named
|
|
4847
|
+
volume already exists, because the next start would orphan every file in it. Pass
|
|
4848
|
+
--force to change it anyway.
|
|
4849
|
+
|
|
4550
4850
|
Fields:
|
|
4551
4851
|
${configFieldList()}
|
|
4552
4852
|
|
|
@@ -4566,7 +4866,7 @@ the registry entry \`${bin} sandbox list\` names.
|
|
|
4566
4866
|
|
|
4567
4867
|
The value is never read from the command line \u2014 an argument would land in your
|
|
4568
4868
|
shell history. \`${bin} secret list\` never prints a raw value. .env is mode 0600 and
|
|
4569
|
-
gitignored; every non-secret setting belongs in
|
|
4869
|
+
gitignored; every non-secret setting belongs in ${stateNames(bin).configFile} (\`${bin} config set\`).
|
|
4570
4870
|
|
|
4571
4871
|
Keys:
|
|
4572
4872
|
${secretKeyList()}
|
|
@@ -4579,7 +4879,7 @@ Usage:
|
|
|
4579
4879
|
${bin} update [--dry-run]
|
|
4580
4880
|
|
|
4581
4881
|
Upgrades exactly one thing: the ${bin} executable that is running. It writes no
|
|
4582
|
-
project file \u2014 no .
|
|
4882
|
+
project file \u2014 no ${stateNames(bin).controlDir}/, no ${stateNames(bin).configFile}, no .env \u2014 and it never asks for sudo.
|
|
4583
4883
|
|
|
4584
4884
|
The upgrade follows whichever mechanism installed this executable:
|
|
4585
4885
|
npm-managed realpath under node_modules/${AGRO_PRODUCT.packageName}/: reads the registry
|
|
@@ -4603,7 +4903,7 @@ Flags:
|
|
|
4603
4903
|
--dry-run Report the installation kind, target, and versions without
|
|
4604
4904
|
changing anything.
|
|
4605
4905
|
|
|
4606
|
-
Project payload vendoring (.
|
|
4906
|
+
Project payload vendoring (${stateNames(bin).controlDir}/ + crons/) is \`oh update\` during the
|
|
4607
4907
|
compatibility window; its --from, --from-remote, --ref and --force flags are
|
|
4608
4908
|
not accepted here.
|
|
4609
4909
|
`);
|
|
@@ -4613,22 +4913,22 @@ function printUpdateHelp(bin = LEGACY_PRODUCT.bin) {
|
|
|
4613
4913
|
printSelfUpgradeHelp(bin);
|
|
4614
4914
|
return;
|
|
4615
4915
|
}
|
|
4616
|
-
process.stdout.write(`${bin} update \u2014 Vendor or upgrade the .
|
|
4916
|
+
process.stdout.write(`${bin} update \u2014 Vendor or upgrade the ${stateNames(bin).controlDir}/ control plane
|
|
4617
4917
|
|
|
4618
4918
|
Usage:
|
|
4619
4919
|
${bin} update [--from <dir> | --from-remote [--ref <ref>]] [--dry-run] [--force]
|
|
4620
4920
|
|
|
4621
|
-
Writes ONLY the .
|
|
4921
|
+
Writes ONLY the ${stateNames(bin).controlDir}/ control plane and crons/ (skills, scripts, CLI) into the
|
|
4622
4922
|
current directory. An empty directory is equipped from scratch; everything else
|
|
4623
|
-
in your project is left untouched \u2014 it writes no
|
|
4923
|
+
in your project is left untouched \u2014 it writes no ${stateNames(bin).configFile}, .env, AGENTS.md,
|
|
4624
4924
|
.gitignore or .devcontainer/.
|
|
4625
4925
|
|
|
4626
4926
|
Payload source precedence: --from <dir> > --from-remote > the CLI's own bundled
|
|
4627
|
-
.
|
|
4927
|
+
${stateNames(bin).controlDir}/ payload > a remote fetch announced on one line.
|
|
4628
4928
|
|
|
4629
4929
|
Flags:
|
|
4630
|
-
--from <dir> A built
|
|
4631
|
-
--from-remote Fetch the source checkout from the public
|
|
4930
|
+
--from <dir> A built AGRO checkout to vendor from.
|
|
4931
|
+
--from-remote Fetch the source checkout from the public AGRO repo
|
|
4632
4932
|
instead (shallow git clone into a temp dir, removed after
|
|
4633
4933
|
the run). Conflicts with --from.
|
|
4634
4934
|
--ref <ref> Branch or tag for --from-remote (default: the clone's
|
|
@@ -4647,28 +4947,38 @@ function printSandboxHelp(bin = LEGACY_PRODUCT.bin) {
|
|
|
4647
4947
|
process.stdout.write(`${bin} sandbox \u2014 Create and list sandboxes
|
|
4648
4948
|
|
|
4649
4949
|
Usage:
|
|
4650
|
-
${bin} sandbox install <runtime> [--name <name>] [--
|
|
4950
|
+
${bin} sandbox install <runtime> [--name <name>] [--checkout <dir>]
|
|
4951
|
+
[--home-mount <dir>] [--yes]
|
|
4651
4952
|
[--image[=<ref>]] [--no-build] [--print-argv]
|
|
4652
4953
|
${bin} sandbox list [--json]
|
|
4653
4954
|
|
|
4654
|
-
\`install\` writes a sandbox entry under \${
|
|
4955
|
+
\`install\` writes a sandbox entry under \${${stateNames(bin).envPrefix}HOME:-~/${stateNames(bin).userStateDir}}/sandboxes/<name>/,
|
|
4655
4956
|
materialises the compose files and the compose wrapper into it, then starts the
|
|
4656
4957
|
container. It needs no project checkout and runs from any directory. Without
|
|
4657
|
-
--
|
|
4658
|
-
bound at /home/sandbox/harness
|
|
4958
|
+
--checkout the sandbox runs the prebuilt image; with --checkout <dir> that
|
|
4959
|
+
checkout is bound at /home/sandbox/harness; the sandbox builds locally only
|
|
4960
|
+
when that directory holds .devcontainer/Dockerfile.
|
|
4659
4961
|
|
|
4660
4962
|
On a terminal without --yes it asks for the sandbox name, the timezone, the git
|
|
4661
|
-
identity, SSH (with its host port),
|
|
4662
|
-
seeds those answers from that
|
|
4963
|
+
identity, SSH (with its host port), the host Docker socket, and the host path
|
|
4964
|
+
for /home/sandbox. With --checkout it seeds those answers from that
|
|
4965
|
+
checkout's ${stateNames(bin).configFile}.
|
|
4663
4966
|
|
|
4664
4967
|
Flags:
|
|
4665
|
-
--name <name> Registry entry name (default: the lowest free
|
|
4666
|
-
--
|
|
4667
|
-
from its
|
|
4968
|
+
--name <name> Registry entry name (default: the lowest free ${DEFAULT_NAME_PREFIX}<n>)
|
|
4969
|
+
--checkout <dir> Bind this checkout into the sandbox and seed the defaults
|
|
4970
|
+
from its ${stateNames(bin).configFile}. The sandbox builds
|
|
4971
|
+
locally only when that directory holds
|
|
4972
|
+
.devcontainer/Dockerfile
|
|
4973
|
+
--repo <dir> Deprecated alias for --checkout
|
|
4974
|
+
--home-mount <dir>
|
|
4975
|
+
Bind this host directory at /home/sandbox instead of the
|
|
4976
|
+
Docker-managed volume. Relative paths resolve against the
|
|
4977
|
+
working directory. The directory is created when absent
|
|
4668
4978
|
--yes Non-interactive: keep every default and ask nothing
|
|
4669
4979
|
--image[=<ref>] Run the prebuilt image instead of building (implies
|
|
4670
4980
|
--no-build). Ref resolves last-wins: --image=<ref> >
|
|
4671
|
-
|
|
4981
|
+
${stateNames(bin).configFile} image.ref > ${DEFAULT_SANDBOX_IMAGE}.
|
|
4672
4982
|
--no-build Suppress the local build and reuse an existing image
|
|
4673
4983
|
--print-argv Print the docker compose argv that would run, then exit
|
|
4674
4984
|
without writing an entry
|
|
@@ -4702,7 +5012,7 @@ Usage:
|
|
|
4702
5012
|
${bin} harness status [name] Show installed state
|
|
4703
5013
|
|
|
4704
5014
|
\`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
|
|
5015
|
+
into the persistent home volume, and reports. It reads and writes no \`${stateNames(bin).configFile}\`
|
|
4706
5016
|
field, and it never rebuilds or restarts the sandbox. It requires a running
|
|
4707
5017
|
sandbox \u2014 start one with \`${bin} sandbox\` first.
|
|
4708
5018
|
|
|
@@ -4723,7 +5033,7 @@ Usage:
|
|
|
4723
5033
|
${bin} gateway status show both sessions
|
|
4724
5034
|
|
|
4725
5035
|
Only a LEADING --help/-h is intercepted here; everything else passes through
|
|
4726
|
-
verbatim to the vendored .
|
|
5036
|
+
verbatim to the vendored ${stateNames(bin).controlDir}/scripts/gateway.sh with ${stateNames(bin).envPrefix}PROJECT_ROOT set to
|
|
4727
5037
|
the equipped project root. Exits with the script's exit code.
|
|
4728
5038
|
`);
|
|
4729
5039
|
}
|
|
@@ -4751,7 +5061,7 @@ Most tools are baked into the image and are report-only; \`install\` works on:
|
|
|
4751
5061
|
${installableToolIds().map((t) => ` ${t}`).join("\n")}
|
|
4752
5062
|
|
|
4753
5063
|
\`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
|
|
5064
|
+
into the persistent home volume, and reports. It reads and writes no \`${stateNames(bin).configFile}\`
|
|
4755
5065
|
field, and it never rebuilds or restarts the sandbox. A large download is
|
|
4756
5066
|
confirmed first, and a non-interactive run without --yes installs nothing.
|
|
4757
5067
|
|
|
@@ -4776,7 +5086,7 @@ function printComposeVerbHelp(verb, bin = LEGACY_PRODUCT.bin) {
|
|
|
4776
5086
|
Usage:
|
|
4777
5087
|
${bin} ${verb} [name] [-- <extra docker compose args>]
|
|
4778
5088
|
|
|
4779
|
-
Runs .
|
|
5089
|
+
Runs ${stateNames(bin).controlDir}/scripts/docker-compose.sh inside the sandbox entry, the single
|
|
4780
5090
|
implementation. \`${bin}\` is the only lifecycle door. [name] is a sandbox entry from
|
|
4781
5091
|
\`${bin} sandbox list\`; with exactly one registered sandbox it can be omitted.
|
|
4782
5092
|
|
|
@@ -4789,7 +5099,7 @@ function printDestroyHelp(bin = LEGACY_PRODUCT.bin) {
|
|
|
4789
5099
|
Usage:
|
|
4790
5100
|
${bin} destroy [name] [--yes]
|
|
4791
5101
|
|
|
4792
|
-
Runs .
|
|
5102
|
+
Runs ${stateNames(bin).controlDir}/scripts/docker-compose.sh with \`down -v\`. This is the one destructive
|
|
4793
5103
|
lifecycle verb: \`-v\` deletes the named
|
|
4794
5104
|
volumes, and those volumes hold every agent CLI login, the gh CLI token, and
|
|
4795
5105
|
the SSH keys. Use \`${bin} stop\` when you only want the containers gone.
|
|
@@ -4812,11 +5122,11 @@ Usage:
|
|
|
4812
5122
|
${bin} compose config [-- <extra docker compose args>]
|
|
4813
5123
|
|
|
4814
5124
|
Subcommands:
|
|
4815
|
-
config Print the compose configuration .
|
|
4816
|
-
resolves from .devcontainer/.env and .
|
|
5125
|
+
config Print the compose configuration ${stateNames(bin).controlDir}/scripts/docker-compose.sh
|
|
5126
|
+
resolves from .devcontainer/.env and ${stateNames(bin).controlDir}/config.json
|
|
4817
5127
|
|
|
4818
5128
|
Namespaced under \`${bin} compose\` because \`${bin} config <integration>\` already means
|
|
4819
|
-
"run an integration wizard", and \`${bin} config show/set\` reads and writes
|
|
5129
|
+
"run an integration wizard", and \`${bin} config show/set\` reads and writes ${stateNames(bin).configFile}.
|
|
4820
5130
|
|
|
4821
5131
|
See ${sourceDocsUrl("docs/lifecycle-commands.md")} for every verb.
|
|
4822
5132
|
`);
|
|
@@ -4842,11 +5152,13 @@ var CONFIG_VERBS = ["show", "set", "repo"];
|
|
|
4842
5152
|
function parseConfigArgs(input, bin = LEGACY_PRODUCT.bin) {
|
|
4843
5153
|
const scoped = extractSandboxFlag(`${bin} config`, input);
|
|
4844
5154
|
if (!scoped.ok) return scoped;
|
|
4845
|
-
const
|
|
5155
|
+
const forced = scoped.args.rest.includes("--force");
|
|
5156
|
+
const rest = scoped.args.rest.filter((arg) => arg !== "--force");
|
|
4846
5157
|
const args = {
|
|
4847
5158
|
help: false,
|
|
4848
5159
|
integrationHelp: false,
|
|
4849
|
-
...scoped.args.sandbox === void 0 ? {} : { sandbox: scoped.args.sandbox }
|
|
5160
|
+
...scoped.args.sandbox === void 0 ? {} : { sandbox: scoped.args.sandbox },
|
|
5161
|
+
...forced ? { force: true } : {}
|
|
4850
5162
|
};
|
|
4851
5163
|
if (rest.length === 0 || isHelpFlag(rest[0])) {
|
|
4852
5164
|
return { ok: true, args: { ...args, help: true } };
|
|
@@ -4962,14 +5274,14 @@ function parseComposeArgs(rest, bin = LEGACY_PRODUCT.bin) {
|
|
|
4962
5274
|
args.subcommand = "config";
|
|
4963
5275
|
const tail = rest.slice(1);
|
|
4964
5276
|
if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
|
|
4965
|
-
const
|
|
4966
|
-
if (
|
|
5277
|
+
const sep5 = tail.indexOf("--");
|
|
5278
|
+
if (sep5 === -1 && tail.length > 0) {
|
|
4967
5279
|
return {
|
|
4968
5280
|
ok: false,
|
|
4969
5281
|
error: `${bin} compose config: unexpected argument "${tail[0]}" \u2014 pass extra docker compose args after \`--\``
|
|
4970
5282
|
};
|
|
4971
5283
|
}
|
|
4972
|
-
if (
|
|
5284
|
+
if (sep5 !== -1) args.passthrough = tail.slice(sep5 + 1);
|
|
4973
5285
|
return { ok: true, args };
|
|
4974
5286
|
}
|
|
4975
5287
|
var PAYLOAD_UPDATE_FLAGS = ["--from", "--from-remote", "--ref", "--force"];
|
|
@@ -5033,7 +5345,9 @@ function parseUpdateArgs(rest, bin = LEGACY_PRODUCT.bin) {
|
|
|
5033
5345
|
}
|
|
5034
5346
|
var SANDBOX_VALUE_FLAGS = {
|
|
5035
5347
|
"--name": "name",
|
|
5036
|
-
"--
|
|
5348
|
+
"--checkout": "checkout",
|
|
5349
|
+
"--repo": "checkout",
|
|
5350
|
+
"--home-mount": "homeMount"
|
|
5037
5351
|
};
|
|
5038
5352
|
function parseSandboxArgs(rest, bin = LEGACY_PRODUCT.bin) {
|
|
5039
5353
|
const args = {
|
|
@@ -5058,6 +5372,7 @@ function parseSandboxArgs(rest, bin = LEGACY_PRODUCT.bin) {
|
|
|
5058
5372
|
args.subcommand = head;
|
|
5059
5373
|
if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
|
|
5060
5374
|
const positionals = [];
|
|
5375
|
+
const checkoutSpellings = /* @__PURE__ */ new Set();
|
|
5061
5376
|
for (let i = 0; i < tail.length; i++) {
|
|
5062
5377
|
const token = tail[i];
|
|
5063
5378
|
const valueFlag = SANDBOX_VALUE_FLAGS[token];
|
|
@@ -5066,6 +5381,7 @@ function parseSandboxArgs(rest, bin = LEGACY_PRODUCT.bin) {
|
|
|
5066
5381
|
if (value === void 0) {
|
|
5067
5382
|
return { ok: false, error: `${bin} sandbox ${head}: ${token} requires a value` };
|
|
5068
5383
|
}
|
|
5384
|
+
if (valueFlag === "checkout") checkoutSpellings.add(token);
|
|
5069
5385
|
args[valueFlag] = value;
|
|
5070
5386
|
i++;
|
|
5071
5387
|
} else if (token === "--yes") {
|
|
@@ -5091,6 +5407,12 @@ function parseSandboxArgs(rest, bin = LEGACY_PRODUCT.bin) {
|
|
|
5091
5407
|
positionals.push(token);
|
|
5092
5408
|
}
|
|
5093
5409
|
}
|
|
5410
|
+
if (checkoutSpellings.has("--checkout") && checkoutSpellings.has("--repo")) {
|
|
5411
|
+
return {
|
|
5412
|
+
ok: false,
|
|
5413
|
+
error: `${bin} sandbox ${head}: --checkout conflicts with --repo \u2014 pass exactly one, and prefer --checkout`
|
|
5414
|
+
};
|
|
5415
|
+
}
|
|
5094
5416
|
if (head === "list") {
|
|
5095
5417
|
if (positionals.length > 0) {
|
|
5096
5418
|
return { ok: false, error: `${bin} sandbox list: unexpected argument "${positionals[0]}"` };
|
|
@@ -5209,13 +5531,13 @@ function bundledPayloadExists(bundled, exists = existsSync16) {
|
|
|
5209
5531
|
function resolveUpdateSource(args, bundled, bin = LEGACY_PRODUCT.bin) {
|
|
5210
5532
|
const exists = bundled.exists ?? existsSync16;
|
|
5211
5533
|
if (args.fromDir !== void 0) {
|
|
5212
|
-
return { kind: "local", fromDir:
|
|
5534
|
+
return { kind: "local", fromDir: resolve10(args.fromDir) };
|
|
5213
5535
|
}
|
|
5214
5536
|
if (args.fromRemote) {
|
|
5215
5537
|
return { kind: "remote", ref: args.ref };
|
|
5216
5538
|
}
|
|
5217
5539
|
if (bundledPayloadExists(bundled, exists)) {
|
|
5218
|
-
return { kind: "local", fromDir:
|
|
5540
|
+
return { kind: "local", fromDir: resolve10(bundled.sourceOhDir, "..") };
|
|
5219
5541
|
}
|
|
5220
5542
|
return {
|
|
5221
5543
|
kind: "remote",
|
|
@@ -5227,7 +5549,7 @@ function resolveUpdateSource(args, bundled, bin = LEGACY_PRODUCT.bin) {
|
|
|
5227
5549
|
function readPayloadVersion(checkoutDir) {
|
|
5228
5550
|
try {
|
|
5229
5551
|
const parsed = JSON.parse(
|
|
5230
|
-
readFileSync11(join10(checkoutDir
|
|
5552
|
+
readFileSync11(join10(resolveControlDir(checkoutDir).path, "cli", "package.json"), "utf8")
|
|
5231
5553
|
);
|
|
5232
5554
|
if (parsed && typeof parsed.version === "string") return parsed.version;
|
|
5233
5555
|
} catch {
|
|
@@ -5280,9 +5602,13 @@ async function main(argv) {
|
|
|
5280
5602
|
stdout: (s) => process.stdout.write(s),
|
|
5281
5603
|
stderr: (s) => process.stderr.write(s)
|
|
5282
5604
|
};
|
|
5283
|
-
const scope =
|
|
5605
|
+
const scope = {
|
|
5606
|
+
bin,
|
|
5607
|
+
...a.sandbox === void 0 ? {} : { sandbox: a.sandbox },
|
|
5608
|
+
...a.force === true ? { force: true } : {}
|
|
5609
|
+
};
|
|
5284
5610
|
if (a.verb === "show") return await runConfigShow(scope, io);
|
|
5285
|
-
if (a.verb === "repo") return await runConfigRepo({}, io);
|
|
5611
|
+
if (a.verb === "repo") return await runConfigRepo({ bin }, io);
|
|
5286
5612
|
return await runConfigSet(a.key, a.value, scope, io);
|
|
5287
5613
|
}
|
|
5288
5614
|
const name = a.integration;
|
|
@@ -5317,10 +5643,28 @@ async function main(argv) {
|
|
|
5317
5643
|
stdout: (s) => process.stdout.write(s),
|
|
5318
5644
|
stderr: (s) => process.stderr.write(s)
|
|
5319
5645
|
};
|
|
5320
|
-
const scope = a.sandbox === void 0 ? {} : { sandbox: a.sandbox };
|
|
5646
|
+
const scope = a.sandbox === void 0 ? { bin } : { bin, sandbox: a.sandbox };
|
|
5321
5647
|
if (a.verb === "list") return await runSecretList(scope, io);
|
|
5322
5648
|
return await runSecretSet(a.key, scope, io);
|
|
5323
5649
|
}
|
|
5650
|
+
if (first === "migrate") {
|
|
5651
|
+
const parsed = parseMigrateArgs(argv.slice(1), bin);
|
|
5652
|
+
if (!parsed.ok) {
|
|
5653
|
+
process.stderr.write(`${parsed.error}
|
|
5654
|
+
`);
|
|
5655
|
+
if (parsed.showHelp) printMigrateHelp(bin);
|
|
5656
|
+
return 1;
|
|
5657
|
+
}
|
|
5658
|
+
if (parsed.args.help) {
|
|
5659
|
+
printMigrateHelp(bin);
|
|
5660
|
+
return 0;
|
|
5661
|
+
}
|
|
5662
|
+
const io = {
|
|
5663
|
+
stdout: (s) => process.stdout.write(s),
|
|
5664
|
+
stderr: (s) => process.stderr.write(s)
|
|
5665
|
+
};
|
|
5666
|
+
return runMigrate(parsed.args, io);
|
|
5667
|
+
}
|
|
5324
5668
|
if (first === "update") {
|
|
5325
5669
|
const parsed = parseUpdateArgs(argv.slice(1), bin);
|
|
5326
5670
|
if (!parsed.ok) {
|
|
@@ -5344,12 +5688,12 @@ async function main(argv) {
|
|
|
5344
5688
|
const targetDir = process.cwd();
|
|
5345
5689
|
const source = resolveUpdateSource(parsed.args, { sourceOhDir: DEFAULT_SOURCE_OH_DIR }, bin);
|
|
5346
5690
|
if (source.kind === "local") {
|
|
5347
|
-
return await runUpdate({ targetDir, fromDir: source.fromDir, force, dryRun }, io);
|
|
5691
|
+
return await runUpdate({ bin, targetDir, fromDir: source.fromDir, force, dryRun }, io);
|
|
5348
5692
|
}
|
|
5349
5693
|
if (source.notice) process.stdout.write(source.notice);
|
|
5350
5694
|
return await runWithRemoteSource(
|
|
5351
5695
|
{ ref: source.ref },
|
|
5352
|
-
(checkoutDir) => runUpdate({ targetDir, fromDir: checkoutDir, force, dryRun }, io)
|
|
5696
|
+
(checkoutDir) => runUpdate({ bin, targetDir, fromDir: checkoutDir, force, dryRun }, io)
|
|
5353
5697
|
);
|
|
5354
5698
|
}
|
|
5355
5699
|
if (first === "sandbox") {
|
|
@@ -5366,12 +5710,14 @@ async function main(argv) {
|
|
|
5366
5710
|
return a.subcommand === void 0 ? 1 : 0;
|
|
5367
5711
|
}
|
|
5368
5712
|
const io = lifecycleIo();
|
|
5369
|
-
if (a.subcommand === "list") return await runSandboxList({ json: a.json }, io);
|
|
5713
|
+
if (a.subcommand === "list") return await runSandboxList({ bin, json: a.json }, io);
|
|
5370
5714
|
return await runSandboxInstall(
|
|
5371
5715
|
{
|
|
5716
|
+
bin,
|
|
5372
5717
|
runtime: a.runtime,
|
|
5373
5718
|
...a.name !== void 0 ? { name: a.name } : {},
|
|
5374
|
-
...a.
|
|
5719
|
+
...a.checkout !== void 0 ? { checkout: a.checkout } : {},
|
|
5720
|
+
...a.homeMount !== void 0 ? { homeMount: a.homeMount } : {},
|
|
5375
5721
|
yes: a.yes,
|
|
5376
5722
|
image: a.image,
|
|
5377
5723
|
...a.imageRef !== void 0 ? { imageRef: a.imageRef } : {},
|
|
@@ -5393,7 +5739,7 @@ async function main(argv) {
|
|
|
5393
5739
|
return 0;
|
|
5394
5740
|
}
|
|
5395
5741
|
return runShell(
|
|
5396
|
-
parsed.args.name === void 0 ? {} : { name: parsed.args.name },
|
|
5742
|
+
parsed.args.name === void 0 ? { bin } : { bin, name: parsed.args.name },
|
|
5397
5743
|
lifecycleIo()
|
|
5398
5744
|
);
|
|
5399
5745
|
}
|
|
@@ -5410,6 +5756,7 @@ async function main(argv) {
|
|
|
5410
5756
|
}
|
|
5411
5757
|
return await runDestroy(
|
|
5412
5758
|
{
|
|
5759
|
+
bin,
|
|
5413
5760
|
yes: parsed.args.yes,
|
|
5414
5761
|
...parsed.args.name !== void 0 ? { name: parsed.args.name } : {}
|
|
5415
5762
|
},
|
|
@@ -5428,7 +5775,7 @@ async function main(argv) {
|
|
|
5428
5775
|
printComposeHelp(bin);
|
|
5429
5776
|
return 0;
|
|
5430
5777
|
}
|
|
5431
|
-
return runComposeConfig({}, parsed.args.passthrough);
|
|
5778
|
+
return runComposeConfig({ bin }, parsed.args.passthrough);
|
|
5432
5779
|
}
|
|
5433
5780
|
if (composeVerbs().includes(first)) {
|
|
5434
5781
|
const verb = first;
|
|
@@ -5437,9 +5784,9 @@ async function main(argv) {
|
|
|
5437
5784
|
printComposeVerbHelp(verb, bin);
|
|
5438
5785
|
return 0;
|
|
5439
5786
|
}
|
|
5440
|
-
const
|
|
5441
|
-
const head =
|
|
5442
|
-
const extra =
|
|
5787
|
+
const sep5 = rest.indexOf("--");
|
|
5788
|
+
const head = sep5 === -1 ? rest : rest.slice(0, sep5);
|
|
5789
|
+
const extra = sep5 === -1 ? [] : rest.slice(sep5 + 1);
|
|
5443
5790
|
if (head.length > 0 && head[0].startsWith("-")) {
|
|
5444
5791
|
process.stderr.write(`${bin} ${verb}: unknown flag "${head[0]}"
|
|
5445
5792
|
`);
|
|
@@ -5452,7 +5799,7 @@ async function main(argv) {
|
|
|
5452
5799
|
);
|
|
5453
5800
|
return 1;
|
|
5454
5801
|
}
|
|
5455
|
-
return runComposeVerb(verb, head.length === 0 ? {} : { name: head[0] }, extra);
|
|
5802
|
+
return runComposeVerb(verb, head.length === 0 ? { bin } : { bin, name: head[0] }, extra);
|
|
5456
5803
|
}
|
|
5457
5804
|
if (first === "harness") {
|
|
5458
5805
|
const parsed = parseHarnessArgs(argv.slice(1), bin);
|
|
@@ -5472,12 +5819,12 @@ async function main(argv) {
|
|
|
5472
5819
|
stderr: (s) => process.stderr.write(s)
|
|
5473
5820
|
};
|
|
5474
5821
|
if (a.subcommand === "list") {
|
|
5475
|
-
return await runHarnessList({ json: a.json }, io);
|
|
5822
|
+
return await runHarnessList({ bin, json: a.json }, io);
|
|
5476
5823
|
}
|
|
5477
5824
|
if (a.subcommand === "status") {
|
|
5478
|
-
return await runHarnessStatus(a.name, { json: a.json }, io);
|
|
5825
|
+
return await runHarnessStatus(a.name, { bin, json: a.json }, io);
|
|
5479
5826
|
}
|
|
5480
|
-
return await runHarnessInstall(a.name, {}, io);
|
|
5827
|
+
return await runHarnessInstall(a.name, { bin }, io);
|
|
5481
5828
|
}
|
|
5482
5829
|
if (first === "tool") {
|
|
5483
5830
|
const parsed = parseToolArgs(argv.slice(1), bin);
|
|
@@ -5497,18 +5844,12 @@ async function main(argv) {
|
|
|
5497
5844
|
stderr: (s) => process.stderr.write(s)
|
|
5498
5845
|
};
|
|
5499
5846
|
if (a.subcommand === "list") {
|
|
5500
|
-
return await runToolList({ json: a.json }, io);
|
|
5847
|
+
return await runToolList({ bin, json: a.json }, io);
|
|
5501
5848
|
}
|
|
5502
5849
|
if (a.subcommand === "status") {
|
|
5503
|
-
return await runToolStatus(a.name, { json: a.json }, io);
|
|
5850
|
+
return await runToolStatus(a.name, { bin, json: a.json }, io);
|
|
5504
5851
|
}
|
|
5505
|
-
return await runToolInstall(a.name, { yes: a.yes }, io);
|
|
5506
|
-
}
|
|
5507
|
-
if (first === "cloud") {
|
|
5508
|
-
return await runCloud(argv.slice(1), {
|
|
5509
|
-
stdout: (s) => process.stdout.write(s),
|
|
5510
|
-
stderr: (s) => process.stderr.write(s)
|
|
5511
|
-
});
|
|
5852
|
+
return await runToolInstall(a.name, { bin, yes: a.yes }, io);
|
|
5512
5853
|
}
|
|
5513
5854
|
if (first === "gateway") {
|
|
5514
5855
|
const parsed = parseGatewayArgs(argv.slice(1));
|
|
@@ -5521,7 +5862,7 @@ async function main(argv) {
|
|
|
5521
5862
|
printGatewayHelp(bin);
|
|
5522
5863
|
return 0;
|
|
5523
5864
|
}
|
|
5524
|
-
return runGateway(parsed.args.passthrough, {});
|
|
5865
|
+
return runGateway(parsed.args.passthrough, { bin });
|
|
5525
5866
|
}
|
|
5526
5867
|
process.stderr.write(`${bin}: unknown command "${first}"
|
|
5527
5868
|
|