@mifune/agro 0.10.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/README.md +11 -37
- package/dist/agro.js +1292 -1609
- package/dist/oh.js +1292 -1609
- package/package.json +2 -2
package/dist/agro.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { dirname as dirname8, join as
|
|
6
|
-
import { existsSync as
|
|
5
|
+
import { dirname as dirname8, join as join10, resolve as resolve10 } from "node:path";
|
|
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
|
|
@@ -315,17 +315,53 @@ import {
|
|
|
315
315
|
copyFileSync
|
|
316
316
|
} from "node:fs";
|
|
317
317
|
import path2 from "node:path";
|
|
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
|
|
318
354
|
function assertDestInTarget(dest, targetOh, sep5) {
|
|
319
355
|
if (dest === targetOh || dest.startsWith(targetOh + sep5)) {
|
|
320
356
|
return;
|
|
321
357
|
}
|
|
322
|
-
throw new Error(
|
|
358
|
+
throw new Error(`${activeBin()}: refusing to write outside target .oh: ` + dest);
|
|
323
359
|
}
|
|
324
360
|
function assertDestInRoot(dest, targetRoot, sep5) {
|
|
325
361
|
if (dest === targetRoot || dest.startsWith(targetRoot + sep5)) {
|
|
326
362
|
return;
|
|
327
363
|
}
|
|
328
|
-
throw new Error(
|
|
364
|
+
throw new Error(`${activeBin()}: refusing to write outside target root: ` + dest);
|
|
329
365
|
}
|
|
330
366
|
function walkFiles(root, dir, acc) {
|
|
331
367
|
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
@@ -467,12 +503,12 @@ function readCliVersion(ohDir) {
|
|
|
467
503
|
return "0.0.0";
|
|
468
504
|
}
|
|
469
505
|
async function runUpdate(opts, io) {
|
|
470
|
-
const { targetDir, fromDir, force, dryRun } = opts;
|
|
506
|
+
const { bin, targetDir, fromDir, force, dryRun } = opts;
|
|
471
507
|
const dryPrefix = dryRun ? "[dry-run] " : "";
|
|
472
508
|
const source = resolveControlDir(fromDir);
|
|
473
509
|
if (source.kind === "absent") {
|
|
474
510
|
io.stderr(
|
|
475
|
-
"
|
|
511
|
+
bin + " update: update source not found at " + source.agroPath + " or " + source.legacyPath + ". Pass --from <built-AGRO-checkout> or --from-remote [--ref <ref>].\n"
|
|
476
512
|
);
|
|
477
513
|
return 1;
|
|
478
514
|
}
|
|
@@ -482,7 +518,7 @@ async function runUpdate(opts, io) {
|
|
|
482
518
|
const targetOh = target.kind === "absent" ? path3.resolve(targetDir, controlName) : target.path;
|
|
483
519
|
const targetName = path3.basename(targetOh);
|
|
484
520
|
if (fromOh === targetOh) {
|
|
485
|
-
io.stderr("
|
|
521
|
+
io.stderr(bin + " update: source and target are the same " + targetName + "; nothing to update.\n");
|
|
486
522
|
return 1;
|
|
487
523
|
}
|
|
488
524
|
const available = readCliVersion(fromOh);
|
|
@@ -492,25 +528,25 @@ async function runUpdate(opts, io) {
|
|
|
492
528
|
io.stdout(dryPrefix + "updating " + targetName + ": " + current + " -> " + available + "\n");
|
|
493
529
|
} else if (cmp === 0) {
|
|
494
530
|
if (!force) {
|
|
495
|
-
io.stdout(dryPrefix + "
|
|
531
|
+
io.stdout(dryPrefix + bin + " update: already up to date (v" + current + ")\n");
|
|
496
532
|
return 0;
|
|
497
533
|
}
|
|
498
|
-
io.stdout(dryPrefix + "
|
|
534
|
+
io.stdout(dryPrefix + bin + " update: re-overlay (v" + current + ", --force)\n");
|
|
499
535
|
} else {
|
|
500
536
|
if (!force) {
|
|
501
537
|
io.stderr(
|
|
502
|
-
"
|
|
538
|
+
bin + " update: refusing downgrade (current v" + current + " > source v" + available + "); pass --force to override\n"
|
|
503
539
|
);
|
|
504
540
|
return 1;
|
|
505
541
|
}
|
|
506
542
|
io.stdout(
|
|
507
|
-
dryPrefix + "
|
|
543
|
+
dryPrefix + bin + " update: downgrading " + targetName + ": " + current + " -> " + available + " (--force)\n"
|
|
508
544
|
);
|
|
509
545
|
}
|
|
510
546
|
const manifest = loadManifest(fromOh);
|
|
511
547
|
if (manifest === null) {
|
|
512
548
|
io.stdout(
|
|
513
|
-
dryPrefix + "
|
|
549
|
+
dryPrefix + bin + " update: no " + controlName + "/manifest.json in source; overlaying all of " + controlName + "/ (legacy mode)\n"
|
|
514
550
|
);
|
|
515
551
|
}
|
|
516
552
|
let created = 0;
|
|
@@ -548,12 +584,12 @@ async function runUpdate(opts, io) {
|
|
|
548
584
|
const stranded = path3.join(targetOh, dir);
|
|
549
585
|
if (existsSync3(stranded)) {
|
|
550
586
|
io.stdout(
|
|
551
|
-
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"
|
|
552
588
|
);
|
|
553
589
|
}
|
|
554
590
|
}
|
|
555
591
|
io.stdout(
|
|
556
|
-
dryPrefix + "
|
|
592
|
+
dryPrefix + bin + " update: " + created + " created, " + overwritten + " overwritten, " + (skipped + rootResult.skipped) + " skipped\n"
|
|
557
593
|
);
|
|
558
594
|
return 0;
|
|
559
595
|
}
|
|
@@ -571,39 +607,6 @@ import {
|
|
|
571
607
|
writeFileSync
|
|
572
608
|
} from "node:fs";
|
|
573
609
|
import { basename as basename2, delimiter, dirname, extname, join as join2 } from "node:path";
|
|
574
|
-
|
|
575
|
-
// src/lib/product.ts
|
|
576
|
-
import { basename } from "node:path";
|
|
577
|
-
var AGRO_PRODUCT = {
|
|
578
|
-
name: "agro",
|
|
579
|
-
bin: "agro",
|
|
580
|
-
title: "AGRO CLI",
|
|
581
|
-
packageName: "@mifune/agro",
|
|
582
|
-
generation: "agro"
|
|
583
|
-
};
|
|
584
|
-
var LEGACY_PRODUCT = {
|
|
585
|
-
name: "oh",
|
|
586
|
-
bin: "oh",
|
|
587
|
-
title: "Open Harness CLI",
|
|
588
|
-
packageName: "@mifune/openharness",
|
|
589
|
-
generation: "legacy"
|
|
590
|
-
};
|
|
591
|
-
var FILE_EXTENSION = /\.[^.]*$/;
|
|
592
|
-
function invokedName(argv1) {
|
|
593
|
-
if (argv1 === void 0) return "";
|
|
594
|
-
return basename(argv1.replace(/\\/g, "/")).replace(FILE_EXTENSION, "");
|
|
595
|
-
}
|
|
596
|
-
function resolveProduct(argv1) {
|
|
597
|
-
return invokedName(argv1) === LEGACY_PRODUCT.bin ? LEGACY_PRODUCT : AGRO_PRODUCT;
|
|
598
|
-
}
|
|
599
|
-
function productFor(bin) {
|
|
600
|
-
return bin === LEGACY_PRODUCT.bin ? LEGACY_PRODUCT : AGRO_PRODUCT;
|
|
601
|
-
}
|
|
602
|
-
function stateNames(bin) {
|
|
603
|
-
return GENERATIONS[productFor(bin).generation];
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// src/commands/self-upgrade.ts
|
|
607
610
|
var DEFAULT_ARTIFACT_URL = "https://github.com/mifunedev/agro/releases/latest/download/agro.js";
|
|
608
611
|
var PREFIX = `${AGRO_PRODUCT.bin} update`;
|
|
609
612
|
var PACKAGE = AGRO_PRODUCT.packageName;
|
|
@@ -701,13 +704,13 @@ function compareSemver(a, b) {
|
|
|
701
704
|
function versionOf(result) {
|
|
702
705
|
return result.status === 0 ? result.stdout.trim() : "";
|
|
703
706
|
}
|
|
704
|
-
function refuseUnsupported(installation) {
|
|
707
|
+
function refuseUnsupported(installation, bin) {
|
|
705
708
|
const { kind, target, reason } = installation;
|
|
706
709
|
const at = `${kind} installation at ${target}`;
|
|
707
710
|
switch (kind) {
|
|
708
711
|
case "image":
|
|
709
712
|
return new Error(
|
|
710
|
-
`${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>)`
|
|
711
714
|
);
|
|
712
715
|
case "source":
|
|
713
716
|
return new Error(`${at} \u2014 rebuild from the checkout: npm --prefix .agro/cli run build`);
|
|
@@ -890,7 +893,7 @@ async function runSelfUpgrade(opts, deps, io) {
|
|
|
890
893
|
try {
|
|
891
894
|
const installation = classifyInstallation(opts.argv1, deps);
|
|
892
895
|
if (installation.kind !== "npm" && installation.kind !== "standalone") {
|
|
893
|
-
throw refuseUnsupported(installation);
|
|
896
|
+
throw refuseUnsupported(installation, resolveProduct(opts.argv1).bin);
|
|
894
897
|
}
|
|
895
898
|
io.stdout(`${PREFIX}: ${installation.kind} installation at ${installation.target}
|
|
896
899
|
`);
|
|
@@ -1505,212 +1508,64 @@ function runMigrate(args, io, deps = defaultMigrateDeps()) {
|
|
|
1505
1508
|
}
|
|
1506
1509
|
}
|
|
1507
1510
|
|
|
1508
|
-
// src/
|
|
1509
|
-
import {
|
|
1510
|
-
import { existsSync as
|
|
1511
|
-
import { join as join4 } from "node:path";
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
bold: "\x1B[1m",
|
|
1518
|
-
dim: "\x1B[2m",
|
|
1519
|
-
green: "\x1B[32m",
|
|
1520
|
-
yellow: "\x1B[33m",
|
|
1521
|
-
red: "\x1B[31m",
|
|
1522
|
-
cyan: "\x1B[36m"
|
|
1523
|
-
};
|
|
1524
|
-
function header(msg) {
|
|
1525
|
-
process.stdout.write(`
|
|
1526
|
-
${COLORS.bold}${msg}${COLORS.reset}
|
|
1527
|
-
|
|
1528
|
-
`);
|
|
1529
|
-
}
|
|
1530
|
-
function bold(s) {
|
|
1531
|
-
return `${COLORS.bold}${s}${COLORS.reset}`;
|
|
1532
|
-
}
|
|
1533
|
-
function redact(token) {
|
|
1534
|
-
if (token.length <= 8) return "****";
|
|
1535
|
-
return token.slice(0, 5) + "*".repeat(token.length - 5);
|
|
1536
|
-
}
|
|
1537
|
-
async function ask(question) {
|
|
1538
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1539
|
-
try {
|
|
1540
|
-
return await new Promise((resolve11) => {
|
|
1541
|
-
rl.question(` ${question} `, (answer) => resolve11(answer.trim()));
|
|
1542
|
-
});
|
|
1543
|
-
} finally {
|
|
1544
|
-
rl.close();
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
async function askSecret(question) {
|
|
1548
|
-
if (!process.stdin.isTTY) return askSecretPiped(question);
|
|
1549
|
-
process.stdout.write(` ${question} `);
|
|
1550
|
-
process.stdin.setRawMode(true);
|
|
1551
|
-
process.stdin.resume();
|
|
1552
|
-
const bytes = [];
|
|
1553
|
-
return await new Promise((resolve11) => {
|
|
1554
|
-
const cleanup = () => {
|
|
1555
|
-
try {
|
|
1556
|
-
process.stdin.setRawMode(false);
|
|
1557
|
-
} catch {
|
|
1558
|
-
}
|
|
1559
|
-
process.stdin.pause();
|
|
1560
|
-
process.stdin.removeListener("data", onData);
|
|
1561
|
-
};
|
|
1562
|
-
const onData = (data) => {
|
|
1563
|
-
for (const byte of data) {
|
|
1564
|
-
if (byte === 3) {
|
|
1565
|
-
cleanup();
|
|
1566
|
-
process.stdout.write("\n");
|
|
1567
|
-
process.exit(130);
|
|
1568
|
-
} else if (byte === 13 || byte === 10) {
|
|
1569
|
-
cleanup();
|
|
1570
|
-
process.stdout.write("\n");
|
|
1571
|
-
resolve11(Buffer.from(bytes).toString("utf8").trim());
|
|
1572
|
-
return;
|
|
1573
|
-
} else if (byte === 127 || byte === 8) {
|
|
1574
|
-
if (bytes.length > 0) {
|
|
1575
|
-
bytes.pop();
|
|
1576
|
-
process.stdout.write("\b \b");
|
|
1577
|
-
}
|
|
1578
|
-
} else if (byte === 21) {
|
|
1579
|
-
while (bytes.length > 0) {
|
|
1580
|
-
bytes.pop();
|
|
1581
|
-
process.stdout.write("\b \b");
|
|
1582
|
-
}
|
|
1583
|
-
} else if (byte >= 32) {
|
|
1584
|
-
bytes.push(byte);
|
|
1585
|
-
process.stdout.write("\u25CF");
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
};
|
|
1589
|
-
process.stdin.on("data", onData);
|
|
1590
|
-
});
|
|
1591
|
-
}
|
|
1592
|
-
async function askSecretPiped(question) {
|
|
1593
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1594
|
-
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
1595
|
-
let prompted = false;
|
|
1596
|
-
const patched = (chunk) => {
|
|
1597
|
-
if (!prompted) {
|
|
1598
|
-
prompted = true;
|
|
1599
|
-
return stdoutWrite(chunk);
|
|
1600
|
-
}
|
|
1601
|
-
return true;
|
|
1602
|
-
};
|
|
1603
|
-
process.stdout.write = patched;
|
|
1604
|
-
let restored = false;
|
|
1605
|
-
const restore = () => {
|
|
1606
|
-
if (restored) return;
|
|
1607
|
-
restored = true;
|
|
1608
|
-
process.stdout.write = stdoutWrite;
|
|
1609
|
-
try {
|
|
1610
|
-
rl.close();
|
|
1611
|
-
} catch {
|
|
1612
|
-
}
|
|
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 } : {}
|
|
1613
1520
|
};
|
|
1614
|
-
const
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
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;
|
|
1523
|
+
return {
|
|
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
|
|
1618
1528
|
};
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
});
|
|
1627
|
-
} finally {
|
|
1628
|
-
process.removeListener("SIGINT", onSigint);
|
|
1629
|
-
restore();
|
|
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;
|
|
1630
1536
|
}
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
// src/lib/oh-config.ts
|
|
1640
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1641
|
-
import { basename as basename3, dirname as dirname4, resolve as resolve5 } from "node:path";
|
|
1642
|
-
|
|
1643
|
-
// src/lib/env-file.ts
|
|
1644
|
-
import { existsSync as existsSync5 } from "node:fs";
|
|
1645
|
-
import { resolve as resolve4, sep as sep2 } from "node:path";
|
|
1646
|
-
function assertInRoot(dest, root) {
|
|
1647
|
-
if (!(dest === root || dest.startsWith(root + sep2))) {
|
|
1648
|
-
throw new Error(`refusing to write outside the project root: ${dest}`);
|
|
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;
|
|
1649
1544
|
}
|
|
1650
|
-
}
|
|
1651
|
-
function
|
|
1652
|
-
if (
|
|
1653
|
-
|
|
1545
|
+
};
|
|
1546
|
+
function assertSpawned(r, what) {
|
|
1547
|
+
if (r.error) {
|
|
1548
|
+
throw new ExecutionSpawnError(what, r.error);
|
|
1654
1549
|
}
|
|
1655
|
-
return s;
|
|
1656
1550
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
const path4 = CONFIG_FIELD_BY_ENV_KEY[key];
|
|
1662
|
-
if (path4 === void 0) {
|
|
1551
|
+
function requireLifecycleScript(root, rel) {
|
|
1552
|
+
const { controlDir } = resolveProjectLayout(root);
|
|
1553
|
+
const script = join4(controlDir, "scripts", rel);
|
|
1554
|
+
if (!existsSync5(script)) {
|
|
1663
1555
|
throw new Error(
|
|
1664
|
-
|
|
1556
|
+
`missing lifecycle script ${script} \u2014 the vendored ${basename3(controlDir)}/ payload looks incomplete; run \`${activeBin()} update\` to re-vendor it`
|
|
1665
1557
|
);
|
|
1666
1558
|
}
|
|
1667
|
-
return
|
|
1668
|
-
}
|
|
1669
|
-
function setConfigField(root, path4, value) {
|
|
1670
|
-
const file = ohConfigPath(root);
|
|
1671
|
-
assertInRoot(file, resolve4(root));
|
|
1672
|
-
const config = readOhConfig(file);
|
|
1673
|
-
const before = getOhConfigValue(config, path4);
|
|
1674
|
-
const next = setOhConfigValue(config, path4, value);
|
|
1675
|
-
const after = getOhConfigValue(next, path4);
|
|
1676
|
-
if (existsSync5(file) && JSON.stringify(before) === JSON.stringify(after)) {
|
|
1677
|
-
return "already-set";
|
|
1678
|
-
}
|
|
1679
|
-
writeOhConfig(root, next);
|
|
1680
|
-
return before === void 0 ? "added" : "updated";
|
|
1681
|
-
}
|
|
1682
|
-
function setKeyInEnv(content, key, value) {
|
|
1683
|
-
const lines = content.split("\n");
|
|
1684
|
-
const live = new RegExp(`^${escapeRegExp(key)}=(.*)$`);
|
|
1685
|
-
const commented = new RegExp(`^[ \\t]*#[ \\t]*${escapeRegExp(key)}=(.*)$`);
|
|
1686
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1687
|
-
const m = live.exec(lines[i]);
|
|
1688
|
-
if (m) {
|
|
1689
|
-
if (stripQuotes(m[1].trim()) === value) return { content, outcome: "already-set" };
|
|
1690
|
-
lines[i] = `${key}=${value}`;
|
|
1691
|
-
return { content: lines.join("\n"), outcome: "updated" };
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1695
|
-
if (commented.test(lines[i])) {
|
|
1696
|
-
lines[i] = `${key}=${value}`;
|
|
1697
|
-
return { content: lines.join("\n"), outcome: "uncommented" };
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
const body = content.replace(/\n+$/, "");
|
|
1701
|
-
return {
|
|
1702
|
-
content: body === "" ? `${key}=${value}
|
|
1703
|
-
` : `${body}
|
|
1704
|
-
${key}=${value}
|
|
1705
|
-
`,
|
|
1706
|
-
outcome: "added"
|
|
1707
|
-
};
|
|
1708
|
-
}
|
|
1709
|
-
function escapeRegExp(s) {
|
|
1710
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1559
|
+
return script;
|
|
1711
1560
|
}
|
|
1712
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
|
+
|
|
1713
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";
|
|
1714
1569
|
var OH_CONFIG_FILE = GENERATIONS.legacy.configFile;
|
|
1715
1570
|
var OH_CONFIG_MODE = 420;
|
|
1716
1571
|
var RESERVED_HOME_PATHS = [
|
|
@@ -1739,6 +1594,10 @@ var LANGFUSE_PRIVACY_PRESETS = [
|
|
|
1739
1594
|
"full-debug"
|
|
1740
1595
|
];
|
|
1741
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
|
+
}
|
|
1742
1601
|
function ohConfigPath(root) {
|
|
1743
1602
|
return resolveProjectLayout(root).configFile;
|
|
1744
1603
|
}
|
|
@@ -1759,13 +1618,12 @@ function defaultOhConfig(name) {
|
|
|
1759
1618
|
cron: { agentBin: "claude" },
|
|
1760
1619
|
build: { skipPnpmInstall: false },
|
|
1761
1620
|
image: { mode: "build", pullPolicy: "missing" },
|
|
1762
|
-
cloud: {},
|
|
1763
1621
|
langfuse: {},
|
|
1764
1622
|
composeOverrides: []
|
|
1765
1623
|
};
|
|
1766
1624
|
}
|
|
1767
1625
|
function readOhConfig(path4) {
|
|
1768
|
-
if (!existsSync6(path4)) return defaultOhConfig(
|
|
1626
|
+
if (!existsSync6(path4)) return defaultOhConfig(basename4(dirname4(resolve4(path4))));
|
|
1769
1627
|
let raw;
|
|
1770
1628
|
try {
|
|
1771
1629
|
raw = readFileSync6(path4, "utf8");
|
|
@@ -1777,13 +1635,13 @@ function readOhConfig(path4) {
|
|
|
1777
1635
|
try {
|
|
1778
1636
|
parsed = JSON.parse(raw);
|
|
1779
1637
|
} catch {
|
|
1780
|
-
throw new Error(`${
|
|
1638
|
+
throw new Error(`${basename4(path4)} is not valid JSON: ${path4}`);
|
|
1781
1639
|
}
|
|
1782
1640
|
return validateOhConfig(parsed);
|
|
1783
1641
|
}
|
|
1784
1642
|
function writeOhConfig(root, config) {
|
|
1785
1643
|
const path4 = ohConfigPath(root);
|
|
1786
|
-
assertInRoot(path4,
|
|
1644
|
+
assertInRoot(path4, resolve4(root));
|
|
1787
1645
|
const validated = validateOhConfig({ ...config, version: 1 });
|
|
1788
1646
|
const body = `${JSON.stringify(validated, null, 2)}
|
|
1789
1647
|
`;
|
|
@@ -1808,6 +1666,7 @@ function validateOhConfig(value) {
|
|
|
1808
1666
|
expectString(record, "name");
|
|
1809
1667
|
expectEnum(record, "runtime", "", SANDBOX_RUNTIMES);
|
|
1810
1668
|
expectString(record, "repo");
|
|
1669
|
+
expectString(record, "checkout");
|
|
1811
1670
|
expectString(record, "timezone");
|
|
1812
1671
|
const storage = expectSection(record, "storage");
|
|
1813
1672
|
if (storage) {
|
|
@@ -1854,8 +1713,6 @@ function validateOhConfig(value) {
|
|
|
1854
1713
|
expectEnum(image, "mode", "image.", ["build", "image"]);
|
|
1855
1714
|
expectEnum(image, "pullPolicy", "image.", ["missing", "always", "never"]);
|
|
1856
1715
|
}
|
|
1857
|
-
const cloud = expectSection(record, "cloud");
|
|
1858
|
-
if (cloud) expectString(cloud, "apiUrl", "cloud.");
|
|
1859
1716
|
const langfuse = expectSection(record, "langfuse");
|
|
1860
1717
|
if (langfuse) {
|
|
1861
1718
|
expectString(langfuse, "baseUrl", "langfuse.");
|
|
@@ -1916,6 +1773,7 @@ var OH_CONFIG_FIELDS = [
|
|
|
1916
1773
|
{ path: "name", type: "string" },
|
|
1917
1774
|
{ path: "runtime", type: "enum", values: SANDBOX_RUNTIMES },
|
|
1918
1775
|
{ path: "repo", type: "string" },
|
|
1776
|
+
{ path: "checkout", type: "string" },
|
|
1919
1777
|
{ path: "timezone", type: "string" },
|
|
1920
1778
|
{ path: "git.userName", type: "string" },
|
|
1921
1779
|
{ path: "git.userEmail", type: "string" },
|
|
@@ -1932,7 +1790,6 @@ var OH_CONFIG_FIELDS = [
|
|
|
1932
1790
|
{ path: "image.mode", type: "enum", values: ["build", "image"] },
|
|
1933
1791
|
{ path: "image.pullPolicy", type: "enum", values: ["missing", "always", "never"] },
|
|
1934
1792
|
{ path: "storage.homePath", type: "string" },
|
|
1935
|
-
{ path: "cloud.apiUrl", type: "string" },
|
|
1936
1793
|
{ path: "langfuse.baseUrl", type: "string" },
|
|
1937
1794
|
{ path: "langfuse.privacyPreset", type: "enum", values: LANGFUSE_PRIVACY_PRESETS },
|
|
1938
1795
|
{ path: "composeOverrides", type: "list" }
|
|
@@ -1993,6 +1850,74 @@ function coerceFieldValue(field, raw) {
|
|
|
1993
1850
|
return raw;
|
|
1994
1851
|
}
|
|
1995
1852
|
|
|
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}`);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
function stripQuotes(s) {
|
|
1860
|
+
if (s.length >= 2 && (s[0] === '"' && s.endsWith('"') || s[0] === "'" && s.endsWith("'"))) {
|
|
1861
|
+
return s.slice(1, -1);
|
|
1862
|
+
}
|
|
1863
|
+
return s;
|
|
1864
|
+
}
|
|
1865
|
+
var CONFIG_FIELD_BY_ENV_KEY = {
|
|
1866
|
+
DOCKER_SOCKET: "access.dockerSocket"
|
|
1867
|
+
};
|
|
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`
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
return setConfigField(root, path4, value);
|
|
1876
|
+
}
|
|
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";
|
|
1886
|
+
}
|
|
1887
|
+
writeOhConfig(root, next);
|
|
1888
|
+
return before === void 0 ? "added" : "updated";
|
|
1889
|
+
}
|
|
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" };
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
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" };
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
const body = content.replace(/\n+$/, "");
|
|
1909
|
+
return {
|
|
1910
|
+
content: body === "" ? `${key}=${value}
|
|
1911
|
+
` : `${body}
|
|
1912
|
+
${key}=${value}
|
|
1913
|
+
`,
|
|
1914
|
+
outcome: "added"
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
function escapeRegExp(s) {
|
|
1918
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1996
1921
|
// src/lib/project.ts
|
|
1997
1922
|
import { dirname as dirname5, resolve as resolve6 } from "node:path";
|
|
1998
1923
|
function resolveProjectRoot(startDir = process.cwd()) {
|
|
@@ -2001,628 +1926,141 @@ function resolveProjectRoot(startDir = process.cwd()) {
|
|
|
2001
1926
|
if (resolveControlDir(dir).kind !== "absent") return dir;
|
|
2002
1927
|
const parent = dirname5(dir);
|
|
2003
1928
|
if (parent === dir) {
|
|
2004
|
-
throw new Error(
|
|
1929
|
+
throw new Error(`not an OpenHarness-equipped repo \u2014 run \`${activeBin()} update\` first`);
|
|
2005
1930
|
}
|
|
2006
1931
|
dir = parent;
|
|
2007
1932
|
}
|
|
2008
1933
|
}
|
|
2009
1934
|
|
|
2010
|
-
// src/lib/
|
|
2011
|
-
import {
|
|
2012
|
-
|
|
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}
|
|
2013
1949
|
|
|
2014
|
-
|
|
2015
|
-
import {
|
|
2016
|
-
readFileSync as readFileSync7,
|
|
2017
|
-
writeFileSync as writeFileSync4,
|
|
2018
|
-
existsSync as existsSync7,
|
|
2019
|
-
fsyncSync as fsyncSync2,
|
|
2020
|
-
renameSync as renameSync4,
|
|
2021
|
-
unlinkSync as unlinkSync4,
|
|
2022
|
-
openSync as openSync2,
|
|
2023
|
-
closeSync as closeSync2
|
|
2024
|
-
} from "node:fs";
|
|
2025
|
-
function loadEnvInto(envPath, env) {
|
|
2026
|
-
if (!existsSync7(envPath)) return;
|
|
2027
|
-
const content = readFileSync7(envPath, "utf8");
|
|
2028
|
-
for (const rawLine of content.split("\n")) {
|
|
2029
|
-
const line = rawLine.trimEnd();
|
|
2030
|
-
if (line === "" || /^\s*#/.test(line)) continue;
|
|
2031
|
-
const eqIdx = line.indexOf("=");
|
|
2032
|
-
if (eqIdx < 1) continue;
|
|
2033
|
-
const key = line.slice(0, eqIdx).trim();
|
|
2034
|
-
const value = line.slice(eqIdx + 1);
|
|
2035
|
-
if (key === "") continue;
|
|
2036
|
-
if (env[key] === void 0) env[key] = value;
|
|
2037
|
-
}
|
|
1950
|
+
`);
|
|
2038
1951
|
}
|
|
2039
|
-
function
|
|
2040
|
-
|
|
2041
|
-
|
|
1952
|
+
function bold(s) {
|
|
1953
|
+
return `${COLORS.bold}${s}${COLORS.reset}`;
|
|
1954
|
+
}
|
|
1955
|
+
function redact(token) {
|
|
1956
|
+
if (token.length <= 8) return "****";
|
|
1957
|
+
return token.slice(0, 5) + "*".repeat(token.length - 5);
|
|
1958
|
+
}
|
|
1959
|
+
async function ask(question) {
|
|
1960
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2042
1961
|
try {
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
1962
|
+
return await new Promise((resolve11) => {
|
|
1963
|
+
rl.question(` ${question} `, (answer) => resolve11(answer.trim()));
|
|
1964
|
+
});
|
|
1965
|
+
} finally {
|
|
1966
|
+
rl.close();
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
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 = () => {
|
|
2051
1977
|
try {
|
|
2052
|
-
|
|
1978
|
+
process.stdin.setRawMode(false);
|
|
2053
1979
|
} catch {
|
|
2054
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
|
+
});
|
|
2013
|
+
}
|
|
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);
|
|
2055
2022
|
}
|
|
2023
|
+
return true;
|
|
2024
|
+
};
|
|
2025
|
+
process.stdout.write = patched;
|
|
2026
|
+
let restored = false;
|
|
2027
|
+
const restore = () => {
|
|
2028
|
+
if (restored) return;
|
|
2029
|
+
restored = true;
|
|
2030
|
+
process.stdout.write = stdoutWrite;
|
|
2056
2031
|
try {
|
|
2057
|
-
|
|
2032
|
+
rl.close();
|
|
2058
2033
|
} catch {
|
|
2059
2034
|
}
|
|
2060
|
-
|
|
2035
|
+
};
|
|
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();
|
|
2061
2052
|
}
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
"XAI_API_KEY",
|
|
2069
|
-
"META_API_KEY",
|
|
2070
|
-
"SANDBOX_PASSWORD",
|
|
2071
|
-
"PI_SLACK_APP_TOKEN",
|
|
2072
|
-
"PI_SLACK_BOT_TOKEN",
|
|
2073
|
-
"LANGFUSE_PUBLIC_KEY",
|
|
2074
|
-
"LANGFUSE_SECRET_KEY",
|
|
2075
|
-
"OH_CLOUD_PROVISION_KEY"
|
|
2076
|
-
];
|
|
2077
|
-
var ALLOWED = new Set(SECRET_KEYS);
|
|
2078
|
-
function secretsFilePath(root) {
|
|
2079
|
-
return resolve7(root, SECRETS_FILE);
|
|
2080
|
-
}
|
|
2081
|
-
function isSecretKey(key) {
|
|
2082
|
-
return ALLOWED.has(key);
|
|
2083
|
-
}
|
|
2084
|
-
function assertSecretKey(key) {
|
|
2085
|
-
if (ALLOWED.has(key)) return;
|
|
2086
|
-
throw new Error(
|
|
2087
|
-
`${key} is not a secret \u2014 non-secret settings live in oh.json; use \`oh config set\` instead`
|
|
2088
|
-
);
|
|
2089
|
-
}
|
|
2090
|
-
function readSecret(root, key) {
|
|
2091
|
-
assertSecretKey(key);
|
|
2092
|
-
const file = secretsFilePath(root);
|
|
2093
|
-
if (!existsSync8(file)) return void 0;
|
|
2094
|
-
const env = {};
|
|
2095
|
-
loadEnvInto(file, env);
|
|
2096
|
-
const value = env[key]?.trim();
|
|
2097
|
-
return value === void 0 || value === "" ? void 0 : stripQuotes2(value);
|
|
2098
|
-
}
|
|
2099
|
-
function setSecret(root, key, value) {
|
|
2100
|
-
assertSecretKey(key);
|
|
2101
|
-
if (/[\r\n]/.test(value)) {
|
|
2102
|
-
throw new Error(`${key} value must not contain a newline`);
|
|
2103
|
-
}
|
|
2104
|
-
const file = secretsFilePath(root);
|
|
2105
|
-
assertInRoot(file, resolve7(root));
|
|
2106
|
-
const original = existsSync8(file) ? readFileSync8(file, "utf8") : "";
|
|
2107
|
-
const { content } = setKeyInEnv(original, key, value);
|
|
2108
|
-
writeEnvFile(file, content);
|
|
2109
|
-
}
|
|
2110
|
-
function listSecretKeys(root) {
|
|
2111
|
-
const file = secretsFilePath(root);
|
|
2112
|
-
if (!existsSync8(file)) return [];
|
|
2113
|
-
const env = {};
|
|
2114
|
-
loadEnvInto(file, env);
|
|
2115
|
-
return SECRET_KEYS.filter((key) => {
|
|
2116
|
-
const value = env[key]?.trim();
|
|
2117
|
-
return value !== void 0 && value !== "";
|
|
2118
|
-
});
|
|
2119
|
-
}
|
|
2120
|
-
function stripQuotes2(s) {
|
|
2121
|
-
if (s.length >= 2 && (s[0] === '"' && s.endsWith('"') || s[0] === "'" && s.endsWith("'"))) {
|
|
2122
|
-
return s.slice(1, -1);
|
|
2123
|
-
}
|
|
2124
|
-
return s;
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
// src/commands/cloud.ts
|
|
2128
|
-
var DEFAULT_API_URL = "http://127.0.0.1:3000";
|
|
2129
|
-
var DEFAULT_TERMINAL_STATUSES = ["running", "failed", "destroyed"];
|
|
2130
|
-
var PROVISION_KEY_SECRET = "OH_CLOUD_PROVISION_KEY";
|
|
2131
|
-
var CLOUD_HELP = `oh cloud \u2014 Manage OpenHarness Cloud nodes
|
|
2132
|
-
|
|
2133
|
-
Usage:
|
|
2134
|
-
oh cloud config [--api-url <url>] [--provision-key <key>]
|
|
2135
|
-
oh cloud config show
|
|
2136
|
-
oh cloud [global options] nodes list
|
|
2137
|
-
oh cloud [global options] nodes get <node-id>
|
|
2138
|
-
oh cloud [global options] nodes create --ssh-key-id <id> [--name <name>]
|
|
2139
|
-
oh cloud [global options] nodes events <node-id>
|
|
2140
|
-
oh cloud [global options] nodes watch <node-id> [--interval <seconds>] [--until <statuses>]
|
|
2141
|
-
oh cloud [global options] nodes destroy <node-id>
|
|
2142
|
-
oh cloud [global options] nodes restart <node-id>
|
|
2143
|
-
oh cloud [global options] nodes rebuild <node-id>
|
|
2144
|
-
oh cloud [global options] ssh-keys list
|
|
2145
|
-
oh cloud [global options] ssh-keys create --name <name> (--public-key <key> | --public-key-file <path>)
|
|
2146
|
-
|
|
2147
|
-
Global options:
|
|
2148
|
-
--api-url <url> API base URL (default: OH_CLOUD_API_URL, OH_API_URL,
|
|
2149
|
-
oh.json cloud.apiUrl, or http://127.0.0.1:3000)
|
|
2150
|
-
--provision-key <key> Admin key (default: OH_CLOUD_PROVISION_KEY in the
|
|
2151
|
-
environment or in the root .env)
|
|
2152
|
-
--output <field> Print one top-level response field instead of JSON
|
|
2153
|
-
-h, --help Show this help
|
|
2154
|
-
|
|
2155
|
-
Configuration:
|
|
2156
|
-
oh cloud config writes cloud.apiUrl to the repository oh.json and securely
|
|
2157
|
-
prompts for the current provisioner key, which it stores as
|
|
2158
|
-
${PROVISION_KEY_SECRET} in the gitignored root .env. This is the temporary
|
|
2159
|
-
credential model until OpenHarness Cloud issues user API tokens.
|
|
2160
|
-
|
|
2161
|
-
Both settings are repository-local, so oh cloud runs inside an
|
|
2162
|
-
OpenHarness-equipped repo. Outside one, pass --api-url and --provision-key.
|
|
2163
|
-
|
|
2164
|
-
Environment:
|
|
2165
|
-
OH_CLOUD_API_URL Override the OpenHarness Cloud API base URL
|
|
2166
|
-
OH_CLOUD_PROVISION_KEY Override the stored key (OH_PROVISION_KEY and
|
|
2167
|
-
PROVISION_KEY are also accepted)
|
|
2168
|
-
|
|
2169
|
-
Examples:
|
|
2170
|
-
oh cloud config
|
|
2171
|
-
oh cloud ssh-keys create --name laptop --public-key-file ~/.ssh/openharness_node.pub
|
|
2172
|
-
oh cloud nodes create --name smoke-1 --ssh-key-id <ssh-key-id>
|
|
2173
|
-
oh cloud nodes watch <node-id>
|
|
2174
|
-
`;
|
|
2175
|
-
var CloudCliError = class extends Error {
|
|
2176
|
-
status;
|
|
2177
|
-
payload;
|
|
2178
|
-
constructor(message, options = {}) {
|
|
2179
|
-
super(message);
|
|
2180
|
-
this.name = "CloudCliError";
|
|
2181
|
-
this.status = options.status;
|
|
2182
|
-
this.payload = options.payload;
|
|
2183
|
-
}
|
|
2184
|
-
};
|
|
2185
|
-
function takeOption(args, name, alias) {
|
|
2186
|
-
const names = alias ? [name, alias] : [name];
|
|
2187
|
-
for (const candidate of names) {
|
|
2188
|
-
const index = args.indexOf(candidate);
|
|
2189
|
-
if (index === -1) continue;
|
|
2190
|
-
const value = args[index + 1];
|
|
2191
|
-
if (value === void 0 || value.startsWith("-")) {
|
|
2192
|
-
throw new CloudCliError(`${candidate} requires a value`);
|
|
2193
|
-
}
|
|
2194
|
-
args.splice(index, 2);
|
|
2195
|
-
return value;
|
|
2196
|
-
}
|
|
2197
|
-
return void 0;
|
|
2198
|
-
}
|
|
2199
|
-
function requireOption(value, name) {
|
|
2200
|
-
if (!value) throw new CloudCliError(`${name} is required`);
|
|
2201
|
-
return value;
|
|
2202
|
-
}
|
|
2203
|
-
function requirePositional(value, name) {
|
|
2204
|
-
if (!value || value.startsWith("-")) throw new CloudCliError(`${name} is required`);
|
|
2205
|
-
return value;
|
|
2206
|
-
}
|
|
2207
|
-
function assertNoExtraArgs(args) {
|
|
2208
|
-
if (args.length > 0) {
|
|
2209
|
-
throw new CloudCliError(
|
|
2210
|
-
`unexpected argument${args.length === 1 ? "" : "s"}: ${args.join(" ")}`
|
|
2211
|
-
);
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
function normalizeApiUrl(value) {
|
|
2215
|
-
let url;
|
|
2216
|
-
try {
|
|
2217
|
-
url = new URL(value);
|
|
2218
|
-
} catch {
|
|
2219
|
-
throw new CloudCliError(`invalid API URL: ${value}`);
|
|
2220
|
-
}
|
|
2221
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
2222
|
-
throw new CloudCliError(`API URL must use http or https: ${value}`);
|
|
2223
|
-
}
|
|
2224
|
-
return url.toString().replace(/\/$/, "");
|
|
2225
|
-
}
|
|
2226
|
-
function formatJson(value) {
|
|
2227
|
-
return `${JSON.stringify(value, null, 2)}
|
|
2228
|
-
`;
|
|
2229
|
-
}
|
|
2230
|
-
function bodyPreview(body) {
|
|
2231
|
-
const compact = body.replace(/\s+/g, " ").trim();
|
|
2232
|
-
return compact.length > 180 ? `${compact.slice(0, 177)}...` : compact;
|
|
2233
|
-
}
|
|
2234
|
-
function responseKind(contentType) {
|
|
2235
|
-
return contentType?.split(";", 1)[0]?.trim() || "non-JSON content";
|
|
2236
|
-
}
|
|
2237
|
-
function formatApiFailure(status, payload, contentType) {
|
|
2238
|
-
if (payload && typeof payload === "object") {
|
|
2239
|
-
const record = payload;
|
|
2240
|
-
const message = record.error ?? record.message;
|
|
2241
|
-
const code = typeof record.code === "string" ? ` (${record.code})` : "";
|
|
2242
|
-
if (typeof message === "string") {
|
|
2243
|
-
return `API request failed with ${status}${code}: ${message}`;
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
if (typeof payload === "string" && payload.length > 0) {
|
|
2247
|
-
return `API request failed with ${status}: server returned ${responseKind(contentType)} instead of JSON (${bodyPreview(payload)})`;
|
|
2248
|
-
}
|
|
2249
|
-
return `API request failed with ${status}`;
|
|
2250
|
-
}
|
|
2251
|
-
async function parseResponse(response) {
|
|
2252
|
-
const text = await response.text();
|
|
2253
|
-
const contentType = response.headers.get("content-type");
|
|
2254
|
-
if (!text) return { payload: null, isJson: true, contentType };
|
|
2255
|
-
try {
|
|
2256
|
-
return { payload: JSON.parse(text), isJson: true, contentType };
|
|
2257
|
-
} catch {
|
|
2258
|
-
return { payload: text, isJson: false, contentType };
|
|
2259
|
-
}
|
|
2260
|
-
}
|
|
2261
|
-
function createCloudApiClient(options) {
|
|
2262
|
-
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
2263
|
-
if (typeof fetchImpl !== "function") {
|
|
2264
|
-
throw new CloudCliError(
|
|
2265
|
-
"the cloud CLI requires Node.js 20 or newer (global fetch is unavailable)"
|
|
2266
|
-
);
|
|
2267
|
-
}
|
|
2268
|
-
return async (method, pathname, body) => {
|
|
2269
|
-
let response;
|
|
2270
|
-
try {
|
|
2271
|
-
response = await fetchImpl(`${options.apiUrl}${pathname}`, {
|
|
2272
|
-
method,
|
|
2273
|
-
headers: {
|
|
2274
|
-
"x-provision-key": options.provisionKey,
|
|
2275
|
-
...body === void 0 ? {} : { "content-type": "application/json" }
|
|
2276
|
-
},
|
|
2277
|
-
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
2278
|
-
});
|
|
2279
|
-
} catch (error) {
|
|
2280
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
2281
|
-
throw new CloudCliError(`could not reach ${options.apiUrl}: ${detail}`);
|
|
2282
|
-
}
|
|
2283
|
-
const parsed = await parseResponse(response);
|
|
2284
|
-
if (!response.ok) {
|
|
2285
|
-
throw new CloudCliError(
|
|
2286
|
-
formatApiFailure(response.status, parsed.payload, parsed.contentType),
|
|
2287
|
-
{
|
|
2288
|
-
status: response.status,
|
|
2289
|
-
payload: parsed.payload
|
|
2290
|
-
}
|
|
2291
|
-
);
|
|
2292
|
-
}
|
|
2293
|
-
if (!parsed.isJson) {
|
|
2294
|
-
throw new CloudCliError(
|
|
2295
|
-
`API returned ${response.status} ${responseKind(parsed.contentType)}; expected JSON (${bodyPreview(parsed.payload)})`,
|
|
2296
|
-
{ status: response.status, payload: parsed.payload }
|
|
2297
|
-
);
|
|
2298
|
-
}
|
|
2299
|
-
return parsed.payload;
|
|
2300
|
-
};
|
|
2301
|
-
}
|
|
2302
|
-
function nodePath(nodeId, suffix = "") {
|
|
2303
|
-
return `/api/nodes/${encodeURIComponent(nodeId)}${suffix}`;
|
|
2304
|
-
}
|
|
2305
|
-
async function readPublicKey(args, readFileImpl) {
|
|
2306
|
-
const inline = takeOption(args, "--public-key");
|
|
2307
|
-
const file = takeOption(args, "--public-key-file");
|
|
2308
|
-
if (inline && file) {
|
|
2309
|
-
throw new CloudCliError("use only one of --public-key or --public-key-file");
|
|
2310
|
-
}
|
|
2311
|
-
if (!inline && !file) {
|
|
2312
|
-
throw new CloudCliError("--public-key or --public-key-file is required");
|
|
2313
|
-
}
|
|
2314
|
-
if (inline) return inline.trim();
|
|
2315
|
-
try {
|
|
2316
|
-
const value = (await readFileImpl(file, "utf8")).trim();
|
|
2317
|
-
if (!value) throw new CloudCliError(`SSH public key file is empty: ${file}`);
|
|
2318
|
-
return value;
|
|
2319
|
-
} catch (error) {
|
|
2320
|
-
if (error instanceof CloudCliError) throw error;
|
|
2321
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
2322
|
-
throw new CloudCliError(`could not read SSH public key file ${file}: ${detail}`);
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2325
|
-
async function dispatch(args, options) {
|
|
2326
|
-
const resource = args.shift();
|
|
2327
|
-
const action = args.shift();
|
|
2328
|
-
if (!resource || !action) {
|
|
2329
|
-
throw new CloudCliError("a resource and action are required. Run `oh cloud --help` for usage.");
|
|
2330
|
-
}
|
|
2331
|
-
if (resource === "ssh-keys") {
|
|
2332
|
-
if (action === "list") {
|
|
2333
|
-
assertNoExtraArgs(args);
|
|
2334
|
-
return options.request("GET", "/api/ssh-keys");
|
|
2335
|
-
}
|
|
2336
|
-
if (action === "create") {
|
|
2337
|
-
const name = requireOption(takeOption(args, "--name"), "--name");
|
|
2338
|
-
const publicKey = await readPublicKey(args, options.readFile);
|
|
2339
|
-
assertNoExtraArgs(args);
|
|
2340
|
-
return options.request("POST", "/api/ssh-keys", { name, publicKey });
|
|
2341
|
-
}
|
|
2342
|
-
throw new CloudCliError(`unknown ssh-keys action: ${action}`);
|
|
2343
|
-
}
|
|
2344
|
-
if (resource !== "nodes") throw new CloudCliError(`unknown resource: ${resource}`);
|
|
2345
|
-
if (action === "list") {
|
|
2346
|
-
assertNoExtraArgs(args);
|
|
2347
|
-
return options.request("GET", "/api/nodes");
|
|
2348
|
-
}
|
|
2349
|
-
if (action === "create") {
|
|
2350
|
-
const name = takeOption(args, "--name");
|
|
2351
|
-
const sshKeyId = requireOption(takeOption(args, "--ssh-key-id"), "--ssh-key-id");
|
|
2352
|
-
assertNoExtraArgs(args);
|
|
2353
|
-
return options.request("POST", "/api/nodes", {
|
|
2354
|
-
...name ? { name } : {},
|
|
2355
|
-
sshKeyId
|
|
2356
|
-
});
|
|
2357
|
-
}
|
|
2358
|
-
const nodeIdActions = ["get", "events", "destroy", "restart", "rebuild", "watch"];
|
|
2359
|
-
if (!nodeIdActions.includes(action)) {
|
|
2360
|
-
throw new CloudCliError(`unknown nodes action: ${action}`);
|
|
2361
|
-
}
|
|
2362
|
-
const nodeId = requirePositional(args.shift(), "<node-id>");
|
|
2363
|
-
if (action === "get") {
|
|
2364
|
-
assertNoExtraArgs(args);
|
|
2365
|
-
return options.request("GET", nodePath(nodeId));
|
|
2366
|
-
}
|
|
2367
|
-
if (action === "events") {
|
|
2368
|
-
assertNoExtraArgs(args);
|
|
2369
|
-
return options.request("GET", nodePath(nodeId, "/events"));
|
|
2370
|
-
}
|
|
2371
|
-
if (action === "destroy") {
|
|
2372
|
-
assertNoExtraArgs(args);
|
|
2373
|
-
return options.request("DELETE", nodePath(nodeId));
|
|
2374
|
-
}
|
|
2375
|
-
if (action === "restart" || action === "rebuild") {
|
|
2376
|
-
assertNoExtraArgs(args);
|
|
2377
|
-
return options.request("POST", nodePath(nodeId, `/${action}`));
|
|
2378
|
-
}
|
|
2379
|
-
if (action === "watch") {
|
|
2380
|
-
const intervalValue = takeOption(args, "--interval") ?? "5";
|
|
2381
|
-
const intervalSeconds = Number(intervalValue);
|
|
2382
|
-
if (!Number.isFinite(intervalSeconds) || intervalSeconds < 0) {
|
|
2383
|
-
throw new CloudCliError(`--interval must be a non-negative number: ${intervalValue}`);
|
|
2384
|
-
}
|
|
2385
|
-
const untilValue = takeOption(args, "--until");
|
|
2386
|
-
const terminalStatuses = new Set(
|
|
2387
|
-
(untilValue ? untilValue.split(",") : DEFAULT_TERMINAL_STATUSES).map((status) => status.trim()).filter(Boolean)
|
|
2388
|
-
);
|
|
2389
|
-
if (terminalStatuses.size === 0) {
|
|
2390
|
-
throw new CloudCliError("--until must contain at least one status");
|
|
2391
|
-
}
|
|
2392
|
-
assertNoExtraArgs(args);
|
|
2393
|
-
let lastStatus;
|
|
2394
|
-
while (true) {
|
|
2395
|
-
const node = await options.request("GET", nodePath(nodeId));
|
|
2396
|
-
if (!node || typeof node !== "object" || typeof node.status !== "string") {
|
|
2397
|
-
throw new CloudCliError("node response did not include a status");
|
|
2398
|
-
}
|
|
2399
|
-
const status = node.status;
|
|
2400
|
-
if (status !== lastStatus) {
|
|
2401
|
-
options.write(node);
|
|
2402
|
-
lastStatus = status;
|
|
2403
|
-
}
|
|
2404
|
-
if (terminalStatuses.has(status)) return void 0;
|
|
2405
|
-
await options.sleep(intervalSeconds * 1e3);
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
throw new CloudCliError(`unknown nodes action: ${action}`);
|
|
2409
|
-
}
|
|
2410
|
-
var OUTSIDE_REPO_HINT = "; pass --api-url and --provision-key to run `oh cloud` outside a repo";
|
|
2411
|
-
function requireRoot(io) {
|
|
2412
|
-
try {
|
|
2413
|
-
return resolveProjectRoot(io.cwd);
|
|
2414
|
-
} catch (error) {
|
|
2415
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
2416
|
-
throw new CloudCliError(`${detail}${OUTSIDE_REPO_HINT}`);
|
|
2417
|
-
}
|
|
2418
|
-
}
|
|
2419
|
-
function legacyCloudConfigPath(env) {
|
|
2420
|
-
const home = env.HOME ?? env.USERPROFILE;
|
|
2421
|
-
if (!home) return void 0;
|
|
2422
|
-
return join4(home, ".config", "openharness", "cloud.json");
|
|
2423
|
-
}
|
|
2424
|
-
function readLegacyCloudConfig(path4) {
|
|
2425
|
-
let parsed;
|
|
2426
|
-
try {
|
|
2427
|
-
parsed = JSON.parse(readFileSync9(path4, "utf8"));
|
|
2428
|
-
} catch {
|
|
2429
|
-
return void 0;
|
|
2430
|
-
}
|
|
2431
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
2432
|
-
const record = parsed;
|
|
2433
|
-
return {
|
|
2434
|
-
...typeof record.apiUrl === "string" ? { apiUrl: record.apiUrl } : {},
|
|
2435
|
-
...typeof record.provisionKey === "string" ? { provisionKey: record.provisionKey } : {}
|
|
2436
|
-
};
|
|
2437
|
-
}
|
|
2438
|
-
function migrateLegacyCloudConfig(root, io, env) {
|
|
2439
|
-
const path4 = legacyCloudConfigPath(env);
|
|
2440
|
-
if (!path4 || !existsSync9(path4)) return;
|
|
2441
|
-
const config = readOhConfig(ohConfigPath(root));
|
|
2442
|
-
if (config.cloud?.apiUrl !== void 0) return;
|
|
2443
|
-
if (readSecret(root, PROVISION_KEY_SECRET) !== void 0) return;
|
|
2444
|
-
const legacy = readLegacyCloudConfig(path4);
|
|
2445
|
-
if (!legacy || !legacy.apiUrl && !legacy.provisionKey) return;
|
|
2446
|
-
if (legacy.apiUrl) {
|
|
2447
|
-
const next = {
|
|
2448
|
-
...config,
|
|
2449
|
-
cloud: { ...config.cloud, apiUrl: normalizeApiUrl(legacy.apiUrl) }
|
|
2450
|
-
};
|
|
2451
|
-
writeOhConfig(root, next);
|
|
2452
|
-
}
|
|
2453
|
-
if (legacy.provisionKey) setSecret(root, PROVISION_KEY_SECRET, legacy.provisionKey);
|
|
2454
|
-
io.stdout(`Migrated OpenHarness Cloud settings out of ${path4}
|
|
2455
|
-
`);
|
|
2456
|
-
if (legacy.apiUrl) io.stdout(` cloud.apiUrl -> ${ohConfigPath(root)}
|
|
2457
|
-
`);
|
|
2458
|
-
if (legacy.provisionKey) io.stdout(` ${PROVISION_KEY_SECRET} -> ${join4(root, ".env")}
|
|
2459
|
-
`);
|
|
2460
|
-
io.stdout(`${path4} is no longer read; delete it when you no longer need the copy.
|
|
2461
|
-
`);
|
|
2462
|
-
}
|
|
2463
|
-
function loadCloudSettings(root, io, env) {
|
|
2464
|
-
migrateLegacyCloudConfig(root, io, env);
|
|
2465
|
-
const config = readOhConfig(ohConfigPath(root));
|
|
2466
|
-
return {
|
|
2467
|
-
...config.cloud?.apiUrl ? { apiUrl: config.cloud.apiUrl } : {},
|
|
2468
|
-
...(() => {
|
|
2469
|
-
const key = readSecret(root, PROVISION_KEY_SECRET);
|
|
2470
|
-
return key ? { provisionKey: key } : {};
|
|
2471
|
-
})()
|
|
2472
|
-
};
|
|
2473
|
-
}
|
|
2474
|
-
function envProvisionKey(env) {
|
|
2475
|
-
return env.OH_CLOUD_PROVISION_KEY ?? env.OH_PROVISION_KEY ?? env.PROVISION_KEY;
|
|
2476
|
-
}
|
|
2477
|
-
async function configureCloud(args, io, env) {
|
|
2478
|
-
const root = requireRoot(io);
|
|
2479
|
-
const current = loadCloudSettings(root, io, env);
|
|
2480
|
-
if (args[0] === "show") {
|
|
2481
|
-
args.shift();
|
|
2482
|
-
assertNoExtraArgs(args);
|
|
2483
|
-
io.stdout(`Config: ${ohConfigPath(root)}
|
|
2484
|
-
`);
|
|
2485
|
-
io.stdout(`Secrets: ${join4(root, ".env")}
|
|
2486
|
-
`);
|
|
2487
|
-
io.stdout(`API URL: ${current.apiUrl ?? DEFAULT_API_URL}
|
|
2488
|
-
`);
|
|
2489
|
-
io.stdout(
|
|
2490
|
-
`Provision credential: ${current.provisionKey ? redact(current.provisionKey) : "not configured"}
|
|
2491
|
-
`
|
|
2492
|
-
);
|
|
2493
|
-
return;
|
|
2494
|
-
}
|
|
2495
|
-
const apiUrl = normalizeApiUrl(
|
|
2496
|
-
takeOption(args, "--api-url") ?? current.apiUrl ?? env.OH_CLOUD_API_URL ?? env.OH_API_URL ?? DEFAULT_API_URL
|
|
2497
|
-
);
|
|
2498
|
-
const providedKey = takeOption(args, "--provision-key");
|
|
2499
|
-
assertNoExtraArgs(args);
|
|
2500
|
-
const provisionKey = (providedKey ?? await (io.askSecret ?? askSecret)("OpenHarness Cloud provisioner key:")).trim();
|
|
2501
|
-
if (!provisionKey) throw new CloudCliError("provisioner key cannot be empty");
|
|
2502
|
-
const config = readOhConfig(ohConfigPath(root));
|
|
2503
|
-
writeOhConfig(root, { ...config, cloud: { ...config.cloud, apiUrl } });
|
|
2504
|
-
setSecret(root, PROVISION_KEY_SECRET, provisionKey);
|
|
2505
|
-
io.stdout(`Saved cloud.apiUrl to ${ohConfigPath(root)}
|
|
2506
|
-
`);
|
|
2507
|
-
io.stdout(`Saved ${PROVISION_KEY_SECRET} to ${join4(root, ".env")}
|
|
2508
|
-
`);
|
|
2509
|
-
io.stdout("Provision credential: configured\n");
|
|
2510
|
-
}
|
|
2511
|
-
async function executeCloud(argv, io) {
|
|
2512
|
-
const args = [...argv];
|
|
2513
|
-
const env = io.env ?? process.env;
|
|
2514
|
-
if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
|
|
2515
|
-
io.stdout(CLOUD_HELP);
|
|
2516
|
-
return;
|
|
2517
|
-
}
|
|
2518
|
-
if (args[0] === "config") {
|
|
2519
|
-
args.shift();
|
|
2520
|
-
await configureCloud(args, io, env);
|
|
2521
|
-
return;
|
|
2522
|
-
}
|
|
2523
|
-
const flagApiUrl = takeOption(args, "--api-url");
|
|
2524
|
-
const flagProvisionKey = takeOption(args, "--provision-key");
|
|
2525
|
-
const stored = flagProvisionKey && flagApiUrl ? {} : loadCloudSettings(requireRoot(io), io, env);
|
|
2526
|
-
const apiUrl = normalizeApiUrl(
|
|
2527
|
-
flagApiUrl ?? env.OH_CLOUD_API_URL ?? env.OH_API_URL ?? stored.apiUrl ?? DEFAULT_API_URL
|
|
2528
|
-
);
|
|
2529
|
-
const provisionKey = flagProvisionKey ?? envProvisionKey(env) ?? stored.provisionKey;
|
|
2530
|
-
if (!provisionKey) {
|
|
2531
|
-
throw new CloudCliError(
|
|
2532
|
-
`no provision credential found. Run \`oh cloud config\`, set ${PROVISION_KEY_SECRET}, or pass --api-url and --provision-key.`
|
|
2533
|
-
);
|
|
2534
|
-
}
|
|
2535
|
-
const outputField = takeOption(args, "--output");
|
|
2536
|
-
const request = createCloudApiClient({
|
|
2537
|
-
apiUrl,
|
|
2538
|
-
provisionKey,
|
|
2539
|
-
fetch: io.fetch
|
|
2540
|
-
});
|
|
2541
|
-
const write = (value) => io.stdout(formatJson(value));
|
|
2542
|
-
const result = await dispatch(args, {
|
|
2543
|
-
request,
|
|
2544
|
-
readFile: io.readFile ?? readFile,
|
|
2545
|
-
sleep: io.sleep ?? ((milliseconds) => new Promise((resolve11) => setTimeout(resolve11, milliseconds))),
|
|
2546
|
-
write
|
|
2547
|
-
});
|
|
2548
|
-
if (result === void 0) return;
|
|
2549
|
-
if (outputField) {
|
|
2550
|
-
if (!result || typeof result !== "object" || !(outputField in result)) {
|
|
2551
|
-
throw new CloudCliError(`response does not contain top-level field: ${outputField}`);
|
|
2552
|
-
}
|
|
2553
|
-
const value = result[outputField];
|
|
2554
|
-
io.stdout(typeof value === "object" ? formatJson(value) : `${String(value)}
|
|
2555
|
-
`);
|
|
2556
|
-
return;
|
|
2557
|
-
}
|
|
2558
|
-
write(result);
|
|
2559
|
-
}
|
|
2560
|
-
async function runCloud(argv, io) {
|
|
2561
|
-
try {
|
|
2562
|
-
await executeCloud(argv, io);
|
|
2563
|
-
return 0;
|
|
2564
|
-
} catch (error) {
|
|
2565
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2566
|
-
io.stderr(`oh cloud: ${message}
|
|
2567
|
-
`);
|
|
2568
|
-
return 1;
|
|
2569
|
-
}
|
|
2570
|
-
}
|
|
2571
|
-
|
|
2572
|
-
// src/lib/execution/runner.ts
|
|
2573
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
2574
|
-
import { existsSync as existsSync10 } from "node:fs";
|
|
2575
|
-
import { basename as basename4, join as join5 } from "node:path";
|
|
2576
|
-
var spawnRunner = (cmd, args, opts) => {
|
|
2577
|
-
const common = {
|
|
2578
|
-
env: opts.env,
|
|
2579
|
-
...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
|
|
2580
|
-
...opts.timeoutMs !== void 0 ? { timeout: opts.timeoutMs } : {}
|
|
2581
|
-
};
|
|
2582
|
-
const r = opts.stdio === "capture" ? spawnSync2(cmd, args, { ...common, stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" }) : spawnSync2(cmd, args, { ...common, stdio: "inherit" });
|
|
2583
|
-
const err = r.error;
|
|
2584
|
-
return {
|
|
2585
|
-
status: r.status,
|
|
2586
|
-
error: err ? { code: err.code, message: err.message } : void 0,
|
|
2587
|
-
stdout: typeof r.stdout === "string" ? r.stdout : void 0,
|
|
2588
|
-
stderr: typeof r.stderr === "string" ? r.stderr : void 0
|
|
2589
|
-
};
|
|
2590
|
-
};
|
|
2591
|
-
var ExecutionSpawnError = class extends Error {
|
|
2592
|
-
code;
|
|
2593
|
-
constructor(what, error) {
|
|
2594
|
-
super(`failed to run ${what}${error?.message ? ` (${error.message})` : ""}`);
|
|
2595
|
-
this.name = "ExecutionSpawnError";
|
|
2596
|
-
this.code = error?.code;
|
|
2597
|
-
}
|
|
2598
|
-
};
|
|
2599
|
-
var ExecutionExitError = class extends Error {
|
|
2600
|
-
exitCode;
|
|
2601
|
-
constructor(what, exitCode) {
|
|
2602
|
-
super(`${what} exited ${exitCode}`);
|
|
2603
|
-
this.name = "ExecutionExitError";
|
|
2604
|
-
this.exitCode = exitCode;
|
|
2605
|
-
}
|
|
2606
|
-
};
|
|
2607
|
-
function assertSpawned(r, what) {
|
|
2608
|
-
if (r.error) {
|
|
2609
|
-
throw new ExecutionSpawnError(what, r.error);
|
|
2610
|
-
}
|
|
2611
|
-
}
|
|
2612
|
-
function requireLifecycleScript(root, rel) {
|
|
2613
|
-
const { controlDir } = resolveProjectLayout(root);
|
|
2614
|
-
const script = join5(controlDir, "scripts", rel);
|
|
2615
|
-
if (!existsSync10(script)) {
|
|
2616
|
-
throw new Error(
|
|
2617
|
-
`missing lifecycle script ${script} \u2014 the vendored ${basename4(controlDir)}/ payload looks incomplete; run \`oh update\` to re-vendor it`
|
|
2618
|
-
);
|
|
2619
|
-
}
|
|
2620
|
-
return script;
|
|
2053
|
+
}
|
|
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);
|
|
2621
2059
|
}
|
|
2622
2060
|
|
|
2623
2061
|
// src/lib/registry.ts
|
|
2624
|
-
import { existsSync as
|
|
2625
|
-
import { dirname as dirname6, join 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";
|
|
2626
2064
|
|
|
2627
2065
|
// oh-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.yml
|
|
2628
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';
|
|
@@ -3087,7 +2525,7 @@ function ohHome() {
|
|
|
3087
2525
|
return resolveUserStateHome(process.env);
|
|
3088
2526
|
}
|
|
3089
2527
|
function registryRoot() {
|
|
3090
|
-
return
|
|
2528
|
+
return join5(ohHome(), "sandboxes");
|
|
3091
2529
|
}
|
|
3092
2530
|
function assertSandboxName(name) {
|
|
3093
2531
|
if (!SANDBOX_NAME_PATTERN.test(name)) {
|
|
@@ -3098,7 +2536,7 @@ function assertSandboxName(name) {
|
|
|
3098
2536
|
}
|
|
3099
2537
|
function entryRoot(name) {
|
|
3100
2538
|
assertSandboxName(name);
|
|
3101
|
-
return
|
|
2539
|
+
return join5(registryRoot(), name);
|
|
3102
2540
|
}
|
|
3103
2541
|
function isDirectory(path4) {
|
|
3104
2542
|
return statSync3(path4, { throwIfNoEntry: false })?.isDirectory() === true;
|
|
@@ -3106,12 +2544,12 @@ function isDirectory(path4) {
|
|
|
3106
2544
|
function listEntries() {
|
|
3107
2545
|
const root = registryRoot();
|
|
3108
2546
|
if (!isDirectory(root)) return [];
|
|
3109
|
-
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();
|
|
3110
2548
|
}
|
|
3111
2549
|
function entryRepo(name) {
|
|
3112
2550
|
const config = readOhConfig(ohConfigPath(entryRoot(name)));
|
|
3113
|
-
const
|
|
3114
|
-
return
|
|
2551
|
+
const checkout = configCheckout(config);
|
|
2552
|
+
return checkout === void 0 ? void 0 : resolve7(checkout);
|
|
3115
2553
|
}
|
|
3116
2554
|
function runningContainerNames(run) {
|
|
3117
2555
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -3137,27 +2575,27 @@ function nextDefaultName(run = spawnRunner) {
|
|
|
3137
2575
|
}
|
|
3138
2576
|
}
|
|
3139
2577
|
function materialize(root, opts = {}) {
|
|
3140
|
-
const entry =
|
|
3141
|
-
const scripts =
|
|
2578
|
+
const entry = resolve7(root);
|
|
2579
|
+
const scripts = join5(resolveProjectLayout(entry).controlDir, "scripts");
|
|
3142
2580
|
const files = [
|
|
3143
2581
|
{
|
|
3144
|
-
dest:
|
|
3145
|
-
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,
|
|
3146
2584
|
mode: 420
|
|
3147
2585
|
},
|
|
3148
|
-
{ dest:
|
|
3149
|
-
{ dest:
|
|
3150
|
-
{ dest:
|
|
3151
|
-
{ dest:
|
|
3152
|
-
{ dest:
|
|
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 }
|
|
3153
2591
|
];
|
|
3154
2592
|
for (const file of files) {
|
|
3155
|
-
const dest =
|
|
2593
|
+
const dest = resolve7(file.dest);
|
|
3156
2594
|
if (!dest.startsWith(entry + sep3)) {
|
|
3157
2595
|
throw new Error(`refusing to materialize outside the sandbox entry: ${dest}`);
|
|
3158
2596
|
}
|
|
3159
2597
|
mkdirSync4(dirname6(dest), { recursive: true });
|
|
3160
|
-
|
|
2598
|
+
writeFileSync4(dest, file.body, { mode: file.mode, encoding: "utf8" });
|
|
3161
2599
|
}
|
|
3162
2600
|
}
|
|
3163
2601
|
function resolveSandboxRoot(opts = {}) {
|
|
@@ -3165,14 +2603,14 @@ function resolveSandboxRoot(opts = {}) {
|
|
|
3165
2603
|
const root = entryRoot(opts.name);
|
|
3166
2604
|
if (!isDirectory(root)) {
|
|
3167
2605
|
throw new Error(
|
|
3168
|
-
`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}\``
|
|
3169
2607
|
);
|
|
3170
2608
|
}
|
|
3171
2609
|
return root;
|
|
3172
2610
|
}
|
|
3173
2611
|
const names = listEntries();
|
|
3174
2612
|
if (names.length === 1) return entryRoot(names[0]);
|
|
3175
|
-
const cwd =
|
|
2613
|
+
const cwd = resolve7(opts.cwd ?? process.cwd());
|
|
3176
2614
|
for (const name of names) {
|
|
3177
2615
|
const repo = entryRepo(name);
|
|
3178
2616
|
if (repo !== void 0 && (cwd === repo || cwd.startsWith(repo + sep3))) {
|
|
@@ -3181,7 +2619,7 @@ function resolveSandboxRoot(opts = {}) {
|
|
|
3181
2619
|
}
|
|
3182
2620
|
if (names.length === 0) {
|
|
3183
2621
|
throw new Error(
|
|
3184
|
-
`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\``
|
|
3185
2623
|
);
|
|
3186
2624
|
}
|
|
3187
2625
|
throw new Error(
|
|
@@ -3189,333 +2627,131 @@ function resolveSandboxRoot(opts = {}) {
|
|
|
3189
2627
|
);
|
|
3190
2628
|
}
|
|
3191
2629
|
|
|
3192
|
-
// src/
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
}
|
|
3196
|
-
function configFieldList() {
|
|
3197
|
-
return ohConfigFieldPaths().map((path4) => ` ${path4}`).join("\n");
|
|
3198
|
-
}
|
|
3199
|
-
async function runConfigShow(opts, io) {
|
|
3200
|
-
const root = configRoot(opts);
|
|
3201
|
-
const config = readOhConfig(ohConfigPath(root));
|
|
3202
|
-
io.stdout(`${JSON.stringify(config, null, 2)}
|
|
3203
|
-
`);
|
|
3204
|
-
return 0;
|
|
3205
|
-
}
|
|
3206
|
-
async function runConfigSet(key, value, opts, io) {
|
|
3207
|
-
if (isSecretKey(key) || isSecretKey(key.toUpperCase())) {
|
|
3208
|
-
io.stderr(
|
|
3209
|
-
`oh config set: ${key.toUpperCase()} is a secret \u2014 oh.json is tracked by git.
|
|
3210
|
-
Set it with \`oh secret set ${key.toUpperCase()}\` instead.
|
|
3211
|
-
`
|
|
3212
|
-
);
|
|
3213
|
-
return 1;
|
|
3214
|
-
}
|
|
3215
|
-
if (!findOhConfigField(key)) {
|
|
3216
|
-
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";
|
|
3217
2633
|
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
return `git@github.com:${owner}/${name}.git`;
|
|
3242
|
-
}
|
|
3243
|
-
function shellLine(step) {
|
|
3244
|
-
return `${step.cmd} ${step.args.join(" ")}`;
|
|
3245
|
-
}
|
|
3246
|
-
function manualSteps(owner, name, visibility) {
|
|
3247
|
-
return [
|
|
3248
|
-
{ cmd: "gh", args: ["repo", "create", `${owner}/${name}`, `--${visibility}`] },
|
|
3249
|
-
{ cmd: "git", args: ["remote", "rename", "origin", UPSTREAM_REMOTE] },
|
|
3250
|
-
{ cmd: "git", args: ["remote", "add", "origin", repoUrl(owner, name)] },
|
|
3251
|
-
{ cmd: "git", args: ["push", "-u", "origin", "HEAD"] }
|
|
3252
|
-
];
|
|
3253
|
-
}
|
|
3254
|
-
function printManual(io, steps, lead) {
|
|
3255
|
-
io.stdout(`
|
|
3256
|
-
${lead}
|
|
3257
|
-
|
|
3258
|
-
`);
|
|
3259
|
-
for (const step of steps) io.stdout(` ${shellLine(step)}
|
|
3260
|
-
`);
|
|
3261
|
-
io.stdout("\n");
|
|
3262
|
-
}
|
|
3263
|
-
function validateSegment(kind, value) {
|
|
3264
|
-
if (value === "") return `${kind} is required`;
|
|
3265
|
-
if (value.startsWith("-")) return `invalid ${kind} "${value}" (must not start with "-")`;
|
|
3266
|
-
if (!/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
3267
|
-
return `invalid ${kind} "${value}" (use letters, digits, ".", "_" or "-")`;
|
|
3268
|
-
}
|
|
3269
|
-
return void 0;
|
|
3270
|
-
}
|
|
3271
|
-
async function askOn(io, question) {
|
|
3272
|
-
const askFn = io.ask ?? ask;
|
|
3273
|
-
return (await askFn(question)).trim();
|
|
3274
|
-
}
|
|
3275
|
-
async function confirmOn(io, question, defaultYes) {
|
|
3276
|
-
const answer = (await askOn(io, `${question} ${defaultYes ? "[Y/n]" : "[y/N]"}`)).toLowerCase();
|
|
3277
|
-
if (answer === "") return defaultYes;
|
|
3278
|
-
return /^y/.test(answer);
|
|
3279
|
-
}
|
|
3280
|
-
async function askVisibility(io) {
|
|
3281
|
-
io.stdout(" Repository visibility\n 1) private (recommended)\n 2) public\n");
|
|
3282
|
-
for (; ; ) {
|
|
3283
|
-
const answer = await askOn(io, "Choose [1-2] (default 1):");
|
|
3284
|
-
if (answer === "" || answer === "1") return "private";
|
|
3285
|
-
if (answer === "2") return "public";
|
|
3286
|
-
io.stderr(" Invalid choice. Pick 1 or 2.\n");
|
|
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;
|
|
3287
2657
|
}
|
|
3288
2658
|
}
|
|
3289
|
-
|
|
3290
|
-
const
|
|
3291
|
-
|
|
3292
|
-
if (!isTTY) {
|
|
3293
|
-
io.stderr(
|
|
3294
|
-
"oh config repo: creating a repo on your GitHub account needs an interactive terminal; re-run it from a TTY.\n"
|
|
3295
|
-
);
|
|
3296
|
-
return 1;
|
|
3297
|
-
}
|
|
3298
|
-
io.stdout(`
|
|
3299
|
-
${bold("Version this harness in your own repo")}
|
|
3300
|
-
`);
|
|
3301
|
-
io.stdout(
|
|
3302
|
-
` Creates a repo on your GitHub account, keeps the upstream you cloned from
|
|
3303
|
-
as the "${UPSTREAM_REMOTE}" remote, and pushes this branch to yours.
|
|
3304
|
-
`
|
|
3305
|
-
);
|
|
3306
|
-
if (!await confirmOn(io, "Version this harness in your own repo?", false)) {
|
|
3307
|
-
printManual(
|
|
3308
|
-
io,
|
|
3309
|
-
manualSteps("<your-user>", "<repo>", "private"),
|
|
3310
|
-
"Skipped \u2014 nothing was created and no remote was touched. To do it later, run `oh config repo`, or by hand:"
|
|
3311
|
-
);
|
|
3312
|
-
return 0;
|
|
3313
|
-
}
|
|
3314
|
-
const owner = await askOn(io, "GitHub owner (user or org):");
|
|
3315
|
-
const ownerError = validateSegment("GitHub owner", owner);
|
|
3316
|
-
if (ownerError) {
|
|
3317
|
-
io.stderr(`oh config repo: ${ownerError}
|
|
3318
|
-
`);
|
|
3319
|
-
return 1;
|
|
3320
|
-
}
|
|
3321
|
-
const name = await askOn(io, "Repository name [openharness]:");
|
|
3322
|
-
const repoName = name === "" ? "openharness" : name;
|
|
3323
|
-
const nameError = validateSegment("repository name", repoName);
|
|
3324
|
-
if (nameError) {
|
|
3325
|
-
io.stderr(`oh config repo: ${nameError}
|
|
3326
|
-
`);
|
|
3327
|
-
return 1;
|
|
3328
|
-
}
|
|
3329
|
-
const visibility = await askVisibility(io);
|
|
3330
|
-
const url = repoUrl(owner, repoName);
|
|
3331
|
-
const manual = manualSteps(owner, repoName, visibility);
|
|
3332
|
-
let root;
|
|
2659
|
+
function writeEnvFile(envPath, content) {
|
|
2660
|
+
const tmpPath = `${envPath}.tmp.${process.pid}`;
|
|
2661
|
+
let fd;
|
|
3333
2662
|
try {
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
}
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
return 0;
|
|
3346
|
-
}
|
|
3347
|
-
assertSpawned(auth, "gh auth status");
|
|
3348
|
-
if (auth.status !== 0) {
|
|
3349
|
-
printManual(
|
|
3350
|
-
io,
|
|
3351
|
-
manual,
|
|
3352
|
-
"gh is not authenticated (`gh auth login`) \u2014 finish this by hand once it is:"
|
|
3353
|
-
);
|
|
3354
|
-
return 0;
|
|
3355
|
-
}
|
|
3356
|
-
const remotesResult = run("git", ["remote"], capture2);
|
|
3357
|
-
assertSpawned(remotesResult, "git remote");
|
|
3358
|
-
if (remotesResult.status !== 0) {
|
|
3359
|
-
printManual(io, manual, "`git remote` failed \u2014 is this a git repo? Finish this by hand:");
|
|
3360
|
-
return 0;
|
|
3361
|
-
}
|
|
3362
|
-
const remotes = (remotesResult.stdout ?? "").split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
3363
|
-
let originUrl = "";
|
|
3364
|
-
if (remotes.includes("origin")) {
|
|
3365
|
-
const shown = run("git", ["remote", "get-url", "origin"], capture2);
|
|
3366
|
-
assertSpawned(shown, "git remote get-url origin");
|
|
3367
|
-
originUrl = (shown.stdout ?? "").trim();
|
|
3368
|
-
}
|
|
3369
|
-
const originIsTarget = originUrl !== "" && sameRemote(originUrl, owner, repoName);
|
|
3370
|
-
if (!originIsTarget && remotes.includes("origin") && remotes.includes(UPSTREAM_REMOTE)) {
|
|
3371
|
-
io.stderr(
|
|
3372
|
-
`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:
|
|
3373
|
-
`
|
|
3374
|
-
);
|
|
3375
|
-
printManual(io, manual.slice(2), "Remaining steps:");
|
|
3376
|
-
return 1;
|
|
3377
|
-
}
|
|
3378
|
-
const steps = [];
|
|
3379
|
-
const exists = run("gh", ["repo", "view", `${owner}/${repoName}`], capture2);
|
|
3380
|
-
assertSpawned(exists, "gh repo view");
|
|
3381
|
-
if (exists.status === 0) {
|
|
3382
|
-
io.stdout(` ${owner}/${repoName} already exists \u2014 skipping creation.
|
|
3383
|
-
`);
|
|
3384
|
-
} else {
|
|
3385
|
-
steps.push(manual[0]);
|
|
3386
|
-
}
|
|
3387
|
-
if (originIsTarget) {
|
|
3388
|
-
io.stdout(" origin already points at your repo \u2014 leaving the remotes alone.\n");
|
|
3389
|
-
} else {
|
|
3390
|
-
if (remotes.includes("origin")) steps.push(manual[1]);
|
|
3391
|
-
steps.push(manual[2]);
|
|
3392
|
-
}
|
|
3393
|
-
steps.push(manual[3]);
|
|
3394
|
-
for (let i = 0; i < steps.length; i++) {
|
|
3395
|
-
const step = steps[i];
|
|
3396
|
-
io.stdout(` $ ${shellLine(step)}
|
|
3397
|
-
`);
|
|
3398
|
-
const result = run(step.cmd, step.args, { stdio: "inherit", cwd: root });
|
|
3399
|
-
if (result.error || result.status !== 0) {
|
|
3400
|
-
const detail = result.error?.message ?? `exit ${result.status}`;
|
|
3401
|
-
io.stderr(`oh config repo: ${shellLine(step)} failed (${detail}) \u2014 stopping here.
|
|
3402
|
-
`);
|
|
3403
|
-
const remaining = steps.slice(i + 1);
|
|
3404
|
-
if (remaining.length > 0) {
|
|
3405
|
-
printManual(io, remaining, "Not run. Finish by hand once the failure is fixed:");
|
|
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 {
|
|
3406
2674
|
}
|
|
3407
|
-
return 1;
|
|
3408
2675
|
}
|
|
2676
|
+
try {
|
|
2677
|
+
unlinkSync4(tmpPath);
|
|
2678
|
+
} catch {
|
|
2679
|
+
}
|
|
2680
|
+
throw err;
|
|
3409
2681
|
}
|
|
3410
|
-
io.stdout(
|
|
3411
|
-
`
|
|
3412
|
-
Done \u2014 origin is ${url}${remotes.includes("origin") && !originIsTarget ? `, upstream kept as "${UPSTREAM_REMOTE}"` : ""}.
|
|
3413
|
-
`
|
|
3414
|
-
);
|
|
3415
|
-
return 0;
|
|
3416
|
-
}
|
|
3417
|
-
function sameRemote(url, owner, name) {
|
|
3418
|
-
const normalized = url.replace(/\.git$/, "").toLowerCase();
|
|
3419
|
-
const slug = `${owner}/${name}`.toLowerCase();
|
|
3420
|
-
return normalized.endsWith(`:${slug}`) || normalized.endsWith(`/${slug}`);
|
|
3421
|
-
}
|
|
3422
|
-
|
|
3423
|
-
// src/commands/secret.ts
|
|
3424
|
-
import { existsSync as existsSync13 } from "node:fs";
|
|
3425
|
-
import { join as join8 } from "node:path";
|
|
3426
|
-
|
|
3427
|
-
// src/lib/gitignore.ts
|
|
3428
|
-
import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3429
|
-
import { dirname as dirname7, join as join7 } from "node:path";
|
|
3430
|
-
function appendGitignoreLines(root, lines) {
|
|
3431
|
-
const target = join7(root, ".gitignore");
|
|
3432
|
-
const existing = existsSync12(target) ? readFileSync10(target, "utf8") : "";
|
|
3433
|
-
const present = new Set(existing.split("\n").map((line) => line.trimEnd()));
|
|
3434
|
-
const added = [];
|
|
3435
|
-
for (const line of lines) {
|
|
3436
|
-
const key = line.trimEnd();
|
|
3437
|
-
if (key === "" || present.has(key)) continue;
|
|
3438
|
-
present.add(key);
|
|
3439
|
-
added.push(line);
|
|
3440
|
-
}
|
|
3441
|
-
if (added.length === 0) return added;
|
|
3442
|
-
let output = existing;
|
|
3443
|
-
if (output.length > 0 && !output.endsWith("\n")) output += "\n";
|
|
3444
|
-
output += `${added.join("\n")}
|
|
3445
|
-
`;
|
|
3446
|
-
mkdirSync5(dirname7(target), { recursive: true });
|
|
3447
|
-
writeFileSync6(target, output, "utf8");
|
|
3448
|
-
return added;
|
|
3449
2682
|
}
|
|
3450
2683
|
|
|
3451
|
-
// src/
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
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);
|
|
3458
2699
|
}
|
|
3459
|
-
function
|
|
3460
|
-
return
|
|
2700
|
+
function isSecretKey(key) {
|
|
2701
|
+
return ALLOWED.has(key);
|
|
3461
2702
|
}
|
|
3462
|
-
|
|
3463
|
-
if (
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
Keys:
|
|
3469
|
-
${secretKeyList()}
|
|
3470
|
-
`
|
|
3471
|
-
);
|
|
3472
|
-
return 1;
|
|
3473
|
-
}
|
|
3474
|
-
const root = secretsRoot(opts);
|
|
3475
|
-
const askSecret2 = io.askSecret ?? askSecret;
|
|
3476
|
-
const value = (await askSecret2(`Value for ${bold(key)} (input hidden):`)).trim();
|
|
3477
|
-
if (value === "") {
|
|
3478
|
-
io.stderr(`oh secret set: no value entered \u2014 ${key} unchanged
|
|
3479
|
-
`);
|
|
3480
|
-
return 1;
|
|
3481
|
-
}
|
|
3482
|
-
try {
|
|
3483
|
-
setSecret(root, key, value);
|
|
3484
|
-
ignoreSecretsFile(root);
|
|
3485
|
-
} catch (error) {
|
|
3486
|
-
io.stderr(`oh secret set: ${error instanceof Error ? error.message : String(error)}
|
|
3487
|
-
`);
|
|
3488
|
-
return 1;
|
|
3489
|
-
}
|
|
3490
|
-
io.stdout(`.env: set ${key}=${redact(value)}
|
|
3491
|
-
`);
|
|
3492
|
-
return 0;
|
|
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
|
+
);
|
|
3493
2708
|
}
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
const
|
|
3497
|
-
if (
|
|
3498
|
-
|
|
3499
|
-
|
|
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);
|
|
2717
|
+
}
|
|
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`);
|
|
3500
2722
|
}
|
|
3501
|
-
const
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
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);
|
|
2728
|
+
}
|
|
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
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
function stripQuotes2(s) {
|
|
2740
|
+
if (s.length >= 2 && (s[0] === '"' && s.endsWith('"') || s[0] === "'" && s.endsWith("'"))) {
|
|
2741
|
+
return s.slice(1, -1);
|
|
3506
2742
|
}
|
|
3507
|
-
return
|
|
2743
|
+
return s;
|
|
3508
2744
|
}
|
|
3509
2745
|
|
|
3510
2746
|
// src/commands/lifecycle.ts
|
|
3511
|
-
import { existsSync as
|
|
2747
|
+
import { existsSync as existsSync12, mkdtempSync, readFileSync as readFileSync9, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3512
2748
|
import { tmpdir } from "node:os";
|
|
3513
|
-
import { basename as basename5, join as
|
|
2749
|
+
import { basename as basename5, join as join6, sep as sep4 } from "node:path";
|
|
3514
2750
|
|
|
3515
2751
|
// src/lib/execution/detect.ts
|
|
3516
|
-
import { existsSync as
|
|
2752
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
3517
2753
|
var SANDBOX_MARKER_FILE = "/.dockerenv";
|
|
3518
|
-
function runningInsideSandbox(env = process.env, fileExists =
|
|
2754
|
+
function runningInsideSandbox(env = process.env, fileExists = existsSync11) {
|
|
3519
2755
|
const override = resolveAliasedEnv(env, "EXECUTION_TARGET").value;
|
|
3520
2756
|
if (override === "local") return true;
|
|
3521
2757
|
if (override === "docker-compose") return false;
|
|
@@ -3672,10 +2908,10 @@ var LocalExecutionTarget = class {
|
|
|
3672
2908
|
this.workspace = { hostRoot: opts.projectRoot, targetRoot: opts.projectRoot };
|
|
3673
2909
|
}
|
|
3674
2910
|
async provision() {
|
|
3675
|
-
throw new HostOnlyError(
|
|
2911
|
+
throw new HostOnlyError(`\`${activeBin()} sandbox\``);
|
|
3676
2912
|
}
|
|
3677
2913
|
async destroy() {
|
|
3678
|
-
throw new HostOnlyError(
|
|
2914
|
+
throw new HostOnlyError(`\`${activeBin()} destroy\``);
|
|
3679
2915
|
}
|
|
3680
2916
|
async status() {
|
|
3681
2917
|
return "ready";
|
|
@@ -3710,398 +2946,743 @@ var LocalExecutionTarget = class {
|
|
|
3710
2946
|
env: this.childEnv(request),
|
|
3711
2947
|
...request.cwd !== void 0 ? { cwd: request.cwd } : {}
|
|
3712
2948
|
});
|
|
3713
|
-
assertSpawned(r, cmd);
|
|
3714
|
-
return r.status ?? 1;
|
|
3715
|
-
}
|
|
3716
|
-
describe() {
|
|
3717
|
-
return `local target inside the sandbox at ${this.projectRoot}`;
|
|
2949
|
+
assertSpawned(r, cmd);
|
|
2950
|
+
return r.status ?? 1;
|
|
2951
|
+
}
|
|
2952
|
+
describe() {
|
|
2953
|
+
return `local target inside the sandbox at ${this.projectRoot}`;
|
|
2954
|
+
}
|
|
2955
|
+
argvFor(request, interactive) {
|
|
2956
|
+
const argv = [...request.argv];
|
|
2957
|
+
const user = request.user;
|
|
2958
|
+
if (user === void 0) return argv;
|
|
2959
|
+
const me = this.identity();
|
|
2960
|
+
if (user === me.name) return argv;
|
|
2961
|
+
if (user === "root" && me.uid === 0) return argv;
|
|
2962
|
+
if (user === "root") {
|
|
2963
|
+
return interactive ? ["sudo", "--", ...argv] : ["sudo", "-n", "--", ...argv];
|
|
2964
|
+
}
|
|
2965
|
+
if (me.uid === 0) return ["gosu", user, ...argv];
|
|
2966
|
+
throw new Error(
|
|
2967
|
+
`cannot run as user "${user}" from user "${me.name}" inside the sandbox`
|
|
2968
|
+
);
|
|
2969
|
+
}
|
|
2970
|
+
childEnv(request) {
|
|
2971
|
+
return { ...this.env ?? process.env, ...request.env ?? {} };
|
|
2972
|
+
}
|
|
2973
|
+
};
|
|
2974
|
+
function defaultIdentity() {
|
|
2975
|
+
const info = userInfo();
|
|
2976
|
+
return { name: info.username, uid: info.uid };
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
// src/lib/execution/index.ts
|
|
2980
|
+
function resolveExecutionTarget(opts) {
|
|
2981
|
+
if (runningInsideSandbox(opts.env ?? process.env)) {
|
|
2982
|
+
return new LocalExecutionTarget({
|
|
2983
|
+
projectRoot: opts.projectRoot,
|
|
2984
|
+
...opts.run ? { run: opts.run } : {},
|
|
2985
|
+
...opts.env ? { env: opts.env } : {}
|
|
2986
|
+
});
|
|
2987
|
+
}
|
|
2988
|
+
return new DockerComposeExecutionTarget(opts);
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
// src/lib/config-render.ts
|
|
2992
|
+
var RETIRED_KEYS = [
|
|
2993
|
+
"WORKTREES_DIR",
|
|
2994
|
+
"PROJECTS_DIR",
|
|
2995
|
+
"CRONS_DIR",
|
|
2996
|
+
"OH_PROJECT_ROOT",
|
|
2997
|
+
"AGRO_PROJECT_ROOT",
|
|
2998
|
+
"INSTALL_DEEPAGENTS",
|
|
2999
|
+
"INSTALL_OPENCODE",
|
|
3000
|
+
"INSTALL_GROK_BUILD",
|
|
3001
|
+
"INSTALL_HERMES",
|
|
3002
|
+
"INSTALL_AGENT_BROWSER",
|
|
3003
|
+
"INSTALL_TAILSCALE",
|
|
3004
|
+
"SANDBOX_SSH_PASSWORD_AUTH",
|
|
3005
|
+
"SANDBOX_SSH_AUTHORIZED_KEYS",
|
|
3006
|
+
"HERMES_DASHBOARD",
|
|
3007
|
+
"HERMES_DASHBOARD_PORT",
|
|
3008
|
+
"CRON_AGENT_BIN",
|
|
3009
|
+
"SKIP_PNPM_INSTALL",
|
|
3010
|
+
"LANGFUSE_BASE_URL",
|
|
3011
|
+
"LANGFUSE_PRIVACY_PRESET"
|
|
3012
|
+
];
|
|
3013
|
+
function renderComposeVars(config) {
|
|
3014
|
+
const out = [];
|
|
3015
|
+
const put = (key, value) => {
|
|
3016
|
+
if (value === void 0) return;
|
|
3017
|
+
out.push({ key, value: String(value) });
|
|
3018
|
+
};
|
|
3019
|
+
put("SANDBOX_NAME", config.name);
|
|
3020
|
+
put("TZ", config.timezone);
|
|
3021
|
+
put("AGRO_HOME_MOUNT", config.storage?.homePath);
|
|
3022
|
+
put("AGRO_REPO_DIR", configCheckout(config));
|
|
3023
|
+
put("GIT_USER_NAME", config.git?.userName);
|
|
3024
|
+
put("GIT_USER_EMAIL", config.git?.userEmail);
|
|
3025
|
+
put("DOCKER_SOCKET", config.access?.dockerSocket);
|
|
3026
|
+
put("SANDBOX_SSH", config.access?.ssh);
|
|
3027
|
+
put("SANDBOX_SSH_PORT", config.access?.sshPort);
|
|
3028
|
+
put("AGRO_SANDBOX_IMAGE", config.image?.ref);
|
|
3029
|
+
put("AGRO_PULL_POLICY", config.image?.pullPolicy);
|
|
3030
|
+
for (const { key, value } of out) {
|
|
3031
|
+
if (isSecretKey(key)) {
|
|
3032
|
+
throw new Error(`refusing to render secret ${key} from the sandbox config`);
|
|
3033
|
+
}
|
|
3034
|
+
if (RETIRED_KEYS.includes(key)) {
|
|
3035
|
+
throw new Error(`refusing to render retired variable ${key}`);
|
|
3036
|
+
}
|
|
3037
|
+
if (/[\r\n]/.test(value)) {
|
|
3038
|
+
throw new Error(`config value for ${key} must not contain a newline`);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
return out;
|
|
3042
|
+
}
|
|
3043
|
+
function renderComposeEnv(config) {
|
|
3044
|
+
const vars = renderComposeVars(config);
|
|
3045
|
+
return vars.length === 0 ? "" : `${vars.map(({ key, value }) => `${key}=${value}`).join("\n")}
|
|
3046
|
+
`;
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
// src/commands/lifecycle.ts
|
|
3050
|
+
var COMPOSE_ENV_DIR_PREFIX = "oh-compose-env-";
|
|
3051
|
+
function composeEnvDir() {
|
|
3052
|
+
return mkdtempSync(join6(tmpdir(), COMPOSE_ENV_DIR_PREFIX));
|
|
3053
|
+
}
|
|
3054
|
+
function writeComposeEnv(root, dir) {
|
|
3055
|
+
const file = join6(dir, "compose.env");
|
|
3056
|
+
writeFileSync6(file, renderComposeEnv(readOhConfig(ohConfigPath(root))), {
|
|
3057
|
+
mode: 384,
|
|
3058
|
+
encoding: "utf8"
|
|
3059
|
+
});
|
|
3060
|
+
return file;
|
|
3061
|
+
}
|
|
3062
|
+
function withComposeEnvFile(root, fn) {
|
|
3063
|
+
if (!existsSync12(ohConfigPath(root))) return fn([]);
|
|
3064
|
+
const dir = composeEnvDir();
|
|
3065
|
+
try {
|
|
3066
|
+
return fn(["--extra-env-file", writeComposeEnv(root, dir)]);
|
|
3067
|
+
} finally {
|
|
3068
|
+
rmSync(dir, { recursive: true, force: true });
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
async function withComposeEnvFileAsync(root, fn) {
|
|
3072
|
+
if (!existsSync12(ohConfigPath(root))) return await fn([]);
|
|
3073
|
+
const dir = composeEnvDir();
|
|
3074
|
+
try {
|
|
3075
|
+
return await fn(["--extra-env-file", writeComposeEnv(root, dir)]);
|
|
3076
|
+
} finally {
|
|
3077
|
+
rmSync(dir, { recursive: true, force: true });
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
function sandboxRoot(opts) {
|
|
3081
|
+
const root = resolveSandboxRoot({ name: opts.name, cwd: opts.cwd });
|
|
3082
|
+
if (existsSync12(ohConfigPath(root))) {
|
|
3083
|
+
const repo = configuredString(root, "repo");
|
|
3084
|
+
materialize(root, repo === void 0 ? {} : { repo });
|
|
3085
|
+
}
|
|
3086
|
+
return root;
|
|
3087
|
+
}
|
|
3088
|
+
var DEFAULT_CONTAINER_NAME = DEFAULT_SANDBOX_NAME;
|
|
3089
|
+
var DEFAULT_SANDBOX_IMAGE = "ghcr.io/mifunedev/agro:latest";
|
|
3090
|
+
function configuredField(root, path4) {
|
|
3091
|
+
const file = ohConfigPath(root);
|
|
3092
|
+
if (!existsSync12(file)) return void 0;
|
|
3093
|
+
try {
|
|
3094
|
+
return getOhConfigValue(readOhConfig(file), path4);
|
|
3095
|
+
} catch {
|
|
3096
|
+
return void 0;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
function configuredString(root, path4) {
|
|
3100
|
+
const value = configuredField(root, path4);
|
|
3101
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
3102
|
+
}
|
|
3103
|
+
function fromProcessEnv(key) {
|
|
3104
|
+
const value = process.env[key];
|
|
3105
|
+
return value !== void 0 && value !== "" ? value : void 0;
|
|
3106
|
+
}
|
|
3107
|
+
function dockerSocketConfigured(root) {
|
|
3108
|
+
return configuredField(root, "access.dockerSocket") !== void 0;
|
|
3109
|
+
}
|
|
3110
|
+
async function maybePromptDockerSocket(root, io, bin) {
|
|
3111
|
+
if (dockerSocketConfigured(root)) return;
|
|
3112
|
+
const interactive = process.stdin.isTTY === true || io.ask !== void 0;
|
|
3113
|
+
if (!interactive) return;
|
|
3114
|
+
const envDir = join6(root, ".devcontainer");
|
|
3115
|
+
if (!existsSync12(envDir)) return;
|
|
3116
|
+
const askFn = io.ask ?? ask;
|
|
3117
|
+
const answer = (await askFn(
|
|
3118
|
+
"Mount host Docker socket into the sandbox? (effectively host root \u2014 enable only if the agent must drive Docker) [y/N]"
|
|
3119
|
+
)).trim().toLowerCase();
|
|
3120
|
+
const enabled = answer === "y" || answer === "yes";
|
|
3121
|
+
setEnvValue(root, "DOCKER_SOCKET", enabled ? "true" : "false", bin);
|
|
3122
|
+
io.stdout(
|
|
3123
|
+
enabled ? "DOCKER_SOCKET=true \u2014 host Docker socket will be mounted\n" : "DOCKER_SOCKET=false \u2014 host Docker socket stays unmounted\n"
|
|
3124
|
+
);
|
|
3125
|
+
}
|
|
3126
|
+
function configuredImage(root) {
|
|
3127
|
+
return aliasedEnvValue(process.env, "SANDBOX_IMAGE") ?? configuredString(root, "image.ref");
|
|
3128
|
+
}
|
|
3129
|
+
async function runSandbox(opts, io) {
|
|
3130
|
+
const run = opts.run ?? spawnRunner;
|
|
3131
|
+
const root = resolveProjectRoot(opts.cwd);
|
|
3132
|
+
const useImage = opts.image === true || opts.imageRef !== void 0;
|
|
3133
|
+
const useNoBuild = useImage || opts.noBuild === true;
|
|
3134
|
+
const imageRef = useImage ? opts.imageRef ?? configuredImage(root) ?? DEFAULT_SANDBOX_IMAGE : void 0;
|
|
3135
|
+
const env = imageRef === void 0 ? void 0 : { ...process.env, ...aliasedEnvPair("SANDBOX_IMAGE", imageRef) };
|
|
3136
|
+
if (opts.printArgv === true) {
|
|
3137
|
+
const script = requireLifecycleScript(root, "docker-compose.sh");
|
|
3138
|
+
return withComposeEnvFile(root, (extraArgs) => {
|
|
3139
|
+
const r = run(
|
|
3140
|
+
"bash",
|
|
3141
|
+
[
|
|
3142
|
+
script,
|
|
3143
|
+
"--repo-dir",
|
|
3144
|
+
root,
|
|
3145
|
+
...extraArgs,
|
|
3146
|
+
"--print-argv",
|
|
3147
|
+
"up",
|
|
3148
|
+
"-d",
|
|
3149
|
+
useNoBuild ? "--no-build" : "--build"
|
|
3150
|
+
],
|
|
3151
|
+
{ stdio: "inherit", ...env ? { env } : {} }
|
|
3152
|
+
);
|
|
3153
|
+
assertSpawned(r, `bash ${script}`);
|
|
3154
|
+
return r.status ?? 1;
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
if (runningInsideSandbox()) {
|
|
3158
|
+
io.stderr(`${new HostOnlyError(`\`${opts.bin} sandbox\``).message}
|
|
3159
|
+
`);
|
|
3160
|
+
return 1;
|
|
3161
|
+
}
|
|
3162
|
+
await maybePromptDockerSocket(root, io, opts.bin);
|
|
3163
|
+
requireLifecycleScript(root, "docker-compose.sh");
|
|
3164
|
+
if (imageRef !== void 0) {
|
|
3165
|
+
io.stdout(`image mode: ${imageRef} (skipping local build)
|
|
3166
|
+
`);
|
|
3167
|
+
} else if (useNoBuild) {
|
|
3168
|
+
io.stdout("no-build mode: reusing the existing image (skipping local build)\n");
|
|
3169
|
+
}
|
|
3170
|
+
try {
|
|
3171
|
+
await withComposeEnvFileAsync(root, async (extraArgs) => {
|
|
3172
|
+
const target = resolveExecutionTarget({
|
|
3173
|
+
projectRoot: root,
|
|
3174
|
+
run,
|
|
3175
|
+
build: !useNoBuild,
|
|
3176
|
+
...extraArgs.length > 0 ? { extraEnvFile: extraArgs[1] } : {},
|
|
3177
|
+
...env ? { env } : {}
|
|
3178
|
+
});
|
|
3179
|
+
await target.provision();
|
|
3180
|
+
});
|
|
3181
|
+
return 0;
|
|
3182
|
+
} catch (err) {
|
|
3183
|
+
if (err instanceof ExecutionExitError) return err.exitCode;
|
|
3184
|
+
if (err instanceof HostOnlyError) {
|
|
3185
|
+
io.stderr(`${err.message}
|
|
3186
|
+
`);
|
|
3187
|
+
return 1;
|
|
3188
|
+
}
|
|
3189
|
+
throw err;
|
|
3718
3190
|
}
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3191
|
+
}
|
|
3192
|
+
function configuredContainerName(root) {
|
|
3193
|
+
return fromProcessEnv("SANDBOX_NAME") ?? configuredString(root, "name");
|
|
3194
|
+
}
|
|
3195
|
+
function runShell(opts, io) {
|
|
3196
|
+
const run = opts.run ?? spawnRunner;
|
|
3197
|
+
const root = sandboxRoot(opts);
|
|
3198
|
+
const name = configuredContainerName(root) ?? DEFAULT_CONTAINER_NAME;
|
|
3199
|
+
const target = resolveExecutionTarget({ projectRoot: root, container: name, run });
|
|
3200
|
+
let code;
|
|
3201
|
+
try {
|
|
3202
|
+
code = target.attach({ argv: ["zsh"], user: "sandbox" });
|
|
3203
|
+
} catch (err) {
|
|
3204
|
+
if (err instanceof ExecutionSpawnError && err.code === "ENOENT") {
|
|
3205
|
+
throw new Error(`docker is required for \`${opts.bin} shell\` but was not found on PATH`);
|
|
3728
3206
|
}
|
|
3729
|
-
|
|
3730
|
-
throw new Error(
|
|
3731
|
-
`cannot run as user "${user}" from user "${me.name}" inside the sandbox`
|
|
3732
|
-
);
|
|
3207
|
+
throw err;
|
|
3733
3208
|
}
|
|
3734
|
-
|
|
3735
|
-
|
|
3209
|
+
if (code !== 0) {
|
|
3210
|
+
io.stderr(
|
|
3211
|
+
`container \`${name}\` not running? start it with \`${opts.bin} sandbox install docker\`
|
|
3212
|
+
`
|
|
3213
|
+
);
|
|
3736
3214
|
}
|
|
3737
|
-
|
|
3738
|
-
function defaultIdentity() {
|
|
3739
|
-
const info = userInfo();
|
|
3740
|
-
return { name: info.username, uid: info.uid };
|
|
3215
|
+
return code;
|
|
3741
3216
|
}
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
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"
|
|
3750
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 [];
|
|
3751
3255
|
}
|
|
3752
|
-
|
|
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;
|
|
3753
3266
|
}
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
"SANDBOX_SSH_PASSWORD_AUTH",
|
|
3769
|
-
"SANDBOX_SSH_AUTHORIZED_KEYS",
|
|
3770
|
-
"HERMES_DASHBOARD",
|
|
3771
|
-
"HERMES_DASHBOARD_PORT",
|
|
3772
|
-
"CRON_AGENT_BIN",
|
|
3773
|
-
"SKIP_PNPM_INSTALL",
|
|
3774
|
-
"LANGFUSE_BASE_URL",
|
|
3775
|
-
"LANGFUSE_PRIVACY_PRESET"
|
|
3776
|
-
];
|
|
3777
|
-
function renderComposeVars(config) {
|
|
3778
|
-
const out = [];
|
|
3779
|
-
const put = (key, value) => {
|
|
3780
|
-
if (value === void 0) return;
|
|
3781
|
-
out.push({ key, value: String(value) });
|
|
3782
|
-
};
|
|
3783
|
-
put("SANDBOX_NAME", config.name);
|
|
3784
|
-
put("TZ", config.timezone);
|
|
3785
|
-
put("AGRO_HOME_MOUNT", config.storage?.homePath);
|
|
3786
|
-
put("AGRO_REPO_DIR", config.repo);
|
|
3787
|
-
put("GIT_USER_NAME", config.git?.userName);
|
|
3788
|
-
put("GIT_USER_EMAIL", config.git?.userEmail);
|
|
3789
|
-
put("DOCKER_SOCKET", config.access?.dockerSocket);
|
|
3790
|
-
put("SANDBOX_SSH", config.access?.ssh);
|
|
3791
|
-
put("SANDBOX_SSH_PORT", config.access?.sshPort);
|
|
3792
|
-
put("AGRO_SANDBOX_IMAGE", config.image?.ref);
|
|
3793
|
-
put("AGRO_PULL_POLICY", config.image?.pullPolicy);
|
|
3794
|
-
for (const { key, value } of out) {
|
|
3795
|
-
if (isSecretKey(key)) {
|
|
3796
|
-
throw new Error(`refusing to render secret ${key} from the sandbox config`);
|
|
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;
|
|
3797
3281
|
}
|
|
3798
|
-
|
|
3799
|
-
|
|
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
|
+
);
|
|
3800
3309
|
}
|
|
3801
|
-
|
|
3802
|
-
|
|
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;
|
|
3803
3316
|
}
|
|
3804
3317
|
}
|
|
3805
|
-
|
|
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;
|
|
3806
3325
|
}
|
|
3807
|
-
function
|
|
3808
|
-
const
|
|
3809
|
-
|
|
3810
|
-
|
|
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;
|
|
3811
3336
|
}
|
|
3812
3337
|
|
|
3813
|
-
// src/commands/
|
|
3814
|
-
var
|
|
3815
|
-
function
|
|
3816
|
-
return
|
|
3817
|
-
}
|
|
3818
|
-
function writeComposeEnv(root, dir) {
|
|
3819
|
-
const file = join9(dir, "compose.env");
|
|
3820
|
-
writeFileSync7(file, renderComposeEnv(readOhConfig(ohConfigPath(root))), {
|
|
3821
|
-
mode: 384,
|
|
3822
|
-
encoding: "utf8"
|
|
3823
|
-
});
|
|
3824
|
-
return file;
|
|
3338
|
+
// src/commands/config.ts
|
|
3339
|
+
var HOME_PATH_FIELD = "storage.homePath";
|
|
3340
|
+
function currentHomePath(root) {
|
|
3341
|
+
return readOhConfig(ohConfigPath(root)).storage?.homePath;
|
|
3825
3342
|
}
|
|
3826
|
-
function
|
|
3827
|
-
|
|
3828
|
-
const
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
} finally {
|
|
3832
|
-
rmSync(dir, { recursive: true, force: true });
|
|
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;
|
|
3833
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;
|
|
3834
3363
|
}
|
|
3835
|
-
async function
|
|
3836
|
-
if (
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
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;
|
|
3842
3372
|
}
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3373
|
+
if (!findOhConfigField(key)) {
|
|
3374
|
+
io.stderr(`${opts.bin} config set: unknown field "${key}"
|
|
3375
|
+
|
|
3376
|
+
Fields:
|
|
3377
|
+
${configFieldList()}
|
|
3378
|
+
`);
|
|
3379
|
+
return 1;
|
|
3849
3380
|
}
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
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;
|
|
3857
3395
|
try {
|
|
3858
|
-
|
|
3859
|
-
} catch {
|
|
3860
|
-
|
|
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;
|
|
3861
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;
|
|
3862
3408
|
}
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
return
|
|
3409
|
+
var UPSTREAM_REMOTE = "openharness";
|
|
3410
|
+
function repoUrl(owner, name) {
|
|
3411
|
+
return `git@github.com:${owner}/${name}.git`;
|
|
3866
3412
|
}
|
|
3867
|
-
function
|
|
3868
|
-
|
|
3869
|
-
return value !== void 0 && value !== "" ? value : void 0;
|
|
3413
|
+
function shellLine(step) {
|
|
3414
|
+
return `${step.cmd} ${step.args.join(" ")}`;
|
|
3870
3415
|
}
|
|
3871
|
-
function
|
|
3872
|
-
return
|
|
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
|
+
];
|
|
3873
3423
|
}
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
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) {
|
|
3880
3442
|
const askFn = io.ask ?? ask;
|
|
3881
|
-
|
|
3882
|
-
"Mount host Docker socket into the sandbox? (effectively host root \u2014 enable only if the agent must drive Docker) [y/N]"
|
|
3883
|
-
)).trim().toLowerCase();
|
|
3884
|
-
const enabled = answer === "y" || answer === "yes";
|
|
3885
|
-
setEnvValue(root, "DOCKER_SOCKET", enabled ? "true" : "false");
|
|
3886
|
-
io.stdout(
|
|
3887
|
-
enabled ? "DOCKER_SOCKET=true \u2014 host Docker socket will be mounted\n" : "DOCKER_SOCKET=false \u2014 host Docker socket stays unmounted\n"
|
|
3888
|
-
);
|
|
3443
|
+
return (await askFn(question)).trim();
|
|
3889
3444
|
}
|
|
3890
|
-
function
|
|
3891
|
-
|
|
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);
|
|
3892
3449
|
}
|
|
3893
|
-
async function
|
|
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) {
|
|
3894
3460
|
const run = opts.run ?? spawnRunner;
|
|
3895
|
-
const
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
return withComposeEnvFile(root, (extraArgs) => {
|
|
3903
|
-
const r = run(
|
|
3904
|
-
"bash",
|
|
3905
|
-
[
|
|
3906
|
-
script,
|
|
3907
|
-
"--repo-dir",
|
|
3908
|
-
root,
|
|
3909
|
-
...extraArgs,
|
|
3910
|
-
"--print-argv",
|
|
3911
|
-
"up",
|
|
3912
|
-
"-d",
|
|
3913
|
-
useNoBuild ? "--no-build" : "--build"
|
|
3914
|
-
],
|
|
3915
|
-
{ stdio: "inherit", ...env ? { env } : {} }
|
|
3916
|
-
);
|
|
3917
|
-
assertSpawned(r, `bash ${script}`);
|
|
3918
|
-
return r.status ?? 1;
|
|
3919
|
-
});
|
|
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;
|
|
3920
3468
|
}
|
|
3921
|
-
|
|
3922
|
-
|
|
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}
|
|
3923
3489
|
`);
|
|
3924
3490
|
return 1;
|
|
3925
3491
|
}
|
|
3926
|
-
await
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
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}
|
|
3930
3497
|
`);
|
|
3931
|
-
|
|
3932
|
-
io.stdout("no-build mode: reusing the existing image (skipping local build)\n");
|
|
3498
|
+
return 1;
|
|
3933
3499
|
}
|
|
3500
|
+
const visibility = await askVisibility(io);
|
|
3501
|
+
const url = repoUrl(owner, repoName);
|
|
3502
|
+
const manual = manualSteps(owner, repoName, visibility);
|
|
3503
|
+
let root;
|
|
3934
3504
|
try {
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
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:");
|
|
3945
3516
|
return 0;
|
|
3946
|
-
}
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
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.
|
|
3950
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
|
+
}
|
|
3951
3578
|
return 1;
|
|
3952
3579
|
}
|
|
3953
|
-
throw err;
|
|
3954
3580
|
}
|
|
3581
|
+
io.stdout(
|
|
3582
|
+
`
|
|
3583
|
+
Done \u2014 origin is ${url}${remotes.includes("origin") && !originIsTarget ? `, upstream kept as "${UPSTREAM_REMOTE}"` : ""}.
|
|
3584
|
+
`
|
|
3585
|
+
);
|
|
3586
|
+
return 0;
|
|
3955
3587
|
}
|
|
3956
|
-
function
|
|
3957
|
-
|
|
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}`);
|
|
3958
3592
|
}
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
);
|
|
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);
|
|
3978
3611
|
}
|
|
3979
|
-
return
|
|
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;
|
|
3980
3620
|
}
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
ps: Object.freeze(["ps"]),
|
|
3986
|
-
destroy: Object.freeze(["down", "-v"])
|
|
3987
|
-
});
|
|
3988
|
-
function composeVerbs() {
|
|
3989
|
-
return Object.keys(COMPOSE_VERBS);
|
|
3621
|
+
|
|
3622
|
+
// src/commands/secret.ts
|
|
3623
|
+
function secretsRoot(opts) {
|
|
3624
|
+
return opts.sandbox === void 0 ? resolveProjectRoot(opts.cwd) : resolveSandboxRoot({ name: opts.sandbox });
|
|
3990
3625
|
}
|
|
3991
|
-
function
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
const script = requireLifecycleScript(root, "docker-compose.sh");
|
|
3995
|
-
return withComposeEnvFile(root, (extraArgs) => {
|
|
3996
|
-
const r = run("bash", [script, ...extraArgs, ...COMPOSE_VERBS[verb], ...extra], {
|
|
3997
|
-
stdio: "inherit"
|
|
3998
|
-
});
|
|
3999
|
-
assertSpawned(r, `bash ${script} ${verb}`);
|
|
4000
|
-
return r.status ?? 1;
|
|
4001
|
-
});
|
|
3626
|
+
function ignoreSecretsFile(root) {
|
|
3627
|
+
if (!existsSync14(join8(root, ".git"))) return;
|
|
3628
|
+
appendGitignoreLines(root, [".env"]);
|
|
4002
3629
|
}
|
|
4003
|
-
function
|
|
4004
|
-
|
|
4005
|
-
const root = sandboxRoot(opts);
|
|
4006
|
-
const script = requireLifecycleScript(root, "docker-compose.sh");
|
|
4007
|
-
return withComposeEnvFile(root, (extraArgs) => {
|
|
4008
|
-
const r = run("bash", [script, ...extraArgs, "config", ...extra], { stdio: "inherit" });
|
|
4009
|
-
assertSpawned(r, `bash ${script} config`);
|
|
4010
|
-
return r.status ?? 1;
|
|
4011
|
-
});
|
|
3630
|
+
function secretKeyList() {
|
|
3631
|
+
return SECRET_KEYS.map((key) => ` ${key}`).join("\n");
|
|
4012
3632
|
}
|
|
4013
|
-
function
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
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.
|
|
3638
|
+
|
|
3639
|
+
Keys:
|
|
3640
|
+
${secretKeyList()}
|
|
3641
|
+
`
|
|
3642
|
+
);
|
|
3643
|
+
return 1;
|
|
4019
3644
|
}
|
|
4020
|
-
const
|
|
4021
|
-
const
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
if (match) names.push(match[1]);
|
|
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
|
|
3650
|
+
`);
|
|
3651
|
+
return 1;
|
|
4028
3652
|
}
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
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;
|
|
4033
3664
|
}
|
|
4034
|
-
async function
|
|
4035
|
-
const root =
|
|
4036
|
-
const
|
|
4037
|
-
if (
|
|
4038
|
-
|
|
4039
|
-
if (!interactive) {
|
|
4040
|
-
io.stderr(
|
|
4041
|
-
`oh destroy: refusing to destroy \`${name}\` without a terminal \u2014 re-run with --yes to confirm non-interactively
|
|
4042
|
-
`
|
|
4043
|
-
);
|
|
4044
|
-
return 1;
|
|
4045
|
-
}
|
|
4046
|
-
const homePath = readOhConfig(ohConfigPath(root)).storage?.homePath;
|
|
4047
|
-
io.stdout(`
|
|
4048
|
-
${bold(`oh destroy \u2014 ${name}`)}
|
|
4049
|
-
|
|
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>\`
|
|
4050
3670
|
`);
|
|
4051
|
-
|
|
4052
|
-
io.stdout("`docker compose down -v` removes the containers.\n");
|
|
4053
|
-
io.stdout(
|
|
4054
|
-
`This sandbox keeps its home on a host bind at ${homePath}, which \`down -v\`
|
|
4055
|
-
does not touch \u2014 every agent CLI login, the gh CLI token, and the SSH keys
|
|
4056
|
-
stay there. Delete that directory yourself if you also want them gone.
|
|
4057
|
-
|
|
4058
|
-
`
|
|
4059
|
-
);
|
|
4060
|
-
} else {
|
|
4061
|
-
const volumes = namedVolumes(root).map((volume) => `${name}_${volume}`);
|
|
4062
|
-
io.stdout("`docker compose down -v` removes the containers and deletes\n");
|
|
4063
|
-
io.stdout(
|
|
4064
|
-
volumes.length > 0 ? `these named volumes with everything in them:
|
|
4065
|
-
|
|
4066
|
-
${volumes.join("\n ")}
|
|
4067
|
-
|
|
4068
|
-
` : "every named volume this project owns, with everything in them.\n\n"
|
|
4069
|
-
);
|
|
4070
|
-
io.stdout(
|
|
4071
|
-
"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"
|
|
4072
|
-
);
|
|
4073
|
-
}
|
|
4074
|
-
const askFn = io.ask ?? ask;
|
|
4075
|
-
const answer = (await askFn(`Type the sandbox name \`${name}\` to destroy it:`)).trim();
|
|
4076
|
-
if (answer !== name) {
|
|
4077
|
-
io.stderr("oh destroy: aborted \u2014 nothing was removed\n");
|
|
4078
|
-
return 1;
|
|
4079
|
-
}
|
|
3671
|
+
return 0;
|
|
4080
3672
|
}
|
|
4081
|
-
const
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
io.stdout(
|
|
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)}
|
|
4085
3677
|
`);
|
|
4086
3678
|
}
|
|
4087
|
-
return
|
|
4088
|
-
}
|
|
4089
|
-
function runGateway(args, opts) {
|
|
4090
|
-
const run = opts.run ?? spawnRunner;
|
|
4091
|
-
const root = resolveProjectRoot(opts.cwd);
|
|
4092
|
-
const script = requireLifecycleScript(root, "gateway.sh");
|
|
4093
|
-
const r = run("bash", [script, ...args], {
|
|
4094
|
-
stdio: "inherit",
|
|
4095
|
-
env: { ...process.env, ...aliasedEnvPair("PROJECT_ROOT", root) }
|
|
4096
|
-
});
|
|
4097
|
-
assertSpawned(r, `bash ${script}`);
|
|
4098
|
-
return r.status ?? 1;
|
|
3679
|
+
return 0;
|
|
4099
3680
|
}
|
|
4100
3681
|
|
|
4101
3682
|
// src/commands/sandbox.ts
|
|
4102
|
-
import { existsSync as
|
|
3683
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, rmSync as rmSync2, statSync as statSync4 } from "node:fs";
|
|
4103
3684
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
4104
|
-
import { join as
|
|
3685
|
+
import { join as join9, resolve as resolve9 } from "node:path";
|
|
4105
3686
|
|
|
4106
3687
|
// src/lib/runtimes/catalog.ts
|
|
4107
3688
|
var RUNTIME_CATALOG = Object.freeze([
|
|
@@ -4119,7 +3700,7 @@ var RUNTIME_CATALOG = Object.freeze([
|
|
|
4119
3700
|
tier: "microvm",
|
|
4120
3701
|
state: "planned",
|
|
4121
3702
|
provisionable: false,
|
|
4122
|
-
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\`.`,
|
|
4123
3704
|
docsPath: "docs/runtimes/microsandbox.md"
|
|
4124
3705
|
})
|
|
4125
3706
|
]);
|
|
@@ -4145,14 +3726,28 @@ function gitIdentity(run, key) {
|
|
|
4145
3726
|
if (result.error || result.status !== 0) return "";
|
|
4146
3727
|
return (result.stdout ?? "").trim();
|
|
4147
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
|
+
}
|
|
4148
3743
|
function readSeedConfig(repo) {
|
|
4149
3744
|
if (repo === void 0) return void 0;
|
|
4150
3745
|
const file = ohConfigPath(repo);
|
|
4151
|
-
return
|
|
3746
|
+
return existsSync15(file) ? readOhConfig(file) : void 0;
|
|
4152
3747
|
}
|
|
4153
3748
|
function readEntryConfig(name) {
|
|
4154
3749
|
const file = ohConfigPath(entryRoot(name));
|
|
4155
|
-
return
|
|
3750
|
+
return existsSync15(file) ? readOhConfig(file) : void 0;
|
|
4156
3751
|
}
|
|
4157
3752
|
function assignSetting(target, path4, value) {
|
|
4158
3753
|
const segments = path4.split(".");
|
|
@@ -4182,11 +3777,11 @@ function mergeSettings(...sources) {
|
|
|
4182
3777
|
function nonEmpty(value) {
|
|
4183
3778
|
return value === void 0 || value === "" ? void 0 : value;
|
|
4184
3779
|
}
|
|
4185
|
-
function seedConfig(name,
|
|
3780
|
+
function seedConfig(name, checkout, seed, run) {
|
|
4186
3781
|
const config = overlaySettings(defaultOhConfig(name), seed);
|
|
4187
3782
|
config.name = name;
|
|
4188
3783
|
config.runtime = "docker";
|
|
4189
|
-
if (
|
|
3784
|
+
if (checkout !== void 0) config.checkout = checkout;
|
|
4190
3785
|
config.timezone = nonEmpty(seed?.timezone) ?? hostTimezone();
|
|
4191
3786
|
config.git = {
|
|
4192
3787
|
userName: nonEmpty(seed?.git?.userName) ?? gitIdentity(run, "user.name"),
|
|
@@ -4194,7 +3789,7 @@ function seedConfig(name, repo, seed, run) {
|
|
|
4194
3789
|
};
|
|
4195
3790
|
config.image = {
|
|
4196
3791
|
...config.image,
|
|
4197
|
-
mode: seed?.image?.mode ?? (config
|
|
3792
|
+
mode: seed?.image?.mode ?? (isBuildCapable(configCheckout(config)) ? "build" : "image")
|
|
4198
3793
|
};
|
|
4199
3794
|
return config;
|
|
4200
3795
|
}
|
|
@@ -4234,58 +3829,99 @@ async function runWizard(config, io) {
|
|
|
4234
3829
|
config.access?.dockerSocket === true
|
|
4235
3830
|
);
|
|
4236
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) };
|
|
4237
3838
|
}
|
|
4238
3839
|
async function runSandboxInstall(opts, io) {
|
|
4239
3840
|
const run = opts.run ?? spawnRunner;
|
|
4240
3841
|
const runtime = findRuntime(opts.runtime);
|
|
4241
3842
|
if (runtime === void 0) {
|
|
4242
3843
|
io.stderr(
|
|
4243
|
-
|
|
3844
|
+
`${opts.bin} sandbox install: unknown runtime "${opts.runtime}" \u2014 known runtimes: ${runtimeIds().join(", ")}
|
|
4244
3845
|
`
|
|
4245
3846
|
);
|
|
4246
3847
|
return 1;
|
|
4247
3848
|
}
|
|
4248
3849
|
if (!runtime.provisionable) {
|
|
4249
|
-
io.stderr(
|
|
3850
|
+
io.stderr(`${opts.bin} sandbox install: ${runtime.notProvisionableReason?.(opts.bin) ?? ""}
|
|
4250
3851
|
`);
|
|
4251
3852
|
return 1;
|
|
4252
3853
|
}
|
|
4253
|
-
const
|
|
4254
|
-
if (
|
|
4255
|
-
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}
|
|
4256
3857
|
`);
|
|
4257
3858
|
return 1;
|
|
4258
3859
|
}
|
|
4259
|
-
const repoSeed = readSeedConfig(
|
|
3860
|
+
const repoSeed = readSeedConfig(checkout);
|
|
4260
3861
|
const name = opts.name ?? repoSeed?.name ?? nextDefaultName(run);
|
|
4261
3862
|
try {
|
|
4262
3863
|
assertSandboxName(name);
|
|
4263
3864
|
} catch (error) {
|
|
4264
|
-
io.stderr(
|
|
3865
|
+
io.stderr(`${opts.bin} sandbox install: ${error instanceof Error ? error.message : String(error)}
|
|
4265
3866
|
`);
|
|
4266
3867
|
return 1;
|
|
4267
3868
|
}
|
|
4268
|
-
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
|
+
}
|
|
4269
3888
|
const interactive = opts.yes !== true && (process.stdin.isTTY === true || io.ask !== void 0);
|
|
3889
|
+
const homePathBeforeWizard = config.storage?.homePath;
|
|
4270
3890
|
if (interactive) {
|
|
4271
3891
|
header("Configure the sandbox (press Enter to accept the shown default)");
|
|
4272
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
|
+
}
|
|
4273
3905
|
}
|
|
4274
3906
|
if (opts.imageRef !== void 0) {
|
|
4275
3907
|
config.image = { ...config.image, ref: opts.imageRef, mode: "image" };
|
|
4276
3908
|
}
|
|
4277
|
-
|
|
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");
|
|
4278
3913
|
const sandboxOpts = {
|
|
3914
|
+
bin: opts.bin,
|
|
4279
3915
|
run,
|
|
4280
3916
|
...opts.image === true ? { image: true } : {},
|
|
4281
3917
|
...opts.imageRef !== void 0 ? { imageRef: opts.imageRef } : {},
|
|
4282
3918
|
...useNoBuild ? { noBuild: true } : {}
|
|
4283
3919
|
};
|
|
4284
3920
|
if (opts.printArgv === true) {
|
|
4285
|
-
const preview = mkdtempSync2(
|
|
3921
|
+
const preview = mkdtempSync2(join9(tmpdir2(), "oh-sandbox-preview-"));
|
|
4286
3922
|
try {
|
|
4287
3923
|
writeOhConfig(preview, config);
|
|
4288
|
-
materialize(preview, { ...config
|
|
3924
|
+
materialize(preview, { ...configCheckout(config) !== void 0 ? { checkout: configCheckout(config) } : {} });
|
|
4289
3925
|
return await runSandbox({ ...sandboxOpts, cwd: preview, printArgv: true }, io);
|
|
4290
3926
|
} finally {
|
|
4291
3927
|
rmSync2(preview, { recursive: true, force: true });
|
|
@@ -4294,9 +3930,9 @@ async function runSandboxInstall(opts, io) {
|
|
|
4294
3930
|
const root = entryRoot(config.name);
|
|
4295
3931
|
mkdirSync6(root, { recursive: true });
|
|
4296
3932
|
writeOhConfig(root, config);
|
|
4297
|
-
materialize(root, { ...config
|
|
3933
|
+
materialize(root, { ...configCheckout(config) !== void 0 ? { checkout: configCheckout(config) } : {} });
|
|
4298
3934
|
const code = await runSandbox({ ...sandboxOpts, cwd: root }, io);
|
|
4299
|
-
if (code === 0) io.stdout(`next:
|
|
3935
|
+
if (code === 0) io.stdout(`next: ${opts.bin} shell ${config.name}
|
|
4300
3936
|
`);
|
|
4301
3937
|
return code;
|
|
4302
3938
|
}
|
|
@@ -4314,10 +3950,12 @@ async function runSandboxList(opts, io) {
|
|
|
4314
3950
|
for (const name of listEntries()) {
|
|
4315
3951
|
const root = entryRoot(name);
|
|
4316
3952
|
const config = readOhConfig(ohConfigPath(root));
|
|
3953
|
+
const checkout = configCheckout(config) ?? "-";
|
|
4317
3954
|
rows.push({
|
|
4318
3955
|
name,
|
|
4319
3956
|
runtime: config.runtime ?? "docker",
|
|
4320
|
-
|
|
3957
|
+
checkout,
|
|
3958
|
+
repo: checkout,
|
|
4321
3959
|
status: await entryStatus(root, name, run)
|
|
4322
3960
|
});
|
|
4323
3961
|
}
|
|
@@ -4328,7 +3966,7 @@ async function runSandboxList(opts, io) {
|
|
|
4328
3966
|
}
|
|
4329
3967
|
if (rows.length === 0) {
|
|
4330
3968
|
io.stdout(
|
|
4331
|
-
`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\`
|
|
4332
3970
|
`
|
|
4333
3971
|
);
|
|
4334
3972
|
return 0;
|
|
@@ -4339,7 +3977,7 @@ async function runSandboxList(opts, io) {
|
|
|
4339
3977
|
const statusWidth = width((row) => row.status);
|
|
4340
3978
|
for (const row of rows) {
|
|
4341
3979
|
io.stdout(
|
|
4342
|
-
`${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}
|
|
4343
3981
|
`
|
|
4344
3982
|
);
|
|
4345
3983
|
}
|
|
@@ -4469,6 +4107,20 @@ var HARNESS_CATALOG = [
|
|
|
4469
4107
|
docsPath: "docs/harnesses/muse-code.md",
|
|
4470
4108
|
kind: "installable"
|
|
4471
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
|
+
},
|
|
4472
4124
|
{
|
|
4473
4125
|
id: "t3code",
|
|
4474
4126
|
title: "T3 Code",
|
|
@@ -4542,7 +4194,7 @@ function cell(value, absent) {
|
|
|
4542
4194
|
if (value === null) return absent;
|
|
4543
4195
|
return value ? "yes" : "no";
|
|
4544
4196
|
}
|
|
4545
|
-
function renderTable(states, io) {
|
|
4197
|
+
function renderTable(states, io, bin) {
|
|
4546
4198
|
const header2 = ["HARNESS", "KIND", "INSTALLED"];
|
|
4547
4199
|
const rows = states.map((s) => [s.id, s.kind, cell(s.installed, "?")]);
|
|
4548
4200
|
const widths = header2.map(
|
|
@@ -4552,7 +4204,9 @@ function renderTable(states, io) {
|
|
|
4552
4204
|
io.stdout(line(header2));
|
|
4553
4205
|
for (const row of rows) io.stdout(line(row));
|
|
4554
4206
|
if (states.some((s) => s.installed === null)) {
|
|
4555
|
-
io.stdout(
|
|
4207
|
+
io.stdout(`
|
|
4208
|
+
INSTALLED is \`?\` \u2014 the sandbox is not running. Start it with \`${bin} sandbox\`.
|
|
4209
|
+
`);
|
|
4556
4210
|
}
|
|
4557
4211
|
}
|
|
4558
4212
|
async function runHarnessList(opts, io) {
|
|
@@ -4563,12 +4217,12 @@ async function runHarnessList(opts, io) {
|
|
|
4563
4217
|
io.stdout(`${JSON.stringify(states, null, 2)}
|
|
4564
4218
|
`);
|
|
4565
4219
|
} else {
|
|
4566
|
-
renderTable(states, io);
|
|
4220
|
+
renderTable(states, io, opts.bin);
|
|
4567
4221
|
}
|
|
4568
4222
|
return 0;
|
|
4569
4223
|
}
|
|
4570
|
-
function unknownHarness(name, io) {
|
|
4571
|
-
io.stderr(
|
|
4224
|
+
function unknownHarness(name, io, bin) {
|
|
4225
|
+
io.stderr(`${bin} harness: unknown harness "${name}"
|
|
4572
4226
|
|
|
4573
4227
|
`);
|
|
4574
4228
|
io.stderr(`Known harnesses:
|
|
@@ -4582,21 +4236,21 @@ async function runHarnessStatus(name, opts, io) {
|
|
|
4582
4236
|
let only;
|
|
4583
4237
|
if (name !== void 0) {
|
|
4584
4238
|
only = findHarness(name);
|
|
4585
|
-
if (!only) return unknownHarness(name, io);
|
|
4239
|
+
if (!only) return unknownHarness(name, io, opts.bin);
|
|
4586
4240
|
}
|
|
4587
4241
|
const states = await collectStates(root, run, opts.env, only ? [only] : void 0);
|
|
4588
4242
|
if (opts.json) {
|
|
4589
4243
|
io.stdout(`${JSON.stringify(only ? states[0] : states, null, 2)}
|
|
4590
4244
|
`);
|
|
4591
4245
|
} else {
|
|
4592
|
-
renderTable(states, io);
|
|
4246
|
+
renderTable(states, io, opts.bin);
|
|
4593
4247
|
}
|
|
4594
4248
|
return 0;
|
|
4595
4249
|
}
|
|
4596
4250
|
function hermesTargetRoot(target) {
|
|
4597
4251
|
return target.kind === "docker-compose" ? "/home/sandbox/harness" : target.workspace.targetRoot;
|
|
4598
4252
|
}
|
|
4599
|
-
async function reconcileHermes(target, io) {
|
|
4253
|
+
async function reconcileHermes(target, io, bin) {
|
|
4600
4254
|
const root = hermesTargetRoot(target);
|
|
4601
4255
|
const result = await target.exec({
|
|
4602
4256
|
argv: remoteControlDirScript(root, "scripts/link-providers.sh", ["--init", "--hermes-only"]),
|
|
@@ -4605,7 +4259,7 @@ async function reconcileHermes(target, io) {
|
|
|
4605
4259
|
stdio: "inherit"
|
|
4606
4260
|
});
|
|
4607
4261
|
if (result.exitCode !== 0) {
|
|
4608
|
-
io.stderr(
|
|
4262
|
+
io.stderr(`${bin} harness: Hermes integration failed (exit ${result.exitCode}); no installation success reported.
|
|
4609
4263
|
`);
|
|
4610
4264
|
}
|
|
4611
4265
|
return result.exitCode;
|
|
@@ -4618,7 +4272,7 @@ async function runHarnessInstall(name, opts, io) {
|
|
|
4618
4272
|
name === "hermes" && runningInsideSandbox(env) && projectRoot !== void 0 ? projectRoot : opts.cwd
|
|
4619
4273
|
);
|
|
4620
4274
|
const entry = findHarness(name);
|
|
4621
|
-
if (!entry) return unknownHarness(name, io);
|
|
4275
|
+
if (!entry) return unknownHarness(name, io, opts.bin);
|
|
4622
4276
|
const target = targetFor(root, run, opts.env);
|
|
4623
4277
|
let status;
|
|
4624
4278
|
try {
|
|
@@ -4632,8 +4286,8 @@ async function runHarnessInstall(name, opts, io) {
|
|
|
4632
4286
|
}
|
|
4633
4287
|
if (!isReachable(status)) {
|
|
4634
4288
|
io.stderr(
|
|
4635
|
-
|
|
4636
|
-
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.
|
|
4637
4291
|
`
|
|
4638
4292
|
);
|
|
4639
4293
|
return 1;
|
|
@@ -4644,7 +4298,7 @@ Start it with \`oh sandbox\`, then re-run this command.
|
|
|
4644
4298
|
HERMES_HOME: `${hermesTargetRoot(target)}/.hermes`
|
|
4645
4299
|
} : void 0;
|
|
4646
4300
|
if (hermes) {
|
|
4647
|
-
const code = await reconcileHermes(target, io);
|
|
4301
|
+
const code = await reconcileHermes(target, io, opts.bin);
|
|
4648
4302
|
if (code !== 0) return code;
|
|
4649
4303
|
}
|
|
4650
4304
|
const already = await probeInstalled(target, entry, installEnv);
|
|
@@ -4666,16 +4320,17 @@ Start it with \`oh sandbox\`, then re-run this command.
|
|
|
4666
4320
|
...installEnv ? { env: installEnv } : {}
|
|
4667
4321
|
});
|
|
4668
4322
|
if (r.exitCode !== 0) {
|
|
4669
|
-
io.stderr(
|
|
4323
|
+
io.stderr(`${opts.bin} harness: installing ${entry.id} failed (exit ${r.exitCode}).
|
|
4670
4324
|
`);
|
|
4671
4325
|
return r.exitCode;
|
|
4672
4326
|
}
|
|
4673
4327
|
if (hermes) {
|
|
4674
4328
|
if (await probeInstalled(target, entry, installEnv) !== true) {
|
|
4675
|
-
io.stderr(
|
|
4329
|
+
io.stderr(`${opts.bin} harness: Hermes installation finished but executable verification failed.
|
|
4330
|
+
`);
|
|
4676
4331
|
return 1;
|
|
4677
4332
|
}
|
|
4678
|
-
const code = await reconcileHermes(target, io);
|
|
4333
|
+
const code = await reconcileHermes(target, io, opts.bin);
|
|
4679
4334
|
if (code !== 0) return code;
|
|
4680
4335
|
}
|
|
4681
4336
|
io.stdout(`${entry.id}: installed \u2014 see ${sourceDocsUrl(entry.docsPath)} for authentication
|
|
@@ -4796,7 +4451,7 @@ var TOOL_CATALOG = Object.freeze([
|
|
|
4796
4451
|
binary: "docker",
|
|
4797
4452
|
verifyArgv: Object.freeze(["bash", "-lc", "command -v docker >/dev/null"]),
|
|
4798
4453
|
versionArgv: Object.freeze(["docker", "--version"]),
|
|
4799
|
-
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.`,
|
|
4800
4455
|
docsPath: TOOLS_DOC
|
|
4801
4456
|
}),
|
|
4802
4457
|
Object.freeze({
|
|
@@ -4806,7 +4461,7 @@ var TOOL_CATALOG = Object.freeze([
|
|
|
4806
4461
|
binary: "gh",
|
|
4807
4462
|
verifyArgv: Object.freeze(["bash", "-lc", "command -v gh >/dev/null"]),
|
|
4808
4463
|
versionArgv: Object.freeze(["gh", "--version"]),
|
|
4809
|
-
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.",
|
|
4810
4465
|
docsPath: TOOLS_DOC
|
|
4811
4466
|
}),
|
|
4812
4467
|
Object.freeze({
|
|
@@ -4913,7 +4568,7 @@ function cell2(value, absent) {
|
|
|
4913
4568
|
if (value === null) return absent;
|
|
4914
4569
|
return value ? "yes" : "no";
|
|
4915
4570
|
}
|
|
4916
|
-
function renderTable2(rows, io) {
|
|
4571
|
+
function renderTable2(rows, io, bin) {
|
|
4917
4572
|
const header2 = ["TOOL", "KIND", "INSTALLED"];
|
|
4918
4573
|
const body = rows.map((r) => [r.id, r.kind, cell2(r.installed, "?")]);
|
|
4919
4574
|
const widths = header2.map(
|
|
@@ -4923,11 +4578,13 @@ function renderTable2(rows, io) {
|
|
|
4923
4578
|
io.stdout(line(header2));
|
|
4924
4579
|
for (const row of body) io.stdout(line(row));
|
|
4925
4580
|
if (rows.some((r) => r.installed === null)) {
|
|
4926
|
-
io.stdout(
|
|
4581
|
+
io.stdout(`
|
|
4582
|
+
INSTALLED is \`?\` \u2014 the sandbox is not running. Start it with \`${bin} sandbox\`.
|
|
4583
|
+
`);
|
|
4927
4584
|
}
|
|
4928
4585
|
}
|
|
4929
|
-
function renderDetail(rows, io) {
|
|
4930
|
-
renderTable2(rows, io);
|
|
4586
|
+
function renderDetail(rows, io, bin) {
|
|
4587
|
+
renderTable2(rows, io, bin);
|
|
4931
4588
|
for (const r of rows) {
|
|
4932
4589
|
io.stdout(`
|
|
4933
4590
|
${r.id} \u2014 ${r.title}
|
|
@@ -4948,12 +4605,12 @@ async function runToolList(opts, io) {
|
|
|
4948
4605
|
io.stdout(`${JSON.stringify(rows, null, 2)}
|
|
4949
4606
|
`);
|
|
4950
4607
|
} else {
|
|
4951
|
-
renderTable2(rows, io);
|
|
4608
|
+
renderTable2(rows, io, opts.bin);
|
|
4952
4609
|
}
|
|
4953
4610
|
return 0;
|
|
4954
4611
|
}
|
|
4955
|
-
function unknownTool(name, io) {
|
|
4956
|
-
io.stderr(
|
|
4612
|
+
function unknownTool(name, io, bin) {
|
|
4613
|
+
io.stderr(`${bin} tool: unknown tool "${name}"
|
|
4957
4614
|
|
|
4958
4615
|
`);
|
|
4959
4616
|
io.stderr(`Known tools:
|
|
@@ -4967,14 +4624,14 @@ async function runToolStatus(name, opts, io) {
|
|
|
4967
4624
|
let only;
|
|
4968
4625
|
if (name !== void 0) {
|
|
4969
4626
|
only = findTool(name);
|
|
4970
|
-
if (!only) return unknownTool(name, io);
|
|
4627
|
+
if (!only) return unknownTool(name, io, opts.bin);
|
|
4971
4628
|
}
|
|
4972
4629
|
const rows = await collectRows(root, run, opts.env, only ? [only] : void 0);
|
|
4973
4630
|
if (opts.json) {
|
|
4974
4631
|
io.stdout(`${JSON.stringify(only ? rows[0] : rows, null, 2)}
|
|
4975
4632
|
`);
|
|
4976
4633
|
} else {
|
|
4977
|
-
renderDetail(rows, io);
|
|
4634
|
+
renderDetail(rows, io, opts.bin);
|
|
4978
4635
|
}
|
|
4979
4636
|
return 0;
|
|
4980
4637
|
}
|
|
@@ -4995,12 +4652,12 @@ async function runToolInstall(name, opts, io) {
|
|
|
4995
4652
|
const run = opts.run ?? spawnRunner;
|
|
4996
4653
|
const root = resolveProjectRoot(opts.cwd);
|
|
4997
4654
|
const entry = findTool(name);
|
|
4998
|
-
if (!entry) return unknownTool(name, io);
|
|
4655
|
+
if (!entry) return unknownTool(name, io, opts.bin);
|
|
4999
4656
|
if (entry.installArgv === void 0) {
|
|
5000
|
-
io.stderr(
|
|
4657
|
+
io.stderr(`${opts.bin} tool: ${entry.id} cannot be installed by this command.
|
|
5001
4658
|
|
|
5002
4659
|
`);
|
|
5003
|
-
io.stderr(`${entry.notInstallableReason ?? ""}
|
|
4660
|
+
io.stderr(`${entry.notInstallableReason?.(opts.bin) ?? ""}
|
|
5004
4661
|
|
|
5005
4662
|
`);
|
|
5006
4663
|
io.stderr(`Installable tools:
|
|
@@ -5021,8 +4678,8 @@ ${installableToolIds().map((t) => ` ${t}`).join("\n")}
|
|
|
5021
4678
|
}
|
|
5022
4679
|
if (!isReachable2(status)) {
|
|
5023
4680
|
io.stderr(
|
|
5024
|
-
|
|
5025
|
-
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.
|
|
5026
4683
|
`
|
|
5027
4684
|
);
|
|
5028
4685
|
return 1;
|
|
@@ -5046,7 +4703,7 @@ Start it with \`oh sandbox\`, then re-run this command.
|
|
|
5046
4703
|
stdio: "inherit"
|
|
5047
4704
|
});
|
|
5048
4705
|
if (r.exitCode !== 0) {
|
|
5049
|
-
io.stderr(
|
|
4706
|
+
io.stderr(`${opts.bin} tool: installing ${entry.id} failed (exit ${r.exitCode}).
|
|
5050
4707
|
`);
|
|
5051
4708
|
return r.exitCode;
|
|
5052
4709
|
}
|
|
@@ -5119,7 +4776,7 @@ function fetchRemoteSource(opts) {
|
|
|
5119
4776
|
}
|
|
5120
4777
|
|
|
5121
4778
|
// src/cli.ts
|
|
5122
|
-
var VERSION = true ? "0.
|
|
4779
|
+
var VERSION = true ? "0.11.0" : "0.0.0-dev";
|
|
5123
4780
|
var DEFAULT_SOURCE_OH_DIR = resolve10(
|
|
5124
4781
|
dirname8(fileURLToPath(import.meta.url)),
|
|
5125
4782
|
"../.."
|
|
@@ -5163,7 +4820,6 @@ Usage:
|
|
|
5163
4820
|
${bin} harness <args...> Install and inspect agent CLI harnesses
|
|
5164
4821
|
${bin} tool <args...> Install and inspect sandbox tooling
|
|
5165
4822
|
${bin} gateway <args...> Manage a messaging client session (pi|hermes)
|
|
5166
|
-
${bin} cloud <args...> Manage OpenHarness Cloud nodes
|
|
5167
4823
|
${bin} --version Print version
|
|
5168
4824
|
${bin} --help Show this help
|
|
5169
4825
|
|
|
@@ -5176,7 +4832,7 @@ function printConfigHelp(bin = LEGACY_PRODUCT.bin) {
|
|
|
5176
4832
|
|
|
5177
4833
|
Usage:
|
|
5178
4834
|
${bin} config show [--sandbox <name>]
|
|
5179
|
-
${bin} config set <field> <value> [--sandbox <name>]
|
|
4835
|
+
${bin} config set <field> <value> [--sandbox <name>] [--force]
|
|
5180
4836
|
${bin} config repo Create your own GitHub repo and point origin at it
|
|
5181
4837
|
${bin} config <integration> Run an integration wizard
|
|
5182
4838
|
${bin} config <integration> --help
|
|
@@ -5187,6 +4843,10 @@ change with \`${bin} stop <name> && ${bin} sandbox install docker --name <name>\
|
|
|
5187
4843
|
reference:
|
|
5188
4844
|
${sourceDocsUrl("docs/configuration.md")}
|
|
5189
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
|
+
|
|
5190
4850
|
Fields:
|
|
5191
4851
|
${configFieldList()}
|
|
5192
4852
|
|
|
@@ -5287,24 +4947,34 @@ function printSandboxHelp(bin = LEGACY_PRODUCT.bin) {
|
|
|
5287
4947
|
process.stdout.write(`${bin} sandbox \u2014 Create and list sandboxes
|
|
5288
4948
|
|
|
5289
4949
|
Usage:
|
|
5290
|
-
${bin} sandbox install <runtime> [--name <name>] [--
|
|
4950
|
+
${bin} sandbox install <runtime> [--name <name>] [--checkout <dir>]
|
|
4951
|
+
[--home-mount <dir>] [--yes]
|
|
5291
4952
|
[--image[=<ref>]] [--no-build] [--print-argv]
|
|
5292
4953
|
${bin} sandbox list [--json]
|
|
5293
4954
|
|
|
5294
4955
|
\`install\` writes a sandbox entry under \${${stateNames(bin).envPrefix}HOME:-~/${stateNames(bin).userStateDir}}/sandboxes/<name>/,
|
|
5295
4956
|
materialises the compose files and the compose wrapper into it, then starts the
|
|
5296
4957
|
container. It needs no project checkout and runs from any directory. Without
|
|
5297
|
-
--
|
|
5298
|
-
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.
|
|
5299
4961
|
|
|
5300
4962
|
On a terminal without --yes it asks for the sandbox name, the timezone, the git
|
|
5301
|
-
identity, SSH (with its host port),
|
|
5302
|
-
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}.
|
|
5303
4966
|
|
|
5304
4967
|
Flags:
|
|
5305
4968
|
--name <name> Registry entry name (default: the lowest free ${DEFAULT_NAME_PREFIX}<n>)
|
|
5306
|
-
--
|
|
5307
|
-
from its ${stateNames(bin).configFile}
|
|
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
|
|
5308
4978
|
--yes Non-interactive: keep every default and ask nothing
|
|
5309
4979
|
--image[=<ref>] Run the prebuilt image instead of building (implies
|
|
5310
4980
|
--no-build). Ref resolves last-wins: --image=<ref> >
|
|
@@ -5482,11 +5152,13 @@ var CONFIG_VERBS = ["show", "set", "repo"];
|
|
|
5482
5152
|
function parseConfigArgs(input, bin = LEGACY_PRODUCT.bin) {
|
|
5483
5153
|
const scoped = extractSandboxFlag(`${bin} config`, input);
|
|
5484
5154
|
if (!scoped.ok) return scoped;
|
|
5485
|
-
const
|
|
5155
|
+
const forced = scoped.args.rest.includes("--force");
|
|
5156
|
+
const rest = scoped.args.rest.filter((arg) => arg !== "--force");
|
|
5486
5157
|
const args = {
|
|
5487
5158
|
help: false,
|
|
5488
5159
|
integrationHelp: false,
|
|
5489
|
-
...scoped.args.sandbox === void 0 ? {} : { sandbox: scoped.args.sandbox }
|
|
5160
|
+
...scoped.args.sandbox === void 0 ? {} : { sandbox: scoped.args.sandbox },
|
|
5161
|
+
...forced ? { force: true } : {}
|
|
5490
5162
|
};
|
|
5491
5163
|
if (rest.length === 0 || isHelpFlag(rest[0])) {
|
|
5492
5164
|
return { ok: true, args: { ...args, help: true } };
|
|
@@ -5673,7 +5345,9 @@ function parseUpdateArgs(rest, bin = LEGACY_PRODUCT.bin) {
|
|
|
5673
5345
|
}
|
|
5674
5346
|
var SANDBOX_VALUE_FLAGS = {
|
|
5675
5347
|
"--name": "name",
|
|
5676
|
-
"--
|
|
5348
|
+
"--checkout": "checkout",
|
|
5349
|
+
"--repo": "checkout",
|
|
5350
|
+
"--home-mount": "homeMount"
|
|
5677
5351
|
};
|
|
5678
5352
|
function parseSandboxArgs(rest, bin = LEGACY_PRODUCT.bin) {
|
|
5679
5353
|
const args = {
|
|
@@ -5698,6 +5372,7 @@ function parseSandboxArgs(rest, bin = LEGACY_PRODUCT.bin) {
|
|
|
5698
5372
|
args.subcommand = head;
|
|
5699
5373
|
if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
|
|
5700
5374
|
const positionals = [];
|
|
5375
|
+
const checkoutSpellings = /* @__PURE__ */ new Set();
|
|
5701
5376
|
for (let i = 0; i < tail.length; i++) {
|
|
5702
5377
|
const token = tail[i];
|
|
5703
5378
|
const valueFlag = SANDBOX_VALUE_FLAGS[token];
|
|
@@ -5706,6 +5381,7 @@ function parseSandboxArgs(rest, bin = LEGACY_PRODUCT.bin) {
|
|
|
5706
5381
|
if (value === void 0) {
|
|
5707
5382
|
return { ok: false, error: `${bin} sandbox ${head}: ${token} requires a value` };
|
|
5708
5383
|
}
|
|
5384
|
+
if (valueFlag === "checkout") checkoutSpellings.add(token);
|
|
5709
5385
|
args[valueFlag] = value;
|
|
5710
5386
|
i++;
|
|
5711
5387
|
} else if (token === "--yes") {
|
|
@@ -5731,6 +5407,12 @@ function parseSandboxArgs(rest, bin = LEGACY_PRODUCT.bin) {
|
|
|
5731
5407
|
positionals.push(token);
|
|
5732
5408
|
}
|
|
5733
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
|
+
}
|
|
5734
5416
|
if (head === "list") {
|
|
5735
5417
|
if (positionals.length > 0) {
|
|
5736
5418
|
return { ok: false, error: `${bin} sandbox list: unexpected argument "${positionals[0]}"` };
|
|
@@ -5843,11 +5525,11 @@ function parseGatewayArgs(rest) {
|
|
|
5843
5525
|
}
|
|
5844
5526
|
return { ok: true, args: { help: false, passthrough: [...rest] } };
|
|
5845
5527
|
}
|
|
5846
|
-
function bundledPayloadExists(bundled, exists =
|
|
5847
|
-
return exists(
|
|
5528
|
+
function bundledPayloadExists(bundled, exists = existsSync16) {
|
|
5529
|
+
return exists(join10(bundled.sourceOhDir, "manifest.json"));
|
|
5848
5530
|
}
|
|
5849
5531
|
function resolveUpdateSource(args, bundled, bin = LEGACY_PRODUCT.bin) {
|
|
5850
|
-
const exists = bundled.exists ??
|
|
5532
|
+
const exists = bundled.exists ?? existsSync16;
|
|
5851
5533
|
if (args.fromDir !== void 0) {
|
|
5852
5534
|
return { kind: "local", fromDir: resolve10(args.fromDir) };
|
|
5853
5535
|
}
|
|
@@ -5867,7 +5549,7 @@ function resolveUpdateSource(args, bundled, bin = LEGACY_PRODUCT.bin) {
|
|
|
5867
5549
|
function readPayloadVersion(checkoutDir) {
|
|
5868
5550
|
try {
|
|
5869
5551
|
const parsed = JSON.parse(
|
|
5870
|
-
|
|
5552
|
+
readFileSync11(join10(resolveControlDir(checkoutDir).path, "cli", "package.json"), "utf8")
|
|
5871
5553
|
);
|
|
5872
5554
|
if (parsed && typeof parsed.version === "string") return parsed.version;
|
|
5873
5555
|
} catch {
|
|
@@ -5876,7 +5558,7 @@ function readPayloadVersion(checkoutDir) {
|
|
|
5876
5558
|
}
|
|
5877
5559
|
async function runWithRemoteSource(hooks, fn) {
|
|
5878
5560
|
const fetch2 = hooks.fetch ?? fetchRemoteSource;
|
|
5879
|
-
const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync3(
|
|
5561
|
+
const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync3(join10(tmpdir3(), "oh-remote-")));
|
|
5880
5562
|
const rm = hooks.rm ?? ((dir) => rmSync3(dir, { recursive: true, force: true }));
|
|
5881
5563
|
const out = hooks.stdout ?? ((s) => process.stdout.write(s));
|
|
5882
5564
|
const checkoutDir = mkdtemp();
|
|
@@ -5920,9 +5602,13 @@ async function main(argv) {
|
|
|
5920
5602
|
stdout: (s) => process.stdout.write(s),
|
|
5921
5603
|
stderr: (s) => process.stderr.write(s)
|
|
5922
5604
|
};
|
|
5923
|
-
const scope =
|
|
5605
|
+
const scope = {
|
|
5606
|
+
bin,
|
|
5607
|
+
...a.sandbox === void 0 ? {} : { sandbox: a.sandbox },
|
|
5608
|
+
...a.force === true ? { force: true } : {}
|
|
5609
|
+
};
|
|
5924
5610
|
if (a.verb === "show") return await runConfigShow(scope, io);
|
|
5925
|
-
if (a.verb === "repo") return await runConfigRepo({}, io);
|
|
5611
|
+
if (a.verb === "repo") return await runConfigRepo({ bin }, io);
|
|
5926
5612
|
return await runConfigSet(a.key, a.value, scope, io);
|
|
5927
5613
|
}
|
|
5928
5614
|
const name = a.integration;
|
|
@@ -5957,7 +5643,7 @@ async function main(argv) {
|
|
|
5957
5643
|
stdout: (s) => process.stdout.write(s),
|
|
5958
5644
|
stderr: (s) => process.stderr.write(s)
|
|
5959
5645
|
};
|
|
5960
|
-
const scope = a.sandbox === void 0 ? {} : { sandbox: a.sandbox };
|
|
5646
|
+
const scope = a.sandbox === void 0 ? { bin } : { bin, sandbox: a.sandbox };
|
|
5961
5647
|
if (a.verb === "list") return await runSecretList(scope, io);
|
|
5962
5648
|
return await runSecretSet(a.key, scope, io);
|
|
5963
5649
|
}
|
|
@@ -6002,12 +5688,12 @@ async function main(argv) {
|
|
|
6002
5688
|
const targetDir = process.cwd();
|
|
6003
5689
|
const source = resolveUpdateSource(parsed.args, { sourceOhDir: DEFAULT_SOURCE_OH_DIR }, bin);
|
|
6004
5690
|
if (source.kind === "local") {
|
|
6005
|
-
return await runUpdate({ targetDir, fromDir: source.fromDir, force, dryRun }, io);
|
|
5691
|
+
return await runUpdate({ bin, targetDir, fromDir: source.fromDir, force, dryRun }, io);
|
|
6006
5692
|
}
|
|
6007
5693
|
if (source.notice) process.stdout.write(source.notice);
|
|
6008
5694
|
return await runWithRemoteSource(
|
|
6009
5695
|
{ ref: source.ref },
|
|
6010
|
-
(checkoutDir) => runUpdate({ targetDir, fromDir: checkoutDir, force, dryRun }, io)
|
|
5696
|
+
(checkoutDir) => runUpdate({ bin, targetDir, fromDir: checkoutDir, force, dryRun }, io)
|
|
6011
5697
|
);
|
|
6012
5698
|
}
|
|
6013
5699
|
if (first === "sandbox") {
|
|
@@ -6024,12 +5710,14 @@ async function main(argv) {
|
|
|
6024
5710
|
return a.subcommand === void 0 ? 1 : 0;
|
|
6025
5711
|
}
|
|
6026
5712
|
const io = lifecycleIo();
|
|
6027
|
-
if (a.subcommand === "list") return await runSandboxList({ json: a.json }, io);
|
|
5713
|
+
if (a.subcommand === "list") return await runSandboxList({ bin, json: a.json }, io);
|
|
6028
5714
|
return await runSandboxInstall(
|
|
6029
5715
|
{
|
|
5716
|
+
bin,
|
|
6030
5717
|
runtime: a.runtime,
|
|
6031
5718
|
...a.name !== void 0 ? { name: a.name } : {},
|
|
6032
|
-
...a.
|
|
5719
|
+
...a.checkout !== void 0 ? { checkout: a.checkout } : {},
|
|
5720
|
+
...a.homeMount !== void 0 ? { homeMount: a.homeMount } : {},
|
|
6033
5721
|
yes: a.yes,
|
|
6034
5722
|
image: a.image,
|
|
6035
5723
|
...a.imageRef !== void 0 ? { imageRef: a.imageRef } : {},
|
|
@@ -6051,7 +5739,7 @@ async function main(argv) {
|
|
|
6051
5739
|
return 0;
|
|
6052
5740
|
}
|
|
6053
5741
|
return runShell(
|
|
6054
|
-
parsed.args.name === void 0 ? {} : { name: parsed.args.name },
|
|
5742
|
+
parsed.args.name === void 0 ? { bin } : { bin, name: parsed.args.name },
|
|
6055
5743
|
lifecycleIo()
|
|
6056
5744
|
);
|
|
6057
5745
|
}
|
|
@@ -6068,6 +5756,7 @@ async function main(argv) {
|
|
|
6068
5756
|
}
|
|
6069
5757
|
return await runDestroy(
|
|
6070
5758
|
{
|
|
5759
|
+
bin,
|
|
6071
5760
|
yes: parsed.args.yes,
|
|
6072
5761
|
...parsed.args.name !== void 0 ? { name: parsed.args.name } : {}
|
|
6073
5762
|
},
|
|
@@ -6086,7 +5775,7 @@ async function main(argv) {
|
|
|
6086
5775
|
printComposeHelp(bin);
|
|
6087
5776
|
return 0;
|
|
6088
5777
|
}
|
|
6089
|
-
return runComposeConfig({}, parsed.args.passthrough);
|
|
5778
|
+
return runComposeConfig({ bin }, parsed.args.passthrough);
|
|
6090
5779
|
}
|
|
6091
5780
|
if (composeVerbs().includes(first)) {
|
|
6092
5781
|
const verb = first;
|
|
@@ -6110,7 +5799,7 @@ async function main(argv) {
|
|
|
6110
5799
|
);
|
|
6111
5800
|
return 1;
|
|
6112
5801
|
}
|
|
6113
|
-
return runComposeVerb(verb, head.length === 0 ? {} : { name: head[0] }, extra);
|
|
5802
|
+
return runComposeVerb(verb, head.length === 0 ? { bin } : { bin, name: head[0] }, extra);
|
|
6114
5803
|
}
|
|
6115
5804
|
if (first === "harness") {
|
|
6116
5805
|
const parsed = parseHarnessArgs(argv.slice(1), bin);
|
|
@@ -6130,12 +5819,12 @@ async function main(argv) {
|
|
|
6130
5819
|
stderr: (s) => process.stderr.write(s)
|
|
6131
5820
|
};
|
|
6132
5821
|
if (a.subcommand === "list") {
|
|
6133
|
-
return await runHarnessList({ json: a.json }, io);
|
|
5822
|
+
return await runHarnessList({ bin, json: a.json }, io);
|
|
6134
5823
|
}
|
|
6135
5824
|
if (a.subcommand === "status") {
|
|
6136
|
-
return await runHarnessStatus(a.name, { json: a.json }, io);
|
|
5825
|
+
return await runHarnessStatus(a.name, { bin, json: a.json }, io);
|
|
6137
5826
|
}
|
|
6138
|
-
return await runHarnessInstall(a.name, {}, io);
|
|
5827
|
+
return await runHarnessInstall(a.name, { bin }, io);
|
|
6139
5828
|
}
|
|
6140
5829
|
if (first === "tool") {
|
|
6141
5830
|
const parsed = parseToolArgs(argv.slice(1), bin);
|
|
@@ -6155,18 +5844,12 @@ async function main(argv) {
|
|
|
6155
5844
|
stderr: (s) => process.stderr.write(s)
|
|
6156
5845
|
};
|
|
6157
5846
|
if (a.subcommand === "list") {
|
|
6158
|
-
return await runToolList({ json: a.json }, io);
|
|
5847
|
+
return await runToolList({ bin, json: a.json }, io);
|
|
6159
5848
|
}
|
|
6160
5849
|
if (a.subcommand === "status") {
|
|
6161
|
-
return await runToolStatus(a.name, { json: a.json }, io);
|
|
5850
|
+
return await runToolStatus(a.name, { bin, json: a.json }, io);
|
|
6162
5851
|
}
|
|
6163
|
-
return await runToolInstall(a.name, { yes: a.yes }, io);
|
|
6164
|
-
}
|
|
6165
|
-
if (first === "cloud") {
|
|
6166
|
-
return await runCloud(argv.slice(1), {
|
|
6167
|
-
stdout: (s) => process.stdout.write(s),
|
|
6168
|
-
stderr: (s) => process.stderr.write(s)
|
|
6169
|
-
});
|
|
5852
|
+
return await runToolInstall(a.name, { bin, yes: a.yes }, io);
|
|
6170
5853
|
}
|
|
6171
5854
|
if (first === "gateway") {
|
|
6172
5855
|
const parsed = parseGatewayArgs(argv.slice(1));
|
|
@@ -6179,7 +5862,7 @@ async function main(argv) {
|
|
|
6179
5862
|
printGatewayHelp(bin);
|
|
6180
5863
|
return 0;
|
|
6181
5864
|
}
|
|
6182
|
-
return runGateway(parsed.args.passthrough, {});
|
|
5865
|
+
return runGateway(parsed.args.passthrough, { bin });
|
|
6183
5866
|
}
|
|
6184
5867
|
process.stderr.write(`${bin}: unknown command "${first}"
|
|
6185
5868
|
|