@tablation/crew 0.90.0 → 0.91.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +855 -473
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -223,8 +223,8 @@ function shellInitScript(shell) {
223
223
  }
224
224
 
225
225
  // src/config.ts
226
- import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
227
- import { resolve, dirname as dirname3, isAbsolute, join as join10, basename as basename3 } from "path";
226
+ import { readFileSync as readFileSync10, existsSync as existsSync9 } from "fs";
227
+ import { resolve, dirname as dirname4, isAbsolute, join as join11, basename as basename3 } from "path";
228
228
  import { homedir as homedir6, tmpdir } from "os";
229
229
  import { parse as parse2 } from "yaml";
230
230
 
@@ -1581,6 +1581,242 @@ function resolveSeat(ship, route, role, persona = {}, repo) {
1581
1581
  return { toolchain, model, toolchainWhy, modelWhy, warnings };
1582
1582
  }
1583
1583
 
1584
+ // src/forges.ts
1585
+ import { execFileSync } from "child_process";
1586
+ import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync8, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
1587
+ import { dirname as dirname3, join as join9 } from "path";
1588
+ var ForgeError = class extends Error {
1589
+ };
1590
+ var DEFAULT_FORGE_USERNAME = "x-access-token";
1591
+ var DEFAULT_KEYCHAIN_SERVICE = "crew-forge";
1592
+ function defaultTokenFilePath(stateDir, host) {
1593
+ return join9(stateDir, "forges", `${host.replace(/[^a-z0-9.-]/gi, "_")}.token`);
1594
+ }
1595
+ var envSource = {
1596
+ read: (s, c) => c.env[s.var]?.trim() || null,
1597
+ describe: (s) => `environment variable ${s.var}`
1598
+ };
1599
+ var fileSource = {
1600
+ read(s, c) {
1601
+ const path = s.path ?? defaultTokenFilePath(c.stateDir, c.host);
1602
+ if (!existsSync7(path)) return null;
1603
+ const mode = statSync2(path).mode & 511;
1604
+ if (mode & 63) {
1605
+ throw new ForgeError(
1606
+ `${path} is mode ${mode.toString(8).padStart(3, "0")}; a forge token file must be owner-only (chmod 600 ${path})`
1607
+ );
1608
+ }
1609
+ return readFileSync8(path, "utf8").trim() || null;
1610
+ },
1611
+ write(s, token, c) {
1612
+ const path = s.path ?? defaultTokenFilePath(c.stateDir, c.host);
1613
+ mkdirSync5(dirname3(path), { recursive: true, mode: 448 });
1614
+ writeFileSync6(path, `${token}
1615
+ `, { mode: 384 });
1616
+ chmodSync(path, 384);
1617
+ },
1618
+ describe: (s, c) => `key file ${s.path ?? defaultTokenFilePath(c.stateDir, c.host)}`
1619
+ };
1620
+ var keychainService = (s) => s.service ?? DEFAULT_KEYCHAIN_SERVICE;
1621
+ var keychainSource = {
1622
+ read(s, c) {
1623
+ try {
1624
+ if (process.platform === "darwin") {
1625
+ return execFileSync(
1626
+ "security",
1627
+ ["find-generic-password", "-s", keychainService(s), "-a", c.host, "-w"],
1628
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
1629
+ ).trim() || null;
1630
+ }
1631
+ return execFileSync(
1632
+ "secret-tool",
1633
+ ["lookup", "service", keychainService(s), "account", c.host],
1634
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
1635
+ ).trim() || null;
1636
+ } catch {
1637
+ return null;
1638
+ }
1639
+ },
1640
+ write(s, token, c) {
1641
+ try {
1642
+ if (process.platform === "darwin") {
1643
+ const q = (v) => `"${v.replace(/(["\\])/g, "\\$1")}"`;
1644
+ execFileSync("security", ["-i"], {
1645
+ input: `add-generic-password -U -s ${q(keychainService(s))} -a ${q(c.host)} -w ${q(token)}
1646
+ `,
1647
+ stdio: ["pipe", "ignore", "pipe"]
1648
+ });
1649
+ } else {
1650
+ execFileSync(
1651
+ "secret-tool",
1652
+ ["store", "--label", `crew ${c.host}`, "service", keychainService(s), "account", c.host],
1653
+ { input: token, stdio: ["pipe", "ignore", "pipe"] }
1654
+ );
1655
+ }
1656
+ } catch (e) {
1657
+ throw new ForgeError(`could not write to the OS secret store: ${e.message.split("\n")[0]}`);
1658
+ }
1659
+ },
1660
+ describe: (s) => `OS secret store (service ${keychainService(s)})`
1661
+ };
1662
+ var TOKEN_SOURCES = {
1663
+ env: envSource,
1664
+ file: fileSource,
1665
+ keychain: keychainSource
1666
+ };
1667
+ function impl(s) {
1668
+ const found = TOKEN_SOURCES[s.source];
1669
+ if (!found) throw new ForgeError(`unknown token source "${s.source}"`);
1670
+ return found;
1671
+ }
1672
+ function bareHost(host) {
1673
+ return host.toLowerCase().replace(/:\d+$/, "");
1674
+ }
1675
+ function forgeFor(forges, host) {
1676
+ const want = bareHost(host);
1677
+ for (const [h, entry] of Object.entries(forges ?? {})) {
1678
+ if (bareHost(h) === want) return { host: h, entry };
1679
+ }
1680
+ return void 0;
1681
+ }
1682
+ function readForgeToken(forges, host, stateDir, env = process.env) {
1683
+ const f = forgeFor(forges, host);
1684
+ if (!f) return null;
1685
+ return impl(f.entry.token).read(f.entry.token, { host: f.host, stateDir, env });
1686
+ }
1687
+ function describeForgeSource(forges, host, stateDir) {
1688
+ const f = forgeFor(forges, host);
1689
+ return f ? impl(f.entry.token).describe(f.entry.token, { host: f.host, stateDir, env: process.env }) : null;
1690
+ }
1691
+ function hasForgeToken(forges, host, stateDir, env = process.env) {
1692
+ try {
1693
+ return !!readForgeToken(forges, host, stateDir, env);
1694
+ } catch {
1695
+ return false;
1696
+ }
1697
+ }
1698
+ function storeForgeToken(forges, host, token, stateDir, env = process.env) {
1699
+ const f = forgeFor(forges, host);
1700
+ if (!f) throw new ForgeError(`no forges.${host} entry in crew.yaml`);
1701
+ const i = impl(f.entry.token);
1702
+ if (!i.write) throw new ForgeError(`forges.${host} reads its token from ${i.describe(f.entry.token, { host: f.host, stateDir, env })}, which crew cannot write`);
1703
+ if (!token.trim()) throw new ForgeError("empty token");
1704
+ i.write(f.entry.token, token.trim(), { host: f.host, stateDir, env });
1705
+ }
1706
+ function parseCredentialInput(text) {
1707
+ const out = {};
1708
+ for (const line of text.split("\n")) {
1709
+ if (line === "") break;
1710
+ const eq2 = line.indexOf("=");
1711
+ if (eq2 > 0) out[line.slice(0, eq2)] = line.slice(eq2 + 1);
1712
+ }
1713
+ return out;
1714
+ }
1715
+ function answerCredential(action, input, forges, stateDir, env = process.env) {
1716
+ if (action !== "get") return "";
1717
+ if (input.protocol && input.protocol !== "https") return "";
1718
+ if (!input.host) return "";
1719
+ const f = forgeFor(forges, input.host);
1720
+ if (!f) return "";
1721
+ const token = impl(f.entry.token).read(f.entry.token, { host: f.host, stateDir, env });
1722
+ if (!token) return "";
1723
+ return `username=${f.entry.username}
1724
+ password=${token}
1725
+ `;
1726
+ }
1727
+ function gitCredentialEnv(o, base = process.env) {
1728
+ const start = Number.parseInt(base.GIT_CONFIG_COUNT ?? "0", 10) || 0;
1729
+ const helper = `!${o.crewCommand} credential`;
1730
+ for (let i = 0; i < start; i++) {
1731
+ if (base[`GIT_CONFIG_KEY_${i}`] === "credential.helper" && base[`GIT_CONFIG_VALUE_${i}`] === helper) {
1732
+ return { GIT_TERMINAL_PROMPT: "0", ...o.configFile ? { CREW_CONFIG: o.configFile } : {} };
1733
+ }
1734
+ }
1735
+ const out = {
1736
+ GIT_CONFIG_COUNT: String(start + 2),
1737
+ [`GIT_CONFIG_KEY_${start}`]: "credential.helper",
1738
+ [`GIT_CONFIG_VALUE_${start}`]: "",
1739
+ [`GIT_CONFIG_KEY_${start + 1}`]: "credential.helper",
1740
+ [`GIT_CONFIG_VALUE_${start + 1}`]: helper,
1741
+ GIT_TERMINAL_PROMPT: "0"
1742
+ };
1743
+ if (o.configFile) out.CREW_CONFIG = o.configFile;
1744
+ return out;
1745
+ }
1746
+ var shq = (v) => `'${v.replace(/'/g, `'\\''`)}'`;
1747
+ function crewCommandLine() {
1748
+ return [process.execPath, ...process.execArgv, process.argv[1] ?? "crew"].map(shq).join(" ");
1749
+ }
1750
+ function tokenEnvFor(host, token) {
1751
+ const out = { CREW_FORGE_TOKEN: token };
1752
+ if (bareHost(host) === "github.com") {
1753
+ out.GH_TOKEN = token;
1754
+ out.GITHUB_TOKEN = token;
1755
+ }
1756
+ return out;
1757
+ }
1758
+ function forgeRunEnv(forges, o, base = process.env) {
1759
+ const hosts = Object.keys(forges ?? {});
1760
+ if (hosts.length === 0) return {};
1761
+ const out = gitCredentialEnv(o, base);
1762
+ for (const h of hosts) {
1763
+ let token = null;
1764
+ try {
1765
+ token = readForgeToken(forges, h, o.stateDir, base);
1766
+ } catch {
1767
+ }
1768
+ if (token) {
1769
+ Object.assign(out, tokenEnvFor(h, token));
1770
+ break;
1771
+ }
1772
+ }
1773
+ return out;
1774
+ }
1775
+ function parseForges(raw) {
1776
+ if (raw === void 0 || raw === null) return void 0;
1777
+ if (typeof raw !== "object" || Array.isArray(raw)) throw new ForgeError("ship.forges must be a map of host to entry");
1778
+ const out = {};
1779
+ for (const [host, v] of Object.entries(raw)) {
1780
+ const where = `ship.forges.${host}`;
1781
+ if (/[/:@\s]/.test(host)) throw new ForgeError(`${where}: the key must be a bare host name like github.com`);
1782
+ if (!v || typeof v !== "object" || Array.isArray(v)) throw new ForgeError(`${where} must be a map`);
1783
+ const e = v;
1784
+ for (const k of Object.keys(e)) {
1785
+ if (!["username", "token"].includes(k)) throw new ForgeError(`${where}: unknown key "${k}" (have: username, token)`);
1786
+ }
1787
+ const t = e.token;
1788
+ if (!t || typeof t !== "object" || Array.isArray(t)) throw new ForgeError(`${where}.token must be a map with a "source"`);
1789
+ const tok = t;
1790
+ const source = tok.source;
1791
+ let token;
1792
+ if (source === "env") {
1793
+ if (typeof tok.var !== "string" || !tok.var) throw new ForgeError(`${where}.token.var is required for source "env"`);
1794
+ token = { source: "env", var: tok.var };
1795
+ } else if (source === "file") {
1796
+ token = typeof tok.path === "string" && tok.path ? { source: "file", path: tok.path } : { source: "file" };
1797
+ } else if (source === "keychain") {
1798
+ token = typeof tok.service === "string" && tok.service ? { source: "keychain", service: tok.service } : { source: "keychain" };
1799
+ } else {
1800
+ throw new ForgeError(`${where}.token.source must be one of: ${Object.keys(TOKEN_SOURCES).join(", ")}`);
1801
+ }
1802
+ out[host.toLowerCase()] = {
1803
+ username: typeof e.username === "string" && e.username ? e.username : DEFAULT_FORGE_USERNAME,
1804
+ token
1805
+ };
1806
+ }
1807
+ return Object.keys(out).length > 0 ? out : void 0;
1808
+ }
1809
+ function hostOfRemote(url) {
1810
+ if (!url) return null;
1811
+ const m = /^[a-z+]+:\/\/(?:[^@/]*@)?([^/:]+)/i.exec(url) ?? /^[\w.-]+@([\w.-]+):/.exec(url);
1812
+ return m ? m[1].toLowerCase() : null;
1813
+ }
1814
+ function forgeOfRemote(forges, remoteUrl, hasToken) {
1815
+ const host = hostOfRemote(remoteUrl);
1816
+ const f = host ? forgeFor(forges, host) : void 0;
1817
+ return f ? { host: f.host, hasToken: hasToken(f.host) } : void 0;
1818
+ }
1819
+
1584
1820
  // src/config.ts
1585
1821
  import { getSessionStore, hostFromUrl } from "@tablation/client";
1586
1822
 
@@ -1621,8 +1857,8 @@ function explain(ship, required) {
1621
1857
  }
1622
1858
 
1623
1859
  // src/repo-config.ts
1624
- import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
1625
- import { join as join9, basename as basename2 } from "path";
1860
+ import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
1861
+ import { join as join10, basename as basename2 } from "path";
1626
1862
  import { parse } from "yaml";
1627
1863
  var SPEC_VERSION = 1;
1628
1864
  var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
@@ -1706,8 +1942,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
1706
1942
  var PROVIDERS = ["github", "buildkite", "other", "none"];
1707
1943
  function findRepoConfig(dir) {
1708
1944
  for (const name of REPO_CONFIG_FILENAMES) {
1709
- const p = join9(dir, name);
1710
- if (existsSync7(p)) return p;
1945
+ const p = join10(dir, name);
1946
+ if (existsSync8(p)) return p;
1711
1947
  }
1712
1948
  return null;
1713
1949
  }
@@ -1945,7 +2181,7 @@ function parseRepoConfig(text, file) {
1945
2181
  function loadRepoConfig(dir) {
1946
2182
  const file = findRepoConfig(dir);
1947
2183
  if (!file) return null;
1948
- return parseRepoConfig(readFileSync8(file, "utf8"), file);
2184
+ return parseRepoConfig(readFileSync9(file, "utf8"), file);
1949
2185
  }
1950
2186
  function hookLabel(cfg, hook2) {
1951
2187
  return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
@@ -2229,6 +2465,14 @@ function parseCloneScheme(v) {
2229
2465
  if (v === "ssh" || v === "https") return v;
2230
2466
  throw new ConfigError(`ship.cloneScheme must be "ssh" or "https", got ${JSON.stringify(v)}`);
2231
2467
  }
2468
+ function parseShipForges(v) {
2469
+ try {
2470
+ return parseForges(v);
2471
+ } catch (e) {
2472
+ if (e instanceof ForgeError) throw new ConfigError(e.message);
2473
+ throw e;
2474
+ }
2475
+ }
2232
2476
  var SHIP_KEYS = /* @__PURE__ */ new Set([
2233
2477
  "name",
2234
2478
  "platform",
@@ -2256,6 +2500,7 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
2256
2500
  "relayPublicDomain",
2257
2501
  "passengerImage",
2258
2502
  "cloneScheme",
2503
+ "forges",
2259
2504
  "previewHost",
2260
2505
  "maxPeriscopes",
2261
2506
  "periscopeIdleMinutes"
@@ -2406,11 +2651,11 @@ function expand(p, base) {
2406
2651
  return isAbsolute(out) ? out : resolve(base, out);
2407
2652
  }
2408
2653
  function resolvePromptsDir(promptSet, crewHome, base) {
2409
- if (!promptSet) return join10(crewHome, "prompts", "default");
2654
+ if (!promptSet) return join11(crewHome, "prompts", "default");
2410
2655
  if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
2411
2656
  return expand(promptSet, base);
2412
2657
  }
2413
- return join10(crewHome, "prompts", promptSet);
2658
+ return join11(crewHome, "prompts", promptSet);
2414
2659
  }
2415
2660
  var Missing = class {
2416
2661
  paths = [];
@@ -2434,7 +2679,7 @@ var Missing = class {
2434
2679
  };
2435
2680
  function crewVersion(crewHome) {
2436
2681
  try {
2437
- const pkg = JSON.parse(readFileSync9(join10(crewHome, "package.json"), "utf8"));
2682
+ const pkg = JSON.parse(readFileSync10(join11(crewHome, "package.json"), "utf8"));
2438
2683
  return pkg.version ?? "0.0.0";
2439
2684
  } catch {
2440
2685
  return "0.0.0";
@@ -2444,26 +2689,26 @@ function configSearchPath(crewHome) {
2444
2689
  if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
2445
2690
  const xdg = process.env.XDG_CONFIG_HOME;
2446
2691
  return [
2447
- ...xdg ? [join10(xdg, "crew", "crew.yaml")] : [],
2448
- join10(homedir6(), ".config", "crew", "crew.yaml"),
2692
+ ...xdg ? [join11(xdg, "crew", "crew.yaml")] : [],
2693
+ join11(homedir6(), ".config", "crew", "crew.yaml"),
2449
2694
  resolve(crewHome, "crew.yaml")
2450
2695
  ];
2451
2696
  }
2452
2697
  function findConfigFile(crewHome) {
2453
2698
  const candidates = configSearchPath(crewHome);
2454
- return candidates.find((c) => existsSync8(c)) ?? candidates[candidates.length - 1];
2699
+ return candidates.find((c) => existsSync9(c)) ?? candidates[candidates.length - 1];
2455
2700
  }
2456
- var defaultConfigPath = () => join10(process.env.XDG_CONFIG_HOME ?? join10(homedir6(), ".config"), "crew", "crew.yaml");
2701
+ var defaultConfigPath = () => join11(process.env.XDG_CONFIG_HOME ?? join11(homedir6(), ".config"), "crew", "crew.yaml");
2457
2702
  function routeSlug(route) {
2458
2703
  return route.replace(/\//g, "-");
2459
2704
  }
2460
2705
  function resolvedPathFor(stateDir, route) {
2461
2706
  const [workspace, project] = splitRoute(route);
2462
- return join10(stateDir, "resolved", workspace, `${project}.json`);
2707
+ return join11(stateDir, "resolved", workspace, `${project}.json`);
2463
2708
  }
2464
2709
  function apiKeyPathFor(stateDir, route) {
2465
2710
  const [workspace, project] = splitRoute(route);
2466
- return join10(stateDir, "keys", workspace, `${project}.env`);
2711
+ return join11(stateDir, "keys", workspace, `${project}.env`);
2467
2712
  }
2468
2713
  function splitRoute(route) {
2469
2714
  const parts = route.split("/");
@@ -2474,9 +2719,9 @@ function splitRoute(route) {
2474
2719
  }
2475
2720
  function readResolvedFile(stateDir, route) {
2476
2721
  const path = resolvedPathFor(stateDir, route);
2477
- if (!existsSync8(path)) return void 0;
2722
+ if (!existsSync9(path)) return void 0;
2478
2723
  try {
2479
- return JSON.parse(readFileSync9(path, "utf8"));
2724
+ return JSON.parse(readFileSync10(path, "utf8"));
2480
2725
  } catch (e) {
2481
2726
  throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
2482
2727
  }
@@ -2536,23 +2781,23 @@ function parseResolved(raw, m, where) {
2536
2781
  }
2537
2782
  function loadConfig(crewHome, configFile, opts = {}) {
2538
2783
  const file = configFile ?? findConfigFile(crewHome);
2539
- if (opts.text === void 0 && !existsSync8(file)) {
2784
+ if (opts.text === void 0 && !existsSync9(file)) {
2540
2785
  throw new ConfigError(
2541
2786
  `no config found. Looked in:
2542
2787
  ` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
2543
2788
 
2544
2789
  Create the first of those:
2545
- mkdir -p ${dirname3(defaultConfigPath())} && cp ${join10(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
2790
+ mkdir -p ${dirname4(defaultConfigPath())} && cp ${join11(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
2546
2791
  );
2547
2792
  }
2548
2793
  let raw;
2549
2794
  try {
2550
- raw = parse2(opts.text ?? readFileSync9(file, "utf8"));
2795
+ raw = parse2(opts.text ?? readFileSync10(file, "utf8"));
2551
2796
  } catch (e) {
2552
2797
  throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
2553
2798
  }
2554
2799
  if (!raw || typeof raw !== "object") throw new ConfigError(`${file} is empty or not a mapping`);
2555
- const base = dirname3(file);
2800
+ const base = dirname4(file);
2556
2801
  const missing = new Missing();
2557
2802
  rejectUnknownKeys(raw, /* @__PURE__ */ new Set(["ship", "routes"]), "top-level", file);
2558
2803
  const shipRaw = raw.ship ?? {};
@@ -2657,7 +2902,7 @@ Create the first of those:
2657
2902
  useNvm: shipRaw.useNvm !== false,
2658
2903
  nvmSh: shipRaw.nvmSh,
2659
2904
  stateDir,
2660
- logFile: expand(shipRaw.logFile ?? join10(tmpdir(), "crew.log"), base),
2905
+ logFile: expand(shipRaw.logFile ?? join11(tmpdir(), "crew.log"), base),
2661
2906
  userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
2662
2907
  maxConcurrentAgents,
2663
2908
  maxConcurrentAgentsSet,
@@ -2671,6 +2916,7 @@ Create the first of those:
2671
2916
  relayPublicDomain: shipRaw.relayPublicDomain ? String(shipRaw.relayPublicDomain) : void 0,
2672
2917
  passengerImage: shipRaw.passengerImage ? String(shipRaw.passengerImage) : void 0,
2673
2918
  cloneScheme: parseCloneScheme(shipRaw.cloneScheme),
2919
+ forges: parseShipForges(shipRaw.forges),
2674
2920
  previewHost: shipRaw.previewHost ? String(shipRaw.previewHost).trim() || void 0 : void 0,
2675
2921
  maxPeriscopes,
2676
2922
  periscopeIdleMinutes
@@ -2817,7 +3063,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
2817
3063
  const hasRepos = Object.keys(repoDirs).length > 0;
2818
3064
  const reposBasePath = expand(String(c?.reposBasePath ?? raw.ship?.reposBasePath ?? DEFAULT_REPOS_BASE_PATH), base);
2819
3065
  const dirIsCloneBase = !c?.dir && !hasRepos && !Array.isArray(c?.repos);
2820
- const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ? join10(reposBasePath, splitRoute(route)[0]) : "";
3066
+ const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ? join11(reposBasePath, splitRoute(route)[0]) : "";
2821
3067
  const weight = c?.weight === void 0 ? void 0 : Number(c.weight);
2822
3068
  if (weight !== void 0 && !(Number.isFinite(weight) && weight > 0)) {
2823
3069
  missing.add(`${where}.weight must be a positive number (got "${c?.weight}")`);
@@ -2879,7 +3125,7 @@ function locateDir(cfg, dir, commonDirOf) {
2879
3125
  const direct = find(target);
2880
3126
  if (direct) return direct;
2881
3127
  const common = commonDirOf?.(target);
2882
- return common ? find(dirname3(resolve(target, common))) : void 0;
3128
+ return common ? find(dirname4(resolve(target, common))) : void 0;
2883
3129
  }
2884
3130
  function findRoute(cfg, route) {
2885
3131
  if (!route) {
@@ -2914,8 +3160,8 @@ function repoIdForName(r, name) {
2914
3160
  }
2915
3161
  function resolveApiKey(r) {
2916
3162
  if (r.apiKey) return r.apiKey;
2917
- if (r.apiKeyFile && r.apiKeyVar && existsSync8(r.apiKeyFile)) {
2918
- for (const line of readFileSync9(r.apiKeyFile, "utf8").split("\n")) {
3163
+ if (r.apiKeyFile && r.apiKeyVar && existsSync9(r.apiKeyFile)) {
3164
+ for (const line of readFileSync10(r.apiKeyFile, "utf8").split("\n")) {
2919
3165
  const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
2920
3166
  if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
2921
3167
  }
@@ -2928,9 +3174,9 @@ function resolveApiKey(r) {
2928
3174
  }
2929
3175
  function credentialSource(r) {
2930
3176
  if (r.apiKey) return KEYCHAIN_ELIGIBLE_ROUTES.has(r) ? "keychain" : "inline";
2931
- if (r.apiKeyFile && r.apiKeyVar && existsSync8(r.apiKeyFile)) {
3177
+ if (r.apiKeyFile && r.apiKeyVar && existsSync9(r.apiKeyFile)) {
2932
3178
  const re = new RegExp(`^${r.apiKeyVar}=`);
2933
- if (readFileSync9(r.apiKeyFile, "utf8").split("\n").some((l) => re.test(l))) return "apiKeyFile";
3179
+ if (readFileSync10(r.apiKeyFile, "utf8").split("\n").some((l) => re.test(l))) return "apiKeyFile";
2934
3180
  }
2935
3181
  return process.env.CREW_API_KEY?.trim() ? "CREW_API_KEY" : "none";
2936
3182
  }
@@ -2986,7 +3232,7 @@ function configuredMembers(rt) {
2986
3232
  return out;
2987
3233
  }
2988
3234
  function defaultRepoDir(basePath, workspace, repoName) {
2989
- return join10(basePath, workspace, repoName);
3235
+ return join11(basePath, workspace, repoName);
2990
3236
  }
2991
3237
  function reposOf(r) {
2992
3238
  const named = Object.entries(r.repos);
@@ -3019,7 +3265,7 @@ function passengerRepoTargets(r) {
3019
3265
  return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
3020
3266
  }
3021
3267
  function passengerCheckoutDir(basePath, workspace, repoName) {
3022
- return join10(basePath, workspace, ".passenger-checkouts", repoName);
3268
+ return join11(basePath, workspace, ".passenger-checkouts", repoName);
3023
3269
  }
3024
3270
  function passengerCheckoutTargets(r) {
3025
3271
  if (!r.hostPassengers) return [];
@@ -3306,9 +3552,9 @@ function recoveredFailureHeadlines(o, route) {
3306
3552
  }
3307
3553
 
3308
3554
  // src/install.ts
3309
- import { dirname as dirname4, join as join11, extname } from "path";
3555
+ import { dirname as dirname5, join as join12, extname } from "path";
3310
3556
  import { homedir as homedir7 } from "os";
3311
- import { accessSync, constants, existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync2, writeFileSync as writeFileSync6, unlinkSync, realpathSync } from "fs";
3557
+ import { accessSync, constants, existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync11, statSync as statSync3, writeFileSync as writeFileSync7, unlinkSync, realpathSync } from "fs";
3312
3558
  import { spawnSync as spawnSync4 } from "child_process";
3313
3559
  import { createHash as createHash2 } from "crypto";
3314
3560
 
@@ -3320,7 +3566,7 @@ function isCompiledBinary(moduleUrl) {
3320
3566
  // src/install.ts
3321
3567
  function crewInvocation(crewHome) {
3322
3568
  if (isCompiledBinary(import.meta.url)) return [process.execPath];
3323
- return [process.execPath, join11(crewHome, "bin", "crew")];
3569
+ return [process.execPath, join12(crewHome, "bin", "crew")];
3324
3570
  }
3325
3571
  var INTERVAL_SECONDS = 120;
3326
3572
  function labelFor(crewHome, job) {
@@ -3351,36 +3597,36 @@ function schedulerLogFor(crewLog, job) {
3351
3597
  return `${stem}.${suffix}${ext || ".log"}`;
3352
3598
  }
3353
3599
  function pathFor(ship) {
3354
- const nodeBinDir = dirname4(process.execPath);
3600
+ const nodeBinDir = dirname5(process.execPath);
3355
3601
  const parts = [nodeBinDir];
3356
3602
  if (ship.extraPath) parts.push(ship.extraPath);
3357
3603
  parts.push("/usr/bin", "/bin", "/usr/sbin", "/sbin");
3358
3604
  return parts.join(":");
3359
3605
  }
3360
3606
  function findOnPath(bin, searchPath) {
3361
- const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join11(dir, bin));
3607
+ const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join12(dir, bin));
3362
3608
  for (const candidate of candidates) {
3363
3609
  try {
3364
3610
  accessSync(candidate, constants.X_OK);
3365
- if (statSync2(candidate).isFile()) return candidate;
3611
+ if (statSync3(candidate).isFile()) return candidate;
3366
3612
  } catch {
3367
3613
  }
3368
3614
  }
3369
3615
  return void 0;
3370
3616
  }
3371
- var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join11(homedir7(), ".docker", "bin")].join(":");
3617
+ var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join12(homedir7(), ".docker", "bin")].join(":");
3372
3618
  function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
3373
3619
  const schedulerPath = pathFor(ship);
3374
3620
  if (findOnPath("docker", schedulerPath)) return void 0;
3375
3621
  const inShell = findOnPath("docker", shellPath);
3376
- return inShell ? `\`docker\` is not on the scheduler PATH (${schedulerPath}) but is at ${inShell} in your shell; add ${dirname4(inShell)} to ship.extraPath in crew.yaml and run \`crew install\`` : `\`docker\` is not on the scheduler PATH (${schedulerPath}) or your shell PATH; install Docker, then add its directory to ship.extraPath in crew.yaml and run \`crew install\``;
3622
+ return inShell ? `\`docker\` is not on the scheduler PATH (${schedulerPath}) but is at ${inShell} in your shell; add ${dirname5(inShell)} to ship.extraPath in crew.yaml and run \`crew install\`` : `\`docker\` is not on the scheduler PATH (${schedulerPath}) or your shell PATH; install Docker, then add its directory to ship.extraPath in crew.yaml and run \`crew install\``;
3377
3623
  }
3378
3624
  function xmlEscape(s) {
3379
3625
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3380
3626
  }
3381
3627
  function planLaunchd(ship, crewHome, job) {
3382
3628
  const label = labelFor(crewHome, job);
3383
- const unitPath = join11(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3629
+ const unitPath = join12(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3384
3630
  const crewLog = ship.logFile;
3385
3631
  const schedulerLog = schedulerLogFor(crewLog, job);
3386
3632
  const persistent = job === "run";
@@ -3434,8 +3680,8 @@ ${scheduleKeys}
3434
3680
  }
3435
3681
  function planSystemd(ship, crewHome, job) {
3436
3682
  const label = labelFor(crewHome, job);
3437
- const unitDir = join11(homedir7(), ".config", "systemd", "user");
3438
- const servicePath = join11(unitDir, `${label}.service`);
3683
+ const unitDir = join12(homedir7(), ".config", "systemd", "user");
3684
+ const servicePath = join12(unitDir, `${label}.service`);
3439
3685
  const crewLog = ship.logFile;
3440
3686
  const schedulerLog = schedulerLogFor(crewLog, job);
3441
3687
  const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
@@ -3475,7 +3721,7 @@ WantedBy=default.target
3475
3721
  schedulerLog
3476
3722
  };
3477
3723
  }
3478
- const timerPath = join11(unitDir, `${label}.timer`);
3724
+ const timerPath = join12(unitDir, `${label}.timer`);
3479
3725
  const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
3480
3726
  [Unit]
3481
3727
  Description=${description}
@@ -3549,7 +3795,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3549
3795
  function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3550
3796
  const label = labelFor(crewHome, job);
3551
3797
  if (hostShip === "macos") {
3552
- const unitPath = join11(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3798
+ const unitPath = join12(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3553
3799
  return {
3554
3800
  mechanism: "launchd",
3555
3801
  unitPaths: [unitPath],
@@ -3559,18 +3805,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3559
3805
  };
3560
3806
  }
3561
3807
  if (hostShip === "linux" && hasSystemd) {
3562
- const unitDir = join11(homedir7(), ".config", "systemd", "user");
3808
+ const unitDir = join12(homedir7(), ".config", "systemd", "user");
3563
3809
  if (job === "run") {
3564
3810
  return {
3565
3811
  mechanism: "systemd",
3566
- unitPaths: [join11(unitDir, `${label}.service`)],
3812
+ unitPaths: [join12(unitDir, `${label}.service`)],
3567
3813
  crontabLine: null,
3568
3814
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
3569
3815
  };
3570
3816
  }
3571
3817
  return {
3572
3818
  mechanism: "systemd",
3573
- unitPaths: [join11(unitDir, `${label}.service`), join11(unitDir, `${label}.timer`)],
3819
+ unitPaths: [join12(unitDir, `${label}.service`), join12(unitDir, `${label}.timer`)],
3574
3820
  crontabLine: null,
3575
3821
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
3576
3822
  };
@@ -3588,7 +3834,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3588
3834
  function planDaemonControl(crewHome, hostShip, hasSystemd) {
3589
3835
  const label = labelFor(crewHome, "run");
3590
3836
  if (hostShip === "macos") {
3591
- const unitPath = join11(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3837
+ const unitPath = join12(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3592
3838
  return {
3593
3839
  mechanism: "launchd",
3594
3840
  startCommand: ["launchctl", "load", unitPath],
@@ -3628,8 +3874,8 @@ async function applyInstall(plan, crewHome, dryRun, log) {
3628
3874
  log.emit(`would write ${path}`);
3629
3875
  continue;
3630
3876
  }
3631
- mkdirSync5(dirname4(path), { recursive: true });
3632
- writeFileSync6(path, plan.unitContent[path]);
3877
+ mkdirSync6(dirname5(path), { recursive: true });
3878
+ writeFileSync7(path, plan.unitContent[path]);
3633
3879
  log.emit(`wrote ${path}`);
3634
3880
  }
3635
3881
  if (plan.crontabLine) {
@@ -3683,7 +3929,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3683
3929
  }
3684
3930
  }
3685
3931
  for (const path of plan.unitPaths) {
3686
- if (!existsSync9(path)) continue;
3932
+ if (!existsSync10(path)) continue;
3687
3933
  if (dryRun) {
3688
3934
  log.emit(`would remove ${path}`);
3689
3935
  continue;
@@ -3702,7 +3948,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3702
3948
  }
3703
3949
  var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
3704
3950
  function launchAgentsDir() {
3705
- return join11(homedir7(), "Library", "LaunchAgents");
3951
+ return join12(homedir7(), "Library", "LaunchAgents");
3706
3952
  }
3707
3953
  function xmlUnescape(s) {
3708
3954
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
@@ -3719,25 +3965,25 @@ function loadedCrewLabels() {
3719
3965
  return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
3720
3966
  }
3721
3967
  function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
3722
- const files = existsSync9(dir) ? readdirSync3(dir) : [];
3968
+ const files = existsSync10(dir) ? readdirSync3(dir) : [];
3723
3969
  const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
3724
- const plistPath = join11(dir, f);
3970
+ const plistPath = join12(dir, f);
3725
3971
  let program = null;
3726
3972
  try {
3727
- program = programFromPlist(readFileSync10(plistPath, "utf8"));
3973
+ program = programFromPlist(readFileSync11(plistPath, "utf8"));
3728
3974
  } catch {
3729
3975
  }
3730
3976
  return {
3731
3977
  label: f.slice(0, -".plist".length),
3732
3978
  plistPath,
3733
3979
  program,
3734
- programExists: program !== null && existsSync9(program)
3980
+ programExists: program !== null && existsSync10(program)
3735
3981
  };
3736
3982
  });
3737
3983
  const seen = new Set(units.map((u) => u.label));
3738
3984
  for (const label of [...loaded].sort()) {
3739
3985
  if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
3740
- units.push({ label, plistPath: join11(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
3986
+ units.push({ label, plistPath: join12(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
3741
3987
  }
3742
3988
  return units;
3743
3989
  }
@@ -3750,9 +3996,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
3750
3996
  log.emit(`would unload and remove ${unit.label}`);
3751
3997
  return;
3752
3998
  }
3753
- if (existsSync9(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3999
+ if (existsSync10(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3754
4000
  spawnSync4("launchctl", ["remove", unit.label], { encoding: "utf8" });
3755
- if (existsSync9(unit.plistPath)) unlinkSync(unit.plistPath);
4001
+ if (existsSync10(unit.plistPath)) unlinkSync(unit.plistPath);
3756
4002
  log.emit(`removed ${unit.label} (${unit.plistPath})`);
3757
4003
  }
3758
4004
  function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
@@ -3830,24 +4076,24 @@ function detectSystemd() {
3830
4076
  }
3831
4077
 
3832
4078
  // src/ship-attention.ts
3833
- import { existsSync as existsSync11, statfsSync } from "fs";
4079
+ import { existsSync as existsSync12, statfsSync } from "fs";
3834
4080
 
3835
4081
  // src/git.ts
3836
- import { execFileSync } from "child_process";
4082
+ import { execFileSync as execFileSync2 } from "child_process";
3837
4083
  import {
3838
- existsSync as existsSync10,
3839
- mkdirSync as mkdirSync6,
4084
+ existsSync as existsSync11,
4085
+ mkdirSync as mkdirSync7,
3840
4086
  openSync as openSync2,
3841
4087
  closeSync as closeSync2,
3842
4088
  writeSync,
3843
- statSync as statSync3,
4089
+ statSync as statSync4,
3844
4090
  unlinkSync as unlinkSync2,
3845
- readFileSync as readFileSync11,
3846
- writeFileSync as writeFileSync7,
4091
+ readFileSync as readFileSync12,
4092
+ writeFileSync as writeFileSync8,
3847
4093
  readdirSync as readdirSync4,
3848
4094
  rmSync as rmSync6
3849
4095
  } from "fs";
3850
- import { dirname as dirname5, basename as basename4, join as join12 } from "path";
4096
+ import { dirname as dirname6, basename as basename4, join as join13 } from "path";
3851
4097
  var GitError = class extends Error {
3852
4098
  };
3853
4099
  function streamText(x) {
@@ -3855,7 +4101,7 @@ function streamText(x) {
3855
4101
  }
3856
4102
  function gitRaw(cwd, args) {
3857
4103
  try {
3858
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
4104
+ return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
3859
4105
  } catch (e) {
3860
4106
  const err = e;
3861
4107
  const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
@@ -3864,7 +4110,7 @@ function gitRaw(cwd, args) {
3864
4110
  }
3865
4111
  function git(cwd, args) {
3866
4112
  try {
3867
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
4113
+ return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
3868
4114
  } catch (e) {
3869
4115
  const err = e;
3870
4116
  const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
@@ -4035,9 +4281,9 @@ function keyCommitsInRange(cwd, key, range) {
4035
4281
  }
4036
4282
  function patchIdOf(cwd, from, to) {
4037
4283
  try {
4038
- const diff = execFileSync("git", ["diff", from, to], { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
4284
+ const diff = execFileSync2("git", ["diff", from, to], { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
4039
4285
  if (!diff) return null;
4040
- const out = execFileSync("git", ["patch-id", "--stable"], { cwd, encoding: "utf8", input: diff });
4286
+ const out = execFileSync2("git", ["patch-id", "--stable"], { cwd, encoding: "utf8", input: diff });
4041
4287
  return out.split(" ")[0]?.trim() || null;
4042
4288
  } catch {
4043
4289
  return null;
@@ -4227,7 +4473,7 @@ function recoverStrandedReleaseCheckouts(cwd) {
4227
4473
  if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
4228
4474
  else if (line.startsWith("branch refs/heads/crew/release-") && path) {
4229
4475
  gitOk(cwd, ["worktree", "remove", "--force", path]);
4230
- const root = dirname5(path);
4476
+ const root = dirname6(path);
4231
4477
  if (basename4(root).startsWith("crew-release-")) rmSync6(root, { recursive: true, force: true });
4232
4478
  }
4233
4479
  }
@@ -4245,11 +4491,11 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
4245
4491
  }
4246
4492
  for (const name of names) {
4247
4493
  if (!name.startsWith("crew-release-")) continue;
4248
- const root = join12(tmp, name);
4494
+ const root = join13(tmp, name);
4249
4495
  try {
4250
- const st = statSync3(root);
4496
+ const st = statSync4(root);
4251
4497
  if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
4252
- if (existsSync10(join12(root, "checkout"))) continue;
4498
+ if (existsSync11(join13(root, "checkout"))) continue;
4253
4499
  rmSync6(root, { recursive: true, force: true });
4254
4500
  removed.push(name);
4255
4501
  } catch {
@@ -4282,9 +4528,13 @@ function worktrees(cwd) {
4282
4528
  if (current.path) list.push({ path: current.path, branch: current.branch ?? null, prunable: current.prunable ?? false });
4283
4529
  return list;
4284
4530
  }
4531
+ var tokenHosts = /* @__PURE__ */ new Set();
4532
+ function setForgeTokenHosts(hosts) {
4533
+ tokenHosts = new Set([...hosts].map((h) => h.toLowerCase()));
4534
+ }
4285
4535
  function cloneUrlFor(remote, opts = {}) {
4286
4536
  const origin = opts.originUrl?.trim() ?? "";
4287
- const scheme = /^https?:\/\//i.test(origin) ? "https" : /^(git@|ssh:\/\/)/i.test(origin) ? "ssh" : opts.scheme ?? "ssh";
4537
+ const scheme = tokenHosts.has("github.com") ? "https" : /^https?:\/\//i.test(origin) ? "https" : /^(git@|ssh:\/\/)/i.test(origin) ? "ssh" : opts.scheme ?? "ssh";
4288
4538
  return scheme === "https" ? `https://github.com/${remote}.git` : `git@github.com:${remote}.git`;
4289
4539
  }
4290
4540
  function originUrlOf(dir, remoteName) {
@@ -4295,16 +4545,16 @@ function originUrlOf(dir, remoteName) {
4295
4545
  }
4296
4546
  }
4297
4547
  function ensureRepoCheckout(dir, remote, opts = {}) {
4298
- if (existsSync10(`${dir}/.git`)) return false;
4548
+ if (existsSync11(`${dir}/.git`)) return false;
4299
4549
  if (!remote) {
4300
4550
  throw new GitError(
4301
4551
  `${dir} does not exist and this repo has no discovered remote to clone from \u2014 re-run \`crew connect\`, or check out the repository yourself and point \`repos:\` at it`
4302
4552
  );
4303
4553
  }
4304
- mkdirSync6(dirname5(dir), { recursive: true });
4554
+ mkdirSync7(dirname6(dir), { recursive: true });
4305
4555
  const url = cloneUrlFor(remote, opts);
4306
4556
  try {
4307
- execFileSync("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
4557
+ execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
4308
4558
  } catch (e) {
4309
4559
  const err = e;
4310
4560
  const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
@@ -4326,7 +4576,7 @@ function sleepSync(ms3) {
4326
4576
  }
4327
4577
  function acquireCheckoutLock(dir, timeoutMs = 3e4) {
4328
4578
  const lockPath = checkoutLockPath(dir);
4329
- mkdirSync6(dirname5(lockPath), { recursive: true });
4579
+ mkdirSync7(dirname6(lockPath), { recursive: true });
4330
4580
  const deadline = Date.now() + timeoutMs;
4331
4581
  for (; ; ) {
4332
4582
  try {
@@ -4344,7 +4594,7 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
4344
4594
  const err = e;
4345
4595
  if (err.code !== "EEXIST") throw err;
4346
4596
  try {
4347
- if (Date.now() - statSync3(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
4597
+ if (Date.now() - statSync4(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
4348
4598
  try {
4349
4599
  unlinkSync2(lockPath);
4350
4600
  } catch {
@@ -4363,17 +4613,17 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
4363
4613
  function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
4364
4614
  const release = acquireCheckoutLock(dir);
4365
4615
  try {
4366
- if (!existsSync10(`${dir}/.git`) && githubRemote) {
4616
+ if (!existsSync11(`${dir}/.git`) && githubRemote) {
4367
4617
  const url = cloneUrlFor(githubRemote, clone);
4368
4618
  const marker = cloneFailureMarker(dir);
4369
- if (existsSync10(marker) && readFileSync11(marker, "utf8").split("\n")[0] === url) {
4370
- return { action: "clone-failed", detail: readFileSync11(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
4619
+ if (existsSync11(marker) && readFileSync12(marker, "utf8").split("\n")[0] === url) {
4620
+ return { action: "clone-failed", detail: readFileSync12(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
4371
4621
  }
4372
4622
  try {
4373
4623
  ensureRepoCheckout(dir, githubRemote, clone);
4374
4624
  } catch (e) {
4375
4625
  if (!(e instanceof GitError)) throw e;
4376
- writeFileSync7(marker, `${url}
4626
+ writeFileSync8(marker, `${url}
4377
4627
  ${e.message}`);
4378
4628
  return { action: "clone-failed", detail: e.message, repeat: false };
4379
4629
  }
@@ -4580,7 +4830,7 @@ function noteHeldWorktree(state, h, nowMs = Date.now()) {
4580
4830
  function reconcileHeldWorktrees(state, repo, log, stillRefused) {
4581
4831
  for (const i of state.shipAttention().previous()) {
4582
4832
  if (i.kind !== "worktree_held" || !i.held || i.held.repo !== repo) continue;
4583
- const gone = !existsSync11(i.held.path);
4833
+ const gone = !existsSync12(i.held.path);
4584
4834
  if (!gone && !(stillRefused && !stillRefused.has(i.held.path))) continue;
4585
4835
  clearShipAttention(state, i.key);
4586
4836
  log.emit(
@@ -4706,10 +4956,10 @@ function attentionProblems(items, already) {
4706
4956
  }
4707
4957
 
4708
4958
  // src/doctor-checks.ts
4709
- import { execFileSync as execFileSync2 } from "child_process";
4959
+ import { execFileSync as execFileSync3 } from "child_process";
4710
4960
  var PROBE_BRANCH = "crew-probe-branch";
4711
4961
  var realGit = (cwd, args, env) => {
4712
- execFileSync2("git", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
4962
+ execFileSync3("git", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
4713
4963
  };
4714
4964
  function isSshRemote(url) {
4715
4965
  return /^ssh:\/\//.test(url) || /^[\w.-]+@[\w.-]+:/.test(url);
@@ -4727,7 +4977,30 @@ function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
4727
4977
  } catch (e) {
4728
4978
  const err = e;
4729
4979
  const raw = (err.stderr ? String(err.stderr) : err.message).trim().split("\n").filter(Boolean).slice(-2).join(" / ");
4730
- return { state: "failed", detail: raw || "push dry-run failed", hint: pushAuthHint(o.remoteUrl) };
4980
+ const detail = raw || "push dry-run failed";
4981
+ const f = o.forge;
4982
+ if (!f) return { state: "failed", detail, hint: pushAuthHint(o.remoteUrl) };
4983
+ if (!f.hasToken) {
4984
+ return {
4985
+ state: "failed",
4986
+ detail: `no token for forge ${f.host}: ${detail}`,
4987
+ hint: `set one with \`crew forge set ${f.host}\` (forges.${f.host} in crew.yaml)`
4988
+ };
4989
+ }
4990
+ try {
4991
+ run2(o.dir, ["ls-remote", "--exit-code", o.remote, "HEAD"], env);
4992
+ return {
4993
+ state: "failed",
4994
+ detail: `the token for forge ${f.host} is read-only (fetch works, push refused): ${detail}`,
4995
+ hint: `replace it with one that can write contents: \`crew forge rotate ${f.host}\``
4996
+ };
4997
+ } catch {
4998
+ return {
4999
+ state: "failed",
5000
+ detail: `the token for forge ${f.host} was refused: ${detail}`,
5001
+ hint: `check that it is current and has access to this repository: \`crew forge rotate ${f.host}\``
5002
+ };
5003
+ }
4731
5004
  }
4732
5005
  }
4733
5006
  var INSTALL_HINTS = {
@@ -4808,30 +5081,30 @@ function roleCapWarnings(ship) {
4808
5081
 
4809
5082
  // src/selftest.ts
4810
5083
  import { spawn as spawn3 } from "child_process";
4811
- import { existsSync as existsSync20, mkdirSync as mkdirSync11, mkdtempSync, readFileSync as readFileSync18, rmSync as rmSync10, writeFileSync as writeFileSync12 } from "fs";
5084
+ import { existsSync as existsSync21, mkdirSync as mkdirSync12, mkdtempSync, readFileSync as readFileSync19, rmSync as rmSync10, writeFileSync as writeFileSync13 } from "fs";
4812
5085
  import { tmpdir as tmpdir2 } from "os";
4813
- import { join as join21 } from "path";
5086
+ import { join as join22 } from "path";
4814
5087
 
4815
5088
  // src/agent.ts
4816
5089
  import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
4817
- import { readFileSync as readFileSync17, existsSync as existsSync19, statSync as statSync5, mkdirSync as mkdirSync10, writeFileSync as writeFileSync11, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
5090
+ import { readFileSync as readFileSync18, existsSync as existsSync20, statSync as statSync6, mkdirSync as mkdirSync11, writeFileSync as writeFileSync12, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
4818
5091
  import { StringDecoder } from "string_decoder";
4819
5092
  import { finished } from "stream/promises";
4820
- import { join as join20, dirname as dirname7, basename as basename7 } from "path";
5093
+ import { join as join21, dirname as dirname8, basename as basename7 } from "path";
4821
5094
  import { createHash as createHash3 } from "crypto";
4822
5095
 
4823
5096
  // src/state.ts
4824
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync7, rmSync as rmSync7, statSync as statSync4, readdirSync as readdirSync5 } from "fs";
4825
- import { join as join13 } from "path";
5097
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, existsSync as existsSync13, mkdirSync as mkdirSync8, rmSync as rmSync7, statSync as statSync5, readdirSync as readdirSync5 } from "fs";
5098
+ import { join as join14 } from "path";
4826
5099
  var EPOCH = "1970-01-01T00:00:00.000Z";
4827
5100
  var State = class _State {
4828
5101
  dir;
4829
5102
  constructor(dir) {
4830
5103
  this.dir = dir;
4831
- mkdirSync7(dir, { recursive: true });
5104
+ mkdirSync8(dir, { recursive: true });
4832
5105
  }
4833
5106
  path(...parts) {
4834
- return join13(this.dir, ...parts);
5107
+ return join14(this.dir, ...parts);
4835
5108
  }
4836
5109
  /**
4837
5110
  * The timestamp every "new since last poll" check is measured against.
@@ -4842,14 +5115,14 @@ var State = class _State {
4842
5115
  */
4843
5116
  watermark() {
4844
5117
  try {
4845
- const v = readFileSync12(this.path(".poll-watermark"), "utf8").trim();
5118
+ const v = readFileSync13(this.path(".poll-watermark"), "utf8").trim();
4846
5119
  return v || EPOCH;
4847
5120
  } catch {
4848
5121
  return EPOCH;
4849
5122
  }
4850
5123
  }
4851
5124
  setWatermark(at) {
4852
- writeFileSync8(this.path(".poll-watermark"), `${at}
5125
+ writeFileSync9(this.path(".poll-watermark"), `${at}
4853
5126
  `);
4854
5127
  }
4855
5128
  /**
@@ -4867,16 +5140,16 @@ var State = class _State {
4867
5140
  }
4868
5141
  /** Whole-crew pause, and the per-role sentinels one level down. */
4869
5142
  isPaused() {
4870
- return existsSync12(this.path(".crew-paused"));
5143
+ return existsSync13(this.path(".crew-paused"));
4871
5144
  }
4872
5145
  isRolePaused(role) {
4873
- return existsSync12(this.path(`.role-paused-${role}`));
5146
+ return existsSync13(this.path(`.role-paused-${role}`));
4874
5147
  }
4875
5148
  pausedRoles(roles) {
4876
5149
  return new Set(roles.filter((r) => this.isRolePaused(r)));
4877
5150
  }
4878
5151
  pause(role) {
4879
- writeFileSync8(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
5152
+ writeFileSync9(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
4880
5153
  }
4881
5154
  resume(role) {
4882
5155
  rmSync7(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
@@ -4887,10 +5160,10 @@ var State = class _State {
4887
5160
  * while agents keep working.
4888
5161
  */
4889
5162
  isReleasePaused() {
4890
- return existsSync12(this.path(".release-paused"));
5163
+ return existsSync13(this.path(".release-paused"));
4891
5164
  }
4892
5165
  setReleasePaused(paused) {
4893
- if (paused) writeFileSync8(this.path(".release-paused"), "");
5166
+ if (paused) writeFileSync9(this.path(".release-paused"), "");
4894
5167
  else rmSync7(this.path(".release-paused"), { force: true });
4895
5168
  }
4896
5169
  /**
@@ -4928,7 +5201,7 @@ var State = class _State {
4928
5201
  const name = _State.safe(rawName);
4929
5202
  const file = this.path(`.${name}.lock`);
4930
5203
  try {
4931
- const existing = Number.parseInt(readFileSync12(file, "utf8").trim(), 10);
5204
+ const existing = Number.parseInt(readFileSync13(file, "utf8").trim(), 10);
4932
5205
  if (Number.isFinite(existing) && existing > 0) {
4933
5206
  try {
4934
5207
  process.kill(existing, 0);
@@ -4938,7 +5211,7 @@ var State = class _State {
4938
5211
  }
4939
5212
  } catch {
4940
5213
  }
4941
- writeFileSync8(file, `${process.pid}
5214
+ writeFileSync9(file, `${process.pid}
4942
5215
  `);
4943
5216
  return {
4944
5217
  ok: true,
@@ -4966,7 +5239,7 @@ var State = class _State {
4966
5239
  if (!/^\.release-.+\.lock$/.test(n)) continue;
4967
5240
  let pid = 0;
4968
5241
  try {
4969
- pid = Number.parseInt(readFileSync12(this.path(n), "utf8").trim(), 10);
5242
+ pid = Number.parseInt(readFileSync13(this.path(n), "utf8").trim(), 10);
4970
5243
  } catch {
4971
5244
  continue;
4972
5245
  }
@@ -5052,7 +5325,7 @@ var State = class _State {
5052
5325
  ticketInFlight(recordId) {
5053
5326
  const file = this.path(`.${_State.safe(`crew-ticket-${recordId}`)}.lock`);
5054
5327
  try {
5055
- const pid = Number.parseInt(readFileSync12(file, "utf8").trim(), 10);
5328
+ const pid = Number.parseInt(readFileSync13(file, "utf8").trim(), 10);
5056
5329
  if (!Number.isFinite(pid) || pid <= 0) return false;
5057
5330
  process.kill(pid, 0);
5058
5331
  return true;
@@ -5086,7 +5359,7 @@ var State = class _State {
5086
5359
  const heldReported = `.release-held-${suffix}`;
5087
5360
  const read = (f) => {
5088
5361
  try {
5089
- return readFileSync12(this.path(f), "utf8").trim() || null;
5362
+ return readFileSync13(this.path(f), "utf8").trim() || null;
5090
5363
  } catch {
5091
5364
  return null;
5092
5365
  }
@@ -5095,7 +5368,7 @@ var State = class _State {
5095
5368
  blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
5096
5369
  noteBlocked: () => {
5097
5370
  const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
5098
- writeFileSync8(this.path(blocked), `${n}
5371
+ writeFileSync9(this.path(blocked), `${n}
5099
5372
  `);
5100
5373
  return n;
5101
5374
  },
@@ -5112,7 +5385,7 @@ var State = class _State {
5112
5385
  */
5113
5386
  deployFailedSha: () => read(failed),
5114
5387
  noteDeployFailed: (sha) => {
5115
- writeFileSync8(this.path(failed), `${sha}
5388
+ writeFileSync9(this.path(failed), `${sha}
5116
5389
  `);
5117
5390
  },
5118
5391
  clearDeployFailed: () => {
@@ -5126,7 +5399,7 @@ var State = class _State {
5126
5399
  noteTestGateFailed: (sha) => {
5127
5400
  const [prev, n] = (read(gateFailed) ?? "").split(" ");
5128
5401
  const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
5129
- writeFileSync8(this.path(gateFailed), `${sha} ${next}
5402
+ writeFileSync9(this.path(gateFailed), `${sha} ${next}
5130
5403
  `);
5131
5404
  return next;
5132
5405
  },
@@ -5136,13 +5409,13 @@ var State = class _State {
5136
5409
  /** Whether the board was already told about this head's red gate. */
5137
5410
  heldReported: () => {
5138
5411
  try {
5139
- return readFileSync12(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
5412
+ return readFileSync13(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
5140
5413
  } catch {
5141
5414
  return null;
5142
5415
  }
5143
5416
  },
5144
5417
  noteHeldReported: (signature) => {
5145
- writeFileSync8(this.path(heldReported), `${signature}
5418
+ writeFileSync9(this.path(heldReported), `${signature}
5146
5419
  `);
5147
5420
  },
5148
5421
  clearHeldReported: () => {
@@ -5150,7 +5423,7 @@ var State = class _State {
5150
5423
  },
5151
5424
  testGateReported: (sha) => read(gateReported) === sha,
5152
5425
  noteTestGateReported: (sha) => {
5153
- writeFileSync8(this.path(gateReported), `${sha}
5426
+ writeFileSync9(this.path(gateReported), `${sha}
5154
5427
  `);
5155
5428
  }
5156
5429
  };
@@ -5173,7 +5446,7 @@ var State = class _State {
5173
5446
  const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
5174
5447
  const read = (f) => {
5175
5448
  try {
5176
- return readFileSync12(this.path(f), "utf8").trim() || null;
5449
+ return readFileSync13(this.path(f), "utf8").trim() || null;
5177
5450
  } catch {
5178
5451
  return null;
5179
5452
  }
@@ -5206,18 +5479,18 @@ var State = class _State {
5206
5479
  clear2();
5207
5480
  return;
5208
5481
  }
5209
- writeFileSync8(this.path(streakFile), `${this.readInt(streakFile) + 1}
5482
+ writeFileSync9(this.path(streakFile), `${this.readInt(streakFile) + 1}
5210
5483
  `);
5211
5484
  const prior = read(ticketFile);
5212
5485
  if (topTicket && topTicket === prior) {
5213
- writeFileSync8(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
5486
+ writeFileSync9(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
5214
5487
  `);
5215
5488
  } else if (topTicket) {
5216
- writeFileSync8(this.path(ticketFile), `${topTicket}
5489
+ writeFileSync9(this.path(ticketFile), `${topTicket}
5217
5490
  `);
5218
- writeFileSync8(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
5491
+ writeFileSync9(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
5219
5492
  `);
5220
- writeFileSync8(this.path(ticketStreakFile), "1\n");
5493
+ writeFileSync9(this.path(ticketStreakFile), "1\n");
5221
5494
  }
5222
5495
  }
5223
5496
  };
@@ -5238,14 +5511,14 @@ var State = class _State {
5238
5511
  return {
5239
5512
  previous: () => {
5240
5513
  try {
5241
- const parsed = JSON.parse(readFileSync12(this.path(file), "utf8"));
5514
+ const parsed = JSON.parse(readFileSync13(this.path(file), "utf8"));
5242
5515
  return parsed && typeof parsed === "object" ? parsed : {};
5243
5516
  } catch {
5244
5517
  return {};
5245
5518
  }
5246
5519
  },
5247
5520
  persist: (next) => {
5248
- writeFileSync8(this.path(file), JSON.stringify(next));
5521
+ writeFileSync9(this.path(file), JSON.stringify(next));
5249
5522
  }
5250
5523
  };
5251
5524
  }
@@ -5261,14 +5534,14 @@ var State = class _State {
5261
5534
  return {
5262
5535
  previous: () => {
5263
5536
  try {
5264
- const parsed = JSON.parse(readFileSync12(this.path(file), "utf8"));
5537
+ const parsed = JSON.parse(readFileSync13(this.path(file), "utf8"));
5265
5538
  return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [];
5266
5539
  } catch {
5267
5540
  return [];
5268
5541
  }
5269
5542
  },
5270
5543
  persist: (next) => {
5271
- writeFileSync8(this.path(file), JSON.stringify(next));
5544
+ writeFileSync9(this.path(file), JSON.stringify(next));
5272
5545
  }
5273
5546
  };
5274
5547
  }
@@ -5282,14 +5555,14 @@ var State = class _State {
5282
5555
  return {
5283
5556
  previous: () => {
5284
5557
  try {
5285
- const parsed = JSON.parse(readFileSync12(this.path(file), "utf8"));
5558
+ const parsed = JSON.parse(readFileSync13(this.path(file), "utf8"));
5286
5559
  return Array.isArray(parsed) ? parsed : [];
5287
5560
  } catch {
5288
5561
  return [];
5289
5562
  }
5290
5563
  },
5291
5564
  persist: (next) => {
5292
- writeFileSync8(this.path(file), JSON.stringify(next));
5565
+ writeFileSync9(this.path(file), JSON.stringify(next));
5293
5566
  }
5294
5567
  };
5295
5568
  }
@@ -5302,11 +5575,11 @@ var State = class _State {
5302
5575
  const file = ".release-last-run";
5303
5576
  return {
5304
5577
  touch: () => {
5305
- writeFileSync8(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
5578
+ writeFileSync9(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
5306
5579
  },
5307
5580
  at: () => {
5308
5581
  try {
5309
- return statSync4(this.path(file)).mtimeMs;
5582
+ return statSync5(this.path(file)).mtimeMs;
5310
5583
  } catch {
5311
5584
  return null;
5312
5585
  }
@@ -5315,7 +5588,7 @@ var State = class _State {
5315
5588
  }
5316
5589
  readInt(file) {
5317
5590
  try {
5318
- return Number.parseInt(readFileSync12(this.path(file), "utf8").trim(), 10) || 0;
5591
+ return Number.parseInt(readFileSync13(this.path(file), "utf8").trim(), 10) || 0;
5319
5592
  } catch {
5320
5593
  return 0;
5321
5594
  }
@@ -5798,8 +6071,8 @@ var PERISCOPE_COLUMNS = [
5798
6071
  ];
5799
6072
 
5800
6073
  // src/agent-log.ts
5801
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync6, readFileSync as readFileSync13, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "fs";
5802
- import { join as join14 } from "path";
6074
+ import { existsSync as existsSync14, mkdirSync as mkdirSync9, readdirSync as readdirSync6, readFileSync as readFileSync14, rmSync as rmSync8, writeFileSync as writeFileSync10 } from "fs";
6075
+ import { join as join15 } from "path";
5803
6076
 
5804
6077
  // src/pending-backoff.ts
5805
6078
  var sleepBackoff = (attempts) => Math.min(6e4 * 2 ** Math.max(0, attempts - 1), 30 * 6e4);
@@ -5971,20 +6244,20 @@ async function addRunToTicket(target, t, recordId, run2) {
5971
6244
  };
5972
6245
  }
5973
6246
  }
5974
- var queueDir = (stateDir) => join14(stateDir, "pending-agent-logs");
6247
+ var queueDir = (stateDir) => join15(stateDir, "pending-agent-logs");
5975
6248
  function queueAgentRun(stateDir, name, q) {
5976
- mkdirSync8(queueDir(stateDir), { recursive: true });
5977
- writeFileSync9(join14(queueDir(stateDir), `${name}.json`), JSON.stringify(q));
6249
+ mkdirSync9(queueDir(stateDir), { recursive: true });
6250
+ writeFileSync10(join15(queueDir(stateDir), `${name}.json`), JSON.stringify(q));
5978
6251
  }
5979
6252
  async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__PURE__ */ new Date()) {
5980
6253
  const dir = queueDir(stateDir);
5981
- if (!existsSync13(dir)) return 0;
6254
+ if (!existsSync14(dir)) return 0;
5982
6255
  let sent = 0;
5983
6256
  for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
5984
- const path = join14(dir, f);
6257
+ const path = join15(dir, f);
5985
6258
  let q;
5986
6259
  try {
5987
- q = JSON.parse(readFileSync13(path, "utf8"));
6260
+ q = JSON.parse(readFileSync14(path, "utf8"));
5988
6261
  } catch {
5989
6262
  continue;
5990
6263
  }
@@ -6013,7 +6286,7 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
6013
6286
  continue;
6014
6287
  }
6015
6288
  const attempts = q.attempts + 1;
6016
- writeFileSync9(path, JSON.stringify({
6289
+ writeFileSync10(path, JSON.stringify({
6017
6290
  ...q,
6018
6291
  attempts,
6019
6292
  warned: true,
@@ -6024,20 +6297,20 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
6024
6297
  }
6025
6298
  return sent;
6026
6299
  }
6027
- var countersDir = (stateDir) => join14(stateDir, "pending-ticket-counters");
6300
+ var countersDir = (stateDir) => join15(stateDir, "pending-ticket-counters");
6028
6301
  function queueTicketCounters(stateDir, name, q) {
6029
- mkdirSync8(countersDir(stateDir), { recursive: true });
6030
- writeFileSync9(join14(countersDir(stateDir), `${name}.json`), JSON.stringify(q));
6302
+ mkdirSync9(countersDir(stateDir), { recursive: true });
6303
+ writeFileSync10(join15(countersDir(stateDir), `${name}.json`), JSON.stringify(q));
6031
6304
  }
6032
6305
  async function replayQueuedTicketCounters(stateDir, route, keyFor, log, now = /* @__PURE__ */ new Date()) {
6033
6306
  const dir = countersDir(stateDir);
6034
- if (!existsSync13(dir)) return 0;
6307
+ if (!existsSync14(dir)) return 0;
6035
6308
  let sent = 0;
6036
6309
  for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
6037
- const path = join14(dir, f);
6310
+ const path = join15(dir, f);
6038
6311
  let q;
6039
6312
  try {
6040
- q = JSON.parse(readFileSync13(path, "utf8"));
6313
+ q = JSON.parse(readFileSync14(path, "utf8"));
6041
6314
  } catch {
6042
6315
  continue;
6043
6316
  }
@@ -6056,7 +6329,7 @@ async function replayQueuedTicketCounters(stateDir, route, keyFor, log, now = /*
6056
6329
  continue;
6057
6330
  }
6058
6331
  const attempts = q.attempts + 1;
6059
- writeFileSync9(path, JSON.stringify({
6332
+ writeFileSync10(path, JSON.stringify({
6060
6333
  ...q,
6061
6334
  attempts,
6062
6335
  warned: true,
@@ -6259,8 +6532,8 @@ var HandoffTranscriptTracker = class {
6259
6532
  };
6260
6533
 
6261
6534
  // src/pending-handoff.ts
6262
- import { existsSync as existsSync16, mkdirSync as mkdirSync9, readdirSync as readdirSync7, readFileSync as readFileSync14, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "fs";
6263
- import { join as join17 } from "path";
6535
+ import { existsSync as existsSync17, mkdirSync as mkdirSync10, readdirSync as readdirSync7, readFileSync as readFileSync15, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "fs";
6536
+ import { join as join18 } from "path";
6264
6537
 
6265
6538
  // src/ticket-branch.ts
6266
6539
  import { basename as basename6 } from "path";
@@ -7343,16 +7616,16 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
7343
7616
  }
7344
7617
 
7345
7618
  // src/run-worktree.ts
7346
- import { existsSync as existsSync15 } from "fs";
7347
- import { dirname as dirname6, join as join16 } from "path";
7619
+ import { existsSync as existsSync16 } from "fs";
7620
+ import { dirname as dirname7, join as join17 } from "path";
7348
7621
 
7349
7622
  // src/ports.ts
7350
- import { execFileSync as execFileSync3 } from "child_process";
7351
- import { existsSync as existsSync14 } from "fs";
7352
- import { join as join15 } from "path";
7623
+ import { execFileSync as execFileSync4 } from "child_process";
7624
+ import { existsSync as existsSync15 } from "fs";
7625
+ import { join as join16 } from "path";
7353
7626
  var run = (cmd, args) => {
7354
7627
  try {
7355
- return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
7628
+ return execFileSync4(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
7356
7629
  } catch {
7357
7630
  return "";
7358
7631
  }
@@ -7394,7 +7667,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
7394
7667
  }
7395
7668
  function worktreeExists(parent, prefix, n) {
7396
7669
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
7397
- if (existsSync14(join15(parent, name, ".git"))) return true;
7670
+ if (existsSync15(join16(parent, name, ".git"))) return true;
7398
7671
  }
7399
7672
  return false;
7400
7673
  }
@@ -7477,7 +7750,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
7477
7750
  const existing = worktreeForNumber(dir, num);
7478
7751
  const located = locateBranchForTicket(dir, cfg, t, role);
7479
7752
  const baseRef = hasRemote ? `${remote}/${base}` : base;
7480
- if (existing && !existing.prunable && existsSync15(existing.path)) {
7753
+ if (existing && !existing.prunable && existsSync16(existing.path)) {
7481
7754
  const branch = existing.branch;
7482
7755
  if (status(existing.path).length > 0) {
7483
7756
  return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
@@ -7493,7 +7766,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
7493
7766
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
7494
7767
  return { action: "recut", path: existing.path, branch, base: baseRef };
7495
7768
  }
7496
- const path = join16(dirname6(dir), worktreeForTicket(dir, cfg, t, role));
7769
+ const path = join17(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
7497
7770
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
7498
7771
  return { action: "cut-remote", path, branch: located.branch };
7499
7772
  }
@@ -7547,7 +7820,7 @@ function dropEmptyRunBranch(dir, cfg, branch) {
7547
7820
  if (remoteConfigured(dir, remote) && remoteBranchExists(dir, remote, branch)) return false;
7548
7821
  if (countCommits(dir, `${baseRef}..${branch}`) !== 0) return false;
7549
7822
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
7550
- if (wt && existsSync15(wt.path)) {
7823
+ if (wt && existsSync16(wt.path)) {
7551
7824
  if (status(wt.path).length > 0) return false;
7552
7825
  if (gitOk(dir, ["worktree", "remove", wt.path]) === null) return false;
7553
7826
  }
@@ -7555,7 +7828,7 @@ function dropEmptyRunBranch(dir, cfg, branch) {
7555
7828
  }
7556
7829
  async function finishRunWorktree(dir, cfg, branch) {
7557
7830
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
7558
- if (!wt || !existsSync15(wt.path)) return { kind: "none" };
7831
+ if (!wt || !existsSync16(wt.path)) return { kind: "none" };
7559
7832
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
7560
7833
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
7561
7834
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -7567,7 +7840,7 @@ async function finishRunWorktree(dir, cfg, branch) {
7567
7840
  if (pids.length) await killGently(pids);
7568
7841
  }
7569
7842
  gitOk(dir, ["worktree", "remove", wt.path]);
7570
- return existsSync15(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
7843
+ return existsSync16(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
7571
7844
  }
7572
7845
 
7573
7846
  // src/handoff-guard.ts
@@ -7950,20 +8223,20 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
7950
8223
 
7951
8224
  // src/pending-handoff.ts
7952
8225
  var PENDING_MAX_AGE_MS = 48 * 36e5;
7953
- var pendingDirFor = (stateDir) => join17(stateDir, "pending-handoffs");
7954
- var stateDirOfStream = (streamPath) => join17(streamPath, "..", "..");
8226
+ var pendingDirFor = (stateDir) => join18(stateDir, "pending-handoffs");
8227
+ var stateDirOfStream = (streamPath) => join18(streamPath, "..", "..");
7955
8228
  function writePendingHandoff(stateDir, rec) {
7956
8229
  const dir = pendingDirFor(stateDir);
7957
- mkdirSync9(dir, { recursive: true });
7958
- writeFileSync10(join17(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
8230
+ mkdirSync10(dir, { recursive: true });
8231
+ writeFileSync11(join18(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
7959
8232
  }
7960
8233
  function listPendingHandoffs(stateDir, route) {
7961
8234
  const dir = pendingDirFor(stateDir);
7962
- if (!existsSync16(dir)) return [];
8235
+ if (!existsSync17(dir)) return [];
7963
8236
  const out = [];
7964
8237
  for (const f of readdirSync7(dir).filter((n) => n.endsWith(".json")).sort()) {
7965
8238
  try {
7966
- const rec = JSON.parse(readFileSync14(join17(dir, f), "utf8"));
8239
+ const rec = JSON.parse(readFileSync15(join18(dir, f), "utf8"));
7967
8240
  if (rec.version === 1 && rec.ticketRecordId && (!route || rec.route === route)) out.push(rec);
7968
8241
  } catch {
7969
8242
  }
@@ -7971,7 +8244,7 @@ function listPendingHandoffs(stateDir, route) {
7971
8244
  return out;
7972
8245
  }
7973
8246
  function removePendingHandoff(stateDir, ticketRecordId) {
7974
- rmSync9(join17(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
8247
+ rmSync9(join18(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
7975
8248
  }
7976
8249
  function pendingHandoffFor(stateDir, ticketRecordId) {
7977
8250
  return listPendingHandoffs(stateDir).find((r) => r.ticketRecordId === ticketRecordId);
@@ -8081,8 +8354,8 @@ function expandSkillMarkers(text, lookup2, opts = {}) {
8081
8354
  }
8082
8355
 
8083
8356
  // src/skills.ts
8084
- import { readFileSync as readFileSync16, readdirSync as readdirSync8, existsSync as existsSync18 } from "fs";
8085
- import { join as join19 } from "path";
8357
+ import { readFileSync as readFileSync17, readdirSync as readdirSync8, existsSync as existsSync19 } from "fs";
8358
+ import { join as join20 } from "path";
8086
8359
  import { parse as parseYaml } from "yaml";
8087
8360
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError3, TablationApiError as TablationApiError2 } from "@tablation/client";
8088
8361
 
@@ -8098,8 +8371,8 @@ ${additions}`;
8098
8371
  }
8099
8372
 
8100
8373
  // src/agents.ts
8101
- import { readFileSync as readFileSync15, existsSync as existsSync17 } from "fs";
8102
- import { join as join18 } from "path";
8374
+ import { readFileSync as readFileSync16, existsSync as existsSync18 } from "fs";
8375
+ import { join as join19 } from "path";
8103
8376
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError2, TablationApiError } from "@tablation/client";
8104
8377
  var PERSONA_NAME = {
8105
8378
  dev: "Developer",
@@ -8113,12 +8386,12 @@ var CREW_AGENT_COLUMN = "agent_id";
8113
8386
  var AgentsSyncError = class extends Error {
8114
8387
  };
8115
8388
  function personaDefaultPrompt(promptsDir, role) {
8116
- const briefPath = join18(promptsDir, "personas", `lane-${role}.md`);
8117
- if (!existsSync17(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
8118
- if (SELF_CONTAINED_ROLES.has(role)) return readFileSync15(briefPath, "utf8");
8119
- const commonPath = join18(promptsDir, "personas", "common.md");
8120
- if (!existsSync17(commonPath)) return readFileSync15(briefPath, "utf8");
8121
- return `${readFileSync15(commonPath, "utf8")}${readFileSync15(briefPath, "utf8")}`;
8389
+ const briefPath = join19(promptsDir, "personas", `lane-${role}.md`);
8390
+ if (!existsSync18(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
8391
+ if (SELF_CONTAINED_ROLES.has(role)) return readFileSync16(briefPath, "utf8");
8392
+ const commonPath = join19(promptsDir, "personas", "common.md");
8393
+ if (!existsSync18(commonPath)) return readFileSync16(briefPath, "utf8");
8394
+ return `${readFileSync16(commonPath, "utf8")}${readFileSync16(briefPath, "utf8")}`;
8122
8395
  }
8123
8396
  async function resolveAgentId(route, role, opts) {
8124
8397
  const cached = route.resolved?.agentPersonas?.[role]?.agentId;
@@ -8565,7 +8838,7 @@ function describeCrewLink(o) {
8565
8838
  var SkillSyncError = class extends Error {
8566
8839
  };
8567
8840
  function parseSkillFile(path) {
8568
- const text = readFileSync16(path, "utf8");
8841
+ const text = readFileSync17(path, "utf8");
8569
8842
  const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
8570
8843
  if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
8571
8844
  const frontmatter = match[1];
@@ -8578,9 +8851,9 @@ function parseSkillFile(path) {
8578
8851
  return { name: meta.name, description: meta.description, prompt: body, file: path };
8579
8852
  }
8580
8853
  function listLocalSkills(promptsDir) {
8581
- const dir = join19(promptsDir, "skills");
8582
- if (!existsSync18(dir)) return [];
8583
- return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join19(dir, f)));
8854
+ const dir = join20(promptsDir, "skills");
8855
+ if (!existsSync19(dir)) return [];
8856
+ return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join20(dir, f)));
8584
8857
  }
8585
8858
  async function syncSkills(route, opts) {
8586
8859
  if (!route.resolved) {
@@ -8782,26 +9055,26 @@ var AgentError = class extends Error {
8782
9055
  };
8783
9056
  var AgentSpawnError = class extends AgentError {
8784
9057
  };
8785
- var digestPathFor = (stateDir, routeLabel, role, runId) => join20(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
9058
+ var digestPathFor = (stateDir, routeLabel, role, runId) => join21(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
8786
9059
  function planAgentRun(o) {
8787
- const briefPath = join20(o.route.promptsDir, "personas", `lane-${o.role}.md`);
8788
- if (!existsSync19(briefPath)) {
9060
+ const briefPath = join21(o.route.promptsDir, "personas", `lane-${o.role}.md`);
9061
+ if (!existsSync20(briefPath)) {
8789
9062
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
8790
9063
  }
8791
- const commonPath = join20(o.route.promptsDir, "personas", "common.md");
8792
- const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync19(commonPath);
9064
+ const commonPath = join21(o.route.promptsDir, "personas", "common.md");
9065
+ const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync20(commonPath);
8793
9066
  const now = o.now ?? (() => Date.now());
8794
9067
  const routeLabel = routeSlug(o.route.route);
8795
9068
  const ownDigest = o.runId ? digestPathFor(o.stateDir, routeLabel, o.role, o.runId) : void 0;
8796
- const digestPath2 = ownDigest && existsSync19(ownDigest) ? ownDigest : digestPathFor(o.stateDir, routeLabel, o.role);
9069
+ const digestPath2 = ownDigest && existsSync20(ownDigest) ? ownDigest : digestPathFor(o.stateDir, routeLabel, o.role);
8797
9070
  let digest;
8798
9071
  let ageSeconds;
8799
- if (existsSync19(digestPath2)) {
8800
- ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
8801
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync17(digestPath2, "utf8");
9072
+ if (existsSync20(digestPath2)) {
9073
+ ageSeconds = Math.floor((now() - statSync6(digestPath2).mtimeMs) / 1e3);
9074
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync18(digestPath2, "utf8");
8802
9075
  }
8803
- const common = usesCommon ? readFileSync17(commonPath, "utf8") : "";
8804
- const brief = readFileSync17(briefPath, "utf8");
9076
+ const common = usesCommon ? readFileSync18(commonPath, "utf8") : "";
9077
+ const brief = readFileSync18(briefPath, "utf8");
8805
9078
  const effectiveCommon = o.divergedPrompt ? "" : common;
8806
9079
  const effectiveBrief = appendAdditions(o.divergedPrompt ?? brief, o.personaAdditions);
8807
9080
  const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
@@ -8823,7 +9096,7 @@ function planAgentRun(o) {
8823
9096
  digest
8824
9097
  });
8825
9098
  const promptSha = createHash3("sha256").update(expanded.text).digest("hex");
8826
- const streamsDir = join20(o.stateDir, "streams");
9099
+ const streamsDir = join21(o.stateDir, "streams");
8827
9100
  const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
8828
9101
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
8829
9102
  const { toolchain, model } = seat;
@@ -8888,13 +9161,16 @@ function planAgentRun(o) {
8888
9161
  // ticket attached, which silently no-ops and strands the ticket
8889
9162
  // in_progress (ISSUE-490, ISSUE-591). Setting this is what lets that
8890
9163
  // hook tell the two apart; it has nothing to do with the run itself.
8891
- [CREW_LANE_ROLE_VAR]: o.role
9164
+ [CREW_LANE_ROLE_VAR]: o.role,
9165
+ // CREW-2783: the ship's forge credential — git helper config plus the
9166
+ // token as an env var for `gh` and friends. Empty without `forges:`.
9167
+ ...forgeRunEnv(o.ship.forges, { crewCommand: crewCommandLine(), configFile: process.env.CREW_CONFIG, stateDir: o.stateDir })
8892
9168
  },
8893
9169
  digestAttached: digest !== void 0,
8894
9170
  digestAgeSeconds: ageSeconds,
8895
9171
  model,
8896
- streamPath: join20(streamsDir, `${base}.jsonl`),
8897
- eventsPath: join20(streamsDir, `${base}.events.jsonl`),
9172
+ streamPath: join21(streamsDir, `${base}.jsonl`),
9173
+ eventsPath: join21(streamsDir, `${base}.events.jsonl`),
8898
9174
  ticket: o.ticket,
8899
9175
  ticketRecordId: o.ticketRecordId ?? null,
8900
9176
  repo: o.repo ?? null,
@@ -8925,7 +9201,7 @@ function describePlan(p) {
8925
9201
  var pidPathFor = (streamPath) => `${streamPath}.pid`;
8926
9202
  function readRunSidecar(streamPath) {
8927
9203
  try {
8928
- const sc = JSON.parse(readFileSync17(pidPathFor(streamPath), "utf8"));
9204
+ const sc = JSON.parse(readFileSync18(pidPathFor(streamPath), "utf8"));
8929
9205
  return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
8930
9206
  } catch {
8931
9207
  return null;
@@ -8961,7 +9237,7 @@ function identityOf(plan, adapterHarness) {
8961
9237
  var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
8962
9238
  function openSink(path) {
8963
9239
  try {
8964
- mkdirSync10(dirname7(path), { recursive: true });
9240
+ mkdirSync11(dirname8(path), { recursive: true });
8965
9241
  return createWriteStream(path, { flags: "a" });
8966
9242
  } catch {
8967
9243
  return void 0;
@@ -9143,7 +9419,7 @@ var RunObserver = class {
9143
9419
  };
9144
9420
  function writeRunMeta(id, startedAt, liveTicket, extra) {
9145
9421
  try {
9146
- writeFileSync11(`${id.streamPath}.meta.json`, JSON.stringify({
9422
+ writeFileSync12(`${id.streamPath}.meta.json`, JSON.stringify({
9147
9423
  route: id.route,
9148
9424
  role: id.role,
9149
9425
  hint: id.ticket,
@@ -9303,7 +9579,7 @@ function spawnAgent(plan, emit, hintMoved) {
9303
9579
  return new Promise((resolve6, reject) => {
9304
9580
  let streamFd;
9305
9581
  try {
9306
- mkdirSync10(dirname7(plan.streamPath), { recursive: true });
9582
+ mkdirSync11(dirname8(plan.streamPath), { recursive: true });
9307
9583
  streamFd = openSync3(plan.streamPath, "a");
9308
9584
  } catch (e) {
9309
9585
  void endSink(eventsSink);
@@ -9338,7 +9614,7 @@ function spawnAgent(plan, emit, hintMoved) {
9338
9614
  promptSha: plan.promptSha
9339
9615
  };
9340
9616
  try {
9341
- writeFileSync11(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
9617
+ writeFileSync12(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
9342
9618
  } catch {
9343
9619
  }
9344
9620
  }
@@ -9841,7 +10117,7 @@ async function runSelftest(i, deps = realDeps) {
9841
10117
  ...adapter.runEnv(spec),
9842
10118
  [KEY_VAR]: i.apiKey
9843
10119
  };
9844
- const cwd = mkdtempSync(join21(tmpdir2(), "crew-selftest-"));
10120
+ const cwd = mkdtempSync(join22(tmpdir2(), "crew-selftest-"));
9845
10121
  try {
9846
10122
  const run2 = await deps.run({
9847
10123
  bin: invocation.bin,
@@ -9874,11 +10150,11 @@ function formatResult(r) {
9874
10150
  const secs = `${(r.ms / 1e3).toFixed(1)}s`;
9875
10151
  return r.ok ? `${r.toolchain}: PASS ${r.detail} (${secs}${tok})` : `${r.toolchain}: FAIL ${r.failure} \u2014 ${r.detail} (${secs})`;
9876
10152
  }
9877
- var fileFor2 = (stateDir) => join21(stateDir, "selftest.json");
10153
+ var fileFor2 = (stateDir) => join22(stateDir, "selftest.json");
9878
10154
  function readSelftest(stateDir) {
9879
10155
  try {
9880
10156
  const p = fileFor2(stateDir);
9881
- return existsSync20(p) ? JSON.parse(readFileSync18(p, "utf8")) : {};
10157
+ return existsSync21(p) ? JSON.parse(readFileSync19(p, "utf8")) : {};
9882
10158
  } catch {
9883
10159
  return {};
9884
10160
  }
@@ -9886,8 +10162,8 @@ function readSelftest(stateDir) {
9886
10162
  function recordSelftest(stateDir, results) {
9887
10163
  const all = readSelftest(stateDir);
9888
10164
  for (const r of results) all[r.toolchain] = r;
9889
- mkdirSync11(stateDir, { recursive: true });
9890
- writeFileSync12(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
10165
+ mkdirSync12(stateDir, { recursive: true });
10166
+ writeFileSync13(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
9891
10167
  `);
9892
10168
  }
9893
10169
  function doctorSelftestLines(stored, toolchains) {
@@ -9899,7 +10175,7 @@ function doctorSelftestLines(stored, toolchains) {
9899
10175
  }
9900
10176
 
9901
10177
  // src/cli.ts
9902
- import { resolve as resolve5, dirname as dirname20, join as join41, basename as basename12 } from "path";
10178
+ import { resolve as resolve5, dirname as dirname21, join as join42, basename as basename12 } from "path";
9903
10179
  import { fileURLToPath } from "url";
9904
10180
  import { hostname as hostname2, homedir as homedir10 } from "os";
9905
10181
  import { spawnSync as spawnSync7 } from "child_process";
@@ -9937,6 +10213,11 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
9937
10213
  "workspace",
9938
10214
  "workspace-id",
9939
10215
  "write",
10216
+ "token",
10217
+ "source",
10218
+ "var",
10219
+ "path",
10220
+ "service",
9940
10221
  "topic",
9941
10222
  "assignee",
9942
10223
  "issue",
@@ -10084,14 +10365,14 @@ function rosterMarkdown(roster, meId) {
10084
10365
  }
10085
10366
 
10086
10367
  // src/connect-resume.ts
10087
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, rmSync as rmSync11, writeFileSync as writeFileSync13 } from "fs";
10088
- import { dirname as dirname8, join as join22 } from "path";
10368
+ import { mkdirSync as mkdirSync13, readFileSync as readFileSync20, rmSync as rmSync11, writeFileSync as writeFileSync14 } from "fs";
10369
+ import { dirname as dirname9, join as join23 } from "path";
10089
10370
  function lastSessionFile(stateDir) {
10090
- return join22(stateDir, "connect-session.json");
10371
+ return join23(stateDir, "connect-session.json");
10091
10372
  }
10092
10373
  function readPointers(stateDir) {
10093
10374
  try {
10094
- const parsed = JSON.parse(readFileSync19(lastSessionFile(stateDir), "utf8"));
10375
+ const parsed = JSON.parse(readFileSync20(lastSessionFile(stateDir), "utf8"));
10095
10376
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
10096
10377
  const out = {};
10097
10378
  for (const [host, v] of Object.entries(parsed)) {
@@ -10105,8 +10386,8 @@ function readPointers(stateDir) {
10105
10386
  }
10106
10387
  function writePointers(stateDir, pointers) {
10107
10388
  const file = lastSessionFile(stateDir);
10108
- mkdirSync12(dirname8(file), { recursive: true });
10109
- writeFileSync13(file, `${JSON.stringify(pointers, null, 2)}
10389
+ mkdirSync13(dirname9(file), { recursive: true });
10390
+ writeFileSync14(file, `${JSON.stringify(pointers, null, 2)}
10110
10391
  `, { mode: 384 });
10111
10392
  }
10112
10393
  function rememberSession(stateDir, host, workspaceSlug) {
@@ -10191,15 +10472,15 @@ async function signOutSessions(o) {
10191
10472
  }
10192
10473
 
10193
10474
  // src/reset.ts
10194
- import { existsSync as existsSync21, mkdirSync as mkdirSync13, readFileSync as readFileSync20, readdirSync as readdirSync9, rmSync as rmSync12, statSync as statSync6, writeFileSync as writeFileSync14 } from "fs";
10195
- import { dirname as dirname9, join as join23 } from "path";
10475
+ import { existsSync as existsSync22, mkdirSync as mkdirSync14, readFileSync as readFileSync21, readdirSync as readdirSync9, rmSync as rmSync12, statSync as statSync7, writeFileSync as writeFileSync15 } from "fs";
10476
+ import { dirname as dirname10, join as join24 } from "path";
10196
10477
  import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
10197
10478
  function clonedCheckoutsFile(stateDir) {
10198
- return join23(stateDir, "cloned-checkouts.json");
10479
+ return join24(stateDir, "cloned-checkouts.json");
10199
10480
  }
10200
10481
  function readClonedCheckouts(stateDir) {
10201
10482
  try {
10202
- const parsed = JSON.parse(readFileSync20(clonedCheckoutsFile(stateDir), "utf8"));
10483
+ const parsed = JSON.parse(readFileSync21(clonedCheckoutsFile(stateDir), "utf8"));
10203
10484
  return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
10204
10485
  } catch {
10205
10486
  return [];
@@ -10209,8 +10490,8 @@ function recordClonedCheckout(stateDir, dir) {
10209
10490
  const known = readClonedCheckouts(stateDir);
10210
10491
  if (known.includes(dir)) return;
10211
10492
  const file = clonedCheckoutsFile(stateDir);
10212
- mkdirSync13(dirname9(file), { recursive: true });
10213
- writeFileSync14(file, `${JSON.stringify([...known, dir], null, 2)}
10493
+ mkdirSync14(dirname10(file), { recursive: true });
10494
+ writeFileSync15(file, `${JSON.stringify([...known, dir], null, 2)}
10214
10495
  `);
10215
10496
  }
10216
10497
  var mark = (dryRun) => dryRun ? "would-remove" : "removed";
@@ -10262,7 +10543,7 @@ async function runReset(o, fx) {
10262
10543
  for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
10263
10544
  }
10264
10545
  steps.push(...removeState(o.stateDir, o.dryRun));
10265
- if (existsSync21(o.configFile)) {
10546
+ if (existsSync22(o.configFile)) {
10266
10547
  if (!o.dryRun) rmSync12(o.configFile, { force: true });
10267
10548
  steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
10268
10549
  } else {
@@ -10271,7 +10552,7 @@ async function runReset(o, fx) {
10271
10552
  return steps;
10272
10553
  }
10273
10554
  function removeCheckout(dir, dryRun, fx) {
10274
- if (!existsSync21(dir)) return { step: "checkout", target: dir, action: "absent", detail: "already gone" };
10555
+ if (!existsSync22(dir)) return { step: "checkout", target: dir, action: "absent", detail: "already gone" };
10275
10556
  const { dirty, linkedWorktrees } = fx.inspectCheckout(dir);
10276
10557
  if (dirty.length) {
10277
10558
  return { step: "checkout", target: dir, action: "kept", detail: `has uncommitted changes (${dirty.length} path${dirty.length === 1 ? "" : "s"}: ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? ", \u2026" : ""})` };
@@ -10284,15 +10565,15 @@ function removeCheckout(dir, dryRun, fx) {
10284
10565
  }
10285
10566
  function removeState(stateDir, dryRun) {
10286
10567
  const out = [];
10287
- const targets = [join23(stateDir, "resolved"), join23(stateDir, "keys"), clonedCheckoutsFile(stateDir), join23(stateDir, "connect-session.json"), join23(stateDir, "reload-requested"), join23(stateDir, "daemon.started")];
10568
+ const targets = [join24(stateDir, "resolved"), join24(stateDir, "keys"), clonedCheckoutsFile(stateDir), join24(stateDir, "connect-session.json"), join24(stateDir, "reload-requested"), join24(stateDir, "daemon.started")];
10288
10569
  try {
10289
10570
  for (const name of readdirSync9(stateDir)) {
10290
- if (name.startsWith(".") && statSync6(join23(stateDir, name)).isFile()) targets.push(join23(stateDir, name));
10571
+ if (name.startsWith(".") && statSync7(join24(stateDir, name)).isFile()) targets.push(join24(stateDir, name));
10291
10572
  }
10292
10573
  } catch {
10293
10574
  }
10294
10575
  for (const t of targets) {
10295
- if (!existsSync21(t)) continue;
10576
+ if (!existsSync22(t)) continue;
10296
10577
  if (!dryRun) rmSync12(t, { recursive: true, force: true });
10297
10578
  out.push({ step: "state", target: t, action: mark(dryRun) });
10298
10579
  }
@@ -11080,8 +11361,8 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
11080
11361
  }
11081
11362
 
11082
11363
  // src/poll.ts
11083
- import { writeFileSync as writeFileSync15 } from "fs";
11084
- import { join as join24 } from "path";
11364
+ import { writeFileSync as writeFileSync16 } from "fs";
11365
+ import { join as join25 } from "path";
11085
11366
 
11086
11367
  // src/stalled.ts
11087
11368
  var STALLED_AFTER_CYCLES = 30;
@@ -11388,7 +11669,7 @@ async function decideCycle(o) {
11388
11669
  }
11389
11670
  return decision;
11390
11671
  }
11391
- var digestPath = (stateDir, route, role, runId) => join24(stateDir, `digest-${route}-${role}${runId ? `-${runId}` : ""}.md`);
11672
+ var digestPath = (stateDir, route, role, runId) => join25(stateDir, `digest-${route}-${role}${runId ? `-${runId}` : ""}.md`);
11392
11673
  function otherRunsRemoved(tickets, own) {
11393
11674
  if (!own) return tickets;
11394
11675
  return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
@@ -11466,7 +11747,7 @@ function writeDigest(o, d, role, stateDir, runId, own) {
11466
11747
 
11467
11748
  ${body}` : body;
11468
11749
  const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
11469
- writeFileSync15(path, text);
11750
+ writeFileSync16(path, text);
11470
11751
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
11471
11752
  data: { role, bytes: Buffer.byteLength(text) }
11472
11753
  });
@@ -11949,13 +12230,13 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
11949
12230
  var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
11950
12231
 
11951
12232
  // src/connect-wizard.ts
11952
- import { delimiter, dirname as dirname10, join as join25 } from "path";
11953
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync22, mkdirSync as mkdirSync14, writeFileSync as writeFileSync16 } from "fs";
12233
+ import { delimiter, dirname as dirname11, join as join26 } from "path";
12234
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync23, mkdirSync as mkdirSync15, writeFileSync as writeFileSync17 } from "fs";
11954
12235
  import { stringify } from "yaml";
11955
12236
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
11956
12237
  function shouldRunWizard(opts) {
11957
12238
  if (!opts.isTTY) return false;
11958
- const exists = opts.configExists ?? existsSync22;
12239
+ const exists = opts.configExists ?? existsSync23;
11959
12240
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
11960
12241
  }
11961
12242
  function firstRunConfigPath() {
@@ -11972,7 +12253,7 @@ function defaultIsExecutable(p) {
11972
12253
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
11973
12254
  for (const dir of pathEnv.split(delimiter)) {
11974
12255
  if (!dir) continue;
11975
- const candidate = join25(dir, name);
12256
+ const candidate = join26(dir, name);
11976
12257
  if (isExecutable(candidate)) return candidate;
11977
12258
  }
11978
12259
  return void 0;
@@ -11998,9 +12279,9 @@ async function runWizard(p, env) {
11998
12279
  if (entered && isExec(entered)) agentBin = entered;
11999
12280
  else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
12000
12281
  }
12001
- const nodeDir = dirname10(env.nodePath);
12282
+ const nodeDir = dirname11(env.nodePath);
12002
12283
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
12003
- const baseDirs = [nodeDir, pnpmPath ? dirname10(pnpmPath) : void 0];
12284
+ const baseDirs = [nodeDir, pnpmPath ? dirname11(pnpmPath) : void 0];
12004
12285
  p.say(
12005
12286
  `
12006
12287
  Host Passengers lets this machine run the agent sessions that other people's
@@ -12011,7 +12292,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
12011
12292
  p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
12012
12293
  }
12013
12294
  const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
12014
- const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname10(dockerPath) : void 0]);
12295
+ const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname11(dockerPath) : void 0]);
12015
12296
  if (extraPath) {
12016
12297
  p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
12017
12298
  }
@@ -12033,9 +12314,9 @@ routes:
12033
12314
  ${routeBlock}`;
12034
12315
  }
12035
12316
  function writeNewConfig(path, text) {
12036
- mkdirSync14(dirname10(path), { recursive: true });
12317
+ mkdirSync15(dirname11(path), { recursive: true });
12037
12318
  try {
12038
- writeFileSync16(path, text, { mode: 384, flag: "wx" });
12319
+ writeFileSync17(path, text, { mode: 384, flag: "wx" });
12039
12320
  return true;
12040
12321
  } catch (e) {
12041
12322
  if (e.code === "EEXIST") return false;
@@ -12069,7 +12350,7 @@ Wrote ${o.configPath}.
12069
12350
  // src/connect-json.ts
12070
12351
  import { accessSync as accessSync3, constants as constants3 } from "fs";
12071
12352
  import { homedir as homedir8 } from "os";
12072
- import { dirname as dirname11, join as join26 } from "path";
12353
+ import { dirname as dirname12, join as join27 } from "path";
12073
12354
  import { delimiter as delimiter2 } from "path";
12074
12355
  var CHOICE_EXIT_CODE = 3;
12075
12356
  function createEmitter(json, write) {
@@ -12108,12 +12389,12 @@ function agentBinSearchDirs(home, pathEnv, extraPath) {
12108
12389
  const dirs = [
12109
12390
  ...pathEnv.split(delimiter2),
12110
12391
  ...(extraPath ?? "").split(delimiter2),
12111
- join26(home, ".local/bin"),
12112
- join26(home, ".claude/local"),
12392
+ join27(home, ".local/bin"),
12393
+ join27(home, ".claude/local"),
12113
12394
  "/opt/homebrew/bin",
12114
12395
  "/usr/local/bin",
12115
- join26(home, ".npm-global/bin"),
12116
- join26(home, ".volta/bin")
12396
+ join27(home, ".npm-global/bin"),
12397
+ join27(home, ".volta/bin")
12117
12398
  ];
12118
12399
  return [...new Set(dirs.filter(Boolean))];
12119
12400
  }
@@ -12135,7 +12416,7 @@ function flagWizardAnswers(f, env) {
12135
12416
  const agentBin = f.agentBin ?? found?.bin;
12136
12417
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
12137
12418
  const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
12138
- const extraPath = f.extraPath ?? deriveExtraPath([dirname11(env.nodePath), pnpmPath ? dirname11(pnpmPath) : void 0, dockerPath ? dirname11(dockerPath) : void 0]);
12419
+ const extraPath = f.extraPath ?? deriveExtraPath([dirname12(env.nodePath), pnpmPath ? dirname12(pnpmPath) : void 0, dockerPath ? dirname12(dockerPath) : void 0]);
12139
12420
  return {
12140
12421
  answers: {
12141
12422
  name: f.shipName?.trim() || env.hostname,
@@ -12149,8 +12430,8 @@ function flagWizardAnswers(f, env) {
12149
12430
  }
12150
12431
 
12151
12432
  // src/release-run.ts
12152
- import { readFileSync as readFileSync21, writeFileSync as writeFileSync17, existsSync as existsSync23, copyFileSync, mkdirSync as mkdirSync15, mkdtempSync as mkdtempSync2, rmSync as rmSync13 } from "fs";
12153
- import { dirname as dirname12, join as join27 } from "path";
12433
+ import { readFileSync as readFileSync22, writeFileSync as writeFileSync18, existsSync as existsSync24, copyFileSync, mkdirSync as mkdirSync16, mkdtempSync as mkdtempSync2, rmSync as rmSync13 } from "fs";
12434
+ import { dirname as dirname13, join as join28 } from "path";
12154
12435
  import { tmpdir as tmpdir3 } from "os";
12155
12436
 
12156
12437
  // src/release.ts
@@ -12891,13 +13172,13 @@ async function bump(o, size) {
12891
13172
  }
12892
13173
  return printed;
12893
13174
  }
12894
- const files = o.repo.release.versionFiles.map((f) => join27(o.cwd, f));
13175
+ const files = o.repo.release.versionFiles.map((f) => join28(o.cwd, f));
12895
13176
  const first = files[0];
12896
- if (!existsSync23(first)) {
13177
+ if (!existsSync24(first)) {
12897
13178
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
12898
13179
  return null;
12899
13180
  }
12900
- const current = JSON.parse(readFileSync21(first, "utf8")).version;
13181
+ const current = JSON.parse(readFileSync22(first, "utf8")).version;
12901
13182
  if (!current) {
12902
13183
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
12903
13184
  return null;
@@ -12910,27 +13191,27 @@ async function bump(o, size) {
12910
13191
  return version;
12911
13192
  }
12912
13193
  for (const f of files) {
12913
- const src = readFileSync21(f, "utf8");
13194
+ const src = readFileSync22(f, "utf8");
12914
13195
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
12915
13196
  if (out === src) {
12916
13197
  o.emit.error(`no version field in ${f}`);
12917
13198
  return null;
12918
13199
  }
12919
- writeFileSync17(f, out);
13200
+ writeFileSync18(f, out);
12920
13201
  }
12921
13202
  return version;
12922
13203
  }
12923
13204
  function writeChangelog(o, version, entries) {
12924
13205
  const file = o.repo.release.changelog;
12925
13206
  if (!file || entries.length === 0) return;
12926
- const path = join27(o.cwd, file);
13207
+ const path = join28(o.cwd, file);
12927
13208
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
12928
13209
  if (o.dryRun) {
12929
13210
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
12930
13211
  return;
12931
13212
  }
12932
- const existing = existsSync23(path) ? readFileSync21(path, "utf8") : "# Changelog\n";
12933
- writeFileSync17(path, insertChangelogSection(existing, section));
13213
+ const existing = existsSync24(path) ? readFileSync22(path, "utf8") : "# Changelog\n";
13214
+ writeFileSync18(path, insertChangelogSection(existing, section));
12934
13215
  }
12935
13216
  async function confirm(o, expected) {
12936
13217
  if (!o.repo.hooks.released) return void 0;
@@ -13023,8 +13304,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
13023
13304
  function openReleaseCheckout(o) {
13024
13305
  const { remote, base } = o.repo.branch;
13025
13306
  const branch = `crew/release-${Date.now()}`;
13026
- const root = mkdtempSync2(join27(tmpdir3(), "crew-release-"));
13027
- const dir = join27(root, "checkout");
13307
+ const root = mkdtempSync2(join28(tmpdir3(), "crew-release-"));
13308
+ const dir = join28(root, "checkout");
13028
13309
  o.emit.emit(`release checkout: ${dir}`, { step: "release" });
13029
13310
  const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
13030
13311
  try {
@@ -13034,10 +13315,10 @@ function openReleaseCheckout(o) {
13034
13315
  throw e;
13035
13316
  }
13036
13317
  for (const f of o.repo.worktrees.copy) {
13037
- const src = join27(o.cwd, f);
13038
- if (!existsSync23(src)) continue;
13039
- mkdirSync15(dirname12(join27(dir, f)), { recursive: true });
13040
- copyFileSync(src, join27(dir, f));
13318
+ const src = join28(o.cwd, f);
13319
+ if (!existsSync24(src)) continue;
13320
+ mkdirSync16(dirname13(join28(dir, f)), { recursive: true });
13321
+ copyFileSync(src, join28(dir, f));
13041
13322
  }
13042
13323
  return { dir, branch, close };
13043
13324
  }
@@ -13738,12 +14019,12 @@ Passed: ${passed}.${willClose ? " Closing this alert." : ""}`,
13738
14019
  }
13739
14020
 
13740
14021
  // src/live-runs.ts
13741
- import { existsSync as existsSync25, readdirSync as readdirSync11 } from "fs";
13742
- import { join as join29 } from "path";
14022
+ import { existsSync as existsSync26, readdirSync as readdirSync11 } from "fs";
14023
+ import { join as join30 } from "path";
13743
14024
 
13744
14025
  // src/interrupted.ts
13745
- import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as readFileSync22 } from "fs";
13746
- import { join as join28 } from "path";
14026
+ import { existsSync as existsSync25, readdirSync as readdirSync10, readFileSync as readFileSync23 } from "fs";
14027
+ import { join as join29 } from "path";
13747
14028
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
13748
14029
  function interruptedComment(b) {
13749
14030
  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.";
@@ -13762,21 +14043,21 @@ var pidAlive = (pid) => {
13762
14043
  };
13763
14044
  function runFinished(streamPath) {
13764
14045
  const path = `${streamPath}.meta.json`;
13765
- if (!existsSync24(path)) return false;
14046
+ if (!existsSync25(path)) return false;
13766
14047
  try {
13767
- return JSON.parse(readFileSync22(path, "utf8")).running !== true;
14048
+ return JSON.parse(readFileSync23(path, "utf8")).running !== true;
13768
14049
  } catch {
13769
14050
  return true;
13770
14051
  }
13771
14052
  }
13772
14053
  function orphanedStreams(o) {
13773
- const dir = join28(o.stateDir, "streams");
13774
- if (!existsSync24(dir)) return [];
14054
+ const dir = join29(o.stateDir, "streams");
14055
+ if (!existsSync25(dir)) return [];
13775
14056
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
13776
14057
  const out = [];
13777
14058
  for (const name of readdirSync10(dir)) {
13778
14059
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
13779
- const streamPath = join28(dir, name);
14060
+ const streamPath = join29(dir, name);
13780
14061
  if (runFinished(streamPath)) continue;
13781
14062
  const sidecar = readRunSidecar(streamPath);
13782
14063
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -13830,12 +14111,12 @@ async function reconcileRuns(o) {
13830
14111
 
13831
14112
  // src/live-runs.ts
13832
14113
  function liveRuns(stateDir, isAlive = runIsAlive) {
13833
- const dir = join29(stateDir, "streams");
13834
- if (!existsSync25(dir)) return [];
14114
+ const dir = join30(stateDir, "streams");
14115
+ if (!existsSync26(dir)) return [];
13835
14116
  const out = [];
13836
14117
  for (const name of readdirSync11(dir)) {
13837
14118
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
13838
- const streamPath = join29(dir, name);
14119
+ const streamPath = join30(dir, name);
13839
14120
  if (runFinished(streamPath)) continue;
13840
14121
  const sidecar = readRunSidecar(streamPath);
13841
14122
  if (!sidecar || !isAlive(sidecar)) continue;
@@ -13934,6 +14215,70 @@ function formatProjectsReport(report) {
13934
14215
  `;
13935
14216
  }
13936
14217
 
14218
+ // src/forge-cmd.ts
14219
+ import { parseDocument } from "yaml";
14220
+ function runCredential(action, stdin, forges, stateDir, env = process.env) {
14221
+ try {
14222
+ return { code: 0, out: answerCredential(action ?? "", parseCredentialInput(stdin), forges, stateDir, env), err: "" };
14223
+ } catch (e) {
14224
+ return { code: 0, out: "", err: `crew credential: ${e.message}
14225
+ ` };
14226
+ }
14227
+ }
14228
+ function sourceFromArgs(a) {
14229
+ const source = a.source ?? "file";
14230
+ if (!(source in TOKEN_SOURCES)) return `--source must be one of: ${Object.keys(TOKEN_SOURCES).join(", ")}`;
14231
+ if (source === "env") return a.var ? { source: "env", var: a.var } : "--source env needs --var NAME";
14232
+ if (source === "file") return a.path ? { source: "file", path: a.path } : { source: "file" };
14233
+ return a.service ? { source: "keychain", service: a.service } : { source: "keychain" };
14234
+ }
14235
+ function runForge(a, io) {
14236
+ const fail = (m, code = 2) => ({ code, out: "", err: `crew forge: ${m}
14237
+ ` });
14238
+ const sub = a.sub ?? "list";
14239
+ if (sub === "list") {
14240
+ const rows = Object.keys(io.forges ?? {}).map((h) => {
14241
+ let state;
14242
+ try {
14243
+ state = hasForgeToken(io.forges, h, io.stateDir, io.env) ? "token present" : "NO TOKEN";
14244
+ } catch {
14245
+ state = "unreadable";
14246
+ }
14247
+ return `${h.padEnd(20)} ${state.padEnd(14)} ${describeForgeSource(io.forges, h, io.stateDir)}`;
14248
+ });
14249
+ return { code: 0, out: rows.length ? `${rows.join("\n")}
14250
+ ` : "no forges configured (ship.forges in crew.yaml)\n", err: "" };
14251
+ }
14252
+ if (sub !== "set" && sub !== "rotate") return fail(`unknown subcommand "${sub}" (have: set, rotate, list)`);
14253
+ if (!a.host) return fail(`crew forge ${sub} <host> [--token T | --token-stdin] [--source file|keychain|env] [--var NAME] [--path P] [--service S]`);
14254
+ const host = a.host.toLowerCase();
14255
+ let existing = forgeFor(io.forges, host);
14256
+ if (sub === "rotate" && !existing) return fail(`no forges.${host} entry to rotate; use \`crew forge set ${host}\``);
14257
+ if (!existing) {
14258
+ const tok = sourceFromArgs(a);
14259
+ if (typeof tok === "string") return fail(tok);
14260
+ const doc = parseDocument(io.readFile(io.configFile));
14261
+ doc.setIn(["ship", "forges", host, "token"], tok);
14262
+ io.writeFile(io.configFile, doc.toString());
14263
+ io.forges = { ...io.forges ?? {}, [host]: { username: "x-access-token", token: tok } };
14264
+ existing = forgeFor(io.forges, host);
14265
+ }
14266
+ if (existing.entry.token.source === "env") {
14267
+ return { code: 0, out: `forges.${host} reads ${describeForgeSource(io.forges, host, io.stateDir)}; export it where crew runs
14268
+ `, err: "" };
14269
+ }
14270
+ const token = a.tokenStdin ? io.stdin().trim() : a.token ?? io.env.CREW_FORGE_TOKEN;
14271
+ if (!token?.trim()) return fail("no token given (use --token, --token-stdin or CREW_FORGE_TOKEN in the environment)");
14272
+ try {
14273
+ storeForgeToken(io.forges, host, token, io.stateDir, io.env);
14274
+ } catch (e) {
14275
+ if (e instanceof ForgeError) return fail(e.message, 1);
14276
+ throw e;
14277
+ }
14278
+ return { code: 0, out: `${sub === "rotate" ? "rotated" : "stored"} the ${host} token in ${describeForgeSource(io.forges, host, io.stateDir)}
14279
+ `, err: "" };
14280
+ }
14281
+
13937
14282
  // src/whoami.ts
13938
14283
  async function buildWhoami(routes, deps = {}) {
13939
14284
  const doFetch = deps.fetch ?? fetch;
@@ -14105,7 +14450,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
14105
14450
  }
14106
14451
 
14107
14452
  // src/watch.ts
14108
- import { watch as watchFile, existsSync as existsSync26, statSync as statSync7, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
14453
+ import { watch as watchFile, existsSync as existsSync27, statSync as statSync8, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
14109
14454
  var E = "\x1B[";
14110
14455
  var dim = (s) => `${E}2m${s}${E}0m`;
14111
14456
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -14206,8 +14551,8 @@ function renderHeaders(states, now = Date.now()) {
14206
14551
  return states.map((s) => renderHeader(s, now)).join("");
14207
14552
  }
14208
14553
  function readFrom(file, from) {
14209
- if (!existsSync26(file)) return { events: [], offset: from };
14210
- const size = statSync7(file).size;
14554
+ if (!existsSync27(file)) return { events: [], offset: from };
14555
+ const size = statSync8(file).size;
14211
14556
  if (size < from) return readFrom(file, 0);
14212
14557
  if (size === from) return { events: [], offset: from };
14213
14558
  const fd = openSync4(file, "r");
@@ -14449,31 +14794,31 @@ function nextRoles(decision, ran) {
14449
14794
  }
14450
14795
 
14451
14796
  // src/ship-name.ts
14452
- import { existsSync as existsSync28, mkdirSync as mkdirSync17, readFileSync as readFileSync24, renameSync as renameSync2, writeFileSync as writeFileSync19 } from "fs";
14453
- import { dirname as dirname14 } from "path";
14454
- import { parseDocument } from "yaml";
14797
+ import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync25, renameSync as renameSync2, writeFileSync as writeFileSync20 } from "fs";
14798
+ import { dirname as dirname15 } from "path";
14799
+ import { parseDocument as parseDocument2 } from "yaml";
14455
14800
 
14456
14801
  // src/machine-id.ts
14457
- import { execFileSync as execFileSync4 } from "child_process";
14802
+ import { execFileSync as execFileSync5 } from "child_process";
14458
14803
  import { createHash as createHash4 } from "crypto";
14459
- import { existsSync as existsSync27, mkdirSync as mkdirSync16, readFileSync as readFileSync23, writeFileSync as writeFileSync18 } from "fs";
14460
- import { dirname as dirname13, join as join30 } from "path";
14804
+ import { existsSync as existsSync28, mkdirSync as mkdirSync17, readFileSync as readFileSync24, writeFileSync as writeFileSync19 } from "fs";
14805
+ import { dirname as dirname14, join as join31 } from "path";
14461
14806
  function machineIdPathFor(stateDir) {
14462
- return join30(stateDir, "machine-id");
14807
+ return join31(stateDir, "machine-id");
14463
14808
  }
14464
14809
  var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
14465
14810
  function readRawMachineId(platform2 = process.platform) {
14466
14811
  try {
14467
14812
  if (platform2 === "darwin") {
14468
- const out = execFileSync4("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
14813
+ const out = execFileSync5("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
14469
14814
  encoding: "utf8",
14470
14815
  stdio: ["ignore", "pipe", "ignore"]
14471
14816
  });
14472
14817
  return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
14473
14818
  }
14474
14819
  for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
14475
- if (existsSync27(p)) {
14476
- const id = readFileSync23(p, "utf8").trim();
14820
+ if (existsSync28(p)) {
14821
+ const id = readFileSync24(p, "utf8").trim();
14477
14822
  if (id) return id;
14478
14823
  }
14479
14824
  }
@@ -14484,15 +14829,15 @@ function readRawMachineId(platform2 = process.platform) {
14484
14829
  function ensureMachineId(stateDir, opts = {}) {
14485
14830
  if (opts.override?.trim()) return sha256(opts.override.trim());
14486
14831
  const path = machineIdPathFor(stateDir);
14487
- if (existsSync27(path)) {
14488
- const cached = readFileSync23(path, "utf8").trim();
14832
+ if (existsSync28(path)) {
14833
+ const cached = readFileSync24(path, "utf8").trim();
14489
14834
  if (/^[0-9a-f]{64}$/.test(cached)) return cached;
14490
14835
  }
14491
14836
  const raw = (opts.read ?? readRawMachineId)();
14492
14837
  if (!raw) return void 0;
14493
14838
  const hashed = sha256(raw);
14494
- mkdirSync16(dirname13(path), { recursive: true });
14495
- writeFileSync18(path, `${hashed}
14839
+ mkdirSync17(dirname14(path), { recursive: true });
14840
+ writeFileSync19(path, `${hashed}
14496
14841
  `, { mode: 384 });
14497
14842
  return hashed;
14498
14843
  }
@@ -14521,11 +14866,11 @@ function planNameSync(local, row, lastSynced) {
14521
14866
  return { drift: "local", action: "push" };
14522
14867
  }
14523
14868
  function shipNameOfYaml(text) {
14524
- const v = parseDocument(text).getIn(["ship", "name"]);
14869
+ const v = parseDocument2(text).getIn(["ship", "name"]);
14525
14870
  return typeof v === "string" && v.trim() ? v.trim() : void 0;
14526
14871
  }
14527
14872
  function setShipNameInYaml(text, name) {
14528
- const doc = parseDocument(text);
14873
+ const doc = parseDocument2(text);
14529
14874
  doc.setIn(["ship", "name"], name);
14530
14875
  return doc.toString();
14531
14876
  }
@@ -14563,34 +14908,34 @@ async function syncRouteName(i) {
14563
14908
  return { status: "ok", ...base, action, name: i.local };
14564
14909
  }
14565
14910
  function readSyncedName(resolvedFile) {
14566
- if (!existsSync28(resolvedFile)) return void 0;
14911
+ if (!existsSync29(resolvedFile)) return void 0;
14567
14912
  try {
14568
- const v = JSON.parse(readFileSync24(resolvedFile, "utf8")).shipName;
14913
+ const v = JSON.parse(readFileSync25(resolvedFile, "utf8")).shipName;
14569
14914
  return typeof v === "string" ? v : void 0;
14570
14915
  } catch {
14571
14916
  return void 0;
14572
14917
  }
14573
14918
  }
14574
14919
  function writeSyncedName(resolvedFile, name) {
14575
- if (!existsSync28(resolvedFile)) return;
14920
+ if (!existsSync29(resolvedFile)) return;
14576
14921
  let raw;
14577
14922
  try {
14578
- raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
14923
+ raw = JSON.parse(readFileSync25(resolvedFile, "utf8"));
14579
14924
  } catch {
14580
14925
  return;
14581
14926
  }
14582
14927
  if (raw.shipName === name) return;
14583
14928
  raw.shipName = name;
14584
- mkdirSync17(dirname14(resolvedFile), { recursive: true });
14929
+ mkdirSync18(dirname15(resolvedFile), { recursive: true });
14585
14930
  const tmp = `${resolvedFile}.tmp${process.pid}`;
14586
- writeFileSync19(tmp, `${JSON.stringify(raw, null, 2)}
14931
+ writeFileSync20(tmp, `${JSON.stringify(raw, null, 2)}
14587
14932
  `);
14588
14933
  renameSync2(tmp, resolvedFile);
14589
14934
  }
14590
14935
  async function reconcileShipName(i) {
14591
14936
  let local = i.ship.name;
14592
14937
  try {
14593
- local = shipNameOfYaml(readFileSync24(i.configFile, "utf8")) ?? local;
14938
+ local = shipNameOfYaml(readFileSync25(i.configFile, "utf8")) ?? local;
14594
14939
  } catch {
14595
14940
  }
14596
14941
  const resolvedFile = resolvedPathFor(i.stateDir, i.route);
@@ -14614,8 +14959,8 @@ async function reconcileShipName(i) {
14614
14959
  }
14615
14960
  if (result.status !== "ok") return result;
14616
14961
  if (result.action === "adopt" || result.action === "row-wins") {
14617
- const text = readFileSync24(i.configFile, "utf8");
14618
- writeFileSync19(i.configFile, setShipNameInYaml(text, result.name));
14962
+ const text = readFileSync25(i.configFile, "utf8");
14963
+ writeFileSync20(i.configFile, setShipNameInYaml(text, result.name));
14619
14964
  i.ship.name = result.name;
14620
14965
  if (result.action === "row-wins") {
14621
14966
  i.emit?.warn(
@@ -15265,8 +15610,8 @@ function describeCapability(i) {
15265
15610
  }
15266
15611
 
15267
15612
  // src/agents-sync-run.ts
15268
- import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync25, writeFileSync as writeFileSync20 } from "fs";
15269
- import { dirname as dirname15 } from "path";
15613
+ import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync26, writeFileSync as writeFileSync21 } from "fs";
15614
+ import { dirname as dirname16 } from "path";
15270
15615
  async function runAgentsSync(target, o) {
15271
15616
  const dryRun = o.dryRun ?? false;
15272
15617
  const force = o.force ?? false;
@@ -15299,12 +15644,12 @@ async function runAgentsSync(target, o) {
15299
15644
  for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
15300
15645
  if (!dryRun) {
15301
15646
  const resolvedPath = resolvedPathFor(o.stateDir, target.route);
15302
- const raw = existsSync29(resolvedPath) ? JSON.parse(readFileSync25(resolvedPath, "utf8")) : {};
15647
+ const raw = existsSync30(resolvedPath) ? JSON.parse(readFileSync26(resolvedPath, "utf8")) : {};
15303
15648
  raw.agentPersonas = personaResult.agentPersonas;
15304
15649
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
15305
15650
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
15306
- mkdirSync18(dirname15(resolvedPath), { recursive: true });
15307
- writeFileSync20(resolvedPath, `${JSON.stringify(raw, null, 2)}
15651
+ mkdirSync19(dirname16(resolvedPath), { recursive: true });
15652
+ writeFileSync21(resolvedPath, `${JSON.stringify(raw, null, 2)}
15308
15653
  `);
15309
15654
  if (target.resolved) {
15310
15655
  target.resolved.agentPersonas = personaResult.agentPersonas;
@@ -15408,7 +15753,7 @@ async function showLogEntry(route, entryId, opts = {}) {
15408
15753
 
15409
15754
  // src/repos-check.ts
15410
15755
  import { spawnSync as spawnSync6 } from "child_process";
15411
- import { readFileSync as readFileSync26 } from "fs";
15756
+ import { readFileSync as readFileSync27 } from "fs";
15412
15757
  import { basename as basename8 } from "path";
15413
15758
  var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
15414
15759
  function checkHookSyntax(script, shell) {
@@ -15449,7 +15794,7 @@ function checkRepoSpec(dir, opts = {}) {
15449
15794
  }
15450
15795
  let parsed;
15451
15796
  try {
15452
- parsed = parseRepoConfig(readFileSync26(file, "utf8"), file);
15797
+ parsed = parseRepoConfig(readFileSync27(file, "utf8"), file);
15453
15798
  } catch (e) {
15454
15799
  if (!(e instanceof RepoConfigError)) throw e;
15455
15800
  return { ...empty, file, error: e.message, exitCode: 1 };
@@ -15502,12 +15847,12 @@ function renderRepoCheck(r) {
15502
15847
  }
15503
15848
 
15504
15849
  // src/repos-hooks.ts
15505
- import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml";
15850
+ import { isMap as isMap2, parseDocument as parseDocument4, YAMLMap } from "yaml";
15506
15851
 
15507
15852
  // src/repos-cmd.ts
15508
15853
  import { homedir as homedir9 } from "os";
15509
- import { basename as basename9, join as join31, resolve as resolve3 } from "path";
15510
- import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
15854
+ import { basename as basename9, join as join32, resolve as resolve3 } from "path";
15855
+ import { isMap, isSeq, parseDocument as parseDocument3, stringify as stringify2 } from "yaml";
15511
15856
  var ReposError = class extends Error {
15512
15857
  };
15513
15858
  function normalizeRemote(url) {
@@ -15531,7 +15876,7 @@ function matchRepoName(origin, resolved) {
15531
15876
  return void 0;
15532
15877
  }
15533
15878
  function routeNamesOf(text) {
15534
- const routes = parseDocument2(text).get("routes", true);
15879
+ const routes = parseDocument3(text).get("routes", true);
15535
15880
  if (!isSeq(routes)) return [];
15536
15881
  return routes.items.flatMap((i) => isMap(i) && typeof i.get("route") === "string" ? [i.get("route")] : []);
15537
15882
  }
@@ -15566,7 +15911,7 @@ function routeMapOf(doc, route) {
15566
15911
  throw new ReposError(`no route "${route}" in the config (have: ${have})`);
15567
15912
  }
15568
15913
  function configuredRepos(text, route, base) {
15569
- const doc = parseDocument2(text);
15914
+ const doc = parseDocument3(text);
15570
15915
  const m = routeMapOf(doc, route);
15571
15916
  const repos = m.get("repos");
15572
15917
  if (isMap(repos)) {
@@ -15581,7 +15926,7 @@ function configuredRepos(text, route, base) {
15581
15926
  return dir ? [{ name: basename9(String(dir)), dir: expandDir(String(dir), base) }] : [];
15582
15927
  }
15583
15928
  function cloneBaseOf(text, route, base, isCheckout) {
15584
- const m = routeMapOf(parseDocument2(text), route);
15929
+ const m = routeMapOf(parseDocument3(text), route);
15585
15930
  const repos = m.get("repos", true);
15586
15931
  if (isMap(repos) && repos.items.length > 0) return null;
15587
15932
  const dir = m.get("dir");
@@ -15590,7 +15935,7 @@ function cloneBaseOf(text, route, base, isCheckout) {
15590
15935
  return isCheckout(abs) ? null : abs;
15591
15936
  }
15592
15937
  function routeSetsHooks(text, route, name) {
15593
- const m = routeMapOf(parseDocument2(text), route);
15938
+ const m = routeMapOf(parseDocument3(text), route);
15594
15939
  const repos = m.get("repos", true);
15595
15940
  let target = m;
15596
15941
  if (isMap(repos)) target = repos.get(name, true);
@@ -15647,7 +15992,7 @@ function listRepoRows(i) {
15647
15992
  return rows;
15648
15993
  }
15649
15994
  function planRepoAdd(i) {
15650
- const doc = parseDocument2(i.text);
15995
+ const doc = parseDocument3(i.text);
15651
15996
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
15652
15997
  const m = routeMapOf(doc, i.route);
15653
15998
  let existing = configuredRepos(i.text, i.route, i.base);
@@ -15698,7 +16043,7 @@ function planRepoAdd(i) {
15698
16043
  return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched, droppedDir };
15699
16044
  }
15700
16045
  function planRepoRemove(text, base, route, path) {
15701
- const doc = parseDocument2(text);
16046
+ const doc = parseDocument3(text);
15702
16047
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
15703
16048
  const m = routeMapOf(doc, route);
15704
16049
  const target = expandDir(path, base);
@@ -15776,7 +16121,7 @@ function planRepoClone(i) {
15776
16121
  `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\``
15777
16122
  );
15778
16123
  }
15779
- const doc = parseDocument2(i.text);
16124
+ const doc = parseDocument3(i.text);
15780
16125
  const m = routeMapOf(doc, i.route);
15781
16126
  const existing = configuredRepos(i.text, i.route, i.base);
15782
16127
  if (existing.some((e) => e.name.toLowerCase() === name.toLowerCase())) {
@@ -15784,7 +16129,7 @@ function planRepoClone(i) {
15784
16129
  }
15785
16130
  const ship = doc.get("ship", true);
15786
16131
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
15787
- const target = i.dir ? expandDir(i.dir, i.base) : join31(expandDir(basePath, i.base), i.route.split("/")[0], name);
16132
+ const target = i.dir ? expandDir(i.dir, i.base) : join32(expandDir(basePath, i.base), i.route.split("/")[0], name);
15788
16133
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
15789
16134
  return { ...add, remote, target };
15790
16135
  }
@@ -15930,7 +16275,7 @@ function repoDirOf(text, base, route, name) {
15930
16275
  return hit.dir;
15931
16276
  }
15932
16277
  function planRouteHooks(text, route, name, hooks, opts = {}) {
15933
- const doc = parseDocument3(text);
16278
+ const doc = parseDocument4(text);
15934
16279
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
15935
16280
  const m = routeMapOf(doc, route);
15936
16281
  const repos = m.get("repos", true);
@@ -15967,7 +16312,7 @@ function planRouteHooks(text, route, name, hooks, opts = {}) {
15967
16312
  return { text: String(doc), written, kept, replaced };
15968
16313
  }
15969
16314
  function clearRouteHooks(text, route, name) {
15970
- const doc = parseDocument3(text);
16315
+ const doc = parseDocument4(text);
15971
16316
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
15972
16317
  const m = routeMapOf(doc, route);
15973
16318
  const repos = m.get("repos", true);
@@ -16011,7 +16356,7 @@ function existingRepoConfig(f) {
16011
16356
  }
16012
16357
 
16013
16358
  // src/repos-hooks-agent.ts
16014
- import { spawn as spawn4, execFileSync as execFileSync5 } from "child_process";
16359
+ import { spawn as spawn4, execFileSync as execFileSync6 } from "child_process";
16015
16360
  import { realpathSync as realpathSync2 } from "fs";
16016
16361
  import { isAbsolute as isAbsolute2, relative } from "path";
16017
16362
  var AgentSuggestError = class extends Error {
@@ -16114,7 +16459,7 @@ function classifyFailure(o) {
16114
16459
  }
16115
16460
  function porcelain(dir) {
16116
16461
  try {
16117
- return execFileSync5("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
16462
+ return execFileSync6("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
16118
16463
  } catch {
16119
16464
  return void 0;
16120
16465
  }
@@ -16183,7 +16528,7 @@ ${run2.err}` });
16183
16528
  }
16184
16529
 
16185
16530
  // src/sample-ticket.ts
16186
- import { basename as basename10, dirname as dirname17, join as join32 } from "path";
16531
+ import { basename as basename10, dirname as dirname18, join as join33 } from "path";
16187
16532
  var SAMPLE_REPO = "crew-sample";
16188
16533
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
16189
16534
  var SAMPLE_MARKER = "sample: true";
@@ -16206,8 +16551,8 @@ has something safe to work on: its test and build hooks always pass, it has no
16206
16551
  remote, and \`crew sample-ticket --remove\` deletes it.
16207
16552
  `;
16208
16553
  function sampleDir(existing, reposBase, workspace) {
16209
- if (existing.length > 0) return join32(dirname17(existing[0]), SAMPLE_REPO);
16210
- return join32(reposBase, workspace, SAMPLE_REPO);
16554
+ if (existing.length > 0) return join33(dirname18(existing[0]), SAMPLE_REPO);
16555
+ return join33(reposBase, workspace, SAMPLE_REPO);
16211
16556
  }
16212
16557
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
16213
16558
  function sampleDescription() {
@@ -16251,7 +16596,7 @@ function planSampleTicket(i) {
16251
16596
  }
16252
16597
 
16253
16598
  // src/doctor-fix.ts
16254
- import { parseDocument as parseDocument4 } from "yaml";
16599
+ import { parseDocument as parseDocument5 } from "yaml";
16255
16600
  var sound = (r) => r.hasRepos && r.problems.length === 0;
16256
16601
  function planFix(f) {
16257
16602
  const actions = [];
@@ -16289,7 +16634,7 @@ function routesInScope(routes, named) {
16289
16634
  return named ? routes.filter((r) => r.route === named) : routes;
16290
16635
  }
16291
16636
  function planEnable(text, route) {
16292
- const doc = parseDocument4(text);
16637
+ const doc = parseDocument5(text);
16293
16638
  routeMapOf(doc, route).set("enabled", true);
16294
16639
  return String(doc);
16295
16640
  }
@@ -16299,8 +16644,8 @@ function schedulerInstalled(plan, exists, crontab) {
16299
16644
  }
16300
16645
 
16301
16646
  // src/worktree-sweep.ts
16302
- import { existsSync as existsSync30, readFileSync as readFileSync27, readdirSync as readdirSync12 } from "fs";
16303
- import { basename as basename11, dirname as dirname18, join as join33, resolve as resolve4 } from "path";
16647
+ import { existsSync as existsSync31, readFileSync as readFileSync28, readdirSync as readdirSync12 } from "fs";
16648
+ import { basename as basename11, dirname as dirname19, join as join34, resolve as resolve4 } from "path";
16304
16649
  function planWorktreeSweep(target, tickets, contract) {
16305
16650
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
16306
16651
  const verified = contract.statuses.verified;
@@ -16364,7 +16709,7 @@ function worktreeTicketNumbers(target) {
16364
16709
  return [...nums];
16365
16710
  }
16366
16711
  function siblingWorktreeDirs(target) {
16367
- const parent = dirname18(target.dir.replace(/[/\\]+$/, ""));
16712
+ const parent = dirname19(target.dir.replace(/[/\\]+$/, ""));
16368
16713
  const stem = `${basename11(target.dir.replace(/[/\\]+$/, ""))}-`;
16369
16714
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
16370
16715
  let names;
@@ -16377,7 +16722,7 @@ function siblingWorktreeDirs(target) {
16377
16722
  for (const name of names) {
16378
16723
  if (!name.startsWith(stem)) continue;
16379
16724
  const m = DIR_NUMBER_SUFFIXED.exec(name);
16380
- const path = join33(parent, name);
16725
+ const path = join34(parent, name);
16381
16726
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
16382
16727
  }
16383
16728
  return out;
@@ -16412,7 +16757,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
16412
16757
  let removed = 0;
16413
16758
  let keptBranches = 0;
16414
16759
  for (const a of actions) {
16415
- if (!dryRun && base && !a.keepBranch && existsSync30(a.path) && isHandBackMerge(a.path, base)) {
16760
+ if (!dryRun && base && !a.keepBranch && existsSync31(a.path) && isHandBackMerge(a.path, base)) {
16416
16761
  if (gitOk(a.path, ["merge", "--abort"]) !== null) {
16417
16762
  log.emit(
16418
16763
  `abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
@@ -16420,7 +16765,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
16420
16765
  );
16421
16766
  }
16422
16767
  }
16423
- if (!dryRun && existsSync30(a.path)) {
16768
+ if (!dryRun && existsSync31(a.path)) {
16424
16769
  let dirty = false;
16425
16770
  try {
16426
16771
  dirty = status(a.path).length > 0;
@@ -16431,7 +16776,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
16431
16776
  continue;
16432
16777
  }
16433
16778
  }
16434
- if (!dryRun && a.requirePushed && a.branch && existsSync30(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
16779
+ if (!dryRun && a.requirePushed && a.branch && existsSync31(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
16435
16780
  refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
16436
16781
  continue;
16437
16782
  }
@@ -16480,7 +16825,7 @@ function isHandBackMerge(path, base) {
16480
16825
  if (!msgPath) return false;
16481
16826
  let first;
16482
16827
  try {
16483
- first = readFileSync27(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
16828
+ first = readFileSync28(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
16484
16829
  } catch {
16485
16830
  return false;
16486
16831
  }
@@ -16585,16 +16930,16 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
16585
16930
  }
16586
16931
 
16587
16932
  // src/stream-sweep.ts
16588
- import { existsSync as existsSync31, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync8 } from "fs";
16589
- import { join as join34 } from "path";
16933
+ import { existsSync as existsSync32, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9 } from "fs";
16934
+ import { join as join35 } from "path";
16590
16935
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
16591
16936
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
16592
- const dir = join34(stateDir, "streams");
16593
- if (!existsSync31(dir)) return [];
16937
+ const dir = join35(stateDir, "streams");
16938
+ if (!existsSync32(dir)) return [];
16594
16939
  const cutoff = now - retentionDays * DAY_MS2;
16595
- return readdirSync13(dir).map((name) => join34(dir, name)).filter((path) => {
16940
+ return readdirSync13(dir).map((name) => join35(dir, name)).filter((path) => {
16596
16941
  try {
16597
- return statSync8(path).mtimeMs < cutoff;
16942
+ return statSync9(path).mtimeMs < cutoff;
16598
16943
  } catch {
16599
16944
  return false;
16600
16945
  }
@@ -16618,11 +16963,11 @@ function applyStreamSweep(paths, dryRun, log) {
16618
16963
  return removed;
16619
16964
  }
16620
16965
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
16621
- if (!existsSync31(stateDir)) return [];
16966
+ if (!existsSync32(stateDir)) return [];
16622
16967
  const cutoff = now - retentionDays * DAY_MS2;
16623
- return readdirSync13(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join34(stateDir, name)).filter((path) => {
16968
+ return readdirSync13(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join35(stateDir, name)).filter((path) => {
16624
16969
  try {
16625
- return statSync8(path).mtimeMs < cutoff;
16970
+ return statSync9(path).mtimeMs < cutoff;
16626
16971
  } catch {
16627
16972
  return false;
16628
16973
  }
@@ -16630,30 +16975,30 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
16630
16975
  }
16631
16976
 
16632
16977
  // src/cli.ts
16633
- import { realpathSync as realpathSync3, readFileSync as readFileSync33, existsSync as existsSync37, mkdirSync as mkdirSync24, writeFileSync as writeFileSync25, rmSync as rmSync17, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync11, readdirSync as readdirSync17 } from "fs";
16978
+ import { realpathSync as realpathSync3, readFileSync as readFileSync34, existsSync as existsSync38, mkdirSync as mkdirSync25, writeFileSync as writeFileSync26, rmSync as rmSync17, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync12, readdirSync as readdirSync17 } from "fs";
16634
16979
  import { createInterface } from "readline/promises";
16635
16980
 
16636
16981
  // src/periscope-cmd.ts
16637
- import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync28, rmSync as rmSync15, writeFileSync as writeFileSync21 } from "fs";
16638
- import { join as join35 } from "path";
16982
+ import { existsSync as existsSync33, mkdirSync as mkdirSync20, readFileSync as readFileSync29, rmSync as rmSync15, writeFileSync as writeFileSync22 } from "fs";
16983
+ import { join as join36 } from "path";
16639
16984
  var PeriscopeError = class extends Error {
16640
16985
  };
16641
- var priorDir = (stateDir) => join35(stateDir, "periscope-priors");
16642
- var priorPath = (stateDir, ticketRecordId) => join35(priorDir(stateDir), `${ticketRecordId}.json`);
16986
+ var priorDir = (stateDir) => join36(stateDir, "periscope-priors");
16987
+ var priorPath = (stateDir, ticketRecordId) => join36(priorDir(stateDir), `${ticketRecordId}.json`);
16643
16988
  function removePeriscopePrior(stateDir, ticketRecordId) {
16644
16989
  rmSync15(priorPath(stateDir, ticketRecordId), { force: true });
16645
16990
  }
16646
16991
  function writePeriscopePrior(stateDir, ticketRecordId, prior) {
16647
16992
  try {
16648
- mkdirSync19(priorDir(stateDir), { recursive: true });
16649
- writeFileSync21(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
16993
+ mkdirSync20(priorDir(stateDir), { recursive: true });
16994
+ writeFileSync22(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
16650
16995
  } catch {
16651
16996
  }
16652
16997
  }
16653
16998
  function readPeriscopePrior(stateDir, ticketRecordId) {
16654
16999
  try {
16655
17000
  const p = priorPath(stateDir, ticketRecordId);
16656
- return existsSync32(p) ? JSON.parse(readFileSync28(p, "utf8")) : void 0;
17001
+ return existsSync33(p) ? JSON.parse(readFileSync29(p, "utf8")) : void 0;
16657
17002
  } catch {
16658
17003
  return void 0;
16659
17004
  }
@@ -17044,14 +17389,14 @@ ${tail2(preview.output)}`);
17044
17389
  }
17045
17390
 
17046
17391
  // src/periscope-idle.ts
17047
- import { execFileSync as execFileSync6 } from "child_process";
17048
- import { statSync as statSync9 } from "fs";
17049
- import { join as join36 } from "path";
17392
+ import { execFileSync as execFileSync7 } from "child_process";
17393
+ import { statSync as statSync10 } from "fs";
17394
+ import { join as join37 } from "path";
17050
17395
  var MAX_SCANNED_FILES = 2e4;
17051
17396
  function newestWorktreeMtime(root) {
17052
17397
  let listing;
17053
17398
  try {
17054
- listing = execFileSync6("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
17399
+ listing = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
17055
17400
  cwd: root,
17056
17401
  encoding: "utf8",
17057
17402
  stdio: ["ignore", "pipe", "ignore"],
@@ -17066,7 +17411,7 @@ function newestWorktreeMtime(root) {
17066
17411
  if (!rel) continue;
17067
17412
  if (++scanned > MAX_SCANNED_FILES) break;
17068
17413
  try {
17069
- const m = statSync9(join36(root, rel)).mtimeMs;
17414
+ const m = statSync10(join37(root, rel)).mtimeMs;
17070
17415
  if (newest === null || m > newest) newest = m;
17071
17416
  } catch {
17072
17417
  }
@@ -17141,21 +17486,21 @@ async function runPeriscopeIdlePass(d) {
17141
17486
  import { dirname as dirOf2, resolve as resolvePath } from "path";
17142
17487
 
17143
17488
  // src/ssh-keys.ts
17144
- import { execFileSync as execFileSync7 } from "child_process";
17145
- import { existsSync as existsSync33, mkdirSync as mkdirSync20, readFileSync as readFileSync29, chmodSync } from "fs";
17146
- import { dirname as dirname19, join as join37 } from "path";
17489
+ import { execFileSync as execFileSync8 } from "child_process";
17490
+ import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync30, chmodSync as chmodSync2 } from "fs";
17491
+ import { dirname as dirname20, join as join38 } from "path";
17147
17492
  var SshKeyError = class extends Error {
17148
17493
  };
17149
17494
  function shipSshKeyPathFor(stateDir) {
17150
- return join37(stateDir, "ssh", "id_ed25519");
17495
+ return join38(stateDir, "ssh", "id_ed25519");
17151
17496
  }
17152
17497
  function ensureShipSshKeypair(stateDir) {
17153
17498
  const privateKeyPath = shipSshKeyPathFor(stateDir);
17154
17499
  const publicKeyPath = `${privateKeyPath}.pub`;
17155
- if (!existsSync33(privateKeyPath)) {
17156
- mkdirSync20(dirname19(privateKeyPath), { recursive: true });
17500
+ if (!existsSync34(privateKeyPath)) {
17501
+ mkdirSync21(dirname20(privateKeyPath), { recursive: true });
17157
17502
  try {
17158
- execFileSync7("ssh-keygen", [
17503
+ execFileSync8("ssh-keygen", [
17159
17504
  "-t",
17160
17505
  "ed25519",
17161
17506
  "-N",
@@ -17171,17 +17516,17 @@ function ensureShipSshKeypair(stateDir) {
17171
17516
  const err = e;
17172
17517
  throw new SshKeyError(`ssh-keygen failed: ${err.stderr?.toString().trim() || err.message}`);
17173
17518
  }
17174
- chmodSync(privateKeyPath, 384);
17519
+ chmodSync2(privateKeyPath, 384);
17175
17520
  }
17176
- if (!existsSync33(publicKeyPath)) {
17521
+ if (!existsSync34(publicKeyPath)) {
17177
17522
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
17178
17523
  }
17179
- const publicKey = readFileSync29(publicKeyPath, "utf8").trim();
17524
+ const publicKey = readFileSync30(publicKeyPath, "utf8").trim();
17180
17525
  return { privateKeyPath, publicKeyPath, publicKey };
17181
17526
  }
17182
17527
  function sshKeygenAvailable() {
17183
17528
  try {
17184
- execFileSync7("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
17529
+ execFileSync8("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
17185
17530
  return true;
17186
17531
  } catch (e) {
17187
17532
  return e.code !== "ENOENT";
@@ -17189,11 +17534,11 @@ function sshKeygenAvailable() {
17189
17534
  }
17190
17535
 
17191
17536
  // src/passenger-containers.ts
17192
- import { execFileSync as execFileSync8 } from "child_process";
17537
+ import { execFileSync as execFileSync9 } from "child_process";
17193
17538
  import { createHash as createHash6 } from "crypto";
17194
17539
  var realExec = (cmd, args, opts) => {
17195
17540
  try {
17196
- return execFileSync8(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
17541
+ return execFileSync9(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
17197
17542
  } catch (e) {
17198
17543
  const err = e;
17199
17544
  const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
@@ -17393,9 +17738,9 @@ function syncAllPassengerCheckouts(cfg) {
17393
17738
 
17394
17739
  // src/tunnel.ts
17395
17740
  import { spawn as spawn5 } from "child_process";
17396
- import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync30, writeFileSync as writeFileSync22, unlinkSync as unlinkSync3, readdirSync as readdirSync14 } from "fs";
17741
+ import { existsSync as existsSync35, mkdirSync as mkdirSync22, readFileSync as readFileSync31, writeFileSync as writeFileSync23, unlinkSync as unlinkSync3, readdirSync as readdirSync14 } from "fs";
17397
17742
  import { randomInt } from "crypto";
17398
- import { join as join38 } from "path";
17743
+ import { join as join39 } from "path";
17399
17744
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
17400
17745
  var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
17401
17746
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -17462,21 +17807,21 @@ function startTunnel(opts) {
17462
17807
  };
17463
17808
  }
17464
17809
  function tunnelStatePath(stateDir, workspaceId) {
17465
- return join38(stateDir, "passengers", `${workspaceId}.json`);
17810
+ return join39(stateDir, "passengers", `${workspaceId}.json`);
17466
17811
  }
17467
17812
  function readPersistedTunnel(stateDir, workspaceId) {
17468
17813
  const path = tunnelStatePath(stateDir, workspaceId);
17469
- if (!existsSync34(path)) return void 0;
17814
+ if (!existsSync35(path)) return void 0;
17470
17815
  try {
17471
- return JSON.parse(readFileSync30(path, "utf8"));
17816
+ return JSON.parse(readFileSync31(path, "utf8"));
17472
17817
  } catch {
17473
17818
  return void 0;
17474
17819
  }
17475
17820
  }
17476
17821
  function writePersistedTunnel(stateDir, state) {
17477
17822
  const path = tunnelStatePath(stateDir, state.workspaceId);
17478
- mkdirSync21(join38(stateDir, "passengers"), { recursive: true });
17479
- writeFileSync22(path, `${JSON.stringify(state, null, 2)}
17823
+ mkdirSync22(join39(stateDir, "passengers"), { recursive: true });
17824
+ writeFileSync23(path, `${JSON.stringify(state, null, 2)}
17480
17825
  `);
17481
17826
  }
17482
17827
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -17573,8 +17918,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
17573
17918
  }
17574
17919
  }
17575
17920
  function readAllPersistedWorkspaceIds(stateDir) {
17576
- const dir = join38(stateDir, "passengers");
17577
- if (!existsSync34(dir)) return [];
17921
+ const dir = join39(stateDir, "passengers");
17922
+ if (!existsSync35(dir)) return [];
17578
17923
  return readdirSync14(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
17579
17924
  }
17580
17925
 
@@ -17695,8 +18040,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
17695
18040
  import { createServer as createHttpServer } from "http";
17696
18041
  import { spawn as spawn6 } from "child_process";
17697
18042
  import { randomBytes } from "crypto";
17698
- import { existsSync as existsSync35, mkdirSync as mkdirSync22, readFileSync as readFileSync31, writeFileSync as writeFileSync23, unlinkSync as unlinkSync4, readdirSync as readdirSync15 } from "fs";
17699
- import { join as join39 } from "path";
18043
+ import { existsSync as existsSync36, mkdirSync as mkdirSync23, readFileSync as readFileSync32, writeFileSync as writeFileSync24, unlinkSync as unlinkSync4, readdirSync as readdirSync15 } from "fs";
18044
+ import { join as join40 } from "path";
17700
18045
 
17701
18046
  // src/secret-compare.ts
17702
18047
  import { timingSafeEqual } from "crypto";
@@ -17785,21 +18130,21 @@ var defaultSpawn2 = (cmd, args, env) => {
17785
18130
  return child;
17786
18131
  };
17787
18132
  function syncDaemonStatePath(stateDir, workspaceId) {
17788
- return join39(stateDir, "passengers", `${workspaceId}-sync.json`);
18133
+ return join40(stateDir, "passengers", `${workspaceId}-sync.json`);
17789
18134
  }
17790
18135
  function readPersistedSyncDaemon(stateDir, workspaceId) {
17791
18136
  const path = syncDaemonStatePath(stateDir, workspaceId);
17792
- if (!existsSync35(path)) return void 0;
18137
+ if (!existsSync36(path)) return void 0;
17793
18138
  try {
17794
- return JSON.parse(readFileSync31(path, "utf8"));
18139
+ return JSON.parse(readFileSync32(path, "utf8"));
17795
18140
  } catch {
17796
18141
  return void 0;
17797
18142
  }
17798
18143
  }
17799
18144
  function writePersistedSyncDaemon(stateDir, state) {
17800
18145
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
17801
- mkdirSync22(join39(stateDir, "passengers"), { recursive: true });
17802
- writeFileSync23(path, `${JSON.stringify(state, null, 2)}
18146
+ mkdirSync23(join40(stateDir, "passengers"), { recursive: true });
18147
+ writeFileSync24(path, `${JSON.stringify(state, null, 2)}
17803
18148
  `);
17804
18149
  }
17805
18150
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -17819,8 +18164,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
17819
18164
  }
17820
18165
  }
17821
18166
  function readAllPersistedSyncWorkspaceIds(stateDir) {
17822
- const dir = join39(stateDir, "passengers");
17823
- if (!existsSync35(dir)) return [];
18167
+ const dir = join40(stateDir, "passengers");
18168
+ if (!existsSync36(dir)) return [];
17824
18169
  const suffix = "-sync.json";
17825
18170
  return readdirSync15(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
17826
18171
  }
@@ -17908,8 +18253,8 @@ function describeCause(err) {
17908
18253
  }
17909
18254
 
17910
18255
  // src/daemon.ts
17911
- import { existsSync as existsSync36, mkdirSync as mkdirSync23, readFileSync as readFileSync32, readdirSync as readdirSync16, rmSync as rmSync16, statSync as statSync10, writeFileSync as writeFileSync24 } from "fs";
17912
- import { join as join40 } from "path";
18256
+ import { existsSync as existsSync37, mkdirSync as mkdirSync24, readFileSync as readFileSync33, readdirSync as readdirSync16, rmSync as rmSync16, statSync as statSync11, writeFileSync as writeFileSync25 } from "fs";
18257
+ import { join as join41 } from "path";
17913
18258
  import { randomBytes as randomBytes2 } from "crypto";
17914
18259
  var DEFAULT_FLOOR_MS = 5e3;
17915
18260
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
@@ -17972,7 +18317,7 @@ async function runDaemonLoop(o) {
17972
18317
  function latestMtime(path) {
17973
18318
  let stat;
17974
18319
  try {
17975
- stat = statSync10(path);
18320
+ stat = statSync11(path);
17976
18321
  } catch {
17977
18322
  return 0;
17978
18323
  }
@@ -17980,7 +18325,7 @@ function latestMtime(path) {
17980
18325
  if (!stat.isDirectory()) return 0;
17981
18326
  let latest = stat.mtimeMs;
17982
18327
  for (const entry of readdirSync16(path, { withFileTypes: true })) {
17983
- latest = Math.max(latest, latestMtime(join40(path, entry.name)));
18328
+ latest = Math.max(latest, latestMtime(join41(path, entry.name)));
17984
18329
  }
17985
18330
  return latest;
17986
18331
  }
@@ -17989,23 +18334,23 @@ function computeTreeSignature(paths) {
17989
18334
  }
17990
18335
  function makeStaleChecker(paths, opts = {}) {
17991
18336
  const initial = computeTreeSignature(paths);
17992
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync36(opts.reloadSentinel);
18337
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync37(opts.reloadSentinel);
17993
18338
  }
17994
18339
  function reloadSentinelFor(stateDir) {
17995
- return join40(stateDir, "reload-requested");
18340
+ return join41(stateDir, "reload-requested");
17996
18341
  }
17997
18342
  function daemonStampFor(stateDir) {
17998
- return join40(stateDir, "daemon.started");
18343
+ return join41(stateDir, "daemon.started");
17999
18344
  }
18000
18345
  function noteDaemonStarted(stateDir, now = Date.now()) {
18001
- mkdirSync23(stateDir, { recursive: true });
18346
+ mkdirSync24(stateDir, { recursive: true });
18002
18347
  rmSync16(reloadSentinelFor(stateDir), { force: true });
18003
- writeFileSync24(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
18348
+ writeFileSync25(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
18004
18349
  `);
18005
18350
  }
18006
18351
  function readDaemonStarted(stateDir) {
18007
18352
  try {
18008
- const v = JSON.parse(readFileSync32(daemonStampFor(stateDir), "utf8")).startedAt;
18353
+ const v = JSON.parse(readFileSync33(daemonStampFor(stateDir), "utf8")).startedAt;
18009
18354
  return typeof v === "number" ? v : null;
18010
18355
  } catch {
18011
18356
  return null;
@@ -18014,7 +18359,7 @@ function readDaemonStarted(stateDir) {
18014
18359
  function configChangedAfter(files, startedAt) {
18015
18360
  for (const f of files) {
18016
18361
  try {
18017
- if (statSync10(f).mtimeMs > startedAt) return f;
18362
+ if (statSync11(f).mtimeMs > startedAt) return f;
18018
18363
  } catch {
18019
18364
  }
18020
18365
  }
@@ -18485,7 +18830,7 @@ async function buildEnvironment(route, ship, ticket) {
18485
18830
  }
18486
18831
 
18487
18832
  // src/cli.ts
18488
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname20(process.execPath) : resolve5(dirname20(fileURLToPath(import.meta.url)), "..");
18833
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname21(process.execPath) : resolve5(dirname21(fileURLToPath(import.meta.url)), "..");
18489
18834
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
18490
18835
  var CYCLE_SECONDS2 = 120;
18491
18836
  var STALE_CYCLES = 3;
@@ -18617,6 +18962,8 @@ function usage(problem, requested = false) {
18617
18962
  crew --version [--json] print the crew version on one line (also: crew version); --json adds home and compiled
18618
18963
  crew status [route] [--json] paused/running state; --json for a machine reader
18619
18964
  crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
18965
+ crew forge [set|rotate <host>] [--token T | --token-stdin] [--source file|keychain|env] this ship's git-forge tokens (ship.forges)
18966
+ crew credential get|store|erase git credential helper answering from ship.forges (git runs it; not for hand use)
18620
18967
  crew whoami [route] [--json] the signed-in identity, key role, operator Crew row and credential source per route
18621
18968
  crew reload make the running daemon re-read crew.yaml (--now: restart it immediately)
18622
18969
  crew doctor [route] [--fix] [--offline] [--json] preflight; --fix enables clean routes and offers crew install;
@@ -18714,6 +19061,8 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
18714
19061
  "passenger-sync-daemon",
18715
19062
  "agents",
18716
19063
  "connect",
19064
+ "credential",
19065
+ "forge",
18717
19066
  "daemon",
18718
19067
  "deploy",
18719
19068
  "disconnect",
@@ -18917,9 +19266,9 @@ if (command === "passenger-sync-daemon") {
18917
19266
  firstRun = { answers, configPath };
18918
19267
  cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
18919
19268
  } else {
18920
- const toleratesNoRoutes = command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
19269
+ const toleratesNoRoutes = command === "credential" || command === "forge" || command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
18921
19270
  const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
18922
- if (configless && !existsSync37(findConfigFile(CREW_HOME))) {
19271
+ if (configless && !existsSync38(findConfigFile(CREW_HOME))) {
18923
19272
  cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
18924
19273
  } else {
18925
19274
  cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
@@ -18933,6 +19282,33 @@ if (command === "passenger-sync-daemon") {
18933
19282
  }
18934
19283
  throw e;
18935
19284
  }
19285
+ if (command === "credential") {
19286
+ const r = runCredential(positional[1], readFileSync34(0, "utf8"), cfg.ship.forges, cfg.ship.stateDir);
19287
+ process.stdout.write(r.out);
19288
+ process.stderr.write(r.err);
19289
+ process.exit(r.code);
19290
+ }
19291
+ if (command === "forge") {
19292
+ const r = runForge(
19293
+ { sub: positional[1], host: positional[2], token: value("token"), tokenStdin: flag("token-stdin"), source: value("source"), var: value("var"), path: value("path"), service: value("service") },
19294
+ {
19295
+ forges: cfg.ship.forges,
19296
+ stateDir: cfg.ship.stateDir,
19297
+ configFile: cfg.configFile,
19298
+ readFile: (p) => readFileSync34(p, "utf8"),
19299
+ writeFile: (p, t) => writeFileSync26(p, t),
19300
+ stdin: () => readFileSync34(0, "utf8"),
19301
+ env: process.env
19302
+ }
19303
+ );
19304
+ process.stdout.write(r.out);
19305
+ process.stderr.write(r.err);
19306
+ process.exit(r.code);
19307
+ }
19308
+ if (cfg.ship.forges) {
19309
+ Object.assign(process.env, gitCredentialEnv({ crewCommand: crewCommandLine(), configFile: cfg.configFile }));
19310
+ setForgeTokenHosts(Object.keys(cfg.ship.forges).filter((h) => hasForgeToken(cfg.ship.forges, h, cfg.ship.stateDir)));
19311
+ }
18936
19312
  await hydrateApiKeys(cfg.routes);
18937
19313
  for (const w of cfg.warnings) process.stderr.write(`crew: warning: ${w}
18938
19314
  `);
@@ -19157,8 +19533,8 @@ if (command === "passenger-sync-daemon") {
19157
19533
  }
19158
19534
  if (host === "linux" && hasSystemd) {
19159
19535
  const label = labelFor(CREW_HOME, "run");
19160
- const staleTimer = join41(homedir10(), ".config", "systemd", "user", `${label}.timer`);
19161
- if (existsSync37(staleTimer)) {
19536
+ const staleTimer = join42(homedir10(), ".config", "systemd", "user", `${label}.timer`);
19537
+ if (existsSync38(staleTimer)) {
19162
19538
  process.stdout.write(
19163
19539
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
19164
19540
  `
@@ -19185,7 +19561,7 @@ if (command === "passenger-sync-daemon") {
19185
19561
  const tracker = new Tracker(c, cfg.ship);
19186
19562
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
19187
19563
  for (const r of await resolvedRepos(c)) {
19188
- if (!existsSync37(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
19564
+ if (!existsSync38(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
19189
19565
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
19190
19566
  readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
19191
19567
  }
@@ -19203,7 +19579,7 @@ if (command === "passenger-sync-daemon") {
19203
19579
  }
19204
19580
  async function doctorFix(o) {
19205
19581
  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))));
19206
- const unitExists = (p) => existsSync37(p);
19582
+ const unitExists = (p) => existsSync38(p);
19207
19583
  const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
19208
19584
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
19209
19585
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -19220,7 +19596,7 @@ if (command === "passenger-sync-daemon") {
19220
19596
  for (const a of plan.actions) {
19221
19597
  if (a.kind === "enable") {
19222
19598
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
19223
- writeFileSync25(cfg.configFile, planEnable(readFileSync33(cfg.configFile, "utf8"), a.route));
19599
+ writeFileSync26(cfg.configFile, planEnable(readFileSync34(cfg.configFile, "utf8"), a.route));
19224
19600
  enabledSound.add(a.route);
19225
19601
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
19226
19602
  } else {
@@ -20324,7 +20700,7 @@ ${cu.tail.trimEnd()}
20324
20700
  process.exit(1);
20325
20701
  }
20326
20702
  requireArmed("run the daemon loop");
20327
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join41(CREW_HOME, "src"), join41(CREW_HOME, "dist"), join41(CREW_HOME, "bin")];
20703
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join42(CREW_HOME, "src"), join42(CREW_HOME, "dist"), join42(CREW_HOME, "bin")];
20328
20704
  const configPaths = daemonConfigPaths(cfg);
20329
20705
  watchPaths.push(...configPaths);
20330
20706
  noteDaemonStarted(cfg.ship.stateDir);
@@ -20553,10 +20929,10 @@ ${cu.tail.trimEnd()}
20553
20929
  `);
20554
20930
  if (!dryRun) {
20555
20931
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
20556
- const raw = existsSync37(resolvedPath) ? JSON.parse(readFileSync33(resolvedPath, "utf8")) : {};
20932
+ const raw = existsSync38(resolvedPath) ? JSON.parse(readFileSync34(resolvedPath, "utf8")) : {};
20557
20933
  raw.agentSkills = result.agentSkills;
20558
- mkdirSync24(dirOf2(resolvedPath), { recursive: true });
20559
- writeFileSync25(resolvedPath, `${JSON.stringify(raw, null, 2)}
20934
+ mkdirSync25(dirOf2(resolvedPath), { recursive: true });
20935
+ writeFileSync26(resolvedPath, `${JSON.stringify(raw, null, 2)}
20560
20936
  `);
20561
20937
  }
20562
20938
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -20579,7 +20955,7 @@ ${cu.tail.trimEnd()}
20579
20955
  throw e;
20580
20956
  }
20581
20957
  const oldName = cfg.ship.name;
20582
- writeFileSync25(cfg.configFile, setShipNameInYaml(readFileSync33(cfg.configFile, "utf8"), newName));
20958
+ writeFileSync26(cfg.configFile, setShipNameInYaml(readFileSync34(cfg.configFile, "utf8"), newName));
20583
20959
  cfg.ship.name = newName;
20584
20960
  let renameMachineId;
20585
20961
  try {
@@ -20626,14 +21002,14 @@ ${cu.tail.trimEnd()}
20626
21002
  process.exit(2);
20627
21003
  };
20628
21004
  if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
20629
- const configText = readFileSync33(cfg.configFile, "utf8");
21005
+ const configText = readFileSync34(cfg.configFile, "utf8");
20630
21006
  if (sub === "check") {
20631
21007
  try {
20632
21008
  const routeArg = value("route") ?? positional[2];
20633
21009
  const found = cfg.routes.find((r) => r.route === routeArg);
20634
21010
  if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
20635
21011
  const named2 = value("route") ? void 0 : positional[3];
20636
- const dir = named2 ? repoDirOf(configText, dirname20(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
21012
+ const dir = named2 ? repoDirOf(configText, dirname21(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
20637
21013
  const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
20638
21014
  const o = target ? found.repoOverrides[target.name] : void 0;
20639
21015
  runReposCheck(dir, {
@@ -20656,9 +21032,9 @@ ${cu.tail.trimEnd()}
20656
21032
  if (sub === "hooks") {
20657
21033
  try {
20658
21034
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
20659
- const dir = repoDirOf(configText, dirname20(cfg.configFile), a.route, a.name);
20660
- if (!existsSync37(dir)) throw new ReposError(`${dir} does not exist on disk`);
20661
- const files = { exists: (r) => existsSync37(resolvePath(dir, r)), list: (r) => {
21035
+ const dir = repoDirOf(configText, dirname21(cfg.configFile), a.route, a.name);
21036
+ if (!existsSync38(dir)) throw new ReposError(`${dir} does not exist on disk`);
21037
+ const files = { exists: (r) => existsSync38(resolvePath(dir, r)), list: (r) => {
20662
21038
  try {
20663
21039
  return readdirSync17(resolvePath(dir, r));
20664
21040
  } catch {
@@ -20666,7 +21042,7 @@ ${cu.tail.trimEnd()}
20666
21042
  }
20667
21043
  }, read: (r) => {
20668
21044
  try {
20669
- return readFileSync33(resolvePath(dir, r), "utf8");
21045
+ return readFileSync34(resolvePath(dir, r), "utf8");
20670
21046
  } catch {
20671
21047
  return void 0;
20672
21048
  }
@@ -20708,19 +21084,19 @@ ${cu.tail.trimEnd()}
20708
21084
  let cleared;
20709
21085
  if (clear2) {
20710
21086
  const plan = clearRouteHooks(configText, a.route, a.name);
20711
- if (plan.removed.length > 0) writeFileSync25(cfg.configFile, plan.text);
21087
+ if (plan.removed.length > 0) writeFileSync26(cfg.configFile, plan.text);
20712
21088
  cleared = { file: cfg.configFile, removed: plan.removed };
20713
21089
  }
20714
21090
  if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
20715
21091
  if (write === "route") {
20716
21092
  const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
20717
- writeFileSync25(cfg.configFile, plan.text);
21093
+ writeFileSync26(cfg.configFile, plan.text);
20718
21094
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
20719
21095
  } else if (write === "repo") {
20720
21096
  const have = existingRepoConfig(files);
20721
21097
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
20722
21098
  const file = resolvePath(dir, ".crew.yaml");
20723
- writeFileSync25(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
21099
+ writeFileSync26(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
20724
21100
  written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
20725
21101
  }
20726
21102
  if (flag("json")) {
@@ -20776,7 +21152,7 @@ ${cu.tail.trimEnd()}
20776
21152
  if (sub === "list" && !routeName) usage2();
20777
21153
  if (sub === "add") {
20778
21154
  try {
20779
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync37(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
21155
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync38(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
20780
21156
  routeName = a.route;
20781
21157
  addPath = a.path;
20782
21158
  addName = a.name;
@@ -20805,16 +21181,16 @@ ${cu.tail.trimEnd()}
20805
21181
  }
20806
21182
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
20807
21183
  let resolvedRemotes;
20808
- if (existsSync37(resolvedFile)) {
21184
+ if (existsSync38(resolvedFile)) {
20809
21185
  try {
20810
- resolvedRemotes = JSON.parse(readFileSync33(resolvedFile, "utf8"));
21186
+ resolvedRemotes = JSON.parse(readFileSync34(resolvedFile, "utf8"));
20811
21187
  } catch {
20812
21188
  }
20813
21189
  }
20814
21190
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
20815
21191
  const text = configText;
20816
- const base = dirname20(cfg.configFile);
20817
- const isCheckout = (d) => existsSync37(`${d}/.git`);
21192
+ const base = dirname21(cfg.configFile);
21193
+ const isCheckout = (d) => existsSync38(`${d}/.git`);
20818
21194
  try {
20819
21195
  if (sub === "list") {
20820
21196
  if (flag("json")) {
@@ -20824,11 +21200,11 @@ ${cu.tail.trimEnd()}
20824
21200
  base,
20825
21201
  resolved: resolvedRemotes,
20826
21202
  originOf,
20827
- exists: (d) => existsSync37(d),
21203
+ exists: (d) => existsSync38(d),
20828
21204
  isCheckout,
20829
21205
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
20830
21206
  try {
20831
- return Object.keys(parseRepoConfig(readFileSync33(resolvePath(d, f), "utf8"), f).hooks).length > 0;
21207
+ return Object.keys(parseRepoConfig(readFileSync34(resolvePath(d, f), "utf8"), f).hooks).length > 0;
20832
21208
  } catch {
20833
21209
  return false;
20834
21210
  }
@@ -20845,7 +21221,7 @@ ${cu.tail.trimEnd()}
20845
21221
  for (const r of repos) {
20846
21222
  const origin2 = originOf(r.dir);
20847
21223
  const row = matchRepoName(origin2, resolvedRemotes);
20848
- const note = !existsSync37(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
21224
+ const note = !existsSync38(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
20849
21225
  process.stdout.write(`${r.name} ${r.dir} ${note}
20850
21226
  `);
20851
21227
  }
@@ -20857,9 +21233,9 @@ ${cu.tail.trimEnd()}
20857
21233
  ` : `${human}
20858
21234
  `);
20859
21235
  const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
20860
- if (existsSync37(plan2.target) && (existsSync37(`${plan2.target}/.git`) || readdirSync17(plan2.target).length > 0)) {
21236
+ if (existsSync38(plan2.target) && (existsSync38(`${plan2.target}/.git`) || readdirSync17(plan2.target).length > 0)) {
20861
21237
  throw new ReposError(
20862
- existsSync37(`${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`
21238
+ existsSync38(`${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`
20863
21239
  );
20864
21240
  }
20865
21241
  if (dryRun) {
@@ -20877,14 +21253,14 @@ ${cu.tail.trimEnd()}
20877
21253
  throw new ReposError(`${e.message}${authHint}
20878
21254
  crew.yaml was not changed`);
20879
21255
  }
20880
- writeFileSync25(cfg.configFile, plan2.text);
21256
+ writeFileSync26(cfg.configFile, plan2.text);
20881
21257
  if (plan2.droppedDir) process.stderr.write(`crew repos: dropped dir: ${plan2.droppedDir} from route ${routeName} \u2014 it is not a checkout (the clone base); checkouts now live under repos:
20882
21258
  `);
20883
21259
  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}`);
20884
21260
  break;
20885
21261
  }
20886
21262
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
20887
- const top = existsSync37(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
21263
+ const top = existsSync38(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
20888
21264
  if (!top) throw new ReposError(`${target} is not a git checkout`);
20889
21265
  const origin = originOf(top);
20890
21266
  const plan = planRepoAdd({
@@ -20936,15 +21312,15 @@ ${cu.tail.trimEnd()}
20936
21312
  const created = await rowTracker.createRepoRow(rowPayload);
20937
21313
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
20938
21314
  `);
20939
- if (existsSync37(resolvedFile)) {
20940
- const raw = JSON.parse(readFileSync33(resolvedFile, "utf8"));
21315
+ if (existsSync38(resolvedFile)) {
21316
+ const raw = JSON.parse(readFileSync34(resolvedFile, "utf8"));
20941
21317
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
20942
21318
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
20943
- writeFileSync25(resolvedFile, `${JSON.stringify(raw, null, 2)}
21319
+ writeFileSync26(resolvedFile, `${JSON.stringify(raw, null, 2)}
20944
21320
  `);
20945
21321
  }
20946
21322
  }
20947
- writeFileSync25(cfg.configFile, plan.text);
21323
+ writeFileSync26(cfg.configFile, plan.text);
20948
21324
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
20949
21325
  `);
20950
21326
  } catch (e) {
@@ -21311,7 +21687,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21311
21687
  process.exit(2);
21312
21688
  }
21313
21689
  if (found.shipNameAdopted && !dryRun) {
21314
- writeFileSync25(cfg.configFile, setShipNameInYaml(readFileSync33(cfg.configFile, "utf8"), found.shipNameAdopted));
21690
+ writeFileSync26(cfg.configFile, setShipNameInYaml(readFileSync34(cfg.configFile, "utf8"), found.shipNameAdopted));
21315
21691
  cfg.ship.name = found.shipNameAdopted;
21316
21692
  }
21317
21693
  if (found.shipRow) {
@@ -21354,10 +21730,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21354
21730
  }
21355
21731
  }
21356
21732
  let previous;
21357
- const hadPreviousFile = existsSync37(resolvedPath);
21733
+ const hadPreviousFile = existsSync38(resolvedPath);
21358
21734
  if (hadPreviousFile) {
21359
21735
  try {
21360
- previous = JSON.parse(readFileSync33(resolvedPath, "utf8"));
21736
+ previous = JSON.parse(readFileSync34(resolvedPath, "utf8"));
21361
21737
  } catch {
21362
21738
  }
21363
21739
  }
@@ -21486,10 +21862,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21486
21862
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
21487
21863
  `);
21488
21864
  } else {
21489
- mkdirSync24(dirname20(resolvedPath), { recursive: true });
21865
+ mkdirSync25(dirname21(resolvedPath), { recursive: true });
21490
21866
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
21491
21867
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
21492
- writeFileSync25(tmpPath, `${JSON.stringify(resolved, null, 2)}
21868
+ writeFileSync26(tmpPath, `${JSON.stringify(resolved, null, 2)}
21493
21869
  `);
21494
21870
  renameSync3(tmpPath, resolvedPath);
21495
21871
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -21535,8 +21911,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21535
21911
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
21536
21912
  `);
21537
21913
  } else {
21538
- mkdirSync24(dirname20(apiKeyFile), { recursive: true });
21539
- writeFileSync25(apiKeyFile, `${apiKeyVar}=${apiKey}
21914
+ mkdirSync25(dirname21(apiKeyFile), { recursive: true });
21915
+ writeFileSync26(apiKeyFile, `${apiKeyVar}=${apiKey}
21540
21916
  `, { mode: 384 });
21541
21917
  process.stderr.write(`API key written to ${apiKeyFile}
21542
21918
  `);
@@ -22270,7 +22646,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22270
22646
  const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
22271
22647
  const resumed = await runPeriscopeResume({
22272
22648
  find: async () => hit && { row: hit.row, tag: hit.tag },
22273
- worktreeExists: (p) => existsSync37(p),
22649
+ worktreeExists: (p) => existsSync38(p),
22274
22650
  caps: periscopeCaps(cfg.ship),
22275
22651
  runHook: async (name, cwd, env) => {
22276
22652
  const script = hit?.repo.config.hooks[name];
@@ -22364,10 +22740,10 @@ ${where.join("\n")}`);
22364
22740
  },
22365
22741
  prepare: async (path) => {
22366
22742
  for (const f of repoP.config.worktrees.copy) {
22367
- const src = join41(repoP.dir, f);
22368
- if (!existsSync37(src)) continue;
22369
- mkdirSync24(dirOf2(join41(path, f)), { recursive: true });
22370
- copyFileSync2(src, join41(path, f));
22743
+ const src = join42(repoP.dir, f);
22744
+ if (!existsSync38(src)) continue;
22745
+ mkdirSync25(dirOf2(join42(path, f)), { recursive: true });
22746
+ copyFileSync2(src, join42(path, f));
22371
22747
  }
22372
22748
  const setup = repoP.config.hooks.setup;
22373
22749
  return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
@@ -22642,7 +23018,7 @@ ${where.join("\n")}`);
22642
23018
  };
22643
23019
  const droppedRoutes = [];
22644
23020
  try {
22645
- const rawCfg = parseYaml2(readFileSync33(cfg.configFile, "utf8"));
23021
+ const rawCfg = parseYaml2(readFileSync34(cfg.configFile, "utf8"));
22646
23022
  for (const r of rawCfg?.routes ?? []) {
22647
23023
  if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
22648
23024
  try {
@@ -22684,8 +23060,8 @@ ${where.join("\n")}`);
22684
23060
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
22685
23061
  break;
22686
23062
  }
22687
- mkdirSync24(cfg.ship.stateDir, { recursive: true });
22688
- writeFileSync25(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
23063
+ mkdirSync25(cfg.ship.stateDir, { recursive: true });
23064
+ writeFileSync26(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
22689
23065
  `);
22690
23066
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
22691
23067
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -22736,7 +23112,7 @@ ${where.join("\n")}`);
22736
23112
  }
22737
23113
  case "log": {
22738
23114
  try {
22739
- const text = readFileSync33(cfg.ship.logFile, "utf8").split("\n");
23115
+ const text = readFileSync34(cfg.ship.logFile, "utf8").split("\n");
22740
23116
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
22741
23117
  `);
22742
23118
  } catch {
@@ -22759,8 +23135,8 @@ ${where.join("\n")}`);
22759
23135
  };
22760
23136
  if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
22761
23137
  const tracker = new Tracker(route, cfg.ship);
22762
- const configText = readFileSync33(cfg.configFile, "utf8");
22763
- const cfgBase = dirname20(cfg.configFile);
23138
+ const configText = readFileSync34(cfg.configFile, "utf8");
23139
+ const cfgBase = dirname21(cfg.configFile);
22764
23140
  const configured = configuredRepos(configText, route.route, cfgBase);
22765
23141
  const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
22766
23142
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
@@ -22788,7 +23164,7 @@ ${where.join("\n")}`);
22788
23164
  }
22789
23165
  if (configured.some((r) => r.dir === dir)) {
22790
23166
  try {
22791
- writeFileSync25(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
23167
+ writeFileSync26(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
22792
23168
  } catch (e) {
22793
23169
  if (e instanceof ReposError) fail(e.message);
22794
23170
  throw e;
@@ -22796,29 +23172,29 @@ ${where.join("\n")}`);
22796
23172
  }
22797
23173
  if (row) {
22798
23174
  await tracker.deleteRepoRow(row.id);
22799
- if (existsSync37(resolvedFile)) {
22800
- const raw = JSON.parse(readFileSync33(resolvedFile, "utf8"));
23175
+ if (existsSync38(resolvedFile)) {
23176
+ const raw = JSON.parse(readFileSync34(resolvedFile, "utf8"));
22801
23177
  if (raw.repoNames) delete raw.repoNames[row.id];
22802
23178
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
22803
- writeFileSync25(resolvedFile, `${JSON.stringify(raw, null, 2)}
23179
+ writeFileSync26(resolvedFile, `${JSON.stringify(raw, null, 2)}
22804
23180
  `);
22805
23181
  }
22806
23182
  }
22807
- if (existsSync37(join41(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync17(dir, { recursive: true, force: true });
23183
+ if (existsSync38(join42(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync17(dir, { recursive: true, force: true });
22808
23184
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
22809
23185
  break;
22810
23186
  }
22811
23187
  const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
22812
- const fresh = !existsSync37(join41(dir, ".git"));
22813
- if (existsSync37(dir) && fresh && readdirSync17(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
23188
+ const fresh = !existsSync38(join42(dir, ".git"));
23189
+ if (existsSync38(dir) && fresh && readdirSync17(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
22814
23190
  if (dryRun) {
22815
23191
  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`);
22816
23192
  break;
22817
23193
  }
22818
23194
  if (fresh) {
22819
- mkdirSync24(dir, { recursive: true });
22820
- writeFileSync25(join41(dir, "README.md"), SAMPLE_README);
22821
- writeFileSync25(join41(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
23195
+ mkdirSync25(dir, { recursive: true });
23196
+ writeFileSync26(join42(dir, "README.md"), SAMPLE_README);
23197
+ writeFileSync26(join42(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
22822
23198
  const g = (...a) => {
22823
23199
  const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
22824
23200
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -22836,17 +23212,17 @@ ${where.join("\n")}`);
22836
23212
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
22837
23213
  });
22838
23214
  repoId = (await tracker.createRepoRow(payload)).id;
22839
- if (existsSync37(resolvedFile)) {
22840
- const raw = JSON.parse(readFileSync33(resolvedFile, "utf8"));
23215
+ if (existsSync38(resolvedFile)) {
23216
+ const raw = JSON.parse(readFileSync34(resolvedFile, "utf8"));
22841
23217
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
22842
23218
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
22843
- writeFileSync25(resolvedFile, `${JSON.stringify(raw, null, 2)}
23219
+ writeFileSync26(resolvedFile, `${JSON.stringify(raw, null, 2)}
22844
23220
  `);
22845
23221
  }
22846
23222
  }
22847
23223
  if (!configured.some((r) => r.dir === dir)) {
22848
23224
  try {
22849
- writeFileSync25(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
23225
+ writeFileSync26(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
22850
23226
  } catch (e) {
22851
23227
  if (e instanceof ReposError) fail(e.message);
22852
23228
  throw e;
@@ -23181,11 +23557,17 @@ watermark: ${state.watermark()}
23181
23557
  }
23182
23558
  for (const r of repos) {
23183
23559
  const remote = r.config.branch.remote;
23184
- if (existsSync37(r.dir)) {
23560
+ if (existsSync38(r.dir)) {
23185
23561
  pushAuth.push({
23186
23562
  route: c.route,
23187
23563
  repo: r.name,
23188
- result: probePushAuth({ dir: r.dir, remote, remoteUrl: originUrlOf(r.dir, remote), offline: flag("offline") })
23564
+ result: probePushAuth({
23565
+ dir: r.dir,
23566
+ remote,
23567
+ remoteUrl: originUrlOf(r.dir, remote),
23568
+ offline: flag("offline"),
23569
+ forge: forgeOfRemote(cfg.ship.forges, originUrlOf(r.dir, remote), (h) => hasForgeToken(cfg.ship.forges, h, cfg.ship.stateDir))
23570
+ })
23189
23571
  });
23190
23572
  }
23191
23573
  if (isPrBased(r.config.hooks)) prBased.add(r.name);
@@ -23258,7 +23640,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
23258
23640
  }
23259
23641
  try {
23260
23642
  const evFile = eventFileFor(cfg.ship.stateDir);
23261
- const evSize = existsSync37(evFile) ? statSync11(evFile).size : 0;
23643
+ const evSize = existsSync38(evFile) ? statSync12(evFile).size : 0;
23262
23644
  process.stdout.write(
23263
23645
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
23264
23646
  ` : "events log: ok\n"
@@ -23269,7 +23651,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
23269
23651
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
23270
23652
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
23271
23653
  try {
23272
- return readFileSync33(p, "utf8");
23654
+ return readFileSync34(p, "utf8");
23273
23655
  } catch {
23274
23656
  return null;
23275
23657
  }
@@ -23285,7 +23667,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
23285
23667
  const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
23286
23668
  if (changed2) {
23287
23669
  process.stdout.write(
23288
- `daemon config: WARN ${changed2} changed at ${new Date(statSync11(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
23670
+ `daemon config: WARN ${changed2} changed at ${new Date(statSync12(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
23289
23671
  `
23290
23672
  );
23291
23673
  }