@tablation/crew 0.100.0 → 0.100.2
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 +387 -197
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7218,6 +7218,12 @@ var Tracker = class {
|
|
|
7218
7218
|
if (!model) return [];
|
|
7219
7219
|
return this.client.records.list(model, { limit: 200 });
|
|
7220
7220
|
}
|
|
7221
|
+
/** Every `Repos` row with the columns the resolved file mirrors (CREW-2885). */
|
|
7222
|
+
async repoRowsForMap() {
|
|
7223
|
+
const model = this.route.resolved?.reposModelId;
|
|
7224
|
+
if (!model) return [];
|
|
7225
|
+
return this.client.records.list(model, { limit: 200 });
|
|
7226
|
+
}
|
|
7221
7227
|
/** Delete a `Repos` row (`crew sample-ticket --remove`). */
|
|
7222
7228
|
async deleteRepoRow(id) {
|
|
7223
7229
|
const model = this.route.resolved?.reposModelId;
|
|
@@ -10685,7 +10691,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
10685
10691
|
}
|
|
10686
10692
|
|
|
10687
10693
|
// src/cli.ts
|
|
10688
|
-
import { resolve as resolve6, dirname as
|
|
10694
|
+
import { resolve as resolve6, dirname as dirname24, join as join46, basename as basename14 } from "path";
|
|
10689
10695
|
import { fileURLToPath } from "url";
|
|
10690
10696
|
import { hostname as hostname3, homedir as homedir10 } from "os";
|
|
10691
10697
|
import { spawnSync as spawnSync8 } from "child_process";
|
|
@@ -12366,6 +12372,115 @@ async function resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affi
|
|
|
12366
12372
|
}
|
|
12367
12373
|
}
|
|
12368
12374
|
|
|
12375
|
+
// src/repo-refresh.ts
|
|
12376
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync16, readFileSync as readFileSync23, writeFileSync as writeFileSync18 } from "fs";
|
|
12377
|
+
import { dirname as dirname11 } from "path";
|
|
12378
|
+
var REFRESH_COOLDOWN_MS = 2 * 6e4;
|
|
12379
|
+
var UNSERVABLE_PASSES_BEFORE_ATTENTION = 3;
|
|
12380
|
+
var lastAsked = /* @__PURE__ */ new Map();
|
|
12381
|
+
function unknownRepoIds(route2, repoIds) {
|
|
12382
|
+
const names = route2.resolved?.repoNames;
|
|
12383
|
+
if (!route2.resolved?.reposModelId || !names) return [];
|
|
12384
|
+
return [...new Set(repoIds.filter((id) => !!id && !(id in names)))];
|
|
12385
|
+
}
|
|
12386
|
+
function applyRepoRows(resolved, rows) {
|
|
12387
|
+
const added = [];
|
|
12388
|
+
const names = resolved.repoNames ??= {};
|
|
12389
|
+
const remotes = resolved.repoRemotes ??= {};
|
|
12390
|
+
const hosts = resolved.repoForgeHosts ??= {};
|
|
12391
|
+
for (const r of rows) {
|
|
12392
|
+
if (!(r.id in names)) added.push(r.id);
|
|
12393
|
+
names[r.id] = r.name ?? "";
|
|
12394
|
+
if (r.remote) remotes[r.id] = r.remote;
|
|
12395
|
+
else delete remotes[r.id];
|
|
12396
|
+
const h = r.forge_host?.trim().toLowerCase();
|
|
12397
|
+
if (h) hosts[r.id] = h;
|
|
12398
|
+
else delete hosts[r.id];
|
|
12399
|
+
}
|
|
12400
|
+
if (Object.keys(remotes).length === 0) delete resolved.repoRemotes;
|
|
12401
|
+
if (Object.keys(hosts).length === 0) delete resolved.repoForgeHosts;
|
|
12402
|
+
return added;
|
|
12403
|
+
}
|
|
12404
|
+
function persistRepoRows(stateDir, routeName, rows) {
|
|
12405
|
+
const path = resolvedPathFor(stateDir, routeName);
|
|
12406
|
+
if (!existsSync24(path)) return;
|
|
12407
|
+
const raw = JSON.parse(readFileSync23(path, "utf8"));
|
|
12408
|
+
applyRepoRows(raw, rows);
|
|
12409
|
+
mkdirSync16(dirname11(path), { recursive: true });
|
|
12410
|
+
writeFileSync18(path, `${JSON.stringify(raw, null, 2)}
|
|
12411
|
+
`);
|
|
12412
|
+
}
|
|
12413
|
+
async function refreshUnknownRepos(o) {
|
|
12414
|
+
const unknown = unknownRepoIds(o.route, o.tickets.map((t) => t.repo_id));
|
|
12415
|
+
if (unknown.length === 0 || !o.route.resolved) return [];
|
|
12416
|
+
const now = o.now ?? Date.now();
|
|
12417
|
+
const last = lastAsked.get(o.route.route);
|
|
12418
|
+
if (!o.force && last !== void 0 && now - last < REFRESH_COOLDOWN_MS) return [];
|
|
12419
|
+
lastAsked.set(o.route.route, now);
|
|
12420
|
+
let rows;
|
|
12421
|
+
try {
|
|
12422
|
+
rows = await o.fetchRows();
|
|
12423
|
+
} catch {
|
|
12424
|
+
return [];
|
|
12425
|
+
}
|
|
12426
|
+
const added = applyRepoRows(o.route.resolved, rows);
|
|
12427
|
+
try {
|
|
12428
|
+
persistRepoRows(o.stateDir, o.route.route, rows);
|
|
12429
|
+
} catch {
|
|
12430
|
+
}
|
|
12431
|
+
return added;
|
|
12432
|
+
}
|
|
12433
|
+
var repoUnservableKey = (route2, issueId) => `repo_unservable:${route2}:${issueId}`;
|
|
12434
|
+
function repoUnservableItem(route2, issueId, repoLabel, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
12435
|
+
return {
|
|
12436
|
+
kind: "repo_unservable",
|
|
12437
|
+
key: repoUnservableKey(route2, issueId),
|
|
12438
|
+
since: nowIso,
|
|
12439
|
+
message: `${route2}: ${issueId} is skipped \u2014 this ship has no checkout for ${repoLabel}. Resumes automatically once the route lists that repository (\`crew repos add\`) or a Repos row for it exists`
|
|
12440
|
+
};
|
|
12441
|
+
}
|
|
12442
|
+
function trackUnservable(streaks, unservable) {
|
|
12443
|
+
const now = new Set(unservable);
|
|
12444
|
+
const recovered = [];
|
|
12445
|
+
for (const id of [...streaks.keys()]) {
|
|
12446
|
+
if (!now.has(id)) {
|
|
12447
|
+
streaks.delete(id);
|
|
12448
|
+
recovered.push(id);
|
|
12449
|
+
}
|
|
12450
|
+
}
|
|
12451
|
+
const raise = [];
|
|
12452
|
+
for (const id of now) {
|
|
12453
|
+
const n = (streaks.get(id) ?? 0) + 1;
|
|
12454
|
+
streaks.set(id, n);
|
|
12455
|
+
if (n >= UNSERVABLE_PASSES_BEFORE_ATTENTION) raise.push(id);
|
|
12456
|
+
}
|
|
12457
|
+
return { raise, recovered };
|
|
12458
|
+
}
|
|
12459
|
+
var streaksByRoute = /* @__PURE__ */ new Map();
|
|
12460
|
+
async function reportUnservable(o, unservable, repoLabelOf) {
|
|
12461
|
+
const streaks = streaksByRoute.get(o.route.route) ?? /* @__PURE__ */ new Map();
|
|
12462
|
+
streaksByRoute.set(o.route.route, streaks);
|
|
12463
|
+
const { raise, recovered } = trackUnservable(streaks, unservable);
|
|
12464
|
+
for (const id of recovered) {
|
|
12465
|
+
try {
|
|
12466
|
+
clearShipAttention(o.state, repoUnservableKey(o.route.route, id));
|
|
12467
|
+
} catch {
|
|
12468
|
+
}
|
|
12469
|
+
}
|
|
12470
|
+
for (const id of raise) {
|
|
12471
|
+
try {
|
|
12472
|
+
await raiseShipAttention(o, repoUnservableItem(o.route.route, id, repoLabelOf(id)));
|
|
12473
|
+
} catch {
|
|
12474
|
+
}
|
|
12475
|
+
}
|
|
12476
|
+
}
|
|
12477
|
+
function repoLabelFor(route2, ticket) {
|
|
12478
|
+
const id = ticket?.repo_id;
|
|
12479
|
+
if (!id) return "a repository (the ticket names none)";
|
|
12480
|
+
const name = route2.resolved?.repoNames?.[id];
|
|
12481
|
+
return name ? `repository "${name}"` : `repository ${id} (no Repos row by that id)`;
|
|
12482
|
+
}
|
|
12483
|
+
|
|
12369
12484
|
// src/conflict.ts
|
|
12370
12485
|
import { StaleWriteError as StaleWriteError7 } from "@tablation/client";
|
|
12371
12486
|
var BOUNCE_MARKER = calloutHeader("WARNING", "Merge Conflict");
|
|
@@ -12742,13 +12857,13 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
|
|
|
12742
12857
|
var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
|
|
12743
12858
|
|
|
12744
12859
|
// src/connect-wizard.ts
|
|
12745
|
-
import { delimiter, dirname as
|
|
12746
|
-
import { accessSync as accessSync3, constants as constants3, existsSync as
|
|
12860
|
+
import { delimiter, dirname as dirname12, join as join27 } from "path";
|
|
12861
|
+
import { accessSync as accessSync3, constants as constants3, existsSync as existsSync25, mkdirSync as mkdirSync17, writeFileSync as writeFileSync19 } from "fs";
|
|
12747
12862
|
import { stringify } from "yaml";
|
|
12748
12863
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
12749
12864
|
function shouldRunWizard(opts) {
|
|
12750
12865
|
if (!opts.isTTY) return false;
|
|
12751
|
-
const exists = opts.configExists ??
|
|
12866
|
+
const exists = opts.configExists ?? existsSync25;
|
|
12752
12867
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
12753
12868
|
}
|
|
12754
12869
|
function firstRunConfigPath() {
|
|
@@ -12791,9 +12906,9 @@ async function runWizard(p, env) {
|
|
|
12791
12906
|
if (entered && isExec(entered)) agentBin = entered;
|
|
12792
12907
|
else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
|
|
12793
12908
|
}
|
|
12794
|
-
const nodeDir =
|
|
12909
|
+
const nodeDir = dirname12(env.nodePath);
|
|
12795
12910
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
12796
|
-
const baseDirs = [nodeDir, pnpmPath ?
|
|
12911
|
+
const baseDirs = [nodeDir, pnpmPath ? dirname12(pnpmPath) : void 0];
|
|
12797
12912
|
p.say(
|
|
12798
12913
|
`
|
|
12799
12914
|
Host Passengers lets this machine run the agent sessions that other people's
|
|
@@ -12804,7 +12919,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
|
|
|
12804
12919
|
p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
|
|
12805
12920
|
}
|
|
12806
12921
|
const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
12807
|
-
const extraPath = deriveExtraPath([...baseDirs, dockerPath ?
|
|
12922
|
+
const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname12(dockerPath) : void 0]);
|
|
12808
12923
|
if (extraPath) {
|
|
12809
12924
|
p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
|
|
12810
12925
|
}
|
|
@@ -12826,9 +12941,9 @@ routes:
|
|
|
12826
12941
|
${routeBlock}`;
|
|
12827
12942
|
}
|
|
12828
12943
|
function writeNewConfig(path, text2) {
|
|
12829
|
-
|
|
12944
|
+
mkdirSync17(dirname12(path), { recursive: true });
|
|
12830
12945
|
try {
|
|
12831
|
-
|
|
12946
|
+
writeFileSync19(path, text2, { mode: 384, flag: "wx" });
|
|
12832
12947
|
return true;
|
|
12833
12948
|
} catch (e) {
|
|
12834
12949
|
if (e.code === "EEXIST") return false;
|
|
@@ -12862,7 +12977,7 @@ Wrote ${o.configPath}.
|
|
|
12862
12977
|
// src/connect-json.ts
|
|
12863
12978
|
import { accessSync as accessSync4, constants as constants4 } from "fs";
|
|
12864
12979
|
import { homedir as homedir8 } from "os";
|
|
12865
|
-
import { dirname as
|
|
12980
|
+
import { dirname as dirname13, join as join28 } from "path";
|
|
12866
12981
|
import { delimiter as delimiter2 } from "path";
|
|
12867
12982
|
var CHOICE_EXIT_CODE = 3;
|
|
12868
12983
|
function createEmitter(json2, write) {
|
|
@@ -12928,7 +13043,7 @@ function flagWizardAnswers(f, env) {
|
|
|
12928
13043
|
const agentBin = f.agentBin ?? found?.bin;
|
|
12929
13044
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
12930
13045
|
const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
12931
|
-
const extraPath = f.extraPath ?? deriveExtraPath([
|
|
13046
|
+
const extraPath = f.extraPath ?? deriveExtraPath([dirname13(env.nodePath), pnpmPath ? dirname13(pnpmPath) : void 0, dockerPath ? dirname13(dockerPath) : void 0]);
|
|
12932
13047
|
return {
|
|
12933
13048
|
answers: {
|
|
12934
13049
|
name: f.shipName?.trim() || env.hostname,
|
|
@@ -12943,7 +13058,7 @@ function flagWizardAnswers(f, env) {
|
|
|
12943
13058
|
|
|
12944
13059
|
// src/provision.ts
|
|
12945
13060
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
12946
|
-
import { existsSync as
|
|
13061
|
+
import { existsSync as existsSync26, readFileSync as readFileSync24, writeFileSync as writeFileSync20 } from "fs";
|
|
12947
13062
|
import { hostname as hostname2 } from "os";
|
|
12948
13063
|
import { parseDocument as parseDocument3 } from "yaml";
|
|
12949
13064
|
|
|
@@ -13460,7 +13575,7 @@ function realProvisionDeps(crewHome) {
|
|
|
13460
13575
|
};
|
|
13461
13576
|
const configFile = () => {
|
|
13462
13577
|
const f = findConfigFile(crewHome);
|
|
13463
|
-
return
|
|
13578
|
+
return existsSync26(f) ? f : null;
|
|
13464
13579
|
};
|
|
13465
13580
|
return {
|
|
13466
13581
|
crew: spawnCrew,
|
|
@@ -13468,13 +13583,13 @@ function realProvisionDeps(crewHome) {
|
|
|
13468
13583
|
enableRoute(route2) {
|
|
13469
13584
|
const file = configFile();
|
|
13470
13585
|
if (!file) return false;
|
|
13471
|
-
const text2 =
|
|
13586
|
+
const text2 = readFileSync24(file, "utf8");
|
|
13472
13587
|
const doc = parseDocument3(text2);
|
|
13473
13588
|
const routes = doc.get("routes");
|
|
13474
13589
|
const entry = routes?.items?.find((r) => r.get("route") === route2);
|
|
13475
13590
|
if (!entry) throw new Error(`route ${route2} is not in ${file}`);
|
|
13476
13591
|
if (entry.get("enabled") === true) return false;
|
|
13477
|
-
|
|
13592
|
+
writeFileSync20(file, planEnable(text2, route2));
|
|
13478
13593
|
return true;
|
|
13479
13594
|
},
|
|
13480
13595
|
installState() {
|
|
@@ -13494,14 +13609,14 @@ function realProvisionDeps(crewHome) {
|
|
|
13494
13609
|
continue;
|
|
13495
13610
|
}
|
|
13496
13611
|
for (const p of plan.unitPaths) {
|
|
13497
|
-
if (!
|
|
13612
|
+
if (!existsSync26(p) || readFileSync24(p, "utf8") !== plan.unitContent[p]) unitsStale = true;
|
|
13498
13613
|
}
|
|
13499
13614
|
}
|
|
13500
13615
|
const label = labelFor(crewHome, "run");
|
|
13501
13616
|
let running = false;
|
|
13502
13617
|
let lingering = true;
|
|
13503
13618
|
if (mechanism === "systemd") {
|
|
13504
|
-
const system = ctx.root ||
|
|
13619
|
+
const system = ctx.root || existsSync26(systemUnitPathFor(label));
|
|
13505
13620
|
const ctl = system ? ["systemctl"] : ["systemctl", "--user"];
|
|
13506
13621
|
running = spawnSync6(ctl[0], [...ctl.slice(1), "is-active", "--quiet", `${label}.service`]).status === 0;
|
|
13507
13622
|
if (!system) {
|
|
@@ -13528,8 +13643,8 @@ host=${host}
|
|
|
13528
13643
|
}
|
|
13529
13644
|
|
|
13530
13645
|
// src/release-run.ts
|
|
13531
|
-
import { readFileSync as
|
|
13532
|
-
import { dirname as
|
|
13646
|
+
import { readFileSync as readFileSync25, writeFileSync as writeFileSync21, existsSync as existsSync27, copyFileSync, mkdirSync as mkdirSync18, mkdtempSync as mkdtempSync2, rmSync as rmSync14 } from "fs";
|
|
13647
|
+
import { dirname as dirname15, join as join30 } from "path";
|
|
13533
13648
|
import { tmpdir as tmpdir3 } from "os";
|
|
13534
13649
|
|
|
13535
13650
|
// src/release.ts
|
|
@@ -14272,11 +14387,11 @@ async function bump(o, size) {
|
|
|
14272
14387
|
}
|
|
14273
14388
|
const files = o.repo.release.versionFiles.map((f) => join30(o.cwd, f));
|
|
14274
14389
|
const first = files[0];
|
|
14275
|
-
if (!
|
|
14390
|
+
if (!existsSync27(first)) {
|
|
14276
14391
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
14277
14392
|
return null;
|
|
14278
14393
|
}
|
|
14279
|
-
const current = JSON.parse(
|
|
14394
|
+
const current = JSON.parse(readFileSync25(first, "utf8")).version;
|
|
14280
14395
|
if (!current) {
|
|
14281
14396
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
14282
14397
|
return null;
|
|
@@ -14289,13 +14404,13 @@ async function bump(o, size) {
|
|
|
14289
14404
|
return version;
|
|
14290
14405
|
}
|
|
14291
14406
|
for (const f of files) {
|
|
14292
|
-
const src =
|
|
14407
|
+
const src = readFileSync25(f, "utf8");
|
|
14293
14408
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
14294
14409
|
if (out === src) {
|
|
14295
14410
|
o.emit.error(`no version field in ${f}`);
|
|
14296
14411
|
return null;
|
|
14297
14412
|
}
|
|
14298
|
-
|
|
14413
|
+
writeFileSync21(f, out);
|
|
14299
14414
|
}
|
|
14300
14415
|
return version;
|
|
14301
14416
|
}
|
|
@@ -14308,8 +14423,8 @@ function writeChangelog(o, version, entries) {
|
|
|
14308
14423
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
14309
14424
|
return;
|
|
14310
14425
|
}
|
|
14311
|
-
const existing =
|
|
14312
|
-
|
|
14426
|
+
const existing = existsSync27(path) ? readFileSync25(path, "utf8") : "# Changelog\n";
|
|
14427
|
+
writeFileSync21(path, insertChangelogSection(existing, section));
|
|
14313
14428
|
}
|
|
14314
14429
|
async function confirm(o, expected) {
|
|
14315
14430
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -14414,8 +14529,8 @@ function openReleaseCheckout(o) {
|
|
|
14414
14529
|
}
|
|
14415
14530
|
for (const f of o.repo.worktrees.copy) {
|
|
14416
14531
|
const src = join30(o.cwd, f);
|
|
14417
|
-
if (!
|
|
14418
|
-
|
|
14532
|
+
if (!existsSync27(src)) continue;
|
|
14533
|
+
mkdirSync18(dirname15(join30(dir, f)), { recursive: true });
|
|
14419
14534
|
copyFileSync(src, join30(dir, f));
|
|
14420
14535
|
}
|
|
14421
14536
|
return { dir, branch, close };
|
|
@@ -15117,11 +15232,11 @@ Passed: ${passed}.${willClose ? " Closing this alert." : ""}`,
|
|
|
15117
15232
|
}
|
|
15118
15233
|
|
|
15119
15234
|
// src/live-runs.ts
|
|
15120
|
-
import { existsSync as
|
|
15235
|
+
import { existsSync as existsSync29, readdirSync as readdirSync11 } from "fs";
|
|
15121
15236
|
import { join as join32 } from "path";
|
|
15122
15237
|
|
|
15123
15238
|
// src/interrupted.ts
|
|
15124
|
-
import { existsSync as
|
|
15239
|
+
import { existsSync as existsSync28, readdirSync as readdirSync10, readFileSync as readFileSync26 } from "fs";
|
|
15125
15240
|
import { join as join31 } from "path";
|
|
15126
15241
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
15127
15242
|
function interruptedComment(b) {
|
|
@@ -15141,16 +15256,16 @@ var pidAlive = (pid) => {
|
|
|
15141
15256
|
};
|
|
15142
15257
|
function runFinished(streamPath) {
|
|
15143
15258
|
const path = `${streamPath}.meta.json`;
|
|
15144
|
-
if (!
|
|
15259
|
+
if (!existsSync28(path)) return false;
|
|
15145
15260
|
try {
|
|
15146
|
-
return JSON.parse(
|
|
15261
|
+
return JSON.parse(readFileSync26(path, "utf8")).running !== true;
|
|
15147
15262
|
} catch {
|
|
15148
15263
|
return true;
|
|
15149
15264
|
}
|
|
15150
15265
|
}
|
|
15151
15266
|
function orphanedStreams(o) {
|
|
15152
15267
|
const dir = join31(o.stateDir, "streams");
|
|
15153
|
-
if (!
|
|
15268
|
+
if (!existsSync28(dir)) return [];
|
|
15154
15269
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
15155
15270
|
const out = [];
|
|
15156
15271
|
for (const name of readdirSync10(dir)) {
|
|
@@ -15210,7 +15325,7 @@ async function reconcileRuns(o) {
|
|
|
15210
15325
|
// src/live-runs.ts
|
|
15211
15326
|
function liveRuns(stateDir, isAlive = runIsAlive) {
|
|
15212
15327
|
const dir = join32(stateDir, "streams");
|
|
15213
|
-
if (!
|
|
15328
|
+
if (!existsSync29(dir)) return [];
|
|
15214
15329
|
const out = [];
|
|
15215
15330
|
for (const name of readdirSync11(dir)) {
|
|
15216
15331
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
@@ -15549,7 +15664,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
15549
15664
|
}
|
|
15550
15665
|
|
|
15551
15666
|
// src/watch.ts
|
|
15552
|
-
import { watch as watchFile, existsSync as
|
|
15667
|
+
import { watch as watchFile, existsSync as existsSync30, statSync as statSync9, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
15553
15668
|
var E = "\x1B[";
|
|
15554
15669
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
15555
15670
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -15650,7 +15765,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
15650
15765
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
15651
15766
|
}
|
|
15652
15767
|
function readFrom(file, from) {
|
|
15653
|
-
if (!
|
|
15768
|
+
if (!existsSync30(file)) return { events: [], offset: from };
|
|
15654
15769
|
const size = statSync9(file).size;
|
|
15655
15770
|
if (size < from) return readFrom(file, 0);
|
|
15656
15771
|
if (size === from) return { events: [], offset: from };
|
|
@@ -15893,15 +16008,15 @@ function nextRoles(decision, ran) {
|
|
|
15893
16008
|
}
|
|
15894
16009
|
|
|
15895
16010
|
// src/ship-name.ts
|
|
15896
|
-
import { existsSync as
|
|
15897
|
-
import { dirname as
|
|
16011
|
+
import { existsSync as existsSync32, mkdirSync as mkdirSync20, readFileSync as readFileSync28, renameSync as renameSync3, writeFileSync as writeFileSync23 } from "fs";
|
|
16012
|
+
import { dirname as dirname17 } from "path";
|
|
15898
16013
|
import { parseDocument as parseDocument5 } from "yaml";
|
|
15899
16014
|
|
|
15900
16015
|
// src/machine-id.ts
|
|
15901
16016
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
15902
16017
|
import { createHash as createHash5 } from "crypto";
|
|
15903
|
-
import { existsSync as
|
|
15904
|
-
import { dirname as
|
|
16018
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync27, writeFileSync as writeFileSync22 } from "fs";
|
|
16019
|
+
import { dirname as dirname16, join as join33 } from "path";
|
|
15905
16020
|
function machineIdPathFor(stateDir) {
|
|
15906
16021
|
return join33(stateDir, "machine-id");
|
|
15907
16022
|
}
|
|
@@ -15916,8 +16031,8 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
15916
16031
|
return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
|
|
15917
16032
|
}
|
|
15918
16033
|
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
15919
|
-
if (
|
|
15920
|
-
const id =
|
|
16034
|
+
if (existsSync31(p)) {
|
|
16035
|
+
const id = readFileSync27(p, "utf8").trim();
|
|
15921
16036
|
if (id) return id;
|
|
15922
16037
|
}
|
|
15923
16038
|
}
|
|
@@ -15928,15 +16043,15 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
15928
16043
|
function ensureMachineId(stateDir, opts = {}) {
|
|
15929
16044
|
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
15930
16045
|
const path = machineIdPathFor(stateDir);
|
|
15931
|
-
if (
|
|
15932
|
-
const cached =
|
|
16046
|
+
if (existsSync31(path)) {
|
|
16047
|
+
const cached = readFileSync27(path, "utf8").trim();
|
|
15933
16048
|
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
15934
16049
|
}
|
|
15935
16050
|
const raw = (opts.read ?? readRawMachineId)();
|
|
15936
16051
|
if (!raw) return void 0;
|
|
15937
16052
|
const hashed = sha256(raw);
|
|
15938
|
-
|
|
15939
|
-
|
|
16053
|
+
mkdirSync19(dirname16(path), { recursive: true });
|
|
16054
|
+
writeFileSync22(path, `${hashed}
|
|
15940
16055
|
`, { mode: 384 });
|
|
15941
16056
|
return hashed;
|
|
15942
16057
|
}
|
|
@@ -16007,34 +16122,34 @@ async function syncRouteName(i) {
|
|
|
16007
16122
|
return { status: "ok", ...base, action, name: i.local };
|
|
16008
16123
|
}
|
|
16009
16124
|
function readSyncedName(resolvedFile) {
|
|
16010
|
-
if (!
|
|
16125
|
+
if (!existsSync32(resolvedFile)) return void 0;
|
|
16011
16126
|
try {
|
|
16012
|
-
const v = JSON.parse(
|
|
16127
|
+
const v = JSON.parse(readFileSync28(resolvedFile, "utf8")).shipName;
|
|
16013
16128
|
return typeof v === "string" ? v : void 0;
|
|
16014
16129
|
} catch {
|
|
16015
16130
|
return void 0;
|
|
16016
16131
|
}
|
|
16017
16132
|
}
|
|
16018
16133
|
function writeSyncedName(resolvedFile, name) {
|
|
16019
|
-
if (!
|
|
16134
|
+
if (!existsSync32(resolvedFile)) return;
|
|
16020
16135
|
let raw;
|
|
16021
16136
|
try {
|
|
16022
|
-
raw = JSON.parse(
|
|
16137
|
+
raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
|
|
16023
16138
|
} catch {
|
|
16024
16139
|
return;
|
|
16025
16140
|
}
|
|
16026
16141
|
if (raw.shipName === name) return;
|
|
16027
16142
|
raw.shipName = name;
|
|
16028
|
-
|
|
16143
|
+
mkdirSync20(dirname17(resolvedFile), { recursive: true });
|
|
16029
16144
|
const tmp = `${resolvedFile}.tmp${process.pid}`;
|
|
16030
|
-
|
|
16145
|
+
writeFileSync23(tmp, `${JSON.stringify(raw, null, 2)}
|
|
16031
16146
|
`);
|
|
16032
16147
|
renameSync3(tmp, resolvedFile);
|
|
16033
16148
|
}
|
|
16034
16149
|
async function reconcileShipName(i) {
|
|
16035
16150
|
let local = i.ship.name;
|
|
16036
16151
|
try {
|
|
16037
|
-
local = shipNameOfYaml(
|
|
16152
|
+
local = shipNameOfYaml(readFileSync28(i.configFile, "utf8")) ?? local;
|
|
16038
16153
|
} catch {
|
|
16039
16154
|
}
|
|
16040
16155
|
const resolvedFile = resolvedPathFor(i.stateDir, i.route);
|
|
@@ -16058,8 +16173,8 @@ async function reconcileShipName(i) {
|
|
|
16058
16173
|
}
|
|
16059
16174
|
if (result.status !== "ok") return result;
|
|
16060
16175
|
if (result.action === "adopt" || result.action === "row-wins") {
|
|
16061
|
-
const text2 =
|
|
16062
|
-
|
|
16176
|
+
const text2 = readFileSync28(i.configFile, "utf8");
|
|
16177
|
+
writeFileSync23(i.configFile, setShipNameInYaml(text2, result.name));
|
|
16063
16178
|
i.ship.name = result.name;
|
|
16064
16179
|
if (result.action === "row-wins") {
|
|
16065
16180
|
i.emit?.warn(
|
|
@@ -16715,8 +16830,8 @@ function describeCapability(i) {
|
|
|
16715
16830
|
}
|
|
16716
16831
|
|
|
16717
16832
|
// src/agents-sync-run.ts
|
|
16718
|
-
import { existsSync as
|
|
16719
|
-
import { dirname as
|
|
16833
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as readFileSync29, writeFileSync as writeFileSync24 } from "fs";
|
|
16834
|
+
import { dirname as dirname18 } from "path";
|
|
16720
16835
|
async function runAgentsSync(target, o) {
|
|
16721
16836
|
const dryRun = o.dryRun ?? false;
|
|
16722
16837
|
const force = o.force ?? false;
|
|
@@ -16749,12 +16864,12 @@ async function runAgentsSync(target, o) {
|
|
|
16749
16864
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
16750
16865
|
if (!dryRun) {
|
|
16751
16866
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
16752
|
-
const raw =
|
|
16867
|
+
const raw = existsSync33(resolvedPath) ? JSON.parse(readFileSync29(resolvedPath, "utf8")) : {};
|
|
16753
16868
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
16754
16869
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
16755
16870
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
16756
|
-
|
|
16757
|
-
|
|
16871
|
+
mkdirSync21(dirname18(resolvedPath), { recursive: true });
|
|
16872
|
+
writeFileSync24(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
16758
16873
|
`);
|
|
16759
16874
|
if (target.resolved) {
|
|
16760
16875
|
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
@@ -16858,7 +16973,7 @@ async function showLogEntry(route2, entryId, opts = {}) {
|
|
|
16858
16973
|
|
|
16859
16974
|
// src/repos-check.ts
|
|
16860
16975
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
16861
|
-
import { readFileSync as
|
|
16976
|
+
import { readFileSync as readFileSync30 } from "fs";
|
|
16862
16977
|
import { basename as basename9 } from "path";
|
|
16863
16978
|
var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
|
|
16864
16979
|
function checkHookSyntax(script, shell) {
|
|
@@ -16899,7 +17014,7 @@ function checkRepoSpec(dir, opts = {}) {
|
|
|
16899
17014
|
}
|
|
16900
17015
|
let parsed;
|
|
16901
17016
|
try {
|
|
16902
|
-
parsed = parseRepoConfig(
|
|
17017
|
+
parsed = parseRepoConfig(readFileSync30(file, "utf8"), file);
|
|
16903
17018
|
} catch (e) {
|
|
16904
17019
|
if (!(e instanceof RepoConfigError)) throw e;
|
|
16905
17020
|
return { ...empty, file, error: e.message, exitCode: 1 };
|
|
@@ -17346,7 +17461,7 @@ ${run2.err}` });
|
|
|
17346
17461
|
}
|
|
17347
17462
|
|
|
17348
17463
|
// src/sample-ticket.ts
|
|
17349
|
-
import { basename as basename10, dirname as
|
|
17464
|
+
import { basename as basename10, dirname as dirname19, join as join34 } from "path";
|
|
17350
17465
|
var SAMPLE_REPO = "crew-sample";
|
|
17351
17466
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
17352
17467
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -17369,7 +17484,7 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
17369
17484
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
17370
17485
|
`;
|
|
17371
17486
|
function sampleDir(existing, reposBase, workspace) {
|
|
17372
|
-
if (existing.length > 0) return join34(
|
|
17487
|
+
if (existing.length > 0) return join34(dirname19(existing[0]), SAMPLE_REPO);
|
|
17373
17488
|
return join34(reposBase, workspace, SAMPLE_REPO);
|
|
17374
17489
|
}
|
|
17375
17490
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
@@ -17414,8 +17529,8 @@ function planSampleTicket(i) {
|
|
|
17414
17529
|
}
|
|
17415
17530
|
|
|
17416
17531
|
// src/worktree-sweep.ts
|
|
17417
|
-
import { existsSync as
|
|
17418
|
-
import { basename as basename11, dirname as
|
|
17532
|
+
import { existsSync as existsSync34, readFileSync as readFileSync31, readdirSync as readdirSync12 } from "fs";
|
|
17533
|
+
import { basename as basename11, dirname as dirname20, join as join35, resolve as resolve4 } from "path";
|
|
17419
17534
|
function planWorktreeSweep(target, tickets, contract) {
|
|
17420
17535
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
17421
17536
|
const verified = contract.statuses.verified;
|
|
@@ -17479,7 +17594,7 @@ function worktreeTicketNumbers(target) {
|
|
|
17479
17594
|
return [...nums];
|
|
17480
17595
|
}
|
|
17481
17596
|
function siblingWorktreeDirs(target) {
|
|
17482
|
-
const parent =
|
|
17597
|
+
const parent = dirname20(target.dir.replace(/[/\\]+$/, ""));
|
|
17483
17598
|
const stem = `${basename11(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
17484
17599
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
17485
17600
|
let names;
|
|
@@ -17527,7 +17642,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
17527
17642
|
let removed = 0;
|
|
17528
17643
|
let keptBranches = 0;
|
|
17529
17644
|
for (const a of actions) {
|
|
17530
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
17645
|
+
if (!dryRun && base && !a.keepBranch && existsSync34(a.path) && isHandBackMerge(a.path, base)) {
|
|
17531
17646
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
17532
17647
|
log.emit(
|
|
17533
17648
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -17535,7 +17650,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
17535
17650
|
);
|
|
17536
17651
|
}
|
|
17537
17652
|
}
|
|
17538
|
-
if (!dryRun &&
|
|
17653
|
+
if (!dryRun && existsSync34(a.path)) {
|
|
17539
17654
|
let dirty = false;
|
|
17540
17655
|
try {
|
|
17541
17656
|
dirty = status(a.path).length > 0;
|
|
@@ -17546,7 +17661,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
17546
17661
|
continue;
|
|
17547
17662
|
}
|
|
17548
17663
|
}
|
|
17549
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
17664
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync34(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
17550
17665
|
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
17551
17666
|
continue;
|
|
17552
17667
|
}
|
|
@@ -17595,7 +17710,7 @@ function isHandBackMerge(path, base) {
|
|
|
17595
17710
|
if (!msgPath) return false;
|
|
17596
17711
|
let first;
|
|
17597
17712
|
try {
|
|
17598
|
-
first =
|
|
17713
|
+
first = readFileSync31(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
17599
17714
|
} catch {
|
|
17600
17715
|
return false;
|
|
17601
17716
|
}
|
|
@@ -17700,12 +17815,12 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
17700
17815
|
}
|
|
17701
17816
|
|
|
17702
17817
|
// src/stream-sweep.ts
|
|
17703
|
-
import { existsSync as
|
|
17818
|
+
import { existsSync as existsSync35, readdirSync as readdirSync13, rmSync as rmSync15, statSync as statSync10 } from "fs";
|
|
17704
17819
|
import { join as join36 } from "path";
|
|
17705
17820
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
17706
17821
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
17707
17822
|
const dir = join36(stateDir, "streams");
|
|
17708
|
-
if (!
|
|
17823
|
+
if (!existsSync35(dir)) return [];
|
|
17709
17824
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
17710
17825
|
return readdirSync13(dir).map((name) => join36(dir, name)).filter((path) => {
|
|
17711
17826
|
try {
|
|
@@ -17733,7 +17848,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
17733
17848
|
return removed;
|
|
17734
17849
|
}
|
|
17735
17850
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
17736
|
-
if (!
|
|
17851
|
+
if (!existsSync35(stateDir)) return [];
|
|
17737
17852
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
17738
17853
|
return readdirSync13(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join36(stateDir, name)).filter((path) => {
|
|
17739
17854
|
try {
|
|
@@ -17745,11 +17860,11 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
17745
17860
|
}
|
|
17746
17861
|
|
|
17747
17862
|
// src/cli.ts
|
|
17748
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
17863
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync38, existsSync as existsSync43, mkdirSync as mkdirSync28, writeFileSync as writeFileSync30, rmSync as rmSync20, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync4, statSync as statSync14, readdirSync as readdirSync19 } from "fs";
|
|
17749
17864
|
import { createInterface } from "readline/promises";
|
|
17750
17865
|
|
|
17751
17866
|
// src/periscope-cmd.ts
|
|
17752
|
-
import { existsSync as
|
|
17867
|
+
import { existsSync as existsSync36, mkdirSync as mkdirSync22, readFileSync as readFileSync32, rmSync as rmSync16, writeFileSync as writeFileSync25 } from "fs";
|
|
17753
17868
|
import { join as join37 } from "path";
|
|
17754
17869
|
var PeriscopeError = class extends Error {
|
|
17755
17870
|
};
|
|
@@ -17767,15 +17882,15 @@ function removePeriscopePrior(stateDir, ticketRecordId) {
|
|
|
17767
17882
|
}
|
|
17768
17883
|
function writePeriscopePrior(stateDir, ticketRecordId, prior) {
|
|
17769
17884
|
try {
|
|
17770
|
-
|
|
17771
|
-
|
|
17885
|
+
mkdirSync22(priorDir(stateDir), { recursive: true });
|
|
17886
|
+
writeFileSync25(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
|
|
17772
17887
|
} catch {
|
|
17773
17888
|
}
|
|
17774
17889
|
}
|
|
17775
17890
|
function readPeriscopePrior(stateDir, ticketRecordId) {
|
|
17776
17891
|
try {
|
|
17777
17892
|
const p = priorPath(stateDir, ticketRecordId);
|
|
17778
|
-
return
|
|
17893
|
+
return existsSync36(p) ? JSON.parse(readFileSync32(p, "utf8")) : void 0;
|
|
17779
17894
|
} catch {
|
|
17780
17895
|
return void 0;
|
|
17781
17896
|
}
|
|
@@ -18270,8 +18385,8 @@ import { dirname as dirOf2, resolve as resolvePath } from "path";
|
|
|
18270
18385
|
|
|
18271
18386
|
// src/ssh-keys.ts
|
|
18272
18387
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
18273
|
-
import { existsSync as
|
|
18274
|
-
import { dirname as
|
|
18388
|
+
import { existsSync as existsSync37, mkdirSync as mkdirSync23, readFileSync as readFileSync33, chmodSync as chmodSync3 } from "fs";
|
|
18389
|
+
import { dirname as dirname21, join as join39 } from "path";
|
|
18275
18390
|
var SshKeyError = class extends Error {
|
|
18276
18391
|
};
|
|
18277
18392
|
function shipSshKeyPathFor(stateDir) {
|
|
@@ -18280,8 +18395,8 @@ function shipSshKeyPathFor(stateDir) {
|
|
|
18280
18395
|
function ensureShipSshKeypair(stateDir) {
|
|
18281
18396
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
18282
18397
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
18283
|
-
if (!
|
|
18284
|
-
|
|
18398
|
+
if (!existsSync37(privateKeyPath)) {
|
|
18399
|
+
mkdirSync23(dirname21(privateKeyPath), { recursive: true });
|
|
18285
18400
|
try {
|
|
18286
18401
|
execFileSync8("ssh-keygen", [
|
|
18287
18402
|
"-t",
|
|
@@ -18301,10 +18416,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
18301
18416
|
}
|
|
18302
18417
|
chmodSync3(privateKeyPath, 384);
|
|
18303
18418
|
}
|
|
18304
|
-
if (!
|
|
18419
|
+
if (!existsSync37(publicKeyPath)) {
|
|
18305
18420
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
18306
18421
|
}
|
|
18307
|
-
const publicKey =
|
|
18422
|
+
const publicKey = readFileSync33(publicKeyPath, "utf8").trim();
|
|
18308
18423
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
18309
18424
|
}
|
|
18310
18425
|
function sshKeygenAvailable() {
|
|
@@ -18336,6 +18451,16 @@ function describeRelayProbe(url, p) {
|
|
|
18336
18451
|
return p.ok ? `relay control: OK ${url}/control/status (${p.status}, ${p.latencyMs} ms)` : `relay control: FAILED ${url}/control/status \u2014 ${p.error}`;
|
|
18337
18452
|
}
|
|
18338
18453
|
|
|
18454
|
+
// src/ship-status.ts
|
|
18455
|
+
function shipIdentityJson(s) {
|
|
18456
|
+
return {
|
|
18457
|
+
id: s.id,
|
|
18458
|
+
kind: s.kind ?? null,
|
|
18459
|
+
instanceClass: s.instance_class ?? null,
|
|
18460
|
+
relayUrl: s.relay_url ?? null
|
|
18461
|
+
};
|
|
18462
|
+
}
|
|
18463
|
+
|
|
18339
18464
|
// src/passenger-containers.ts
|
|
18340
18465
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
18341
18466
|
import { createHash as createHash7 } from "crypto";
|
|
@@ -18542,7 +18667,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
18542
18667
|
|
|
18543
18668
|
// src/tunnel.ts
|
|
18544
18669
|
import { spawn as spawn5 } from "child_process";
|
|
18545
|
-
import { existsSync as
|
|
18670
|
+
import { existsSync as existsSync38, mkdirSync as mkdirSync24, readFileSync as readFileSync34, writeFileSync as writeFileSync26, unlinkSync as unlinkSync3, readdirSync as readdirSync14 } from "fs";
|
|
18546
18671
|
import { randomInt } from "crypto";
|
|
18547
18672
|
import { join as join40 } from "path";
|
|
18548
18673
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
@@ -18620,17 +18745,17 @@ function tunnelStatePath(stateDir, workspaceId) {
|
|
|
18620
18745
|
}
|
|
18621
18746
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
18622
18747
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
18623
|
-
if (!
|
|
18748
|
+
if (!existsSync38(path)) return void 0;
|
|
18624
18749
|
try {
|
|
18625
|
-
return JSON.parse(
|
|
18750
|
+
return JSON.parse(readFileSync34(path, "utf8"));
|
|
18626
18751
|
} catch {
|
|
18627
18752
|
return void 0;
|
|
18628
18753
|
}
|
|
18629
18754
|
}
|
|
18630
18755
|
function writePersistedTunnel(stateDir, state) {
|
|
18631
18756
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
18632
|
-
|
|
18633
|
-
|
|
18757
|
+
mkdirSync24(join40(stateDir, "passengers"), { recursive: true });
|
|
18758
|
+
writeFileSync26(path, `${JSON.stringify(state, null, 2)}
|
|
18634
18759
|
`);
|
|
18635
18760
|
}
|
|
18636
18761
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -18733,7 +18858,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
18733
18858
|
}
|
|
18734
18859
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
18735
18860
|
const dir = join40(stateDir, "passengers");
|
|
18736
|
-
if (!
|
|
18861
|
+
if (!existsSync38(dir)) return [];
|
|
18737
18862
|
return readdirSync14(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
18738
18863
|
}
|
|
18739
18864
|
|
|
@@ -18854,7 +18979,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
18854
18979
|
import { createServer as createHttpServer } from "http";
|
|
18855
18980
|
import { spawn as spawn6 } from "child_process";
|
|
18856
18981
|
import { randomBytes } from "crypto";
|
|
18857
|
-
import { existsSync as
|
|
18982
|
+
import { existsSync as existsSync39, mkdirSync as mkdirSync25, readFileSync as readFileSync35, writeFileSync as writeFileSync27, unlinkSync as unlinkSync4, readdirSync as readdirSync15 } from "fs";
|
|
18858
18983
|
import { join as join41 } from "path";
|
|
18859
18984
|
|
|
18860
18985
|
// src/secret-compare.ts
|
|
@@ -18948,17 +19073,17 @@ function syncDaemonStatePath(stateDir, workspaceId) {
|
|
|
18948
19073
|
}
|
|
18949
19074
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
18950
19075
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
18951
|
-
if (!
|
|
19076
|
+
if (!existsSync39(path)) return void 0;
|
|
18952
19077
|
try {
|
|
18953
|
-
return JSON.parse(
|
|
19078
|
+
return JSON.parse(readFileSync35(path, "utf8"));
|
|
18954
19079
|
} catch {
|
|
18955
19080
|
return void 0;
|
|
18956
19081
|
}
|
|
18957
19082
|
}
|
|
18958
19083
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
18959
19084
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
18960
|
-
|
|
18961
|
-
|
|
19085
|
+
mkdirSync25(join41(stateDir, "passengers"), { recursive: true });
|
|
19086
|
+
writeFileSync27(path, `${JSON.stringify(state, null, 2)}
|
|
18962
19087
|
`);
|
|
18963
19088
|
}
|
|
18964
19089
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -18979,7 +19104,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
18979
19104
|
}
|
|
18980
19105
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
18981
19106
|
const dir = join41(stateDir, "passengers");
|
|
18982
|
-
if (!
|
|
19107
|
+
if (!existsSync39(dir)) return [];
|
|
18983
19108
|
const suffix = "-sync.json";
|
|
18984
19109
|
return readdirSync15(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
18985
19110
|
}
|
|
@@ -19067,12 +19192,13 @@ function describeCause(err3) {
|
|
|
19067
19192
|
}
|
|
19068
19193
|
|
|
19069
19194
|
// src/daemon.ts
|
|
19070
|
-
import { existsSync as
|
|
19195
|
+
import { existsSync as existsSync40, mkdirSync as mkdirSync26, readFileSync as readFileSync36, readdirSync as readdirSync16, rmSync as rmSync17, statSync as statSync12, writeFileSync as writeFileSync28 } from "fs";
|
|
19071
19196
|
import { join as join42 } from "path";
|
|
19072
19197
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
19073
19198
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
19074
19199
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
19075
19200
|
var ALLOWANCE_PAUSE_MS = 5 * 60 * 1e3;
|
|
19201
|
+
var RELOAD_WARN_MS = 10 * 60 * 1e3;
|
|
19076
19202
|
var defaultSleep = (ms3) => new Promise((resolve7) => setTimeout(resolve7, ms3));
|
|
19077
19203
|
async function runDaemonLoop(o) {
|
|
19078
19204
|
const floor = o.floorMs ?? DEFAULT_FLOOR_MS;
|
|
@@ -19083,6 +19209,20 @@ async function runDaemonLoop(o) {
|
|
|
19083
19209
|
const now = o.now ?? Date.now;
|
|
19084
19210
|
let transientRun = 0;
|
|
19085
19211
|
let downSince = 0;
|
|
19212
|
+
const staleStop = async () => {
|
|
19213
|
+
const since2 = now();
|
|
19214
|
+
let warned = false;
|
|
19215
|
+
while (o.inFlight && o.inFlight.size > 0) {
|
|
19216
|
+
if (o.signal?.aborted) return false;
|
|
19217
|
+
if (!warned || now() - since2 >= RELOAD_WARN_MS) {
|
|
19218
|
+
o.onStaleDeferred?.({ runs: o.inFlight.size, waitedMs: now() - since2 });
|
|
19219
|
+
warned = true;
|
|
19220
|
+
}
|
|
19221
|
+
const drained = Promise.all([...o.inFlight].map((p) => p.catch(() => void 0))).then(() => void 0);
|
|
19222
|
+
await Promise.race([drained, sleep(RELOAD_WARN_MS)]);
|
|
19223
|
+
}
|
|
19224
|
+
return true;
|
|
19225
|
+
};
|
|
19086
19226
|
for (; ; ) {
|
|
19087
19227
|
if (o.signal?.aborted) return "aborted";
|
|
19088
19228
|
let result;
|
|
@@ -19119,12 +19259,13 @@ async function runDaemonLoop(o) {
|
|
|
19119
19259
|
backoff = floor;
|
|
19120
19260
|
o.onPass?.(result, 0);
|
|
19121
19261
|
if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
|
|
19262
|
+
if (o.checkStale?.() && await staleStop()) return "stale";
|
|
19122
19263
|
continue;
|
|
19123
19264
|
}
|
|
19124
19265
|
o.onPass?.(result, backoff);
|
|
19125
19266
|
if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
|
|
19126
19267
|
if (o.signal?.aborted) return "aborted";
|
|
19127
|
-
if (o.checkStale?.()) return "stale";
|
|
19268
|
+
if (o.checkStale?.() && await staleStop()) return "stale";
|
|
19128
19269
|
await sleep(backoff);
|
|
19129
19270
|
backoff = Math.min(backoff * 2, ceiling);
|
|
19130
19271
|
}
|
|
@@ -19149,23 +19290,30 @@ function computeTreeSignature(paths) {
|
|
|
19149
19290
|
}
|
|
19150
19291
|
function makeStaleChecker(paths, opts = {}) {
|
|
19151
19292
|
const initial = computeTreeSignature(paths);
|
|
19152
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
19293
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync40(opts.reloadSentinel);
|
|
19153
19294
|
}
|
|
19154
19295
|
function reloadSentinelFor(stateDir) {
|
|
19155
19296
|
return join42(stateDir, "reload-requested");
|
|
19156
19297
|
}
|
|
19298
|
+
function readReloadRequestedAt(stateDir) {
|
|
19299
|
+
try {
|
|
19300
|
+
return statSync12(reloadSentinelFor(stateDir)).mtimeMs;
|
|
19301
|
+
} catch {
|
|
19302
|
+
return null;
|
|
19303
|
+
}
|
|
19304
|
+
}
|
|
19157
19305
|
function daemonStampFor(stateDir) {
|
|
19158
19306
|
return join42(stateDir, "daemon.started");
|
|
19159
19307
|
}
|
|
19160
19308
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
19161
|
-
|
|
19309
|
+
mkdirSync26(stateDir, { recursive: true });
|
|
19162
19310
|
rmSync17(reloadSentinelFor(stateDir), { force: true });
|
|
19163
|
-
|
|
19311
|
+
writeFileSync28(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
19164
19312
|
`);
|
|
19165
19313
|
}
|
|
19166
19314
|
function readDaemonStarted(stateDir) {
|
|
19167
19315
|
try {
|
|
19168
|
-
const v = JSON.parse(
|
|
19316
|
+
const v = JSON.parse(readFileSync36(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
19169
19317
|
return typeof v === "number" ? v : null;
|
|
19170
19318
|
} catch {
|
|
19171
19319
|
return null;
|
|
@@ -19545,6 +19693,7 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
19545
19693
|
if (seat) {
|
|
19546
19694
|
const candidates2 = rankedCandidates(role, decision.selectionInput);
|
|
19547
19695
|
const contract = decision.selectionInput.contract ?? DEFAULT_CONTRACT;
|
|
19696
|
+
await refreshUnknownRepos({ route: route2, stateDir: ship.stateDir, fetchRows: () => tracker.repoRowsForMap(), tickets: candidates2 });
|
|
19548
19697
|
const result = await resolveTopCandidate(
|
|
19549
19698
|
tracker,
|
|
19550
19699
|
candidates2,
|
|
@@ -19571,6 +19720,11 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
19571
19720
|
role
|
|
19572
19721
|
});
|
|
19573
19722
|
}
|
|
19723
|
+
await reportUnservable(
|
|
19724
|
+
{ state: ctx.state, emit, route: route2, ship },
|
|
19725
|
+
result.unservable,
|
|
19726
|
+
(id) => repoLabelFor(route2, candidates2.find((c) => c.issue_id === id))
|
|
19727
|
+
);
|
|
19574
19728
|
if (result.held.length) {
|
|
19575
19729
|
emit.emit(`skipped ${result.held.join(", ")} \u2014 held by another live ship`, { step: "select", role });
|
|
19576
19730
|
}
|
|
@@ -19714,7 +19868,7 @@ import { createHash as createHash9 } from "crypto";
|
|
|
19714
19868
|
import {
|
|
19715
19869
|
chmodSync as chmodSync4,
|
|
19716
19870
|
closeSync as closeSync5,
|
|
19717
|
-
existsSync as
|
|
19871
|
+
existsSync as existsSync41,
|
|
19718
19872
|
fstatSync,
|
|
19719
19873
|
openSync as openSync5,
|
|
19720
19874
|
readSync as readSync3,
|
|
@@ -19725,7 +19879,7 @@ import {
|
|
|
19725
19879
|
} from "fs";
|
|
19726
19880
|
import { createConnection } from "net";
|
|
19727
19881
|
import { createServer } from "http";
|
|
19728
|
-
import { basename as basename12, dirname as
|
|
19882
|
+
import { basename as basename12, dirname as dirname22, join as join43 } from "path";
|
|
19729
19883
|
import { execFile, spawn as spawn8 } from "child_process";
|
|
19730
19884
|
|
|
19731
19885
|
// src/periscope-control.ts
|
|
@@ -20029,7 +20183,7 @@ function candidates(live, rotated) {
|
|
|
20029
20183
|
const out = [live];
|
|
20030
20184
|
if (!rotated) return out;
|
|
20031
20185
|
try {
|
|
20032
|
-
for (const n of readdirSync17(
|
|
20186
|
+
for (const n of readdirSync17(dirname22(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join43(dirname22(live), n));
|
|
20033
20187
|
} catch {
|
|
20034
20188
|
}
|
|
20035
20189
|
return out;
|
|
@@ -20073,7 +20227,7 @@ function lastLines2(path, n) {
|
|
|
20073
20227
|
}
|
|
20074
20228
|
function readTail(live, opts) {
|
|
20075
20229
|
const max = opts.maxBytes ?? MAX_TAIL_BYTES;
|
|
20076
|
-
if (!
|
|
20230
|
+
if (!existsSync41(live)) {
|
|
20077
20231
|
return { lines: [], cursor: opts.after ?? encodeCursor({ dev: 0, ino: 0, off: 0 }), reset: false };
|
|
20078
20232
|
}
|
|
20079
20233
|
const cur = opts.after ? decodeCursor(opts.after) : null;
|
|
@@ -20189,7 +20343,7 @@ async function tailReply(path, q, rotated, abort) {
|
|
|
20189
20343
|
const deadline = Date.now() + Math.min(wait, MAX_WAIT_SECONDS) * 1e3;
|
|
20190
20344
|
let r = readTail(path, { after, tail: tail3, rotated });
|
|
20191
20345
|
while (after && !r.lines.length && !r.reset && Date.now() < deadline && !abort.aborted) {
|
|
20192
|
-
await waitForChange(
|
|
20346
|
+
await waitForChange(dirname22(path), deadline - Date.now(), abort);
|
|
20193
20347
|
r = readTail(path, { after: r.cursor, rotated });
|
|
20194
20348
|
}
|
|
20195
20349
|
return reply2(200, r);
|
|
@@ -20230,7 +20384,7 @@ async function route(deps, method, url, req, abort) {
|
|
|
20230
20384
|
const run2 = decodeURIComponent(p.slice("/streams/".length));
|
|
20231
20385
|
if (!RUN_NAME_RE.test(run2) || run2.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run2)) return err2(400, "not a run name");
|
|
20232
20386
|
const file = join43(deps.stateDir, "streams", run2.endsWith(".jsonl") ? run2 : `${run2}.jsonl`);
|
|
20233
|
-
if (!
|
|
20387
|
+
if (!existsSync41(file)) return err2(404, "no such run");
|
|
20234
20388
|
return tailReply(file, url.searchParams, false, abort);
|
|
20235
20389
|
}
|
|
20236
20390
|
if ((p === "/pause" || p === "/resume") && post3) {
|
|
@@ -20274,7 +20428,7 @@ function send(res, r) {
|
|
|
20274
20428
|
res.end(body);
|
|
20275
20429
|
}
|
|
20276
20430
|
async function socketIsStale(path) {
|
|
20277
|
-
if (!
|
|
20431
|
+
if (!existsSync41(path)) return false;
|
|
20278
20432
|
return new Promise((resolve7) => {
|
|
20279
20433
|
const c = createConnection(path);
|
|
20280
20434
|
c.once("connect", () => {
|
|
@@ -20293,7 +20447,7 @@ async function startControlServer(deps) {
|
|
|
20293
20447
|
return null;
|
|
20294
20448
|
}
|
|
20295
20449
|
if (await socketIsStale(path)) rmSync18(path, { force: true });
|
|
20296
|
-
else if (
|
|
20450
|
+
else if (existsSync41(path)) {
|
|
20297
20451
|
log(`control API not started: ${path} is in use by another process`);
|
|
20298
20452
|
return null;
|
|
20299
20453
|
}
|
|
@@ -20376,31 +20530,31 @@ function crewJson(args, timeoutMs = 6e4) {
|
|
|
20376
20530
|
|
|
20377
20531
|
// src/periscope-mcp.ts
|
|
20378
20532
|
import { lstat, mkdir, readdir, readFile, readlink, realpath, rm, stat, writeFile } from "fs/promises";
|
|
20379
|
-
import { basename as basename13, dirname as
|
|
20533
|
+
import { basename as basename13, dirname as dirname23, isAbsolute as isAbsolute3, join as join45, relative as relative2, resolve as resolve5, sep } from "path";
|
|
20380
20534
|
import { z } from "zod";
|
|
20381
20535
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
20382
20536
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
20383
20537
|
|
|
20384
20538
|
// src/periscope-endpoints.ts
|
|
20385
20539
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
20386
|
-
import { chmodSync as chmodSync5, existsSync as
|
|
20540
|
+
import { chmodSync as chmodSync5, existsSync as existsSync42, mkdirSync as mkdirSync27, readdirSync as readdirSync18, readFileSync as readFileSync37, rmSync as rmSync19, writeFileSync as writeFileSync29 } from "fs";
|
|
20387
20541
|
import { join as join44 } from "path";
|
|
20388
20542
|
var secretDir = (stateDir) => join44(stateDir, "periscope-mcp");
|
|
20389
20543
|
var secretFile = (stateDir, id) => join44(secretDir(stateDir), `${id}.json`);
|
|
20390
20544
|
var endpointDir = (stateDir) => join44(stateDir, "periscope-endpoints");
|
|
20391
20545
|
var endpointFile = (stateDir, id) => join44(endpointDir(stateDir), `${id}.json`);
|
|
20392
20546
|
function mintPeriscopeSecret(stateDir, ticketRecordId) {
|
|
20393
|
-
|
|
20547
|
+
mkdirSync27(secretDir(stateDir), { recursive: true, mode: 448 });
|
|
20394
20548
|
const secret = randomBytes3(32).toString("base64url");
|
|
20395
20549
|
const file = secretFile(stateDir, ticketRecordId);
|
|
20396
|
-
|
|
20550
|
+
writeFileSync29(file, `${JSON.stringify({ secret })}
|
|
20397
20551
|
`, { mode: 384 });
|
|
20398
20552
|
chmodSync5(file, 384);
|
|
20399
20553
|
return secret;
|
|
20400
20554
|
}
|
|
20401
20555
|
function readPeriscopeSecret(stateDir, ticketRecordId) {
|
|
20402
20556
|
try {
|
|
20403
|
-
const v = JSON.parse(
|
|
20557
|
+
const v = JSON.parse(readFileSync37(secretFile(stateDir, ticketRecordId), "utf8"));
|
|
20404
20558
|
return typeof v.secret === "string" && v.secret ? v.secret : void 0;
|
|
20405
20559
|
} catch {
|
|
20406
20560
|
return void 0;
|
|
@@ -20411,14 +20565,14 @@ function removePeriscopeSecret(stateDir, ticketRecordId) {
|
|
|
20411
20565
|
}
|
|
20412
20566
|
function readPeriscopeEndpoint(stateDir, ticketRecordId) {
|
|
20413
20567
|
try {
|
|
20414
|
-
return JSON.parse(
|
|
20568
|
+
return JSON.parse(readFileSync37(endpointFile(stateDir, ticketRecordId), "utf8"));
|
|
20415
20569
|
} catch {
|
|
20416
20570
|
return void 0;
|
|
20417
20571
|
}
|
|
20418
20572
|
}
|
|
20419
20573
|
function writePeriscopeEndpoint(stateDir, rec) {
|
|
20420
|
-
|
|
20421
|
-
|
|
20574
|
+
mkdirSync27(endpointDir(stateDir), { recursive: true });
|
|
20575
|
+
writeFileSync29(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
|
|
20422
20576
|
`);
|
|
20423
20577
|
}
|
|
20424
20578
|
function removePeriscopeEndpointRecord(stateDir, ticketRecordId) {
|
|
@@ -20426,7 +20580,7 @@ function removePeriscopeEndpointRecord(stateDir, ticketRecordId) {
|
|
|
20426
20580
|
}
|
|
20427
20581
|
function listPeriscopeEndpoints(stateDir) {
|
|
20428
20582
|
const dir = endpointDir(stateDir);
|
|
20429
|
-
if (!
|
|
20583
|
+
if (!existsSync42(dir)) return [];
|
|
20430
20584
|
const out = [];
|
|
20431
20585
|
for (const name of readdirSync18(dir)) {
|
|
20432
20586
|
if (!name.endsWith(".json")) continue;
|
|
@@ -20513,10 +20667,10 @@ async function realAncestor(p) {
|
|
|
20513
20667
|
}
|
|
20514
20668
|
if (isLink) {
|
|
20515
20669
|
if (++hops > 40) throw new PathRefusal(`too many symbolic links resolving ${p}`);
|
|
20516
|
-
cur = resolve5(
|
|
20670
|
+
cur = resolve5(dirname23(cur), await readlink(cur));
|
|
20517
20671
|
continue;
|
|
20518
20672
|
}
|
|
20519
|
-
const parent =
|
|
20673
|
+
const parent = dirname23(cur);
|
|
20520
20674
|
if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
|
|
20521
20675
|
tail3.push(basename13(cur));
|
|
20522
20676
|
cur = parent;
|
|
@@ -20627,7 +20781,7 @@ function createPeriscopeMcpServer(d, sc) {
|
|
|
20627
20781
|
if (Buffer.byteLength(content) > MAX_WRITE_BYTES) return text(`content exceeds ${MAX_WRITE_BYTES} bytes`, true);
|
|
20628
20782
|
const abs = await inTree(path);
|
|
20629
20783
|
if (abs === await realpath(root)) return text("path must name a file", true);
|
|
20630
|
-
await mkdir(
|
|
20784
|
+
await mkdir(dirname23(abs), { recursive: true });
|
|
20631
20785
|
await inTree(path);
|
|
20632
20786
|
await writeFile(abs, content);
|
|
20633
20787
|
return text(`Wrote ${Buffer.byteLength(content)} bytes to ${await rel(abs)}`);
|
|
@@ -20667,7 +20821,7 @@ function createPeriscopeMcpServer(d, sc) {
|
|
|
20667
20821
|
inputSchema: { path: z.string() },
|
|
20668
20822
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
20669
20823
|
}, async ({ path }) => {
|
|
20670
|
-
const parent = await inTree(
|
|
20824
|
+
const parent = await inTree(dirname23(path) === "" ? "." : dirname23(path));
|
|
20671
20825
|
const abs = join45(parent, basename13(path));
|
|
20672
20826
|
if (basename13(path) === ".git" || abs === parent) return text("path must name a file", true);
|
|
20673
20827
|
const st = await lstat(abs).catch(() => void 0);
|
|
@@ -20789,7 +20943,7 @@ function findPeriscope(sidecars, segment) {
|
|
|
20789
20943
|
}
|
|
20790
20944
|
|
|
20791
20945
|
// src/cli.ts
|
|
20792
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
20946
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname24(process.execPath) : resolve6(dirname24(fileURLToPath(import.meta.url)), "..");
|
|
20793
20947
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
20794
20948
|
var CYCLE_SECONDS2 = 120;
|
|
20795
20949
|
var STALE_CYCLES = 3;
|
|
@@ -21117,6 +21271,7 @@ function findSyncRoute(c, name) {
|
|
|
21117
21271
|
function daemonConfigPaths(c) {
|
|
21118
21272
|
const files = [c.configFile];
|
|
21119
21273
|
for (const r of c.routes) if (r.apiKeyFile && !files.includes(r.apiKeyFile)) files.push(r.apiKeyFile);
|
|
21274
|
+
for (const r of c.routes) files.push(resolvedPathFor(c.ship.stateDir, r.route));
|
|
21120
21275
|
return files;
|
|
21121
21276
|
}
|
|
21122
21277
|
var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "provision", "disconnect", "install", "uninstall", "reload", "release", "merge", "deploy"]);
|
|
@@ -21306,7 +21461,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21306
21461
|
} else {
|
|
21307
21462
|
const toleratesNoRoutes = command === "credential" || command === "forge" || command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
21308
21463
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
21309
|
-
if (configless && !
|
|
21464
|
+
if (configless && !existsSync43(findConfigFile(CREW_HOME))) {
|
|
21310
21465
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
21311
21466
|
} else {
|
|
21312
21467
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -21321,7 +21476,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21321
21476
|
throw e;
|
|
21322
21477
|
}
|
|
21323
21478
|
if (command === "credential") {
|
|
21324
|
-
const r = runCredential(positional[1],
|
|
21479
|
+
const r = runCredential(positional[1], readFileSync38(0, "utf8"), cfg.ship.forges, cfg.ship.stateDir);
|
|
21325
21480
|
process.stdout.write(r.out);
|
|
21326
21481
|
process.stderr.write(r.err);
|
|
21327
21482
|
process.exit(r.code);
|
|
@@ -21333,9 +21488,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
21333
21488
|
forges: cfg.ship.forges,
|
|
21334
21489
|
stateDir: cfg.ship.stateDir,
|
|
21335
21490
|
configFile: cfg.configFile,
|
|
21336
|
-
readFile: (p) =>
|
|
21337
|
-
writeFile: (p, t) =>
|
|
21338
|
-
stdin: () =>
|
|
21491
|
+
readFile: (p) => readFileSync38(p, "utf8"),
|
|
21492
|
+
writeFile: (p, t) => writeFileSync30(p, t),
|
|
21493
|
+
stdin: () => readFileSync38(0, "utf8"),
|
|
21339
21494
|
env: process.env
|
|
21340
21495
|
}
|
|
21341
21496
|
);
|
|
@@ -21619,7 +21774,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21619
21774
|
if (host === "linux" && hasSystemd) {
|
|
21620
21775
|
const label = labelFor(CREW_HOME, "run");
|
|
21621
21776
|
const staleTimer = join46(homedir10(), ".config", "systemd", "user", `${label}.timer`);
|
|
21622
|
-
if (
|
|
21777
|
+
if (existsSync43(staleTimer)) {
|
|
21623
21778
|
process.stdout.write(
|
|
21624
21779
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
21625
21780
|
`
|
|
@@ -21646,7 +21801,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21646
21801
|
const tracker = new Tracker(c, cfg.ship);
|
|
21647
21802
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
21648
21803
|
for (const r of await resolvedRepos(c)) {
|
|
21649
|
-
if (!
|
|
21804
|
+
if (!existsSync43(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
21650
21805
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
21651
21806
|
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
21652
21807
|
}
|
|
@@ -21664,7 +21819,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21664
21819
|
}
|
|
21665
21820
|
async function doctorFix(o) {
|
|
21666
21821
|
const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
|
|
21667
|
-
const unitExists = (p) =>
|
|
21822
|
+
const unitExists = (p) => existsSync43(p);
|
|
21668
21823
|
const crontab = o.host === "linux" ? spawnSync8("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
21669
21824
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run", installContext()), unitExists, crontab) : true;
|
|
21670
21825
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -21681,7 +21836,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21681
21836
|
for (const a of plan.actions) {
|
|
21682
21837
|
if (a.kind === "enable") {
|
|
21683
21838
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
21684
|
-
|
|
21839
|
+
writeFileSync30(cfg.configFile, planEnable(readFileSync38(cfg.configFile, "utf8"), a.route));
|
|
21685
21840
|
enabledSound.add(a.route);
|
|
21686
21841
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
21687
21842
|
} else {
|
|
@@ -22360,6 +22515,7 @@ ${cu.tail.trimEnd()}
|
|
|
22360
22515
|
if (seat) {
|
|
22361
22516
|
const candidates2 = rankedCandidates(w.role, w.decision.selectionInput);
|
|
22362
22517
|
const contract = w.decision.selectionInput.contract ?? DEFAULT_CONTRACT;
|
|
22518
|
+
await refreshUnknownRepos({ route: w.route, stateDir: cfg.ship.stateDir, fetchRows: () => fleetTracker.repoRowsForMap(), tickets: candidates2 });
|
|
22363
22519
|
const result = await resolveTopCandidate(
|
|
22364
22520
|
fleetTracker,
|
|
22365
22521
|
candidates2,
|
|
@@ -22571,6 +22727,7 @@ ${cu.tail.trimEnd()}
|
|
|
22571
22727
|
if (seat) {
|
|
22572
22728
|
const candidates2 = rankedCandidates(current, decision.selectionInput);
|
|
22573
22729
|
const contract = decision.selectionInput.contract ?? DEFAULT_CONTRACT;
|
|
22730
|
+
await refreshUnknownRepos({ route: route2, stateDir: cfg.ship.stateDir, fetchRows: () => tracker2.repoRowsForMap(), tickets: candidates2 });
|
|
22574
22731
|
const result = await resolveTopCandidate(
|
|
22575
22732
|
tracker2,
|
|
22576
22733
|
candidates2,
|
|
@@ -22852,8 +23009,14 @@ ${cu.tail.trimEnd()}
|
|
|
22852
23009
|
log: (m) => emit.warn(m)
|
|
22853
23010
|
});
|
|
22854
23011
|
if (controlServer) emit.emit(`control API listening on ${controlServer.path}`);
|
|
23012
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
22855
23013
|
const stopReason = await runDaemonLoop({
|
|
23014
|
+
inFlight,
|
|
23015
|
+
onStaleDeferred: ({ runs, waitedMs }) => emit.warn(
|
|
23016
|
+
`reload or update pending for ${Math.round(waitedMs / 1e3)}s \u2014 waiting for ${runs} agent run(s) to end before relaunching (\`crew reload --now\` forces it)`
|
|
23017
|
+
),
|
|
22856
23018
|
runPass: () => runOnePass({
|
|
23019
|
+
inFlight,
|
|
22857
23020
|
route: route2,
|
|
22858
23021
|
ship: cfg.ship,
|
|
22859
23022
|
state,
|
|
@@ -23077,10 +23240,10 @@ ${cu.tail.trimEnd()}
|
|
|
23077
23240
|
`);
|
|
23078
23241
|
if (!dryRun) {
|
|
23079
23242
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
23080
|
-
const raw =
|
|
23243
|
+
const raw = existsSync43(resolvedPath) ? JSON.parse(readFileSync38(resolvedPath, "utf8")) : {};
|
|
23081
23244
|
raw.agentSkills = result.agentSkills;
|
|
23082
|
-
|
|
23083
|
-
|
|
23245
|
+
mkdirSync28(dirOf2(resolvedPath), { recursive: true });
|
|
23246
|
+
writeFileSync30(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
23084
23247
|
`);
|
|
23085
23248
|
}
|
|
23086
23249
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -23103,7 +23266,7 @@ ${cu.tail.trimEnd()}
|
|
|
23103
23266
|
throw e;
|
|
23104
23267
|
}
|
|
23105
23268
|
const oldName = cfg.ship.name;
|
|
23106
|
-
|
|
23269
|
+
writeFileSync30(cfg.configFile, setShipNameInYaml(readFileSync38(cfg.configFile, "utf8"), newName));
|
|
23107
23270
|
cfg.ship.name = newName;
|
|
23108
23271
|
let renameMachineId;
|
|
23109
23272
|
try {
|
|
@@ -23150,14 +23313,14 @@ ${cu.tail.trimEnd()}
|
|
|
23150
23313
|
process.exit(2);
|
|
23151
23314
|
};
|
|
23152
23315
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
|
|
23153
|
-
const configText =
|
|
23316
|
+
const configText = readFileSync38(cfg.configFile, "utf8");
|
|
23154
23317
|
if (sub === "check") {
|
|
23155
23318
|
try {
|
|
23156
23319
|
const routeArg = value("route") ?? positional[2];
|
|
23157
23320
|
const found = cfg.routes.find((r) => r.route === routeArg);
|
|
23158
23321
|
if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
|
|
23159
23322
|
const named2 = value("route") ? void 0 : positional[3];
|
|
23160
|
-
const dir = named2 ? repoDirOf(configText,
|
|
23323
|
+
const dir = named2 ? repoDirOf(configText, dirname24(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
|
|
23161
23324
|
const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
|
|
23162
23325
|
const o = target ? found.repoOverrides[target.name] : void 0;
|
|
23163
23326
|
runReposCheck(dir, {
|
|
@@ -23180,9 +23343,9 @@ ${cu.tail.trimEnd()}
|
|
|
23180
23343
|
if (sub === "hooks") {
|
|
23181
23344
|
try {
|
|
23182
23345
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
23183
|
-
const dir = repoDirOf(configText,
|
|
23184
|
-
if (!
|
|
23185
|
-
const files = { exists: (r) =>
|
|
23346
|
+
const dir = repoDirOf(configText, dirname24(cfg.configFile), a.route, a.name);
|
|
23347
|
+
if (!existsSync43(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
23348
|
+
const files = { exists: (r) => existsSync43(resolvePath(dir, r)), list: (r) => {
|
|
23186
23349
|
try {
|
|
23187
23350
|
return readdirSync19(resolvePath(dir, r));
|
|
23188
23351
|
} catch {
|
|
@@ -23190,7 +23353,7 @@ ${cu.tail.trimEnd()}
|
|
|
23190
23353
|
}
|
|
23191
23354
|
}, read: (r) => {
|
|
23192
23355
|
try {
|
|
23193
|
-
return
|
|
23356
|
+
return readFileSync38(resolvePath(dir, r), "utf8");
|
|
23194
23357
|
} catch {
|
|
23195
23358
|
return void 0;
|
|
23196
23359
|
}
|
|
@@ -23232,19 +23395,19 @@ ${cu.tail.trimEnd()}
|
|
|
23232
23395
|
let cleared;
|
|
23233
23396
|
if (clear2) {
|
|
23234
23397
|
const plan = clearRouteHooks(configText, a.route, a.name);
|
|
23235
|
-
if (plan.removed.length > 0)
|
|
23398
|
+
if (plan.removed.length > 0) writeFileSync30(cfg.configFile, plan.text);
|
|
23236
23399
|
cleared = { file: cfg.configFile, removed: plan.removed };
|
|
23237
23400
|
}
|
|
23238
23401
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
23239
23402
|
if (write === "route") {
|
|
23240
23403
|
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
|
|
23241
|
-
|
|
23404
|
+
writeFileSync30(cfg.configFile, plan.text);
|
|
23242
23405
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
|
|
23243
23406
|
} else if (write === "repo") {
|
|
23244
23407
|
const have = existingRepoConfig(files);
|
|
23245
23408
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
23246
23409
|
const file = resolvePath(dir, ".crew.yaml");
|
|
23247
|
-
|
|
23410
|
+
writeFileSync30(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
23248
23411
|
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
|
|
23249
23412
|
}
|
|
23250
23413
|
if (flag("json")) {
|
|
@@ -23300,7 +23463,7 @@ ${cu.tail.trimEnd()}
|
|
|
23300
23463
|
if (sub === "list" && !routeName) usage2();
|
|
23301
23464
|
if (sub === "add") {
|
|
23302
23465
|
try {
|
|
23303
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
23466
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync43(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
|
|
23304
23467
|
routeName = a.route;
|
|
23305
23468
|
addPath = a.path;
|
|
23306
23469
|
addName = a.name;
|
|
@@ -23328,17 +23491,27 @@ ${cu.tail.trimEnd()}
|
|
|
23328
23491
|
}
|
|
23329
23492
|
}
|
|
23330
23493
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
23494
|
+
if (sub === "list" || sub === "add") {
|
|
23495
|
+
const liveRoute = cfg.routes.find((r) => r.route === routeName);
|
|
23496
|
+
if (liveRoute?.resolved?.reposModelId && existsSync43(resolvedFile) && !dryRun) {
|
|
23497
|
+
try {
|
|
23498
|
+
await hydrateApiKeys([liveRoute]);
|
|
23499
|
+
persistRepoRows(cfg.ship.stateDir, liveRoute.route, await new Tracker(liveRoute, cfg.ship).repoRowsForMap());
|
|
23500
|
+
} catch {
|
|
23501
|
+
}
|
|
23502
|
+
}
|
|
23503
|
+
}
|
|
23331
23504
|
let resolvedRemotes;
|
|
23332
|
-
if (
|
|
23505
|
+
if (existsSync43(resolvedFile)) {
|
|
23333
23506
|
try {
|
|
23334
|
-
resolvedRemotes = JSON.parse(
|
|
23507
|
+
resolvedRemotes = JSON.parse(readFileSync38(resolvedFile, "utf8"));
|
|
23335
23508
|
} catch {
|
|
23336
23509
|
}
|
|
23337
23510
|
}
|
|
23338
23511
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
23339
23512
|
const text2 = configText;
|
|
23340
|
-
const base =
|
|
23341
|
-
const isCheckout = (d) =>
|
|
23513
|
+
const base = dirname24(cfg.configFile);
|
|
23514
|
+
const isCheckout = (d) => existsSync43(`${d}/.git`);
|
|
23342
23515
|
try {
|
|
23343
23516
|
if (sub === "list") {
|
|
23344
23517
|
if (flag("json")) {
|
|
@@ -23348,11 +23521,11 @@ ${cu.tail.trimEnd()}
|
|
|
23348
23521
|
base,
|
|
23349
23522
|
resolved: resolvedRemotes,
|
|
23350
23523
|
originOf,
|
|
23351
|
-
exists: (d) =>
|
|
23524
|
+
exists: (d) => existsSync43(d),
|
|
23352
23525
|
isCheckout,
|
|
23353
23526
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
23354
23527
|
try {
|
|
23355
|
-
return Object.keys(parseRepoConfig(
|
|
23528
|
+
return Object.keys(parseRepoConfig(readFileSync38(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
23356
23529
|
} catch {
|
|
23357
23530
|
return false;
|
|
23358
23531
|
}
|
|
@@ -23369,7 +23542,7 @@ ${cu.tail.trimEnd()}
|
|
|
23369
23542
|
for (const r of repos) {
|
|
23370
23543
|
const origin2 = originOf(r.dir);
|
|
23371
23544
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
23372
|
-
const note = !
|
|
23545
|
+
const note = !existsSync43(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
23373
23546
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
23374
23547
|
`);
|
|
23375
23548
|
}
|
|
@@ -23381,9 +23554,9 @@ ${cu.tail.trimEnd()}
|
|
|
23381
23554
|
` : `${human}
|
|
23382
23555
|
`);
|
|
23383
23556
|
const plan2 = planRepoClone({ text: text2, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
23384
|
-
if (
|
|
23557
|
+
if (existsSync43(plan2.target) && (existsSync43(`${plan2.target}/.git`) || readdirSync19(plan2.target).length > 0)) {
|
|
23385
23558
|
throw new ReposError(
|
|
23386
|
-
|
|
23559
|
+
existsSync43(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
|
|
23387
23560
|
);
|
|
23388
23561
|
}
|
|
23389
23562
|
if (dryRun) {
|
|
@@ -23401,14 +23574,14 @@ ${cu.tail.trimEnd()}
|
|
|
23401
23574
|
throw new ReposError(`${e.message}${authHint}
|
|
23402
23575
|
crew.yaml was not changed`);
|
|
23403
23576
|
}
|
|
23404
|
-
|
|
23577
|
+
writeFileSync30(cfg.configFile, plan2.text);
|
|
23405
23578
|
if (plan2.droppedDir) process.stderr.write(`crew repos: dropped dir: ${plan2.droppedDir} from route ${routeName} \u2014 it is not a checkout (the clone base); checkouts now live under repos:
|
|
23406
23579
|
`);
|
|
23407
23580
|
say("attached", { route: routeName, name: plan2.name, dir: plan2.target, mode: plan2.mode }, `cloned and attached ${plan2.target} to ${routeName} as ${plan2.mode === "dir" ? "dir" : `repos.${plan2.name}`} in ${cfg.configFile}`);
|
|
23408
23581
|
break;
|
|
23409
23582
|
}
|
|
23410
23583
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
|
|
23411
|
-
const top =
|
|
23584
|
+
const top = existsSync43(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
23412
23585
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
23413
23586
|
const origin = originOf(top);
|
|
23414
23587
|
const plan = planRepoAdd({
|
|
@@ -23460,15 +23633,15 @@ ${cu.tail.trimEnd()}
|
|
|
23460
23633
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
23461
23634
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
23462
23635
|
`);
|
|
23463
|
-
if (
|
|
23464
|
-
const raw = JSON.parse(
|
|
23636
|
+
if (existsSync43(resolvedFile)) {
|
|
23637
|
+
const raw = JSON.parse(readFileSync38(resolvedFile, "utf8"));
|
|
23465
23638
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
23466
23639
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
23467
|
-
|
|
23640
|
+
writeFileSync30(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
23468
23641
|
`);
|
|
23469
23642
|
}
|
|
23470
23643
|
}
|
|
23471
|
-
|
|
23644
|
+
writeFileSync30(cfg.configFile, plan.text);
|
|
23472
23645
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
23473
23646
|
`);
|
|
23474
23647
|
} catch (e) {
|
|
@@ -23835,7 +24008,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23835
24008
|
process.exit(2);
|
|
23836
24009
|
}
|
|
23837
24010
|
if (found.shipNameAdopted && !dryRun) {
|
|
23838
|
-
|
|
24011
|
+
writeFileSync30(cfg.configFile, setShipNameInYaml(readFileSync38(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
23839
24012
|
cfg.ship.name = found.shipNameAdopted;
|
|
23840
24013
|
}
|
|
23841
24014
|
if (found.shipRow) {
|
|
@@ -23878,10 +24051,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23878
24051
|
}
|
|
23879
24052
|
}
|
|
23880
24053
|
let previous;
|
|
23881
|
-
const hadPreviousFile =
|
|
24054
|
+
const hadPreviousFile = existsSync43(resolvedPath);
|
|
23882
24055
|
if (hadPreviousFile) {
|
|
23883
24056
|
try {
|
|
23884
|
-
previous = JSON.parse(
|
|
24057
|
+
previous = JSON.parse(readFileSync38(resolvedPath, "utf8"));
|
|
23885
24058
|
} catch {
|
|
23886
24059
|
}
|
|
23887
24060
|
}
|
|
@@ -24011,10 +24184,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
24011
24184
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
24012
24185
|
`);
|
|
24013
24186
|
} else {
|
|
24014
|
-
|
|
24187
|
+
mkdirSync28(dirname24(resolvedPath), { recursive: true });
|
|
24015
24188
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
24016
24189
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
24017
|
-
|
|
24190
|
+
writeFileSync30(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
24018
24191
|
`);
|
|
24019
24192
|
renameSync4(tmpPath, resolvedPath);
|
|
24020
24193
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -24060,8 +24233,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
24060
24233
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
24061
24234
|
`);
|
|
24062
24235
|
} else {
|
|
24063
|
-
|
|
24064
|
-
|
|
24236
|
+
mkdirSync28(dirname24(apiKeyFile), { recursive: true });
|
|
24237
|
+
writeFileSync30(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
24065
24238
|
`, { mode: 384 });
|
|
24066
24239
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
24067
24240
|
`);
|
|
@@ -24797,7 +24970,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
24797
24970
|
const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
|
|
24798
24971
|
const resumed = await runPeriscopeResume({
|
|
24799
24972
|
find: async () => hit && { row: hit.row, tag: hit.tag },
|
|
24800
|
-
worktreeExists: (p) =>
|
|
24973
|
+
worktreeExists: (p) => existsSync43(p),
|
|
24801
24974
|
caps: periscopeCaps(cfg.ship),
|
|
24802
24975
|
runHook: async (name, cwd, env) => {
|
|
24803
24976
|
const script = hit?.repo.config.hooks[name];
|
|
@@ -24892,8 +25065,8 @@ ${where.join("\n")}`);
|
|
|
24892
25065
|
prepare: async (path) => {
|
|
24893
25066
|
for (const f of repoP.config.worktrees.copy) {
|
|
24894
25067
|
const src = join46(repoP.dir, f);
|
|
24895
|
-
if (!
|
|
24896
|
-
|
|
25068
|
+
if (!existsSync43(src)) continue;
|
|
25069
|
+
mkdirSync28(dirOf2(join46(path, f)), { recursive: true });
|
|
24897
25070
|
copyFileSync2(src, join46(path, f));
|
|
24898
25071
|
}
|
|
24899
25072
|
const setup = repoP.config.hooks.setup;
|
|
@@ -25187,7 +25360,7 @@ ${where.join("\n")}`);
|
|
|
25187
25360
|
};
|
|
25188
25361
|
const droppedRoutes = [];
|
|
25189
25362
|
try {
|
|
25190
|
-
const rawCfg = parseYaml2(
|
|
25363
|
+
const rawCfg = parseYaml2(readFileSync38(cfg.configFile, "utf8"));
|
|
25191
25364
|
for (const r of rawCfg?.routes ?? []) {
|
|
25192
25365
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
25193
25366
|
try {
|
|
@@ -25229,10 +25402,12 @@ ${where.join("\n")}`);
|
|
|
25229
25402
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
25230
25403
|
break;
|
|
25231
25404
|
}
|
|
25232
|
-
|
|
25233
|
-
|
|
25405
|
+
mkdirSync28(cfg.ship.stateDir, { recursive: true });
|
|
25406
|
+
writeFileSync30(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
25234
25407
|
`);
|
|
25235
|
-
process.stdout.write(
|
|
25408
|
+
process.stdout.write(
|
|
25409
|
+
"crew reload: requested \u2014 the daemon relaunches with the new config once its running agents end (it starts no new work meanwhile); if it is busy, `crew reload --now` forces the relaunch and interrupts them\n"
|
|
25410
|
+
);
|
|
25236
25411
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
25237
25412
|
for (const cmd of [control.stopCommand, control.startCommand]) {
|
|
25238
25413
|
const res = spawnSync8(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
@@ -25281,7 +25456,7 @@ ${where.join("\n")}`);
|
|
|
25281
25456
|
}
|
|
25282
25457
|
case "log": {
|
|
25283
25458
|
try {
|
|
25284
|
-
const text2 =
|
|
25459
|
+
const text2 = readFileSync38(cfg.ship.logFile, "utf8").split("\n");
|
|
25285
25460
|
process.stdout.write(`${text2.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
25286
25461
|
`);
|
|
25287
25462
|
} catch {
|
|
@@ -25304,8 +25479,8 @@ ${where.join("\n")}`);
|
|
|
25304
25479
|
};
|
|
25305
25480
|
if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
|
|
25306
25481
|
const tracker = new Tracker(route2, cfg.ship);
|
|
25307
|
-
const configText =
|
|
25308
|
-
const cfgBase =
|
|
25482
|
+
const configText = readFileSync38(cfg.configFile, "utf8");
|
|
25483
|
+
const cfgBase = dirname24(cfg.configFile);
|
|
25309
25484
|
const configured2 = configuredRepos(configText, route2.route, cfgBase);
|
|
25310
25485
|
const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
|
|
25311
25486
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
|
|
@@ -25333,7 +25508,7 @@ ${where.join("\n")}`);
|
|
|
25333
25508
|
}
|
|
25334
25509
|
if (configured2.some((r) => r.dir === dir)) {
|
|
25335
25510
|
try {
|
|
25336
|
-
|
|
25511
|
+
writeFileSync30(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
|
|
25337
25512
|
} catch (e) {
|
|
25338
25513
|
if (e instanceof ReposError) fail(e.message);
|
|
25339
25514
|
throw e;
|
|
@@ -25341,29 +25516,29 @@ ${where.join("\n")}`);
|
|
|
25341
25516
|
}
|
|
25342
25517
|
if (row) {
|
|
25343
25518
|
await tracker.deleteRepoRow(row.id);
|
|
25344
|
-
if (
|
|
25345
|
-
const raw = JSON.parse(
|
|
25519
|
+
if (existsSync43(resolvedFile)) {
|
|
25520
|
+
const raw = JSON.parse(readFileSync38(resolvedFile, "utf8"));
|
|
25346
25521
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
25347
25522
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
25348
|
-
|
|
25523
|
+
writeFileSync30(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
25349
25524
|
`);
|
|
25350
25525
|
}
|
|
25351
25526
|
}
|
|
25352
|
-
if (
|
|
25527
|
+
if (existsSync43(join46(dir, ".git")) && basename14(dir) === SAMPLE_REPO) rmSync20(dir, { recursive: true, force: true });
|
|
25353
25528
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
25354
25529
|
break;
|
|
25355
25530
|
}
|
|
25356
25531
|
const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
25357
|
-
const fresh = !
|
|
25358
|
-
if (
|
|
25532
|
+
const fresh = !existsSync43(join46(dir, ".git"));
|
|
25533
|
+
if (existsSync43(dir) && fresh && readdirSync19(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
25359
25534
|
if (dryRun) {
|
|
25360
25535
|
say("dry-run", { dir, newCheckout: fresh, repoRow: row?.id ?? null }, `would ${fresh ? "create" : "reuse"} ${dir}${row ? "" : ", create the Repos row"}, and file one ${tracker.contract.statuses.approved} ticket`);
|
|
25361
25536
|
break;
|
|
25362
25537
|
}
|
|
25363
25538
|
if (fresh) {
|
|
25364
|
-
|
|
25365
|
-
|
|
25366
|
-
|
|
25539
|
+
mkdirSync28(dir, { recursive: true });
|
|
25540
|
+
writeFileSync30(join46(dir, "README.md"), SAMPLE_README);
|
|
25541
|
+
writeFileSync30(join46(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
25367
25542
|
const g = (...a) => {
|
|
25368
25543
|
const r = spawnSync8("git", a, { cwd: dir, encoding: "utf8" });
|
|
25369
25544
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -25381,17 +25556,17 @@ ${where.join("\n")}`);
|
|
|
25381
25556
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
25382
25557
|
});
|
|
25383
25558
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
25384
|
-
if (
|
|
25385
|
-
const raw = JSON.parse(
|
|
25559
|
+
if (existsSync43(resolvedFile)) {
|
|
25560
|
+
const raw = JSON.parse(readFileSync38(resolvedFile, "utf8"));
|
|
25386
25561
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
25387
25562
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
25388
|
-
|
|
25563
|
+
writeFileSync30(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
25389
25564
|
`);
|
|
25390
25565
|
}
|
|
25391
25566
|
}
|
|
25392
25567
|
if (!configured2.some((r) => r.dir === dir)) {
|
|
25393
25568
|
try {
|
|
25394
|
-
|
|
25569
|
+
writeFileSync30(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
|
|
25395
25570
|
} catch (e) {
|
|
25396
25571
|
if (e instanceof ReposError) fail(e.message);
|
|
25397
25572
|
throw e;
|
|
@@ -25566,7 +25741,9 @@ ${where.join("\n")}`);
|
|
|
25566
25741
|
rowName: nameSync.rowName ?? null,
|
|
25567
25742
|
nameDrift: nameSync.drift,
|
|
25568
25743
|
// Additive (CREW-2803): the relay URL this route's tunnel is reachable at, or null.
|
|
25569
|
-
relayUrl: routeRelayUrl(route2)
|
|
25744
|
+
relayUrl: routeRelayUrl(route2),
|
|
25745
|
+
// Additive (CREW-2887): when a `crew reload` request was written and not yet honored, else null.
|
|
25746
|
+
reloadRequestedAt: ((at) => at === null ? null : new Date(at).toISOString())(readReloadRequestedAt(cfg.ship.stateDir))
|
|
25570
25747
|
},
|
|
25571
25748
|
// Additive (CREW-1402): what the operator owes. `version` stays 1.
|
|
25572
25749
|
todo: todo.map((i) => ({ ticket: displayKey(i.ticket), title: i.ticket.title ?? null, reasons: i.reasons })),
|
|
@@ -25630,6 +25807,7 @@ ${where.join("\n")}`);
|
|
|
25630
25807
|
waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
|
|
25631
25808
|
},
|
|
25632
25809
|
ships: shipRows.map((s) => ({
|
|
25810
|
+
...shipIdentityJson(s),
|
|
25633
25811
|
name: s.name ?? null,
|
|
25634
25812
|
lastSeen: s.last_seen ?? null,
|
|
25635
25813
|
engaged: !!s.engaged,
|
|
@@ -25665,6 +25843,11 @@ watermark: ${state.watermark()}
|
|
|
25665
25843
|
);
|
|
25666
25844
|
for (const r of ["dev", "design", "qa"]) {
|
|
25667
25845
|
if (state.isRolePaused(r)) process.stdout.write(`role ${r}: paused
|
|
25846
|
+
`);
|
|
25847
|
+
}
|
|
25848
|
+
const reloadWaitingAt = readReloadRequestedAt(cfg.ship.stateDir);
|
|
25849
|
+
if (reloadWaitingAt !== null) {
|
|
25850
|
+
process.stdout.write(`reload: requested ${Math.max(0, Math.round((Date.now() - reloadWaitingAt) / 1e3))}s ago, not yet honored \u2014 \`crew reload --now\` forces it
|
|
25668
25851
|
`);
|
|
25669
25852
|
}
|
|
25670
25853
|
if (projects.length) process.stdout.write(`projects: ${projects.map((p) => p.label).join(", ")}
|
|
@@ -25729,7 +25912,7 @@ watermark: ${state.watermark()}
|
|
|
25729
25912
|
}
|
|
25730
25913
|
for (const r of repos) {
|
|
25731
25914
|
const remote = r.config.branch.remote;
|
|
25732
|
-
if (
|
|
25915
|
+
if (existsSync43(r.dir)) {
|
|
25733
25916
|
pushAuth.push({
|
|
25734
25917
|
route: c.route,
|
|
25735
25918
|
repo: r.name,
|
|
@@ -25815,7 +25998,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
25815
25998
|
}
|
|
25816
25999
|
try {
|
|
25817
26000
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
25818
|
-
const evSize =
|
|
26001
|
+
const evSize = existsSync43(evFile) ? statSync14(evFile).size : 0;
|
|
25819
26002
|
process.stdout.write(
|
|
25820
26003
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
25821
26004
|
` : "events log: ok\n"
|
|
@@ -25826,7 +26009,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
25826
26009
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run", installContext());
|
|
25827
26010
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
25828
26011
|
try {
|
|
25829
|
-
return
|
|
26012
|
+
return readFileSync38(p, "utf8");
|
|
25830
26013
|
} catch {
|
|
25831
26014
|
return null;
|
|
25832
26015
|
}
|
|
@@ -25837,6 +26020,13 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
25837
26020
|
);
|
|
25838
26021
|
}
|
|
25839
26022
|
}
|
|
26023
|
+
const reloadAt = readReloadRequestedAt(cfg.ship.stateDir);
|
|
26024
|
+
if (reloadAt !== null) {
|
|
26025
|
+
process.stdout.write(
|
|
26026
|
+
`reload request: WARN pending for ${Math.max(0, Math.round((Date.now() - reloadAt) / 1e3))}s \u2014 the daemon is waiting for its running agents to end; \`crew reload --now\` forces it
|
|
26027
|
+
`
|
|
26028
|
+
);
|
|
26029
|
+
}
|
|
25840
26030
|
const daemonStartedAt = readDaemonStarted(cfg.ship.stateDir);
|
|
25841
26031
|
if (daemonStartedAt !== null) {
|
|
25842
26032
|
const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
|