@tablation/crew 0.89.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 +1048 -501
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -139,9 +139,92 @@ async function periscopesForStatus(stateDir, probe = portListening) {
139
139
  }));
140
140
  }
141
141
 
142
+ // src/shell-init.ts
143
+ var SHELLS = ["zsh", "bash", "fish"];
144
+ function shellFor(requested, envShell) {
145
+ const pick = (requested ?? envShell ?? "").split("/").pop() ?? "";
146
+ return SHELLS.includes(pick) ? pick : "zsh";
147
+ }
148
+ var POSIX = String.raw`# crew shell-init: "periscope up" lands in the worktree, "periscope down" returns to the checkout.
149
+ periscope() {
150
+ case "${"$"}{1-}" in
151
+ up|start)
152
+ shift
153
+ local _out _wt _url _noopen=
154
+ _out=$(mktemp) || return 1
155
+ case " $* " in *" --no-open "*) _noopen=1 ;; esac
156
+ command crew periscope up --json "$@" 2>&1 >"$_out" \
157
+ | command sed -e 's/^{.*"message":"\([^"]*\)".*}$/ \1/' >&2
158
+ if command grep -q '"event":"done"' "$_out"; then
159
+ _wt=$(command sed -n 's/.*"worktree":"\([^"]*\)".*/\1/p' "$_out" | command tail -n 1)
160
+ _url=$(command sed -n 's/.*"url":"\([^"]*\)".*/\1/p' "$_out" | command tail -n 1)
161
+ fi
162
+ command rm -f "$_out"
163
+ [ -n "$_wt" ] && [ -d "$_wt" ] || return 1
164
+ builtin cd "$_wt" || return 1
165
+ printf '%s\n%s\n' "$_url" "$_wt"
166
+ if [ -z "$_noopen" ] && [ -n "$_url" ]; then
167
+ if command -v open >/dev/null 2>&1; then command open "$_url"
168
+ elif command -v xdg-open >/dev/null 2>&1; then command xdg-open "$_url" >/dev/null 2>&1; fi
169
+ fi
170
+ ;;
171
+ down|end)
172
+ local _common _checkout=
173
+ _common=$(command git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) && _checkout=${"$"}{_common%/.git}
174
+ command crew periscope "$@" || return $?
175
+ if [ -n "$_checkout" ] && [ -d "$_checkout" ]; then builtin cd "$_checkout" || return 1; fi
176
+ ;;
177
+ *)
178
+ command crew periscope "$@"
179
+ ;;
180
+ esac
181
+ }
182
+ `;
183
+ var FISH = String.raw`# crew shell-init: "periscope up" lands in the worktree, "periscope down" returns to the checkout.
184
+ function periscope
185
+ switch "$argv[1]"
186
+ case up start
187
+ set -e argv[1]
188
+ set -l out (mktemp); or return 1
189
+ command crew periscope up --json $argv 2>&1 >$out \
190
+ | command sed -e 's/^{.*"message":"\([^"]*\)".*}$/ \1/' >&2
191
+ set -l wt
192
+ set -l url
193
+ if command grep -q '"event":"done"' $out
194
+ set wt (command sed -n 's/.*"worktree":"\([^"]*\)".*/\1/p' $out | command tail -n 1)
195
+ set url (command sed -n 's/.*"url":"\([^"]*\)".*/\1/p' $out | command tail -n 1)
196
+ end
197
+ command rm -f $out
198
+ test -n "$wt" -a -d "$wt"; or return 1
199
+ builtin cd $wt; or return 1
200
+ printf '%s\n%s\n' $url $wt
201
+ if not contains -- --no-open $argv; and test -n "$url"
202
+ if command -sq open
203
+ command open $url
204
+ else if command -sq xdg-open
205
+ command xdg-open $url >/dev/null 2>&1
206
+ end
207
+ end
208
+ case down end
209
+ set -l checkout
210
+ set -l common (command git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)
211
+ and set checkout (string replace -r '/\.git$' '' -- $common)
212
+ command crew periscope $argv; or return $status
213
+ if test -n "$checkout" -a -d "$checkout"
214
+ builtin cd $checkout
215
+ end
216
+ case '*'
217
+ command crew periscope $argv
218
+ end
219
+ end
220
+ `;
221
+ function shellInitScript(shell) {
222
+ return shell === "fish" ? FISH : POSIX;
223
+ }
224
+
142
225
  // src/config.ts
143
- import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
144
- 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";
145
228
  import { homedir as homedir6, tmpdir } from "os";
146
229
  import { parse as parse2 } from "yaml";
147
230
 
@@ -1498,6 +1581,242 @@ function resolveSeat(ship, route, role, persona = {}, repo) {
1498
1581
  return { toolchain, model, toolchainWhy, modelWhy, warnings };
1499
1582
  }
1500
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
+
1501
1820
  // src/config.ts
1502
1821
  import { getSessionStore, hostFromUrl } from "@tablation/client";
1503
1822
 
@@ -1538,8 +1857,8 @@ function explain(ship, required) {
1538
1857
  }
1539
1858
 
1540
1859
  // src/repo-config.ts
1541
- import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
1542
- 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";
1543
1862
  import { parse } from "yaml";
1544
1863
  var SPEC_VERSION = 1;
1545
1864
  var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
@@ -1623,8 +1942,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
1623
1942
  var PROVIDERS = ["github", "buildkite", "other", "none"];
1624
1943
  function findRepoConfig(dir) {
1625
1944
  for (const name of REPO_CONFIG_FILENAMES) {
1626
- const p = join9(dir, name);
1627
- if (existsSync7(p)) return p;
1945
+ const p = join10(dir, name);
1946
+ if (existsSync8(p)) return p;
1628
1947
  }
1629
1948
  return null;
1630
1949
  }
@@ -1862,7 +2181,7 @@ function parseRepoConfig(text, file) {
1862
2181
  function loadRepoConfig(dir) {
1863
2182
  const file = findRepoConfig(dir);
1864
2183
  if (!file) return null;
1865
- return parseRepoConfig(readFileSync8(file, "utf8"), file);
2184
+ return parseRepoConfig(readFileSync9(file, "utf8"), file);
1866
2185
  }
1867
2186
  function hookLabel(cfg, hook2) {
1868
2187
  return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
@@ -2146,6 +2465,14 @@ function parseCloneScheme(v) {
2146
2465
  if (v === "ssh" || v === "https") return v;
2147
2466
  throw new ConfigError(`ship.cloneScheme must be "ssh" or "https", got ${JSON.stringify(v)}`);
2148
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
+ }
2149
2476
  var SHIP_KEYS = /* @__PURE__ */ new Set([
2150
2477
  "name",
2151
2478
  "platform",
@@ -2173,6 +2500,7 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
2173
2500
  "relayPublicDomain",
2174
2501
  "passengerImage",
2175
2502
  "cloneScheme",
2503
+ "forges",
2176
2504
  "previewHost",
2177
2505
  "maxPeriscopes",
2178
2506
  "periscopeIdleMinutes"
@@ -2323,11 +2651,11 @@ function expand(p, base) {
2323
2651
  return isAbsolute(out) ? out : resolve(base, out);
2324
2652
  }
2325
2653
  function resolvePromptsDir(promptSet, crewHome, base) {
2326
- if (!promptSet) return join10(crewHome, "prompts", "default");
2654
+ if (!promptSet) return join11(crewHome, "prompts", "default");
2327
2655
  if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
2328
2656
  return expand(promptSet, base);
2329
2657
  }
2330
- return join10(crewHome, "prompts", promptSet);
2658
+ return join11(crewHome, "prompts", promptSet);
2331
2659
  }
2332
2660
  var Missing = class {
2333
2661
  paths = [];
@@ -2351,7 +2679,7 @@ var Missing = class {
2351
2679
  };
2352
2680
  function crewVersion(crewHome) {
2353
2681
  try {
2354
- const pkg = JSON.parse(readFileSync9(join10(crewHome, "package.json"), "utf8"));
2682
+ const pkg = JSON.parse(readFileSync10(join11(crewHome, "package.json"), "utf8"));
2355
2683
  return pkg.version ?? "0.0.0";
2356
2684
  } catch {
2357
2685
  return "0.0.0";
@@ -2361,26 +2689,26 @@ function configSearchPath(crewHome) {
2361
2689
  if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
2362
2690
  const xdg = process.env.XDG_CONFIG_HOME;
2363
2691
  return [
2364
- ...xdg ? [join10(xdg, "crew", "crew.yaml")] : [],
2365
- join10(homedir6(), ".config", "crew", "crew.yaml"),
2692
+ ...xdg ? [join11(xdg, "crew", "crew.yaml")] : [],
2693
+ join11(homedir6(), ".config", "crew", "crew.yaml"),
2366
2694
  resolve(crewHome, "crew.yaml")
2367
2695
  ];
2368
2696
  }
2369
2697
  function findConfigFile(crewHome) {
2370
2698
  const candidates = configSearchPath(crewHome);
2371
- return candidates.find((c) => existsSync8(c)) ?? candidates[candidates.length - 1];
2699
+ return candidates.find((c) => existsSync9(c)) ?? candidates[candidates.length - 1];
2372
2700
  }
2373
- 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");
2374
2702
  function routeSlug(route) {
2375
2703
  return route.replace(/\//g, "-");
2376
2704
  }
2377
2705
  function resolvedPathFor(stateDir, route) {
2378
2706
  const [workspace, project] = splitRoute(route);
2379
- return join10(stateDir, "resolved", workspace, `${project}.json`);
2707
+ return join11(stateDir, "resolved", workspace, `${project}.json`);
2380
2708
  }
2381
2709
  function apiKeyPathFor(stateDir, route) {
2382
2710
  const [workspace, project] = splitRoute(route);
2383
- return join10(stateDir, "keys", workspace, `${project}.env`);
2711
+ return join11(stateDir, "keys", workspace, `${project}.env`);
2384
2712
  }
2385
2713
  function splitRoute(route) {
2386
2714
  const parts = route.split("/");
@@ -2391,9 +2719,9 @@ function splitRoute(route) {
2391
2719
  }
2392
2720
  function readResolvedFile(stateDir, route) {
2393
2721
  const path = resolvedPathFor(stateDir, route);
2394
- if (!existsSync8(path)) return void 0;
2722
+ if (!existsSync9(path)) return void 0;
2395
2723
  try {
2396
- return JSON.parse(readFileSync9(path, "utf8"));
2724
+ return JSON.parse(readFileSync10(path, "utf8"));
2397
2725
  } catch (e) {
2398
2726
  throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
2399
2727
  }
@@ -2453,23 +2781,23 @@ function parseResolved(raw, m, where) {
2453
2781
  }
2454
2782
  function loadConfig(crewHome, configFile, opts = {}) {
2455
2783
  const file = configFile ?? findConfigFile(crewHome);
2456
- if (opts.text === void 0 && !existsSync8(file)) {
2784
+ if (opts.text === void 0 && !existsSync9(file)) {
2457
2785
  throw new ConfigError(
2458
2786
  `no config found. Looked in:
2459
2787
  ` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
2460
2788
 
2461
2789
  Create the first of those:
2462
- mkdir -p ${dirname3(defaultConfigPath())} && cp ${join10(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
2790
+ mkdir -p ${dirname4(defaultConfigPath())} && cp ${join11(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
2463
2791
  );
2464
2792
  }
2465
2793
  let raw;
2466
2794
  try {
2467
- raw = parse2(opts.text ?? readFileSync9(file, "utf8"));
2795
+ raw = parse2(opts.text ?? readFileSync10(file, "utf8"));
2468
2796
  } catch (e) {
2469
2797
  throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
2470
2798
  }
2471
2799
  if (!raw || typeof raw !== "object") throw new ConfigError(`${file} is empty or not a mapping`);
2472
- const base = dirname3(file);
2800
+ const base = dirname4(file);
2473
2801
  const missing = new Missing();
2474
2802
  rejectUnknownKeys(raw, /* @__PURE__ */ new Set(["ship", "routes"]), "top-level", file);
2475
2803
  const shipRaw = raw.ship ?? {};
@@ -2574,7 +2902,7 @@ Create the first of those:
2574
2902
  useNvm: shipRaw.useNvm !== false,
2575
2903
  nvmSh: shipRaw.nvmSh,
2576
2904
  stateDir,
2577
- logFile: expand(shipRaw.logFile ?? join10(tmpdir(), "crew.log"), base),
2905
+ logFile: expand(shipRaw.logFile ?? join11(tmpdir(), "crew.log"), base),
2578
2906
  userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
2579
2907
  maxConcurrentAgents,
2580
2908
  maxConcurrentAgentsSet,
@@ -2588,6 +2916,7 @@ Create the first of those:
2588
2916
  relayPublicDomain: shipRaw.relayPublicDomain ? String(shipRaw.relayPublicDomain) : void 0,
2589
2917
  passengerImage: shipRaw.passengerImage ? String(shipRaw.passengerImage) : void 0,
2590
2918
  cloneScheme: parseCloneScheme(shipRaw.cloneScheme),
2919
+ forges: parseShipForges(shipRaw.forges),
2591
2920
  previewHost: shipRaw.previewHost ? String(shipRaw.previewHost).trim() || void 0 : void 0,
2592
2921
  maxPeriscopes,
2593
2922
  periscopeIdleMinutes
@@ -2734,7 +3063,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
2734
3063
  const hasRepos = Object.keys(repoDirs).length > 0;
2735
3064
  const reposBasePath = expand(String(c?.reposBasePath ?? raw.ship?.reposBasePath ?? DEFAULT_REPOS_BASE_PATH), base);
2736
3065
  const dirIsCloneBase = !c?.dir && !hasRepos && !Array.isArray(c?.repos);
2737
- 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]) : "";
2738
3067
  const weight = c?.weight === void 0 ? void 0 : Number(c.weight);
2739
3068
  if (weight !== void 0 && !(Number.isFinite(weight) && weight > 0)) {
2740
3069
  missing.add(`${where}.weight must be a positive number (got "${c?.weight}")`);
@@ -2778,12 +3107,25 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
2778
3107
  return result;
2779
3108
  }
2780
3109
  function routeForDir(cfg, dir) {
2781
- const target = resolve(dir);
2782
- const isInside = (base) => {
3110
+ return locateDir(cfg, dir)?.route;
3111
+ }
3112
+ function locateDir(cfg, dir, commonDirOf) {
3113
+ const within = (base, target2) => {
2783
3114
  const b = resolve(base);
2784
- return target === b || target.startsWith(`${b}/`);
3115
+ return target2 === b || target2.startsWith(`${b}/`);
2785
3116
  };
2786
- return cfg.routes.find((r) => reposOf(r).some((repo) => isInside(repo.dir)));
3117
+ const find = (target2) => {
3118
+ for (const route of cfg.routes) {
3119
+ const repo = reposOf(route).find((x) => within(x.dir, target2));
3120
+ if (repo) return { route, repo, checkout: resolve(repo.dir) };
3121
+ }
3122
+ return void 0;
3123
+ };
3124
+ const target = resolve(dir);
3125
+ const direct = find(target);
3126
+ if (direct) return direct;
3127
+ const common = commonDirOf?.(target);
3128
+ return common ? find(dirname4(resolve(target, common))) : void 0;
2787
3129
  }
2788
3130
  function findRoute(cfg, route) {
2789
3131
  if (!route) {
@@ -2818,8 +3160,8 @@ function repoIdForName(r, name) {
2818
3160
  }
2819
3161
  function resolveApiKey(r) {
2820
3162
  if (r.apiKey) return r.apiKey;
2821
- if (r.apiKeyFile && r.apiKeyVar && existsSync8(r.apiKeyFile)) {
2822
- 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")) {
2823
3165
  const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
2824
3166
  if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
2825
3167
  }
@@ -2832,9 +3174,9 @@ function resolveApiKey(r) {
2832
3174
  }
2833
3175
  function credentialSource(r) {
2834
3176
  if (r.apiKey) return KEYCHAIN_ELIGIBLE_ROUTES.has(r) ? "keychain" : "inline";
2835
- if (r.apiKeyFile && r.apiKeyVar && existsSync8(r.apiKeyFile)) {
3177
+ if (r.apiKeyFile && r.apiKeyVar && existsSync9(r.apiKeyFile)) {
2836
3178
  const re = new RegExp(`^${r.apiKeyVar}=`);
2837
- 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";
2838
3180
  }
2839
3181
  return process.env.CREW_API_KEY?.trim() ? "CREW_API_KEY" : "none";
2840
3182
  }
@@ -2890,7 +3232,7 @@ function configuredMembers(rt) {
2890
3232
  return out;
2891
3233
  }
2892
3234
  function defaultRepoDir(basePath, workspace, repoName) {
2893
- return join10(basePath, workspace, repoName);
3235
+ return join11(basePath, workspace, repoName);
2894
3236
  }
2895
3237
  function reposOf(r) {
2896
3238
  const named = Object.entries(r.repos);
@@ -2923,7 +3265,7 @@ function passengerRepoTargets(r) {
2923
3265
  return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
2924
3266
  }
2925
3267
  function passengerCheckoutDir(basePath, workspace, repoName) {
2926
- return join10(basePath, workspace, ".passenger-checkouts", repoName);
3268
+ return join11(basePath, workspace, ".passenger-checkouts", repoName);
2927
3269
  }
2928
3270
  function passengerCheckoutTargets(r) {
2929
3271
  if (!r.hostPassengers) return [];
@@ -3210,9 +3552,9 @@ function recoveredFailureHeadlines(o, route) {
3210
3552
  }
3211
3553
 
3212
3554
  // src/install.ts
3213
- import { dirname as dirname4, join as join11, extname } from "path";
3555
+ import { dirname as dirname5, join as join12, extname } from "path";
3214
3556
  import { homedir as homedir7 } from "os";
3215
- 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";
3216
3558
  import { spawnSync as spawnSync4 } from "child_process";
3217
3559
  import { createHash as createHash2 } from "crypto";
3218
3560
 
@@ -3224,7 +3566,7 @@ function isCompiledBinary(moduleUrl) {
3224
3566
  // src/install.ts
3225
3567
  function crewInvocation(crewHome) {
3226
3568
  if (isCompiledBinary(import.meta.url)) return [process.execPath];
3227
- return [process.execPath, join11(crewHome, "bin", "crew")];
3569
+ return [process.execPath, join12(crewHome, "bin", "crew")];
3228
3570
  }
3229
3571
  var INTERVAL_SECONDS = 120;
3230
3572
  function labelFor(crewHome, job) {
@@ -3255,36 +3597,36 @@ function schedulerLogFor(crewLog, job) {
3255
3597
  return `${stem}.${suffix}${ext || ".log"}`;
3256
3598
  }
3257
3599
  function pathFor(ship) {
3258
- const nodeBinDir = dirname4(process.execPath);
3600
+ const nodeBinDir = dirname5(process.execPath);
3259
3601
  const parts = [nodeBinDir];
3260
3602
  if (ship.extraPath) parts.push(ship.extraPath);
3261
3603
  parts.push("/usr/bin", "/bin", "/usr/sbin", "/sbin");
3262
3604
  return parts.join(":");
3263
3605
  }
3264
3606
  function findOnPath(bin, searchPath) {
3265
- 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));
3266
3608
  for (const candidate of candidates) {
3267
3609
  try {
3268
3610
  accessSync(candidate, constants.X_OK);
3269
- if (statSync2(candidate).isFile()) return candidate;
3611
+ if (statSync3(candidate).isFile()) return candidate;
3270
3612
  } catch {
3271
3613
  }
3272
3614
  }
3273
3615
  return void 0;
3274
3616
  }
3275
- 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(":");
3276
3618
  function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
3277
3619
  const schedulerPath = pathFor(ship);
3278
3620
  if (findOnPath("docker", schedulerPath)) return void 0;
3279
3621
  const inShell = findOnPath("docker", shellPath);
3280
- 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\``;
3281
3623
  }
3282
3624
  function xmlEscape(s) {
3283
3625
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3284
3626
  }
3285
3627
  function planLaunchd(ship, crewHome, job) {
3286
3628
  const label = labelFor(crewHome, job);
3287
- const unitPath = join11(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3629
+ const unitPath = join12(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3288
3630
  const crewLog = ship.logFile;
3289
3631
  const schedulerLog = schedulerLogFor(crewLog, job);
3290
3632
  const persistent = job === "run";
@@ -3338,8 +3680,8 @@ ${scheduleKeys}
3338
3680
  }
3339
3681
  function planSystemd(ship, crewHome, job) {
3340
3682
  const label = labelFor(crewHome, job);
3341
- const unitDir = join11(homedir7(), ".config", "systemd", "user");
3342
- const servicePath = join11(unitDir, `${label}.service`);
3683
+ const unitDir = join12(homedir7(), ".config", "systemd", "user");
3684
+ const servicePath = join12(unitDir, `${label}.service`);
3343
3685
  const crewLog = ship.logFile;
3344
3686
  const schedulerLog = schedulerLogFor(crewLog, job);
3345
3687
  const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
@@ -3379,7 +3721,7 @@ WantedBy=default.target
3379
3721
  schedulerLog
3380
3722
  };
3381
3723
  }
3382
- const timerPath = join11(unitDir, `${label}.timer`);
3724
+ const timerPath = join12(unitDir, `${label}.timer`);
3383
3725
  const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
3384
3726
  [Unit]
3385
3727
  Description=${description}
@@ -3453,7 +3795,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3453
3795
  function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3454
3796
  const label = labelFor(crewHome, job);
3455
3797
  if (hostShip === "macos") {
3456
- const unitPath = join11(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3798
+ const unitPath = join12(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3457
3799
  return {
3458
3800
  mechanism: "launchd",
3459
3801
  unitPaths: [unitPath],
@@ -3463,18 +3805,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3463
3805
  };
3464
3806
  }
3465
3807
  if (hostShip === "linux" && hasSystemd) {
3466
- const unitDir = join11(homedir7(), ".config", "systemd", "user");
3808
+ const unitDir = join12(homedir7(), ".config", "systemd", "user");
3467
3809
  if (job === "run") {
3468
3810
  return {
3469
3811
  mechanism: "systemd",
3470
- unitPaths: [join11(unitDir, `${label}.service`)],
3812
+ unitPaths: [join12(unitDir, `${label}.service`)],
3471
3813
  crontabLine: null,
3472
3814
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
3473
3815
  };
3474
3816
  }
3475
3817
  return {
3476
3818
  mechanism: "systemd",
3477
- unitPaths: [join11(unitDir, `${label}.service`), join11(unitDir, `${label}.timer`)],
3819
+ unitPaths: [join12(unitDir, `${label}.service`), join12(unitDir, `${label}.timer`)],
3478
3820
  crontabLine: null,
3479
3821
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
3480
3822
  };
@@ -3492,7 +3834,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3492
3834
  function planDaemonControl(crewHome, hostShip, hasSystemd) {
3493
3835
  const label = labelFor(crewHome, "run");
3494
3836
  if (hostShip === "macos") {
3495
- const unitPath = join11(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3837
+ const unitPath = join12(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3496
3838
  return {
3497
3839
  mechanism: "launchd",
3498
3840
  startCommand: ["launchctl", "load", unitPath],
@@ -3532,8 +3874,8 @@ async function applyInstall(plan, crewHome, dryRun, log) {
3532
3874
  log.emit(`would write ${path}`);
3533
3875
  continue;
3534
3876
  }
3535
- mkdirSync5(dirname4(path), { recursive: true });
3536
- writeFileSync6(path, plan.unitContent[path]);
3877
+ mkdirSync6(dirname5(path), { recursive: true });
3878
+ writeFileSync7(path, plan.unitContent[path]);
3537
3879
  log.emit(`wrote ${path}`);
3538
3880
  }
3539
3881
  if (plan.crontabLine) {
@@ -3587,7 +3929,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3587
3929
  }
3588
3930
  }
3589
3931
  for (const path of plan.unitPaths) {
3590
- if (!existsSync9(path)) continue;
3932
+ if (!existsSync10(path)) continue;
3591
3933
  if (dryRun) {
3592
3934
  log.emit(`would remove ${path}`);
3593
3935
  continue;
@@ -3606,7 +3948,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3606
3948
  }
3607
3949
  var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
3608
3950
  function launchAgentsDir() {
3609
- return join11(homedir7(), "Library", "LaunchAgents");
3951
+ return join12(homedir7(), "Library", "LaunchAgents");
3610
3952
  }
3611
3953
  function xmlUnescape(s) {
3612
3954
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
@@ -3623,25 +3965,25 @@ function loadedCrewLabels() {
3623
3965
  return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
3624
3966
  }
3625
3967
  function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
3626
- const files = existsSync9(dir) ? readdirSync3(dir) : [];
3968
+ const files = existsSync10(dir) ? readdirSync3(dir) : [];
3627
3969
  const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
3628
- const plistPath = join11(dir, f);
3970
+ const plistPath = join12(dir, f);
3629
3971
  let program = null;
3630
3972
  try {
3631
- program = programFromPlist(readFileSync10(plistPath, "utf8"));
3973
+ program = programFromPlist(readFileSync11(plistPath, "utf8"));
3632
3974
  } catch {
3633
3975
  }
3634
3976
  return {
3635
3977
  label: f.slice(0, -".plist".length),
3636
3978
  plistPath,
3637
3979
  program,
3638
- programExists: program !== null && existsSync9(program)
3980
+ programExists: program !== null && existsSync10(program)
3639
3981
  };
3640
3982
  });
3641
3983
  const seen = new Set(units.map((u) => u.label));
3642
3984
  for (const label of [...loaded].sort()) {
3643
3985
  if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
3644
- 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 });
3645
3987
  }
3646
3988
  return units;
3647
3989
  }
@@ -3654,9 +3996,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
3654
3996
  log.emit(`would unload and remove ${unit.label}`);
3655
3997
  return;
3656
3998
  }
3657
- if (existsSync9(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3999
+ if (existsSync10(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3658
4000
  spawnSync4("launchctl", ["remove", unit.label], { encoding: "utf8" });
3659
- if (existsSync9(unit.plistPath)) unlinkSync(unit.plistPath);
4001
+ if (existsSync10(unit.plistPath)) unlinkSync(unit.plistPath);
3660
4002
  log.emit(`removed ${unit.label} (${unit.plistPath})`);
3661
4003
  }
3662
4004
  function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
@@ -3734,24 +4076,24 @@ function detectSystemd() {
3734
4076
  }
3735
4077
 
3736
4078
  // src/ship-attention.ts
3737
- import { existsSync as existsSync11, statfsSync } from "fs";
4079
+ import { existsSync as existsSync12, statfsSync } from "fs";
3738
4080
 
3739
4081
  // src/git.ts
3740
- import { execFileSync } from "child_process";
4082
+ import { execFileSync as execFileSync2 } from "child_process";
3741
4083
  import {
3742
- existsSync as existsSync10,
3743
- mkdirSync as mkdirSync6,
4084
+ existsSync as existsSync11,
4085
+ mkdirSync as mkdirSync7,
3744
4086
  openSync as openSync2,
3745
4087
  closeSync as closeSync2,
3746
4088
  writeSync,
3747
- statSync as statSync3,
4089
+ statSync as statSync4,
3748
4090
  unlinkSync as unlinkSync2,
3749
- readFileSync as readFileSync11,
3750
- writeFileSync as writeFileSync7,
4091
+ readFileSync as readFileSync12,
4092
+ writeFileSync as writeFileSync8,
3751
4093
  readdirSync as readdirSync4,
3752
4094
  rmSync as rmSync6
3753
4095
  } from "fs";
3754
- 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";
3755
4097
  var GitError = class extends Error {
3756
4098
  };
3757
4099
  function streamText(x) {
@@ -3759,7 +4101,7 @@ function streamText(x) {
3759
4101
  }
3760
4102
  function gitRaw(cwd, args) {
3761
4103
  try {
3762
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
4104
+ return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
3763
4105
  } catch (e) {
3764
4106
  const err = e;
3765
4107
  const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
@@ -3768,7 +4110,7 @@ function gitRaw(cwd, args) {
3768
4110
  }
3769
4111
  function git(cwd, args) {
3770
4112
  try {
3771
- 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();
3772
4114
  } catch (e) {
3773
4115
  const err = e;
3774
4116
  const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
@@ -3939,9 +4281,9 @@ function keyCommitsInRange(cwd, key, range) {
3939
4281
  }
3940
4282
  function patchIdOf(cwd, from, to) {
3941
4283
  try {
3942
- 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 });
3943
4285
  if (!diff) return null;
3944
- 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 });
3945
4287
  return out.split(" ")[0]?.trim() || null;
3946
4288
  } catch {
3947
4289
  return null;
@@ -4131,7 +4473,7 @@ function recoverStrandedReleaseCheckouts(cwd) {
4131
4473
  if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
4132
4474
  else if (line.startsWith("branch refs/heads/crew/release-") && path) {
4133
4475
  gitOk(cwd, ["worktree", "remove", "--force", path]);
4134
- const root = dirname5(path);
4476
+ const root = dirname6(path);
4135
4477
  if (basename4(root).startsWith("crew-release-")) rmSync6(root, { recursive: true, force: true });
4136
4478
  }
4137
4479
  }
@@ -4149,11 +4491,11 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
4149
4491
  }
4150
4492
  for (const name of names) {
4151
4493
  if (!name.startsWith("crew-release-")) continue;
4152
- const root = join12(tmp, name);
4494
+ const root = join13(tmp, name);
4153
4495
  try {
4154
- const st = statSync3(root);
4496
+ const st = statSync4(root);
4155
4497
  if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
4156
- if (existsSync10(join12(root, "checkout"))) continue;
4498
+ if (existsSync11(join13(root, "checkout"))) continue;
4157
4499
  rmSync6(root, { recursive: true, force: true });
4158
4500
  removed.push(name);
4159
4501
  } catch {
@@ -4186,9 +4528,13 @@ function worktrees(cwd) {
4186
4528
  if (current.path) list.push({ path: current.path, branch: current.branch ?? null, prunable: current.prunable ?? false });
4187
4529
  return list;
4188
4530
  }
4531
+ var tokenHosts = /* @__PURE__ */ new Set();
4532
+ function setForgeTokenHosts(hosts) {
4533
+ tokenHosts = new Set([...hosts].map((h) => h.toLowerCase()));
4534
+ }
4189
4535
  function cloneUrlFor(remote, opts = {}) {
4190
4536
  const origin = opts.originUrl?.trim() ?? "";
4191
- 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";
4192
4538
  return scheme === "https" ? `https://github.com/${remote}.git` : `git@github.com:${remote}.git`;
4193
4539
  }
4194
4540
  function originUrlOf(dir, remoteName) {
@@ -4199,16 +4545,16 @@ function originUrlOf(dir, remoteName) {
4199
4545
  }
4200
4546
  }
4201
4547
  function ensureRepoCheckout(dir, remote, opts = {}) {
4202
- if (existsSync10(`${dir}/.git`)) return false;
4548
+ if (existsSync11(`${dir}/.git`)) return false;
4203
4549
  if (!remote) {
4204
4550
  throw new GitError(
4205
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`
4206
4552
  );
4207
4553
  }
4208
- mkdirSync6(dirname5(dir), { recursive: true });
4554
+ mkdirSync7(dirname6(dir), { recursive: true });
4209
4555
  const url = cloneUrlFor(remote, opts);
4210
4556
  try {
4211
- execFileSync("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
4557
+ execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
4212
4558
  } catch (e) {
4213
4559
  const err = e;
4214
4560
  const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
@@ -4230,7 +4576,7 @@ function sleepSync(ms3) {
4230
4576
  }
4231
4577
  function acquireCheckoutLock(dir, timeoutMs = 3e4) {
4232
4578
  const lockPath = checkoutLockPath(dir);
4233
- mkdirSync6(dirname5(lockPath), { recursive: true });
4579
+ mkdirSync7(dirname6(lockPath), { recursive: true });
4234
4580
  const deadline = Date.now() + timeoutMs;
4235
4581
  for (; ; ) {
4236
4582
  try {
@@ -4248,7 +4594,7 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
4248
4594
  const err = e;
4249
4595
  if (err.code !== "EEXIST") throw err;
4250
4596
  try {
4251
- if (Date.now() - statSync3(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
4597
+ if (Date.now() - statSync4(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
4252
4598
  try {
4253
4599
  unlinkSync2(lockPath);
4254
4600
  } catch {
@@ -4267,17 +4613,17 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
4267
4613
  function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
4268
4614
  const release = acquireCheckoutLock(dir);
4269
4615
  try {
4270
- if (!existsSync10(`${dir}/.git`) && githubRemote) {
4616
+ if (!existsSync11(`${dir}/.git`) && githubRemote) {
4271
4617
  const url = cloneUrlFor(githubRemote, clone);
4272
4618
  const marker = cloneFailureMarker(dir);
4273
- if (existsSync10(marker) && readFileSync11(marker, "utf8").split("\n")[0] === url) {
4274
- 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 };
4275
4621
  }
4276
4622
  try {
4277
4623
  ensureRepoCheckout(dir, githubRemote, clone);
4278
4624
  } catch (e) {
4279
4625
  if (!(e instanceof GitError)) throw e;
4280
- writeFileSync7(marker, `${url}
4626
+ writeFileSync8(marker, `${url}
4281
4627
  ${e.message}`);
4282
4628
  return { action: "clone-failed", detail: e.message, repeat: false };
4283
4629
  }
@@ -4484,7 +4830,7 @@ function noteHeldWorktree(state, h, nowMs = Date.now()) {
4484
4830
  function reconcileHeldWorktrees(state, repo, log, stillRefused) {
4485
4831
  for (const i of state.shipAttention().previous()) {
4486
4832
  if (i.kind !== "worktree_held" || !i.held || i.held.repo !== repo) continue;
4487
- const gone = !existsSync11(i.held.path);
4833
+ const gone = !existsSync12(i.held.path);
4488
4834
  if (!gone && !(stillRefused && !stillRefused.has(i.held.path))) continue;
4489
4835
  clearShipAttention(state, i.key);
4490
4836
  log.emit(
@@ -4610,10 +4956,10 @@ function attentionProblems(items, already) {
4610
4956
  }
4611
4957
 
4612
4958
  // src/doctor-checks.ts
4613
- import { execFileSync as execFileSync2 } from "child_process";
4959
+ import { execFileSync as execFileSync3 } from "child_process";
4614
4960
  var PROBE_BRANCH = "crew-probe-branch";
4615
4961
  var realGit = (cwd, args, env) => {
4616
- execFileSync2("git", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
4962
+ execFileSync3("git", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
4617
4963
  };
4618
4964
  function isSshRemote(url) {
4619
4965
  return /^ssh:\/\//.test(url) || /^[\w.-]+@[\w.-]+:/.test(url);
@@ -4631,7 +4977,30 @@ function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
4631
4977
  } catch (e) {
4632
4978
  const err = e;
4633
4979
  const raw = (err.stderr ? String(err.stderr) : err.message).trim().split("\n").filter(Boolean).slice(-2).join(" / ");
4634
- 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
+ }
4635
5004
  }
4636
5005
  }
4637
5006
  var INSTALL_HINTS = {
@@ -4712,30 +5081,30 @@ function roleCapWarnings(ship) {
4712
5081
 
4713
5082
  // src/selftest.ts
4714
5083
  import { spawn as spawn3 } from "child_process";
4715
- 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";
4716
5085
  import { tmpdir as tmpdir2 } from "os";
4717
- import { join as join21 } from "path";
5086
+ import { join as join22 } from "path";
4718
5087
 
4719
5088
  // src/agent.ts
4720
5089
  import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
4721
- 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";
4722
5091
  import { StringDecoder } from "string_decoder";
4723
5092
  import { finished } from "stream/promises";
4724
- 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";
4725
5094
  import { createHash as createHash3 } from "crypto";
4726
5095
 
4727
5096
  // src/state.ts
4728
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync7, rmSync as rmSync7, statSync as statSync4, readdirSync as readdirSync5 } from "fs";
4729
- 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";
4730
5099
  var EPOCH = "1970-01-01T00:00:00.000Z";
4731
5100
  var State = class _State {
4732
5101
  dir;
4733
5102
  constructor(dir) {
4734
5103
  this.dir = dir;
4735
- mkdirSync7(dir, { recursive: true });
5104
+ mkdirSync8(dir, { recursive: true });
4736
5105
  }
4737
5106
  path(...parts) {
4738
- return join13(this.dir, ...parts);
5107
+ return join14(this.dir, ...parts);
4739
5108
  }
4740
5109
  /**
4741
5110
  * The timestamp every "new since last poll" check is measured against.
@@ -4746,14 +5115,14 @@ var State = class _State {
4746
5115
  */
4747
5116
  watermark() {
4748
5117
  try {
4749
- const v = readFileSync12(this.path(".poll-watermark"), "utf8").trim();
5118
+ const v = readFileSync13(this.path(".poll-watermark"), "utf8").trim();
4750
5119
  return v || EPOCH;
4751
5120
  } catch {
4752
5121
  return EPOCH;
4753
5122
  }
4754
5123
  }
4755
5124
  setWatermark(at) {
4756
- writeFileSync8(this.path(".poll-watermark"), `${at}
5125
+ writeFileSync9(this.path(".poll-watermark"), `${at}
4757
5126
  `);
4758
5127
  }
4759
5128
  /**
@@ -4771,16 +5140,16 @@ var State = class _State {
4771
5140
  }
4772
5141
  /** Whole-crew pause, and the per-role sentinels one level down. */
4773
5142
  isPaused() {
4774
- return existsSync12(this.path(".crew-paused"));
5143
+ return existsSync13(this.path(".crew-paused"));
4775
5144
  }
4776
5145
  isRolePaused(role) {
4777
- return existsSync12(this.path(`.role-paused-${role}`));
5146
+ return existsSync13(this.path(`.role-paused-${role}`));
4778
5147
  }
4779
5148
  pausedRoles(roles) {
4780
5149
  return new Set(roles.filter((r) => this.isRolePaused(r)));
4781
5150
  }
4782
5151
  pause(role) {
4783
- writeFileSync8(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
5152
+ writeFileSync9(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
4784
5153
  }
4785
5154
  resume(role) {
4786
5155
  rmSync7(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
@@ -4791,10 +5160,10 @@ var State = class _State {
4791
5160
  * while agents keep working.
4792
5161
  */
4793
5162
  isReleasePaused() {
4794
- return existsSync12(this.path(".release-paused"));
5163
+ return existsSync13(this.path(".release-paused"));
4795
5164
  }
4796
5165
  setReleasePaused(paused) {
4797
- if (paused) writeFileSync8(this.path(".release-paused"), "");
5166
+ if (paused) writeFileSync9(this.path(".release-paused"), "");
4798
5167
  else rmSync7(this.path(".release-paused"), { force: true });
4799
5168
  }
4800
5169
  /**
@@ -4832,7 +5201,7 @@ var State = class _State {
4832
5201
  const name = _State.safe(rawName);
4833
5202
  const file = this.path(`.${name}.lock`);
4834
5203
  try {
4835
- const existing = Number.parseInt(readFileSync12(file, "utf8").trim(), 10);
5204
+ const existing = Number.parseInt(readFileSync13(file, "utf8").trim(), 10);
4836
5205
  if (Number.isFinite(existing) && existing > 0) {
4837
5206
  try {
4838
5207
  process.kill(existing, 0);
@@ -4842,7 +5211,7 @@ var State = class _State {
4842
5211
  }
4843
5212
  } catch {
4844
5213
  }
4845
- writeFileSync8(file, `${process.pid}
5214
+ writeFileSync9(file, `${process.pid}
4846
5215
  `);
4847
5216
  return {
4848
5217
  ok: true,
@@ -4870,7 +5239,7 @@ var State = class _State {
4870
5239
  if (!/^\.release-.+\.lock$/.test(n)) continue;
4871
5240
  let pid = 0;
4872
5241
  try {
4873
- pid = Number.parseInt(readFileSync12(this.path(n), "utf8").trim(), 10);
5242
+ pid = Number.parseInt(readFileSync13(this.path(n), "utf8").trim(), 10);
4874
5243
  } catch {
4875
5244
  continue;
4876
5245
  }
@@ -4956,7 +5325,7 @@ var State = class _State {
4956
5325
  ticketInFlight(recordId) {
4957
5326
  const file = this.path(`.${_State.safe(`crew-ticket-${recordId}`)}.lock`);
4958
5327
  try {
4959
- const pid = Number.parseInt(readFileSync12(file, "utf8").trim(), 10);
5328
+ const pid = Number.parseInt(readFileSync13(file, "utf8").trim(), 10);
4960
5329
  if (!Number.isFinite(pid) || pid <= 0) return false;
4961
5330
  process.kill(pid, 0);
4962
5331
  return true;
@@ -4990,7 +5359,7 @@ var State = class _State {
4990
5359
  const heldReported = `.release-held-${suffix}`;
4991
5360
  const read = (f) => {
4992
5361
  try {
4993
- return readFileSync12(this.path(f), "utf8").trim() || null;
5362
+ return readFileSync13(this.path(f), "utf8").trim() || null;
4994
5363
  } catch {
4995
5364
  return null;
4996
5365
  }
@@ -4999,7 +5368,7 @@ var State = class _State {
4999
5368
  blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
5000
5369
  noteBlocked: () => {
5001
5370
  const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
5002
- writeFileSync8(this.path(blocked), `${n}
5371
+ writeFileSync9(this.path(blocked), `${n}
5003
5372
  `);
5004
5373
  return n;
5005
5374
  },
@@ -5016,7 +5385,7 @@ var State = class _State {
5016
5385
  */
5017
5386
  deployFailedSha: () => read(failed),
5018
5387
  noteDeployFailed: (sha) => {
5019
- writeFileSync8(this.path(failed), `${sha}
5388
+ writeFileSync9(this.path(failed), `${sha}
5020
5389
  `);
5021
5390
  },
5022
5391
  clearDeployFailed: () => {
@@ -5030,7 +5399,7 @@ var State = class _State {
5030
5399
  noteTestGateFailed: (sha) => {
5031
5400
  const [prev, n] = (read(gateFailed) ?? "").split(" ");
5032
5401
  const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
5033
- writeFileSync8(this.path(gateFailed), `${sha} ${next}
5402
+ writeFileSync9(this.path(gateFailed), `${sha} ${next}
5034
5403
  `);
5035
5404
  return next;
5036
5405
  },
@@ -5040,13 +5409,13 @@ var State = class _State {
5040
5409
  /** Whether the board was already told about this head's red gate. */
5041
5410
  heldReported: () => {
5042
5411
  try {
5043
- return readFileSync12(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
5412
+ return readFileSync13(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
5044
5413
  } catch {
5045
5414
  return null;
5046
5415
  }
5047
5416
  },
5048
5417
  noteHeldReported: (signature) => {
5049
- writeFileSync8(this.path(heldReported), `${signature}
5418
+ writeFileSync9(this.path(heldReported), `${signature}
5050
5419
  `);
5051
5420
  },
5052
5421
  clearHeldReported: () => {
@@ -5054,7 +5423,7 @@ var State = class _State {
5054
5423
  },
5055
5424
  testGateReported: (sha) => read(gateReported) === sha,
5056
5425
  noteTestGateReported: (sha) => {
5057
- writeFileSync8(this.path(gateReported), `${sha}
5426
+ writeFileSync9(this.path(gateReported), `${sha}
5058
5427
  `);
5059
5428
  }
5060
5429
  };
@@ -5077,7 +5446,7 @@ var State = class _State {
5077
5446
  const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
5078
5447
  const read = (f) => {
5079
5448
  try {
5080
- return readFileSync12(this.path(f), "utf8").trim() || null;
5449
+ return readFileSync13(this.path(f), "utf8").trim() || null;
5081
5450
  } catch {
5082
5451
  return null;
5083
5452
  }
@@ -5110,18 +5479,18 @@ var State = class _State {
5110
5479
  clear2();
5111
5480
  return;
5112
5481
  }
5113
- writeFileSync8(this.path(streakFile), `${this.readInt(streakFile) + 1}
5482
+ writeFileSync9(this.path(streakFile), `${this.readInt(streakFile) + 1}
5114
5483
  `);
5115
5484
  const prior = read(ticketFile);
5116
5485
  if (topTicket && topTicket === prior) {
5117
- writeFileSync8(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
5486
+ writeFileSync9(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
5118
5487
  `);
5119
5488
  } else if (topTicket) {
5120
- writeFileSync8(this.path(ticketFile), `${topTicket}
5489
+ writeFileSync9(this.path(ticketFile), `${topTicket}
5121
5490
  `);
5122
- writeFileSync8(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
5491
+ writeFileSync9(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
5123
5492
  `);
5124
- writeFileSync8(this.path(ticketStreakFile), "1\n");
5493
+ writeFileSync9(this.path(ticketStreakFile), "1\n");
5125
5494
  }
5126
5495
  }
5127
5496
  };
@@ -5142,14 +5511,14 @@ var State = class _State {
5142
5511
  return {
5143
5512
  previous: () => {
5144
5513
  try {
5145
- const parsed = JSON.parse(readFileSync12(this.path(file), "utf8"));
5514
+ const parsed = JSON.parse(readFileSync13(this.path(file), "utf8"));
5146
5515
  return parsed && typeof parsed === "object" ? parsed : {};
5147
5516
  } catch {
5148
5517
  return {};
5149
5518
  }
5150
5519
  },
5151
5520
  persist: (next) => {
5152
- writeFileSync8(this.path(file), JSON.stringify(next));
5521
+ writeFileSync9(this.path(file), JSON.stringify(next));
5153
5522
  }
5154
5523
  };
5155
5524
  }
@@ -5165,14 +5534,14 @@ var State = class _State {
5165
5534
  return {
5166
5535
  previous: () => {
5167
5536
  try {
5168
- const parsed = JSON.parse(readFileSync12(this.path(file), "utf8"));
5537
+ const parsed = JSON.parse(readFileSync13(this.path(file), "utf8"));
5169
5538
  return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [];
5170
5539
  } catch {
5171
5540
  return [];
5172
5541
  }
5173
5542
  },
5174
5543
  persist: (next) => {
5175
- writeFileSync8(this.path(file), JSON.stringify(next));
5544
+ writeFileSync9(this.path(file), JSON.stringify(next));
5176
5545
  }
5177
5546
  };
5178
5547
  }
@@ -5186,14 +5555,14 @@ var State = class _State {
5186
5555
  return {
5187
5556
  previous: () => {
5188
5557
  try {
5189
- const parsed = JSON.parse(readFileSync12(this.path(file), "utf8"));
5558
+ const parsed = JSON.parse(readFileSync13(this.path(file), "utf8"));
5190
5559
  return Array.isArray(parsed) ? parsed : [];
5191
5560
  } catch {
5192
5561
  return [];
5193
5562
  }
5194
5563
  },
5195
5564
  persist: (next) => {
5196
- writeFileSync8(this.path(file), JSON.stringify(next));
5565
+ writeFileSync9(this.path(file), JSON.stringify(next));
5197
5566
  }
5198
5567
  };
5199
5568
  }
@@ -5206,11 +5575,11 @@ var State = class _State {
5206
5575
  const file = ".release-last-run";
5207
5576
  return {
5208
5577
  touch: () => {
5209
- writeFileSync8(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
5578
+ writeFileSync9(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
5210
5579
  },
5211
5580
  at: () => {
5212
5581
  try {
5213
- return statSync4(this.path(file)).mtimeMs;
5582
+ return statSync5(this.path(file)).mtimeMs;
5214
5583
  } catch {
5215
5584
  return null;
5216
5585
  }
@@ -5219,7 +5588,7 @@ var State = class _State {
5219
5588
  }
5220
5589
  readInt(file) {
5221
5590
  try {
5222
- return Number.parseInt(readFileSync12(this.path(file), "utf8").trim(), 10) || 0;
5591
+ return Number.parseInt(readFileSync13(this.path(file), "utf8").trim(), 10) || 0;
5223
5592
  } catch {
5224
5593
  return 0;
5225
5594
  }
@@ -5702,8 +6071,8 @@ var PERISCOPE_COLUMNS = [
5702
6071
  ];
5703
6072
 
5704
6073
  // src/agent-log.ts
5705
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync6, readFileSync as readFileSync13, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "fs";
5706
- 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";
5707
6076
 
5708
6077
  // src/pending-backoff.ts
5709
6078
  var sleepBackoff = (attempts) => Math.min(6e4 * 2 ** Math.max(0, attempts - 1), 30 * 6e4);
@@ -5875,20 +6244,20 @@ async function addRunToTicket(target, t, recordId, run2) {
5875
6244
  };
5876
6245
  }
5877
6246
  }
5878
- var queueDir = (stateDir) => join14(stateDir, "pending-agent-logs");
6247
+ var queueDir = (stateDir) => join15(stateDir, "pending-agent-logs");
5879
6248
  function queueAgentRun(stateDir, name, q) {
5880
- mkdirSync8(queueDir(stateDir), { recursive: true });
5881
- 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));
5882
6251
  }
5883
6252
  async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__PURE__ */ new Date()) {
5884
6253
  const dir = queueDir(stateDir);
5885
- if (!existsSync13(dir)) return 0;
6254
+ if (!existsSync14(dir)) return 0;
5886
6255
  let sent = 0;
5887
6256
  for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
5888
- const path = join14(dir, f);
6257
+ const path = join15(dir, f);
5889
6258
  let q;
5890
6259
  try {
5891
- q = JSON.parse(readFileSync13(path, "utf8"));
6260
+ q = JSON.parse(readFileSync14(path, "utf8"));
5892
6261
  } catch {
5893
6262
  continue;
5894
6263
  }
@@ -5917,7 +6286,7 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
5917
6286
  continue;
5918
6287
  }
5919
6288
  const attempts = q.attempts + 1;
5920
- writeFileSync9(path, JSON.stringify({
6289
+ writeFileSync10(path, JSON.stringify({
5921
6290
  ...q,
5922
6291
  attempts,
5923
6292
  warned: true,
@@ -5928,20 +6297,20 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
5928
6297
  }
5929
6298
  return sent;
5930
6299
  }
5931
- var countersDir = (stateDir) => join14(stateDir, "pending-ticket-counters");
6300
+ var countersDir = (stateDir) => join15(stateDir, "pending-ticket-counters");
5932
6301
  function queueTicketCounters(stateDir, name, q) {
5933
- mkdirSync8(countersDir(stateDir), { recursive: true });
5934
- 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));
5935
6304
  }
5936
6305
  async function replayQueuedTicketCounters(stateDir, route, keyFor, log, now = /* @__PURE__ */ new Date()) {
5937
6306
  const dir = countersDir(stateDir);
5938
- if (!existsSync13(dir)) return 0;
6307
+ if (!existsSync14(dir)) return 0;
5939
6308
  let sent = 0;
5940
6309
  for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
5941
- const path = join14(dir, f);
6310
+ const path = join15(dir, f);
5942
6311
  let q;
5943
6312
  try {
5944
- q = JSON.parse(readFileSync13(path, "utf8"));
6313
+ q = JSON.parse(readFileSync14(path, "utf8"));
5945
6314
  } catch {
5946
6315
  continue;
5947
6316
  }
@@ -5960,7 +6329,7 @@ async function replayQueuedTicketCounters(stateDir, route, keyFor, log, now = /*
5960
6329
  continue;
5961
6330
  }
5962
6331
  const attempts = q.attempts + 1;
5963
- writeFileSync9(path, JSON.stringify({
6332
+ writeFileSync10(path, JSON.stringify({
5964
6333
  ...q,
5965
6334
  attempts,
5966
6335
  warned: true,
@@ -6163,8 +6532,8 @@ var HandoffTranscriptTracker = class {
6163
6532
  };
6164
6533
 
6165
6534
  // src/pending-handoff.ts
6166
- import { existsSync as existsSync16, mkdirSync as mkdirSync9, readdirSync as readdirSync7, readFileSync as readFileSync14, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "fs";
6167
- 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";
6168
6537
 
6169
6538
  // src/ticket-branch.ts
6170
6539
  import { basename as basename6 } from "path";
@@ -7247,16 +7616,16 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
7247
7616
  }
7248
7617
 
7249
7618
  // src/run-worktree.ts
7250
- import { existsSync as existsSync15 } from "fs";
7251
- 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";
7252
7621
 
7253
7622
  // src/ports.ts
7254
- import { execFileSync as execFileSync3 } from "child_process";
7255
- import { existsSync as existsSync14 } from "fs";
7256
- 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";
7257
7626
  var run = (cmd, args) => {
7258
7627
  try {
7259
- return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
7628
+ return execFileSync4(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
7260
7629
  } catch {
7261
7630
  return "";
7262
7631
  }
@@ -7298,7 +7667,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
7298
7667
  }
7299
7668
  function worktreeExists(parent, prefix, n) {
7300
7669
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
7301
- if (existsSync14(join15(parent, name, ".git"))) return true;
7670
+ if (existsSync15(join16(parent, name, ".git"))) return true;
7302
7671
  }
7303
7672
  return false;
7304
7673
  }
@@ -7381,7 +7750,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
7381
7750
  const existing = worktreeForNumber(dir, num);
7382
7751
  const located = locateBranchForTicket(dir, cfg, t, role);
7383
7752
  const baseRef = hasRemote ? `${remote}/${base}` : base;
7384
- if (existing && !existing.prunable && existsSync15(existing.path)) {
7753
+ if (existing && !existing.prunable && existsSync16(existing.path)) {
7385
7754
  const branch = existing.branch;
7386
7755
  if (status(existing.path).length > 0) {
7387
7756
  return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
@@ -7397,7 +7766,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
7397
7766
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
7398
7767
  return { action: "recut", path: existing.path, branch, base: baseRef };
7399
7768
  }
7400
- const path = join16(dirname6(dir), worktreeForTicket(dir, cfg, t, role));
7769
+ const path = join17(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
7401
7770
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
7402
7771
  return { action: "cut-remote", path, branch: located.branch };
7403
7772
  }
@@ -7451,7 +7820,7 @@ function dropEmptyRunBranch(dir, cfg, branch) {
7451
7820
  if (remoteConfigured(dir, remote) && remoteBranchExists(dir, remote, branch)) return false;
7452
7821
  if (countCommits(dir, `${baseRef}..${branch}`) !== 0) return false;
7453
7822
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
7454
- if (wt && existsSync15(wt.path)) {
7823
+ if (wt && existsSync16(wt.path)) {
7455
7824
  if (status(wt.path).length > 0) return false;
7456
7825
  if (gitOk(dir, ["worktree", "remove", wt.path]) === null) return false;
7457
7826
  }
@@ -7459,7 +7828,7 @@ function dropEmptyRunBranch(dir, cfg, branch) {
7459
7828
  }
7460
7829
  async function finishRunWorktree(dir, cfg, branch) {
7461
7830
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
7462
- if (!wt || !existsSync15(wt.path)) return { kind: "none" };
7831
+ if (!wt || !existsSync16(wt.path)) return { kind: "none" };
7463
7832
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
7464
7833
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
7465
7834
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -7471,7 +7840,7 @@ async function finishRunWorktree(dir, cfg, branch) {
7471
7840
  if (pids.length) await killGently(pids);
7472
7841
  }
7473
7842
  gitOk(dir, ["worktree", "remove", wt.path]);
7474
- 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 };
7475
7844
  }
7476
7845
 
7477
7846
  // src/handoff-guard.ts
@@ -7854,20 +8223,20 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
7854
8223
 
7855
8224
  // src/pending-handoff.ts
7856
8225
  var PENDING_MAX_AGE_MS = 48 * 36e5;
7857
- var pendingDirFor = (stateDir) => join17(stateDir, "pending-handoffs");
7858
- var stateDirOfStream = (streamPath) => join17(streamPath, "..", "..");
8226
+ var pendingDirFor = (stateDir) => join18(stateDir, "pending-handoffs");
8227
+ var stateDirOfStream = (streamPath) => join18(streamPath, "..", "..");
7859
8228
  function writePendingHandoff(stateDir, rec) {
7860
8229
  const dir = pendingDirFor(stateDir);
7861
- mkdirSync9(dir, { recursive: true });
7862
- 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));
7863
8232
  }
7864
8233
  function listPendingHandoffs(stateDir, route) {
7865
8234
  const dir = pendingDirFor(stateDir);
7866
- if (!existsSync16(dir)) return [];
8235
+ if (!existsSync17(dir)) return [];
7867
8236
  const out = [];
7868
8237
  for (const f of readdirSync7(dir).filter((n) => n.endsWith(".json")).sort()) {
7869
8238
  try {
7870
- const rec = JSON.parse(readFileSync14(join17(dir, f), "utf8"));
8239
+ const rec = JSON.parse(readFileSync15(join18(dir, f), "utf8"));
7871
8240
  if (rec.version === 1 && rec.ticketRecordId && (!route || rec.route === route)) out.push(rec);
7872
8241
  } catch {
7873
8242
  }
@@ -7875,7 +8244,7 @@ function listPendingHandoffs(stateDir, route) {
7875
8244
  return out;
7876
8245
  }
7877
8246
  function removePendingHandoff(stateDir, ticketRecordId) {
7878
- rmSync9(join17(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
8247
+ rmSync9(join18(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
7879
8248
  }
7880
8249
  function pendingHandoffFor(stateDir, ticketRecordId) {
7881
8250
  return listPendingHandoffs(stateDir).find((r) => r.ticketRecordId === ticketRecordId);
@@ -7985,8 +8354,8 @@ function expandSkillMarkers(text, lookup2, opts = {}) {
7985
8354
  }
7986
8355
 
7987
8356
  // src/skills.ts
7988
- import { readFileSync as readFileSync16, readdirSync as readdirSync8, existsSync as existsSync18 } from "fs";
7989
- 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";
7990
8359
  import { parse as parseYaml } from "yaml";
7991
8360
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError3, TablationApiError as TablationApiError2 } from "@tablation/client";
7992
8361
 
@@ -8002,8 +8371,8 @@ ${additions}`;
8002
8371
  }
8003
8372
 
8004
8373
  // src/agents.ts
8005
- import { readFileSync as readFileSync15, existsSync as existsSync17 } from "fs";
8006
- import { join as join18 } from "path";
8374
+ import { readFileSync as readFileSync16, existsSync as existsSync18 } from "fs";
8375
+ import { join as join19 } from "path";
8007
8376
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError2, TablationApiError } from "@tablation/client";
8008
8377
  var PERSONA_NAME = {
8009
8378
  dev: "Developer",
@@ -8017,12 +8386,12 @@ var CREW_AGENT_COLUMN = "agent_id";
8017
8386
  var AgentsSyncError = class extends Error {
8018
8387
  };
8019
8388
  function personaDefaultPrompt(promptsDir, role) {
8020
- const briefPath = join18(promptsDir, "personas", `lane-${role}.md`);
8021
- if (!existsSync17(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
8022
- if (SELF_CONTAINED_ROLES.has(role)) return readFileSync15(briefPath, "utf8");
8023
- const commonPath = join18(promptsDir, "personas", "common.md");
8024
- if (!existsSync17(commonPath)) return readFileSync15(briefPath, "utf8");
8025
- 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")}`;
8026
8395
  }
8027
8396
  async function resolveAgentId(route, role, opts) {
8028
8397
  const cached = route.resolved?.agentPersonas?.[role]?.agentId;
@@ -8469,7 +8838,7 @@ function describeCrewLink(o) {
8469
8838
  var SkillSyncError = class extends Error {
8470
8839
  };
8471
8840
  function parseSkillFile(path) {
8472
- const text = readFileSync16(path, "utf8");
8841
+ const text = readFileSync17(path, "utf8");
8473
8842
  const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
8474
8843
  if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
8475
8844
  const frontmatter = match[1];
@@ -8482,9 +8851,9 @@ function parseSkillFile(path) {
8482
8851
  return { name: meta.name, description: meta.description, prompt: body, file: path };
8483
8852
  }
8484
8853
  function listLocalSkills(promptsDir) {
8485
- const dir = join19(promptsDir, "skills");
8486
- if (!existsSync18(dir)) return [];
8487
- 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)));
8488
8857
  }
8489
8858
  async function syncSkills(route, opts) {
8490
8859
  if (!route.resolved) {
@@ -8686,26 +9055,26 @@ var AgentError = class extends Error {
8686
9055
  };
8687
9056
  var AgentSpawnError = class extends AgentError {
8688
9057
  };
8689
- 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`);
8690
9059
  function planAgentRun(o) {
8691
- const briefPath = join20(o.route.promptsDir, "personas", `lane-${o.role}.md`);
8692
- if (!existsSync19(briefPath)) {
9060
+ const briefPath = join21(o.route.promptsDir, "personas", `lane-${o.role}.md`);
9061
+ if (!existsSync20(briefPath)) {
8693
9062
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
8694
9063
  }
8695
- const commonPath = join20(o.route.promptsDir, "personas", "common.md");
8696
- 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);
8697
9066
  const now = o.now ?? (() => Date.now());
8698
9067
  const routeLabel = routeSlug(o.route.route);
8699
9068
  const ownDigest = o.runId ? digestPathFor(o.stateDir, routeLabel, o.role, o.runId) : void 0;
8700
- 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);
8701
9070
  let digest;
8702
9071
  let ageSeconds;
8703
- if (existsSync19(digestPath2)) {
8704
- ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
8705
- 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");
8706
9075
  }
8707
- const common = usesCommon ? readFileSync17(commonPath, "utf8") : "";
8708
- const brief = readFileSync17(briefPath, "utf8");
9076
+ const common = usesCommon ? readFileSync18(commonPath, "utf8") : "";
9077
+ const brief = readFileSync18(briefPath, "utf8");
8709
9078
  const effectiveCommon = o.divergedPrompt ? "" : common;
8710
9079
  const effectiveBrief = appendAdditions(o.divergedPrompt ?? brief, o.personaAdditions);
8711
9080
  const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
@@ -8727,7 +9096,7 @@ function planAgentRun(o) {
8727
9096
  digest
8728
9097
  });
8729
9098
  const promptSha = createHash3("sha256").update(expanded.text).digest("hex");
8730
- const streamsDir = join20(o.stateDir, "streams");
9099
+ const streamsDir = join21(o.stateDir, "streams");
8731
9100
  const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
8732
9101
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
8733
9102
  const { toolchain, model } = seat;
@@ -8792,13 +9161,16 @@ function planAgentRun(o) {
8792
9161
  // ticket attached, which silently no-ops and strands the ticket
8793
9162
  // in_progress (ISSUE-490, ISSUE-591). Setting this is what lets that
8794
9163
  // hook tell the two apart; it has nothing to do with the run itself.
8795
- [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 })
8796
9168
  },
8797
9169
  digestAttached: digest !== void 0,
8798
9170
  digestAgeSeconds: ageSeconds,
8799
9171
  model,
8800
- streamPath: join20(streamsDir, `${base}.jsonl`),
8801
- eventsPath: join20(streamsDir, `${base}.events.jsonl`),
9172
+ streamPath: join21(streamsDir, `${base}.jsonl`),
9173
+ eventsPath: join21(streamsDir, `${base}.events.jsonl`),
8802
9174
  ticket: o.ticket,
8803
9175
  ticketRecordId: o.ticketRecordId ?? null,
8804
9176
  repo: o.repo ?? null,
@@ -8829,7 +9201,7 @@ function describePlan(p) {
8829
9201
  var pidPathFor = (streamPath) => `${streamPath}.pid`;
8830
9202
  function readRunSidecar(streamPath) {
8831
9203
  try {
8832
- const sc = JSON.parse(readFileSync17(pidPathFor(streamPath), "utf8"));
9204
+ const sc = JSON.parse(readFileSync18(pidPathFor(streamPath), "utf8"));
8833
9205
  return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
8834
9206
  } catch {
8835
9207
  return null;
@@ -8865,7 +9237,7 @@ function identityOf(plan, adapterHarness) {
8865
9237
  var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
8866
9238
  function openSink(path) {
8867
9239
  try {
8868
- mkdirSync10(dirname7(path), { recursive: true });
9240
+ mkdirSync11(dirname8(path), { recursive: true });
8869
9241
  return createWriteStream(path, { flags: "a" });
8870
9242
  } catch {
8871
9243
  return void 0;
@@ -9047,7 +9419,7 @@ var RunObserver = class {
9047
9419
  };
9048
9420
  function writeRunMeta(id, startedAt, liveTicket, extra) {
9049
9421
  try {
9050
- writeFileSync11(`${id.streamPath}.meta.json`, JSON.stringify({
9422
+ writeFileSync12(`${id.streamPath}.meta.json`, JSON.stringify({
9051
9423
  route: id.route,
9052
9424
  role: id.role,
9053
9425
  hint: id.ticket,
@@ -9207,7 +9579,7 @@ function spawnAgent(plan, emit, hintMoved) {
9207
9579
  return new Promise((resolve6, reject) => {
9208
9580
  let streamFd;
9209
9581
  try {
9210
- mkdirSync10(dirname7(plan.streamPath), { recursive: true });
9582
+ mkdirSync11(dirname8(plan.streamPath), { recursive: true });
9211
9583
  streamFd = openSync3(plan.streamPath, "a");
9212
9584
  } catch (e) {
9213
9585
  void endSink(eventsSink);
@@ -9242,7 +9614,7 @@ function spawnAgent(plan, emit, hintMoved) {
9242
9614
  promptSha: plan.promptSha
9243
9615
  };
9244
9616
  try {
9245
- writeFileSync11(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
9617
+ writeFileSync12(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
9246
9618
  } catch {
9247
9619
  }
9248
9620
  }
@@ -9745,7 +10117,7 @@ async function runSelftest(i, deps = realDeps) {
9745
10117
  ...adapter.runEnv(spec),
9746
10118
  [KEY_VAR]: i.apiKey
9747
10119
  };
9748
- const cwd = mkdtempSync(join21(tmpdir2(), "crew-selftest-"));
10120
+ const cwd = mkdtempSync(join22(tmpdir2(), "crew-selftest-"));
9749
10121
  try {
9750
10122
  const run2 = await deps.run({
9751
10123
  bin: invocation.bin,
@@ -9778,11 +10150,11 @@ function formatResult(r) {
9778
10150
  const secs = `${(r.ms / 1e3).toFixed(1)}s`;
9779
10151
  return r.ok ? `${r.toolchain}: PASS ${r.detail} (${secs}${tok})` : `${r.toolchain}: FAIL ${r.failure} \u2014 ${r.detail} (${secs})`;
9780
10152
  }
9781
- var fileFor2 = (stateDir) => join21(stateDir, "selftest.json");
10153
+ var fileFor2 = (stateDir) => join22(stateDir, "selftest.json");
9782
10154
  function readSelftest(stateDir) {
9783
10155
  try {
9784
10156
  const p = fileFor2(stateDir);
9785
- return existsSync20(p) ? JSON.parse(readFileSync18(p, "utf8")) : {};
10157
+ return existsSync21(p) ? JSON.parse(readFileSync19(p, "utf8")) : {};
9786
10158
  } catch {
9787
10159
  return {};
9788
10160
  }
@@ -9790,8 +10162,8 @@ function readSelftest(stateDir) {
9790
10162
  function recordSelftest(stateDir, results) {
9791
10163
  const all = readSelftest(stateDir);
9792
10164
  for (const r of results) all[r.toolchain] = r;
9793
- mkdirSync11(stateDir, { recursive: true });
9794
- writeFileSync12(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
10165
+ mkdirSync12(stateDir, { recursive: true });
10166
+ writeFileSync13(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
9795
10167
  `);
9796
10168
  }
9797
10169
  function doctorSelftestLines(stored, toolchains) {
@@ -9803,7 +10175,7 @@ function doctorSelftestLines(stored, toolchains) {
9803
10175
  }
9804
10176
 
9805
10177
  // src/cli.ts
9806
- 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";
9807
10179
  import { fileURLToPath } from "url";
9808
10180
  import { hostname as hostname2, homedir as homedir10 } from "os";
9809
10181
  import { spawnSync as spawnSync7 } from "child_process";
@@ -9841,9 +10213,15 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
9841
10213
  "workspace",
9842
10214
  "workspace-id",
9843
10215
  "write",
10216
+ "token",
10217
+ "source",
10218
+ "var",
10219
+ "path",
10220
+ "service",
9844
10221
  "topic",
9845
10222
  "assignee",
9846
10223
  "issue",
10224
+ "shell",
9847
10225
  "resume"
9848
10226
  ]);
9849
10227
  function positionalArgs(argv2) {
@@ -9987,14 +10365,14 @@ function rosterMarkdown(roster, meId) {
9987
10365
  }
9988
10366
 
9989
10367
  // src/connect-resume.ts
9990
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, rmSync as rmSync11, writeFileSync as writeFileSync13 } from "fs";
9991
- 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";
9992
10370
  function lastSessionFile(stateDir) {
9993
- return join22(stateDir, "connect-session.json");
10371
+ return join23(stateDir, "connect-session.json");
9994
10372
  }
9995
10373
  function readPointers(stateDir) {
9996
10374
  try {
9997
- const parsed = JSON.parse(readFileSync19(lastSessionFile(stateDir), "utf8"));
10375
+ const parsed = JSON.parse(readFileSync20(lastSessionFile(stateDir), "utf8"));
9998
10376
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
9999
10377
  const out = {};
10000
10378
  for (const [host, v] of Object.entries(parsed)) {
@@ -10008,8 +10386,8 @@ function readPointers(stateDir) {
10008
10386
  }
10009
10387
  function writePointers(stateDir, pointers) {
10010
10388
  const file = lastSessionFile(stateDir);
10011
- mkdirSync12(dirname8(file), { recursive: true });
10012
- writeFileSync13(file, `${JSON.stringify(pointers, null, 2)}
10389
+ mkdirSync13(dirname9(file), { recursive: true });
10390
+ writeFileSync14(file, `${JSON.stringify(pointers, null, 2)}
10013
10391
  `, { mode: 384 });
10014
10392
  }
10015
10393
  function rememberSession(stateDir, host, workspaceSlug) {
@@ -10094,15 +10472,15 @@ async function signOutSessions(o) {
10094
10472
  }
10095
10473
 
10096
10474
  // src/reset.ts
10097
- import { existsSync as existsSync21, mkdirSync as mkdirSync13, readFileSync as readFileSync20, readdirSync as readdirSync9, rmSync as rmSync12, statSync as statSync6, writeFileSync as writeFileSync14 } from "fs";
10098
- 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";
10099
10477
  import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
10100
10478
  function clonedCheckoutsFile(stateDir) {
10101
- return join23(stateDir, "cloned-checkouts.json");
10479
+ return join24(stateDir, "cloned-checkouts.json");
10102
10480
  }
10103
10481
  function readClonedCheckouts(stateDir) {
10104
10482
  try {
10105
- const parsed = JSON.parse(readFileSync20(clonedCheckoutsFile(stateDir), "utf8"));
10483
+ const parsed = JSON.parse(readFileSync21(clonedCheckoutsFile(stateDir), "utf8"));
10106
10484
  return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
10107
10485
  } catch {
10108
10486
  return [];
@@ -10112,8 +10490,8 @@ function recordClonedCheckout(stateDir, dir) {
10112
10490
  const known = readClonedCheckouts(stateDir);
10113
10491
  if (known.includes(dir)) return;
10114
10492
  const file = clonedCheckoutsFile(stateDir);
10115
- mkdirSync13(dirname9(file), { recursive: true });
10116
- writeFileSync14(file, `${JSON.stringify([...known, dir], null, 2)}
10493
+ mkdirSync14(dirname10(file), { recursive: true });
10494
+ writeFileSync15(file, `${JSON.stringify([...known, dir], null, 2)}
10117
10495
  `);
10118
10496
  }
10119
10497
  var mark = (dryRun) => dryRun ? "would-remove" : "removed";
@@ -10165,7 +10543,7 @@ async function runReset(o, fx) {
10165
10543
  for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
10166
10544
  }
10167
10545
  steps.push(...removeState(o.stateDir, o.dryRun));
10168
- if (existsSync21(o.configFile)) {
10546
+ if (existsSync22(o.configFile)) {
10169
10547
  if (!o.dryRun) rmSync12(o.configFile, { force: true });
10170
10548
  steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
10171
10549
  } else {
@@ -10174,7 +10552,7 @@ async function runReset(o, fx) {
10174
10552
  return steps;
10175
10553
  }
10176
10554
  function removeCheckout(dir, dryRun, fx) {
10177
- 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" };
10178
10556
  const { dirty, linkedWorktrees } = fx.inspectCheckout(dir);
10179
10557
  if (dirty.length) {
10180
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" : ""})` };
@@ -10187,15 +10565,15 @@ function removeCheckout(dir, dryRun, fx) {
10187
10565
  }
10188
10566
  function removeState(stateDir, dryRun) {
10189
10567
  const out = [];
10190
- 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")];
10191
10569
  try {
10192
10570
  for (const name of readdirSync9(stateDir)) {
10193
- 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));
10194
10572
  }
10195
10573
  } catch {
10196
10574
  }
10197
10575
  for (const t of targets) {
10198
- if (!existsSync21(t)) continue;
10576
+ if (!existsSync22(t)) continue;
10199
10577
  if (!dryRun) rmSync12(t, { recursive: true, force: true });
10200
10578
  out.push({ step: "state", target: t, action: mark(dryRun) });
10201
10579
  }
@@ -10983,8 +11361,8 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
10983
11361
  }
10984
11362
 
10985
11363
  // src/poll.ts
10986
- import { writeFileSync as writeFileSync15 } from "fs";
10987
- import { join as join24 } from "path";
11364
+ import { writeFileSync as writeFileSync16 } from "fs";
11365
+ import { join as join25 } from "path";
10988
11366
 
10989
11367
  // src/stalled.ts
10990
11368
  var STALLED_AFTER_CYCLES = 30;
@@ -11291,7 +11669,7 @@ async function decideCycle(o) {
11291
11669
  }
11292
11670
  return decision;
11293
11671
  }
11294
- 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`);
11295
11673
  function otherRunsRemoved(tickets, own) {
11296
11674
  if (!own) return tickets;
11297
11675
  return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
@@ -11369,7 +11747,7 @@ function writeDigest(o, d, role, stateDir, runId, own) {
11369
11747
 
11370
11748
  ${body}` : body;
11371
11749
  const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
11372
- writeFileSync15(path, text);
11750
+ writeFileSync16(path, text);
11373
11751
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
11374
11752
  data: { role, bytes: Buffer.byteLength(text) }
11375
11753
  });
@@ -11852,13 +12230,13 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
11852
12230
  var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
11853
12231
 
11854
12232
  // src/connect-wizard.ts
11855
- import { delimiter, dirname as dirname10, join as join25 } from "path";
11856
- 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";
11857
12235
  import { stringify } from "yaml";
11858
12236
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
11859
12237
  function shouldRunWizard(opts) {
11860
12238
  if (!opts.isTTY) return false;
11861
- const exists = opts.configExists ?? existsSync22;
12239
+ const exists = opts.configExists ?? existsSync23;
11862
12240
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
11863
12241
  }
11864
12242
  function firstRunConfigPath() {
@@ -11875,7 +12253,7 @@ function defaultIsExecutable(p) {
11875
12253
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
11876
12254
  for (const dir of pathEnv.split(delimiter)) {
11877
12255
  if (!dir) continue;
11878
- const candidate = join25(dir, name);
12256
+ const candidate = join26(dir, name);
11879
12257
  if (isExecutable(candidate)) return candidate;
11880
12258
  }
11881
12259
  return void 0;
@@ -11901,9 +12279,9 @@ async function runWizard(p, env) {
11901
12279
  if (entered && isExec(entered)) agentBin = entered;
11902
12280
  else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
11903
12281
  }
11904
- const nodeDir = dirname10(env.nodePath);
12282
+ const nodeDir = dirname11(env.nodePath);
11905
12283
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
11906
- const baseDirs = [nodeDir, pnpmPath ? dirname10(pnpmPath) : void 0];
12284
+ const baseDirs = [nodeDir, pnpmPath ? dirname11(pnpmPath) : void 0];
11907
12285
  p.say(
11908
12286
  `
11909
12287
  Host Passengers lets this machine run the agent sessions that other people's
@@ -11914,7 +12292,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
11914
12292
  p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
11915
12293
  }
11916
12294
  const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
11917
- const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname10(dockerPath) : void 0]);
12295
+ const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname11(dockerPath) : void 0]);
11918
12296
  if (extraPath) {
11919
12297
  p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
11920
12298
  }
@@ -11936,9 +12314,9 @@ routes:
11936
12314
  ${routeBlock}`;
11937
12315
  }
11938
12316
  function writeNewConfig(path, text) {
11939
- mkdirSync14(dirname10(path), { recursive: true });
12317
+ mkdirSync15(dirname11(path), { recursive: true });
11940
12318
  try {
11941
- writeFileSync16(path, text, { mode: 384, flag: "wx" });
12319
+ writeFileSync17(path, text, { mode: 384, flag: "wx" });
11942
12320
  return true;
11943
12321
  } catch (e) {
11944
12322
  if (e.code === "EEXIST") return false;
@@ -11972,7 +12350,7 @@ Wrote ${o.configPath}.
11972
12350
  // src/connect-json.ts
11973
12351
  import { accessSync as accessSync3, constants as constants3 } from "fs";
11974
12352
  import { homedir as homedir8 } from "os";
11975
- import { dirname as dirname11, join as join26 } from "path";
12353
+ import { dirname as dirname12, join as join27 } from "path";
11976
12354
  import { delimiter as delimiter2 } from "path";
11977
12355
  var CHOICE_EXIT_CODE = 3;
11978
12356
  function createEmitter(json, write) {
@@ -12011,12 +12389,12 @@ function agentBinSearchDirs(home, pathEnv, extraPath) {
12011
12389
  const dirs = [
12012
12390
  ...pathEnv.split(delimiter2),
12013
12391
  ...(extraPath ?? "").split(delimiter2),
12014
- join26(home, ".local/bin"),
12015
- join26(home, ".claude/local"),
12392
+ join27(home, ".local/bin"),
12393
+ join27(home, ".claude/local"),
12016
12394
  "/opt/homebrew/bin",
12017
12395
  "/usr/local/bin",
12018
- join26(home, ".npm-global/bin"),
12019
- join26(home, ".volta/bin")
12396
+ join27(home, ".npm-global/bin"),
12397
+ join27(home, ".volta/bin")
12020
12398
  ];
12021
12399
  return [...new Set(dirs.filter(Boolean))];
12022
12400
  }
@@ -12038,7 +12416,7 @@ function flagWizardAnswers(f, env) {
12038
12416
  const agentBin = f.agentBin ?? found?.bin;
12039
12417
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
12040
12418
  const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
12041
- 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]);
12042
12420
  return {
12043
12421
  answers: {
12044
12422
  name: f.shipName?.trim() || env.hostname,
@@ -12052,8 +12430,8 @@ function flagWizardAnswers(f, env) {
12052
12430
  }
12053
12431
 
12054
12432
  // src/release-run.ts
12055
- import { readFileSync as readFileSync21, writeFileSync as writeFileSync17, existsSync as existsSync23, copyFileSync, mkdirSync as mkdirSync15, mkdtempSync as mkdtempSync2, rmSync as rmSync13 } from "fs";
12056
- 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";
12057
12435
  import { tmpdir as tmpdir3 } from "os";
12058
12436
 
12059
12437
  // src/release.ts
@@ -12794,13 +13172,13 @@ async function bump(o, size) {
12794
13172
  }
12795
13173
  return printed;
12796
13174
  }
12797
- 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));
12798
13176
  const first = files[0];
12799
- if (!existsSync23(first)) {
13177
+ if (!existsSync24(first)) {
12800
13178
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
12801
13179
  return null;
12802
13180
  }
12803
- const current = JSON.parse(readFileSync21(first, "utf8")).version;
13181
+ const current = JSON.parse(readFileSync22(first, "utf8")).version;
12804
13182
  if (!current) {
12805
13183
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
12806
13184
  return null;
@@ -12813,27 +13191,27 @@ async function bump(o, size) {
12813
13191
  return version;
12814
13192
  }
12815
13193
  for (const f of files) {
12816
- const src = readFileSync21(f, "utf8");
13194
+ const src = readFileSync22(f, "utf8");
12817
13195
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
12818
13196
  if (out === src) {
12819
13197
  o.emit.error(`no version field in ${f}`);
12820
13198
  return null;
12821
13199
  }
12822
- writeFileSync17(f, out);
13200
+ writeFileSync18(f, out);
12823
13201
  }
12824
13202
  return version;
12825
13203
  }
12826
13204
  function writeChangelog(o, version, entries) {
12827
13205
  const file = o.repo.release.changelog;
12828
13206
  if (!file || entries.length === 0) return;
12829
- const path = join27(o.cwd, file);
13207
+ const path = join28(o.cwd, file);
12830
13208
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
12831
13209
  if (o.dryRun) {
12832
13210
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
12833
13211
  return;
12834
13212
  }
12835
- const existing = existsSync23(path) ? readFileSync21(path, "utf8") : "# Changelog\n";
12836
- writeFileSync17(path, insertChangelogSection(existing, section));
13213
+ const existing = existsSync24(path) ? readFileSync22(path, "utf8") : "# Changelog\n";
13214
+ writeFileSync18(path, insertChangelogSection(existing, section));
12837
13215
  }
12838
13216
  async function confirm(o, expected) {
12839
13217
  if (!o.repo.hooks.released) return void 0;
@@ -12926,8 +13304,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
12926
13304
  function openReleaseCheckout(o) {
12927
13305
  const { remote, base } = o.repo.branch;
12928
13306
  const branch = `crew/release-${Date.now()}`;
12929
- const root = mkdtempSync2(join27(tmpdir3(), "crew-release-"));
12930
- const dir = join27(root, "checkout");
13307
+ const root = mkdtempSync2(join28(tmpdir3(), "crew-release-"));
13308
+ const dir = join28(root, "checkout");
12931
13309
  o.emit.emit(`release checkout: ${dir}`, { step: "release" });
12932
13310
  const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
12933
13311
  try {
@@ -12937,10 +13315,10 @@ function openReleaseCheckout(o) {
12937
13315
  throw e;
12938
13316
  }
12939
13317
  for (const f of o.repo.worktrees.copy) {
12940
- const src = join27(o.cwd, f);
12941
- if (!existsSync23(src)) continue;
12942
- mkdirSync15(dirname12(join27(dir, f)), { recursive: true });
12943
- 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));
12944
13322
  }
12945
13323
  return { dir, branch, close };
12946
13324
  }
@@ -13641,12 +14019,12 @@ Passed: ${passed}.${willClose ? " Closing this alert." : ""}`,
13641
14019
  }
13642
14020
 
13643
14021
  // src/live-runs.ts
13644
- import { existsSync as existsSync25, readdirSync as readdirSync11 } from "fs";
13645
- import { join as join29 } from "path";
14022
+ import { existsSync as existsSync26, readdirSync as readdirSync11 } from "fs";
14023
+ import { join as join30 } from "path";
13646
14024
 
13647
14025
  // src/interrupted.ts
13648
- import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as readFileSync22 } from "fs";
13649
- 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";
13650
14028
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
13651
14029
  function interruptedComment(b) {
13652
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.";
@@ -13665,21 +14043,21 @@ var pidAlive = (pid) => {
13665
14043
  };
13666
14044
  function runFinished(streamPath) {
13667
14045
  const path = `${streamPath}.meta.json`;
13668
- if (!existsSync24(path)) return false;
14046
+ if (!existsSync25(path)) return false;
13669
14047
  try {
13670
- return JSON.parse(readFileSync22(path, "utf8")).running !== true;
14048
+ return JSON.parse(readFileSync23(path, "utf8")).running !== true;
13671
14049
  } catch {
13672
14050
  return true;
13673
14051
  }
13674
14052
  }
13675
14053
  function orphanedStreams(o) {
13676
- const dir = join28(o.stateDir, "streams");
13677
- if (!existsSync24(dir)) return [];
14054
+ const dir = join29(o.stateDir, "streams");
14055
+ if (!existsSync25(dir)) return [];
13678
14056
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
13679
14057
  const out = [];
13680
14058
  for (const name of readdirSync10(dir)) {
13681
14059
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
13682
- const streamPath = join28(dir, name);
14060
+ const streamPath = join29(dir, name);
13683
14061
  if (runFinished(streamPath)) continue;
13684
14062
  const sidecar = readRunSidecar(streamPath);
13685
14063
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -13733,12 +14111,12 @@ async function reconcileRuns(o) {
13733
14111
 
13734
14112
  // src/live-runs.ts
13735
14113
  function liveRuns(stateDir, isAlive = runIsAlive) {
13736
- const dir = join29(stateDir, "streams");
13737
- if (!existsSync25(dir)) return [];
14114
+ const dir = join30(stateDir, "streams");
14115
+ if (!existsSync26(dir)) return [];
13738
14116
  const out = [];
13739
14117
  for (const name of readdirSync11(dir)) {
13740
14118
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
13741
- const streamPath = join29(dir, name);
14119
+ const streamPath = join30(dir, name);
13742
14120
  if (runFinished(streamPath)) continue;
13743
14121
  const sidecar = readRunSidecar(streamPath);
13744
14122
  if (!sidecar || !isAlive(sidecar)) continue;
@@ -13837,6 +14215,70 @@ function formatProjectsReport(report) {
13837
14215
  `;
13838
14216
  }
13839
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
+
13840
14282
  // src/whoami.ts
13841
14283
  async function buildWhoami(routes, deps = {}) {
13842
14284
  const doFetch = deps.fetch ?? fetch;
@@ -14008,7 +14450,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
14008
14450
  }
14009
14451
 
14010
14452
  // src/watch.ts
14011
- 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";
14012
14454
  var E = "\x1B[";
14013
14455
  var dim = (s) => `${E}2m${s}${E}0m`;
14014
14456
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -14109,8 +14551,8 @@ function renderHeaders(states, now = Date.now()) {
14109
14551
  return states.map((s) => renderHeader(s, now)).join("");
14110
14552
  }
14111
14553
  function readFrom(file, from) {
14112
- if (!existsSync26(file)) return { events: [], offset: from };
14113
- const size = statSync7(file).size;
14554
+ if (!existsSync27(file)) return { events: [], offset: from };
14555
+ const size = statSync8(file).size;
14114
14556
  if (size < from) return readFrom(file, 0);
14115
14557
  if (size === from) return { events: [], offset: from };
14116
14558
  const fd = openSync4(file, "r");
@@ -14352,31 +14794,31 @@ function nextRoles(decision, ran) {
14352
14794
  }
14353
14795
 
14354
14796
  // src/ship-name.ts
14355
- import { existsSync as existsSync28, mkdirSync as mkdirSync17, readFileSync as readFileSync24, renameSync as renameSync2, writeFileSync as writeFileSync19 } from "fs";
14356
- import { dirname as dirname14 } from "path";
14357
- 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";
14358
14800
 
14359
14801
  // src/machine-id.ts
14360
- import { execFileSync as execFileSync4 } from "child_process";
14802
+ import { execFileSync as execFileSync5 } from "child_process";
14361
14803
  import { createHash as createHash4 } from "crypto";
14362
- import { existsSync as existsSync27, mkdirSync as mkdirSync16, readFileSync as readFileSync23, writeFileSync as writeFileSync18 } from "fs";
14363
- 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";
14364
14806
  function machineIdPathFor(stateDir) {
14365
- return join30(stateDir, "machine-id");
14807
+ return join31(stateDir, "machine-id");
14366
14808
  }
14367
14809
  var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
14368
14810
  function readRawMachineId(platform2 = process.platform) {
14369
14811
  try {
14370
14812
  if (platform2 === "darwin") {
14371
- const out = execFileSync4("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
14813
+ const out = execFileSync5("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
14372
14814
  encoding: "utf8",
14373
14815
  stdio: ["ignore", "pipe", "ignore"]
14374
14816
  });
14375
14817
  return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
14376
14818
  }
14377
14819
  for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
14378
- if (existsSync27(p)) {
14379
- const id = readFileSync23(p, "utf8").trim();
14820
+ if (existsSync28(p)) {
14821
+ const id = readFileSync24(p, "utf8").trim();
14380
14822
  if (id) return id;
14381
14823
  }
14382
14824
  }
@@ -14387,15 +14829,15 @@ function readRawMachineId(platform2 = process.platform) {
14387
14829
  function ensureMachineId(stateDir, opts = {}) {
14388
14830
  if (opts.override?.trim()) return sha256(opts.override.trim());
14389
14831
  const path = machineIdPathFor(stateDir);
14390
- if (existsSync27(path)) {
14391
- const cached = readFileSync23(path, "utf8").trim();
14832
+ if (existsSync28(path)) {
14833
+ const cached = readFileSync24(path, "utf8").trim();
14392
14834
  if (/^[0-9a-f]{64}$/.test(cached)) return cached;
14393
14835
  }
14394
14836
  const raw = (opts.read ?? readRawMachineId)();
14395
14837
  if (!raw) return void 0;
14396
14838
  const hashed = sha256(raw);
14397
- mkdirSync16(dirname13(path), { recursive: true });
14398
- writeFileSync18(path, `${hashed}
14839
+ mkdirSync17(dirname14(path), { recursive: true });
14840
+ writeFileSync19(path, `${hashed}
14399
14841
  `, { mode: 384 });
14400
14842
  return hashed;
14401
14843
  }
@@ -14424,11 +14866,11 @@ function planNameSync(local, row, lastSynced) {
14424
14866
  return { drift: "local", action: "push" };
14425
14867
  }
14426
14868
  function shipNameOfYaml(text) {
14427
- const v = parseDocument(text).getIn(["ship", "name"]);
14869
+ const v = parseDocument2(text).getIn(["ship", "name"]);
14428
14870
  return typeof v === "string" && v.trim() ? v.trim() : void 0;
14429
14871
  }
14430
14872
  function setShipNameInYaml(text, name) {
14431
- const doc = parseDocument(text);
14873
+ const doc = parseDocument2(text);
14432
14874
  doc.setIn(["ship", "name"], name);
14433
14875
  return doc.toString();
14434
14876
  }
@@ -14466,34 +14908,34 @@ async function syncRouteName(i) {
14466
14908
  return { status: "ok", ...base, action, name: i.local };
14467
14909
  }
14468
14910
  function readSyncedName(resolvedFile) {
14469
- if (!existsSync28(resolvedFile)) return void 0;
14911
+ if (!existsSync29(resolvedFile)) return void 0;
14470
14912
  try {
14471
- const v = JSON.parse(readFileSync24(resolvedFile, "utf8")).shipName;
14913
+ const v = JSON.parse(readFileSync25(resolvedFile, "utf8")).shipName;
14472
14914
  return typeof v === "string" ? v : void 0;
14473
14915
  } catch {
14474
14916
  return void 0;
14475
14917
  }
14476
14918
  }
14477
14919
  function writeSyncedName(resolvedFile, name) {
14478
- if (!existsSync28(resolvedFile)) return;
14920
+ if (!existsSync29(resolvedFile)) return;
14479
14921
  let raw;
14480
14922
  try {
14481
- raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
14923
+ raw = JSON.parse(readFileSync25(resolvedFile, "utf8"));
14482
14924
  } catch {
14483
14925
  return;
14484
14926
  }
14485
14927
  if (raw.shipName === name) return;
14486
14928
  raw.shipName = name;
14487
- mkdirSync17(dirname14(resolvedFile), { recursive: true });
14929
+ mkdirSync18(dirname15(resolvedFile), { recursive: true });
14488
14930
  const tmp = `${resolvedFile}.tmp${process.pid}`;
14489
- writeFileSync19(tmp, `${JSON.stringify(raw, null, 2)}
14931
+ writeFileSync20(tmp, `${JSON.stringify(raw, null, 2)}
14490
14932
  `);
14491
14933
  renameSync2(tmp, resolvedFile);
14492
14934
  }
14493
14935
  async function reconcileShipName(i) {
14494
14936
  let local = i.ship.name;
14495
14937
  try {
14496
- local = shipNameOfYaml(readFileSync24(i.configFile, "utf8")) ?? local;
14938
+ local = shipNameOfYaml(readFileSync25(i.configFile, "utf8")) ?? local;
14497
14939
  } catch {
14498
14940
  }
14499
14941
  const resolvedFile = resolvedPathFor(i.stateDir, i.route);
@@ -14517,8 +14959,8 @@ async function reconcileShipName(i) {
14517
14959
  }
14518
14960
  if (result.status !== "ok") return result;
14519
14961
  if (result.action === "adopt" || result.action === "row-wins") {
14520
- const text = readFileSync24(i.configFile, "utf8");
14521
- writeFileSync19(i.configFile, setShipNameInYaml(text, result.name));
14962
+ const text = readFileSync25(i.configFile, "utf8");
14963
+ writeFileSync20(i.configFile, setShipNameInYaml(text, result.name));
14522
14964
  i.ship.name = result.name;
14523
14965
  if (result.action === "row-wins") {
14524
14966
  i.emit?.warn(
@@ -15168,8 +15610,8 @@ function describeCapability(i) {
15168
15610
  }
15169
15611
 
15170
15612
  // src/agents-sync-run.ts
15171
- import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync25, writeFileSync as writeFileSync20 } from "fs";
15172
- 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";
15173
15615
  async function runAgentsSync(target, o) {
15174
15616
  const dryRun = o.dryRun ?? false;
15175
15617
  const force = o.force ?? false;
@@ -15202,12 +15644,12 @@ async function runAgentsSync(target, o) {
15202
15644
  for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
15203
15645
  if (!dryRun) {
15204
15646
  const resolvedPath = resolvedPathFor(o.stateDir, target.route);
15205
- const raw = existsSync29(resolvedPath) ? JSON.parse(readFileSync25(resolvedPath, "utf8")) : {};
15647
+ const raw = existsSync30(resolvedPath) ? JSON.parse(readFileSync26(resolvedPath, "utf8")) : {};
15206
15648
  raw.agentPersonas = personaResult.agentPersonas;
15207
15649
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
15208
15650
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
15209
- mkdirSync18(dirname15(resolvedPath), { recursive: true });
15210
- writeFileSync20(resolvedPath, `${JSON.stringify(raw, null, 2)}
15651
+ mkdirSync19(dirname16(resolvedPath), { recursive: true });
15652
+ writeFileSync21(resolvedPath, `${JSON.stringify(raw, null, 2)}
15211
15653
  `);
15212
15654
  if (target.resolved) {
15213
15655
  target.resolved.agentPersonas = personaResult.agentPersonas;
@@ -15311,7 +15753,7 @@ async function showLogEntry(route, entryId, opts = {}) {
15311
15753
 
15312
15754
  // src/repos-check.ts
15313
15755
  import { spawnSync as spawnSync6 } from "child_process";
15314
- import { readFileSync as readFileSync26 } from "fs";
15756
+ import { readFileSync as readFileSync27 } from "fs";
15315
15757
  import { basename as basename8 } from "path";
15316
15758
  var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
15317
15759
  function checkHookSyntax(script, shell) {
@@ -15352,7 +15794,7 @@ function checkRepoSpec(dir, opts = {}) {
15352
15794
  }
15353
15795
  let parsed;
15354
15796
  try {
15355
- parsed = parseRepoConfig(readFileSync26(file, "utf8"), file);
15797
+ parsed = parseRepoConfig(readFileSync27(file, "utf8"), file);
15356
15798
  } catch (e) {
15357
15799
  if (!(e instanceof RepoConfigError)) throw e;
15358
15800
  return { ...empty, file, error: e.message, exitCode: 1 };
@@ -15405,12 +15847,12 @@ function renderRepoCheck(r) {
15405
15847
  }
15406
15848
 
15407
15849
  // src/repos-hooks.ts
15408
- import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml";
15850
+ import { isMap as isMap2, parseDocument as parseDocument4, YAMLMap } from "yaml";
15409
15851
 
15410
15852
  // src/repos-cmd.ts
15411
15853
  import { homedir as homedir9 } from "os";
15412
- import { basename as basename9, join as join31, resolve as resolve3 } from "path";
15413
- 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";
15414
15856
  var ReposError = class extends Error {
15415
15857
  };
15416
15858
  function normalizeRemote(url) {
@@ -15434,7 +15876,7 @@ function matchRepoName(origin, resolved) {
15434
15876
  return void 0;
15435
15877
  }
15436
15878
  function routeNamesOf(text) {
15437
- const routes = parseDocument2(text).get("routes", true);
15879
+ const routes = parseDocument3(text).get("routes", true);
15438
15880
  if (!isSeq(routes)) return [];
15439
15881
  return routes.items.flatMap((i) => isMap(i) && typeof i.get("route") === "string" ? [i.get("route")] : []);
15440
15882
  }
@@ -15469,7 +15911,7 @@ function routeMapOf(doc, route) {
15469
15911
  throw new ReposError(`no route "${route}" in the config (have: ${have})`);
15470
15912
  }
15471
15913
  function configuredRepos(text, route, base) {
15472
- const doc = parseDocument2(text);
15914
+ const doc = parseDocument3(text);
15473
15915
  const m = routeMapOf(doc, route);
15474
15916
  const repos = m.get("repos");
15475
15917
  if (isMap(repos)) {
@@ -15484,7 +15926,7 @@ function configuredRepos(text, route, base) {
15484
15926
  return dir ? [{ name: basename9(String(dir)), dir: expandDir(String(dir), base) }] : [];
15485
15927
  }
15486
15928
  function cloneBaseOf(text, route, base, isCheckout) {
15487
- const m = routeMapOf(parseDocument2(text), route);
15929
+ const m = routeMapOf(parseDocument3(text), route);
15488
15930
  const repos = m.get("repos", true);
15489
15931
  if (isMap(repos) && repos.items.length > 0) return null;
15490
15932
  const dir = m.get("dir");
@@ -15493,7 +15935,7 @@ function cloneBaseOf(text, route, base, isCheckout) {
15493
15935
  return isCheckout(abs) ? null : abs;
15494
15936
  }
15495
15937
  function routeSetsHooks(text, route, name) {
15496
- const m = routeMapOf(parseDocument2(text), route);
15938
+ const m = routeMapOf(parseDocument3(text), route);
15497
15939
  const repos = m.get("repos", true);
15498
15940
  let target = m;
15499
15941
  if (isMap(repos)) target = repos.get(name, true);
@@ -15550,7 +15992,7 @@ function listRepoRows(i) {
15550
15992
  return rows;
15551
15993
  }
15552
15994
  function planRepoAdd(i) {
15553
- const doc = parseDocument2(i.text);
15995
+ const doc = parseDocument3(i.text);
15554
15996
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
15555
15997
  const m = routeMapOf(doc, i.route);
15556
15998
  let existing = configuredRepos(i.text, i.route, i.base);
@@ -15601,7 +16043,7 @@ function planRepoAdd(i) {
15601
16043
  return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched, droppedDir };
15602
16044
  }
15603
16045
  function planRepoRemove(text, base, route, path) {
15604
- const doc = parseDocument2(text);
16046
+ const doc = parseDocument3(text);
15605
16047
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
15606
16048
  const m = routeMapOf(doc, route);
15607
16049
  const target = expandDir(path, base);
@@ -15679,7 +16121,7 @@ function planRepoClone(i) {
15679
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\``
15680
16122
  );
15681
16123
  }
15682
- const doc = parseDocument2(i.text);
16124
+ const doc = parseDocument3(i.text);
15683
16125
  const m = routeMapOf(doc, i.route);
15684
16126
  const existing = configuredRepos(i.text, i.route, i.base);
15685
16127
  if (existing.some((e) => e.name.toLowerCase() === name.toLowerCase())) {
@@ -15687,7 +16129,7 @@ function planRepoClone(i) {
15687
16129
  }
15688
16130
  const ship = doc.get("ship", true);
15689
16131
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
15690
- 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);
15691
16133
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
15692
16134
  return { ...add, remote, target };
15693
16135
  }
@@ -15833,7 +16275,7 @@ function repoDirOf(text, base, route, name) {
15833
16275
  return hit.dir;
15834
16276
  }
15835
16277
  function planRouteHooks(text, route, name, hooks, opts = {}) {
15836
- const doc = parseDocument3(text);
16278
+ const doc = parseDocument4(text);
15837
16279
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
15838
16280
  const m = routeMapOf(doc, route);
15839
16281
  const repos = m.get("repos", true);
@@ -15870,7 +16312,7 @@ function planRouteHooks(text, route, name, hooks, opts = {}) {
15870
16312
  return { text: String(doc), written, kept, replaced };
15871
16313
  }
15872
16314
  function clearRouteHooks(text, route, name) {
15873
- const doc = parseDocument3(text);
16315
+ const doc = parseDocument4(text);
15874
16316
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
15875
16317
  const m = routeMapOf(doc, route);
15876
16318
  const repos = m.get("repos", true);
@@ -15914,7 +16356,7 @@ function existingRepoConfig(f) {
15914
16356
  }
15915
16357
 
15916
16358
  // src/repos-hooks-agent.ts
15917
- import { spawn as spawn4, execFileSync as execFileSync5 } from "child_process";
16359
+ import { spawn as spawn4, execFileSync as execFileSync6 } from "child_process";
15918
16360
  import { realpathSync as realpathSync2 } from "fs";
15919
16361
  import { isAbsolute as isAbsolute2, relative } from "path";
15920
16362
  var AgentSuggestError = class extends Error {
@@ -16017,7 +16459,7 @@ function classifyFailure(o) {
16017
16459
  }
16018
16460
  function porcelain(dir) {
16019
16461
  try {
16020
- 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" });
16021
16463
  } catch {
16022
16464
  return void 0;
16023
16465
  }
@@ -16086,7 +16528,7 @@ ${run2.err}` });
16086
16528
  }
16087
16529
 
16088
16530
  // src/sample-ticket.ts
16089
- 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";
16090
16532
  var SAMPLE_REPO = "crew-sample";
16091
16533
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
16092
16534
  var SAMPLE_MARKER = "sample: true";
@@ -16109,8 +16551,8 @@ has something safe to work on: its test and build hooks always pass, it has no
16109
16551
  remote, and \`crew sample-ticket --remove\` deletes it.
16110
16552
  `;
16111
16553
  function sampleDir(existing, reposBase, workspace) {
16112
- if (existing.length > 0) return join32(dirname17(existing[0]), SAMPLE_REPO);
16113
- 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);
16114
16556
  }
16115
16557
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
16116
16558
  function sampleDescription() {
@@ -16154,7 +16596,7 @@ function planSampleTicket(i) {
16154
16596
  }
16155
16597
 
16156
16598
  // src/doctor-fix.ts
16157
- import { parseDocument as parseDocument4 } from "yaml";
16599
+ import { parseDocument as parseDocument5 } from "yaml";
16158
16600
  var sound = (r) => r.hasRepos && r.problems.length === 0;
16159
16601
  function planFix(f) {
16160
16602
  const actions = [];
@@ -16192,7 +16634,7 @@ function routesInScope(routes, named) {
16192
16634
  return named ? routes.filter((r) => r.route === named) : routes;
16193
16635
  }
16194
16636
  function planEnable(text, route) {
16195
- const doc = parseDocument4(text);
16637
+ const doc = parseDocument5(text);
16196
16638
  routeMapOf(doc, route).set("enabled", true);
16197
16639
  return String(doc);
16198
16640
  }
@@ -16202,8 +16644,8 @@ function schedulerInstalled(plan, exists, crontab) {
16202
16644
  }
16203
16645
 
16204
16646
  // src/worktree-sweep.ts
16205
- import { existsSync as existsSync30, readFileSync as readFileSync27, readdirSync as readdirSync12 } from "fs";
16206
- 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";
16207
16649
  function planWorktreeSweep(target, tickets, contract) {
16208
16650
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
16209
16651
  const verified = contract.statuses.verified;
@@ -16267,7 +16709,7 @@ function worktreeTicketNumbers(target) {
16267
16709
  return [...nums];
16268
16710
  }
16269
16711
  function siblingWorktreeDirs(target) {
16270
- const parent = dirname18(target.dir.replace(/[/\\]+$/, ""));
16712
+ const parent = dirname19(target.dir.replace(/[/\\]+$/, ""));
16271
16713
  const stem = `${basename11(target.dir.replace(/[/\\]+$/, ""))}-`;
16272
16714
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
16273
16715
  let names;
@@ -16280,7 +16722,7 @@ function siblingWorktreeDirs(target) {
16280
16722
  for (const name of names) {
16281
16723
  if (!name.startsWith(stem)) continue;
16282
16724
  const m = DIR_NUMBER_SUFFIXED.exec(name);
16283
- const path = join33(parent, name);
16725
+ const path = join34(parent, name);
16284
16726
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
16285
16727
  }
16286
16728
  return out;
@@ -16315,7 +16757,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
16315
16757
  let removed = 0;
16316
16758
  let keptBranches = 0;
16317
16759
  for (const a of actions) {
16318
- 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)) {
16319
16761
  if (gitOk(a.path, ["merge", "--abort"]) !== null) {
16320
16762
  log.emit(
16321
16763
  `abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
@@ -16323,7 +16765,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
16323
16765
  );
16324
16766
  }
16325
16767
  }
16326
- if (!dryRun && existsSync30(a.path)) {
16768
+ if (!dryRun && existsSync31(a.path)) {
16327
16769
  let dirty = false;
16328
16770
  try {
16329
16771
  dirty = status(a.path).length > 0;
@@ -16334,7 +16776,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
16334
16776
  continue;
16335
16777
  }
16336
16778
  }
16337
- 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)) {
16338
16780
  refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
16339
16781
  continue;
16340
16782
  }
@@ -16383,7 +16825,7 @@ function isHandBackMerge(path, base) {
16383
16825
  if (!msgPath) return false;
16384
16826
  let first;
16385
16827
  try {
16386
- first = readFileSync27(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
16828
+ first = readFileSync28(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
16387
16829
  } catch {
16388
16830
  return false;
16389
16831
  }
@@ -16488,16 +16930,16 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
16488
16930
  }
16489
16931
 
16490
16932
  // src/stream-sweep.ts
16491
- import { existsSync as existsSync31, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync8 } from "fs";
16492
- 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";
16493
16935
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
16494
16936
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
16495
- const dir = join34(stateDir, "streams");
16496
- if (!existsSync31(dir)) return [];
16937
+ const dir = join35(stateDir, "streams");
16938
+ if (!existsSync32(dir)) return [];
16497
16939
  const cutoff = now - retentionDays * DAY_MS2;
16498
- return readdirSync13(dir).map((name) => join34(dir, name)).filter((path) => {
16940
+ return readdirSync13(dir).map((name) => join35(dir, name)).filter((path) => {
16499
16941
  try {
16500
- return statSync8(path).mtimeMs < cutoff;
16942
+ return statSync9(path).mtimeMs < cutoff;
16501
16943
  } catch {
16502
16944
  return false;
16503
16945
  }
@@ -16521,11 +16963,11 @@ function applyStreamSweep(paths, dryRun, log) {
16521
16963
  return removed;
16522
16964
  }
16523
16965
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
16524
- if (!existsSync31(stateDir)) return [];
16966
+ if (!existsSync32(stateDir)) return [];
16525
16967
  const cutoff = now - retentionDays * DAY_MS2;
16526
- 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) => {
16527
16969
  try {
16528
- return statSync8(path).mtimeMs < cutoff;
16970
+ return statSync9(path).mtimeMs < cutoff;
16529
16971
  } catch {
16530
16972
  return false;
16531
16973
  }
@@ -16533,30 +16975,30 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
16533
16975
  }
16534
16976
 
16535
16977
  // src/cli.ts
16536
- 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";
16537
16979
  import { createInterface } from "readline/promises";
16538
16980
 
16539
16981
  // src/periscope-cmd.ts
16540
- import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync28, rmSync as rmSync15, writeFileSync as writeFileSync21 } from "fs";
16541
- 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";
16542
16984
  var PeriscopeError = class extends Error {
16543
16985
  };
16544
- var priorDir = (stateDir) => join35(stateDir, "periscope-priors");
16545
- 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`);
16546
16988
  function removePeriscopePrior(stateDir, ticketRecordId) {
16547
16989
  rmSync15(priorPath(stateDir, ticketRecordId), { force: true });
16548
16990
  }
16549
16991
  function writePeriscopePrior(stateDir, ticketRecordId, prior) {
16550
16992
  try {
16551
- mkdirSync19(priorDir(stateDir), { recursive: true });
16552
- writeFileSync21(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
16993
+ mkdirSync20(priorDir(stateDir), { recursive: true });
16994
+ writeFileSync22(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
16553
16995
  } catch {
16554
16996
  }
16555
16997
  }
16556
16998
  function readPeriscopePrior(stateDir, ticketRecordId) {
16557
16999
  try {
16558
17000
  const p = priorPath(stateDir, ticketRecordId);
16559
- return existsSync32(p) ? JSON.parse(readFileSync28(p, "utf8")) : void 0;
17001
+ return existsSync33(p) ? JSON.parse(readFileSync29(p, "utf8")) : void 0;
16560
17002
  } catch {
16561
17003
  return void 0;
16562
17004
  }
@@ -16564,9 +17006,9 @@ function readPeriscopePrior(stateDir, ticketRecordId) {
16564
17006
  var DEFAULT_MAX_PERISCOPES2 = 3;
16565
17007
  var DEFAULT_PERISCOPE_IDLE_MINUTES2 = 120;
16566
17008
  function parsePeriscopeUpArgs(positional2, flags) {
16567
- const usage2 = "usage: crew periscope up [route] REPO [--topic TEXT | --issue NNN] [--isolate] [--host HOST] [--assignee CREW_ROW_ID] [--json]";
17009
+ const usage2 = "usage: crew periscope up [[route] REPO] [--topic TEXT | --issue NNN] [--isolate] [--host HOST] [--assignee CREW_ROW_ID] [--no-open] [--json]";
16568
17010
  const rest = positional2.slice(2);
16569
- if (rest.length === 0 || rest.length > 2) throw new PeriscopeError(usage2);
17011
+ if (rest.length > 2) throw new PeriscopeError(usage2);
16570
17012
  const topic = flags.topic?.trim();
16571
17013
  let issue;
16572
17014
  if (flags.issue !== void 0) {
@@ -16579,7 +17021,7 @@ ${usage2}`);
16579
17021
  }
16580
17022
  return {
16581
17023
  ...rest.length === 2 ? { route: rest[0] } : {},
16582
- repo: rest[rest.length - 1],
17024
+ ...rest.length > 0 ? { repo: rest[rest.length - 1] } : {},
16583
17025
  ...topic ? { topic } : {},
16584
17026
  ...issue ? { issue } : {},
16585
17027
  isolate: flags.isolate,
@@ -16618,6 +17060,25 @@ async function existingRefusal(e, t) {
16618
17060
  return void 0;
16619
17061
  }
16620
17062
  var tail = (s, n = 20) => s.trimEnd().split("\n").slice(-n).join("\n");
17063
+ function defaultTopic(repo, now) {
17064
+ return `${repo}, ${now.toISOString().slice(0, 10)}`;
17065
+ }
17066
+ function openerFor(platform2) {
17067
+ return platform2 === "darwin" ? "open" : platform2 === "linux" ? "xdg-open" : void 0;
17068
+ }
17069
+ function upOutput(res, terminal) {
17070
+ return terminal ? `${res.ticket}
17071
+ ${res.url}
17072
+ ${res.worktree}
17073
+ ` : `${res.ticket}
17074
+ ${res.worktree}
17075
+ ${res.url}
17076
+ `;
17077
+ }
17078
+ function shouldOpen(f) {
17079
+ if (f.noOpen || f.json) return false;
17080
+ return f.open || f.terminal;
17081
+ }
16621
17082
  function progressComment(url, handoff) {
16622
17083
  return [
16623
17084
  `Periscope is up: ${url}`,
@@ -16654,8 +17115,7 @@ async function runPeriscopeUp(d, args) {
16654
17115
  draft = prior;
16655
17116
  d.say("held", `holding ${prior.tag} (status ${prior.status} stays)`, { ticket: prior.tag, ticketRecordId: prior.id });
16656
17117
  } else {
16657
- const topic = args.topic ?? (await d.askTopic())?.trim();
16658
- if (!topic) throw new PeriscopeError("a topic is required: pass --topic TEXT");
17118
+ const topic = args.topic ?? (await d.askTopic())?.trim() ?? defaultTopic(args.repo, d.now());
16659
17119
  const unsafe = d.baseProblem();
16660
17120
  if (unsafe) throw new PeriscopeError(`DO NOT CUT A WORKTREE FROM THIS BASE \u2014 ${unsafe}`);
16661
17121
  draft = await d.fileDraft(`Periscope: ${topic}`);
@@ -16755,22 +17215,25 @@ ${tail(preview.output)}`);
16755
17215
  };
16756
17216
  }
16757
17217
  function parsePeriscopeDownArgs(positional2, flags) {
16758
- const usage2 = "usage: crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]";
17218
+ const usage2 = "usage: crew periscope down [[route] NNN] --keep|--discard [--allow-empty] [--json]";
16759
17219
  const rest = positional2.slice(2);
16760
- if (rest.length === 0 || rest.length > 2) throw new PeriscopeError(usage2);
17220
+ if (rest.length > 2) throw new PeriscopeError(usage2);
16761
17221
  if (flags.keep === flags.discard) throw new PeriscopeError(`pass exactly one of --keep or --discard
16762
17222
  ${usage2}`);
16763
- const m = /^(?:[A-Za-z]+-)?(\d+)$/.exec(rest[rest.length - 1]);
16764
- if (!m) throw new PeriscopeError(`"${rest[rest.length - 1]}" is not a ticket number
17223
+ const m = rest.length === 0 ? null : /^(?:[A-Za-z]+-)?(\d+)$/.exec(rest[rest.length - 1]);
17224
+ if (rest.length > 0 && !m) throw new PeriscopeError(`"${rest[rest.length - 1]}" is not a ticket number
16765
17225
  ${usage2}`);
16766
17226
  return {
16767
17227
  ...rest.length === 2 ? { route: rest[0] } : {},
16768
- number: m[1],
17228
+ ...m ? { number: m[1] } : {},
16769
17229
  mode: flags.keep ? "keep" : "discard",
16770
17230
  allowEmpty: flags.allowEmpty,
16771
17231
  json: flags.json
16772
17232
  };
16773
17233
  }
17234
+ function rowForCwd(rows, cwd) {
17235
+ return rows.find((r) => r.state === "live" && r.worktree !== "" && (cwd === r.worktree || cwd.startsWith(`${r.worktree}/`)));
17236
+ }
16774
17237
  var PROTOTYPE_HEADING = "## Prototype";
16775
17238
  function prototypeSection(files) {
16776
17239
  const added = files.reduce((n, f) => n + f.added, 0);
@@ -16926,14 +17389,14 @@ ${tail2(preview.output)}`);
16926
17389
  }
16927
17390
 
16928
17391
  // src/periscope-idle.ts
16929
- import { execFileSync as execFileSync6 } from "child_process";
16930
- import { statSync as statSync9 } from "fs";
16931
- 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";
16932
17395
  var MAX_SCANNED_FILES = 2e4;
16933
17396
  function newestWorktreeMtime(root) {
16934
17397
  let listing;
16935
17398
  try {
16936
- listing = execFileSync6("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
17399
+ listing = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
16937
17400
  cwd: root,
16938
17401
  encoding: "utf8",
16939
17402
  stdio: ["ignore", "pipe", "ignore"],
@@ -16948,7 +17411,7 @@ function newestWorktreeMtime(root) {
16948
17411
  if (!rel) continue;
16949
17412
  if (++scanned > MAX_SCANNED_FILES) break;
16950
17413
  try {
16951
- const m = statSync9(join36(root, rel)).mtimeMs;
17414
+ const m = statSync10(join37(root, rel)).mtimeMs;
16952
17415
  if (newest === null || m > newest) newest = m;
16953
17416
  } catch {
16954
17417
  }
@@ -17023,21 +17486,21 @@ async function runPeriscopeIdlePass(d) {
17023
17486
  import { dirname as dirOf2, resolve as resolvePath } from "path";
17024
17487
 
17025
17488
  // src/ssh-keys.ts
17026
- import { execFileSync as execFileSync7 } from "child_process";
17027
- import { existsSync as existsSync33, mkdirSync as mkdirSync20, readFileSync as readFileSync29, chmodSync } from "fs";
17028
- 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";
17029
17492
  var SshKeyError = class extends Error {
17030
17493
  };
17031
17494
  function shipSshKeyPathFor(stateDir) {
17032
- return join37(stateDir, "ssh", "id_ed25519");
17495
+ return join38(stateDir, "ssh", "id_ed25519");
17033
17496
  }
17034
17497
  function ensureShipSshKeypair(stateDir) {
17035
17498
  const privateKeyPath = shipSshKeyPathFor(stateDir);
17036
17499
  const publicKeyPath = `${privateKeyPath}.pub`;
17037
- if (!existsSync33(privateKeyPath)) {
17038
- mkdirSync20(dirname19(privateKeyPath), { recursive: true });
17500
+ if (!existsSync34(privateKeyPath)) {
17501
+ mkdirSync21(dirname20(privateKeyPath), { recursive: true });
17039
17502
  try {
17040
- execFileSync7("ssh-keygen", [
17503
+ execFileSync8("ssh-keygen", [
17041
17504
  "-t",
17042
17505
  "ed25519",
17043
17506
  "-N",
@@ -17053,17 +17516,17 @@ function ensureShipSshKeypair(stateDir) {
17053
17516
  const err = e;
17054
17517
  throw new SshKeyError(`ssh-keygen failed: ${err.stderr?.toString().trim() || err.message}`);
17055
17518
  }
17056
- chmodSync(privateKeyPath, 384);
17519
+ chmodSync2(privateKeyPath, 384);
17057
17520
  }
17058
- if (!existsSync33(publicKeyPath)) {
17521
+ if (!existsSync34(publicKeyPath)) {
17059
17522
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
17060
17523
  }
17061
- const publicKey = readFileSync29(publicKeyPath, "utf8").trim();
17524
+ const publicKey = readFileSync30(publicKeyPath, "utf8").trim();
17062
17525
  return { privateKeyPath, publicKeyPath, publicKey };
17063
17526
  }
17064
17527
  function sshKeygenAvailable() {
17065
17528
  try {
17066
- execFileSync7("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
17529
+ execFileSync8("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
17067
17530
  return true;
17068
17531
  } catch (e) {
17069
17532
  return e.code !== "ENOENT";
@@ -17071,11 +17534,11 @@ function sshKeygenAvailable() {
17071
17534
  }
17072
17535
 
17073
17536
  // src/passenger-containers.ts
17074
- import { execFileSync as execFileSync8 } from "child_process";
17537
+ import { execFileSync as execFileSync9 } from "child_process";
17075
17538
  import { createHash as createHash6 } from "crypto";
17076
17539
  var realExec = (cmd, args, opts) => {
17077
17540
  try {
17078
- 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 });
17079
17542
  } catch (e) {
17080
17543
  const err = e;
17081
17544
  const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
@@ -17275,9 +17738,9 @@ function syncAllPassengerCheckouts(cfg) {
17275
17738
 
17276
17739
  // src/tunnel.ts
17277
17740
  import { spawn as spawn5 } from "child_process";
17278
- 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";
17279
17742
  import { randomInt } from "crypto";
17280
- import { join as join38 } from "path";
17743
+ import { join as join39 } from "path";
17281
17744
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
17282
17745
  var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
17283
17746
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -17344,21 +17807,21 @@ function startTunnel(opts) {
17344
17807
  };
17345
17808
  }
17346
17809
  function tunnelStatePath(stateDir, workspaceId) {
17347
- return join38(stateDir, "passengers", `${workspaceId}.json`);
17810
+ return join39(stateDir, "passengers", `${workspaceId}.json`);
17348
17811
  }
17349
17812
  function readPersistedTunnel(stateDir, workspaceId) {
17350
17813
  const path = tunnelStatePath(stateDir, workspaceId);
17351
- if (!existsSync34(path)) return void 0;
17814
+ if (!existsSync35(path)) return void 0;
17352
17815
  try {
17353
- return JSON.parse(readFileSync30(path, "utf8"));
17816
+ return JSON.parse(readFileSync31(path, "utf8"));
17354
17817
  } catch {
17355
17818
  return void 0;
17356
17819
  }
17357
17820
  }
17358
17821
  function writePersistedTunnel(stateDir, state) {
17359
17822
  const path = tunnelStatePath(stateDir, state.workspaceId);
17360
- mkdirSync21(join38(stateDir, "passengers"), { recursive: true });
17361
- writeFileSync22(path, `${JSON.stringify(state, null, 2)}
17823
+ mkdirSync22(join39(stateDir, "passengers"), { recursive: true });
17824
+ writeFileSync23(path, `${JSON.stringify(state, null, 2)}
17362
17825
  `);
17363
17826
  }
17364
17827
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -17455,8 +17918,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
17455
17918
  }
17456
17919
  }
17457
17920
  function readAllPersistedWorkspaceIds(stateDir) {
17458
- const dir = join38(stateDir, "passengers");
17459
- if (!existsSync34(dir)) return [];
17921
+ const dir = join39(stateDir, "passengers");
17922
+ if (!existsSync35(dir)) return [];
17460
17923
  return readdirSync14(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
17461
17924
  }
17462
17925
 
@@ -17577,8 +18040,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
17577
18040
  import { createServer as createHttpServer } from "http";
17578
18041
  import { spawn as spawn6 } from "child_process";
17579
18042
  import { randomBytes } from "crypto";
17580
- import { existsSync as existsSync35, mkdirSync as mkdirSync22, readFileSync as readFileSync31, writeFileSync as writeFileSync23, unlinkSync as unlinkSync4, readdirSync as readdirSync15 } from "fs";
17581
- 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";
17582
18045
 
17583
18046
  // src/secret-compare.ts
17584
18047
  import { timingSafeEqual } from "crypto";
@@ -17667,21 +18130,21 @@ var defaultSpawn2 = (cmd, args, env) => {
17667
18130
  return child;
17668
18131
  };
17669
18132
  function syncDaemonStatePath(stateDir, workspaceId) {
17670
- return join39(stateDir, "passengers", `${workspaceId}-sync.json`);
18133
+ return join40(stateDir, "passengers", `${workspaceId}-sync.json`);
17671
18134
  }
17672
18135
  function readPersistedSyncDaemon(stateDir, workspaceId) {
17673
18136
  const path = syncDaemonStatePath(stateDir, workspaceId);
17674
- if (!existsSync35(path)) return void 0;
18137
+ if (!existsSync36(path)) return void 0;
17675
18138
  try {
17676
- return JSON.parse(readFileSync31(path, "utf8"));
18139
+ return JSON.parse(readFileSync32(path, "utf8"));
17677
18140
  } catch {
17678
18141
  return void 0;
17679
18142
  }
17680
18143
  }
17681
18144
  function writePersistedSyncDaemon(stateDir, state) {
17682
18145
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
17683
- mkdirSync22(join39(stateDir, "passengers"), { recursive: true });
17684
- writeFileSync23(path, `${JSON.stringify(state, null, 2)}
18146
+ mkdirSync23(join40(stateDir, "passengers"), { recursive: true });
18147
+ writeFileSync24(path, `${JSON.stringify(state, null, 2)}
17685
18148
  `);
17686
18149
  }
17687
18150
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -17701,8 +18164,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
17701
18164
  }
17702
18165
  }
17703
18166
  function readAllPersistedSyncWorkspaceIds(stateDir) {
17704
- const dir = join39(stateDir, "passengers");
17705
- if (!existsSync35(dir)) return [];
18167
+ const dir = join40(stateDir, "passengers");
18168
+ if (!existsSync36(dir)) return [];
17706
18169
  const suffix = "-sync.json";
17707
18170
  return readdirSync15(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
17708
18171
  }
@@ -17790,8 +18253,8 @@ function describeCause(err) {
17790
18253
  }
17791
18254
 
17792
18255
  // src/daemon.ts
17793
- import { existsSync as existsSync36, mkdirSync as mkdirSync23, readFileSync as readFileSync32, readdirSync as readdirSync16, rmSync as rmSync16, statSync as statSync10, writeFileSync as writeFileSync24 } from "fs";
17794
- 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";
17795
18258
  import { randomBytes as randomBytes2 } from "crypto";
17796
18259
  var DEFAULT_FLOOR_MS = 5e3;
17797
18260
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
@@ -17854,7 +18317,7 @@ async function runDaemonLoop(o) {
17854
18317
  function latestMtime(path) {
17855
18318
  let stat;
17856
18319
  try {
17857
- stat = statSync10(path);
18320
+ stat = statSync11(path);
17858
18321
  } catch {
17859
18322
  return 0;
17860
18323
  }
@@ -17862,7 +18325,7 @@ function latestMtime(path) {
17862
18325
  if (!stat.isDirectory()) return 0;
17863
18326
  let latest = stat.mtimeMs;
17864
18327
  for (const entry of readdirSync16(path, { withFileTypes: true })) {
17865
- latest = Math.max(latest, latestMtime(join40(path, entry.name)));
18328
+ latest = Math.max(latest, latestMtime(join41(path, entry.name)));
17866
18329
  }
17867
18330
  return latest;
17868
18331
  }
@@ -17871,23 +18334,23 @@ function computeTreeSignature(paths) {
17871
18334
  }
17872
18335
  function makeStaleChecker(paths, opts = {}) {
17873
18336
  const initial = computeTreeSignature(paths);
17874
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync36(opts.reloadSentinel);
18337
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync37(opts.reloadSentinel);
17875
18338
  }
17876
18339
  function reloadSentinelFor(stateDir) {
17877
- return join40(stateDir, "reload-requested");
18340
+ return join41(stateDir, "reload-requested");
17878
18341
  }
17879
18342
  function daemonStampFor(stateDir) {
17880
- return join40(stateDir, "daemon.started");
18343
+ return join41(stateDir, "daemon.started");
17881
18344
  }
17882
18345
  function noteDaemonStarted(stateDir, now = Date.now()) {
17883
- mkdirSync23(stateDir, { recursive: true });
18346
+ mkdirSync24(stateDir, { recursive: true });
17884
18347
  rmSync16(reloadSentinelFor(stateDir), { force: true });
17885
- writeFileSync24(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
18348
+ writeFileSync25(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
17886
18349
  `);
17887
18350
  }
17888
18351
  function readDaemonStarted(stateDir) {
17889
18352
  try {
17890
- const v = JSON.parse(readFileSync32(daemonStampFor(stateDir), "utf8")).startedAt;
18353
+ const v = JSON.parse(readFileSync33(daemonStampFor(stateDir), "utf8")).startedAt;
17891
18354
  return typeof v === "number" ? v : null;
17892
18355
  } catch {
17893
18356
  return null;
@@ -17896,7 +18359,7 @@ function readDaemonStarted(stateDir) {
17896
18359
  function configChangedAfter(files, startedAt) {
17897
18360
  for (const f of files) {
17898
18361
  try {
17899
- if (statSync10(f).mtimeMs > startedAt) return f;
18362
+ if (statSync11(f).mtimeMs > startedAt) return f;
17900
18363
  } catch {
17901
18364
  }
17902
18365
  }
@@ -18367,7 +18830,7 @@ async function buildEnvironment(route, ship, ticket) {
18367
18830
  }
18368
18831
 
18369
18832
  // src/cli.ts
18370
- 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)), "..");
18371
18834
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
18372
18835
  var CYCLE_SECONDS2 = 120;
18373
18836
  var STALE_CYCLES = 3;
@@ -18499,6 +18962,8 @@ function usage(problem, requested = false) {
18499
18962
  crew --version [--json] print the crew version on one line (also: crew version); --json adds home and compiled
18500
18963
  crew status [route] [--json] paused/running state; --json for a machine reader
18501
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)
18502
18967
  crew whoami [route] [--json] the signed-in identity, key role, operator Crew row and credential source per route
18503
18968
  crew reload make the running daemon re-read crew.yaml (--now: restart it immediately)
18504
18969
  crew doctor [route] [--fix] [--offline] [--json] preflight; --fix enables clean routes and offers crew install;
@@ -18509,12 +18974,13 @@ function usage(problem, requested = false) {
18509
18974
  crew reap [route] kill orphaned servers, drop worktrees for closed tickets
18510
18975
  crew drop [route] NNN remove a merged ticket's worktree and branch
18511
18976
  crew rotate-passenger-url [route] force a Host Passengers tunnel to reconnect with a fresh public URL
18512
- crew periscope up [route] REPO --topic TEXT|--issue NNN [--isolate] [--host H] [--assignee ID] [--dry-run] [--json]
18977
+ crew periscope up [[route] REPO] [--topic TEXT|--issue NNN] [--isolate] [--host H] [--assignee ID] [--no-open] [--open] [--dry-run] [--json]
18513
18978
  crew periscope up --resume NNN restart the preview an idle periscope's server was stopped for
18514
18979
  file a draft ticket, cut and prepare its worktree, start the repo's preview hook and record a Periscopes row (alias: start)
18515
- crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]
18980
+ crew periscope down [[route] NNN] --keep|--discard [--allow-empty] [--json]
18516
18981
  end a periscope: commit and push its branch (ticket back to new), or throw it away (alias: end)
18517
18982
  crew periscope list [route] [--json] the live periscopes on this ship, their idle minutes and whether each server is listening
18983
+ crew shell-init [--shell zsh|bash|fish] print the periscope shell function (install: eval "$(crew shell-init)")
18518
18984
  crew worktree [route] NNN cut (or refresh) this run's worktree for a ticket from the remote; prints its path
18519
18985
  crew unassign [route] NNN hand back a session's ticket \u2014 clears assignee, next cycle picks it up
18520
18986
  crew sync [route] fast-forward the checkout and its worktrees from the remote
@@ -18595,6 +19061,8 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
18595
19061
  "passenger-sync-daemon",
18596
19062
  "agents",
18597
19063
  "connect",
19064
+ "credential",
19065
+ "forge",
18598
19066
  "daemon",
18599
19067
  "deploy",
18600
19068
  "disconnect",
@@ -18620,6 +19088,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
18620
19088
  "run",
18621
19089
  "sample-ticket",
18622
19090
  "selftest",
19091
+ "shell-init",
18623
19092
  "ship",
18624
19093
  "skills",
18625
19094
  "status",
@@ -18631,6 +19100,16 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
18631
19100
  "worktree"
18632
19101
  ]);
18633
19102
  if (!KNOWN_COMMANDS.has(command)) usage(`unknown command "${command}"`);
19103
+ if (command === "shell-init") {
19104
+ const asked = value("shell");
19105
+ if (asked !== void 0 && !SHELLS.includes(asked)) {
19106
+ process.stderr.write(`crew shell-init: unknown shell "${asked}" (have: ${SHELLS.join(", ")})
19107
+ `);
19108
+ process.exit(2);
19109
+ }
19110
+ process.stdout.write(shellInitScript(shellFor(asked, process.env.SHELL)));
19111
+ process.exit(0);
19112
+ }
18634
19113
  function runReposCheck(dir, ship) {
18635
19114
  const report = checkRepoSpec(dir, { ship, strict: flag("strict") });
18636
19115
  if (flag("json")) {
@@ -18787,9 +19266,9 @@ if (command === "passenger-sync-daemon") {
18787
19266
  firstRun = { answers, configPath };
18788
19267
  cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
18789
19268
  } else {
18790
- 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");
18791
19270
  const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
18792
- if (configless && !existsSync37(findConfigFile(CREW_HOME))) {
19271
+ if (configless && !existsSync38(findConfigFile(CREW_HOME))) {
18793
19272
  cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
18794
19273
  } else {
18795
19274
  cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
@@ -18803,6 +19282,33 @@ if (command === "passenger-sync-daemon") {
18803
19282
  }
18804
19283
  throw e;
18805
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
+ }
18806
19312
  await hydrateApiKeys(cfg.routes);
18807
19313
  for (const w of cfg.warnings) process.stderr.write(`crew: warning: ${w}
18808
19314
  `);
@@ -19027,8 +19533,8 @@ if (command === "passenger-sync-daemon") {
19027
19533
  }
19028
19534
  if (host === "linux" && hasSystemd) {
19029
19535
  const label = labelFor(CREW_HOME, "run");
19030
- const staleTimer = join41(homedir10(), ".config", "systemd", "user", `${label}.timer`);
19031
- if (existsSync37(staleTimer)) {
19536
+ const staleTimer = join42(homedir10(), ".config", "systemd", "user", `${label}.timer`);
19537
+ if (existsSync38(staleTimer)) {
19032
19538
  process.stdout.write(
19033
19539
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
19034
19540
  `
@@ -19055,7 +19561,7 @@ if (command === "passenger-sync-daemon") {
19055
19561
  const tracker = new Tracker(c, cfg.ship);
19056
19562
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
19057
19563
  for (const r of await resolvedRepos(c)) {
19058
- 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`);
19059
19565
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
19060
19566
  readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
19061
19567
  }
@@ -19073,7 +19579,7 @@ if (command === "passenger-sync-daemon") {
19073
19579
  }
19074
19580
  async function doctorFix(o) {
19075
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))));
19076
- const unitExists = (p) => existsSync37(p);
19582
+ const unitExists = (p) => existsSync38(p);
19077
19583
  const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
19078
19584
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
19079
19585
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -19090,7 +19596,7 @@ if (command === "passenger-sync-daemon") {
19090
19596
  for (const a of plan.actions) {
19091
19597
  if (a.kind === "enable") {
19092
19598
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
19093
- writeFileSync25(cfg.configFile, planEnable(readFileSync33(cfg.configFile, "utf8"), a.route));
19599
+ writeFileSync26(cfg.configFile, planEnable(readFileSync34(cfg.configFile, "utf8"), a.route));
19094
19600
  enabledSound.add(a.route);
19095
19601
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
19096
19602
  } else {
@@ -20194,7 +20700,7 @@ ${cu.tail.trimEnd()}
20194
20700
  process.exit(1);
20195
20701
  }
20196
20702
  requireArmed("run the daemon loop");
20197
- 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")];
20198
20704
  const configPaths = daemonConfigPaths(cfg);
20199
20705
  watchPaths.push(...configPaths);
20200
20706
  noteDaemonStarted(cfg.ship.stateDir);
@@ -20423,10 +20929,10 @@ ${cu.tail.trimEnd()}
20423
20929
  `);
20424
20930
  if (!dryRun) {
20425
20931
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
20426
- const raw = existsSync37(resolvedPath) ? JSON.parse(readFileSync33(resolvedPath, "utf8")) : {};
20932
+ const raw = existsSync38(resolvedPath) ? JSON.parse(readFileSync34(resolvedPath, "utf8")) : {};
20427
20933
  raw.agentSkills = result.agentSkills;
20428
- mkdirSync24(dirOf2(resolvedPath), { recursive: true });
20429
- writeFileSync25(resolvedPath, `${JSON.stringify(raw, null, 2)}
20934
+ mkdirSync25(dirOf2(resolvedPath), { recursive: true });
20935
+ writeFileSync26(resolvedPath, `${JSON.stringify(raw, null, 2)}
20430
20936
  `);
20431
20937
  }
20432
20938
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -20449,7 +20955,7 @@ ${cu.tail.trimEnd()}
20449
20955
  throw e;
20450
20956
  }
20451
20957
  const oldName = cfg.ship.name;
20452
- writeFileSync25(cfg.configFile, setShipNameInYaml(readFileSync33(cfg.configFile, "utf8"), newName));
20958
+ writeFileSync26(cfg.configFile, setShipNameInYaml(readFileSync34(cfg.configFile, "utf8"), newName));
20453
20959
  cfg.ship.name = newName;
20454
20960
  let renameMachineId;
20455
20961
  try {
@@ -20496,14 +21002,14 @@ ${cu.tail.trimEnd()}
20496
21002
  process.exit(2);
20497
21003
  };
20498
21004
  if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
20499
- const configText = readFileSync33(cfg.configFile, "utf8");
21005
+ const configText = readFileSync34(cfg.configFile, "utf8");
20500
21006
  if (sub === "check") {
20501
21007
  try {
20502
21008
  const routeArg = value("route") ?? positional[2];
20503
21009
  const found = cfg.routes.find((r) => r.route === routeArg);
20504
21010
  if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
20505
21011
  const named2 = value("route") ? void 0 : positional[3];
20506
- 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] ?? ".");
20507
21013
  const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
20508
21014
  const o = target ? found.repoOverrides[target.name] : void 0;
20509
21015
  runReposCheck(dir, {
@@ -20526,9 +21032,9 @@ ${cu.tail.trimEnd()}
20526
21032
  if (sub === "hooks") {
20527
21033
  try {
20528
21034
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
20529
- const dir = repoDirOf(configText, dirname20(cfg.configFile), a.route, a.name);
20530
- if (!existsSync37(dir)) throw new ReposError(`${dir} does not exist on disk`);
20531
- 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) => {
20532
21038
  try {
20533
21039
  return readdirSync17(resolvePath(dir, r));
20534
21040
  } catch {
@@ -20536,7 +21042,7 @@ ${cu.tail.trimEnd()}
20536
21042
  }
20537
21043
  }, read: (r) => {
20538
21044
  try {
20539
- return readFileSync33(resolvePath(dir, r), "utf8");
21045
+ return readFileSync34(resolvePath(dir, r), "utf8");
20540
21046
  } catch {
20541
21047
  return void 0;
20542
21048
  }
@@ -20578,19 +21084,19 @@ ${cu.tail.trimEnd()}
20578
21084
  let cleared;
20579
21085
  if (clear2) {
20580
21086
  const plan = clearRouteHooks(configText, a.route, a.name);
20581
- if (plan.removed.length > 0) writeFileSync25(cfg.configFile, plan.text);
21087
+ if (plan.removed.length > 0) writeFileSync26(cfg.configFile, plan.text);
20582
21088
  cleared = { file: cfg.configFile, removed: plan.removed };
20583
21089
  }
20584
21090
  if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
20585
21091
  if (write === "route") {
20586
21092
  const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
20587
- writeFileSync25(cfg.configFile, plan.text);
21093
+ writeFileSync26(cfg.configFile, plan.text);
20588
21094
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
20589
21095
  } else if (write === "repo") {
20590
21096
  const have = existingRepoConfig(files);
20591
21097
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
20592
21098
  const file = resolvePath(dir, ".crew.yaml");
20593
- writeFileSync25(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
21099
+ writeFileSync26(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
20594
21100
  written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
20595
21101
  }
20596
21102
  if (flag("json")) {
@@ -20646,7 +21152,7 @@ ${cu.tail.trimEnd()}
20646
21152
  if (sub === "list" && !routeName) usage2();
20647
21153
  if (sub === "add") {
20648
21154
  try {
20649
- 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()))));
20650
21156
  routeName = a.route;
20651
21157
  addPath = a.path;
20652
21158
  addName = a.name;
@@ -20675,16 +21181,16 @@ ${cu.tail.trimEnd()}
20675
21181
  }
20676
21182
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
20677
21183
  let resolvedRemotes;
20678
- if (existsSync37(resolvedFile)) {
21184
+ if (existsSync38(resolvedFile)) {
20679
21185
  try {
20680
- resolvedRemotes = JSON.parse(readFileSync33(resolvedFile, "utf8"));
21186
+ resolvedRemotes = JSON.parse(readFileSync34(resolvedFile, "utf8"));
20681
21187
  } catch {
20682
21188
  }
20683
21189
  }
20684
21190
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
20685
21191
  const text = configText;
20686
- const base = dirname20(cfg.configFile);
20687
- const isCheckout = (d) => existsSync37(`${d}/.git`);
21192
+ const base = dirname21(cfg.configFile);
21193
+ const isCheckout = (d) => existsSync38(`${d}/.git`);
20688
21194
  try {
20689
21195
  if (sub === "list") {
20690
21196
  if (flag("json")) {
@@ -20694,11 +21200,11 @@ ${cu.tail.trimEnd()}
20694
21200
  base,
20695
21201
  resolved: resolvedRemotes,
20696
21202
  originOf,
20697
- exists: (d) => existsSync37(d),
21203
+ exists: (d) => existsSync38(d),
20698
21204
  isCheckout,
20699
21205
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
20700
21206
  try {
20701
- 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;
20702
21208
  } catch {
20703
21209
  return false;
20704
21210
  }
@@ -20715,7 +21221,7 @@ ${cu.tail.trimEnd()}
20715
21221
  for (const r of repos) {
20716
21222
  const origin2 = originOf(r.dir);
20717
21223
  const row = matchRepoName(origin2, resolvedRemotes);
20718
- 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)"}`;
20719
21225
  process.stdout.write(`${r.name} ${r.dir} ${note}
20720
21226
  `);
20721
21227
  }
@@ -20727,9 +21233,9 @@ ${cu.tail.trimEnd()}
20727
21233
  ` : `${human}
20728
21234
  `);
20729
21235
  const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
20730
- 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)) {
20731
21237
  throw new ReposError(
20732
- 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`
20733
21239
  );
20734
21240
  }
20735
21241
  if (dryRun) {
@@ -20747,14 +21253,14 @@ ${cu.tail.trimEnd()}
20747
21253
  throw new ReposError(`${e.message}${authHint}
20748
21254
  crew.yaml was not changed`);
20749
21255
  }
20750
- writeFileSync25(cfg.configFile, plan2.text);
21256
+ writeFileSync26(cfg.configFile, plan2.text);
20751
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:
20752
21258
  `);
20753
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}`);
20754
21260
  break;
20755
21261
  }
20756
21262
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
20757
- const top = existsSync37(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
21263
+ const top = existsSync38(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
20758
21264
  if (!top) throw new ReposError(`${target} is not a git checkout`);
20759
21265
  const origin = originOf(top);
20760
21266
  const plan = planRepoAdd({
@@ -20806,15 +21312,15 @@ ${cu.tail.trimEnd()}
20806
21312
  const created = await rowTracker.createRepoRow(rowPayload);
20807
21313
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
20808
21314
  `);
20809
- if (existsSync37(resolvedFile)) {
20810
- const raw = JSON.parse(readFileSync33(resolvedFile, "utf8"));
21315
+ if (existsSync38(resolvedFile)) {
21316
+ const raw = JSON.parse(readFileSync34(resolvedFile, "utf8"));
20811
21317
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
20812
21318
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
20813
- writeFileSync25(resolvedFile, `${JSON.stringify(raw, null, 2)}
21319
+ writeFileSync26(resolvedFile, `${JSON.stringify(raw, null, 2)}
20814
21320
  `);
20815
21321
  }
20816
21322
  }
20817
- writeFileSync25(cfg.configFile, plan.text);
21323
+ writeFileSync26(cfg.configFile, plan.text);
20818
21324
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
20819
21325
  `);
20820
21326
  } catch (e) {
@@ -21181,7 +21687,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21181
21687
  process.exit(2);
21182
21688
  }
21183
21689
  if (found.shipNameAdopted && !dryRun) {
21184
- writeFileSync25(cfg.configFile, setShipNameInYaml(readFileSync33(cfg.configFile, "utf8"), found.shipNameAdopted));
21690
+ writeFileSync26(cfg.configFile, setShipNameInYaml(readFileSync34(cfg.configFile, "utf8"), found.shipNameAdopted));
21185
21691
  cfg.ship.name = found.shipNameAdopted;
21186
21692
  }
21187
21693
  if (found.shipRow) {
@@ -21224,10 +21730,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21224
21730
  }
21225
21731
  }
21226
21732
  let previous;
21227
- const hadPreviousFile = existsSync37(resolvedPath);
21733
+ const hadPreviousFile = existsSync38(resolvedPath);
21228
21734
  if (hadPreviousFile) {
21229
21735
  try {
21230
- previous = JSON.parse(readFileSync33(resolvedPath, "utf8"));
21736
+ previous = JSON.parse(readFileSync34(resolvedPath, "utf8"));
21231
21737
  } catch {
21232
21738
  }
21233
21739
  }
@@ -21356,10 +21862,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21356
21862
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
21357
21863
  `);
21358
21864
  } else {
21359
- mkdirSync24(dirname20(resolvedPath), { recursive: true });
21865
+ mkdirSync25(dirname21(resolvedPath), { recursive: true });
21360
21866
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
21361
21867
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
21362
- writeFileSync25(tmpPath, `${JSON.stringify(resolved, null, 2)}
21868
+ writeFileSync26(tmpPath, `${JSON.stringify(resolved, null, 2)}
21363
21869
  `);
21364
21870
  renameSync3(tmpPath, resolvedPath);
21365
21871
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -21405,8 +21911,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21405
21911
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
21406
21912
  `);
21407
21913
  } else {
21408
- mkdirSync24(dirname20(apiKeyFile), { recursive: true });
21409
- writeFileSync25(apiKeyFile, `${apiKeyVar}=${apiKey}
21914
+ mkdirSync25(dirname21(apiKeyFile), { recursive: true });
21915
+ writeFileSync26(apiKeyFile, `${apiKeyVar}=${apiKey}
21410
21916
  `, { mode: 384 });
21411
21917
  process.stderr.write(`API key written to ${apiKeyFile}
21412
21918
  `);
@@ -21956,8 +22462,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21956
22462
  case "periscope": {
21957
22463
  const subP = positional[1];
21958
22464
  if (subP !== "up" && subP !== "start" && subP !== "down" && subP !== "end" && subP !== "list") {
21959
- process.stderr.write("crew periscope up [route] REPO --topic TEXT|--issue NNN [--isolate] [--host H] [--assignee ID] [--dry-run] [--json]\n");
21960
- process.stderr.write("crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]\n");
22465
+ process.stderr.write("crew periscope up [[route] REPO] [--topic TEXT|--issue NNN] [--isolate] [--host H] [--assignee ID] [--no-open] [--open] [--dry-run] [--json]\n");
22466
+ process.stderr.write("crew periscope down [[route] NNN] --keep|--discard [--allow-empty] [--json]\n");
21961
22467
  process.stderr.write("crew periscope list [route] [--json]\n");
21962
22468
  process.exit(2);
21963
22469
  }
@@ -22002,11 +22508,27 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22002
22508
  `);
22003
22509
  break;
22004
22510
  }
22005
- const a = downArgs;
22511
+ const real = (p) => {
22512
+ try {
22513
+ return realpathSync3(p);
22514
+ } catch {
22515
+ return p;
22516
+ }
22517
+ };
22518
+ const gathered = await Promise.all(routesP.map(async (rt) => ({ rt, rows: await gather(rt) })));
22519
+ const number = downArgs.number ?? (() => {
22520
+ const here = real(process.cwd());
22521
+ for (const g of gathered) {
22522
+ const hit = rowForCwd(g.rows.map((r) => ({ ...r, worktree: real(r.worktree) })), here);
22523
+ if (hit) return hit.number;
22524
+ }
22525
+ throw new PeriscopeError("not inside a periscope worktree \u2014 name the ticket: crew periscope down NNN --keep|--discard");
22526
+ })();
22527
+ const a = { ...downArgs, number };
22006
22528
  const hits = [];
22007
- for (const rt of routesP) {
22008
- const rows = (await gather(rt)).filter((r) => r.number === a.number);
22009
- if (rows.length) hits.push({ rt, rows });
22529
+ for (const g of gathered) {
22530
+ const rows = g.rows.filter((r) => r.number === a.number);
22531
+ if (rows.length) hits.push({ rt: g.rt, rows });
22010
22532
  }
22011
22533
  if (hits.length !== 1) {
22012
22534
  throw new PeriscopeError(hits.length === 0 ? `no periscope for ticket ${a.number} on this ship` : `ticket ${a.number} has periscopes on more than one route (${hits.map((h) => h.rt.route).join(", ")}) \u2014 name the route`);
@@ -22085,6 +22607,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22085
22607
  say("dry-run", `would end the periscope for ${rowsD[0].ticket} (${a.mode})`);
22086
22608
  break;
22087
22609
  }
22610
+ process.chdir(repoD.dir);
22088
22611
  const res = await runPeriscopeDown(depsD, a);
22089
22612
  if (jsonP) process.stdout.write(`${JSON.stringify({ event: "done", ...res })}
22090
22613
  `);
@@ -22123,7 +22646,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22123
22646
  const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
22124
22647
  const resumed = await runPeriscopeResume({
22125
22648
  find: async () => hit && { row: hit.row, tag: hit.tag },
22126
- worktreeExists: (p) => existsSync37(p),
22649
+ worktreeExists: (p) => existsSync38(p),
22127
22650
  caps: periscopeCaps(cfg.ship),
22128
22651
  runHook: async (name, cwd, env) => {
22129
22652
  const script = hit?.repo.config.hooks[name];
@@ -22142,7 +22665,19 @@ ${resumed.url}
22142
22665
  `);
22143
22666
  break;
22144
22667
  }
22145
- const a = parsePeriscopeUpArgs(positional, { topic: value("topic"), issue: value("issue"), host: value("host"), assignee: value("assignee"), isolate: flag("isolate"), json: jsonP });
22668
+ const parsedUp = parsePeriscopeUpArgs(positional, { topic: value("topic"), issue: value("issue"), host: value("host"), assignee: value("assignee"), isolate: flag("isolate"), json: jsonP });
22669
+ let a;
22670
+ if (parsedUp.repo !== void 0) a = { ...parsedUp, repo: parsedUp.repo };
22671
+ else {
22672
+ const here = locateDir(cfg, process.cwd(), (d) => gitOk(d, ["rev-parse", "--path-format=absolute", "--git-common-dir"]) ?? void 0);
22673
+ if (!here) {
22674
+ const where = cfg.routes.flatMap((r) => reposOf(r).map((x) => ` ${x.name}: ${x.dir}`));
22675
+ throw new PeriscopeError(`run it from inside a configured checkout, or name the repository (crew periscope up REPO). Configured checkouts:
22676
+ ${where.join("\n")}`);
22677
+ }
22678
+ a = { ...parsedUp, repo: here.repo.name, route: parsedUp.route ?? here.route.route };
22679
+ }
22680
+ const terminal = !!process.stdin.isTTY && !!process.stdout.isTTY && !jsonP;
22146
22681
  const candidatesP = a.route ? [findRoute(cfg, a.route)] : cfg.routes.filter((r) => r.enabled);
22147
22682
  const servedP = [];
22148
22683
  for (const cand of candidatesP) {
@@ -22174,10 +22709,11 @@ ${resumed.url}
22174
22709
  return h.unsafe ? h.detail : void 0;
22175
22710
  },
22176
22711
  askTopic: async () => {
22177
- if (!process.stdin.isTTY || jsonP) return void 0;
22712
+ if (!terminal) return void 0;
22178
22713
  const rl = createInterface({ input: process.stdin, output: process.stderr });
22179
22714
  try {
22180
- return await rl.question("Periscope topic: ");
22715
+ const answer = (await rl.question(`Periscope topic for ${a.repo} (blank for "${defaultTopic(a.repo, /* @__PURE__ */ new Date())}"): `)).trim();
22716
+ return answer || void 0;
22181
22717
  } finally {
22182
22718
  rl.close();
22183
22719
  }
@@ -22204,10 +22740,10 @@ ${resumed.url}
22204
22740
  },
22205
22741
  prepare: async (path) => {
22206
22742
  for (const f of repoP.config.worktrees.copy) {
22207
- const src = join41(repoP.dir, f);
22208
- if (!existsSync37(src)) continue;
22209
- mkdirSync24(dirOf2(join41(path, f)), { recursive: true });
22210
- 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));
22211
22747
  }
22212
22748
  const setup = repoP.config.hooks.setup;
22213
22749
  return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
@@ -22283,16 +22819,21 @@ ${resumed.url}
22283
22819
  say("dry-run", `would hold ${t.tag} (status ${t.status} stays) for ${pairId}, cut or reuse its worktree for ${a.repo}, run setup${a.isolate ? ", isolate" : ""}, ports, handoff and preview on ${deps.host}, and record a Periscopes row`);
22284
22820
  break;
22285
22821
  }
22286
- say("dry-run", `would file "Periscope: ${a.topic ?? "<topic>"}" as ${cP.statuses.draft} for ${pairId}, cut a worktree for ${a.repo}, run setup${a.isolate ? ", isolate" : ""}, ports, handoff and preview on ${deps.host}, and record a Periscopes row (expires after ${caps.idleMinutes} minutes idle)`);
22822
+ say("dry-run", `would file "Periscope: ${a.topic ?? defaultTopic(a.repo, /* @__PURE__ */ new Date())}" as ${cP.statuses.draft} for ${pairId}, cut a worktree for ${a.repo}, run setup${a.isolate ? ", isolate" : ""}, ports, handoff and preview on ${deps.host}, and record a Periscopes row (expires after ${caps.idleMinutes} minutes idle)`);
22287
22823
  break;
22288
22824
  }
22289
22825
  const res = await runPeriscopeUp(deps, a);
22290
22826
  if (jsonP) process.stdout.write(`${JSON.stringify({ event: "done", ...res })}
22291
22827
  `);
22292
- else process.stdout.write(`${res.ticket}
22293
- ${res.worktree}
22294
- ${res.url}
22828
+ else {
22829
+ process.stdout.write(upOutput(res, terminal));
22830
+ const opener = openerFor(process.platform);
22831
+ if (opener && shouldOpen({ open: flag("open"), noOpen: flag("no-open"), json: jsonP, terminal })) {
22832
+ const r = spawnSync7(opener, [res.url], { stdio: "ignore" });
22833
+ if (r.error || r.status !== 0) process.stderr.write(`crew periscope: could not open ${res.url} with ${opener}
22295
22834
  `);
22835
+ }
22836
+ }
22296
22837
  } catch (e) {
22297
22838
  if (e instanceof PeriscopeError || e instanceof ConfigError) {
22298
22839
  process.stderr.write(`crew periscope: ${e.message}
@@ -22477,7 +23018,7 @@ ${res.url}
22477
23018
  };
22478
23019
  const droppedRoutes = [];
22479
23020
  try {
22480
- const rawCfg = parseYaml2(readFileSync33(cfg.configFile, "utf8"));
23021
+ const rawCfg = parseYaml2(readFileSync34(cfg.configFile, "utf8"));
22481
23022
  for (const r of rawCfg?.routes ?? []) {
22482
23023
  if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
22483
23024
  try {
@@ -22519,8 +23060,8 @@ ${res.url}
22519
23060
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
22520
23061
  break;
22521
23062
  }
22522
- mkdirSync24(cfg.ship.stateDir, { recursive: true });
22523
- writeFileSync25(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
23063
+ mkdirSync25(cfg.ship.stateDir, { recursive: true });
23064
+ writeFileSync26(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
22524
23065
  `);
22525
23066
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
22526
23067
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -22571,7 +23112,7 @@ ${res.url}
22571
23112
  }
22572
23113
  case "log": {
22573
23114
  try {
22574
- const text = readFileSync33(cfg.ship.logFile, "utf8").split("\n");
23115
+ const text = readFileSync34(cfg.ship.logFile, "utf8").split("\n");
22575
23116
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
22576
23117
  `);
22577
23118
  } catch {
@@ -22594,8 +23135,8 @@ ${res.url}
22594
23135
  };
22595
23136
  if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
22596
23137
  const tracker = new Tracker(route, cfg.ship);
22597
- const configText = readFileSync33(cfg.configFile, "utf8");
22598
- const cfgBase = dirname20(cfg.configFile);
23138
+ const configText = readFileSync34(cfg.configFile, "utf8");
23139
+ const cfgBase = dirname21(cfg.configFile);
22599
23140
  const configured = configuredRepos(configText, route.route, cfgBase);
22600
23141
  const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
22601
23142
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
@@ -22623,7 +23164,7 @@ ${res.url}
22623
23164
  }
22624
23165
  if (configured.some((r) => r.dir === dir)) {
22625
23166
  try {
22626
- writeFileSync25(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
23167
+ writeFileSync26(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
22627
23168
  } catch (e) {
22628
23169
  if (e instanceof ReposError) fail(e.message);
22629
23170
  throw e;
@@ -22631,29 +23172,29 @@ ${res.url}
22631
23172
  }
22632
23173
  if (row) {
22633
23174
  await tracker.deleteRepoRow(row.id);
22634
- if (existsSync37(resolvedFile)) {
22635
- const raw = JSON.parse(readFileSync33(resolvedFile, "utf8"));
23175
+ if (existsSync38(resolvedFile)) {
23176
+ const raw = JSON.parse(readFileSync34(resolvedFile, "utf8"));
22636
23177
  if (raw.repoNames) delete raw.repoNames[row.id];
22637
23178
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
22638
- writeFileSync25(resolvedFile, `${JSON.stringify(raw, null, 2)}
23179
+ writeFileSync26(resolvedFile, `${JSON.stringify(raw, null, 2)}
22639
23180
  `);
22640
23181
  }
22641
23182
  }
22642
- 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 });
22643
23184
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
22644
23185
  break;
22645
23186
  }
22646
23187
  const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
22647
- const fresh = !existsSync37(join41(dir, ".git"));
22648
- 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`);
22649
23190
  if (dryRun) {
22650
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`);
22651
23192
  break;
22652
23193
  }
22653
23194
  if (fresh) {
22654
- mkdirSync24(dir, { recursive: true });
22655
- writeFileSync25(join41(dir, "README.md"), SAMPLE_README);
22656
- 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);
22657
23198
  const g = (...a) => {
22658
23199
  const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
22659
23200
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -22671,17 +23212,17 @@ ${res.url}
22671
23212
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
22672
23213
  });
22673
23214
  repoId = (await tracker.createRepoRow(payload)).id;
22674
- if (existsSync37(resolvedFile)) {
22675
- const raw = JSON.parse(readFileSync33(resolvedFile, "utf8"));
23215
+ if (existsSync38(resolvedFile)) {
23216
+ const raw = JSON.parse(readFileSync34(resolvedFile, "utf8"));
22676
23217
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
22677
23218
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
22678
- writeFileSync25(resolvedFile, `${JSON.stringify(raw, null, 2)}
23219
+ writeFileSync26(resolvedFile, `${JSON.stringify(raw, null, 2)}
22679
23220
  `);
22680
23221
  }
22681
23222
  }
22682
23223
  if (!configured.some((r) => r.dir === dir)) {
22683
23224
  try {
22684
- 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);
22685
23226
  } catch (e) {
22686
23227
  if (e instanceof ReposError) fail(e.message);
22687
23228
  throw e;
@@ -23016,11 +23557,17 @@ watermark: ${state.watermark()}
23016
23557
  }
23017
23558
  for (const r of repos) {
23018
23559
  const remote = r.config.branch.remote;
23019
- if (existsSync37(r.dir)) {
23560
+ if (existsSync38(r.dir)) {
23020
23561
  pushAuth.push({
23021
23562
  route: c.route,
23022
23563
  repo: r.name,
23023
- 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
+ })
23024
23571
  });
23025
23572
  }
23026
23573
  if (isPrBased(r.config.hooks)) prBased.add(r.name);
@@ -23093,7 +23640,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
23093
23640
  }
23094
23641
  try {
23095
23642
  const evFile = eventFileFor(cfg.ship.stateDir);
23096
- const evSize = existsSync37(evFile) ? statSync11(evFile).size : 0;
23643
+ const evSize = existsSync38(evFile) ? statSync12(evFile).size : 0;
23097
23644
  process.stdout.write(
23098
23645
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
23099
23646
  ` : "events log: ok\n"
@@ -23104,7 +23651,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
23104
23651
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
23105
23652
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
23106
23653
  try {
23107
- return readFileSync33(p, "utf8");
23654
+ return readFileSync34(p, "utf8");
23108
23655
  } catch {
23109
23656
  return null;
23110
23657
  }
@@ -23120,7 +23667,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
23120
23667
  const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
23121
23668
  if (changed2) {
23122
23669
  process.stdout.write(
23123
- `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\`
23124
23671
  `
23125
23672
  );
23126
23673
  }