@tablation/crew 0.71.13 → 0.72.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +478 -213
  2. 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 dirname19, join as join35, basename as basename11 } from "path";
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";
@@ -8792,40 +8798,47 @@ function readPointers(stateDir) {
8792
8798
  try {
8793
8799
  const parsed = JSON.parse(readFileSync16(lastSessionFile(stateDir), "utf8"));
8794
8800
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
8795
- return Object.fromEntries(Object.entries(parsed).filter((e) => typeof e[1] === "string"));
8801
+ const out = {};
8802
+ for (const [host, v] of Object.entries(parsed)) {
8803
+ const slugs = (Array.isArray(v) ? v : [v]).filter((x) => typeof x === "string" && x !== "");
8804
+ if (slugs.length) out[host] = slugs;
8805
+ }
8806
+ return out;
8796
8807
  } catch {
8797
8808
  return {};
8798
8809
  }
8799
8810
  }
8811
+ function writePointers(stateDir, pointers) {
8812
+ const file = lastSessionFile(stateDir);
8813
+ mkdirSync10(dirname8(file), { recursive: true });
8814
+ writeFileSync11(file, `${JSON.stringify(pointers, null, 2)}
8815
+ `, { mode: 384 });
8816
+ }
8800
8817
  function rememberSession(stateDir, host, workspaceSlug) {
8801
8818
  try {
8802
- const file = lastSessionFile(stateDir);
8803
- mkdirSync10(dirname8(file), { recursive: true });
8804
- writeFileSync11(file, `${JSON.stringify({ ...readPointers(stateDir), [host]: workspaceSlug }, null, 2)}
8805
- `, { mode: 384 });
8819
+ const all = readPointers(stateDir);
8820
+ writePointers(stateDir, { ...all, [host]: [...(all[host] ?? []).filter((w) => w !== workspaceSlug), workspaceSlug] });
8806
8821
  } catch {
8807
8822
  }
8808
8823
  }
8809
- async function resumeKeychainSession(o) {
8810
- const workspace = readPointers(o.stateDir)[o.host];
8811
- if (!workspace) return void 0;
8812
- try {
8813
- const stored = await o.store.get(o.host, workspace);
8814
- if (!stored?.apiKey) return void 0;
8815
- if (!await o.verify(stored.apiKey)) return void 0;
8816
- return { workspace, apiKey: stored.apiKey };
8817
- } catch {
8818
- return void 0;
8824
+ async function resumableSessions(o) {
8825
+ const out = [];
8826
+ for (const workspace of readPointers(o.stateDir)[o.host] ?? []) {
8827
+ try {
8828
+ const stored = await o.store.get(o.host, workspace);
8829
+ if (stored?.apiKey && await o.verify(stored.apiKey)) out.push({ workspace, apiKey: stored.apiKey });
8830
+ } catch {
8831
+ }
8819
8832
  }
8833
+ return out;
8820
8834
  }
8821
8835
  function rememberedSessions(stateDir) {
8822
- return Object.entries(readPointers(stateDir)).map(([host, workspace]) => ({ host, workspace }));
8836
+ return Object.entries(readPointers(stateDir)).flatMap(([host, ws]) => ws.map((workspace) => ({ host, workspace })));
8823
8837
  }
8824
8838
  function forgetSession(stateDir, host) {
8825
8839
  try {
8826
8840
  const { [host]: _gone, ...rest } = readPointers(stateDir);
8827
- if (Object.keys(rest).length) writeFileSync11(lastSessionFile(stateDir), `${JSON.stringify(rest, null, 2)}
8828
- `, { mode: 384 });
8841
+ if (Object.keys(rest).length) writePointers(stateDir, rest);
8829
8842
  else rmSync10(lastSessionFile(stateDir), { force: true });
8830
8843
  } catch {
8831
8844
  }
@@ -12636,6 +12649,217 @@ function nextRoles(decision, ran) {
12636
12649
  return decision.selection.pending.filter((r) => !ran.includes(r)).sort((a, b) => (decision.selection.ranks[a] ?? 0) - (decision.selection.ranks[b] ?? 0));
12637
12650
  }
12638
12651
 
12652
+ // src/ship-name.ts
12653
+ import { existsSync as existsSync25, mkdirSync as mkdirSync15, readFileSync as readFileSync20, renameSync as renameSync2, writeFileSync as writeFileSync17 } from "fs";
12654
+ import { dirname as dirname14 } from "path";
12655
+ import { parseDocument } from "yaml";
12656
+
12657
+ // src/machine-id.ts
12658
+ import { execFileSync as execFileSync4 } from "child_process";
12659
+ import { createHash as createHash3 } from "crypto";
12660
+ import { existsSync as existsSync24, mkdirSync as mkdirSync14, readFileSync as readFileSync19, writeFileSync as writeFileSync16 } from "fs";
12661
+ import { dirname as dirname13, join as join25 } from "path";
12662
+ function machineIdPathFor(stateDir) {
12663
+ return join25(stateDir, "machine-id");
12664
+ }
12665
+ var sha256 = (s) => createHash3("sha256").update(s).digest("hex");
12666
+ function readRawMachineId(platform2 = process.platform) {
12667
+ try {
12668
+ if (platform2 === "darwin") {
12669
+ const out = execFileSync4("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
12670
+ encoding: "utf8",
12671
+ stdio: ["ignore", "pipe", "ignore"]
12672
+ });
12673
+ return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
12674
+ }
12675
+ for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
12676
+ if (existsSync24(p)) {
12677
+ const id = readFileSync19(p, "utf8").trim();
12678
+ if (id) return id;
12679
+ }
12680
+ }
12681
+ } catch {
12682
+ }
12683
+ return void 0;
12684
+ }
12685
+ function ensureMachineId(stateDir, opts = {}) {
12686
+ if (opts.override?.trim()) return sha256(opts.override.trim());
12687
+ const path = machineIdPathFor(stateDir);
12688
+ if (existsSync24(path)) {
12689
+ const cached = readFileSync19(path, "utf8").trim();
12690
+ if (/^[0-9a-f]{64}$/.test(cached)) return cached;
12691
+ }
12692
+ const raw = (opts.read ?? readRawMachineId)();
12693
+ if (!raw) return void 0;
12694
+ const hashed = sha256(raw);
12695
+ mkdirSync14(dirname13(path), { recursive: true });
12696
+ writeFileSync16(path, `${hashed}
12697
+ `, { mode: 384 });
12698
+ return hashed;
12699
+ }
12700
+ function matchShipRows(ships, shipName2, machineId) {
12701
+ const hasColumn = ships.some((r) => "machine_id" in r);
12702
+ const byId = hasColumn && machineId ? ships.filter((r) => r.machine_id === machineId) : [];
12703
+ if (byId.length > 0) return { rows: byId, by: "machine_id", hasColumn };
12704
+ return { rows: ships.filter((r) => (r.name ?? "").trim() === shipName2.trim()), by: "name", hasColumn };
12705
+ }
12706
+
12707
+ // src/ship-name.ts
12708
+ var ShipNameError = class extends Error {
12709
+ };
12710
+ function validateShipName(raw) {
12711
+ const name = raw.trim();
12712
+ if (!name) throw new ShipNameError("the ship name cannot be empty");
12713
+ if (/[\u0000-\u001f\u007f]/.test(name)) throw new ShipNameError("the ship name cannot contain control characters");
12714
+ return name;
12715
+ }
12716
+ function planNameSync(local, row, lastSynced) {
12717
+ if (local === row) return { drift: "none", action: "none" };
12718
+ const rowMoved = lastSynced === void 0 || row !== lastSynced;
12719
+ const localMoved = lastSynced === void 0 || local !== lastSynced;
12720
+ if (rowMoved && localMoved) return { drift: "both", action: "row-wins" };
12721
+ if (rowMoved) return { drift: "row", action: "adopt" };
12722
+ return { drift: "local", action: "push" };
12723
+ }
12724
+ function shipNameOfYaml(text) {
12725
+ const v = parseDocument(text).getIn(["ship", "name"]);
12726
+ return typeof v === "string" && v.trim() ? v.trim() : void 0;
12727
+ }
12728
+ function setShipNameInYaml(text, name) {
12729
+ const doc = parseDocument(text);
12730
+ doc.setIn(["ship", "name"], name);
12731
+ return doc.toString();
12732
+ }
12733
+ function pickShipRow(rows, machineId, byName = []) {
12734
+ const hasColumn = rows.some((r) => "machine_id" in r);
12735
+ if (!hasColumn || !machineId) return { kind: "unsupported" };
12736
+ const byId = rows.filter((r) => r.machine_id === machineId);
12737
+ if (byId.length === 1) return { kind: "row", row: byId[0], by: "machine_id" };
12738
+ if (byId.length > 1) return { kind: "ambiguous" };
12739
+ const names = new Set(byName.map((n) => n.trim()));
12740
+ const named = rows.filter((r) => !r.machine_id && names.has((r.name ?? "").trim()));
12741
+ if (named.length === 1) return { kind: "row", row: named[0], by: "name" };
12742
+ return { kind: named.length > 1 ? "ambiguous" : "none" };
12743
+ }
12744
+ async function syncRouteName(i) {
12745
+ const picked = pickShipRow(i.rows, i.machineId);
12746
+ if (picked.kind !== "row") {
12747
+ const status2 = picked.kind === "none" ? "no-row" : picked.kind;
12748
+ return { status: status2, drift: "none", action: "none", name: i.local };
12749
+ }
12750
+ const rowName = (picked.row.name ?? "").trim();
12751
+ if (!rowName) return { status: "no-row", drift: "none", action: "none", name: i.local };
12752
+ const { drift, action } = planNameSync(i.local, rowName, i.lastSynced);
12753
+ const base = { drift, rowId: picked.row.id, rowName };
12754
+ if (action === "push") {
12755
+ if (!i.rename) return { status: "ok", ...base, action, name: i.local };
12756
+ try {
12757
+ await i.rename(picked.row.id, i.local);
12758
+ } catch (e) {
12759
+ return { status: "refused", ...base, action: "none", name: i.local, error: e.message };
12760
+ }
12761
+ return { status: "ok", ...base, action, name: i.local };
12762
+ }
12763
+ if (action === "adopt" || action === "row-wins") return { status: "ok", ...base, action, name: rowName, from: i.local };
12764
+ return { status: "ok", ...base, action, name: i.local };
12765
+ }
12766
+ function readSyncedName(resolvedFile) {
12767
+ if (!existsSync25(resolvedFile)) return void 0;
12768
+ try {
12769
+ const v = JSON.parse(readFileSync20(resolvedFile, "utf8")).shipName;
12770
+ return typeof v === "string" ? v : void 0;
12771
+ } catch {
12772
+ return void 0;
12773
+ }
12774
+ }
12775
+ function writeSyncedName(resolvedFile, name) {
12776
+ if (!existsSync25(resolvedFile)) return;
12777
+ let raw;
12778
+ try {
12779
+ raw = JSON.parse(readFileSync20(resolvedFile, "utf8"));
12780
+ } catch {
12781
+ return;
12782
+ }
12783
+ if (raw.shipName === name) return;
12784
+ raw.shipName = name;
12785
+ mkdirSync15(dirname14(resolvedFile), { recursive: true });
12786
+ const tmp = `${resolvedFile}.tmp${process.pid}`;
12787
+ writeFileSync17(tmp, `${JSON.stringify(raw, null, 2)}
12788
+ `);
12789
+ renameSync2(tmp, resolvedFile);
12790
+ }
12791
+ async function reconcileShipName(i) {
12792
+ let local = i.ship.name;
12793
+ try {
12794
+ local = shipNameOfYaml(readFileSync20(i.configFile, "utf8")) ?? local;
12795
+ } catch {
12796
+ }
12797
+ const resolvedFile = resolvedPathFor(i.stateDir, i.route);
12798
+ const lastSynced = readSyncedName(resolvedFile);
12799
+ let rows = [];
12800
+ try {
12801
+ rows = await i.rows();
12802
+ } catch {
12803
+ }
12804
+ const result = await syncRouteName({
12805
+ local,
12806
+ lastSynced,
12807
+ rows,
12808
+ machineId: i.machineId,
12809
+ rename: i.readOnly ? void 0 : i.rename
12810
+ });
12811
+ if (i.readOnly || result.status === "unsupported") return result;
12812
+ if (result.status === "refused") {
12813
+ 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 } });
12814
+ return result;
12815
+ }
12816
+ if (result.status !== "ok") return result;
12817
+ if (result.action === "adopt" || result.action === "row-wins") {
12818
+ const text = readFileSync20(i.configFile, "utf8");
12819
+ writeFileSync17(i.configFile, setShipNameInYaml(text, result.name));
12820
+ i.ship.name = result.name;
12821
+ if (result.action === "row-wins") {
12822
+ i.emit?.warn(
12823
+ `ship name changed in both crew.yaml ("${result.from}") and the Ships table ("${result.name}"); the Ships row wins`,
12824
+ { step: "poll", data: { event: "ship_renamed", route: i.route, from: result.from, to: result.name, source: "table", conflict: true } }
12825
+ );
12826
+ } else {
12827
+ i.emit?.emit(`ship renamed to "${result.name}" in the Ships table; crew.yaml updated`, {
12828
+ step: "poll",
12829
+ data: { event: "ship_renamed", route: i.route, from: result.from, to: result.name, source: "table" }
12830
+ });
12831
+ }
12832
+ } else if (result.action === "push") {
12833
+ i.emit?.emit(`ship name "${result.name}" pushed to the Ships table`, {
12834
+ step: "poll",
12835
+ data: { event: "ship_renamed", route: i.route, from: result.rowName, to: result.name, source: "crew.yaml" }
12836
+ });
12837
+ }
12838
+ writeSyncedName(resolvedFile, result.name);
12839
+ return result;
12840
+ }
12841
+ async function reconcileRouteShipName(o) {
12842
+ const fallback = { status: "unsupported", drift: "none", action: "none", name: o.ship.name };
12843
+ if (!o.route.resolved?.shipsModelId) return fallback;
12844
+ try {
12845
+ const machineId = o.machineId ?? ensureMachineId(o.ship.stateDir);
12846
+ const tracker = new Tracker(o.route, o.ship);
12847
+ return await reconcileShipName({
12848
+ route: o.route.route,
12849
+ configFile: o.configFile,
12850
+ stateDir: o.ship.stateDir,
12851
+ ship: o.ship,
12852
+ machineId,
12853
+ rows: () => tracker.shipRows(),
12854
+ rename: (id, name) => tracker.renameShipRow(id, name),
12855
+ emit: o.emit,
12856
+ readOnly: o.readOnly
12857
+ });
12858
+ } catch {
12859
+ return fallback;
12860
+ }
12861
+ }
12862
+
12639
12863
  // src/connect.ts
12640
12864
  var ConnectHttpError = class extends Error {
12641
12865
  status;
@@ -12941,10 +13165,16 @@ async function discover(o) {
12941
13165
  out.shipRow = { id: shipRowId, name: o.ship.name, action: "matched", matchedBy };
12942
13166
  const patchBody = {};
12943
13167
  if (matchedBy === "machine_id") {
12944
- const keptName = (mine[0].name ?? "").trim();
12945
- if (keptName !== o.ship.name.trim()) {
12946
- out.shipRow.name = keptName;
12947
- provisioning.push(`matched Ships row "${keptName}" by machine id \u2014 this host is now called "${o.ship.name}"; the row's name is kept`);
13168
+ const rowName = (mine[0].name ?? "").trim();
13169
+ const route = `${out.workspaceSlug}/${out.projectSlug ?? out.projectId}`;
13170
+ const plan = planNameSync(o.ship.name.trim(), rowName, o.ship.lastSyncedName?.(route));
13171
+ if (plan.action === "push") {
13172
+ patchBody.name = o.ship.name.trim();
13173
+ provisioning.push(`renamed Ships row "${rowName}" to "${o.ship.name.trim()}" (crew.yaml changed)`);
13174
+ } else if (plan.action === "adopt" || plan.action === "row-wins") {
13175
+ out.shipRow.name = rowName;
13176
+ out.shipNameAdopted = rowName;
13177
+ 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
13178
  }
12949
13179
  patchBody.last_seen = (/* @__PURE__ */ new Date()).toISOString();
12950
13180
  }
@@ -13494,8 +13724,8 @@ function describeCapability(i) {
13494
13724
  }
13495
13725
 
13496
13726
  // src/agents-sync-run.ts
13497
- import { existsSync as existsSync24, mkdirSync as mkdirSync14, readFileSync as readFileSync19, writeFileSync as writeFileSync16 } from "fs";
13498
- import { dirname as dirname13 } from "path";
13727
+ import { existsSync as existsSync26, mkdirSync as mkdirSync16, readFileSync as readFileSync21, writeFileSync as writeFileSync18 } from "fs";
13728
+ import { dirname as dirname15 } from "path";
13499
13729
  async function runAgentsSync(target, o) {
13500
13730
  const dryRun = o.dryRun ?? false;
13501
13731
  const force = o.force ?? false;
@@ -13528,12 +13758,12 @@ async function runAgentsSync(target, o) {
13528
13758
  for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
13529
13759
  if (!dryRun) {
13530
13760
  const resolvedPath = resolvedPathFor(o.stateDir, target.route);
13531
- const raw = existsSync24(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
13761
+ const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync21(resolvedPath, "utf8")) : {};
13532
13762
  raw.agentPersonas = personaResult.agentPersonas;
13533
13763
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
13534
13764
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
13535
- mkdirSync14(dirname13(resolvedPath), { recursive: true });
13536
- writeFileSync16(resolvedPath, `${JSON.stringify(raw, null, 2)}
13765
+ mkdirSync16(dirname15(resolvedPath), { recursive: true });
13766
+ writeFileSync18(resolvedPath, `${JSON.stringify(raw, null, 2)}
13537
13767
  `);
13538
13768
  if (target.resolved) {
13539
13769
  target.resolved.agentPersonas = personaResult.agentPersonas;
@@ -13548,7 +13778,7 @@ async function runAgentsSync(target, o) {
13548
13778
  }
13549
13779
 
13550
13780
  // src/logbook.ts
13551
- import { createHash as createHash3 } from "crypto";
13781
+ import { createHash as createHash4 } from "crypto";
13552
13782
  import { TablationClient as TablationClient4 } from "@tablation/client";
13553
13783
  var LogbookError = class extends Error {
13554
13784
  };
@@ -13621,7 +13851,7 @@ async function showLogEntry(route, entryId, opts = {}) {
13621
13851
  };
13622
13852
  }
13623
13853
  const prompt = appendAdditions(match.prompt ?? "", match.prompt_additions);
13624
- const actualSha = createHash3("sha256").update(prompt).digest("hex");
13854
+ const actualSha = createHash4("sha256").update(prompt).digest("hex");
13625
13855
  const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
13626
13856
  return {
13627
13857
  entry,
@@ -13636,12 +13866,12 @@ async function showLogEntry(route, entryId, opts = {}) {
13636
13866
  }
13637
13867
 
13638
13868
  // src/repos-hooks.ts
13639
- import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml";
13869
+ import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml";
13640
13870
 
13641
13871
  // src/repos-cmd.ts
13642
13872
  import { homedir as homedir8 } from "os";
13643
- import { basename as basename8, join as join25, resolve as resolve3 } from "path";
13644
- import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
13873
+ import { basename as basename8, join as join26, resolve as resolve3 } from "path";
13874
+ import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
13645
13875
  var ReposError = class extends Error {
13646
13876
  };
13647
13877
  function normalizeRemote(url) {
@@ -13665,7 +13895,7 @@ function matchRepoName(origin, resolved) {
13665
13895
  return void 0;
13666
13896
  }
13667
13897
  function routeNamesOf(text) {
13668
- const routes = parseDocument(text).get("routes", true);
13898
+ const routes = parseDocument2(text).get("routes", true);
13669
13899
  if (!isSeq(routes)) return [];
13670
13900
  return routes.items.flatMap((i) => isMap(i) && typeof i.get("route") === "string" ? [i.get("route")] : []);
13671
13901
  }
@@ -13700,7 +13930,7 @@ function routeMapOf(doc, route) {
13700
13930
  throw new ReposError(`no route "${route}" in the config (have: ${have})`);
13701
13931
  }
13702
13932
  function configuredRepos(text, route, base) {
13703
- const doc = parseDocument(text);
13933
+ const doc = parseDocument2(text);
13704
13934
  const m = routeMapOf(doc, route);
13705
13935
  const repos = m.get("repos");
13706
13936
  if (isMap(repos)) {
@@ -13715,7 +13945,7 @@ function configuredRepos(text, route, base) {
13715
13945
  return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
13716
13946
  }
13717
13947
  function routeSetsHooks(text, route, name) {
13718
- const m = routeMapOf(parseDocument(text), route);
13948
+ const m = routeMapOf(parseDocument2(text), route);
13719
13949
  const repos = m.get("repos", true);
13720
13950
  let target = m;
13721
13951
  if (isMap(repos)) target = repos.get(name, true);
@@ -13771,7 +14001,7 @@ function listRepoRows(i) {
13771
14001
  return rows;
13772
14002
  }
13773
14003
  function planRepoAdd(i) {
13774
- const doc = parseDocument(i.text);
14004
+ const doc = parseDocument2(i.text);
13775
14005
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
13776
14006
  const m = routeMapOf(doc, i.route);
13777
14007
  const existing = configuredRepos(i.text, i.route, i.base);
@@ -13808,7 +14038,7 @@ function planRepoAdd(i) {
13808
14038
  return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched };
13809
14039
  }
13810
14040
  function planRepoRemove(text, base, route, path) {
13811
- const doc = parseDocument(text);
14041
+ const doc = parseDocument2(text);
13812
14042
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
13813
14043
  const m = routeMapOf(doc, route);
13814
14044
  const target = expandDir(path, base);
@@ -13886,7 +14116,7 @@ function planRepoClone(i) {
13886
14116
  `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
14117
  );
13888
14118
  }
13889
- const doc = parseDocument(i.text);
14119
+ const doc = parseDocument2(i.text);
13890
14120
  const m = routeMapOf(doc, i.route);
13891
14121
  const existing = configuredRepos(i.text, i.route, i.base);
13892
14122
  if (existing.some((e) => e.name.toLowerCase() === name.toLowerCase())) {
@@ -13894,7 +14124,7 @@ function planRepoClone(i) {
13894
14124
  }
13895
14125
  const ship = doc.get("ship", true);
13896
14126
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
13897
- const target = i.dir ? expandDir(i.dir, i.base) : join25(expandDir(basePath, i.base), i.route.split("/")[0], name);
14127
+ const target = i.dir ? expandDir(i.dir, i.base) : join26(expandDir(basePath, i.base), i.route.split("/")[0], name);
13898
14128
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
13899
14129
  return { ...add, remote, target };
13900
14130
  }
@@ -14040,7 +14270,7 @@ function repoDirOf(text, base, route, name) {
14040
14270
  return hit.dir;
14041
14271
  }
14042
14272
  function planRouteHooks(text, route, name, hooks) {
14043
- const doc = parseDocument2(text);
14273
+ const doc = parseDocument3(text);
14044
14274
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
14045
14275
  const m = routeMapOf(doc, route);
14046
14276
  const repos = m.get("repos", true);
@@ -14102,7 +14332,7 @@ function existingRepoConfig(f) {
14102
14332
  }
14103
14333
 
14104
14334
  // src/repos-hooks-agent.ts
14105
- import { spawn as spawn4, execFileSync as execFileSync4 } from "child_process";
14335
+ import { spawn as spawn4, execFileSync as execFileSync5 } from "child_process";
14106
14336
  import { realpathSync as realpathSync2 } from "fs";
14107
14337
  import { isAbsolute as isAbsolute2, relative } from "path";
14108
14338
  var AgentSuggestError = class extends Error {
@@ -14205,7 +14435,7 @@ function classifyFailure(o) {
14205
14435
  }
14206
14436
  function porcelain(dir) {
14207
14437
  try {
14208
- return execFileSync4("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
14438
+ return execFileSync5("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
14209
14439
  } catch {
14210
14440
  return void 0;
14211
14441
  }
@@ -14274,7 +14504,7 @@ ${run2.err}` });
14274
14504
  }
14275
14505
 
14276
14506
  // src/sample-ticket.ts
14277
- import { basename as basename9, dirname as dirname15, join as join26 } from "path";
14507
+ import { basename as basename9, dirname as dirname17, join as join27 } from "path";
14278
14508
  var SAMPLE_REPO = "crew-sample";
14279
14509
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
14280
14510
  var SAMPLE_MARKER = "sample: true";
@@ -14297,8 +14527,8 @@ has something safe to work on: its test and build hooks always pass, it has no
14297
14527
  remote, and \`crew sample-ticket --remove\` deletes it.
14298
14528
  `;
14299
14529
  function sampleDir(existing, reposBase, workspace) {
14300
- if (existing.length > 0) return join26(dirname15(existing[0]), SAMPLE_REPO);
14301
- return join26(reposBase, workspace, SAMPLE_REPO);
14530
+ if (existing.length > 0) return join27(dirname17(existing[0]), SAMPLE_REPO);
14531
+ return join27(reposBase, workspace, SAMPLE_REPO);
14302
14532
  }
14303
14533
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
14304
14534
  function sampleDescription() {
@@ -14342,7 +14572,7 @@ function planSampleTicket(i) {
14342
14572
  }
14343
14573
 
14344
14574
  // src/doctor-fix.ts
14345
- import { parseDocument as parseDocument3 } from "yaml";
14575
+ import { parseDocument as parseDocument4 } from "yaml";
14346
14576
  var sound = (r) => r.hasRepos && r.problems.length === 0;
14347
14577
  function planFix(f) {
14348
14578
  const actions = [];
@@ -14369,7 +14599,7 @@ function routesInScope(routes, named) {
14369
14599
  return named ? routes.filter((r) => r.route === named) : routes;
14370
14600
  }
14371
14601
  function planEnable(text, route) {
14372
- const doc = parseDocument3(text);
14602
+ const doc = parseDocument4(text);
14373
14603
  routeMapOf(doc, route).set("enabled", true);
14374
14604
  return String(doc);
14375
14605
  }
@@ -14379,8 +14609,8 @@ function schedulerInstalled(plan, exists, crontab) {
14379
14609
  }
14380
14610
 
14381
14611
  // src/worktree-sweep.ts
14382
- import { existsSync as existsSync25, readFileSync as readFileSync20, readdirSync as readdirSync8 } from "fs";
14383
- import { basename as basename10, dirname as dirname16, join as join27, resolve as resolve4 } from "path";
14612
+ import { existsSync as existsSync27, readFileSync as readFileSync22, readdirSync as readdirSync8 } from "fs";
14613
+ import { basename as basename10, dirname as dirname18, join as join28, resolve as resolve4 } from "path";
14384
14614
  function planWorktreeSweep(target, tickets, contract) {
14385
14615
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
14386
14616
  const verified = contract.statuses.verified;
@@ -14434,7 +14664,7 @@ function worktreeTicketNumbers(target) {
14434
14664
  return [...nums];
14435
14665
  }
14436
14666
  function siblingWorktreeDirs(target) {
14437
- const parent = dirname16(target.dir.replace(/[/\\]+$/, ""));
14667
+ const parent = dirname18(target.dir.replace(/[/\\]+$/, ""));
14438
14668
  const stem = `${basename10(target.dir.replace(/[/\\]+$/, ""))}-`;
14439
14669
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
14440
14670
  let names;
@@ -14447,7 +14677,7 @@ function siblingWorktreeDirs(target) {
14447
14677
  for (const name of names) {
14448
14678
  if (!name.startsWith(stem)) continue;
14449
14679
  const m = DIR_NUMBER_SUFFIXED.exec(name);
14450
- const path = join27(parent, name);
14680
+ const path = join28(parent, name);
14451
14681
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
14452
14682
  }
14453
14683
  return out;
@@ -14482,7 +14712,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
14482
14712
  let removed = 0;
14483
14713
  let keptBranches = 0;
14484
14714
  for (const a of actions) {
14485
- if (!dryRun && base && !a.keepBranch && existsSync25(a.path) && isHandBackMerge(a.path, base)) {
14715
+ if (!dryRun && base && !a.keepBranch && existsSync27(a.path) && isHandBackMerge(a.path, base)) {
14486
14716
  if (gitOk(a.path, ["merge", "--abort"]) !== null) {
14487
14717
  log.emit(
14488
14718
  `abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
@@ -14490,7 +14720,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
14490
14720
  );
14491
14721
  }
14492
14722
  }
14493
- if (!dryRun && existsSync25(a.path)) {
14723
+ if (!dryRun && existsSync27(a.path)) {
14494
14724
  let dirty = false;
14495
14725
  try {
14496
14726
  dirty = status(a.path).length > 0;
@@ -14501,7 +14731,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
14501
14731
  continue;
14502
14732
  }
14503
14733
  }
14504
- if (!dryRun && a.requirePushed && a.branch && existsSync25(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
14734
+ if (!dryRun && a.requirePushed && a.branch && existsSync27(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
14505
14735
  refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
14506
14736
  continue;
14507
14737
  }
@@ -14550,7 +14780,7 @@ function isHandBackMerge(path, base) {
14550
14780
  if (!msgPath) return false;
14551
14781
  let first;
14552
14782
  try {
14553
- first = readFileSync20(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
14783
+ first = readFileSync22(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
14554
14784
  } catch {
14555
14785
  return false;
14556
14786
  }
@@ -14655,14 +14885,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
14655
14885
  }
14656
14886
 
14657
14887
  // src/stream-sweep.ts
14658
- import { existsSync as existsSync26, readdirSync as readdirSync9, rmSync as rmSync13, statSync as statSync8 } from "fs";
14659
- import { join as join28 } from "path";
14888
+ import { existsSync as existsSync28, readdirSync as readdirSync9, rmSync as rmSync13, statSync as statSync8 } from "fs";
14889
+ import { join as join29 } from "path";
14660
14890
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
14661
14891
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
14662
- const dir = join28(stateDir, "streams");
14663
- if (!existsSync26(dir)) return [];
14892
+ const dir = join29(stateDir, "streams");
14893
+ if (!existsSync28(dir)) return [];
14664
14894
  const cutoff = now - retentionDays * DAY_MS2;
14665
- return readdirSync9(dir).map((name) => join28(dir, name)).filter((path) => {
14895
+ return readdirSync9(dir).map((name) => join29(dir, name)).filter((path) => {
14666
14896
  try {
14667
14897
  return statSync8(path).mtimeMs < cutoff;
14668
14898
  } catch {
@@ -14688,9 +14918,9 @@ function applyStreamSweep(paths, dryRun, log) {
14688
14918
  return removed;
14689
14919
  }
14690
14920
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
14691
- if (!existsSync26(stateDir)) return [];
14921
+ if (!existsSync28(stateDir)) return [];
14692
14922
  const cutoff = now - retentionDays * DAY_MS2;
14693
- return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join28(stateDir, name)).filter((path) => {
14923
+ return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join29(stateDir, name)).filter((path) => {
14694
14924
  try {
14695
14925
  return statSync8(path).mtimeMs < cutoff;
14696
14926
  } catch {
@@ -14700,26 +14930,26 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
14700
14930
  }
14701
14931
 
14702
14932
  // src/cli.ts
14703
- import { realpathSync as realpathSync3, readFileSync as readFileSync27, existsSync as existsSync33, mkdirSync as mkdirSync20, writeFileSync as writeFileSync21, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync14 } from "fs";
14933
+ 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
14934
  import { createInterface } from "readline/promises";
14705
14935
  import { dirname as dirOf, resolve as resolvePath } from "path";
14706
14936
 
14707
14937
  // src/ssh-keys.ts
14708
- import { execFileSync as execFileSync5 } from "child_process";
14709
- import { existsSync as existsSync27, mkdirSync as mkdirSync15, readFileSync as readFileSync21, chmodSync } from "fs";
14710
- import { dirname as dirname17, join as join29 } from "path";
14938
+ import { execFileSync as execFileSync6 } from "child_process";
14939
+ import { existsSync as existsSync29, mkdirSync as mkdirSync17, readFileSync as readFileSync23, chmodSync } from "fs";
14940
+ import { dirname as dirname19, join as join30 } from "path";
14711
14941
  var SshKeyError = class extends Error {
14712
14942
  };
14713
14943
  function shipSshKeyPathFor(stateDir) {
14714
- return join29(stateDir, "ssh", "id_ed25519");
14944
+ return join30(stateDir, "ssh", "id_ed25519");
14715
14945
  }
14716
14946
  function ensureShipSshKeypair(stateDir) {
14717
14947
  const privateKeyPath = shipSshKeyPathFor(stateDir);
14718
14948
  const publicKeyPath = `${privateKeyPath}.pub`;
14719
- if (!existsSync27(privateKeyPath)) {
14720
- mkdirSync15(dirname17(privateKeyPath), { recursive: true });
14949
+ if (!existsSync29(privateKeyPath)) {
14950
+ mkdirSync17(dirname19(privateKeyPath), { recursive: true });
14721
14951
  try {
14722
- execFileSync5("ssh-keygen", [
14952
+ execFileSync6("ssh-keygen", [
14723
14953
  "-t",
14724
14954
  "ed25519",
14725
14955
  "-N",
@@ -14737,71 +14967,21 @@ function ensureShipSshKeypair(stateDir) {
14737
14967
  }
14738
14968
  chmodSync(privateKeyPath, 384);
14739
14969
  }
14740
- if (!existsSync27(publicKeyPath)) {
14970
+ if (!existsSync29(publicKeyPath)) {
14741
14971
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
14742
14972
  }
14743
- const publicKey = readFileSync21(publicKeyPath, "utf8").trim();
14973
+ const publicKey = readFileSync23(publicKeyPath, "utf8").trim();
14744
14974
  return { privateKeyPath, publicKeyPath, publicKey };
14745
14975
  }
14746
14976
  function sshKeygenAvailable() {
14747
14977
  try {
14748
- execFileSync5("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
14978
+ execFileSync6("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
14749
14979
  return true;
14750
14980
  } catch (e) {
14751
14981
  return e.code !== "ENOENT";
14752
14982
  }
14753
14983
  }
14754
14984
 
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
14985
  // src/passenger-containers.ts
14806
14986
  import { execFileSync as execFileSync7 } from "child_process";
14807
14987
  import { createHash as createHash5 } from "crypto";
@@ -15006,7 +15186,7 @@ function syncAllPassengerCheckouts(cfg) {
15006
15186
 
15007
15187
  // src/tunnel.ts
15008
15188
  import { spawn as spawn5 } from "child_process";
15009
- import { existsSync as existsSync29, mkdirSync as mkdirSync17, readFileSync as readFileSync23, writeFileSync as writeFileSync18, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
15189
+ import { existsSync as existsSync30, mkdirSync as mkdirSync18, readFileSync as readFileSync24, writeFileSync as writeFileSync19, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
15010
15190
  import { randomInt } from "crypto";
15011
15191
  import { join as join31 } from "path";
15012
15192
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
@@ -15079,17 +15259,17 @@ function tunnelStatePath(stateDir, workspaceId) {
15079
15259
  }
15080
15260
  function readPersistedTunnel(stateDir, workspaceId) {
15081
15261
  const path = tunnelStatePath(stateDir, workspaceId);
15082
- if (!existsSync29(path)) return void 0;
15262
+ if (!existsSync30(path)) return void 0;
15083
15263
  try {
15084
- return JSON.parse(readFileSync23(path, "utf8"));
15264
+ return JSON.parse(readFileSync24(path, "utf8"));
15085
15265
  } catch {
15086
15266
  return void 0;
15087
15267
  }
15088
15268
  }
15089
15269
  function writePersistedTunnel(stateDir, state) {
15090
15270
  const path = tunnelStatePath(stateDir, state.workspaceId);
15091
- mkdirSync17(join31(stateDir, "passengers"), { recursive: true });
15092
- writeFileSync18(path, `${JSON.stringify(state, null, 2)}
15271
+ mkdirSync18(join31(stateDir, "passengers"), { recursive: true });
15272
+ writeFileSync19(path, `${JSON.stringify(state, null, 2)}
15093
15273
  `);
15094
15274
  }
15095
15275
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -15187,7 +15367,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
15187
15367
  }
15188
15368
  function readAllPersistedWorkspaceIds(stateDir) {
15189
15369
  const dir = join31(stateDir, "passengers");
15190
- if (!existsSync29(dir)) return [];
15370
+ if (!existsSync30(dir)) return [];
15191
15371
  return readdirSync10(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
15192
15372
  }
15193
15373
 
@@ -15308,7 +15488,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
15308
15488
  import { createServer as createHttpServer } from "http";
15309
15489
  import { spawn as spawn6 } from "child_process";
15310
15490
  import { randomBytes } from "crypto";
15311
- import { existsSync as existsSync30, mkdirSync as mkdirSync18, readFileSync as readFileSync24, writeFileSync as writeFileSync19, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
15491
+ import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync25, writeFileSync as writeFileSync20, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
15312
15492
  import { join as join32 } from "path";
15313
15493
 
15314
15494
  // src/secret-compare.ts
@@ -15402,17 +15582,17 @@ function syncDaemonStatePath(stateDir, workspaceId) {
15402
15582
  }
15403
15583
  function readPersistedSyncDaemon(stateDir, workspaceId) {
15404
15584
  const path = syncDaemonStatePath(stateDir, workspaceId);
15405
- if (!existsSync30(path)) return void 0;
15585
+ if (!existsSync31(path)) return void 0;
15406
15586
  try {
15407
- return JSON.parse(readFileSync24(path, "utf8"));
15587
+ return JSON.parse(readFileSync25(path, "utf8"));
15408
15588
  } catch {
15409
15589
  return void 0;
15410
15590
  }
15411
15591
  }
15412
15592
  function writePersistedSyncDaemon(stateDir, state) {
15413
15593
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
15414
- mkdirSync18(join32(stateDir, "passengers"), { recursive: true });
15415
- writeFileSync19(path, `${JSON.stringify(state, null, 2)}
15594
+ mkdirSync19(join32(stateDir, "passengers"), { recursive: true });
15595
+ writeFileSync20(path, `${JSON.stringify(state, null, 2)}
15416
15596
  `);
15417
15597
  }
15418
15598
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -15433,7 +15613,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
15433
15613
  }
15434
15614
  function readAllPersistedSyncWorkspaceIds(stateDir) {
15435
15615
  const dir = join32(stateDir, "passengers");
15436
- if (!existsSync30(dir)) return [];
15616
+ if (!existsSync31(dir)) return [];
15437
15617
  const suffix = "-sync.json";
15438
15618
  return readdirSync11(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
15439
15619
  }
@@ -15521,11 +15701,11 @@ function describeCause(err) {
15521
15701
  }
15522
15702
 
15523
15703
  // src/daemon.ts
15524
- import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync26, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync20 } from "fs";
15704
+ 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
15705
  import { join as join34 } from "path";
15526
15706
 
15527
15707
  // src/interrupted.ts
15528
- import { existsSync as existsSync31, readdirSync as readdirSync12, readFileSync as readFileSync25 } from "fs";
15708
+ import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as readFileSync26 } from "fs";
15529
15709
  import { join as join33 } from "path";
15530
15710
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
15531
15711
  function interruptedComment(b) {
@@ -15545,16 +15725,16 @@ var pidAlive = (pid) => {
15545
15725
  };
15546
15726
  function runFinished(streamPath) {
15547
15727
  const path = `${streamPath}.meta.json`;
15548
- if (!existsSync31(path)) return false;
15728
+ if (!existsSync32(path)) return false;
15549
15729
  try {
15550
- return JSON.parse(readFileSync25(path, "utf8")).running !== true;
15730
+ return JSON.parse(readFileSync26(path, "utf8")).running !== true;
15551
15731
  } catch {
15552
15732
  return true;
15553
15733
  }
15554
15734
  }
15555
15735
  function orphanedStreams(o) {
15556
15736
  const dir = join33(o.stateDir, "streams");
15557
- if (!existsSync31(dir)) return [];
15737
+ if (!existsSync32(dir)) return [];
15558
15738
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
15559
15739
  const out = [];
15560
15740
  for (const name of readdirSync12(dir)) {
@@ -15688,7 +15868,7 @@ function computeTreeSignature(paths) {
15688
15868
  }
15689
15869
  function makeStaleChecker(paths, opts = {}) {
15690
15870
  const initial = computeTreeSignature(paths);
15691
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync32(opts.reloadSentinel);
15871
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync33(opts.reloadSentinel);
15692
15872
  }
15693
15873
  function reloadSentinelFor(stateDir) {
15694
15874
  return join34(stateDir, "reload-requested");
@@ -15697,14 +15877,14 @@ function daemonStampFor(stateDir) {
15697
15877
  return join34(stateDir, "daemon.started");
15698
15878
  }
15699
15879
  function noteDaemonStarted(stateDir, now = Date.now()) {
15700
- mkdirSync19(stateDir, { recursive: true });
15880
+ mkdirSync20(stateDir, { recursive: true });
15701
15881
  rmSync14(reloadSentinelFor(stateDir), { force: true });
15702
- writeFileSync20(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
15882
+ writeFileSync21(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
15703
15883
  `);
15704
15884
  }
15705
15885
  function readDaemonStarted(stateDir) {
15706
15886
  try {
15707
- const v = JSON.parse(readFileSync26(daemonStampFor(stateDir), "utf8")).startedAt;
15887
+ const v = JSON.parse(readFileSync27(daemonStampFor(stateDir), "utf8")).startedAt;
15708
15888
  return typeof v === "number" ? v : null;
15709
15889
  } catch {
15710
15890
  return null;
@@ -15767,6 +15947,7 @@ async function runOnePassUnguarded(o) {
15767
15947
  const spawn7 = o.spawnRoleAgent ?? runRoleAgent;
15768
15948
  const breaker = o.spawnBreaker ?? new SpawnBreaker();
15769
15949
  const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
15950
+ if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
15770
15951
  try {
15771
15952
  await (o.reconcile ?? reconcileForRoute)(ctx, o);
15772
15953
  } catch (e) {
@@ -16089,7 +16270,7 @@ async function buildEnvironment(route, ship, ticket) {
16089
16270
  }
16090
16271
 
16091
16272
  // src/cli.ts
16092
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname19(process.execPath) : resolve5(dirname19(fileURLToPath(import.meta.url)), "..");
16273
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname20(process.execPath) : resolve5(dirname20(fileURLToPath(import.meta.url)), "..");
16093
16274
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
16094
16275
  var CYCLE_SECONDS2 = 120;
16095
16276
  var STALE_CYCLES = 3;
@@ -16245,6 +16426,7 @@ function usage(problem, requested = false) {
16245
16426
  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
16427
  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
16428
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
16429
+ crew ship rename NAME [--json] rename this ship in crew.yaml and on every workspace's Ships row
16248
16430
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
16249
16431
  crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
16250
16432
  crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--agent, --write route|repo, --recipes, --json)
@@ -16333,6 +16515,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
16333
16515
  "run",
16334
16516
  "sample-ticket",
16335
16517
  "selftest",
16518
+ "ship",
16336
16519
  "skills",
16337
16520
  "status",
16338
16521
  "sync",
@@ -16482,9 +16665,9 @@ if (command === "passenger-sync-daemon") {
16482
16665
  firstRun = { answers, configPath };
16483
16666
  cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
16484
16667
  } else {
16485
- const toleratesNoRoutes = command === "repos" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
16668
+ const toleratesNoRoutes = command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
16486
16669
  const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
16487
- if (configless && !existsSync33(findConfigFile(CREW_HOME))) {
16670
+ if (configless && !existsSync34(findConfigFile(CREW_HOME))) {
16488
16671
  cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
16489
16672
  } else {
16490
16673
  cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
@@ -16506,7 +16689,7 @@ if (command === "passenger-sync-daemon") {
16506
16689
  const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
16507
16690
  const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
16508
16691
  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;
16692
+ 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
16693
  let route;
16511
16694
  try {
16512
16695
  route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
@@ -16723,7 +16906,7 @@ if (command === "passenger-sync-daemon") {
16723
16906
  if (host === "linux" && hasSystemd) {
16724
16907
  const label = labelFor(CREW_HOME, "run");
16725
16908
  const staleTimer = join35(homedir9(), ".config", "systemd", "user", `${label}.timer`);
16726
- if (existsSync33(staleTimer)) {
16909
+ if (existsSync34(staleTimer)) {
16727
16910
  process.stdout.write(
16728
16911
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
16729
16912
  `
@@ -16749,7 +16932,7 @@ if (command === "passenger-sync-daemon") {
16749
16932
  const tracker = new Tracker(c, cfg.ship);
16750
16933
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
16751
16934
  for (const r of await resolvedRepos(c)) {
16752
- if (!existsSync33(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
16935
+ if (!existsSync34(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
16753
16936
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
16754
16937
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
16755
16938
  }
@@ -16767,7 +16950,7 @@ if (command === "passenger-sync-daemon") {
16767
16950
  }
16768
16951
  async function doctorFix(o) {
16769
16952
  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) => existsSync33(p);
16953
+ const unitExists = (p) => existsSync34(p);
16771
16954
  const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
16772
16955
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
16773
16956
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -16784,7 +16967,7 @@ if (command === "passenger-sync-daemon") {
16784
16967
  for (const a of plan.actions) {
16785
16968
  if (a.kind === "enable") {
16786
16969
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
16787
- writeFileSync21(cfg.configFile, planEnable(readFileSync27(cfg.configFile, "utf8"), a.route));
16970
+ writeFileSync22(cfg.configFile, planEnable(readFileSync28(cfg.configFile, "utf8"), a.route));
16788
16971
  enabledSound.add(a.route);
16789
16972
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
16790
16973
  } else {
@@ -17267,6 +17450,11 @@ ${changed2.map((c) => ` - ${c}
17267
17450
  case "poll":
17268
17451
  case "run": {
17269
17452
  const skipInlineRelease = flag("no-release");
17453
+ if (!dryRun) {
17454
+ for (const r of fleetWide ? cfg.routes : [route]) {
17455
+ await reconcileRouteShipName({ route: r, ship: cfg.ship, configFile: cfg.configFile, emit: emit.forRoute(r.route) });
17456
+ }
17457
+ }
17270
17458
  if (fleetWide) {
17271
17459
  const fleet = await decideFleet({
17272
17460
  routes: cfg.routes,
@@ -17800,6 +17988,7 @@ ${changed2.map((c) => ` - ${c}
17800
17988
  state,
17801
17989
  maxConcurrentAgents: cfg.ship.maxConcurrentAgents,
17802
17990
  spawnBreaker,
17991
+ configFile: cfg.configFile,
17803
17992
  newEmitter: () => new Emitter({
17804
17993
  route: route.route,
17805
17994
  eventFile: eventFileFor(cfg.ship.stateDir),
@@ -18006,15 +18195,72 @@ ${changed2.map((c) => ` - ${c}
18006
18195
  `);
18007
18196
  if (!dryRun) {
18008
18197
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
18009
- const raw = existsSync33(resolvedPath) ? JSON.parse(readFileSync27(resolvedPath, "utf8")) : {};
18198
+ const raw = existsSync34(resolvedPath) ? JSON.parse(readFileSync28(resolvedPath, "utf8")) : {};
18010
18199
  raw.agentSkills = result.agentSkills;
18011
- mkdirSync20(dirOf(resolvedPath), { recursive: true });
18012
- writeFileSync21(resolvedPath, `${JSON.stringify(raw, null, 2)}
18200
+ mkdirSync21(dirOf(resolvedPath), { recursive: true });
18201
+ writeFileSync22(resolvedPath, `${JSON.stringify(raw, null, 2)}
18013
18202
  `);
18014
18203
  }
18015
18204
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
18016
18205
  break;
18017
18206
  }
18207
+ case "ship": {
18208
+ if (positional[1] !== "rename" || positional.length !== 3) {
18209
+ process.stderr.write("crew ship rename NAME [--json]\n");
18210
+ process.exit(2);
18211
+ }
18212
+ let newName;
18213
+ try {
18214
+ newName = validateShipName(positional[2]);
18215
+ } catch (e) {
18216
+ if (e instanceof ShipNameError) {
18217
+ process.stderr.write(`crew ship: ${e.message}
18218
+ `);
18219
+ process.exit(2);
18220
+ }
18221
+ throw e;
18222
+ }
18223
+ const oldName = cfg.ship.name;
18224
+ writeFileSync22(cfg.configFile, setShipNameInYaml(readFileSync28(cfg.configFile, "utf8"), newName));
18225
+ cfg.ship.name = newName;
18226
+ let renameMachineId;
18227
+ try {
18228
+ renameMachineId = ensureMachineId(cfg.ship.stateDir);
18229
+ } catch {
18230
+ }
18231
+ const outcomes = [];
18232
+ for (const r of cfg.routes) {
18233
+ if (!r.resolved?.shipsModelId) {
18234
+ outcomes.push({ route: r.route, status: "no-ships-table" });
18235
+ continue;
18236
+ }
18237
+ const resolvedFile = resolvedPathFor(cfg.ship.stateDir, r.route);
18238
+ const tracker = new Tracker(r, cfg.ship);
18239
+ const picked = pickShipRow(await tracker.shipRows(), renameMachineId, [oldName, readSyncedName(resolvedFile) ?? ""]);
18240
+ if (picked.kind !== "row") {
18241
+ outcomes.push({ route: r.route, status: picked.kind === "unsupported" ? "unsupported" : picked.kind === "none" ? "no-row" : "ambiguous" });
18242
+ continue;
18243
+ }
18244
+ try {
18245
+ await tracker.renameShipRow(picked.row.id, newName);
18246
+ writeSyncedName(resolvedFile, newName);
18247
+ outcomes.push({ route: r.route, status: "renamed", rowId: picked.row.id });
18248
+ } catch (e) {
18249
+ outcomes.push({ route: r.route, status: "refused", rowId: picked.row.id, error: e.message });
18250
+ }
18251
+ }
18252
+ if (flag("json")) {
18253
+ process.stdout.write(`${JSON.stringify({ name: newName, previous: oldName, configFile: cfg.configFile, routes: outcomes })}
18254
+ `);
18255
+ } else {
18256
+ process.stdout.write(`ship renamed "${oldName}" -> "${newName}" in ${cfg.configFile}
18257
+ `);
18258
+ for (const o of outcomes) process.stdout.write(` ${o.route}: ${o.status}${o.error ? ` (${o.error})` : ""}
18259
+ `);
18260
+ }
18261
+ if (outcomes.some((o) => o.status === "refused")) process.exitCode = 1;
18262
+ break;
18263
+ }
18018
18264
  case "repos": {
18019
18265
  const sub = positional[1];
18020
18266
  const usage2 = () => {
@@ -18022,13 +18268,13 @@ ${changed2.map((c) => ` - ${c}
18022
18268
  process.exit(2);
18023
18269
  };
18024
18270
  if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
18025
- const configText = readFileSync27(cfg.configFile, "utf8");
18271
+ const configText = readFileSync28(cfg.configFile, "utf8");
18026
18272
  if (sub === "hooks") {
18027
18273
  try {
18028
18274
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
18029
- const dir = repoDirOf(configText, dirname19(cfg.configFile), a.route, a.name);
18030
- if (!existsSync33(dir)) throw new ReposError(`${dir} does not exist on disk`);
18031
- const files = { exists: (r) => existsSync33(resolvePath(dir, r)), list: (r) => {
18275
+ const dir = repoDirOf(configText, dirname20(cfg.configFile), a.route, a.name);
18276
+ if (!existsSync34(dir)) throw new ReposError(`${dir} does not exist on disk`);
18277
+ const files = { exists: (r) => existsSync34(resolvePath(dir, r)), list: (r) => {
18032
18278
  try {
18033
18279
  return readdirSync14(resolvePath(dir, r));
18034
18280
  } catch {
@@ -18036,7 +18282,7 @@ ${changed2.map((c) => ` - ${c}
18036
18282
  }
18037
18283
  }, read: (r) => {
18038
18284
  try {
18039
- return readFileSync27(resolvePath(dir, r), "utf8");
18285
+ return readFileSync28(resolvePath(dir, r), "utf8");
18040
18286
  } catch {
18041
18287
  return void 0;
18042
18288
  }
@@ -18074,13 +18320,13 @@ ${changed2.map((c) => ` - ${c}
18074
18320
  if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
18075
18321
  if (write === "route") {
18076
18322
  const plan = planRouteHooks(configText, a.route, a.name, picked.hooks);
18077
- writeFileSync21(cfg.configFile, plan.text);
18323
+ writeFileSync22(cfg.configFile, plan.text);
18078
18324
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
18079
18325
  } else if (write === "repo") {
18080
18326
  const have = existingRepoConfig(files);
18081
18327
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
18082
18328
  const file = resolvePath(dir, ".crew.yaml");
18083
- writeFileSync21(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
18329
+ writeFileSync22(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
18084
18330
  written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [] };
18085
18331
  }
18086
18332
  if (flag("json")) {
@@ -18132,7 +18378,7 @@ ${changed2.map((c) => ` - ${c}
18132
18378
  if (sub === "list" && !routeName) usage2();
18133
18379
  if (sub === "add") {
18134
18380
  try {
18135
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync33(resolvePath(p.replace(/^~(?=\/|$)/, homedir9()))));
18381
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync34(resolvePath(p.replace(/^~(?=\/|$)/, homedir9()))));
18136
18382
  routeName = a.route;
18137
18383
  addPath = a.path;
18138
18384
  addName = a.name;
@@ -18161,15 +18407,15 @@ ${changed2.map((c) => ` - ${c}
18161
18407
  }
18162
18408
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
18163
18409
  let resolvedRemotes;
18164
- if (existsSync33(resolvedFile)) {
18410
+ if (existsSync34(resolvedFile)) {
18165
18411
  try {
18166
- resolvedRemotes = JSON.parse(readFileSync27(resolvedFile, "utf8"));
18412
+ resolvedRemotes = JSON.parse(readFileSync28(resolvedFile, "utf8"));
18167
18413
  } catch {
18168
18414
  }
18169
18415
  }
18170
18416
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
18171
18417
  const text = configText;
18172
- const base = dirname19(cfg.configFile);
18418
+ const base = dirname20(cfg.configFile);
18173
18419
  try {
18174
18420
  if (sub === "list") {
18175
18421
  if (flag("json")) {
@@ -18179,10 +18425,10 @@ ${changed2.map((c) => ` - ${c}
18179
18425
  base,
18180
18426
  resolved: resolvedRemotes,
18181
18427
  originOf,
18182
- exists: (d) => existsSync33(d),
18428
+ exists: (d) => existsSync34(d),
18183
18429
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
18184
18430
  try {
18185
- return Object.keys(parseRepoConfig(readFileSync27(resolvePath(d, f), "utf8"), f).hooks).length > 0;
18431
+ return Object.keys(parseRepoConfig(readFileSync28(resolvePath(d, f), "utf8"), f).hooks).length > 0;
18186
18432
  } catch {
18187
18433
  return false;
18188
18434
  }
@@ -18198,7 +18444,7 @@ ${changed2.map((c) => ` - ${c}
18198
18444
  for (const r of repos) {
18199
18445
  const origin2 = originOf(r.dir);
18200
18446
  const row = matchRepoName(origin2, resolvedRemotes);
18201
- const note = !existsSync33(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
18447
+ const note = !existsSync34(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
18202
18448
  process.stdout.write(`${r.name} ${r.dir} ${note}
18203
18449
  `);
18204
18450
  }
@@ -18210,9 +18456,9 @@ ${changed2.map((c) => ` - ${c}
18210
18456
  ` : `${human}
18211
18457
  `);
18212
18458
  const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
18213
- if (existsSync33(plan2.target) && (existsSync33(`${plan2.target}/.git`) || readdirSync14(plan2.target).length > 0)) {
18459
+ if (existsSync34(plan2.target) && (existsSync34(`${plan2.target}/.git`) || readdirSync14(plan2.target).length > 0)) {
18214
18460
  throw new ReposError(
18215
- existsSync33(`${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`
18461
+ 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
18462
  );
18217
18463
  }
18218
18464
  if (dryRun) {
@@ -18230,12 +18476,12 @@ ${changed2.map((c) => ` - ${c}
18230
18476
  throw new ReposError(`${e.message}${authHint}
18231
18477
  crew.yaml was not changed`);
18232
18478
  }
18233
- writeFileSync21(cfg.configFile, plan2.text);
18479
+ writeFileSync22(cfg.configFile, plan2.text);
18234
18480
  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
18481
  break;
18236
18482
  }
18237
18483
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir9()));
18238
- const top = existsSync33(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
18484
+ const top = existsSync34(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
18239
18485
  if (!top) throw new ReposError(`${target} is not a git checkout`);
18240
18486
  const origin = originOf(top);
18241
18487
  const plan = planRepoAdd({
@@ -18284,15 +18530,15 @@ ${changed2.map((c) => ` - ${c}
18284
18530
  const created = await rowTracker.createRepoRow(rowPayload);
18285
18531
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
18286
18532
  `);
18287
- if (existsSync33(resolvedFile)) {
18288
- const raw = JSON.parse(readFileSync27(resolvedFile, "utf8"));
18533
+ if (existsSync34(resolvedFile)) {
18534
+ const raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
18289
18535
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
18290
18536
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
18291
- writeFileSync21(resolvedFile, `${JSON.stringify(raw, null, 2)}
18537
+ writeFileSync22(resolvedFile, `${JSON.stringify(raw, null, 2)}
18292
18538
  `);
18293
18539
  }
18294
18540
  }
18295
- writeFileSync21(cfg.configFile, plan.text);
18541
+ writeFileSync22(cfg.configFile, plan.text);
18296
18542
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
18297
18543
  `);
18298
18544
  } catch (e) {
@@ -18447,8 +18693,8 @@ ${reconstruction.prompt}
18447
18693
  }
18448
18694
  }
18449
18695
  let signInSource = apiKey ? "key" : "device";
18450
- if (!apiKey && !workspace && dryRun && store) {
18451
- const resumed = await resumeKeychainSession({
18696
+ if (!apiKey && !workspace && store) {
18697
+ const resumable = await resumableSessions({
18452
18698
  store,
18453
18699
  stateDir: cfg.ship.stateDir,
18454
18700
  host: hostFromUrl4(baseUrl),
@@ -18461,6 +18707,14 @@ ${reconstruction.prompt}
18461
18707
  }
18462
18708
  }
18463
18709
  });
18710
+ if (resumable.length > 1 && connectEvents.json) {
18711
+ stopForChoice(
18712
+ "workspace",
18713
+ `${resumable.length} workspaces have a remembered sign-in on ${hostFromUrl4(baseUrl)} \u2014 which one?`,
18714
+ resumable.map((r) => ({ value: r.workspace, label: r.workspace }))
18715
+ );
18716
+ }
18717
+ const resumed = resumable.length === 1 ? resumable[0] : void 0;
18464
18718
  if (resumed) {
18465
18719
  apiKey = resumed.apiKey;
18466
18720
  workspace = resumed.workspace;
@@ -18576,7 +18830,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18576
18830
  platform: cfg.ship.platform,
18577
18831
  machineId,
18578
18832
  hostPassengers: firstRun?.answers.hostPassengers ?? existingRoute?.hostPassengers === true,
18579
- sshPublicKey
18833
+ sshPublicKey,
18834
+ lastSyncedName: (r) => readSyncedName(resolvedPathFor(cfg.ship.stateDir, r))
18580
18835
  };
18581
18836
  const runConnect = async (ws, proj) => {
18582
18837
  let found = await discover({ ...authOpts, workspace: ws, project: proj, area: value("area"), ship, dryRun, interactive: canAsk || connectEvents.json });
@@ -18633,6 +18888,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18633
18888
  }
18634
18889
  process.exit(2);
18635
18890
  }
18891
+ if (found.shipNameAdopted && !dryRun) {
18892
+ writeFileSync22(cfg.configFile, setShipNameInYaml(readFileSync28(cfg.configFile, "utf8"), found.shipNameAdopted));
18893
+ cfg.ship.name = found.shipNameAdopted;
18894
+ }
18636
18895
  if (found.shipRow) {
18637
18896
  connectEvents.emit("ship_row", `Ships row "${found.shipRow.name}" ${found.shipRow.action}`, found.shipRow);
18638
18897
  }
@@ -18673,10 +18932,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18673
18932
  }
18674
18933
  }
18675
18934
  let previous;
18676
- const hadPreviousFile = existsSync33(resolvedPath);
18935
+ const hadPreviousFile = existsSync34(resolvedPath);
18677
18936
  if (hadPreviousFile) {
18678
18937
  try {
18679
- previous = JSON.parse(readFileSync27(resolvedPath, "utf8"));
18938
+ previous = JSON.parse(readFileSync28(resolvedPath, "utf8"));
18680
18939
  } catch {
18681
18940
  }
18682
18941
  }
@@ -18788,18 +19047,20 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18788
19047
  // nothing about its prompt actually changed.
18789
19048
  ...previous?.agentPersonas ? { agentPersonas: previous.agentPersonas } : {},
18790
19049
  ...previous?.agentSkills ? { agentSkills: previous.agentSkills } : {},
18791
- ...previous?.releaseAgentId ? { releaseAgentId: previous.releaseAgentId } : {}
19050
+ ...previous?.releaseAgentId ? { releaseAgentId: previous.releaseAgentId } : {},
19051
+ // CREW-2495: the ship name this route last saw agree with its Ships row.
19052
+ ...found.shipRow ? { shipName: found.shipRow.name } : previous?.shipName ? { shipName: previous.shipName } : {}
18792
19053
  };
18793
19054
  if (dryRun) {
18794
19055
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
18795
19056
  `);
18796
19057
  } else {
18797
- mkdirSync20(dirname19(resolvedPath), { recursive: true });
19058
+ mkdirSync21(dirname20(resolvedPath), { recursive: true });
18798
19059
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
18799
19060
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
18800
- writeFileSync21(tmpPath, `${JSON.stringify(resolved, null, 2)}
19061
+ writeFileSync22(tmpPath, `${JSON.stringify(resolved, null, 2)}
18801
19062
  `);
18802
- renameSync2(tmpPath, resolvedPath);
19063
+ renameSync3(tmpPath, resolvedPath);
18803
19064
  process.stderr.write(`Resolved ids written to ${resolvedPath}
18804
19065
  `);
18805
19066
  }
@@ -18839,8 +19100,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18839
19100
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
18840
19101
  `);
18841
19102
  } else {
18842
- mkdirSync20(dirname19(apiKeyFile), { recursive: true });
18843
- writeFileSync21(apiKeyFile, `${apiKeyVar}=${apiKey}
19103
+ mkdirSync21(dirname20(apiKeyFile), { recursive: true });
19104
+ writeFileSync22(apiKeyFile, `${apiKeyVar}=${apiKey}
18844
19105
  `, { mode: 384 });
18845
19106
  process.stderr.write(`API key written to ${apiKeyFile}
18846
19107
  `);
@@ -19528,7 +19789,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19528
19789
  };
19529
19790
  const droppedRoutes = [];
19530
19791
  try {
19531
- const rawCfg = parseYaml2(readFileSync27(cfg.configFile, "utf8"));
19792
+ const rawCfg = parseYaml2(readFileSync28(cfg.configFile, "utf8"));
19532
19793
  for (const r of rawCfg?.routes ?? []) {
19533
19794
  if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
19534
19795
  try {
@@ -19570,8 +19831,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19570
19831
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
19571
19832
  break;
19572
19833
  }
19573
- mkdirSync20(cfg.ship.stateDir, { recursive: true });
19574
- writeFileSync21(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
19834
+ mkdirSync21(cfg.ship.stateDir, { recursive: true });
19835
+ writeFileSync22(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
19575
19836
  `);
19576
19837
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
19577
19838
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -19622,7 +19883,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19622
19883
  }
19623
19884
  case "log": {
19624
19885
  try {
19625
- const text = readFileSync27(cfg.ship.logFile, "utf8").split("\n");
19886
+ const text = readFileSync28(cfg.ship.logFile, "utf8").split("\n");
19626
19887
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
19627
19888
  `);
19628
19889
  } catch {
@@ -19645,8 +19906,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19645
19906
  };
19646
19907
  if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
19647
19908
  const tracker = new Tracker(route, cfg.ship);
19648
- const configText = readFileSync27(cfg.configFile, "utf8");
19649
- const cfgBase = dirname19(cfg.configFile);
19909
+ const configText = readFileSync28(cfg.configFile, "utf8");
19910
+ const cfgBase = dirname20(cfg.configFile);
19650
19911
  const configured = configuredRepos(configText, route.route, cfgBase);
19651
19912
  const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
19652
19913
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
@@ -19674,7 +19935,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19674
19935
  }
19675
19936
  if (configured.some((r) => r.dir === dir)) {
19676
19937
  try {
19677
- writeFileSync21(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
19938
+ writeFileSync22(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
19678
19939
  } catch (e) {
19679
19940
  if (e instanceof ReposError) fail(e.message);
19680
19941
  throw e;
@@ -19682,29 +19943,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19682
19943
  }
19683
19944
  if (row) {
19684
19945
  await tracker.deleteRepoRow(row.id);
19685
- if (existsSync33(resolvedFile)) {
19686
- const raw = JSON.parse(readFileSync27(resolvedFile, "utf8"));
19946
+ if (existsSync34(resolvedFile)) {
19947
+ const raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
19687
19948
  if (raw.repoNames) delete raw.repoNames[row.id];
19688
19949
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
19689
- writeFileSync21(resolvedFile, `${JSON.stringify(raw, null, 2)}
19950
+ writeFileSync22(resolvedFile, `${JSON.stringify(raw, null, 2)}
19690
19951
  `);
19691
19952
  }
19692
19953
  }
19693
- if (existsSync33(join35(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
19954
+ if (existsSync34(join35(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
19694
19955
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
19695
19956
  break;
19696
19957
  }
19697
19958
  const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
19698
- const fresh = !existsSync33(join35(dir, ".git"));
19699
- if (existsSync33(dir) && fresh && readdirSync14(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
19959
+ const fresh = !existsSync34(join35(dir, ".git"));
19960
+ if (existsSync34(dir) && fresh && readdirSync14(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
19700
19961
  if (dryRun) {
19701
19962
  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
19963
  break;
19703
19964
  }
19704
19965
  if (fresh) {
19705
- mkdirSync20(dir, { recursive: true });
19706
- writeFileSync21(join35(dir, "README.md"), SAMPLE_README);
19707
- writeFileSync21(join35(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
19966
+ mkdirSync21(dir, { recursive: true });
19967
+ writeFileSync22(join35(dir, "README.md"), SAMPLE_README);
19968
+ writeFileSync22(join35(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
19708
19969
  const g = (...a) => {
19709
19970
  const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
19710
19971
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -19722,17 +19983,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19722
19983
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
19723
19984
  });
19724
19985
  repoId = (await tracker.createRepoRow(payload)).id;
19725
- if (existsSync33(resolvedFile)) {
19726
- const raw = JSON.parse(readFileSync27(resolvedFile, "utf8"));
19986
+ if (existsSync34(resolvedFile)) {
19987
+ const raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
19727
19988
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
19728
19989
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
19729
- writeFileSync21(resolvedFile, `${JSON.stringify(raw, null, 2)}
19990
+ writeFileSync22(resolvedFile, `${JSON.stringify(raw, null, 2)}
19730
19991
  `);
19731
19992
  }
19732
19993
  }
19733
19994
  if (!configured.some((r) => r.dir === dir)) {
19734
19995
  try {
19735
- writeFileSync21(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
19996
+ writeFileSync22(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
19736
19997
  } catch (e) {
19737
19998
  if (e instanceof ReposError) fail(e.message);
19738
19999
  throw e;
@@ -19853,6 +20114,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19853
20114
  const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
19854
20115
  const crewMinVersion = await readCrewFloor(route, cfg.ship) ?? null;
19855
20116
  if (flag("json")) {
20117
+ const nameSync = await reconcileRouteShipName({ route, ship: cfg.ship, configFile: cfg.configFile, readOnly: true });
19856
20118
  const repos = await resolvedRepos(route);
19857
20119
  const seatIds = route.resolved?.seats ?? {};
19858
20120
  const crewRows = Object.keys(seatIds).length ? await statusTracker.crewRows() : [];
@@ -19896,7 +20158,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19896
20158
  eventFile: eventFileFor(cfg.ship.stateDir),
19897
20159
  // Additive (CREW-1373): open ship-level items, including the
19898
20160
  // derived `release_stale`. `version` stays 1.
19899
- attention: openShipAttention(state)
20161
+ attention: openShipAttention(state),
20162
+ // Additive (CREW-2495): this ship's Ships-row name and whether it has drifted from `name`.
20163
+ rowName: nameSync.rowName ?? null,
20164
+ nameDrift: nameSync.drift
19900
20165
  },
19901
20166
  // Additive (CREW-1402): what the operator owes. `version` stays 1.
19902
20167
  todo: todo.map((i) => ({ ticket: displayKey(i.ticket), title: i.ticket.title ?? null, reasons: i.reasons })),
@@ -20043,7 +20308,7 @@ watermark: ${state.watermark()}
20043
20308
  }
20044
20309
  for (const r of repos) {
20045
20310
  const remote = r.config.branch.remote;
20046
- if (existsSync33(r.dir)) {
20311
+ if (existsSync34(r.dir)) {
20047
20312
  pushAuth.push({
20048
20313
  route: c.route,
20049
20314
  repo: r.name,
@@ -20071,7 +20336,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
20071
20336
  );
20072
20337
  try {
20073
20338
  const evFile = eventFileFor(cfg.ship.stateDir);
20074
- const evSize = existsSync33(evFile) ? statSync10(evFile).size : 0;
20339
+ const evSize = existsSync34(evFile) ? statSync10(evFile).size : 0;
20075
20340
  process.stdout.write(
20076
20341
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
20077
20342
  ` : "events log: ok\n"
@@ -20082,7 +20347,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
20082
20347
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
20083
20348
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
20084
20349
  try {
20085
- return readFileSync27(p, "utf8");
20350
+ return readFileSync28(p, "utf8");
20086
20351
  } catch {
20087
20352
  return null;
20088
20353
  }