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