@tablation/crew 0.71.13 → 0.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +443 -193
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5920,6 +5920,12 @@ var Tracker = class {
|
|
|
5920
5920
|
return [];
|
|
5921
5921
|
}
|
|
5922
5922
|
}
|
|
5923
|
+
/** Renames a Ships row (CREW-2495). Throws on a refused write (a member key may not edit the table). */
|
|
5924
|
+
async renameShipRow(rowId, name) {
|
|
5925
|
+
const model = this.route.resolved?.shipsModelId;
|
|
5926
|
+
if (!model) throw new Error("this workspace has no Ships table");
|
|
5927
|
+
await this.client.records.update(model, rowId, { name });
|
|
5928
|
+
}
|
|
5923
5929
|
/**
|
|
5924
5930
|
* This ship's own row on the `Ships` table, matched by name — the same
|
|
5925
5931
|
* exact-string match `doctor` has always used (see the callsign comment
|
|
@@ -8606,7 +8612,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
8606
8612
|
}
|
|
8607
8613
|
|
|
8608
8614
|
// src/cli.ts
|
|
8609
|
-
import { resolve as resolve5, dirname as
|
|
8615
|
+
import { resolve as resolve5, dirname as dirname20, join as join35, basename as basename11 } from "path";
|
|
8610
8616
|
import { fileURLToPath } from "url";
|
|
8611
8617
|
import { hostname as hostname2, homedir as homedir9 } from "os";
|
|
8612
8618
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
@@ -12636,6 +12642,217 @@ function nextRoles(decision, ran) {
|
|
|
12636
12642
|
return decision.selection.pending.filter((r) => !ran.includes(r)).sort((a, b) => (decision.selection.ranks[a] ?? 0) - (decision.selection.ranks[b] ?? 0));
|
|
12637
12643
|
}
|
|
12638
12644
|
|
|
12645
|
+
// src/ship-name.ts
|
|
12646
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync15, readFileSync as readFileSync20, renameSync as renameSync2, writeFileSync as writeFileSync17 } from "fs";
|
|
12647
|
+
import { dirname as dirname14 } from "path";
|
|
12648
|
+
import { parseDocument } from "yaml";
|
|
12649
|
+
|
|
12650
|
+
// src/machine-id.ts
|
|
12651
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
12652
|
+
import { createHash as createHash3 } from "crypto";
|
|
12653
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync14, readFileSync as readFileSync19, writeFileSync as writeFileSync16 } from "fs";
|
|
12654
|
+
import { dirname as dirname13, join as join25 } from "path";
|
|
12655
|
+
function machineIdPathFor(stateDir) {
|
|
12656
|
+
return join25(stateDir, "machine-id");
|
|
12657
|
+
}
|
|
12658
|
+
var sha256 = (s) => createHash3("sha256").update(s).digest("hex");
|
|
12659
|
+
function readRawMachineId(platform2 = process.platform) {
|
|
12660
|
+
try {
|
|
12661
|
+
if (platform2 === "darwin") {
|
|
12662
|
+
const out = execFileSync4("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
12663
|
+
encoding: "utf8",
|
|
12664
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
12665
|
+
});
|
|
12666
|
+
return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
|
|
12667
|
+
}
|
|
12668
|
+
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
12669
|
+
if (existsSync24(p)) {
|
|
12670
|
+
const id = readFileSync19(p, "utf8").trim();
|
|
12671
|
+
if (id) return id;
|
|
12672
|
+
}
|
|
12673
|
+
}
|
|
12674
|
+
} catch {
|
|
12675
|
+
}
|
|
12676
|
+
return void 0;
|
|
12677
|
+
}
|
|
12678
|
+
function ensureMachineId(stateDir, opts = {}) {
|
|
12679
|
+
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
12680
|
+
const path = machineIdPathFor(stateDir);
|
|
12681
|
+
if (existsSync24(path)) {
|
|
12682
|
+
const cached = readFileSync19(path, "utf8").trim();
|
|
12683
|
+
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
12684
|
+
}
|
|
12685
|
+
const raw = (opts.read ?? readRawMachineId)();
|
|
12686
|
+
if (!raw) return void 0;
|
|
12687
|
+
const hashed = sha256(raw);
|
|
12688
|
+
mkdirSync14(dirname13(path), { recursive: true });
|
|
12689
|
+
writeFileSync16(path, `${hashed}
|
|
12690
|
+
`, { mode: 384 });
|
|
12691
|
+
return hashed;
|
|
12692
|
+
}
|
|
12693
|
+
function matchShipRows(ships, shipName2, machineId) {
|
|
12694
|
+
const hasColumn = ships.some((r) => "machine_id" in r);
|
|
12695
|
+
const byId = hasColumn && machineId ? ships.filter((r) => r.machine_id === machineId) : [];
|
|
12696
|
+
if (byId.length > 0) return { rows: byId, by: "machine_id", hasColumn };
|
|
12697
|
+
return { rows: ships.filter((r) => (r.name ?? "").trim() === shipName2.trim()), by: "name", hasColumn };
|
|
12698
|
+
}
|
|
12699
|
+
|
|
12700
|
+
// src/ship-name.ts
|
|
12701
|
+
var ShipNameError = class extends Error {
|
|
12702
|
+
};
|
|
12703
|
+
function validateShipName(raw) {
|
|
12704
|
+
const name = raw.trim();
|
|
12705
|
+
if (!name) throw new ShipNameError("the ship name cannot be empty");
|
|
12706
|
+
if (/[\u0000-\u001f\u007f]/.test(name)) throw new ShipNameError("the ship name cannot contain control characters");
|
|
12707
|
+
return name;
|
|
12708
|
+
}
|
|
12709
|
+
function planNameSync(local, row, lastSynced) {
|
|
12710
|
+
if (local === row) return { drift: "none", action: "none" };
|
|
12711
|
+
const rowMoved = lastSynced === void 0 || row !== lastSynced;
|
|
12712
|
+
const localMoved = lastSynced === void 0 || local !== lastSynced;
|
|
12713
|
+
if (rowMoved && localMoved) return { drift: "both", action: "row-wins" };
|
|
12714
|
+
if (rowMoved) return { drift: "row", action: "adopt" };
|
|
12715
|
+
return { drift: "local", action: "push" };
|
|
12716
|
+
}
|
|
12717
|
+
function shipNameOfYaml(text) {
|
|
12718
|
+
const v = parseDocument(text).getIn(["ship", "name"]);
|
|
12719
|
+
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
12720
|
+
}
|
|
12721
|
+
function setShipNameInYaml(text, name) {
|
|
12722
|
+
const doc = parseDocument(text);
|
|
12723
|
+
doc.setIn(["ship", "name"], name);
|
|
12724
|
+
return doc.toString();
|
|
12725
|
+
}
|
|
12726
|
+
function pickShipRow(rows, machineId, byName = []) {
|
|
12727
|
+
const hasColumn = rows.some((r) => "machine_id" in r);
|
|
12728
|
+
if (!hasColumn || !machineId) return { kind: "unsupported" };
|
|
12729
|
+
const byId = rows.filter((r) => r.machine_id === machineId);
|
|
12730
|
+
if (byId.length === 1) return { kind: "row", row: byId[0], by: "machine_id" };
|
|
12731
|
+
if (byId.length > 1) return { kind: "ambiguous" };
|
|
12732
|
+
const names = new Set(byName.map((n) => n.trim()));
|
|
12733
|
+
const named = rows.filter((r) => !r.machine_id && names.has((r.name ?? "").trim()));
|
|
12734
|
+
if (named.length === 1) return { kind: "row", row: named[0], by: "name" };
|
|
12735
|
+
return { kind: named.length > 1 ? "ambiguous" : "none" };
|
|
12736
|
+
}
|
|
12737
|
+
async function syncRouteName(i) {
|
|
12738
|
+
const picked = pickShipRow(i.rows, i.machineId);
|
|
12739
|
+
if (picked.kind !== "row") {
|
|
12740
|
+
const status2 = picked.kind === "none" ? "no-row" : picked.kind;
|
|
12741
|
+
return { status: status2, drift: "none", action: "none", name: i.local };
|
|
12742
|
+
}
|
|
12743
|
+
const rowName = (picked.row.name ?? "").trim();
|
|
12744
|
+
if (!rowName) return { status: "no-row", drift: "none", action: "none", name: i.local };
|
|
12745
|
+
const { drift, action } = planNameSync(i.local, rowName, i.lastSynced);
|
|
12746
|
+
const base = { drift, rowId: picked.row.id, rowName };
|
|
12747
|
+
if (action === "push") {
|
|
12748
|
+
if (!i.rename) return { status: "ok", ...base, action, name: i.local };
|
|
12749
|
+
try {
|
|
12750
|
+
await i.rename(picked.row.id, i.local);
|
|
12751
|
+
} catch (e) {
|
|
12752
|
+
return { status: "refused", ...base, action: "none", name: i.local, error: e.message };
|
|
12753
|
+
}
|
|
12754
|
+
return { status: "ok", ...base, action, name: i.local };
|
|
12755
|
+
}
|
|
12756
|
+
if (action === "adopt" || action === "row-wins") return { status: "ok", ...base, action, name: rowName, from: i.local };
|
|
12757
|
+
return { status: "ok", ...base, action, name: i.local };
|
|
12758
|
+
}
|
|
12759
|
+
function readSyncedName(resolvedFile) {
|
|
12760
|
+
if (!existsSync25(resolvedFile)) return void 0;
|
|
12761
|
+
try {
|
|
12762
|
+
const v = JSON.parse(readFileSync20(resolvedFile, "utf8")).shipName;
|
|
12763
|
+
return typeof v === "string" ? v : void 0;
|
|
12764
|
+
} catch {
|
|
12765
|
+
return void 0;
|
|
12766
|
+
}
|
|
12767
|
+
}
|
|
12768
|
+
function writeSyncedName(resolvedFile, name) {
|
|
12769
|
+
if (!existsSync25(resolvedFile)) return;
|
|
12770
|
+
let raw;
|
|
12771
|
+
try {
|
|
12772
|
+
raw = JSON.parse(readFileSync20(resolvedFile, "utf8"));
|
|
12773
|
+
} catch {
|
|
12774
|
+
return;
|
|
12775
|
+
}
|
|
12776
|
+
if (raw.shipName === name) return;
|
|
12777
|
+
raw.shipName = name;
|
|
12778
|
+
mkdirSync15(dirname14(resolvedFile), { recursive: true });
|
|
12779
|
+
const tmp = `${resolvedFile}.tmp${process.pid}`;
|
|
12780
|
+
writeFileSync17(tmp, `${JSON.stringify(raw, null, 2)}
|
|
12781
|
+
`);
|
|
12782
|
+
renameSync2(tmp, resolvedFile);
|
|
12783
|
+
}
|
|
12784
|
+
async function reconcileShipName(i) {
|
|
12785
|
+
let local = i.ship.name;
|
|
12786
|
+
try {
|
|
12787
|
+
local = shipNameOfYaml(readFileSync20(i.configFile, "utf8")) ?? local;
|
|
12788
|
+
} catch {
|
|
12789
|
+
}
|
|
12790
|
+
const resolvedFile = resolvedPathFor(i.stateDir, i.route);
|
|
12791
|
+
const lastSynced = readSyncedName(resolvedFile);
|
|
12792
|
+
let rows = [];
|
|
12793
|
+
try {
|
|
12794
|
+
rows = await i.rows();
|
|
12795
|
+
} catch {
|
|
12796
|
+
}
|
|
12797
|
+
const result = await syncRouteName({
|
|
12798
|
+
local,
|
|
12799
|
+
lastSynced,
|
|
12800
|
+
rows,
|
|
12801
|
+
machineId: i.machineId,
|
|
12802
|
+
rename: i.readOnly ? void 0 : i.rename
|
|
12803
|
+
});
|
|
12804
|
+
if (i.readOnly || result.status === "unsupported") return result;
|
|
12805
|
+
if (result.status === "refused") {
|
|
12806
|
+
i.emit?.warn(`could not rename the Ships row to "${local}": ${result.error}`, { step: "poll", data: { event: "ship_rename_refused", route: i.route, to: local } });
|
|
12807
|
+
return result;
|
|
12808
|
+
}
|
|
12809
|
+
if (result.status !== "ok") return result;
|
|
12810
|
+
if (result.action === "adopt" || result.action === "row-wins") {
|
|
12811
|
+
const text = readFileSync20(i.configFile, "utf8");
|
|
12812
|
+
writeFileSync17(i.configFile, setShipNameInYaml(text, result.name));
|
|
12813
|
+
i.ship.name = result.name;
|
|
12814
|
+
if (result.action === "row-wins") {
|
|
12815
|
+
i.emit?.warn(
|
|
12816
|
+
`ship name changed in both crew.yaml ("${result.from}") and the Ships table ("${result.name}"); the Ships row wins`,
|
|
12817
|
+
{ step: "poll", data: { event: "ship_renamed", route: i.route, from: result.from, to: result.name, source: "table", conflict: true } }
|
|
12818
|
+
);
|
|
12819
|
+
} else {
|
|
12820
|
+
i.emit?.emit(`ship renamed to "${result.name}" in the Ships table; crew.yaml updated`, {
|
|
12821
|
+
step: "poll",
|
|
12822
|
+
data: { event: "ship_renamed", route: i.route, from: result.from, to: result.name, source: "table" }
|
|
12823
|
+
});
|
|
12824
|
+
}
|
|
12825
|
+
} else if (result.action === "push") {
|
|
12826
|
+
i.emit?.emit(`ship name "${result.name}" pushed to the Ships table`, {
|
|
12827
|
+
step: "poll",
|
|
12828
|
+
data: { event: "ship_renamed", route: i.route, from: result.rowName, to: result.name, source: "crew.yaml" }
|
|
12829
|
+
});
|
|
12830
|
+
}
|
|
12831
|
+
writeSyncedName(resolvedFile, result.name);
|
|
12832
|
+
return result;
|
|
12833
|
+
}
|
|
12834
|
+
async function reconcileRouteShipName(o) {
|
|
12835
|
+
const fallback = { status: "unsupported", drift: "none", action: "none", name: o.ship.name };
|
|
12836
|
+
if (!o.route.resolved?.shipsModelId) return fallback;
|
|
12837
|
+
try {
|
|
12838
|
+
const machineId = o.machineId ?? ensureMachineId(o.ship.stateDir);
|
|
12839
|
+
const tracker = new Tracker(o.route, o.ship);
|
|
12840
|
+
return await reconcileShipName({
|
|
12841
|
+
route: o.route.route,
|
|
12842
|
+
configFile: o.configFile,
|
|
12843
|
+
stateDir: o.ship.stateDir,
|
|
12844
|
+
ship: o.ship,
|
|
12845
|
+
machineId,
|
|
12846
|
+
rows: () => tracker.shipRows(),
|
|
12847
|
+
rename: (id, name) => tracker.renameShipRow(id, name),
|
|
12848
|
+
emit: o.emit,
|
|
12849
|
+
readOnly: o.readOnly
|
|
12850
|
+
});
|
|
12851
|
+
} catch {
|
|
12852
|
+
return fallback;
|
|
12853
|
+
}
|
|
12854
|
+
}
|
|
12855
|
+
|
|
12639
12856
|
// src/connect.ts
|
|
12640
12857
|
var ConnectHttpError = class extends Error {
|
|
12641
12858
|
status;
|
|
@@ -12941,10 +13158,16 @@ async function discover(o) {
|
|
|
12941
13158
|
out.shipRow = { id: shipRowId, name: o.ship.name, action: "matched", matchedBy };
|
|
12942
13159
|
const patchBody = {};
|
|
12943
13160
|
if (matchedBy === "machine_id") {
|
|
12944
|
-
const
|
|
12945
|
-
|
|
12946
|
-
|
|
12947
|
-
|
|
13161
|
+
const rowName = (mine[0].name ?? "").trim();
|
|
13162
|
+
const route = `${out.workspaceSlug}/${out.projectSlug ?? out.projectId}`;
|
|
13163
|
+
const plan = planNameSync(o.ship.name.trim(), rowName, o.ship.lastSyncedName?.(route));
|
|
13164
|
+
if (plan.action === "push") {
|
|
13165
|
+
patchBody.name = o.ship.name.trim();
|
|
13166
|
+
provisioning.push(`renamed Ships row "${rowName}" to "${o.ship.name.trim()}" (crew.yaml changed)`);
|
|
13167
|
+
} else if (plan.action === "adopt" || plan.action === "row-wins") {
|
|
13168
|
+
out.shipRow.name = rowName;
|
|
13169
|
+
out.shipNameAdopted = rowName;
|
|
13170
|
+
provisioning.push(`matched Ships row "${rowName}" by machine id \u2014 this host's name becomes "${rowName}"${plan.action === "row-wins" ? " (crew.yaml also changed; the Ships row wins)" : ""}`);
|
|
12948
13171
|
}
|
|
12949
13172
|
patchBody.last_seen = (/* @__PURE__ */ new Date()).toISOString();
|
|
12950
13173
|
}
|
|
@@ -13494,8 +13717,8 @@ function describeCapability(i) {
|
|
|
13494
13717
|
}
|
|
13495
13718
|
|
|
13496
13719
|
// src/agents-sync-run.ts
|
|
13497
|
-
import { existsSync as
|
|
13498
|
-
import { dirname as
|
|
13720
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync16, readFileSync as readFileSync21, writeFileSync as writeFileSync18 } from "fs";
|
|
13721
|
+
import { dirname as dirname15 } from "path";
|
|
13499
13722
|
async function runAgentsSync(target, o) {
|
|
13500
13723
|
const dryRun = o.dryRun ?? false;
|
|
13501
13724
|
const force = o.force ?? false;
|
|
@@ -13528,12 +13751,12 @@ async function runAgentsSync(target, o) {
|
|
|
13528
13751
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
13529
13752
|
if (!dryRun) {
|
|
13530
13753
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
13531
|
-
const raw =
|
|
13754
|
+
const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync21(resolvedPath, "utf8")) : {};
|
|
13532
13755
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
13533
13756
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
13534
13757
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
13535
|
-
|
|
13536
|
-
|
|
13758
|
+
mkdirSync16(dirname15(resolvedPath), { recursive: true });
|
|
13759
|
+
writeFileSync18(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
13537
13760
|
`);
|
|
13538
13761
|
if (target.resolved) {
|
|
13539
13762
|
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
@@ -13548,7 +13771,7 @@ async function runAgentsSync(target, o) {
|
|
|
13548
13771
|
}
|
|
13549
13772
|
|
|
13550
13773
|
// src/logbook.ts
|
|
13551
|
-
import { createHash as
|
|
13774
|
+
import { createHash as createHash4 } from "crypto";
|
|
13552
13775
|
import { TablationClient as TablationClient4 } from "@tablation/client";
|
|
13553
13776
|
var LogbookError = class extends Error {
|
|
13554
13777
|
};
|
|
@@ -13621,7 +13844,7 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
13621
13844
|
};
|
|
13622
13845
|
}
|
|
13623
13846
|
const prompt = appendAdditions(match.prompt ?? "", match.prompt_additions);
|
|
13624
|
-
const actualSha =
|
|
13847
|
+
const actualSha = createHash4("sha256").update(prompt).digest("hex");
|
|
13625
13848
|
const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
|
|
13626
13849
|
return {
|
|
13627
13850
|
entry,
|
|
@@ -13636,12 +13859,12 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
13636
13859
|
}
|
|
13637
13860
|
|
|
13638
13861
|
// src/repos-hooks.ts
|
|
13639
|
-
import { isMap as isMap2, parseDocument as
|
|
13862
|
+
import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml";
|
|
13640
13863
|
|
|
13641
13864
|
// src/repos-cmd.ts
|
|
13642
13865
|
import { homedir as homedir8 } from "os";
|
|
13643
|
-
import { basename as basename8, join as
|
|
13644
|
-
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
13866
|
+
import { basename as basename8, join as join26, resolve as resolve3 } from "path";
|
|
13867
|
+
import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
|
|
13645
13868
|
var ReposError = class extends Error {
|
|
13646
13869
|
};
|
|
13647
13870
|
function normalizeRemote(url) {
|
|
@@ -13665,7 +13888,7 @@ function matchRepoName(origin, resolved) {
|
|
|
13665
13888
|
return void 0;
|
|
13666
13889
|
}
|
|
13667
13890
|
function routeNamesOf(text) {
|
|
13668
|
-
const routes =
|
|
13891
|
+
const routes = parseDocument2(text).get("routes", true);
|
|
13669
13892
|
if (!isSeq(routes)) return [];
|
|
13670
13893
|
return routes.items.flatMap((i) => isMap(i) && typeof i.get("route") === "string" ? [i.get("route")] : []);
|
|
13671
13894
|
}
|
|
@@ -13700,7 +13923,7 @@ function routeMapOf(doc, route) {
|
|
|
13700
13923
|
throw new ReposError(`no route "${route}" in the config (have: ${have})`);
|
|
13701
13924
|
}
|
|
13702
13925
|
function configuredRepos(text, route, base) {
|
|
13703
|
-
const doc =
|
|
13926
|
+
const doc = parseDocument2(text);
|
|
13704
13927
|
const m = routeMapOf(doc, route);
|
|
13705
13928
|
const repos = m.get("repos");
|
|
13706
13929
|
if (isMap(repos)) {
|
|
@@ -13715,7 +13938,7 @@ function configuredRepos(text, route, base) {
|
|
|
13715
13938
|
return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
13716
13939
|
}
|
|
13717
13940
|
function routeSetsHooks(text, route, name) {
|
|
13718
|
-
const m = routeMapOf(
|
|
13941
|
+
const m = routeMapOf(parseDocument2(text), route);
|
|
13719
13942
|
const repos = m.get("repos", true);
|
|
13720
13943
|
let target = m;
|
|
13721
13944
|
if (isMap(repos)) target = repos.get(name, true);
|
|
@@ -13771,7 +13994,7 @@ function listRepoRows(i) {
|
|
|
13771
13994
|
return rows;
|
|
13772
13995
|
}
|
|
13773
13996
|
function planRepoAdd(i) {
|
|
13774
|
-
const doc =
|
|
13997
|
+
const doc = parseDocument2(i.text);
|
|
13775
13998
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
13776
13999
|
const m = routeMapOf(doc, i.route);
|
|
13777
14000
|
const existing = configuredRepos(i.text, i.route, i.base);
|
|
@@ -13808,7 +14031,7 @@ function planRepoAdd(i) {
|
|
|
13808
14031
|
return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched };
|
|
13809
14032
|
}
|
|
13810
14033
|
function planRepoRemove(text, base, route, path) {
|
|
13811
|
-
const doc =
|
|
14034
|
+
const doc = parseDocument2(text);
|
|
13812
14035
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
13813
14036
|
const m = routeMapOf(doc, route);
|
|
13814
14037
|
const target = expandDir(path, base);
|
|
@@ -13886,7 +14109,7 @@ function planRepoClone(i) {
|
|
|
13886
14109
|
`the Repos row "${name}" has no remote to clone from \u2014 attach an existing checkout with \`crew repos add ${i.route} ${name} <path>\`, or set the row's remote in the tracker and re-run \`crew connect\``
|
|
13887
14110
|
);
|
|
13888
14111
|
}
|
|
13889
|
-
const doc =
|
|
14112
|
+
const doc = parseDocument2(i.text);
|
|
13890
14113
|
const m = routeMapOf(doc, i.route);
|
|
13891
14114
|
const existing = configuredRepos(i.text, i.route, i.base);
|
|
13892
14115
|
if (existing.some((e) => e.name.toLowerCase() === name.toLowerCase())) {
|
|
@@ -13894,7 +14117,7 @@ function planRepoClone(i) {
|
|
|
13894
14117
|
}
|
|
13895
14118
|
const ship = doc.get("ship", true);
|
|
13896
14119
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
13897
|
-
const target = i.dir ? expandDir(i.dir, i.base) :
|
|
14120
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join26(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
13898
14121
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
|
|
13899
14122
|
return { ...add, remote, target };
|
|
13900
14123
|
}
|
|
@@ -14040,7 +14263,7 @@ function repoDirOf(text, base, route, name) {
|
|
|
14040
14263
|
return hit.dir;
|
|
14041
14264
|
}
|
|
14042
14265
|
function planRouteHooks(text, route, name, hooks) {
|
|
14043
|
-
const doc =
|
|
14266
|
+
const doc = parseDocument3(text);
|
|
14044
14267
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
14045
14268
|
const m = routeMapOf(doc, route);
|
|
14046
14269
|
const repos = m.get("repos", true);
|
|
@@ -14102,7 +14325,7 @@ function existingRepoConfig(f) {
|
|
|
14102
14325
|
}
|
|
14103
14326
|
|
|
14104
14327
|
// src/repos-hooks-agent.ts
|
|
14105
|
-
import { spawn as spawn4, execFileSync as
|
|
14328
|
+
import { spawn as spawn4, execFileSync as execFileSync5 } from "child_process";
|
|
14106
14329
|
import { realpathSync as realpathSync2 } from "fs";
|
|
14107
14330
|
import { isAbsolute as isAbsolute2, relative } from "path";
|
|
14108
14331
|
var AgentSuggestError = class extends Error {
|
|
@@ -14205,7 +14428,7 @@ function classifyFailure(o) {
|
|
|
14205
14428
|
}
|
|
14206
14429
|
function porcelain(dir) {
|
|
14207
14430
|
try {
|
|
14208
|
-
return
|
|
14431
|
+
return execFileSync5("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
|
|
14209
14432
|
} catch {
|
|
14210
14433
|
return void 0;
|
|
14211
14434
|
}
|
|
@@ -14274,7 +14497,7 @@ ${run2.err}` });
|
|
|
14274
14497
|
}
|
|
14275
14498
|
|
|
14276
14499
|
// src/sample-ticket.ts
|
|
14277
|
-
import { basename as basename9, dirname as
|
|
14500
|
+
import { basename as basename9, dirname as dirname17, join as join27 } from "path";
|
|
14278
14501
|
var SAMPLE_REPO = "crew-sample";
|
|
14279
14502
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
14280
14503
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -14297,8 +14520,8 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
14297
14520
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
14298
14521
|
`;
|
|
14299
14522
|
function sampleDir(existing, reposBase, workspace) {
|
|
14300
|
-
if (existing.length > 0) return
|
|
14301
|
-
return
|
|
14523
|
+
if (existing.length > 0) return join27(dirname17(existing[0]), SAMPLE_REPO);
|
|
14524
|
+
return join27(reposBase, workspace, SAMPLE_REPO);
|
|
14302
14525
|
}
|
|
14303
14526
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
14304
14527
|
function sampleDescription() {
|
|
@@ -14342,7 +14565,7 @@ function planSampleTicket(i) {
|
|
|
14342
14565
|
}
|
|
14343
14566
|
|
|
14344
14567
|
// src/doctor-fix.ts
|
|
14345
|
-
import { parseDocument as
|
|
14568
|
+
import { parseDocument as parseDocument4 } from "yaml";
|
|
14346
14569
|
var sound = (r) => r.hasRepos && r.problems.length === 0;
|
|
14347
14570
|
function planFix(f) {
|
|
14348
14571
|
const actions = [];
|
|
@@ -14369,7 +14592,7 @@ function routesInScope(routes, named) {
|
|
|
14369
14592
|
return named ? routes.filter((r) => r.route === named) : routes;
|
|
14370
14593
|
}
|
|
14371
14594
|
function planEnable(text, route) {
|
|
14372
|
-
const doc =
|
|
14595
|
+
const doc = parseDocument4(text);
|
|
14373
14596
|
routeMapOf(doc, route).set("enabled", true);
|
|
14374
14597
|
return String(doc);
|
|
14375
14598
|
}
|
|
@@ -14379,8 +14602,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
14379
14602
|
}
|
|
14380
14603
|
|
|
14381
14604
|
// src/worktree-sweep.ts
|
|
14382
|
-
import { existsSync as
|
|
14383
|
-
import { basename as basename10, dirname as
|
|
14605
|
+
import { existsSync as existsSync27, readFileSync as readFileSync22, readdirSync as readdirSync8 } from "fs";
|
|
14606
|
+
import { basename as basename10, dirname as dirname18, join as join28, resolve as resolve4 } from "path";
|
|
14384
14607
|
function planWorktreeSweep(target, tickets, contract) {
|
|
14385
14608
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
14386
14609
|
const verified = contract.statuses.verified;
|
|
@@ -14434,7 +14657,7 @@ function worktreeTicketNumbers(target) {
|
|
|
14434
14657
|
return [...nums];
|
|
14435
14658
|
}
|
|
14436
14659
|
function siblingWorktreeDirs(target) {
|
|
14437
|
-
const parent =
|
|
14660
|
+
const parent = dirname18(target.dir.replace(/[/\\]+$/, ""));
|
|
14438
14661
|
const stem = `${basename10(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
14439
14662
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
14440
14663
|
let names;
|
|
@@ -14447,7 +14670,7 @@ function siblingWorktreeDirs(target) {
|
|
|
14447
14670
|
for (const name of names) {
|
|
14448
14671
|
if (!name.startsWith(stem)) continue;
|
|
14449
14672
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
14450
|
-
const path =
|
|
14673
|
+
const path = join28(parent, name);
|
|
14451
14674
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
14452
14675
|
}
|
|
14453
14676
|
return out;
|
|
@@ -14482,7 +14705,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
14482
14705
|
let removed = 0;
|
|
14483
14706
|
let keptBranches = 0;
|
|
14484
14707
|
for (const a of actions) {
|
|
14485
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
14708
|
+
if (!dryRun && base && !a.keepBranch && existsSync27(a.path) && isHandBackMerge(a.path, base)) {
|
|
14486
14709
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
14487
14710
|
log.emit(
|
|
14488
14711
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -14490,7 +14713,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
14490
14713
|
);
|
|
14491
14714
|
}
|
|
14492
14715
|
}
|
|
14493
|
-
if (!dryRun &&
|
|
14716
|
+
if (!dryRun && existsSync27(a.path)) {
|
|
14494
14717
|
let dirty = false;
|
|
14495
14718
|
try {
|
|
14496
14719
|
dirty = status(a.path).length > 0;
|
|
@@ -14501,7 +14724,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
14501
14724
|
continue;
|
|
14502
14725
|
}
|
|
14503
14726
|
}
|
|
14504
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
14727
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync27(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
14505
14728
|
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
14506
14729
|
continue;
|
|
14507
14730
|
}
|
|
@@ -14550,7 +14773,7 @@ function isHandBackMerge(path, base) {
|
|
|
14550
14773
|
if (!msgPath) return false;
|
|
14551
14774
|
let first;
|
|
14552
14775
|
try {
|
|
14553
|
-
first =
|
|
14776
|
+
first = readFileSync22(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
14554
14777
|
} catch {
|
|
14555
14778
|
return false;
|
|
14556
14779
|
}
|
|
@@ -14655,14 +14878,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
14655
14878
|
}
|
|
14656
14879
|
|
|
14657
14880
|
// src/stream-sweep.ts
|
|
14658
|
-
import { existsSync as
|
|
14659
|
-
import { join as
|
|
14881
|
+
import { existsSync as existsSync28, readdirSync as readdirSync9, rmSync as rmSync13, statSync as statSync8 } from "fs";
|
|
14882
|
+
import { join as join29 } from "path";
|
|
14660
14883
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
14661
14884
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
14662
|
-
const dir =
|
|
14663
|
-
if (!
|
|
14885
|
+
const dir = join29(stateDir, "streams");
|
|
14886
|
+
if (!existsSync28(dir)) return [];
|
|
14664
14887
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
14665
|
-
return readdirSync9(dir).map((name) =>
|
|
14888
|
+
return readdirSync9(dir).map((name) => join29(dir, name)).filter((path) => {
|
|
14666
14889
|
try {
|
|
14667
14890
|
return statSync8(path).mtimeMs < cutoff;
|
|
14668
14891
|
} catch {
|
|
@@ -14688,9 +14911,9 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
14688
14911
|
return removed;
|
|
14689
14912
|
}
|
|
14690
14913
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
14691
|
-
if (!
|
|
14914
|
+
if (!existsSync28(stateDir)) return [];
|
|
14692
14915
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
14693
|
-
return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) =>
|
|
14916
|
+
return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join29(stateDir, name)).filter((path) => {
|
|
14694
14917
|
try {
|
|
14695
14918
|
return statSync8(path).mtimeMs < cutoff;
|
|
14696
14919
|
} catch {
|
|
@@ -14700,26 +14923,26 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
14700
14923
|
}
|
|
14701
14924
|
|
|
14702
14925
|
// src/cli.ts
|
|
14703
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
14926
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync28, existsSync as existsSync34, mkdirSync as mkdirSync21, writeFileSync as writeFileSync22, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync14 } from "fs";
|
|
14704
14927
|
import { createInterface } from "readline/promises";
|
|
14705
14928
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
14706
14929
|
|
|
14707
14930
|
// src/ssh-keys.ts
|
|
14708
|
-
import { execFileSync as
|
|
14709
|
-
import { existsSync as
|
|
14710
|
-
import { dirname as
|
|
14931
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
14932
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync17, readFileSync as readFileSync23, chmodSync } from "fs";
|
|
14933
|
+
import { dirname as dirname19, join as join30 } from "path";
|
|
14711
14934
|
var SshKeyError = class extends Error {
|
|
14712
14935
|
};
|
|
14713
14936
|
function shipSshKeyPathFor(stateDir) {
|
|
14714
|
-
return
|
|
14937
|
+
return join30(stateDir, "ssh", "id_ed25519");
|
|
14715
14938
|
}
|
|
14716
14939
|
function ensureShipSshKeypair(stateDir) {
|
|
14717
14940
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
14718
14941
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
14719
|
-
if (!
|
|
14720
|
-
|
|
14942
|
+
if (!existsSync29(privateKeyPath)) {
|
|
14943
|
+
mkdirSync17(dirname19(privateKeyPath), { recursive: true });
|
|
14721
14944
|
try {
|
|
14722
|
-
|
|
14945
|
+
execFileSync6("ssh-keygen", [
|
|
14723
14946
|
"-t",
|
|
14724
14947
|
"ed25519",
|
|
14725
14948
|
"-N",
|
|
@@ -14737,71 +14960,21 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
14737
14960
|
}
|
|
14738
14961
|
chmodSync(privateKeyPath, 384);
|
|
14739
14962
|
}
|
|
14740
|
-
if (!
|
|
14963
|
+
if (!existsSync29(publicKeyPath)) {
|
|
14741
14964
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
14742
14965
|
}
|
|
14743
|
-
const publicKey =
|
|
14966
|
+
const publicKey = readFileSync23(publicKeyPath, "utf8").trim();
|
|
14744
14967
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
14745
14968
|
}
|
|
14746
14969
|
function sshKeygenAvailable() {
|
|
14747
14970
|
try {
|
|
14748
|
-
|
|
14971
|
+
execFileSync6("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
14749
14972
|
return true;
|
|
14750
14973
|
} catch (e) {
|
|
14751
14974
|
return e.code !== "ENOENT";
|
|
14752
14975
|
}
|
|
14753
14976
|
}
|
|
14754
14977
|
|
|
14755
|
-
// src/machine-id.ts
|
|
14756
|
-
import { execFileSync as execFileSync6 } from "child_process";
|
|
14757
|
-
import { createHash as createHash4 } from "crypto";
|
|
14758
|
-
import { existsSync as existsSync28, mkdirSync as mkdirSync16, readFileSync as readFileSync22, writeFileSync as writeFileSync17 } from "fs";
|
|
14759
|
-
import { dirname as dirname18, join as join30 } from "path";
|
|
14760
|
-
function machineIdPathFor(stateDir) {
|
|
14761
|
-
return join30(stateDir, "machine-id");
|
|
14762
|
-
}
|
|
14763
|
-
var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
|
|
14764
|
-
function readRawMachineId(platform2 = process.platform) {
|
|
14765
|
-
try {
|
|
14766
|
-
if (platform2 === "darwin") {
|
|
14767
|
-
const out = execFileSync6("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
14768
|
-
encoding: "utf8",
|
|
14769
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
14770
|
-
});
|
|
14771
|
-
return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
|
|
14772
|
-
}
|
|
14773
|
-
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
14774
|
-
if (existsSync28(p)) {
|
|
14775
|
-
const id = readFileSync22(p, "utf8").trim();
|
|
14776
|
-
if (id) return id;
|
|
14777
|
-
}
|
|
14778
|
-
}
|
|
14779
|
-
} catch {
|
|
14780
|
-
}
|
|
14781
|
-
return void 0;
|
|
14782
|
-
}
|
|
14783
|
-
function ensureMachineId(stateDir, opts = {}) {
|
|
14784
|
-
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
14785
|
-
const path = machineIdPathFor(stateDir);
|
|
14786
|
-
if (existsSync28(path)) {
|
|
14787
|
-
const cached = readFileSync22(path, "utf8").trim();
|
|
14788
|
-
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
14789
|
-
}
|
|
14790
|
-
const raw = (opts.read ?? readRawMachineId)();
|
|
14791
|
-
if (!raw) return void 0;
|
|
14792
|
-
const hashed = sha256(raw);
|
|
14793
|
-
mkdirSync16(dirname18(path), { recursive: true });
|
|
14794
|
-
writeFileSync17(path, `${hashed}
|
|
14795
|
-
`, { mode: 384 });
|
|
14796
|
-
return hashed;
|
|
14797
|
-
}
|
|
14798
|
-
function matchShipRows(ships, shipName2, machineId) {
|
|
14799
|
-
const hasColumn = ships.some((r) => "machine_id" in r);
|
|
14800
|
-
const byId = hasColumn && machineId ? ships.filter((r) => r.machine_id === machineId) : [];
|
|
14801
|
-
if (byId.length > 0) return { rows: byId, by: "machine_id", hasColumn };
|
|
14802
|
-
return { rows: ships.filter((r) => (r.name ?? "").trim() === shipName2.trim()), by: "name", hasColumn };
|
|
14803
|
-
}
|
|
14804
|
-
|
|
14805
14978
|
// src/passenger-containers.ts
|
|
14806
14979
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
14807
14980
|
import { createHash as createHash5 } from "crypto";
|
|
@@ -15006,7 +15179,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
15006
15179
|
|
|
15007
15180
|
// src/tunnel.ts
|
|
15008
15181
|
import { spawn as spawn5 } from "child_process";
|
|
15009
|
-
import { existsSync as
|
|
15182
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync18, readFileSync as readFileSync24, writeFileSync as writeFileSync19, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
|
|
15010
15183
|
import { randomInt } from "crypto";
|
|
15011
15184
|
import { join as join31 } from "path";
|
|
15012
15185
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
@@ -15079,17 +15252,17 @@ function tunnelStatePath(stateDir, workspaceId) {
|
|
|
15079
15252
|
}
|
|
15080
15253
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
15081
15254
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
15082
|
-
if (!
|
|
15255
|
+
if (!existsSync30(path)) return void 0;
|
|
15083
15256
|
try {
|
|
15084
|
-
return JSON.parse(
|
|
15257
|
+
return JSON.parse(readFileSync24(path, "utf8"));
|
|
15085
15258
|
} catch {
|
|
15086
15259
|
return void 0;
|
|
15087
15260
|
}
|
|
15088
15261
|
}
|
|
15089
15262
|
function writePersistedTunnel(stateDir, state) {
|
|
15090
15263
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
15091
|
-
|
|
15092
|
-
|
|
15264
|
+
mkdirSync18(join31(stateDir, "passengers"), { recursive: true });
|
|
15265
|
+
writeFileSync19(path, `${JSON.stringify(state, null, 2)}
|
|
15093
15266
|
`);
|
|
15094
15267
|
}
|
|
15095
15268
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -15187,7 +15360,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
15187
15360
|
}
|
|
15188
15361
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
15189
15362
|
const dir = join31(stateDir, "passengers");
|
|
15190
|
-
if (!
|
|
15363
|
+
if (!existsSync30(dir)) return [];
|
|
15191
15364
|
return readdirSync10(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
15192
15365
|
}
|
|
15193
15366
|
|
|
@@ -15308,7 +15481,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
15308
15481
|
import { createServer as createHttpServer } from "http";
|
|
15309
15482
|
import { spawn as spawn6 } from "child_process";
|
|
15310
15483
|
import { randomBytes } from "crypto";
|
|
15311
|
-
import { existsSync as
|
|
15484
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync25, writeFileSync as writeFileSync20, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
|
|
15312
15485
|
import { join as join32 } from "path";
|
|
15313
15486
|
|
|
15314
15487
|
// src/secret-compare.ts
|
|
@@ -15402,17 +15575,17 @@ function syncDaemonStatePath(stateDir, workspaceId) {
|
|
|
15402
15575
|
}
|
|
15403
15576
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
15404
15577
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
15405
|
-
if (!
|
|
15578
|
+
if (!existsSync31(path)) return void 0;
|
|
15406
15579
|
try {
|
|
15407
|
-
return JSON.parse(
|
|
15580
|
+
return JSON.parse(readFileSync25(path, "utf8"));
|
|
15408
15581
|
} catch {
|
|
15409
15582
|
return void 0;
|
|
15410
15583
|
}
|
|
15411
15584
|
}
|
|
15412
15585
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
15413
15586
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
15414
|
-
|
|
15415
|
-
|
|
15587
|
+
mkdirSync19(join32(stateDir, "passengers"), { recursive: true });
|
|
15588
|
+
writeFileSync20(path, `${JSON.stringify(state, null, 2)}
|
|
15416
15589
|
`);
|
|
15417
15590
|
}
|
|
15418
15591
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -15433,7 +15606,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
15433
15606
|
}
|
|
15434
15607
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
15435
15608
|
const dir = join32(stateDir, "passengers");
|
|
15436
|
-
if (!
|
|
15609
|
+
if (!existsSync31(dir)) return [];
|
|
15437
15610
|
const suffix = "-sync.json";
|
|
15438
15611
|
return readdirSync11(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
15439
15612
|
}
|
|
@@ -15521,11 +15694,11 @@ function describeCause(err) {
|
|
|
15521
15694
|
}
|
|
15522
15695
|
|
|
15523
15696
|
// src/daemon.ts
|
|
15524
|
-
import { existsSync as
|
|
15697
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync20, readFileSync as readFileSync27, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync21 } from "fs";
|
|
15525
15698
|
import { join as join34 } from "path";
|
|
15526
15699
|
|
|
15527
15700
|
// src/interrupted.ts
|
|
15528
|
-
import { existsSync as
|
|
15701
|
+
import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as readFileSync26 } from "fs";
|
|
15529
15702
|
import { join as join33 } from "path";
|
|
15530
15703
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
15531
15704
|
function interruptedComment(b) {
|
|
@@ -15545,16 +15718,16 @@ var pidAlive = (pid) => {
|
|
|
15545
15718
|
};
|
|
15546
15719
|
function runFinished(streamPath) {
|
|
15547
15720
|
const path = `${streamPath}.meta.json`;
|
|
15548
|
-
if (!
|
|
15721
|
+
if (!existsSync32(path)) return false;
|
|
15549
15722
|
try {
|
|
15550
|
-
return JSON.parse(
|
|
15723
|
+
return JSON.parse(readFileSync26(path, "utf8")).running !== true;
|
|
15551
15724
|
} catch {
|
|
15552
15725
|
return true;
|
|
15553
15726
|
}
|
|
15554
15727
|
}
|
|
15555
15728
|
function orphanedStreams(o) {
|
|
15556
15729
|
const dir = join33(o.stateDir, "streams");
|
|
15557
|
-
if (!
|
|
15730
|
+
if (!existsSync32(dir)) return [];
|
|
15558
15731
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
15559
15732
|
const out = [];
|
|
15560
15733
|
for (const name of readdirSync12(dir)) {
|
|
@@ -15688,7 +15861,7 @@ function computeTreeSignature(paths) {
|
|
|
15688
15861
|
}
|
|
15689
15862
|
function makeStaleChecker(paths, opts = {}) {
|
|
15690
15863
|
const initial = computeTreeSignature(paths);
|
|
15691
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
15864
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync33(opts.reloadSentinel);
|
|
15692
15865
|
}
|
|
15693
15866
|
function reloadSentinelFor(stateDir) {
|
|
15694
15867
|
return join34(stateDir, "reload-requested");
|
|
@@ -15697,14 +15870,14 @@ function daemonStampFor(stateDir) {
|
|
|
15697
15870
|
return join34(stateDir, "daemon.started");
|
|
15698
15871
|
}
|
|
15699
15872
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
15700
|
-
|
|
15873
|
+
mkdirSync20(stateDir, { recursive: true });
|
|
15701
15874
|
rmSync14(reloadSentinelFor(stateDir), { force: true });
|
|
15702
|
-
|
|
15875
|
+
writeFileSync21(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
15703
15876
|
`);
|
|
15704
15877
|
}
|
|
15705
15878
|
function readDaemonStarted(stateDir) {
|
|
15706
15879
|
try {
|
|
15707
|
-
const v = JSON.parse(
|
|
15880
|
+
const v = JSON.parse(readFileSync27(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
15708
15881
|
return typeof v === "number" ? v : null;
|
|
15709
15882
|
} catch {
|
|
15710
15883
|
return null;
|
|
@@ -15767,6 +15940,7 @@ async function runOnePassUnguarded(o) {
|
|
|
15767
15940
|
const spawn7 = o.spawnRoleAgent ?? runRoleAgent;
|
|
15768
15941
|
const breaker = o.spawnBreaker ?? new SpawnBreaker();
|
|
15769
15942
|
const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
|
|
15943
|
+
if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
|
|
15770
15944
|
try {
|
|
15771
15945
|
await (o.reconcile ?? reconcileForRoute)(ctx, o);
|
|
15772
15946
|
} catch (e) {
|
|
@@ -16089,7 +16263,7 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
16089
16263
|
}
|
|
16090
16264
|
|
|
16091
16265
|
// src/cli.ts
|
|
16092
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
16266
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname20(process.execPath) : resolve5(dirname20(fileURLToPath(import.meta.url)), "..");
|
|
16093
16267
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
16094
16268
|
var CYCLE_SECONDS2 = 120;
|
|
16095
16269
|
var STALE_CYCLES = 3;
|
|
@@ -16245,6 +16419,7 @@ function usage(problem, requested = false) {
|
|
|
16245
16419
|
crew agents sync [route] [--force] [--revoke-seat-keys] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt)
|
|
16246
16420
|
crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook; --harness H adds which CLI it serves)
|
|
16247
16421
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
16422
|
+
crew ship rename NAME [--json] rename this ship in crew.yaml and on every workspace's Ships row
|
|
16248
16423
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
|
|
16249
16424
|
crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
|
|
16250
16425
|
crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--agent, --write route|repo, --recipes, --json)
|
|
@@ -16333,6 +16508,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
16333
16508
|
"run",
|
|
16334
16509
|
"sample-ticket",
|
|
16335
16510
|
"selftest",
|
|
16511
|
+
"ship",
|
|
16336
16512
|
"skills",
|
|
16337
16513
|
"status",
|
|
16338
16514
|
"sync",
|
|
@@ -16482,9 +16658,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
16482
16658
|
firstRun = { answers, configPath };
|
|
16483
16659
|
cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
|
|
16484
16660
|
} else {
|
|
16485
|
-
const toleratesNoRoutes = command === "repos" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
16661
|
+
const toleratesNoRoutes = command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
16486
16662
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
16487
|
-
if (configless && !
|
|
16663
|
+
if (configless && !existsSync34(findConfigFile(CREW_HOME))) {
|
|
16488
16664
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
16489
16665
|
} else {
|
|
16490
16666
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -16506,7 +16682,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16506
16682
|
const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
|
|
16507
16683
|
const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
|
|
16508
16684
|
const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
|
|
16509
|
-
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
|
|
16685
|
+
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "ship" || command === "projects" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
|
|
16510
16686
|
let route;
|
|
16511
16687
|
try {
|
|
16512
16688
|
route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
|
|
@@ -16723,7 +16899,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16723
16899
|
if (host === "linux" && hasSystemd) {
|
|
16724
16900
|
const label = labelFor(CREW_HOME, "run");
|
|
16725
16901
|
const staleTimer = join35(homedir9(), ".config", "systemd", "user", `${label}.timer`);
|
|
16726
|
-
if (
|
|
16902
|
+
if (existsSync34(staleTimer)) {
|
|
16727
16903
|
process.stdout.write(
|
|
16728
16904
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
16729
16905
|
`
|
|
@@ -16749,7 +16925,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16749
16925
|
const tracker = new Tracker(c, cfg.ship);
|
|
16750
16926
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
16751
16927
|
for (const r of await resolvedRepos(c)) {
|
|
16752
|
-
if (!
|
|
16928
|
+
if (!existsSync34(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
16753
16929
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
16754
16930
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
16755
16931
|
}
|
|
@@ -16767,7 +16943,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16767
16943
|
}
|
|
16768
16944
|
async function doctorFix(o) {
|
|
16769
16945
|
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))));
|
|
16770
|
-
const unitExists = (p) =>
|
|
16946
|
+
const unitExists = (p) => existsSync34(p);
|
|
16771
16947
|
const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
16772
16948
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
16773
16949
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -16784,7 +16960,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16784
16960
|
for (const a of plan.actions) {
|
|
16785
16961
|
if (a.kind === "enable") {
|
|
16786
16962
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
16787
|
-
|
|
16963
|
+
writeFileSync22(cfg.configFile, planEnable(readFileSync28(cfg.configFile, "utf8"), a.route));
|
|
16788
16964
|
enabledSound.add(a.route);
|
|
16789
16965
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
16790
16966
|
} else {
|
|
@@ -17267,6 +17443,11 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17267
17443
|
case "poll":
|
|
17268
17444
|
case "run": {
|
|
17269
17445
|
const skipInlineRelease = flag("no-release");
|
|
17446
|
+
if (!dryRun) {
|
|
17447
|
+
for (const r of fleetWide ? cfg.routes : [route]) {
|
|
17448
|
+
await reconcileRouteShipName({ route: r, ship: cfg.ship, configFile: cfg.configFile, emit: emit.forRoute(r.route) });
|
|
17449
|
+
}
|
|
17450
|
+
}
|
|
17270
17451
|
if (fleetWide) {
|
|
17271
17452
|
const fleet = await decideFleet({
|
|
17272
17453
|
routes: cfg.routes,
|
|
@@ -17800,6 +17981,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17800
17981
|
state,
|
|
17801
17982
|
maxConcurrentAgents: cfg.ship.maxConcurrentAgents,
|
|
17802
17983
|
spawnBreaker,
|
|
17984
|
+
configFile: cfg.configFile,
|
|
17803
17985
|
newEmitter: () => new Emitter({
|
|
17804
17986
|
route: route.route,
|
|
17805
17987
|
eventFile: eventFileFor(cfg.ship.stateDir),
|
|
@@ -18006,15 +18188,72 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18006
18188
|
`);
|
|
18007
18189
|
if (!dryRun) {
|
|
18008
18190
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
18009
|
-
const raw =
|
|
18191
|
+
const raw = existsSync34(resolvedPath) ? JSON.parse(readFileSync28(resolvedPath, "utf8")) : {};
|
|
18010
18192
|
raw.agentSkills = result.agentSkills;
|
|
18011
|
-
|
|
18012
|
-
|
|
18193
|
+
mkdirSync21(dirOf(resolvedPath), { recursive: true });
|
|
18194
|
+
writeFileSync22(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
18013
18195
|
`);
|
|
18014
18196
|
}
|
|
18015
18197
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
18016
18198
|
break;
|
|
18017
18199
|
}
|
|
18200
|
+
case "ship": {
|
|
18201
|
+
if (positional[1] !== "rename" || positional.length !== 3) {
|
|
18202
|
+
process.stderr.write("crew ship rename NAME [--json]\n");
|
|
18203
|
+
process.exit(2);
|
|
18204
|
+
}
|
|
18205
|
+
let newName;
|
|
18206
|
+
try {
|
|
18207
|
+
newName = validateShipName(positional[2]);
|
|
18208
|
+
} catch (e) {
|
|
18209
|
+
if (e instanceof ShipNameError) {
|
|
18210
|
+
process.stderr.write(`crew ship: ${e.message}
|
|
18211
|
+
`);
|
|
18212
|
+
process.exit(2);
|
|
18213
|
+
}
|
|
18214
|
+
throw e;
|
|
18215
|
+
}
|
|
18216
|
+
const oldName = cfg.ship.name;
|
|
18217
|
+
writeFileSync22(cfg.configFile, setShipNameInYaml(readFileSync28(cfg.configFile, "utf8"), newName));
|
|
18218
|
+
cfg.ship.name = newName;
|
|
18219
|
+
let renameMachineId;
|
|
18220
|
+
try {
|
|
18221
|
+
renameMachineId = ensureMachineId(cfg.ship.stateDir);
|
|
18222
|
+
} catch {
|
|
18223
|
+
}
|
|
18224
|
+
const outcomes = [];
|
|
18225
|
+
for (const r of cfg.routes) {
|
|
18226
|
+
if (!r.resolved?.shipsModelId) {
|
|
18227
|
+
outcomes.push({ route: r.route, status: "no-ships-table" });
|
|
18228
|
+
continue;
|
|
18229
|
+
}
|
|
18230
|
+
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, r.route);
|
|
18231
|
+
const tracker = new Tracker(r, cfg.ship);
|
|
18232
|
+
const picked = pickShipRow(await tracker.shipRows(), renameMachineId, [oldName, readSyncedName(resolvedFile) ?? ""]);
|
|
18233
|
+
if (picked.kind !== "row") {
|
|
18234
|
+
outcomes.push({ route: r.route, status: picked.kind === "unsupported" ? "unsupported" : picked.kind === "none" ? "no-row" : "ambiguous" });
|
|
18235
|
+
continue;
|
|
18236
|
+
}
|
|
18237
|
+
try {
|
|
18238
|
+
await tracker.renameShipRow(picked.row.id, newName);
|
|
18239
|
+
writeSyncedName(resolvedFile, newName);
|
|
18240
|
+
outcomes.push({ route: r.route, status: "renamed", rowId: picked.row.id });
|
|
18241
|
+
} catch (e) {
|
|
18242
|
+
outcomes.push({ route: r.route, status: "refused", rowId: picked.row.id, error: e.message });
|
|
18243
|
+
}
|
|
18244
|
+
}
|
|
18245
|
+
if (flag("json")) {
|
|
18246
|
+
process.stdout.write(`${JSON.stringify({ name: newName, previous: oldName, configFile: cfg.configFile, routes: outcomes })}
|
|
18247
|
+
`);
|
|
18248
|
+
} else {
|
|
18249
|
+
process.stdout.write(`ship renamed "${oldName}" -> "${newName}" in ${cfg.configFile}
|
|
18250
|
+
`);
|
|
18251
|
+
for (const o of outcomes) process.stdout.write(` ${o.route}: ${o.status}${o.error ? ` (${o.error})` : ""}
|
|
18252
|
+
`);
|
|
18253
|
+
}
|
|
18254
|
+
if (outcomes.some((o) => o.status === "refused")) process.exitCode = 1;
|
|
18255
|
+
break;
|
|
18256
|
+
}
|
|
18018
18257
|
case "repos": {
|
|
18019
18258
|
const sub = positional[1];
|
|
18020
18259
|
const usage2 = () => {
|
|
@@ -18022,13 +18261,13 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18022
18261
|
process.exit(2);
|
|
18023
18262
|
};
|
|
18024
18263
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
|
|
18025
|
-
const configText =
|
|
18264
|
+
const configText = readFileSync28(cfg.configFile, "utf8");
|
|
18026
18265
|
if (sub === "hooks") {
|
|
18027
18266
|
try {
|
|
18028
18267
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
18029
|
-
const dir = repoDirOf(configText,
|
|
18030
|
-
if (!
|
|
18031
|
-
const files = { exists: (r) =>
|
|
18268
|
+
const dir = repoDirOf(configText, dirname20(cfg.configFile), a.route, a.name);
|
|
18269
|
+
if (!existsSync34(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
18270
|
+
const files = { exists: (r) => existsSync34(resolvePath(dir, r)), list: (r) => {
|
|
18032
18271
|
try {
|
|
18033
18272
|
return readdirSync14(resolvePath(dir, r));
|
|
18034
18273
|
} catch {
|
|
@@ -18036,7 +18275,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18036
18275
|
}
|
|
18037
18276
|
}, read: (r) => {
|
|
18038
18277
|
try {
|
|
18039
|
-
return
|
|
18278
|
+
return readFileSync28(resolvePath(dir, r), "utf8");
|
|
18040
18279
|
} catch {
|
|
18041
18280
|
return void 0;
|
|
18042
18281
|
}
|
|
@@ -18074,13 +18313,13 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18074
18313
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
18075
18314
|
if (write === "route") {
|
|
18076
18315
|
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks);
|
|
18077
|
-
|
|
18316
|
+
writeFileSync22(cfg.configFile, plan.text);
|
|
18078
18317
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
|
|
18079
18318
|
} else if (write === "repo") {
|
|
18080
18319
|
const have = existingRepoConfig(files);
|
|
18081
18320
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
18082
18321
|
const file = resolvePath(dir, ".crew.yaml");
|
|
18083
|
-
|
|
18322
|
+
writeFileSync22(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
18084
18323
|
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [] };
|
|
18085
18324
|
}
|
|
18086
18325
|
if (flag("json")) {
|
|
@@ -18132,7 +18371,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18132
18371
|
if (sub === "list" && !routeName) usage2();
|
|
18133
18372
|
if (sub === "add") {
|
|
18134
18373
|
try {
|
|
18135
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
18374
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync34(resolvePath(p.replace(/^~(?=\/|$)/, homedir9()))));
|
|
18136
18375
|
routeName = a.route;
|
|
18137
18376
|
addPath = a.path;
|
|
18138
18377
|
addName = a.name;
|
|
@@ -18161,15 +18400,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18161
18400
|
}
|
|
18162
18401
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
18163
18402
|
let resolvedRemotes;
|
|
18164
|
-
if (
|
|
18403
|
+
if (existsSync34(resolvedFile)) {
|
|
18165
18404
|
try {
|
|
18166
|
-
resolvedRemotes = JSON.parse(
|
|
18405
|
+
resolvedRemotes = JSON.parse(readFileSync28(resolvedFile, "utf8"));
|
|
18167
18406
|
} catch {
|
|
18168
18407
|
}
|
|
18169
18408
|
}
|
|
18170
18409
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
18171
18410
|
const text = configText;
|
|
18172
|
-
const base =
|
|
18411
|
+
const base = dirname20(cfg.configFile);
|
|
18173
18412
|
try {
|
|
18174
18413
|
if (sub === "list") {
|
|
18175
18414
|
if (flag("json")) {
|
|
@@ -18179,10 +18418,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18179
18418
|
base,
|
|
18180
18419
|
resolved: resolvedRemotes,
|
|
18181
18420
|
originOf,
|
|
18182
|
-
exists: (d) =>
|
|
18421
|
+
exists: (d) => existsSync34(d),
|
|
18183
18422
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
18184
18423
|
try {
|
|
18185
|
-
return Object.keys(parseRepoConfig(
|
|
18424
|
+
return Object.keys(parseRepoConfig(readFileSync28(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
18186
18425
|
} catch {
|
|
18187
18426
|
return false;
|
|
18188
18427
|
}
|
|
@@ -18198,7 +18437,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18198
18437
|
for (const r of repos) {
|
|
18199
18438
|
const origin2 = originOf(r.dir);
|
|
18200
18439
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
18201
|
-
const note = !
|
|
18440
|
+
const note = !existsSync34(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
18202
18441
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
18203
18442
|
`);
|
|
18204
18443
|
}
|
|
@@ -18210,9 +18449,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18210
18449
|
` : `${human}
|
|
18211
18450
|
`);
|
|
18212
18451
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
|
|
18213
|
-
if (
|
|
18452
|
+
if (existsSync34(plan2.target) && (existsSync34(`${plan2.target}/.git`) || readdirSync14(plan2.target).length > 0)) {
|
|
18214
18453
|
throw new ReposError(
|
|
18215
|
-
|
|
18454
|
+
existsSync34(`${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`
|
|
18216
18455
|
);
|
|
18217
18456
|
}
|
|
18218
18457
|
if (dryRun) {
|
|
@@ -18230,12 +18469,12 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18230
18469
|
throw new ReposError(`${e.message}${authHint}
|
|
18231
18470
|
crew.yaml was not changed`);
|
|
18232
18471
|
}
|
|
18233
|
-
|
|
18472
|
+
writeFileSync22(cfg.configFile, plan2.text);
|
|
18234
18473
|
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}`);
|
|
18235
18474
|
break;
|
|
18236
18475
|
}
|
|
18237
18476
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir9()));
|
|
18238
|
-
const top =
|
|
18477
|
+
const top = existsSync34(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
18239
18478
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
18240
18479
|
const origin = originOf(top);
|
|
18241
18480
|
const plan = planRepoAdd({
|
|
@@ -18284,15 +18523,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18284
18523
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
18285
18524
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
18286
18525
|
`);
|
|
18287
|
-
if (
|
|
18288
|
-
const raw = JSON.parse(
|
|
18526
|
+
if (existsSync34(resolvedFile)) {
|
|
18527
|
+
const raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
|
|
18289
18528
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
18290
18529
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
18291
|
-
|
|
18530
|
+
writeFileSync22(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
18292
18531
|
`);
|
|
18293
18532
|
}
|
|
18294
18533
|
}
|
|
18295
|
-
|
|
18534
|
+
writeFileSync22(cfg.configFile, plan.text);
|
|
18296
18535
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
18297
18536
|
`);
|
|
18298
18537
|
} catch (e) {
|
|
@@ -18576,7 +18815,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18576
18815
|
platform: cfg.ship.platform,
|
|
18577
18816
|
machineId,
|
|
18578
18817
|
hostPassengers: firstRun?.answers.hostPassengers ?? existingRoute?.hostPassengers === true,
|
|
18579
|
-
sshPublicKey
|
|
18818
|
+
sshPublicKey,
|
|
18819
|
+
lastSyncedName: (r) => readSyncedName(resolvedPathFor(cfg.ship.stateDir, r))
|
|
18580
18820
|
};
|
|
18581
18821
|
const runConnect = async (ws, proj) => {
|
|
18582
18822
|
let found = await discover({ ...authOpts, workspace: ws, project: proj, area: value("area"), ship, dryRun, interactive: canAsk || connectEvents.json });
|
|
@@ -18633,6 +18873,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18633
18873
|
}
|
|
18634
18874
|
process.exit(2);
|
|
18635
18875
|
}
|
|
18876
|
+
if (found.shipNameAdopted && !dryRun) {
|
|
18877
|
+
writeFileSync22(cfg.configFile, setShipNameInYaml(readFileSync28(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
18878
|
+
cfg.ship.name = found.shipNameAdopted;
|
|
18879
|
+
}
|
|
18636
18880
|
if (found.shipRow) {
|
|
18637
18881
|
connectEvents.emit("ship_row", `Ships row "${found.shipRow.name}" ${found.shipRow.action}`, found.shipRow);
|
|
18638
18882
|
}
|
|
@@ -18673,10 +18917,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18673
18917
|
}
|
|
18674
18918
|
}
|
|
18675
18919
|
let previous;
|
|
18676
|
-
const hadPreviousFile =
|
|
18920
|
+
const hadPreviousFile = existsSync34(resolvedPath);
|
|
18677
18921
|
if (hadPreviousFile) {
|
|
18678
18922
|
try {
|
|
18679
|
-
previous = JSON.parse(
|
|
18923
|
+
previous = JSON.parse(readFileSync28(resolvedPath, "utf8"));
|
|
18680
18924
|
} catch {
|
|
18681
18925
|
}
|
|
18682
18926
|
}
|
|
@@ -18788,18 +19032,20 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18788
19032
|
// nothing about its prompt actually changed.
|
|
18789
19033
|
...previous?.agentPersonas ? { agentPersonas: previous.agentPersonas } : {},
|
|
18790
19034
|
...previous?.agentSkills ? { agentSkills: previous.agentSkills } : {},
|
|
18791
|
-
...previous?.releaseAgentId ? { releaseAgentId: previous.releaseAgentId } : {}
|
|
19035
|
+
...previous?.releaseAgentId ? { releaseAgentId: previous.releaseAgentId } : {},
|
|
19036
|
+
// CREW-2495: the ship name this route last saw agree with its Ships row.
|
|
19037
|
+
...found.shipRow ? { shipName: found.shipRow.name } : previous?.shipName ? { shipName: previous.shipName } : {}
|
|
18792
19038
|
};
|
|
18793
19039
|
if (dryRun) {
|
|
18794
19040
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
18795
19041
|
`);
|
|
18796
19042
|
} else {
|
|
18797
|
-
|
|
19043
|
+
mkdirSync21(dirname20(resolvedPath), { recursive: true });
|
|
18798
19044
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
18799
19045
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
18800
|
-
|
|
19046
|
+
writeFileSync22(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
18801
19047
|
`);
|
|
18802
|
-
|
|
19048
|
+
renameSync3(tmpPath, resolvedPath);
|
|
18803
19049
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
18804
19050
|
`);
|
|
18805
19051
|
}
|
|
@@ -18839,8 +19085,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18839
19085
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
18840
19086
|
`);
|
|
18841
19087
|
} else {
|
|
18842
|
-
|
|
18843
|
-
|
|
19088
|
+
mkdirSync21(dirname20(apiKeyFile), { recursive: true });
|
|
19089
|
+
writeFileSync22(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
18844
19090
|
`, { mode: 384 });
|
|
18845
19091
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
18846
19092
|
`);
|
|
@@ -19528,7 +19774,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19528
19774
|
};
|
|
19529
19775
|
const droppedRoutes = [];
|
|
19530
19776
|
try {
|
|
19531
|
-
const rawCfg = parseYaml2(
|
|
19777
|
+
const rawCfg = parseYaml2(readFileSync28(cfg.configFile, "utf8"));
|
|
19532
19778
|
for (const r of rawCfg?.routes ?? []) {
|
|
19533
19779
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
19534
19780
|
try {
|
|
@@ -19570,8 +19816,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19570
19816
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
19571
19817
|
break;
|
|
19572
19818
|
}
|
|
19573
|
-
|
|
19574
|
-
|
|
19819
|
+
mkdirSync21(cfg.ship.stateDir, { recursive: true });
|
|
19820
|
+
writeFileSync22(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
19575
19821
|
`);
|
|
19576
19822
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
19577
19823
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -19622,7 +19868,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19622
19868
|
}
|
|
19623
19869
|
case "log": {
|
|
19624
19870
|
try {
|
|
19625
|
-
const text =
|
|
19871
|
+
const text = readFileSync28(cfg.ship.logFile, "utf8").split("\n");
|
|
19626
19872
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
19627
19873
|
`);
|
|
19628
19874
|
} catch {
|
|
@@ -19645,8 +19891,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19645
19891
|
};
|
|
19646
19892
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
19647
19893
|
const tracker = new Tracker(route, cfg.ship);
|
|
19648
|
-
const configText =
|
|
19649
|
-
const cfgBase =
|
|
19894
|
+
const configText = readFileSync28(cfg.configFile, "utf8");
|
|
19895
|
+
const cfgBase = dirname20(cfg.configFile);
|
|
19650
19896
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
19651
19897
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
19652
19898
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
|
|
@@ -19674,7 +19920,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19674
19920
|
}
|
|
19675
19921
|
if (configured.some((r) => r.dir === dir)) {
|
|
19676
19922
|
try {
|
|
19677
|
-
|
|
19923
|
+
writeFileSync22(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
|
|
19678
19924
|
} catch (e) {
|
|
19679
19925
|
if (e instanceof ReposError) fail(e.message);
|
|
19680
19926
|
throw e;
|
|
@@ -19682,29 +19928,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19682
19928
|
}
|
|
19683
19929
|
if (row) {
|
|
19684
19930
|
await tracker.deleteRepoRow(row.id);
|
|
19685
|
-
if (
|
|
19686
|
-
const raw = JSON.parse(
|
|
19931
|
+
if (existsSync34(resolvedFile)) {
|
|
19932
|
+
const raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
|
|
19687
19933
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
19688
19934
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
19689
|
-
|
|
19935
|
+
writeFileSync22(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
19690
19936
|
`);
|
|
19691
19937
|
}
|
|
19692
19938
|
}
|
|
19693
|
-
if (
|
|
19939
|
+
if (existsSync34(join35(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
|
|
19694
19940
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
19695
19941
|
break;
|
|
19696
19942
|
}
|
|
19697
19943
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
19698
|
-
const fresh = !
|
|
19699
|
-
if (
|
|
19944
|
+
const fresh = !existsSync34(join35(dir, ".git"));
|
|
19945
|
+
if (existsSync34(dir) && fresh && readdirSync14(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
19700
19946
|
if (dryRun) {
|
|
19701
19947
|
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`);
|
|
19702
19948
|
break;
|
|
19703
19949
|
}
|
|
19704
19950
|
if (fresh) {
|
|
19705
|
-
|
|
19706
|
-
|
|
19707
|
-
|
|
19951
|
+
mkdirSync21(dir, { recursive: true });
|
|
19952
|
+
writeFileSync22(join35(dir, "README.md"), SAMPLE_README);
|
|
19953
|
+
writeFileSync22(join35(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
19708
19954
|
const g = (...a) => {
|
|
19709
19955
|
const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
|
|
19710
19956
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -19722,17 +19968,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19722
19968
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
19723
19969
|
});
|
|
19724
19970
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
19725
|
-
if (
|
|
19726
|
-
const raw = JSON.parse(
|
|
19971
|
+
if (existsSync34(resolvedFile)) {
|
|
19972
|
+
const raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
|
|
19727
19973
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
19728
19974
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
19729
|
-
|
|
19975
|
+
writeFileSync22(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
19730
19976
|
`);
|
|
19731
19977
|
}
|
|
19732
19978
|
}
|
|
19733
19979
|
if (!configured.some((r) => r.dir === dir)) {
|
|
19734
19980
|
try {
|
|
19735
|
-
|
|
19981
|
+
writeFileSync22(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
|
|
19736
19982
|
} catch (e) {
|
|
19737
19983
|
if (e instanceof ReposError) fail(e.message);
|
|
19738
19984
|
throw e;
|
|
@@ -19853,6 +20099,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19853
20099
|
const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
|
|
19854
20100
|
const crewMinVersion = await readCrewFloor(route, cfg.ship) ?? null;
|
|
19855
20101
|
if (flag("json")) {
|
|
20102
|
+
const nameSync = await reconcileRouteShipName({ route, ship: cfg.ship, configFile: cfg.configFile, readOnly: true });
|
|
19856
20103
|
const repos = await resolvedRepos(route);
|
|
19857
20104
|
const seatIds = route.resolved?.seats ?? {};
|
|
19858
20105
|
const crewRows = Object.keys(seatIds).length ? await statusTracker.crewRows() : [];
|
|
@@ -19896,7 +20143,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19896
20143
|
eventFile: eventFileFor(cfg.ship.stateDir),
|
|
19897
20144
|
// Additive (CREW-1373): open ship-level items, including the
|
|
19898
20145
|
// derived `release_stale`. `version` stays 1.
|
|
19899
|
-
attention: openShipAttention(state)
|
|
20146
|
+
attention: openShipAttention(state),
|
|
20147
|
+
// Additive (CREW-2495): this ship's Ships-row name and whether it has drifted from `name`.
|
|
20148
|
+
rowName: nameSync.rowName ?? null,
|
|
20149
|
+
nameDrift: nameSync.drift
|
|
19900
20150
|
},
|
|
19901
20151
|
// Additive (CREW-1402): what the operator owes. `version` stays 1.
|
|
19902
20152
|
todo: todo.map((i) => ({ ticket: displayKey(i.ticket), title: i.ticket.title ?? null, reasons: i.reasons })),
|
|
@@ -20043,7 +20293,7 @@ watermark: ${state.watermark()}
|
|
|
20043
20293
|
}
|
|
20044
20294
|
for (const r of repos) {
|
|
20045
20295
|
const remote = r.config.branch.remote;
|
|
20046
|
-
if (
|
|
20296
|
+
if (existsSync34(r.dir)) {
|
|
20047
20297
|
pushAuth.push({
|
|
20048
20298
|
route: c.route,
|
|
20049
20299
|
repo: r.name,
|
|
@@ -20071,7 +20321,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
20071
20321
|
);
|
|
20072
20322
|
try {
|
|
20073
20323
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
20074
|
-
const evSize =
|
|
20324
|
+
const evSize = existsSync34(evFile) ? statSync10(evFile).size : 0;
|
|
20075
20325
|
process.stdout.write(
|
|
20076
20326
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
20077
20327
|
` : "events log: ok\n"
|
|
@@ -20082,7 +20332,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
20082
20332
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
20083
20333
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
20084
20334
|
try {
|
|
20085
|
-
return
|
|
20335
|
+
return readFileSync28(p, "utf8");
|
|
20086
20336
|
} catch {
|
|
20087
20337
|
return null;
|
|
20088
20338
|
}
|