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