@tablation/crew 0.110.0 → 0.111.1
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/dist/cli.js +166 -130
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1800,6 +1800,29 @@ function answerCredential(action, input, forges, stateDir, env = process.env) {
|
|
|
1800
1800
|
password=${token}
|
|
1801
1801
|
`;
|
|
1802
1802
|
}
|
|
1803
|
+
var missesPath = (stateDir) => join9(stateDir, "forge-misses.json");
|
|
1804
|
+
function readForgeMisses(stateDir) {
|
|
1805
|
+
try {
|
|
1806
|
+
const raw = JSON.parse(readFileSync8(missesPath(stateDir), "utf8"));
|
|
1807
|
+
return Object.entries(raw).filter(([, t]) => typeof t === "string").map(([host, lastMissAt]) => ({ host, lastMissAt })).sort((a, b) => a.host.localeCompare(b.host));
|
|
1808
|
+
} catch {
|
|
1809
|
+
return [];
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
function noteForgeMiss(stateDir, host, at) {
|
|
1813
|
+
try {
|
|
1814
|
+
const bare = bareHost(host);
|
|
1815
|
+
const cur = {};
|
|
1816
|
+
for (const m of readForgeMisses(stateDir)) cur[m.host] = m.lastMissAt;
|
|
1817
|
+
if (at === null) {
|
|
1818
|
+
if (!(bare in cur)) return;
|
|
1819
|
+
delete cur[bare];
|
|
1820
|
+
} else cur[bare] = at;
|
|
1821
|
+
mkdirSync5(stateDir, { recursive: true });
|
|
1822
|
+
writeFileSync6(missesPath(stateDir), JSON.stringify(cur), { mode: 384 });
|
|
1823
|
+
} catch {
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1803
1826
|
function gitCredentialEnv(o, base = process.env) {
|
|
1804
1827
|
const start = Number.parseInt(base.GIT_CONFIG_COUNT ?? "0", 10) || 0;
|
|
1805
1828
|
const helper = `!${o.crewCommand} credential`;
|
|
@@ -12831,6 +12854,7 @@ async function runPoolMember(deps) {
|
|
|
12831
12854
|
body.lastRunEndedAt = act.lastRunEndedAt;
|
|
12832
12855
|
}
|
|
12833
12856
|
if (act) busy = act.activeRuns > 0;
|
|
12857
|
+
if (state === "running" && deps.missingForgeHosts) body.missingForgeHosts = deps.missingForgeHosts();
|
|
12834
12858
|
const r = await settle("status", () => deps.api.status(body));
|
|
12835
12859
|
if (!r) return "stopped";
|
|
12836
12860
|
if (r.status === 401) return "credential";
|
|
@@ -12957,7 +12981,7 @@ async function runPoolMember(deps) {
|
|
|
12957
12981
|
}
|
|
12958
12982
|
if (ans.status === 204) {
|
|
12959
12983
|
const url = current ? deps.relayUrl() : null;
|
|
12960
|
-
if (current && (url !== relayReported && url || deps.activity)) {
|
|
12984
|
+
if (current && (url !== relayReported && url || deps.activity || deps.missingForgeHosts)) {
|
|
12961
12985
|
const r2 = await report("running", { leaseId: current });
|
|
12962
12986
|
if (r2 === "credential") return POOL_EXIT.credential;
|
|
12963
12987
|
relayReported = url ?? relayReported;
|
|
@@ -14034,6 +14058,10 @@ function realPoolMemberDeps(o) {
|
|
|
14034
14058
|
}
|
|
14035
14059
|
return null;
|
|
14036
14060
|
},
|
|
14061
|
+
missingForgeHosts() {
|
|
14062
|
+
const cfg = loadShip(o.crewHome);
|
|
14063
|
+
return cfg ? readForgeMisses(cfg.ship.stateDir) : [];
|
|
14064
|
+
},
|
|
14037
14065
|
activity() {
|
|
14038
14066
|
const cfg = loadShip(o.crewHome);
|
|
14039
14067
|
return cfg ? runActivity(cfg.ship.stateDir) : { activeRuns: 0, lastRunEndedAt: null };
|
|
@@ -17632,7 +17660,12 @@ function formatProjectsReport(report) {
|
|
|
17632
17660
|
import { parseDocument as parseDocument4 } from "yaml";
|
|
17633
17661
|
function runCredential(action, stdin, forges, stateDir, env = process.env) {
|
|
17634
17662
|
try {
|
|
17635
|
-
|
|
17663
|
+
const input = parseCredentialInput(stdin);
|
|
17664
|
+
const out = answerCredential(action ?? "", input, forges, stateDir, env);
|
|
17665
|
+
if (action === "get" && input.host && (!input.protocol || input.protocol === "https")) {
|
|
17666
|
+
noteForgeMiss(stateDir, input.host, out ? null : (/* @__PURE__ */ new Date()).toISOString());
|
|
17667
|
+
}
|
|
17668
|
+
return { code: 0, out, err: "" };
|
|
17636
17669
|
} catch (e) {
|
|
17637
17670
|
return { code: 0, out: "", err: `crew credential: ${e.message}
|
|
17638
17671
|
` };
|
|
@@ -26485,35 +26518,141 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
26485
26518
|
break;
|
|
26486
26519
|
}
|
|
26487
26520
|
case "sandbox": {
|
|
26488
|
-
|
|
26489
|
-
|
|
26490
|
-
|
|
26491
|
-
|
|
26492
|
-
|
|
26493
|
-
|
|
26494
|
-
if (wanted.length !== 1) {
|
|
26495
|
-
process.stderr.write(`crew sandbox policy: ${only ? `no route "${only}"` : "name the route with --route"} (have: ${cfg.routes.map((r2) => r2.route).join(", ")})
|
|
26521
|
+
switch (positional[1]) {
|
|
26522
|
+
case "policy": {
|
|
26523
|
+
const only = value("route");
|
|
26524
|
+
const wanted = only ? cfg.routes.filter((r2) => r2.route === only) : cfg.routes;
|
|
26525
|
+
if (wanted.length !== 1) {
|
|
26526
|
+
process.stderr.write(`crew sandbox policy: ${only ? `no route "${only}"` : "name the route with --route"} (have: ${cfg.routes.map((r2) => r2.route).join(", ")})
|
|
26496
26527
|
`);
|
|
26497
|
-
|
|
26498
|
-
|
|
26499
|
-
|
|
26500
|
-
|
|
26501
|
-
|
|
26528
|
+
process.exit(2);
|
|
26529
|
+
}
|
|
26530
|
+
const role = value("role") ?? "dev";
|
|
26531
|
+
if (!ROLE_NAMES.includes(role)) {
|
|
26532
|
+
process.stderr.write(`crew sandbox policy: --role must be one of ${ROLE_NAMES.join(", ")}
|
|
26502
26533
|
`);
|
|
26503
|
-
|
|
26504
|
-
|
|
26505
|
-
|
|
26506
|
-
|
|
26507
|
-
|
|
26508
|
-
|
|
26509
|
-
|
|
26510
|
-
|
|
26511
|
-
|
|
26534
|
+
process.exit(2);
|
|
26535
|
+
}
|
|
26536
|
+
const r = wanted[0];
|
|
26537
|
+
const repoName = value("repo");
|
|
26538
|
+
const seat = resolveSeat(cfg.ship, r, role, {}, repoName);
|
|
26539
|
+
const input = sandboxPolicyInputFor(r, seat.toolchain, { harnessBin: seat.toolchain.bin, repo: repoName, sandbox: cfg.ship.sandbox });
|
|
26540
|
+
try {
|
|
26541
|
+
const policy = generateSandboxPolicy({ mirrorDir: cfg.ship.sandbox?.mirrorDir, workDir: cfg.ship.sandbox?.workDir, ...input });
|
|
26542
|
+
process.stdout.write(flag("json") ? `${JSON.stringify({ route: r.route, role, input, groups: policy.groups, yaml: policy.yaml }, null, 2)}
|
|
26512
26543
|
` : policy.yaml);
|
|
26513
|
-
|
|
26514
|
-
|
|
26544
|
+
} catch (e) {
|
|
26545
|
+
process.stderr.write(`crew sandbox policy: ${e.message}
|
|
26515
26546
|
`);
|
|
26516
|
-
|
|
26547
|
+
process.exit(1);
|
|
26548
|
+
}
|
|
26549
|
+
break;
|
|
26550
|
+
}
|
|
26551
|
+
case "build":
|
|
26552
|
+
case "images": {
|
|
26553
|
+
const subS = positional[1];
|
|
26554
|
+
const jsonS = flag("json");
|
|
26555
|
+
let routesS;
|
|
26556
|
+
try {
|
|
26557
|
+
routesS = value("route") ? [findRoute(cfg, value("route"))] : cfg.routes.filter((r) => r.enabled);
|
|
26558
|
+
} catch (e) {
|
|
26559
|
+
if (e instanceof ConfigError) {
|
|
26560
|
+
process.stderr.write(`crew: ${e.message}
|
|
26561
|
+
`);
|
|
26562
|
+
process.exit(2);
|
|
26563
|
+
}
|
|
26564
|
+
throw e;
|
|
26565
|
+
}
|
|
26566
|
+
const declared = await sandboxReposOf(routesS);
|
|
26567
|
+
if (subS === "images") {
|
|
26568
|
+
const stateS = readSandboxState(cfg.ship.stateDir);
|
|
26569
|
+
const rows = declared.map((r) => {
|
|
26570
|
+
const plan = planSandbox(r.name, r.decl, r.warm);
|
|
26571
|
+
return { repo: r.name, wants: plan.tag, tags: stateS[r.name]?.tags ?? [], ...sandboxImageStatus(stateS, plan) };
|
|
26572
|
+
});
|
|
26573
|
+
if (jsonS) {
|
|
26574
|
+
process.stdout.write(`${JSON.stringify(rows, null, 2)}
|
|
26575
|
+
`);
|
|
26576
|
+
break;
|
|
26577
|
+
}
|
|
26578
|
+
if (rows.length === 0) process.stdout.write("no repository declares a sandbox: section in its .crew.yaml\n");
|
|
26579
|
+
for (const r of rows) {
|
|
26580
|
+
process.stdout.write(`${r.repo}: ${r.status} \u2014 ${r.detail}
|
|
26581
|
+
`);
|
|
26582
|
+
for (const t of r.tags) {
|
|
26583
|
+
const ageH = Math.max(0, Math.round((Date.now() - Date.parse(t.builtAt)) / 36e5));
|
|
26584
|
+
process.stdout.write(` ${t.tag} built ${t.builtAt} (${ageH}h ago)
|
|
26585
|
+
`);
|
|
26586
|
+
}
|
|
26587
|
+
}
|
|
26588
|
+
break;
|
|
26589
|
+
}
|
|
26590
|
+
const only = positional[2];
|
|
26591
|
+
const targets = only ? declared.filter((r) => r.name === only) : declared;
|
|
26592
|
+
if (targets.length === 0) {
|
|
26593
|
+
process.stderr.write(only ? `crew: no repository "${only}" declares a sandbox: section
|
|
26594
|
+
` : "crew: no repository declares a sandbox: section in its .crew.yaml\n");
|
|
26595
|
+
process.exit(2);
|
|
26596
|
+
}
|
|
26597
|
+
const emitS = new Emitter({ route: routesS[0].route, eventFile: eventFileFor(cfg.ship.stateDir), rotateBytes: cfg.ship.eventsRotateBytes, logFile: cfg.ship.logFile, console: (l) => process.stderr.write(`${l}
|
|
26598
|
+
`) });
|
|
26599
|
+
const depsS = sandboxTickDeps(emitS, routesS[0]);
|
|
26600
|
+
let failed = false;
|
|
26601
|
+
for (const t of targets) {
|
|
26602
|
+
await sandboxBuildTick([t], depsS, true);
|
|
26603
|
+
const after = readSandboxState(cfg.ship.stateDir);
|
|
26604
|
+
const st = sandboxImageStatus(after, planSandbox(t.name, t.decl, t.warm));
|
|
26605
|
+
if (st.status === "failed") failed = true;
|
|
26606
|
+
process.stdout.write(`${t.name}: ${st.status} \u2014 ${st.detail}
|
|
26607
|
+
`);
|
|
26608
|
+
}
|
|
26609
|
+
if (failed) process.exit(1);
|
|
26610
|
+
break;
|
|
26611
|
+
}
|
|
26612
|
+
case "cache": {
|
|
26613
|
+
const settings = cacheSettingsOf(cfg.ship.sandbox);
|
|
26614
|
+
if (!settings) {
|
|
26615
|
+
process.stderr.write("crew sandbox cache: ship.sandbox.cacheVolume is not set, so there is no cache volume\n");
|
|
26616
|
+
process.exit(2);
|
|
26617
|
+
}
|
|
26618
|
+
const cache = new SandboxCache(settings);
|
|
26619
|
+
const served = cfg.routes.flatMap((r) => sandboxCacheRepos(r, cfg.ship));
|
|
26620
|
+
try {
|
|
26621
|
+
if (flag("reset")) {
|
|
26622
|
+
await cache.reset();
|
|
26623
|
+
process.stdout.write(`recreated volume ${settings.volume}; the next daemon pass refreshes every mirror
|
|
26624
|
+
`);
|
|
26625
|
+
break;
|
|
26626
|
+
}
|
|
26627
|
+
const dropped = flag("prune") ? await cache.prune(served) : [];
|
|
26628
|
+
const mirrors = await cache.list();
|
|
26629
|
+
if (flag("json")) {
|
|
26630
|
+
process.stdout.write(`${JSON.stringify({ volume: settings.volume, mirrors, dropped }, null, 2)}
|
|
26631
|
+
`);
|
|
26632
|
+
} else {
|
|
26633
|
+
for (const d of dropped) process.stdout.write(`dropped ${d.host}/${d.path}
|
|
26634
|
+
`);
|
|
26635
|
+
if (mirrors.length === 0) process.stdout.write(`volume ${settings.volume}: no mirrors yet
|
|
26636
|
+
`);
|
|
26637
|
+
for (const m of mirrors) process.stdout.write(`${m.repo.host}/${m.repo.path} last refresh ${m.lastRefresh ?? "never"}
|
|
26638
|
+
`);
|
|
26639
|
+
}
|
|
26640
|
+
} catch (e) {
|
|
26641
|
+
process.stderr.write(`crew sandbox cache: ${e instanceof Error ? e.message : String(e)}
|
|
26642
|
+
`);
|
|
26643
|
+
process.exitCode = 1;
|
|
26644
|
+
}
|
|
26645
|
+
break;
|
|
26646
|
+
}
|
|
26647
|
+
default:
|
|
26648
|
+
process.stderr.write([
|
|
26649
|
+
"crew sandbox policy --route ROUTE --role ROLE [--repo NAME] [--json]",
|
|
26650
|
+
"crew sandbox build [REPO] [--route ROUTE] [--json]",
|
|
26651
|
+
"crew sandbox images [--route ROUTE] [--json]",
|
|
26652
|
+
"crew sandbox cache [--prune|--reset] [--json]",
|
|
26653
|
+
""
|
|
26654
|
+
].join("\n"));
|
|
26655
|
+
process.exit(2);
|
|
26517
26656
|
}
|
|
26518
26657
|
break;
|
|
26519
26658
|
}
|
|
@@ -26766,70 +26905,6 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
26766
26905
|
if (!planIsUsable(planWt)) process.exitCode = 1;
|
|
26767
26906
|
break;
|
|
26768
26907
|
}
|
|
26769
|
-
case "sandbox": {
|
|
26770
|
-
const subS = positional[1];
|
|
26771
|
-
if (subS !== "build" && subS !== "images") {
|
|
26772
|
-
process.stderr.write("crew sandbox build [REPO] [--route ROUTE] [--json]\ncrew sandbox images [--route ROUTE] [--json]\n");
|
|
26773
|
-
process.exit(2);
|
|
26774
|
-
}
|
|
26775
|
-
const jsonS = flag("json");
|
|
26776
|
-
let routesS;
|
|
26777
|
-
try {
|
|
26778
|
-
routesS = value("route") ? [findRoute(cfg, value("route"))] : cfg.routes.filter((r) => r.enabled);
|
|
26779
|
-
} catch (e) {
|
|
26780
|
-
if (e instanceof ConfigError) {
|
|
26781
|
-
process.stderr.write(`crew: ${e.message}
|
|
26782
|
-
`);
|
|
26783
|
-
process.exit(2);
|
|
26784
|
-
}
|
|
26785
|
-
throw e;
|
|
26786
|
-
}
|
|
26787
|
-
const declared = await sandboxReposOf(routesS);
|
|
26788
|
-
if (subS === "images") {
|
|
26789
|
-
const stateS = readSandboxState(cfg.ship.stateDir);
|
|
26790
|
-
const rows = declared.map((r) => {
|
|
26791
|
-
const plan = planSandbox(r.name, r.decl, r.warm);
|
|
26792
|
-
return { repo: r.name, wants: plan.tag, tags: stateS[r.name]?.tags ?? [], ...sandboxImageStatus(stateS, plan) };
|
|
26793
|
-
});
|
|
26794
|
-
if (jsonS) {
|
|
26795
|
-
process.stdout.write(`${JSON.stringify(rows, null, 2)}
|
|
26796
|
-
`);
|
|
26797
|
-
break;
|
|
26798
|
-
}
|
|
26799
|
-
if (rows.length === 0) process.stdout.write("no repository declares a sandbox: section in its .crew.yaml\n");
|
|
26800
|
-
for (const r of rows) {
|
|
26801
|
-
process.stdout.write(`${r.repo}: ${r.status} \u2014 ${r.detail}
|
|
26802
|
-
`);
|
|
26803
|
-
for (const t of r.tags) {
|
|
26804
|
-
const ageH = Math.max(0, Math.round((Date.now() - Date.parse(t.builtAt)) / 36e5));
|
|
26805
|
-
process.stdout.write(` ${t.tag} built ${t.builtAt} (${ageH}h ago)
|
|
26806
|
-
`);
|
|
26807
|
-
}
|
|
26808
|
-
}
|
|
26809
|
-
break;
|
|
26810
|
-
}
|
|
26811
|
-
const only = positional[2];
|
|
26812
|
-
const targets = only ? declared.filter((r) => r.name === only) : declared;
|
|
26813
|
-
if (targets.length === 0) {
|
|
26814
|
-
process.stderr.write(only ? `crew: no repository "${only}" declares a sandbox: section
|
|
26815
|
-
` : "crew: no repository declares a sandbox: section in its .crew.yaml\n");
|
|
26816
|
-
process.exit(2);
|
|
26817
|
-
}
|
|
26818
|
-
const emitS = new Emitter({ route: routesS[0].route, eventFile: eventFileFor(cfg.ship.stateDir), rotateBytes: cfg.ship.eventsRotateBytes, logFile: cfg.ship.logFile, console: (l) => process.stderr.write(`${l}
|
|
26819
|
-
`) });
|
|
26820
|
-
const depsS = sandboxTickDeps(emitS, routesS[0]);
|
|
26821
|
-
let failed = false;
|
|
26822
|
-
for (const t of targets) {
|
|
26823
|
-
await sandboxBuildTick([t], depsS, true);
|
|
26824
|
-
const after = readSandboxState(cfg.ship.stateDir);
|
|
26825
|
-
const st = sandboxImageStatus(after, planSandbox(t.name, t.decl, t.warm));
|
|
26826
|
-
if (st.status === "failed") failed = true;
|
|
26827
|
-
process.stdout.write(`${t.name}: ${st.status} \u2014 ${st.detail}
|
|
26828
|
-
`);
|
|
26829
|
-
}
|
|
26830
|
-
if (failed) process.exit(1);
|
|
26831
|
-
break;
|
|
26832
|
-
}
|
|
26833
26908
|
case "periscope": {
|
|
26834
26909
|
const subP = positional[1];
|
|
26835
26910
|
if (subP !== "up" && subP !== "start" && subP !== "down" && subP !== "end" && subP !== "list") {
|
|
@@ -27715,45 +27790,6 @@ ${where.join("\n")}`);
|
|
|
27715
27790
|
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
27716
27791
|
break;
|
|
27717
27792
|
}
|
|
27718
|
-
case "sandbox": {
|
|
27719
|
-
const settings = cacheSettingsOf(cfg.ship.sandbox);
|
|
27720
|
-
if (positional[1] !== "cache") {
|
|
27721
|
-
process.stderr.write("crew sandbox cache [--prune|--reset] [--json]\n");
|
|
27722
|
-
process.exit(2);
|
|
27723
|
-
}
|
|
27724
|
-
if (!settings) {
|
|
27725
|
-
process.stderr.write("crew sandbox cache: ship.sandbox.cacheVolume is not set, so there is no cache volume\n");
|
|
27726
|
-
process.exit(2);
|
|
27727
|
-
}
|
|
27728
|
-
const cache = new SandboxCache(settings);
|
|
27729
|
-
const served = cfg.routes.flatMap((r) => sandboxCacheRepos(r, cfg.ship));
|
|
27730
|
-
try {
|
|
27731
|
-
if (flag("reset")) {
|
|
27732
|
-
await cache.reset();
|
|
27733
|
-
process.stdout.write(`recreated volume ${settings.volume}; the next daemon pass refreshes every mirror
|
|
27734
|
-
`);
|
|
27735
|
-
break;
|
|
27736
|
-
}
|
|
27737
|
-
const dropped = flag("prune") ? await cache.prune(served) : [];
|
|
27738
|
-
const mirrors = await cache.list();
|
|
27739
|
-
if (flag("json")) {
|
|
27740
|
-
process.stdout.write(`${JSON.stringify({ volume: settings.volume, mirrors, dropped }, null, 2)}
|
|
27741
|
-
`);
|
|
27742
|
-
} else {
|
|
27743
|
-
for (const d of dropped) process.stdout.write(`dropped ${d.host}/${d.path}
|
|
27744
|
-
`);
|
|
27745
|
-
if (mirrors.length === 0) process.stdout.write(`volume ${settings.volume}: no mirrors yet
|
|
27746
|
-
`);
|
|
27747
|
-
for (const m of mirrors) process.stdout.write(`${m.repo.host}/${m.repo.path} last refresh ${m.lastRefresh ?? "never"}
|
|
27748
|
-
`);
|
|
27749
|
-
}
|
|
27750
|
-
} catch (e) {
|
|
27751
|
-
process.stderr.write(`crew sandbox cache: ${e instanceof Error ? e.message : String(e)}
|
|
27752
|
-
`);
|
|
27753
|
-
process.exitCode = 1;
|
|
27754
|
-
}
|
|
27755
|
-
break;
|
|
27756
|
-
}
|
|
27757
27793
|
case "whoami": {
|
|
27758
27794
|
const only = value("route") ?? positional[1];
|
|
27759
27795
|
const wanted = only ? cfg.routes.filter((r) => r.route === only) : cfg.routes;
|
package/package.json
CHANGED