@tablation/crew 0.62.0 → 0.63.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 +219 -121
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5183,7 +5183,7 @@ function doctorSelftestLines(stored, toolchains) {
5183
5183
  }
5184
5184
 
5185
5185
  // src/cli.ts
5186
- import { resolve as resolve5, dirname as dirname17, join as join30, basename as basename10 } from "path";
5186
+ import { resolve as resolve5, dirname as dirname18, join as join31, basename as basename10 } from "path";
5187
5187
  import { fileURLToPath } from "url";
5188
5188
  import { hostname as hostname2, homedir as homedir8 } from "os";
5189
5189
  import { spawnSync as spawnSync6 } from "child_process";
@@ -12235,24 +12235,49 @@ async function discover(o) {
12235
12235
  o,
12236
12236
  `/data-models/${out.shipsModelId}/records?limit=200`
12237
12237
  );
12238
- const mine = shipRows.filter((r) => (r.name ?? "").trim() === o.ship.name.trim());
12239
12238
  const hostPassengers = o.ship.hostPassengers ?? false;
12240
12239
  const sshPublicKey = o.ship.sshPublicKey;
12240
+ let shipColumns = /* @__PURE__ */ new Set();
12241
+ try {
12242
+ const shipsModel = await get(o, `/data-models/${out.shipsModelId}`);
12243
+ shipColumns = new Set((shipsModel.fields ?? []).map((f) => f.columnName));
12244
+ } catch {
12245
+ }
12241
12246
  let ownerId;
12242
12247
  if (out.meId) {
12243
- let hasOwnerColumn = false;
12244
- try {
12245
- const shipsModel = await get(o, `/data-models/${out.shipsModelId}`);
12246
- hasOwnerColumn = (shipsModel.fields ?? []).some((f) => f.columnName === "owner_id");
12247
- } catch {
12248
- }
12249
- if (hasOwnerColumn) ownerId = out.meId;
12248
+ if (shipColumns.has("owner_id")) ownerId = out.meId;
12250
12249
  else provisioning.push("could not record the ship owner \u2014 the Ships table has no owner_id column yet (update the Issues template)");
12251
12250
  }
12251
+ const machineId = shipColumns.has("machine_id") ? o.ship.machineId : void 0;
12252
+ let mine;
12253
+ let matchedBy = "name";
12254
+ let writeMachineId = false;
12255
+ const byName = shipRows.filter((r) => (r.name ?? "").trim() === o.ship.name.trim());
12256
+ if (machineId) {
12257
+ const byId = shipRows.filter((r) => (r.machine_id ?? "") === machineId);
12258
+ if (byId.length > 0) {
12259
+ mine = byId;
12260
+ matchedBy = "machine_id";
12261
+ } else {
12262
+ mine = byName.filter((r) => !r.machine_id);
12263
+ writeMachineId = true;
12264
+ }
12265
+ } else {
12266
+ mine = byName;
12267
+ }
12252
12268
  if (mine.length === 1) {
12253
12269
  shipRowId = mine[0].id;
12254
- out.shipRow = { id: shipRowId, name: o.ship.name, action: "matched" };
12270
+ out.shipRow = { id: shipRowId, name: o.ship.name, action: "matched", matchedBy };
12255
12271
  const patchBody = {};
12272
+ if (matchedBy === "machine_id") {
12273
+ const keptName = (mine[0].name ?? "").trim();
12274
+ if (keptName !== o.ship.name.trim()) {
12275
+ out.shipRow.name = keptName;
12276
+ provisioning.push(`matched Ships row "${keptName}" by machine id \u2014 this host is now called "${o.ship.name}"; the row's name is kept`);
12277
+ }
12278
+ patchBody.last_seen = (/* @__PURE__ */ new Date()).toISOString();
12279
+ }
12280
+ if (writeMachineId) patchBody.machine_id = machineId;
12256
12281
  if ((mine[0].host_passengers ?? false) !== hostPassengers) patchBody.host_passengers = hostPassengers;
12257
12282
  if (sshPublicKey !== void 0 && (mine[0].ssh_public_key ?? "") !== sshPublicKey) {
12258
12283
  patchBody.ssh_public_key = sshPublicKey;
@@ -12270,13 +12295,16 @@ async function discover(o) {
12270
12295
  ...o.ship.platform ? { platform: o.ship.platform } : {},
12271
12296
  host_passengers: hostPassengers,
12272
12297
  ...sshPublicKey !== void 0 ? { ssh_public_key: sshPublicKey } : {},
12273
- ...ownerId !== void 0 ? { owner_id: ownerId } : {}
12298
+ ...ownerId !== void 0 ? { owner_id: ownerId } : {},
12299
+ ...writeMachineId ? { machine_id: machineId } : {}
12274
12300
  });
12275
12301
  shipRowId = created.id;
12276
- out.shipRow = { id: shipRowId, name: o.ship.name, action: "created" };
12302
+ out.shipRow = { id: shipRowId, name: o.ship.name, action: "created", matchedBy };
12277
12303
  provisioning.push(`created Ships row "${o.ship.name}"`);
12278
12304
  } else {
12279
- problems.push(`${mine.length} Ships rows named "${o.ship.name}" \u2014 which one is this machine?`);
12305
+ problems.push(
12306
+ matchedBy === "machine_id" ? `${mine.length} Ships rows carry this machine's id \u2014 which one is this machine?` : `${mine.length} Ships rows named "${o.ship.name}" \u2014 which one is this machine?`
12307
+ );
12280
12308
  }
12281
12309
  }
12282
12310
  let agentIdFor = () => void 0;
@@ -13654,7 +13682,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
13654
13682
  }
13655
13683
 
13656
13684
  // src/cli.ts
13657
- import { realpathSync as realpathSync2, readFileSync as readFileSync23, existsSync as existsSync30, mkdirSync as mkdirSync16, writeFileSync as writeFileSync17, rmSync as rmSync12, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync12 } from "fs";
13685
+ import { realpathSync as realpathSync2, readFileSync as readFileSync24, existsSync as existsSync31, mkdirSync as mkdirSync17, writeFileSync as writeFileSync18, rmSync as rmSync12, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync12 } from "fs";
13658
13686
  import { createInterface } from "readline/promises";
13659
13687
  import { dirname as dirOf, resolve as resolvePath } from "path";
13660
13688
 
@@ -13706,12 +13734,62 @@ function sshKeygenAvailable() {
13706
13734
  }
13707
13735
  }
13708
13736
 
13709
- // src/passenger-containers.ts
13737
+ // src/machine-id.ts
13710
13738
  import { execFileSync as execFileSync5 } from "child_process";
13711
13739
  import { createHash as createHash4 } from "crypto";
13740
+ import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "fs";
13741
+ import { dirname as dirname17, join as join26 } from "path";
13742
+ function machineIdPathFor(stateDir) {
13743
+ return join26(stateDir, "machine-id");
13744
+ }
13745
+ var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
13746
+ function readRawMachineId(platform2 = process.platform) {
13747
+ try {
13748
+ if (platform2 === "darwin") {
13749
+ const out = execFileSync5("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
13750
+ encoding: "utf8",
13751
+ stdio: ["ignore", "pipe", "ignore"]
13752
+ });
13753
+ return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
13754
+ }
13755
+ for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
13756
+ if (existsSync26(p)) {
13757
+ const id = readFileSync19(p, "utf8").trim();
13758
+ if (id) return id;
13759
+ }
13760
+ }
13761
+ } catch {
13762
+ }
13763
+ return void 0;
13764
+ }
13765
+ function ensureMachineId(stateDir, opts = {}) {
13766
+ if (opts.override?.trim()) return sha256(opts.override.trim());
13767
+ const path = machineIdPathFor(stateDir);
13768
+ if (existsSync26(path)) {
13769
+ const cached = readFileSync19(path, "utf8").trim();
13770
+ if (/^[0-9a-f]{64}$/.test(cached)) return cached;
13771
+ }
13772
+ const raw = (opts.read ?? readRawMachineId)();
13773
+ if (!raw) return void 0;
13774
+ const hashed = sha256(raw);
13775
+ mkdirSync13(dirname17(path), { recursive: true });
13776
+ writeFileSync14(path, `${hashed}
13777
+ `, { mode: 384 });
13778
+ return hashed;
13779
+ }
13780
+ function matchShipRows(ships, shipName2, machineId) {
13781
+ const hasColumn = ships.some((r) => "machine_id" in r);
13782
+ const byId = hasColumn && machineId ? ships.filter((r) => r.machine_id === machineId) : [];
13783
+ if (byId.length > 0) return { rows: byId, by: "machine_id", hasColumn };
13784
+ return { rows: ships.filter((r) => (r.name ?? "").trim() === shipName2.trim()), by: "name", hasColumn };
13785
+ }
13786
+
13787
+ // src/passenger-containers.ts
13788
+ import { execFileSync as execFileSync6 } from "child_process";
13789
+ import { createHash as createHash5 } from "crypto";
13712
13790
  var realExec = (cmd, args) => {
13713
13791
  try {
13714
- return execFileSync5(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
13792
+ return execFileSync6(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
13715
13793
  } catch (e) {
13716
13794
  const err = e;
13717
13795
  const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
@@ -13745,12 +13823,12 @@ function containerNameFor(workspaceId) {
13745
13823
  return `crew-passenger-${workspaceId}`;
13746
13824
  }
13747
13825
  function portFor(workspaceId, base = 28765, span = 1e3) {
13748
- const digest = createHash4("sha256").update(workspaceId).digest();
13826
+ const digest = createHash5("sha256").update(workspaceId).digest();
13749
13827
  return base + digest.readUInt32BE(0) % span;
13750
13828
  }
13751
13829
  function mountsDigest(mounts, image) {
13752
13830
  const sorted2 = [...mounts].sort((a, b) => a.containerPath.localeCompare(b.containerPath));
13753
- return createHash4("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
13831
+ return createHash5("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
13754
13832
  }
13755
13833
  function planContainers(cfg) {
13756
13834
  const byWorkspace = /* @__PURE__ */ new Map();
@@ -13910,9 +13988,9 @@ function syncAllPassengerCheckouts(cfg) {
13910
13988
 
13911
13989
  // src/tunnel.ts
13912
13990
  import { spawn as spawn4 } from "child_process";
13913
- import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14, unlinkSync as unlinkSync3, readdirSync as readdirSync8 } from "fs";
13991
+ import { existsSync as existsSync27, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync15, unlinkSync as unlinkSync3, readdirSync as readdirSync8 } from "fs";
13914
13992
  import { randomInt } from "crypto";
13915
- import { join as join26 } from "path";
13993
+ import { join as join27 } from "path";
13916
13994
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
13917
13995
  var defaultSpawn = (cmd, args) => spawn4(cmd, args, DEFAULT_SPAWN_OPTIONS);
13918
13996
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -13979,21 +14057,21 @@ function startTunnel(opts) {
13979
14057
  };
13980
14058
  }
13981
14059
  function tunnelStatePath(stateDir, workspaceId) {
13982
- return join26(stateDir, "passengers", `${workspaceId}.json`);
14060
+ return join27(stateDir, "passengers", `${workspaceId}.json`);
13983
14061
  }
13984
14062
  function readPersistedTunnel(stateDir, workspaceId) {
13985
14063
  const path = tunnelStatePath(stateDir, workspaceId);
13986
- if (!existsSync26(path)) return void 0;
14064
+ if (!existsSync27(path)) return void 0;
13987
14065
  try {
13988
- return JSON.parse(readFileSync19(path, "utf8"));
14066
+ return JSON.parse(readFileSync20(path, "utf8"));
13989
14067
  } catch {
13990
14068
  return void 0;
13991
14069
  }
13992
14070
  }
13993
14071
  function writePersistedTunnel(stateDir, state) {
13994
14072
  const path = tunnelStatePath(stateDir, state.workspaceId);
13995
- mkdirSync13(join26(stateDir, "passengers"), { recursive: true });
13996
- writeFileSync14(path, `${JSON.stringify(state, null, 2)}
14073
+ mkdirSync14(join27(stateDir, "passengers"), { recursive: true });
14074
+ writeFileSync15(path, `${JSON.stringify(state, null, 2)}
13997
14075
  `);
13998
14076
  }
13999
14077
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -14090,13 +14168,13 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
14090
14168
  }
14091
14169
  }
14092
14170
  function readAllPersistedWorkspaceIds(stateDir) {
14093
- const dir = join26(stateDir, "passengers");
14094
- if (!existsSync26(dir)) return [];
14171
+ const dir = join27(stateDir, "passengers");
14172
+ if (!existsSync27(dir)) return [];
14095
14173
  return readdirSync8(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
14096
14174
  }
14097
14175
 
14098
14176
  // src/mcp-endpoint-registry.ts
14099
- import { createHash as createHash5 } from "crypto";
14177
+ import { createHash as createHash6 } from "crypto";
14100
14178
  function endpointsUrl(api, workspaceId, id) {
14101
14179
  const base = `${api.baseUrl.replace(/\/+$/, "")}/api/workspaces/${workspaceId}/mcp-endpoints`;
14102
14180
  return id ? `${base}/${id}` : base;
@@ -14164,7 +14242,7 @@ async function probeEndpointAccess(api, workspaceId) {
14164
14242
  }
14165
14243
  var REGISTER_MCP_ENDPOINTS_HINT = "give this key's role the REGISTER_MCP_ENDPOINTS capability, e.g. via the group that holds the Crew project role";
14166
14244
  function registrationKey(url, mcpSecret) {
14167
- return createHash5("sha256").update(`${url}
14245
+ return createHash6("sha256").update(`${url}
14168
14246
  ${mcpSecret ?? ""}`).digest("hex").slice(0, 16);
14169
14247
  }
14170
14248
  async function reconcileEndpoint(workspaceId, d) {
@@ -14212,8 +14290,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
14212
14290
  import { createServer as createHttpServer } from "http";
14213
14291
  import { spawn as spawn5 } from "child_process";
14214
14292
  import { randomBytes } from "crypto";
14215
- import { existsSync as existsSync27, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync15, unlinkSync as unlinkSync4, readdirSync as readdirSync9 } from "fs";
14216
- import { join as join27 } from "path";
14293
+ import { existsSync as existsSync28, mkdirSync as mkdirSync15, readFileSync as readFileSync21, writeFileSync as writeFileSync16, unlinkSync as unlinkSync4, readdirSync as readdirSync9 } from "fs";
14294
+ import { join as join28 } from "path";
14217
14295
 
14218
14296
  // src/secret-compare.ts
14219
14297
  import { timingSafeEqual } from "crypto";
@@ -14302,21 +14380,21 @@ var defaultSpawn2 = (cmd, args, env) => {
14302
14380
  return child;
14303
14381
  };
14304
14382
  function syncDaemonStatePath(stateDir, workspaceId) {
14305
- return join27(stateDir, "passengers", `${workspaceId}-sync.json`);
14383
+ return join28(stateDir, "passengers", `${workspaceId}-sync.json`);
14306
14384
  }
14307
14385
  function readPersistedSyncDaemon(stateDir, workspaceId) {
14308
14386
  const path = syncDaemonStatePath(stateDir, workspaceId);
14309
- if (!existsSync27(path)) return void 0;
14387
+ if (!existsSync28(path)) return void 0;
14310
14388
  try {
14311
- return JSON.parse(readFileSync20(path, "utf8"));
14389
+ return JSON.parse(readFileSync21(path, "utf8"));
14312
14390
  } catch {
14313
14391
  return void 0;
14314
14392
  }
14315
14393
  }
14316
14394
  function writePersistedSyncDaemon(stateDir, state) {
14317
14395
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
14318
- mkdirSync14(join27(stateDir, "passengers"), { recursive: true });
14319
- writeFileSync15(path, `${JSON.stringify(state, null, 2)}
14396
+ mkdirSync15(join28(stateDir, "passengers"), { recursive: true });
14397
+ writeFileSync16(path, `${JSON.stringify(state, null, 2)}
14320
14398
  `);
14321
14399
  }
14322
14400
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -14336,8 +14414,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
14336
14414
  }
14337
14415
  }
14338
14416
  function readAllPersistedSyncWorkspaceIds(stateDir) {
14339
- const dir = join27(stateDir, "passengers");
14340
- if (!existsSync27(dir)) return [];
14417
+ const dir = join28(stateDir, "passengers");
14418
+ if (!existsSync28(dir)) return [];
14341
14419
  const suffix = "-sync.json";
14342
14420
  return readdirSync9(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
14343
14421
  }
@@ -14394,12 +14472,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
14394
14472
  }
14395
14473
 
14396
14474
  // src/daemon.ts
14397
- import { existsSync as existsSync29, mkdirSync as mkdirSync15, readFileSync as readFileSync22, readdirSync as readdirSync11, rmSync as rmSync11, statSync as statSync9, writeFileSync as writeFileSync16 } from "fs";
14398
- import { join as join29 } from "path";
14475
+ import { existsSync as existsSync30, mkdirSync as mkdirSync16, readFileSync as readFileSync23, readdirSync as readdirSync11, rmSync as rmSync11, statSync as statSync9, writeFileSync as writeFileSync17 } from "fs";
14476
+ import { join as join30 } from "path";
14399
14477
 
14400
14478
  // src/interrupted.ts
14401
- import { existsSync as existsSync28, readdirSync as readdirSync10, readFileSync as readFileSync21 } from "fs";
14402
- import { join as join28 } from "path";
14479
+ import { existsSync as existsSync29, readdirSync as readdirSync10, readFileSync as readFileSync22 } from "fs";
14480
+ import { join as join29 } from "path";
14403
14481
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
14404
14482
  function interruptedComment(b) {
14405
14483
  const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
@@ -14418,21 +14496,21 @@ var pidAlive = (pid) => {
14418
14496
  };
14419
14497
  function runFinished(streamPath) {
14420
14498
  const path = `${streamPath}.meta.json`;
14421
- if (!existsSync28(path)) return false;
14499
+ if (!existsSync29(path)) return false;
14422
14500
  try {
14423
- return JSON.parse(readFileSync21(path, "utf8")).running !== true;
14501
+ return JSON.parse(readFileSync22(path, "utf8")).running !== true;
14424
14502
  } catch {
14425
14503
  return true;
14426
14504
  }
14427
14505
  }
14428
14506
  function orphanedStreams(o) {
14429
- const dir = join28(o.stateDir, "streams");
14430
- if (!existsSync28(dir)) return [];
14507
+ const dir = join29(o.stateDir, "streams");
14508
+ if (!existsSync29(dir)) return [];
14431
14509
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
14432
14510
  const out = [];
14433
14511
  for (const name of readdirSync10(dir)) {
14434
14512
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
14435
- const streamPath = join28(dir, name);
14513
+ const streamPath = join29(dir, name);
14436
14514
  if (runFinished(streamPath)) continue;
14437
14515
  const sidecar = readRunSidecar(streamPath);
14438
14516
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -14532,7 +14610,7 @@ function latestMtime(path) {
14532
14610
  if (!stat.isDirectory()) return 0;
14533
14611
  let latest = stat.mtimeMs;
14534
14612
  for (const entry of readdirSync11(path, { withFileTypes: true })) {
14535
- latest = Math.max(latest, latestMtime(join29(path, entry.name)));
14613
+ latest = Math.max(latest, latestMtime(join30(path, entry.name)));
14536
14614
  }
14537
14615
  return latest;
14538
14616
  }
@@ -14541,23 +14619,23 @@ function computeTreeSignature(paths) {
14541
14619
  }
14542
14620
  function makeStaleChecker(paths, opts = {}) {
14543
14621
  const initial = computeTreeSignature(paths);
14544
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync29(opts.reloadSentinel);
14622
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync30(opts.reloadSentinel);
14545
14623
  }
14546
14624
  function reloadSentinelFor(stateDir) {
14547
- return join29(stateDir, "reload-requested");
14625
+ return join30(stateDir, "reload-requested");
14548
14626
  }
14549
14627
  function daemonStampFor(stateDir) {
14550
- return join29(stateDir, "daemon.started");
14628
+ return join30(stateDir, "daemon.started");
14551
14629
  }
14552
14630
  function noteDaemonStarted(stateDir, now = Date.now()) {
14553
- mkdirSync15(stateDir, { recursive: true });
14631
+ mkdirSync16(stateDir, { recursive: true });
14554
14632
  rmSync11(reloadSentinelFor(stateDir), { force: true });
14555
- writeFileSync16(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
14633
+ writeFileSync17(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
14556
14634
  `);
14557
14635
  }
14558
14636
  function readDaemonStarted(stateDir) {
14559
14637
  try {
14560
- const v = JSON.parse(readFileSync22(daemonStampFor(stateDir), "utf8")).startedAt;
14638
+ const v = JSON.parse(readFileSync23(daemonStampFor(stateDir), "utf8")).startedAt;
14561
14639
  return typeof v === "number" ? v : null;
14562
14640
  } catch {
14563
14641
  return null;
@@ -14923,7 +15001,7 @@ async function buildEnvironment(route, ship, ticket) {
14923
15001
  }
14924
15002
 
14925
15003
  // src/cli.ts
14926
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname17(process.execPath) : resolve5(dirname17(fileURLToPath(import.meta.url)), "..");
15004
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname18(process.execPath) : resolve5(dirname18(fileURLToPath(import.meta.url)), "..");
14927
15005
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
14928
15006
  var CYCLE_SECONDS2 = 120;
14929
15007
  var STALE_CYCLES = 3;
@@ -15069,7 +15147,7 @@ function usage() {
15069
15147
  resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
15070
15148
  crew log [route] tail the log
15071
15149
  crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
15072
- crew connect resolve a workspace's ids into a crew.yaml block, then (admin key) sync personas/skills and mint seat keys (--no-agents-sync skips that; --no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one)
15150
+ crew connect resolve a workspace's ids into a crew.yaml block, then (admin key) sync personas/skills and mint seat keys (--no-agents-sync skips that; --no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one; --dry-run works with no crew.yaml and writes none: no file, no rows, no seat keys \u2014 the device approval and workspace credential behave as in a real run)
15073
15151
  crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
15074
15152
  crew disconnect --reset [--remove-checkouts] [--dry-run] [--json] remove everything connect and install wrote on this machine: scheduler units, Pair/MCP entries, seat keys, keychain credential, resolved state, then crew.yaml (--remove-checkouts also deletes checkouts crew cloned, never one with local changes)
15075
15153
  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)
@@ -15085,7 +15163,7 @@ function usage() {
15085
15163
  crew install write and load this platform's scheduler unit
15086
15164
  crew uninstall unload and remove it
15087
15165
  --workspace-id ID --key K [--project NAME] [--area NAME] [--dir PATH] [--name N]
15088
- --json --ship-name N --agent-bin PATH --extra-path DIRS --host-passengers --no-wizard --choose KEY=VALUE (non-interactive: docs/commands.md)
15166
+ --json --machine-id ID --ship-name N --agent-bin PATH --extra-path DIRS --host-passengers --no-wizard --choose KEY=VALUE (non-interactive: docs/commands.md)
15089
15167
 
15090
15168
  Options:
15091
15169
  --dry-run decide everything, perform nothing
@@ -15192,13 +15270,19 @@ if (command === "passenger-sync-daemon") {
15192
15270
  if (!why) return false;
15193
15271
  emit.emit(`${why} \u2014 release skipped`, { step: "release" });
15194
15272
  return true;
15273
+ }, doctorMachineId = function(stateDir) {
15274
+ try {
15275
+ return ensureMachineId(stateDir, { override: value("machine-id") });
15276
+ } catch {
15277
+ return void 0;
15278
+ }
15195
15279
  }, baseHealth = function(r) {
15196
15280
  const { remote, base } = r.config.branch;
15197
15281
  const out = refreshBaseBranch(r.dir, remote, base, true);
15198
15282
  const unsafe = baseBranchUnsafe(out);
15199
15283
  return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
15200
15284
  };
15201
- requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, baseHealth2 = baseHealth;
15285
+ requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth;
15202
15286
  let cfg;
15203
15287
  let firstRun;
15204
15288
  let firstRunFromFlags = false;
@@ -15211,7 +15295,7 @@ if (command === "passenger-sync-daemon") {
15211
15295
  hostPassengers: flag("host-passengers"),
15212
15296
  noWizard: flag("no-wizard")
15213
15297
  };
15214
- if (command === "connect" && !flag("dry-run") && usesFlagAnswers(wizardFlags, connectIsTTY) && shouldRunWizard({ isTTY: true, crewHome: CREW_HOME })) {
15298
+ if (command === "connect" && usesFlagAnswers(wizardFlags, connectIsTTY) && shouldRunWizard({ isTTY: true, crewHome: CREW_HOME })) {
15215
15299
  const flagged = flagWizardAnswers(wizardFlags, {
15216
15300
  hostname: hostname2(),
15217
15301
  platform: hostPlatform(),
@@ -15229,7 +15313,7 @@ if (command === "passenger-sync-daemon") {
15229
15313
  firstRun = { answers: flagged.answers, configPath };
15230
15314
  firstRunFromFlags = true;
15231
15315
  cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(flagged.answers), allowNoRoutes: true });
15232
- } else if (command === "connect" && !flag("dry-run") && shouldRunWizard({
15316
+ } else if (command === "connect" && shouldRunWizard({
15233
15317
  isTTY: connectIsTTY,
15234
15318
  crewHome: CREW_HOME
15235
15319
  })) {
@@ -15484,8 +15568,8 @@ if (command === "passenger-sync-daemon") {
15484
15568
  }
15485
15569
  if (host === "linux" && hasSystemd) {
15486
15570
  const label = labelFor(CREW_HOME, "run");
15487
- const staleTimer = join30(homedir8(), ".config", "systemd", "user", `${label}.timer`);
15488
- if (existsSync30(staleTimer)) {
15571
+ const staleTimer = join31(homedir8(), ".config", "systemd", "user", `${label}.timer`);
15572
+ if (existsSync31(staleTimer)) {
15489
15573
  process.stdout.write(
15490
15574
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
15491
15575
  `
@@ -15511,14 +15595,14 @@ if (command === "passenger-sync-daemon") {
15511
15595
  const tracker = new Tracker(c, cfg.ship);
15512
15596
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
15513
15597
  for (const r of await resolvedRepos(c)) {
15514
- if (!existsSync30(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
15598
+ if (!existsSync31(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
15515
15599
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
15516
15600
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
15517
15601
  }
15518
15602
  try {
15519
15603
  const ships = await tracker.shipRows();
15520
15604
  if (ships.length > 0) {
15521
- const mine = ships.filter((s2) => (s2.name ?? "").trim() === cfg.ship.name.trim());
15605
+ const mine = matchShipRows(ships, cfg.ship.name, doctorMachineId(cfg.ship.stateDir)).rows;
15522
15606
  if (mine.length !== 1) problems.push(`ship record: ${mine.length} rows named "${cfg.ship.name}"`);
15523
15607
  else if (mine[0].platform && mine[0].platform !== host) problems.push(`ship record: declares ${mine[0].platform}, this host is ${host}`);
15524
15608
  }
@@ -15529,7 +15613,7 @@ if (command === "passenger-sync-daemon") {
15529
15613
  }
15530
15614
  async function doctorFix(o) {
15531
15615
  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))));
15532
- const unitExists = (p) => existsSync30(p);
15616
+ const unitExists = (p) => existsSync31(p);
15533
15617
  const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
15534
15618
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
15535
15619
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -15546,7 +15630,7 @@ if (command === "passenger-sync-daemon") {
15546
15630
  for (const a of plan.actions) {
15547
15631
  if (a.kind === "enable") {
15548
15632
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
15549
- writeFileSync17(cfg.configFile, planEnable(readFileSync23(cfg.configFile, "utf8"), a.route));
15633
+ writeFileSync18(cfg.configFile, planEnable(readFileSync24(cfg.configFile, "utf8"), a.route));
15550
15634
  enabledSound.add(a.route);
15551
15635
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
15552
15636
  } else {
@@ -16545,7 +16629,7 @@ ${changed2.map((c) => ` - ${c}
16545
16629
  process.exit(1);
16546
16630
  }
16547
16631
  requireArmed("run the daemon loop");
16548
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join30(CREW_HOME, "src"), join30(CREW_HOME, "dist"), join30(CREW_HOME, "bin")];
16632
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join31(CREW_HOME, "src"), join31(CREW_HOME, "dist"), join31(CREW_HOME, "bin")];
16549
16633
  const configPaths = daemonConfigPaths(cfg);
16550
16634
  watchPaths.push(...configPaths);
16551
16635
  noteDaemonStarted(cfg.ship.stateDir);
@@ -16746,10 +16830,10 @@ ${changed2.map((c) => ` - ${c}
16746
16830
  `);
16747
16831
  if (!dryRun) {
16748
16832
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
16749
- const raw = existsSync30(resolvedPath) ? JSON.parse(readFileSync23(resolvedPath, "utf8")) : {};
16833
+ const raw = existsSync31(resolvedPath) ? JSON.parse(readFileSync24(resolvedPath, "utf8")) : {};
16750
16834
  raw.agentSkills = result.agentSkills;
16751
- mkdirSync16(dirOf(resolvedPath), { recursive: true });
16752
- writeFileSync17(resolvedPath, `${JSON.stringify(raw, null, 2)}
16835
+ mkdirSync17(dirOf(resolvedPath), { recursive: true });
16836
+ writeFileSync18(resolvedPath, `${JSON.stringify(raw, null, 2)}
16753
16837
  `);
16754
16838
  }
16755
16839
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -16762,15 +16846,15 @@ ${changed2.map((c) => ` - ${c}
16762
16846
  process.exit(2);
16763
16847
  };
16764
16848
  if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
16765
- const configText = readFileSync23(cfg.configFile, "utf8");
16849
+ const configText = readFileSync24(cfg.configFile, "utf8");
16766
16850
  if (sub === "hooks") {
16767
16851
  try {
16768
16852
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
16769
- const dir = repoDirOf(configText, dirname17(cfg.configFile), a.route, a.name);
16770
- if (!existsSync30(dir)) throw new ReposError(`${dir} does not exist on disk`);
16771
- const files = { exists: (r) => existsSync30(resolvePath(dir, r)), read: (r) => {
16853
+ const dir = repoDirOf(configText, dirname18(cfg.configFile), a.route, a.name);
16854
+ if (!existsSync31(dir)) throw new ReposError(`${dir} does not exist on disk`);
16855
+ const files = { exists: (r) => existsSync31(resolvePath(dir, r)), read: (r) => {
16772
16856
  try {
16773
- return readFileSync23(resolvePath(dir, r), "utf8");
16857
+ return readFileSync24(resolvePath(dir, r), "utf8");
16774
16858
  } catch {
16775
16859
  return void 0;
16776
16860
  }
@@ -16782,13 +16866,13 @@ ${changed2.map((c) => ` - ${c}
16782
16866
  if (write && suggestion.source === null) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
16783
16867
  if (write === "route") {
16784
16868
  const plan = planRouteHooks(configText, a.route, a.name, suggestion.hooks);
16785
- writeFileSync17(cfg.configFile, plan.text);
16869
+ writeFileSync18(cfg.configFile, plan.text);
16786
16870
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
16787
16871
  } else if (write === "repo") {
16788
16872
  const have = existingRepoConfig(files);
16789
16873
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
16790
16874
  const file = resolvePath(dir, ".crew.yaml");
16791
- writeFileSync17(file, starterRepoConfig(suggestion.hooks), { flag: "wx" });
16875
+ writeFileSync18(file, starterRepoConfig(suggestion.hooks), { flag: "wx" });
16792
16876
  written = { target: "repo", file, hooks: Object.keys(suggestion.hooks), kept: [] };
16793
16877
  }
16794
16878
  if (flag("json")) {
@@ -16825,7 +16909,7 @@ ${changed2.map((c) => ` - ${c}
16825
16909
  if (sub === "list" && !routeName) usage2();
16826
16910
  if (sub === "add") {
16827
16911
  try {
16828
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync30(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
16912
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync31(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
16829
16913
  routeName = a.route;
16830
16914
  addPath = a.path;
16831
16915
  addName = a.name;
@@ -16854,15 +16938,15 @@ ${changed2.map((c) => ` - ${c}
16854
16938
  }
16855
16939
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
16856
16940
  let resolvedRemotes;
16857
- if (existsSync30(resolvedFile)) {
16941
+ if (existsSync31(resolvedFile)) {
16858
16942
  try {
16859
- resolvedRemotes = JSON.parse(readFileSync23(resolvedFile, "utf8"));
16943
+ resolvedRemotes = JSON.parse(readFileSync24(resolvedFile, "utf8"));
16860
16944
  } catch {
16861
16945
  }
16862
16946
  }
16863
16947
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
16864
16948
  const text = configText;
16865
- const base = dirname17(cfg.configFile);
16949
+ const base = dirname18(cfg.configFile);
16866
16950
  try {
16867
16951
  if (sub === "list") {
16868
16952
  const repos = configuredRepos(text, routeName, base);
@@ -16871,7 +16955,7 @@ ${changed2.map((c) => ` - ${c}
16871
16955
  for (const r of repos) {
16872
16956
  const origin2 = originOf(r.dir);
16873
16957
  const row = matchRepoName(origin2, resolvedRemotes);
16874
- const note = !existsSync30(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
16958
+ const note = !existsSync31(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
16875
16959
  process.stdout.write(`${r.name} ${r.dir} ${note}
16876
16960
  `);
16877
16961
  }
@@ -16883,9 +16967,9 @@ ${changed2.map((c) => ` - ${c}
16883
16967
  ` : `${human}
16884
16968
  `);
16885
16969
  const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
16886
- if (existsSync30(plan2.target) && (existsSync30(`${plan2.target}/.git`) || readdirSync12(plan2.target).length > 0)) {
16970
+ if (existsSync31(plan2.target) && (existsSync31(`${plan2.target}/.git`) || readdirSync12(plan2.target).length > 0)) {
16887
16971
  throw new ReposError(
16888
- existsSync30(`${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`
16972
+ existsSync31(`${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`
16889
16973
  );
16890
16974
  }
16891
16975
  if (dryRun) {
@@ -16903,12 +16987,12 @@ ${changed2.map((c) => ` - ${c}
16903
16987
  throw new ReposError(`${e.message}${authHint}
16904
16988
  crew.yaml was not changed`);
16905
16989
  }
16906
- writeFileSync17(cfg.configFile, plan2.text);
16990
+ writeFileSync18(cfg.configFile, plan2.text);
16907
16991
  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}`);
16908
16992
  break;
16909
16993
  }
16910
16994
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
16911
- const top = existsSync30(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
16995
+ const top = existsSync31(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
16912
16996
  if (!top) throw new ReposError(`${target} is not a git checkout`);
16913
16997
  const origin = originOf(top);
16914
16998
  const plan = planRepoAdd({
@@ -16957,15 +17041,15 @@ ${changed2.map((c) => ` - ${c}
16957
17041
  const created = await rowTracker.createRepoRow(rowPayload);
16958
17042
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
16959
17043
  `);
16960
- if (existsSync30(resolvedFile)) {
16961
- const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
17044
+ if (existsSync31(resolvedFile)) {
17045
+ const raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
16962
17046
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
16963
17047
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
16964
- writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
17048
+ writeFileSync18(resolvedFile, `${JSON.stringify(raw, null, 2)}
16965
17049
  `);
16966
17050
  }
16967
17051
  }
16968
- writeFileSync17(cfg.configFile, plan.text);
17052
+ writeFileSync18(cfg.configFile, plan.text);
16969
17053
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
16970
17054
  `);
16971
17055
  } catch (e) {
@@ -17186,9 +17270,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17186
17270
  `);
17187
17271
  }
17188
17272
  }
17273
+ let machineId;
17274
+ try {
17275
+ machineId = ensureMachineId(cfg.ship.stateDir, { override: value("machine-id") });
17276
+ } catch (e) {
17277
+ process.stderr.write(`(no machine id \u2014 ${e.message})
17278
+ `);
17279
+ }
17189
17280
  const ship = {
17190
17281
  name: cfg.ship.name,
17191
17282
  platform: cfg.ship.platform,
17283
+ machineId,
17192
17284
  hostPassengers: firstRun?.answers.hostPassengers ?? existingRoute?.hostPassengers === true,
17193
17285
  sshPublicKey
17194
17286
  };
@@ -17285,10 +17377,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17285
17377
  }
17286
17378
  }
17287
17379
  let previous;
17288
- const hadPreviousFile = existsSync30(resolvedPath);
17380
+ const hadPreviousFile = existsSync31(resolvedPath);
17289
17381
  if (hadPreviousFile) {
17290
17382
  try {
17291
- previous = JSON.parse(readFileSync23(resolvedPath, "utf8"));
17383
+ previous = JSON.parse(readFileSync24(resolvedPath, "utf8"));
17292
17384
  } catch {
17293
17385
  }
17294
17386
  }
@@ -17397,10 +17489,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17397
17489
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
17398
17490
  `);
17399
17491
  } else {
17400
- mkdirSync16(dirname17(resolvedPath), { recursive: true });
17492
+ mkdirSync17(dirname18(resolvedPath), { recursive: true });
17401
17493
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
17402
17494
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
17403
- writeFileSync17(tmpPath, `${JSON.stringify(resolved, null, 2)}
17495
+ writeFileSync18(tmpPath, `${JSON.stringify(resolved, null, 2)}
17404
17496
  `);
17405
17497
  renameSync2(tmpPath, resolvedPath);
17406
17498
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -17435,8 +17527,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17435
17527
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
17436
17528
  `);
17437
17529
  } else {
17438
- mkdirSync16(dirname17(apiKeyFile), { recursive: true });
17439
- writeFileSync17(apiKeyFile, `${apiKeyVar}=${apiKey}
17530
+ mkdirSync17(dirname18(apiKeyFile), { recursive: true });
17531
+ writeFileSync18(apiKeyFile, `${apiKeyVar}=${apiKey}
17440
17532
  `, { mode: 384 });
17441
17533
  process.stderr.write(`API key written to ${apiKeyFile}
17442
17534
  `);
@@ -17445,7 +17537,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17445
17537
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
17446
17538
  const wizard = firstRun;
17447
17539
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
17448
- const routeDir = value("dir") ?? (writeFirstRun ? join30(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
17540
+ const routeDir = value("dir") ?? (writeFirstRun ? join31(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
17449
17541
  const routeBlock = renderConnection(
17450
17542
  found,
17451
17543
  route2,
@@ -18089,7 +18181,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18089
18181
  };
18090
18182
  const droppedRoutes = [];
18091
18183
  try {
18092
- const rawCfg = parseYaml2(readFileSync23(cfg.configFile, "utf8"));
18184
+ const rawCfg = parseYaml2(readFileSync24(cfg.configFile, "utf8"));
18093
18185
  for (const r of rawCfg?.routes ?? []) {
18094
18186
  if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
18095
18187
  try {
@@ -18131,8 +18223,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18131
18223
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
18132
18224
  break;
18133
18225
  }
18134
- mkdirSync16(cfg.ship.stateDir, { recursive: true });
18135
- writeFileSync17(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
18226
+ mkdirSync17(cfg.ship.stateDir, { recursive: true });
18227
+ writeFileSync18(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
18136
18228
  `);
18137
18229
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
18138
18230
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -18183,7 +18275,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18183
18275
  }
18184
18276
  case "log": {
18185
18277
  try {
18186
- const text = readFileSync23(cfg.ship.logFile, "utf8").split("\n");
18278
+ const text = readFileSync24(cfg.ship.logFile, "utf8").split("\n");
18187
18279
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
18188
18280
  `);
18189
18281
  } catch {
@@ -18206,8 +18298,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18206
18298
  };
18207
18299
  if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
18208
18300
  const tracker = new Tracker(route, cfg.ship);
18209
- const configText = readFileSync23(cfg.configFile, "utf8");
18210
- const cfgBase = dirname17(cfg.configFile);
18301
+ const configText = readFileSync24(cfg.configFile, "utf8");
18302
+ const cfgBase = dirname18(cfg.configFile);
18211
18303
  const configured = configuredRepos(configText, route.route, cfgBase);
18212
18304
  const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
18213
18305
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
@@ -18235,7 +18327,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18235
18327
  }
18236
18328
  if (configured.some((r) => r.dir === dir)) {
18237
18329
  try {
18238
- writeFileSync17(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
18330
+ writeFileSync18(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
18239
18331
  } catch (e) {
18240
18332
  if (e instanceof ReposError) fail(e.message);
18241
18333
  throw e;
@@ -18243,29 +18335,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18243
18335
  }
18244
18336
  if (row) {
18245
18337
  await tracker.deleteRepoRow(row.id);
18246
- if (existsSync30(resolvedFile)) {
18247
- const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
18338
+ if (existsSync31(resolvedFile)) {
18339
+ const raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
18248
18340
  if (raw.repoNames) delete raw.repoNames[row.id];
18249
18341
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
18250
- writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
18342
+ writeFileSync18(resolvedFile, `${JSON.stringify(raw, null, 2)}
18251
18343
  `);
18252
18344
  }
18253
18345
  }
18254
- if (existsSync30(join30(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync12(dir, { recursive: true, force: true });
18346
+ if (existsSync31(join31(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync12(dir, { recursive: true, force: true });
18255
18347
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
18256
18348
  break;
18257
18349
  }
18258
18350
  const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
18259
- const fresh = !existsSync30(join30(dir, ".git"));
18260
- if (existsSync30(dir) && fresh && readdirSync12(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
18351
+ const fresh = !existsSync31(join31(dir, ".git"));
18352
+ if (existsSync31(dir) && fresh && readdirSync12(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
18261
18353
  if (dryRun) {
18262
18354
  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`);
18263
18355
  break;
18264
18356
  }
18265
18357
  if (fresh) {
18266
- mkdirSync16(dir, { recursive: true });
18267
- writeFileSync17(join30(dir, "README.md"), SAMPLE_README);
18268
- writeFileSync17(join30(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
18358
+ mkdirSync17(dir, { recursive: true });
18359
+ writeFileSync18(join31(dir, "README.md"), SAMPLE_README);
18360
+ writeFileSync18(join31(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
18269
18361
  const g = (...a) => {
18270
18362
  const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
18271
18363
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -18283,17 +18375,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18283
18375
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
18284
18376
  });
18285
18377
  repoId = (await tracker.createRepoRow(payload)).id;
18286
- if (existsSync30(resolvedFile)) {
18287
- const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
18378
+ if (existsSync31(resolvedFile)) {
18379
+ const raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
18288
18380
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
18289
18381
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
18290
- writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
18382
+ writeFileSync18(resolvedFile, `${JSON.stringify(raw, null, 2)}
18291
18383
  `);
18292
18384
  }
18293
18385
  }
18294
18386
  if (!configured.some((r) => r.dir === dir)) {
18295
18387
  try {
18296
- writeFileSync17(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
18388
+ writeFileSync18(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
18297
18389
  } catch (e) {
18298
18390
  if (e instanceof ReposError) fail(e.message);
18299
18391
  throw e;
@@ -18587,7 +18679,7 @@ watermark: ${state.watermark()}
18587
18679
  }
18588
18680
  for (const r of repos) {
18589
18681
  const remote = r.config.branch.remote;
18590
- if (existsSync30(r.dir)) {
18682
+ if (existsSync31(r.dir)) {
18591
18683
  pushAuth.push({
18592
18684
  route: c.route,
18593
18685
  repo: r.name,
@@ -18615,7 +18707,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18615
18707
  );
18616
18708
  try {
18617
18709
  const evFile = eventFileFor(cfg.ship.stateDir);
18618
- const evSize = existsSync30(evFile) ? statSync10(evFile).size : 0;
18710
+ const evSize = existsSync31(evFile) ? statSync10(evFile).size : 0;
18619
18711
  process.stdout.write(
18620
18712
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
18621
18713
  ` : "events log: ok\n"
@@ -18626,7 +18718,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18626
18718
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
18627
18719
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
18628
18720
  try {
18629
- return readFileSync23(p, "utf8");
18721
+ return readFileSync24(p, "utf8");
18630
18722
  } catch {
18631
18723
  return null;
18632
18724
  }
@@ -18740,7 +18832,12 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18740
18832
  if (ships.length === 0) {
18741
18833
  process.stdout.write("ship record: this workspace has no Ships table (fine)\n");
18742
18834
  } else {
18743
- const mine = ships.filter((s2) => (s2.name ?? "").trim() === cfg.ship.name.trim());
18835
+ const matched = matchShipRows(ships, cfg.ship.name, doctorMachineId(cfg.ship.stateDir));
18836
+ const mine = matched.rows;
18837
+ process.stdout.write(
18838
+ matched.hasColumn ? `ship row: ${mine.length === 1 ? `matched by ${matched.by === "machine_id" ? "machine id" : "name"}` : "not matched"}
18839
+ ` : "ship row: matched by name only \u2014 the Ships table has no machine_id column (update the Issues template)\n"
18840
+ );
18744
18841
  if (mine.length === 0) {
18745
18842
  process.stdout.write(
18746
18843
  `ship record: NO row named "${cfg.ship.name}" \u2014 have: ${ships.map((s2) => s2.name).join(", ")}
@@ -19023,4 +19120,5 @@ var passengerApiKeys2;
19023
19120
  var endpointApiFor2;
19024
19121
  var endpointReconcileDeps2;
19025
19122
  var releaseRefused2;
19123
+ var doctorMachineId2;
19026
19124
  var baseHealth2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.62.0",
3
+ "version": "0.63.1",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",