@tablation/crew 0.92.0 → 0.94.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 +1067 -621
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -223,8 +223,8 @@ function shellInitScript(shell) {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
// src/config.ts
|
|
226
|
-
import { readFileSync as
|
|
227
|
-
import { resolve, dirname as dirname4, isAbsolute, join as
|
|
226
|
+
import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
|
|
227
|
+
import { resolve, dirname as dirname4, isAbsolute, join as join12, basename as basename3 } from "path";
|
|
228
228
|
import { homedir as homedir6, tmpdir } from "os";
|
|
229
229
|
import { parse as parse2 } from "yaml";
|
|
230
230
|
|
|
@@ -363,11 +363,11 @@ var Emitter = class _Emitter {
|
|
|
363
363
|
const result = await fn();
|
|
364
364
|
this.emit(message, { data: { ms: this.now().getTime() - started, ok: true } });
|
|
365
365
|
return result;
|
|
366
|
-
} catch (
|
|
367
|
-
this.error(`${message} \u2014 ${
|
|
366
|
+
} catch (err3) {
|
|
367
|
+
this.error(`${message} \u2014 ${err3.message}`, {
|
|
368
368
|
data: { ms: this.now().getTime() - started, ok: false }
|
|
369
369
|
});
|
|
370
|
-
throw
|
|
370
|
+
throw err3;
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
373
|
};
|
|
@@ -1818,7 +1818,144 @@ function forgeOfRemote(forges, remoteUrl, hasToken) {
|
|
|
1818
1818
|
}
|
|
1819
1819
|
|
|
1820
1820
|
// src/config.ts
|
|
1821
|
-
import {
|
|
1821
|
+
import { hostFromUrl } from "@tablation/client";
|
|
1822
|
+
|
|
1823
|
+
// src/secret-store.ts
|
|
1824
|
+
import { createHash as createHash2 } from "crypto";
|
|
1825
|
+
import { accessSync, constants, existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync9, renameSync as renameSync2, rmSync as rmSync6, statSync as statSync3, writeFileSync as writeFileSync7, chmodSync as chmodSync2 } from "fs";
|
|
1826
|
+
import { join as join10 } from "path";
|
|
1827
|
+
import { getSessionStore as getOsSessionStore } from "@tablation/client";
|
|
1828
|
+
var SECRET_STORE_MODES = ["auto", "os", "file"];
|
|
1829
|
+
function isSecretStoreMode(v) {
|
|
1830
|
+
return typeof v === "string" && SECRET_STORE_MODES.includes(v);
|
|
1831
|
+
}
|
|
1832
|
+
var configured;
|
|
1833
|
+
function configureSecretStore(c) {
|
|
1834
|
+
configured = c;
|
|
1835
|
+
}
|
|
1836
|
+
function keysDir(stateDir) {
|
|
1837
|
+
return join10(stateDir, "keys");
|
|
1838
|
+
}
|
|
1839
|
+
function entryName(host, slug) {
|
|
1840
|
+
return `${createHash2("sha256").update(`${host}\0${slug}`).digest("hex")}.json`;
|
|
1841
|
+
}
|
|
1842
|
+
var FileSessionStore = class {
|
|
1843
|
+
dir;
|
|
1844
|
+
constructor(dir) {
|
|
1845
|
+
this.dir = dir;
|
|
1846
|
+
}
|
|
1847
|
+
ensureDir() {
|
|
1848
|
+
mkdirSync6(this.dir, { recursive: true, mode: 448 });
|
|
1849
|
+
if ((statSync3(this.dir).mode & 63) !== 0) chmodSync2(this.dir, 448);
|
|
1850
|
+
}
|
|
1851
|
+
async get(host, slug) {
|
|
1852
|
+
const file = join10(this.dir, entryName(host, slug));
|
|
1853
|
+
if (!existsSync8(this.dir)) return void 0;
|
|
1854
|
+
accessSync(this.dir, constants.R_OK | constants.X_OK);
|
|
1855
|
+
if (!existsSync8(file)) return void 0;
|
|
1856
|
+
return JSON.parse(readFileSync9(file, "utf8"));
|
|
1857
|
+
}
|
|
1858
|
+
async set(host, slug, session) {
|
|
1859
|
+
this.ensureDir();
|
|
1860
|
+
const file = join10(this.dir, entryName(host, slug));
|
|
1861
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
1862
|
+
writeFileSync7(tmp, JSON.stringify(session), { mode: 384 });
|
|
1863
|
+
renameSync2(tmp, file);
|
|
1864
|
+
}
|
|
1865
|
+
async clear(host, slug) {
|
|
1866
|
+
if (!existsSync8(this.dir)) return;
|
|
1867
|
+
rmSync6(join10(this.dir, entryName(host, slug)), { force: true });
|
|
1868
|
+
}
|
|
1869
|
+
/** Whether a key could be written here right now, without leaving anything behind. */
|
|
1870
|
+
writable() {
|
|
1871
|
+
try {
|
|
1872
|
+
this.ensureDir();
|
|
1873
|
+
accessSync(this.dir, constants.R_OK | constants.W_OK | constants.X_OK);
|
|
1874
|
+
const probe = join10(this.dir, `.probe.${process.pid}`);
|
|
1875
|
+
writeFileSync7(probe, "", { mode: 384 });
|
|
1876
|
+
rmSync6(probe, { force: true });
|
|
1877
|
+
return { ok: true };
|
|
1878
|
+
} catch (e) {
|
|
1879
|
+
return { ok: false, reason: e.code ?? e.message };
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
function crewifyStoreError(e) {
|
|
1884
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1885
|
+
if (!/TABLATION_API_KEY/.test(msg)) return e instanceof Error ? e : new Error(msg);
|
|
1886
|
+
const out = new Error(
|
|
1887
|
+
msg.replace(
|
|
1888
|
+
/Set TABLATION_API_KEY instead of running `login`\./,
|
|
1889
|
+
"Set CREW_API_KEY for the ship key, or set `ship.secretStore: file` in crew.yaml to keep keys under the state directory."
|
|
1890
|
+
),
|
|
1891
|
+
{ cause: e }
|
|
1892
|
+
);
|
|
1893
|
+
return out;
|
|
1894
|
+
}
|
|
1895
|
+
var CrewHintedStore = class {
|
|
1896
|
+
inner;
|
|
1897
|
+
constructor(inner) {
|
|
1898
|
+
this.inner = inner;
|
|
1899
|
+
}
|
|
1900
|
+
async get(host, slug) {
|
|
1901
|
+
try {
|
|
1902
|
+
return await this.inner.get(host, slug);
|
|
1903
|
+
} catch (e) {
|
|
1904
|
+
throw crewifyStoreError(e);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
async set(host, slug, s) {
|
|
1908
|
+
try {
|
|
1909
|
+
await this.inner.set(host, slug, s);
|
|
1910
|
+
} catch (e) {
|
|
1911
|
+
throw crewifyStoreError(e);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
async clear(host, slug) {
|
|
1915
|
+
try {
|
|
1916
|
+
await this.inner.clear(host, slug);
|
|
1917
|
+
} catch (e) {
|
|
1918
|
+
throw crewifyStoreError(e);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
};
|
|
1922
|
+
function osStoreReachable(env = process.env, platform2 = process.platform, hasBinary = onPath) {
|
|
1923
|
+
if (platform2 !== "linux") return true;
|
|
1924
|
+
if (!hasBinary("secret-tool")) return false;
|
|
1925
|
+
if (env.DBUS_SESSION_BUS_ADDRESS) return true;
|
|
1926
|
+
return !!env.XDG_RUNTIME_DIR && existsSync8(join10(env.XDG_RUNTIME_DIR, "bus"));
|
|
1927
|
+
}
|
|
1928
|
+
function onPath(name) {
|
|
1929
|
+
for (const dir of (process.env.PATH ?? "").split(":")) {
|
|
1930
|
+
if (dir && existsSync8(join10(dir, name))) return true;
|
|
1931
|
+
}
|
|
1932
|
+
return false;
|
|
1933
|
+
}
|
|
1934
|
+
function resolveStoreKind(mode, reachable = osStoreReachable()) {
|
|
1935
|
+
if (mode === "file") return "file";
|
|
1936
|
+
if (mode === "os") return "os";
|
|
1937
|
+
return reachable ? "os" : "file";
|
|
1938
|
+
}
|
|
1939
|
+
function describeSecretStore(c = configured) {
|
|
1940
|
+
const mode = c?.mode ?? "auto";
|
|
1941
|
+
const kind = c ? resolveStoreKind(mode) : "os";
|
|
1942
|
+
return kind === "file" && c ? { kind, mode, dir: keysDir(c.stateDir) } : { kind: "os", mode };
|
|
1943
|
+
}
|
|
1944
|
+
function getSessionStore(c = configured) {
|
|
1945
|
+
const r = describeSecretStore(c);
|
|
1946
|
+
if (r.kind === "file" && r.dir) return new FileSessionStore(r.dir);
|
|
1947
|
+
return new CrewHintedStore(getOsSessionStore());
|
|
1948
|
+
}
|
|
1949
|
+
function describeKeyStore(ship) {
|
|
1950
|
+
const c = { mode: ship.secretStore ?? "auto", stateDir: ship.stateDir };
|
|
1951
|
+
const r = describeSecretStore(c);
|
|
1952
|
+
const why = c.mode === "auto" ? " (auto: no desktop secret service reachable)" : ` (ship.secretStore: ${c.mode})`;
|
|
1953
|
+
if (r.kind === "file" && r.dir) {
|
|
1954
|
+
const w = new FileSessionStore(r.dir).writable();
|
|
1955
|
+
return `file ${r.dir}${why} \u2014 ${w.ok ? "writable" : `NOT writable (${w.reason})`}`;
|
|
1956
|
+
}
|
|
1957
|
+
return `OS secret store${c.mode === "auto" ? "" : why}`;
|
|
1958
|
+
}
|
|
1822
1959
|
|
|
1823
1960
|
// src/platform.ts
|
|
1824
1961
|
var SHIP_PLATFORMS = ["macos", "linux", "windows"];
|
|
@@ -1857,8 +1994,8 @@ function explain(ship, required) {
|
|
|
1857
1994
|
}
|
|
1858
1995
|
|
|
1859
1996
|
// src/repo-config.ts
|
|
1860
|
-
import { readFileSync as
|
|
1861
|
-
import { join as
|
|
1997
|
+
import { readFileSync as readFileSync10, existsSync as existsSync9 } from "fs";
|
|
1998
|
+
import { join as join11, basename as basename2 } from "path";
|
|
1862
1999
|
import { parse } from "yaml";
|
|
1863
2000
|
var SPEC_VERSION = 1;
|
|
1864
2001
|
var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
|
|
@@ -1942,8 +2079,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
|
|
|
1942
2079
|
var PROVIDERS = ["github", "buildkite", "other", "none"];
|
|
1943
2080
|
function findRepoConfig(dir) {
|
|
1944
2081
|
for (const name of REPO_CONFIG_FILENAMES) {
|
|
1945
|
-
const p =
|
|
1946
|
-
if (
|
|
2082
|
+
const p = join11(dir, name);
|
|
2083
|
+
if (existsSync9(p)) return p;
|
|
1947
2084
|
}
|
|
1948
2085
|
return null;
|
|
1949
2086
|
}
|
|
@@ -2181,7 +2318,7 @@ function parseRepoConfig(text, file) {
|
|
|
2181
2318
|
function loadRepoConfig(dir) {
|
|
2182
2319
|
const file = findRepoConfig(dir);
|
|
2183
2320
|
if (!file) return null;
|
|
2184
|
-
return parseRepoConfig(
|
|
2321
|
+
return parseRepoConfig(readFileSync10(file, "utf8"), file);
|
|
2185
2322
|
}
|
|
2186
2323
|
function hookLabel(cfg, hook2) {
|
|
2187
2324
|
return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
|
|
@@ -2503,7 +2640,8 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
|
|
|
2503
2640
|
"forges",
|
|
2504
2641
|
"previewHost",
|
|
2505
2642
|
"maxPeriscopes",
|
|
2506
|
-
"periscopeIdleMinutes"
|
|
2643
|
+
"periscopeIdleMinutes",
|
|
2644
|
+
"secretStore"
|
|
2507
2645
|
]);
|
|
2508
2646
|
var ROUTE_KEYS = /* @__PURE__ */ new Set([
|
|
2509
2647
|
"route",
|
|
@@ -2651,11 +2789,11 @@ function expand(p, base) {
|
|
|
2651
2789
|
return isAbsolute(out) ? out : resolve(base, out);
|
|
2652
2790
|
}
|
|
2653
2791
|
function resolvePromptsDir(promptSet, crewHome, base) {
|
|
2654
|
-
if (!promptSet) return
|
|
2792
|
+
if (!promptSet) return join12(crewHome, "prompts", "default");
|
|
2655
2793
|
if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
|
|
2656
2794
|
return expand(promptSet, base);
|
|
2657
2795
|
}
|
|
2658
|
-
return
|
|
2796
|
+
return join12(crewHome, "prompts", promptSet);
|
|
2659
2797
|
}
|
|
2660
2798
|
var Missing = class {
|
|
2661
2799
|
paths = [];
|
|
@@ -2679,7 +2817,7 @@ var Missing = class {
|
|
|
2679
2817
|
};
|
|
2680
2818
|
function crewVersion(crewHome) {
|
|
2681
2819
|
try {
|
|
2682
|
-
const pkg = JSON.parse(
|
|
2820
|
+
const pkg = JSON.parse(readFileSync11(join12(crewHome, "package.json"), "utf8"));
|
|
2683
2821
|
return pkg.version ?? "0.0.0";
|
|
2684
2822
|
} catch {
|
|
2685
2823
|
return "0.0.0";
|
|
@@ -2689,26 +2827,26 @@ function configSearchPath(crewHome) {
|
|
|
2689
2827
|
if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
|
|
2690
2828
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
2691
2829
|
return [
|
|
2692
|
-
...xdg ? [
|
|
2693
|
-
|
|
2830
|
+
...xdg ? [join12(xdg, "crew", "crew.yaml")] : [],
|
|
2831
|
+
join12(homedir6(), ".config", "crew", "crew.yaml"),
|
|
2694
2832
|
resolve(crewHome, "crew.yaml")
|
|
2695
2833
|
];
|
|
2696
2834
|
}
|
|
2697
2835
|
function findConfigFile(crewHome) {
|
|
2698
2836
|
const candidates2 = configSearchPath(crewHome);
|
|
2699
|
-
return candidates2.find((c) =>
|
|
2837
|
+
return candidates2.find((c) => existsSync10(c)) ?? candidates2[candidates2.length - 1];
|
|
2700
2838
|
}
|
|
2701
|
-
var defaultConfigPath = () =>
|
|
2839
|
+
var defaultConfigPath = () => join12(process.env.XDG_CONFIG_HOME ?? join12(homedir6(), ".config"), "crew", "crew.yaml");
|
|
2702
2840
|
function routeSlug(route2) {
|
|
2703
2841
|
return route2.replace(/\//g, "-");
|
|
2704
2842
|
}
|
|
2705
2843
|
function resolvedPathFor(stateDir, route2) {
|
|
2706
2844
|
const [workspace, project] = splitRoute(route2);
|
|
2707
|
-
return
|
|
2845
|
+
return join12(stateDir, "resolved", workspace, `${project}.json`);
|
|
2708
2846
|
}
|
|
2709
2847
|
function apiKeyPathFor(stateDir, route2) {
|
|
2710
2848
|
const [workspace, project] = splitRoute(route2);
|
|
2711
|
-
return
|
|
2849
|
+
return join12(stateDir, "keys", workspace, `${project}.env`);
|
|
2712
2850
|
}
|
|
2713
2851
|
function splitRoute(route2) {
|
|
2714
2852
|
const parts = route2.split("/");
|
|
@@ -2719,9 +2857,9 @@ function splitRoute(route2) {
|
|
|
2719
2857
|
}
|
|
2720
2858
|
function readResolvedFile(stateDir, route2) {
|
|
2721
2859
|
const path = resolvedPathFor(stateDir, route2);
|
|
2722
|
-
if (!
|
|
2860
|
+
if (!existsSync10(path)) return void 0;
|
|
2723
2861
|
try {
|
|
2724
|
-
return JSON.parse(
|
|
2862
|
+
return JSON.parse(readFileSync11(path, "utf8"));
|
|
2725
2863
|
} catch (e) {
|
|
2726
2864
|
throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
|
|
2727
2865
|
}
|
|
@@ -2781,18 +2919,18 @@ function parseResolved(raw, m, where) {
|
|
|
2781
2919
|
}
|
|
2782
2920
|
function loadConfig(crewHome, configFile, opts = {}) {
|
|
2783
2921
|
const file = configFile ?? findConfigFile(crewHome);
|
|
2784
|
-
if (opts.text === void 0 && !
|
|
2922
|
+
if (opts.text === void 0 && !existsSync10(file)) {
|
|
2785
2923
|
throw new ConfigError(
|
|
2786
2924
|
`no config found. Looked in:
|
|
2787
2925
|
` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
|
|
2788
2926
|
|
|
2789
2927
|
Create the first of those:
|
|
2790
|
-
mkdir -p ${dirname4(defaultConfigPath())} && cp ${
|
|
2928
|
+
mkdir -p ${dirname4(defaultConfigPath())} && cp ${join12(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
|
|
2791
2929
|
);
|
|
2792
2930
|
}
|
|
2793
2931
|
let raw;
|
|
2794
2932
|
try {
|
|
2795
|
-
raw = parse2(opts.text ??
|
|
2933
|
+
raw = parse2(opts.text ?? readFileSync11(file, "utf8"));
|
|
2796
2934
|
} catch (e) {
|
|
2797
2935
|
throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
|
|
2798
2936
|
}
|
|
@@ -2846,6 +2984,11 @@ Create the first of those:
|
|
|
2846
2984
|
if (!Number.isInteger(periscopeIdleMinutes) || periscopeIdleMinutes < 1) {
|
|
2847
2985
|
missing.add(`ship.periscopeIdleMinutes must be a positive integer (got "${shipRaw.periscopeIdleMinutes}")`);
|
|
2848
2986
|
}
|
|
2987
|
+
const secretStoreRaw = shipRaw.secretStore === void 0 ? "auto" : shipRaw.secretStore;
|
|
2988
|
+
if (!isSecretStoreMode(secretStoreRaw)) {
|
|
2989
|
+
missing.add(`ship.secretStore must be one of ${SECRET_STORE_MODES.join(" | ")} (got "${String(shipRaw.secretStore)}")`);
|
|
2990
|
+
}
|
|
2991
|
+
const secretStore = isSecretStoreMode(secretStoreRaw) ? secretStoreRaw : "auto";
|
|
2849
2992
|
const eventsRotateBytes = shipRaw.eventsRotateBytes === void 0 ? DEFAULT_EVENTS_ROTATE_BYTES : Number(shipRaw.eventsRotateBytes);
|
|
2850
2993
|
if (!Number.isInteger(eventsRotateBytes) || eventsRotateBytes < 1) {
|
|
2851
2994
|
missing.add(`ship.eventsRotateBytes must be a positive integer (got "${shipRaw.eventsRotateBytes}")`);
|
|
@@ -2885,6 +3028,7 @@ Create the first of those:
|
|
|
2885
3028
|
const dupe = seen.find((n, i) => n && seen.indexOf(n) !== i);
|
|
2886
3029
|
if (dupe) missing.add(`routes: duplicate route "${dupe}" \u2014 a route addresses itself, so it must be unique`);
|
|
2887
3030
|
missing.throwIfAny(file);
|
|
3031
|
+
configureSecretStore({ mode: secretStore, stateDir });
|
|
2888
3032
|
return {
|
|
2889
3033
|
ship: {
|
|
2890
3034
|
name: shipRaw.name ?? "this ship",
|
|
@@ -2902,7 +3046,7 @@ Create the first of those:
|
|
|
2902
3046
|
useNvm: shipRaw.useNvm !== false,
|
|
2903
3047
|
nvmSh: shipRaw.nvmSh,
|
|
2904
3048
|
stateDir,
|
|
2905
|
-
logFile: expand(shipRaw.logFile ??
|
|
3049
|
+
logFile: expand(shipRaw.logFile ?? join12(tmpdir(), "crew.log"), base),
|
|
2906
3050
|
userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
|
|
2907
3051
|
maxConcurrentAgents,
|
|
2908
3052
|
maxConcurrentAgentsSet,
|
|
@@ -2919,7 +3063,8 @@ Create the first of those:
|
|
|
2919
3063
|
forges: parseShipForges(shipRaw.forges),
|
|
2920
3064
|
previewHost: shipRaw.previewHost ? String(shipRaw.previewHost).trim() || void 0 : void 0,
|
|
2921
3065
|
maxPeriscopes,
|
|
2922
|
-
periscopeIdleMinutes
|
|
3066
|
+
periscopeIdleMinutes,
|
|
3067
|
+
secretStore
|
|
2923
3068
|
},
|
|
2924
3069
|
routes,
|
|
2925
3070
|
crewHome,
|
|
@@ -3063,7 +3208,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
|
|
|
3063
3208
|
const hasRepos = Object.keys(repoDirs).length > 0;
|
|
3064
3209
|
const reposBasePath = expand(String(c?.reposBasePath ?? raw.ship?.reposBasePath ?? DEFAULT_REPOS_BASE_PATH), base);
|
|
3065
3210
|
const dirIsCloneBase = !c?.dir && !hasRepos && !Array.isArray(c?.repos);
|
|
3066
|
-
const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ?
|
|
3211
|
+
const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ? join12(reposBasePath, splitRoute(route2)[0]) : "";
|
|
3067
3212
|
const weight = c?.weight === void 0 ? void 0 : Number(c.weight);
|
|
3068
3213
|
if (weight !== void 0 && !(Number.isFinite(weight) && weight > 0)) {
|
|
3069
3214
|
missing.add(`${where}.weight must be a positive number (got "${c?.weight}")`);
|
|
@@ -3160,8 +3305,8 @@ function repoIdForName(r, name) {
|
|
|
3160
3305
|
}
|
|
3161
3306
|
function resolveApiKey(r) {
|
|
3162
3307
|
if (r.apiKey) return r.apiKey;
|
|
3163
|
-
if (r.apiKeyFile && r.apiKeyVar &&
|
|
3164
|
-
for (const line of
|
|
3308
|
+
if (r.apiKeyFile && r.apiKeyVar && existsSync10(r.apiKeyFile)) {
|
|
3309
|
+
for (const line of readFileSync11(r.apiKeyFile, "utf8").split("\n")) {
|
|
3165
3310
|
const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
|
|
3166
3311
|
if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
3167
3312
|
}
|
|
@@ -3174,9 +3319,9 @@ function resolveApiKey(r) {
|
|
|
3174
3319
|
}
|
|
3175
3320
|
function credentialSource(r) {
|
|
3176
3321
|
if (r.apiKey) return KEYCHAIN_ELIGIBLE_ROUTES.has(r) ? "keychain" : "inline";
|
|
3177
|
-
if (r.apiKeyFile && r.apiKeyVar &&
|
|
3322
|
+
if (r.apiKeyFile && r.apiKeyVar && existsSync10(r.apiKeyFile)) {
|
|
3178
3323
|
const re = new RegExp(`^${r.apiKeyVar}=`);
|
|
3179
|
-
if (
|
|
3324
|
+
if (readFileSync11(r.apiKeyFile, "utf8").split("\n").some((l) => re.test(l))) return "apiKeyFile";
|
|
3180
3325
|
}
|
|
3181
3326
|
return process.env.CREW_API_KEY?.trim() ? "CREW_API_KEY" : "none";
|
|
3182
3327
|
}
|
|
@@ -3232,7 +3377,7 @@ function configuredMembers(rt) {
|
|
|
3232
3377
|
return out;
|
|
3233
3378
|
}
|
|
3234
3379
|
function defaultRepoDir(basePath, workspace, repoName) {
|
|
3235
|
-
return
|
|
3380
|
+
return join12(basePath, workspace, repoName);
|
|
3236
3381
|
}
|
|
3237
3382
|
function reposOf(r) {
|
|
3238
3383
|
const named = Object.entries(r.repos);
|
|
@@ -3265,7 +3410,7 @@ function passengerRepoTargets(r) {
|
|
|
3265
3410
|
return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
|
|
3266
3411
|
}
|
|
3267
3412
|
function passengerCheckoutDir(basePath, workspace, repoName) {
|
|
3268
|
-
return
|
|
3413
|
+
return join12(basePath, workspace, ".passenger-checkouts", repoName);
|
|
3269
3414
|
}
|
|
3270
3415
|
function passengerCheckoutTargets(r) {
|
|
3271
3416
|
if (!r.hostPassengers) return [];
|
|
@@ -3372,10 +3517,10 @@ import { platform } from "process";
|
|
|
3372
3517
|
function defaultShell() {
|
|
3373
3518
|
return platform === "win32" ? { bin: "powershell", args: ["-NoProfile", "-NonInteractive", "-Command"] } : { bin: "bash", args: ["-c"] };
|
|
3374
3519
|
}
|
|
3375
|
-
function resolveShell(
|
|
3376
|
-
if (!
|
|
3377
|
-
if (typeof
|
|
3378
|
-
const bin =
|
|
3520
|
+
function resolveShell(configured2) {
|
|
3521
|
+
if (!configured2) return defaultShell();
|
|
3522
|
+
if (typeof configured2 !== "string") return configured2;
|
|
3523
|
+
const bin = configured2.trim();
|
|
3379
3524
|
if (/pwsh|powershell/i.test(bin)) {
|
|
3380
3525
|
return { bin, args: ["-NoProfile", "-NonInteractive", "-Command"] };
|
|
3381
3526
|
}
|
|
@@ -3404,10 +3549,10 @@ function runScript(script, opts) {
|
|
|
3404
3549
|
child.stdout.on("data", absorb);
|
|
3405
3550
|
child.stderr.on("data", absorb);
|
|
3406
3551
|
const timer = opts.timeoutMs ? setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs) : void 0;
|
|
3407
|
-
child.on("error", (
|
|
3552
|
+
child.on("error", (err3) => {
|
|
3408
3553
|
if (timer) clearTimeout(timer);
|
|
3409
3554
|
resolvePromise({ code: 127, output: `${output}
|
|
3410
|
-
crew: cannot run ${shell.bin}: ${
|
|
3555
|
+
crew: cannot run ${shell.bin}: ${err3.message}` });
|
|
3411
3556
|
});
|
|
3412
3557
|
child.on("close", (code) => {
|
|
3413
3558
|
if (timer) clearTimeout(timer);
|
|
@@ -3552,11 +3697,11 @@ function recoveredFailureHeadlines(o, route2) {
|
|
|
3552
3697
|
}
|
|
3553
3698
|
|
|
3554
3699
|
// src/install.ts
|
|
3555
|
-
import { dirname as dirname5, join as
|
|
3700
|
+
import { dirname as dirname5, join as join13, extname } from "path";
|
|
3556
3701
|
import { homedir as homedir7 } from "os";
|
|
3557
|
-
import { accessSync, constants, existsSync as
|
|
3702
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync11, mkdirSync as mkdirSync7, readdirSync as readdirSync3, readFileSync as readFileSync12, statSync as statSync4, writeFileSync as writeFileSync8, unlinkSync, realpathSync } from "fs";
|
|
3558
3703
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
3559
|
-
import { createHash as
|
|
3704
|
+
import { createHash as createHash3 } from "crypto";
|
|
3560
3705
|
|
|
3561
3706
|
// src/runtime-info.ts
|
|
3562
3707
|
function isCompiledBinary(moduleUrl) {
|
|
@@ -3566,7 +3711,7 @@ function isCompiledBinary(moduleUrl) {
|
|
|
3566
3711
|
// src/install.ts
|
|
3567
3712
|
function crewInvocation(crewHome) {
|
|
3568
3713
|
if (isCompiledBinary(import.meta.url)) return [process.execPath];
|
|
3569
|
-
return [process.execPath,
|
|
3714
|
+
return [process.execPath, join13(crewHome, "bin", "crew")];
|
|
3570
3715
|
}
|
|
3571
3716
|
var INTERVAL_SECONDS = 120;
|
|
3572
3717
|
function labelFor(crewHome, job) {
|
|
@@ -3577,7 +3722,7 @@ function labelFor(crewHome, job) {
|
|
|
3577
3722
|
return crewHome;
|
|
3578
3723
|
}
|
|
3579
3724
|
})();
|
|
3580
|
-
const hash =
|
|
3725
|
+
const hash = createHash3("sha1").update(real).digest("hex").slice(0, 8);
|
|
3581
3726
|
const stem = job === "release" ? "com.tablation.crew-release" : job === "passengers" ? "com.tablation.crew-passengers" : "com.tablation.crew";
|
|
3582
3727
|
return `${stem}.${hash}`;
|
|
3583
3728
|
}
|
|
@@ -3604,17 +3749,17 @@ function pathFor(ship) {
|
|
|
3604
3749
|
return parts.join(":");
|
|
3605
3750
|
}
|
|
3606
3751
|
function findOnPath(bin, searchPath) {
|
|
3607
|
-
const candidates2 = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) =>
|
|
3752
|
+
const candidates2 = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join13(dir, bin));
|
|
3608
3753
|
for (const candidate of candidates2) {
|
|
3609
3754
|
try {
|
|
3610
|
-
|
|
3611
|
-
if (
|
|
3755
|
+
accessSync2(candidate, constants2.X_OK);
|
|
3756
|
+
if (statSync4(candidate).isFile()) return candidate;
|
|
3612
3757
|
} catch {
|
|
3613
3758
|
}
|
|
3614
3759
|
}
|
|
3615
3760
|
return void 0;
|
|
3616
3761
|
}
|
|
3617
|
-
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin",
|
|
3762
|
+
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join13(homedir7(), ".docker", "bin")].join(":");
|
|
3618
3763
|
function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
3619
3764
|
const schedulerPath = pathFor(ship);
|
|
3620
3765
|
if (findOnPath("docker", schedulerPath)) return void 0;
|
|
@@ -3626,7 +3771,7 @@ function xmlEscape(s) {
|
|
|
3626
3771
|
}
|
|
3627
3772
|
function planLaunchd(ship, crewHome, job) {
|
|
3628
3773
|
const label = labelFor(crewHome, job);
|
|
3629
|
-
const unitPath =
|
|
3774
|
+
const unitPath = join13(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3630
3775
|
const crewLog = ship.logFile;
|
|
3631
3776
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
3632
3777
|
const persistent = job === "run";
|
|
@@ -3680,8 +3825,8 @@ ${scheduleKeys}
|
|
|
3680
3825
|
}
|
|
3681
3826
|
function planSystemd(ship, crewHome, job) {
|
|
3682
3827
|
const label = labelFor(crewHome, job);
|
|
3683
|
-
const unitDir =
|
|
3684
|
-
const servicePath =
|
|
3828
|
+
const unitDir = join13(homedir7(), ".config", "systemd", "user");
|
|
3829
|
+
const servicePath = join13(unitDir, `${label}.service`);
|
|
3685
3830
|
const crewLog = ship.logFile;
|
|
3686
3831
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
3687
3832
|
const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
|
|
@@ -3721,7 +3866,7 @@ WantedBy=default.target
|
|
|
3721
3866
|
schedulerLog
|
|
3722
3867
|
};
|
|
3723
3868
|
}
|
|
3724
|
-
const timerPath =
|
|
3869
|
+
const timerPath = join13(unitDir, `${label}.timer`);
|
|
3725
3870
|
const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
|
|
3726
3871
|
[Unit]
|
|
3727
3872
|
Description=${description}
|
|
@@ -3795,7 +3940,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3795
3940
|
function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
3796
3941
|
const label = labelFor(crewHome, job);
|
|
3797
3942
|
if (hostShip === "macos") {
|
|
3798
|
-
const unitPath =
|
|
3943
|
+
const unitPath = join13(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3799
3944
|
return {
|
|
3800
3945
|
mechanism: "launchd",
|
|
3801
3946
|
unitPaths: [unitPath],
|
|
@@ -3805,18 +3950,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3805
3950
|
};
|
|
3806
3951
|
}
|
|
3807
3952
|
if (hostShip === "linux" && hasSystemd) {
|
|
3808
|
-
const unitDir =
|
|
3953
|
+
const unitDir = join13(homedir7(), ".config", "systemd", "user");
|
|
3809
3954
|
if (job === "run") {
|
|
3810
3955
|
return {
|
|
3811
3956
|
mechanism: "systemd",
|
|
3812
|
-
unitPaths: [
|
|
3957
|
+
unitPaths: [join13(unitDir, `${label}.service`)],
|
|
3813
3958
|
crontabLine: null,
|
|
3814
3959
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
|
|
3815
3960
|
};
|
|
3816
3961
|
}
|
|
3817
3962
|
return {
|
|
3818
3963
|
mechanism: "systemd",
|
|
3819
|
-
unitPaths: [
|
|
3964
|
+
unitPaths: [join13(unitDir, `${label}.service`), join13(unitDir, `${label}.timer`)],
|
|
3820
3965
|
crontabLine: null,
|
|
3821
3966
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
|
|
3822
3967
|
};
|
|
@@ -3834,7 +3979,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3834
3979
|
function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
3835
3980
|
const label = labelFor(crewHome, "run");
|
|
3836
3981
|
if (hostShip === "macos") {
|
|
3837
|
-
const unitPath =
|
|
3982
|
+
const unitPath = join13(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3838
3983
|
return {
|
|
3839
3984
|
mechanism: "launchd",
|
|
3840
3985
|
startCommand: ["launchctl", "load", unitPath],
|
|
@@ -3874,8 +4019,8 @@ async function applyInstall(plan, crewHome, dryRun, log) {
|
|
|
3874
4019
|
log.emit(`would write ${path}`);
|
|
3875
4020
|
continue;
|
|
3876
4021
|
}
|
|
3877
|
-
|
|
3878
|
-
|
|
4022
|
+
mkdirSync7(dirname5(path), { recursive: true });
|
|
4023
|
+
writeFileSync8(path, plan.unitContent[path]);
|
|
3879
4024
|
log.emit(`wrote ${path}`);
|
|
3880
4025
|
}
|
|
3881
4026
|
if (plan.crontabLine) {
|
|
@@ -3929,7 +4074,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
3929
4074
|
}
|
|
3930
4075
|
}
|
|
3931
4076
|
for (const path of plan.unitPaths) {
|
|
3932
|
-
if (!
|
|
4077
|
+
if (!existsSync11(path)) continue;
|
|
3933
4078
|
if (dryRun) {
|
|
3934
4079
|
log.emit(`would remove ${path}`);
|
|
3935
4080
|
continue;
|
|
@@ -3948,7 +4093,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
3948
4093
|
}
|
|
3949
4094
|
var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
|
|
3950
4095
|
function launchAgentsDir() {
|
|
3951
|
-
return
|
|
4096
|
+
return join13(homedir7(), "Library", "LaunchAgents");
|
|
3952
4097
|
}
|
|
3953
4098
|
function xmlUnescape(s) {
|
|
3954
4099
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
@@ -3965,25 +4110,25 @@ function loadedCrewLabels() {
|
|
|
3965
4110
|
return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
|
|
3966
4111
|
}
|
|
3967
4112
|
function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
3968
|
-
const files =
|
|
4113
|
+
const files = existsSync11(dir) ? readdirSync3(dir) : [];
|
|
3969
4114
|
const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
|
|
3970
|
-
const plistPath =
|
|
4115
|
+
const plistPath = join13(dir, f);
|
|
3971
4116
|
let program = null;
|
|
3972
4117
|
try {
|
|
3973
|
-
program = programFromPlist(
|
|
4118
|
+
program = programFromPlist(readFileSync12(plistPath, "utf8"));
|
|
3974
4119
|
} catch {
|
|
3975
4120
|
}
|
|
3976
4121
|
return {
|
|
3977
4122
|
label: f.slice(0, -".plist".length),
|
|
3978
4123
|
plistPath,
|
|
3979
4124
|
program,
|
|
3980
|
-
programExists: program !== null &&
|
|
4125
|
+
programExists: program !== null && existsSync11(program)
|
|
3981
4126
|
};
|
|
3982
4127
|
});
|
|
3983
4128
|
const seen = new Set(units.map((u) => u.label));
|
|
3984
4129
|
for (const label of [...loaded].sort()) {
|
|
3985
4130
|
if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
|
|
3986
|
-
units.push({ label, plistPath:
|
|
4131
|
+
units.push({ label, plistPath: join13(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
|
|
3987
4132
|
}
|
|
3988
4133
|
return units;
|
|
3989
4134
|
}
|
|
@@ -3996,9 +4141,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
|
|
|
3996
4141
|
log.emit(`would unload and remove ${unit.label}`);
|
|
3997
4142
|
return;
|
|
3998
4143
|
}
|
|
3999
|
-
if (
|
|
4144
|
+
if (existsSync11(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
|
|
4000
4145
|
spawnSync4("launchctl", ["remove", unit.label], { encoding: "utf8" });
|
|
4001
|
-
if (
|
|
4146
|
+
if (existsSync11(unit.plistPath)) unlinkSync(unit.plistPath);
|
|
4002
4147
|
log.emit(`removed ${unit.label} (${unit.plistPath})`);
|
|
4003
4148
|
}
|
|
4004
4149
|
function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
|
|
@@ -4076,24 +4221,24 @@ function detectSystemd() {
|
|
|
4076
4221
|
}
|
|
4077
4222
|
|
|
4078
4223
|
// src/ship-attention.ts
|
|
4079
|
-
import { existsSync as
|
|
4224
|
+
import { existsSync as existsSync13, statfsSync } from "fs";
|
|
4080
4225
|
|
|
4081
4226
|
// src/git.ts
|
|
4082
4227
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
4083
4228
|
import {
|
|
4084
|
-
existsSync as
|
|
4085
|
-
mkdirSync as
|
|
4229
|
+
existsSync as existsSync12,
|
|
4230
|
+
mkdirSync as mkdirSync8,
|
|
4086
4231
|
openSync as openSync2,
|
|
4087
4232
|
closeSync as closeSync2,
|
|
4088
4233
|
writeSync,
|
|
4089
|
-
statSync as
|
|
4234
|
+
statSync as statSync5,
|
|
4090
4235
|
unlinkSync as unlinkSync2,
|
|
4091
|
-
readFileSync as
|
|
4092
|
-
writeFileSync as
|
|
4236
|
+
readFileSync as readFileSync13,
|
|
4237
|
+
writeFileSync as writeFileSync9,
|
|
4093
4238
|
readdirSync as readdirSync4,
|
|
4094
|
-
rmSync as
|
|
4239
|
+
rmSync as rmSync7
|
|
4095
4240
|
} from "fs";
|
|
4096
|
-
import { dirname as dirname6, basename as basename4, join as
|
|
4241
|
+
import { dirname as dirname6, basename as basename4, join as join14 } from "path";
|
|
4097
4242
|
var GitError = class extends Error {
|
|
4098
4243
|
};
|
|
4099
4244
|
function streamText(x) {
|
|
@@ -4103,18 +4248,18 @@ function gitRaw(cwd, args) {
|
|
|
4103
4248
|
try {
|
|
4104
4249
|
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
4105
4250
|
} catch (e) {
|
|
4106
|
-
const
|
|
4107
|
-
const why = [
|
|
4108
|
-
throw new GitError(`git ${args.join(" ")}: ${why ||
|
|
4251
|
+
const err3 = e;
|
|
4252
|
+
const why = [err3.stderr, err3.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
4253
|
+
throw new GitError(`git ${args.join(" ")}: ${why || err3.message.trim()}`);
|
|
4109
4254
|
}
|
|
4110
4255
|
}
|
|
4111
4256
|
function git(cwd, args) {
|
|
4112
4257
|
try {
|
|
4113
4258
|
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
4114
4259
|
} catch (e) {
|
|
4115
|
-
const
|
|
4116
|
-
const why = [
|
|
4117
|
-
throw new GitError(`git ${args.join(" ")}: ${why ||
|
|
4260
|
+
const err3 = e;
|
|
4261
|
+
const why = [err3.stderr, err3.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
4262
|
+
throw new GitError(`git ${args.join(" ")}: ${why || err3.message.trim()}`);
|
|
4118
4263
|
}
|
|
4119
4264
|
}
|
|
4120
4265
|
function gitOk(cwd, args) {
|
|
@@ -4474,7 +4619,7 @@ function recoverStrandedReleaseCheckouts(cwd) {
|
|
|
4474
4619
|
else if (line.startsWith("branch refs/heads/crew/release-") && path) {
|
|
4475
4620
|
gitOk(cwd, ["worktree", "remove", "--force", path]);
|
|
4476
4621
|
const root = dirname6(path);
|
|
4477
|
-
if (basename4(root).startsWith("crew-release-"))
|
|
4622
|
+
if (basename4(root).startsWith("crew-release-")) rmSync7(root, { recursive: true, force: true });
|
|
4478
4623
|
}
|
|
4479
4624
|
}
|
|
4480
4625
|
const stranded = (gitOk(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/crew/release-*"]) ?? "").split("\n").filter(Boolean);
|
|
@@ -4491,12 +4636,12 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
|
|
|
4491
4636
|
}
|
|
4492
4637
|
for (const name of names) {
|
|
4493
4638
|
if (!name.startsWith("crew-release-")) continue;
|
|
4494
|
-
const root =
|
|
4639
|
+
const root = join14(tmp, name);
|
|
4495
4640
|
try {
|
|
4496
|
-
const st =
|
|
4641
|
+
const st = statSync5(root);
|
|
4497
4642
|
if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
|
|
4498
|
-
if (
|
|
4499
|
-
|
|
4643
|
+
if (existsSync12(join14(root, "checkout"))) continue;
|
|
4644
|
+
rmSync7(root, { recursive: true, force: true });
|
|
4500
4645
|
removed.push(name);
|
|
4501
4646
|
} catch {
|
|
4502
4647
|
}
|
|
@@ -4545,20 +4690,20 @@ function originUrlOf(dir, remoteName) {
|
|
|
4545
4690
|
}
|
|
4546
4691
|
}
|
|
4547
4692
|
function ensureRepoCheckout(dir, remote, opts = {}) {
|
|
4548
|
-
if (
|
|
4693
|
+
if (existsSync12(`${dir}/.git`)) return false;
|
|
4549
4694
|
if (!remote) {
|
|
4550
4695
|
throw new GitError(
|
|
4551
4696
|
`${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`
|
|
4552
4697
|
);
|
|
4553
4698
|
}
|
|
4554
|
-
|
|
4699
|
+
mkdirSync8(dirname6(dir), { recursive: true });
|
|
4555
4700
|
const url = cloneUrlFor(remote, opts);
|
|
4556
4701
|
try {
|
|
4557
4702
|
execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
4558
4703
|
} catch (e) {
|
|
4559
|
-
const
|
|
4560
|
-
const why = [
|
|
4561
|
-
throw new GitError(`git clone ${url} ${dir}: ${why ||
|
|
4704
|
+
const err3 = e;
|
|
4705
|
+
const why = [err3.stderr, err3.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
4706
|
+
throw new GitError(`git clone ${url} ${dir}: ${why || err3.message.trim()}`);
|
|
4562
4707
|
}
|
|
4563
4708
|
cloneRecorder?.(dir);
|
|
4564
4709
|
return true;
|
|
@@ -4576,7 +4721,7 @@ function sleepSync(ms3) {
|
|
|
4576
4721
|
}
|
|
4577
4722
|
function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
4578
4723
|
const lockPath = checkoutLockPath(dir);
|
|
4579
|
-
|
|
4724
|
+
mkdirSync8(dirname6(lockPath), { recursive: true });
|
|
4580
4725
|
const deadline = Date.now() + timeoutMs;
|
|
4581
4726
|
for (; ; ) {
|
|
4582
4727
|
try {
|
|
@@ -4591,10 +4736,10 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
4591
4736
|
}
|
|
4592
4737
|
};
|
|
4593
4738
|
} catch (e) {
|
|
4594
|
-
const
|
|
4595
|
-
if (
|
|
4739
|
+
const err3 = e;
|
|
4740
|
+
if (err3.code !== "EEXIST") throw err3;
|
|
4596
4741
|
try {
|
|
4597
|
-
if (Date.now() -
|
|
4742
|
+
if (Date.now() - statSync5(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
|
|
4598
4743
|
try {
|
|
4599
4744
|
unlinkSync2(lockPath);
|
|
4600
4745
|
} catch {
|
|
@@ -4613,17 +4758,17 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
4613
4758
|
function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
|
|
4614
4759
|
const release = acquireCheckoutLock(dir);
|
|
4615
4760
|
try {
|
|
4616
|
-
if (!
|
|
4761
|
+
if (!existsSync12(`${dir}/.git`) && githubRemote) {
|
|
4617
4762
|
const url = cloneUrlFor(githubRemote, clone);
|
|
4618
4763
|
const marker = cloneFailureMarker(dir);
|
|
4619
|
-
if (
|
|
4620
|
-
return { action: "clone-failed", detail:
|
|
4764
|
+
if (existsSync12(marker) && readFileSync13(marker, "utf8").split("\n")[0] === url) {
|
|
4765
|
+
return { action: "clone-failed", detail: readFileSync13(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
|
|
4621
4766
|
}
|
|
4622
4767
|
try {
|
|
4623
4768
|
ensureRepoCheckout(dir, githubRemote, clone);
|
|
4624
4769
|
} catch (e) {
|
|
4625
4770
|
if (!(e instanceof GitError)) throw e;
|
|
4626
|
-
|
|
4771
|
+
writeFileSync9(marker, `${url}
|
|
4627
4772
|
${e.message}`);
|
|
4628
4773
|
return { action: "clone-failed", detail: e.message, repeat: false };
|
|
4629
4774
|
}
|
|
@@ -4689,6 +4834,15 @@ function baseAttention(repo, outcome, remote, base, nowIso = (/* @__PURE__ */ ne
|
|
|
4689
4834
|
if (outcome.action === "level" || outcome.action === "fast-forwarded") return { clear: baseUnsafeKey(repo) };
|
|
4690
4835
|
return null;
|
|
4691
4836
|
}
|
|
4837
|
+
var seatKeyUnreadableKey = (route2, seat) => `seat_key_unreadable:${route2}:${seat}`;
|
|
4838
|
+
function seatKeyUnreadableItem(route2, seat, storeError, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4839
|
+
return {
|
|
4840
|
+
kind: "seat_key_unreadable",
|
|
4841
|
+
key: seatKeyUnreadableKey(route2, seat),
|
|
4842
|
+
since: nowIso,
|
|
4843
|
+
message: `${route2}: the ${seat} seat's agent key cannot be read (${storeError}) \u2014 its runs are refused until the key store is readable. Resumes automatically once it is; \`crew doctor\` names the store in use`
|
|
4844
|
+
};
|
|
4845
|
+
}
|
|
4692
4846
|
var agentLogRefusedKey = (route2) => `agent_log_refused:${route2}`;
|
|
4693
4847
|
function agentLogRefusedItem(route2, role, status2, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4694
4848
|
return {
|
|
@@ -4756,7 +4910,7 @@ function repoStopFor(items, route2) {
|
|
|
4756
4910
|
};
|
|
4757
4911
|
}
|
|
4758
4912
|
function recheckEnvironmentStops(state, route2, ship, deps = {}) {
|
|
4759
|
-
const
|
|
4913
|
+
const onPath2 = deps.onPath ?? findOnPath;
|
|
4760
4914
|
const dockerProblem = deps.dockerProblem ?? dockerPathProblem;
|
|
4761
4915
|
const cleared = [];
|
|
4762
4916
|
for (const item of state.shipAttention().previous()) {
|
|
@@ -4779,7 +4933,7 @@ function recheckEnvironmentStops(state, route2, ship, deps = {}) {
|
|
|
4779
4933
|
}
|
|
4780
4934
|
const script = cfg.hooks[h.hook];
|
|
4781
4935
|
const cmd = script ? hookCommand(script) : null;
|
|
4782
|
-
if (!cmd ||
|
|
4936
|
+
if (!cmd || onPath2(cmd, pathFor(ship))) {
|
|
4783
4937
|
clearShipAttention(state, item.key);
|
|
4784
4938
|
cleared.push(`${h.repo}/${h.hook}`);
|
|
4785
4939
|
}
|
|
@@ -4830,7 +4984,7 @@ function noteHeldWorktree(state, h, nowMs = Date.now()) {
|
|
|
4830
4984
|
function reconcileHeldWorktrees(state, repo, log, stillRefused) {
|
|
4831
4985
|
for (const i of state.shipAttention().previous()) {
|
|
4832
4986
|
if (i.kind !== "worktree_held" || !i.held || i.held.repo !== repo) continue;
|
|
4833
|
-
const gone = !
|
|
4987
|
+
const gone = !existsSync13(i.held.path);
|
|
4834
4988
|
if (!gone && !(stillRefused && !stillRefused.has(i.held.path))) continue;
|
|
4835
4989
|
clearShipAttention(state, i.key);
|
|
4836
4990
|
log.emit(
|
|
@@ -4975,8 +5129,8 @@ function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
|
|
|
4975
5129
|
run2(o.dir, ["push", "--dry-run", o.remote, `HEAD:refs/heads/${PROBE_BRANCH}`], env);
|
|
4976
5130
|
return { state: "ok" };
|
|
4977
5131
|
} catch (e) {
|
|
4978
|
-
const
|
|
4979
|
-
const raw = (
|
|
5132
|
+
const err3 = e;
|
|
5133
|
+
const raw = (err3.stderr ? String(err3.stderr) : err3.message).trim().split("\n").filter(Boolean).slice(-2).join(" / ");
|
|
4980
5134
|
const detail = raw || "push dry-run failed";
|
|
4981
5135
|
const f = o.forge;
|
|
4982
5136
|
if (!f) return { state: "failed", detail, hint: pushAuthHint(o.remoteUrl) };
|
|
@@ -5081,30 +5235,30 @@ function roleCapWarnings(ship) {
|
|
|
5081
5235
|
|
|
5082
5236
|
// src/selftest.ts
|
|
5083
5237
|
import { spawn as spawn3 } from "child_process";
|
|
5084
|
-
import { existsSync as
|
|
5238
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync13, mkdtempSync, readFileSync as readFileSync20, rmSync as rmSync11, writeFileSync as writeFileSync14 } from "fs";
|
|
5085
5239
|
import { tmpdir as tmpdir2 } from "os";
|
|
5086
|
-
import { join as
|
|
5240
|
+
import { join as join23 } from "path";
|
|
5087
5241
|
|
|
5088
5242
|
// src/agent.ts
|
|
5089
5243
|
import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
|
|
5090
|
-
import { readFileSync as
|
|
5244
|
+
import { readFileSync as readFileSync19, existsSync as existsSync21, statSync as statSync7, mkdirSync as mkdirSync12, writeFileSync as writeFileSync13, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
|
|
5091
5245
|
import { StringDecoder } from "string_decoder";
|
|
5092
5246
|
import { finished } from "stream/promises";
|
|
5093
|
-
import { join as
|
|
5094
|
-
import { createHash as
|
|
5247
|
+
import { join as join22, dirname as dirname8, basename as basename7 } from "path";
|
|
5248
|
+
import { createHash as createHash4 } from "crypto";
|
|
5095
5249
|
|
|
5096
5250
|
// src/state.ts
|
|
5097
|
-
import { readFileSync as
|
|
5098
|
-
import { join as
|
|
5251
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, existsSync as existsSync14, mkdirSync as mkdirSync9, rmSync as rmSync8, statSync as statSync6, readdirSync as readdirSync5 } from "fs";
|
|
5252
|
+
import { join as join15 } from "path";
|
|
5099
5253
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
5100
5254
|
var State = class _State {
|
|
5101
5255
|
dir;
|
|
5102
5256
|
constructor(dir) {
|
|
5103
5257
|
this.dir = dir;
|
|
5104
|
-
|
|
5258
|
+
mkdirSync9(dir, { recursive: true });
|
|
5105
5259
|
}
|
|
5106
5260
|
path(...parts) {
|
|
5107
|
-
return
|
|
5261
|
+
return join15(this.dir, ...parts);
|
|
5108
5262
|
}
|
|
5109
5263
|
/**
|
|
5110
5264
|
* The timestamp every "new since last poll" check is measured against.
|
|
@@ -5115,14 +5269,14 @@ var State = class _State {
|
|
|
5115
5269
|
*/
|
|
5116
5270
|
watermark() {
|
|
5117
5271
|
try {
|
|
5118
|
-
const v =
|
|
5272
|
+
const v = readFileSync14(this.path(".poll-watermark"), "utf8").trim();
|
|
5119
5273
|
return v || EPOCH;
|
|
5120
5274
|
} catch {
|
|
5121
5275
|
return EPOCH;
|
|
5122
5276
|
}
|
|
5123
5277
|
}
|
|
5124
5278
|
setWatermark(at) {
|
|
5125
|
-
|
|
5279
|
+
writeFileSync10(this.path(".poll-watermark"), `${at}
|
|
5126
5280
|
`);
|
|
5127
5281
|
}
|
|
5128
5282
|
/**
|
|
@@ -5140,19 +5294,19 @@ var State = class _State {
|
|
|
5140
5294
|
}
|
|
5141
5295
|
/** Whole-crew pause, and the per-role sentinels one level down. */
|
|
5142
5296
|
isPaused() {
|
|
5143
|
-
return
|
|
5297
|
+
return existsSync14(this.path(".crew-paused"));
|
|
5144
5298
|
}
|
|
5145
5299
|
isRolePaused(role) {
|
|
5146
|
-
return
|
|
5300
|
+
return existsSync14(this.path(`.role-paused-${role}`));
|
|
5147
5301
|
}
|
|
5148
5302
|
pausedRoles(roles) {
|
|
5149
5303
|
return new Set(roles.filter((r) => this.isRolePaused(r)));
|
|
5150
5304
|
}
|
|
5151
5305
|
pause(role) {
|
|
5152
|
-
|
|
5306
|
+
writeFileSync10(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
|
|
5153
5307
|
}
|
|
5154
5308
|
resume(role) {
|
|
5155
|
-
|
|
5309
|
+
rmSync8(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
|
|
5156
5310
|
}
|
|
5157
5311
|
/**
|
|
5158
5312
|
* The release phase (merge, deploy, release) has its own sentinel, one
|
|
@@ -5160,11 +5314,11 @@ var State = class _State {
|
|
|
5160
5314
|
* while agents keep working.
|
|
5161
5315
|
*/
|
|
5162
5316
|
isReleasePaused() {
|
|
5163
|
-
return
|
|
5317
|
+
return existsSync14(this.path(".release-paused"));
|
|
5164
5318
|
}
|
|
5165
5319
|
setReleasePaused(paused) {
|
|
5166
|
-
if (paused)
|
|
5167
|
-
else
|
|
5320
|
+
if (paused) writeFileSync10(this.path(".release-paused"), "");
|
|
5321
|
+
else rmSync8(this.path(".release-paused"), { force: true });
|
|
5168
5322
|
}
|
|
5169
5323
|
/**
|
|
5170
5324
|
* Why a release cycle must not run right now, or null when it may. Every
|
|
@@ -5201,7 +5355,7 @@ var State = class _State {
|
|
|
5201
5355
|
const name = _State.safe(rawName);
|
|
5202
5356
|
const file = this.path(`.${name}.lock`);
|
|
5203
5357
|
try {
|
|
5204
|
-
const existing = Number.parseInt(
|
|
5358
|
+
const existing = Number.parseInt(readFileSync14(file, "utf8").trim(), 10);
|
|
5205
5359
|
if (Number.isFinite(existing) && existing > 0) {
|
|
5206
5360
|
try {
|
|
5207
5361
|
process.kill(existing, 0);
|
|
@@ -5211,13 +5365,13 @@ var State = class _State {
|
|
|
5211
5365
|
}
|
|
5212
5366
|
} catch {
|
|
5213
5367
|
}
|
|
5214
|
-
|
|
5368
|
+
writeFileSync10(file, `${process.pid}
|
|
5215
5369
|
`);
|
|
5216
5370
|
return {
|
|
5217
5371
|
ok: true,
|
|
5218
5372
|
release: () => {
|
|
5219
5373
|
try {
|
|
5220
|
-
|
|
5374
|
+
rmSync8(file, { force: true });
|
|
5221
5375
|
} catch {
|
|
5222
5376
|
}
|
|
5223
5377
|
}
|
|
@@ -5239,7 +5393,7 @@ var State = class _State {
|
|
|
5239
5393
|
if (!/^\.release-.+\.lock$/.test(n)) continue;
|
|
5240
5394
|
let pid = 0;
|
|
5241
5395
|
try {
|
|
5242
|
-
pid = Number.parseInt(
|
|
5396
|
+
pid = Number.parseInt(readFileSync14(this.path(n), "utf8").trim(), 10);
|
|
5243
5397
|
} catch {
|
|
5244
5398
|
continue;
|
|
5245
5399
|
}
|
|
@@ -5325,7 +5479,7 @@ var State = class _State {
|
|
|
5325
5479
|
ticketInFlight(recordId) {
|
|
5326
5480
|
const file = this.path(`.${_State.safe(`crew-ticket-${recordId}`)}.lock`);
|
|
5327
5481
|
try {
|
|
5328
|
-
const pid = Number.parseInt(
|
|
5482
|
+
const pid = Number.parseInt(readFileSync14(file, "utf8").trim(), 10);
|
|
5329
5483
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
5330
5484
|
process.kill(pid, 0);
|
|
5331
5485
|
return true;
|
|
@@ -5359,7 +5513,7 @@ var State = class _State {
|
|
|
5359
5513
|
const heldReported = `.release-held-${suffix}`;
|
|
5360
5514
|
const read = (f) => {
|
|
5361
5515
|
try {
|
|
5362
|
-
return
|
|
5516
|
+
return readFileSync14(this.path(f), "utf8").trim() || null;
|
|
5363
5517
|
} catch {
|
|
5364
5518
|
return null;
|
|
5365
5519
|
}
|
|
@@ -5368,12 +5522,12 @@ var State = class _State {
|
|
|
5368
5522
|
blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
|
|
5369
5523
|
noteBlocked: () => {
|
|
5370
5524
|
const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
|
|
5371
|
-
|
|
5525
|
+
writeFileSync10(this.path(blocked), `${n}
|
|
5372
5526
|
`);
|
|
5373
5527
|
return n;
|
|
5374
5528
|
},
|
|
5375
5529
|
clearBlocked: () => {
|
|
5376
|
-
|
|
5530
|
+
rmSync8(this.path(blocked), { force: true });
|
|
5377
5531
|
},
|
|
5378
5532
|
/**
|
|
5379
5533
|
* The commit a deploy last failed on.
|
|
@@ -5385,11 +5539,11 @@ var State = class _State {
|
|
|
5385
5539
|
*/
|
|
5386
5540
|
deployFailedSha: () => read(failed),
|
|
5387
5541
|
noteDeployFailed: (sha) => {
|
|
5388
|
-
|
|
5542
|
+
writeFileSync10(this.path(failed), `${sha}
|
|
5389
5543
|
`);
|
|
5390
5544
|
},
|
|
5391
5545
|
clearDeployFailed: () => {
|
|
5392
|
-
|
|
5546
|
+
rmSync8(this.path(failed), { force: true });
|
|
5393
5547
|
},
|
|
5394
5548
|
/**
|
|
5395
5549
|
* Consecutive test-gate failures on the same head (CREW-1368). A failure
|
|
@@ -5399,31 +5553,31 @@ var State = class _State {
|
|
|
5399
5553
|
noteTestGateFailed: (sha) => {
|
|
5400
5554
|
const [prev, n] = (read(gateFailed) ?? "").split(" ");
|
|
5401
5555
|
const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
|
|
5402
|
-
|
|
5556
|
+
writeFileSync10(this.path(gateFailed), `${sha} ${next}
|
|
5403
5557
|
`);
|
|
5404
5558
|
return next;
|
|
5405
5559
|
},
|
|
5406
5560
|
clearTestGateFailed: () => {
|
|
5407
|
-
|
|
5561
|
+
rmSync8(this.path(gateFailed), { force: true });
|
|
5408
5562
|
},
|
|
5409
5563
|
/** Whether the board was already told about this head's red gate. */
|
|
5410
5564
|
heldReported: () => {
|
|
5411
5565
|
try {
|
|
5412
|
-
return
|
|
5566
|
+
return readFileSync14(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
|
|
5413
5567
|
} catch {
|
|
5414
5568
|
return null;
|
|
5415
5569
|
}
|
|
5416
5570
|
},
|
|
5417
5571
|
noteHeldReported: (signature) => {
|
|
5418
|
-
|
|
5572
|
+
writeFileSync10(this.path(heldReported), `${signature}
|
|
5419
5573
|
`);
|
|
5420
5574
|
},
|
|
5421
5575
|
clearHeldReported: () => {
|
|
5422
|
-
|
|
5576
|
+
rmSync8(this.path(heldReported), { force: true });
|
|
5423
5577
|
},
|
|
5424
5578
|
testGateReported: (sha) => read(gateReported) === sha,
|
|
5425
5579
|
noteTestGateReported: (sha) => {
|
|
5426
|
-
|
|
5580
|
+
writeFileSync10(this.path(gateReported), `${sha}
|
|
5427
5581
|
`);
|
|
5428
5582
|
}
|
|
5429
5583
|
};
|
|
@@ -5446,13 +5600,13 @@ var State = class _State {
|
|
|
5446
5600
|
const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
|
|
5447
5601
|
const read = (f) => {
|
|
5448
5602
|
try {
|
|
5449
|
-
return
|
|
5603
|
+
return readFileSync14(this.path(f), "utf8").trim() || null;
|
|
5450
5604
|
} catch {
|
|
5451
5605
|
return null;
|
|
5452
5606
|
}
|
|
5453
5607
|
};
|
|
5454
5608
|
const clear2 = () => {
|
|
5455
|
-
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile])
|
|
5609
|
+
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync8(this.path(f), { force: true });
|
|
5456
5610
|
};
|
|
5457
5611
|
return {
|
|
5458
5612
|
/** Consecutive cycles this route had actionable work and did not win. */
|
|
@@ -5479,18 +5633,18 @@ var State = class _State {
|
|
|
5479
5633
|
clear2();
|
|
5480
5634
|
return;
|
|
5481
5635
|
}
|
|
5482
|
-
|
|
5636
|
+
writeFileSync10(this.path(streakFile), `${this.readInt(streakFile) + 1}
|
|
5483
5637
|
`);
|
|
5484
5638
|
const prior = read(ticketFile);
|
|
5485
5639
|
if (topTicket && topTicket === prior) {
|
|
5486
|
-
|
|
5640
|
+
writeFileSync10(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
|
|
5487
5641
|
`);
|
|
5488
5642
|
} else if (topTicket) {
|
|
5489
|
-
|
|
5643
|
+
writeFileSync10(this.path(ticketFile), `${topTicket}
|
|
5490
5644
|
`);
|
|
5491
|
-
|
|
5645
|
+
writeFileSync10(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
5492
5646
|
`);
|
|
5493
|
-
|
|
5647
|
+
writeFileSync10(this.path(ticketStreakFile), "1\n");
|
|
5494
5648
|
}
|
|
5495
5649
|
}
|
|
5496
5650
|
};
|
|
@@ -5511,14 +5665,14 @@ var State = class _State {
|
|
|
5511
5665
|
return {
|
|
5512
5666
|
previous: () => {
|
|
5513
5667
|
try {
|
|
5514
|
-
const parsed = JSON.parse(
|
|
5668
|
+
const parsed = JSON.parse(readFileSync14(this.path(file), "utf8"));
|
|
5515
5669
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
5516
5670
|
} catch {
|
|
5517
5671
|
return {};
|
|
5518
5672
|
}
|
|
5519
5673
|
},
|
|
5520
5674
|
persist: (next) => {
|
|
5521
|
-
|
|
5675
|
+
writeFileSync10(this.path(file), JSON.stringify(next));
|
|
5522
5676
|
}
|
|
5523
5677
|
};
|
|
5524
5678
|
}
|
|
@@ -5534,14 +5688,14 @@ var State = class _State {
|
|
|
5534
5688
|
return {
|
|
5535
5689
|
previous: () => {
|
|
5536
5690
|
try {
|
|
5537
|
-
const parsed = JSON.parse(
|
|
5691
|
+
const parsed = JSON.parse(readFileSync14(this.path(file), "utf8"));
|
|
5538
5692
|
return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [];
|
|
5539
5693
|
} catch {
|
|
5540
5694
|
return [];
|
|
5541
5695
|
}
|
|
5542
5696
|
},
|
|
5543
5697
|
persist: (next) => {
|
|
5544
|
-
|
|
5698
|
+
writeFileSync10(this.path(file), JSON.stringify(next));
|
|
5545
5699
|
}
|
|
5546
5700
|
};
|
|
5547
5701
|
}
|
|
@@ -5555,14 +5709,14 @@ var State = class _State {
|
|
|
5555
5709
|
return {
|
|
5556
5710
|
previous: () => {
|
|
5557
5711
|
try {
|
|
5558
|
-
const parsed = JSON.parse(
|
|
5712
|
+
const parsed = JSON.parse(readFileSync14(this.path(file), "utf8"));
|
|
5559
5713
|
return Array.isArray(parsed) ? parsed : [];
|
|
5560
5714
|
} catch {
|
|
5561
5715
|
return [];
|
|
5562
5716
|
}
|
|
5563
5717
|
},
|
|
5564
5718
|
persist: (next) => {
|
|
5565
|
-
|
|
5719
|
+
writeFileSync10(this.path(file), JSON.stringify(next));
|
|
5566
5720
|
}
|
|
5567
5721
|
};
|
|
5568
5722
|
}
|
|
@@ -5575,11 +5729,11 @@ var State = class _State {
|
|
|
5575
5729
|
const file = ".release-last-run";
|
|
5576
5730
|
return {
|
|
5577
5731
|
touch: () => {
|
|
5578
|
-
|
|
5732
|
+
writeFileSync10(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
|
|
5579
5733
|
},
|
|
5580
5734
|
at: () => {
|
|
5581
5735
|
try {
|
|
5582
|
-
return
|
|
5736
|
+
return statSync6(this.path(file)).mtimeMs;
|
|
5583
5737
|
} catch {
|
|
5584
5738
|
return null;
|
|
5585
5739
|
}
|
|
@@ -5588,7 +5742,7 @@ var State = class _State {
|
|
|
5588
5742
|
}
|
|
5589
5743
|
readInt(file) {
|
|
5590
5744
|
try {
|
|
5591
|
-
return Number.parseInt(
|
|
5745
|
+
return Number.parseInt(readFileSync14(this.path(file), "utf8").trim(), 10) || 0;
|
|
5592
5746
|
} catch {
|
|
5593
5747
|
return 0;
|
|
5594
5748
|
}
|
|
@@ -6071,8 +6225,8 @@ var PERISCOPE_COLUMNS = [
|
|
|
6071
6225
|
];
|
|
6072
6226
|
|
|
6073
6227
|
// src/agent-log.ts
|
|
6074
|
-
import { existsSync as
|
|
6075
|
-
import { join as
|
|
6228
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, readdirSync as readdirSync6, readFileSync as readFileSync15, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "fs";
|
|
6229
|
+
import { join as join16 } from "path";
|
|
6076
6230
|
|
|
6077
6231
|
// src/pending-backoff.ts
|
|
6078
6232
|
var sleepBackoff = (attempts) => Math.min(6e4 * 2 ** Math.max(0, attempts - 1), 30 * 6e4);
|
|
@@ -6244,20 +6398,20 @@ async function addRunToTicket(target, t, recordId, run2) {
|
|
|
6244
6398
|
};
|
|
6245
6399
|
}
|
|
6246
6400
|
}
|
|
6247
|
-
var queueDir = (stateDir) =>
|
|
6401
|
+
var queueDir = (stateDir) => join16(stateDir, "pending-agent-logs");
|
|
6248
6402
|
function queueAgentRun(stateDir, name, q) {
|
|
6249
|
-
|
|
6250
|
-
|
|
6403
|
+
mkdirSync10(queueDir(stateDir), { recursive: true });
|
|
6404
|
+
writeFileSync11(join16(queueDir(stateDir), `${name}.json`), JSON.stringify(q));
|
|
6251
6405
|
}
|
|
6252
6406
|
async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
6253
6407
|
const dir = queueDir(stateDir);
|
|
6254
|
-
if (!
|
|
6408
|
+
if (!existsSync15(dir)) return 0;
|
|
6255
6409
|
let sent = 0;
|
|
6256
6410
|
for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
6257
|
-
const path =
|
|
6411
|
+
const path = join16(dir, f);
|
|
6258
6412
|
let q;
|
|
6259
6413
|
try {
|
|
6260
|
-
q = JSON.parse(
|
|
6414
|
+
q = JSON.parse(readFileSync15(path, "utf8"));
|
|
6261
6415
|
} catch {
|
|
6262
6416
|
continue;
|
|
6263
6417
|
}
|
|
@@ -6276,17 +6430,17 @@ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__
|
|
|
6276
6430
|
attempts: 0,
|
|
6277
6431
|
nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6278
6432
|
});
|
|
6279
|
-
|
|
6433
|
+
rmSync9(path, { force: true });
|
|
6280
6434
|
sent += 1;
|
|
6281
6435
|
log.emit(`sent a queued Agent Log report (${f})`);
|
|
6282
6436
|
} catch (e) {
|
|
6283
6437
|
if (!(e instanceof AgentLogUnreachableError)) {
|
|
6284
|
-
|
|
6438
|
+
rmSync9(path, { force: true });
|
|
6285
6439
|
log.warn(`dropped a queued Agent Log report (${f}): ${e.message}`);
|
|
6286
6440
|
continue;
|
|
6287
6441
|
}
|
|
6288
6442
|
const attempts = q.attempts + 1;
|
|
6289
|
-
|
|
6443
|
+
writeFileSync11(path, JSON.stringify({
|
|
6290
6444
|
...q,
|
|
6291
6445
|
attempts,
|
|
6292
6446
|
warned: true,
|
|
@@ -6297,20 +6451,20 @@ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__
|
|
|
6297
6451
|
}
|
|
6298
6452
|
return sent;
|
|
6299
6453
|
}
|
|
6300
|
-
var countersDir = (stateDir) =>
|
|
6454
|
+
var countersDir = (stateDir) => join16(stateDir, "pending-ticket-counters");
|
|
6301
6455
|
function queueTicketCounters(stateDir, name, q) {
|
|
6302
|
-
|
|
6303
|
-
|
|
6456
|
+
mkdirSync10(countersDir(stateDir), { recursive: true });
|
|
6457
|
+
writeFileSync11(join16(countersDir(stateDir), `${name}.json`), JSON.stringify(q));
|
|
6304
6458
|
}
|
|
6305
6459
|
async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
6306
6460
|
const dir = countersDir(stateDir);
|
|
6307
|
-
if (!
|
|
6461
|
+
if (!existsSync15(dir)) return 0;
|
|
6308
6462
|
let sent = 0;
|
|
6309
6463
|
for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
6310
|
-
const path =
|
|
6464
|
+
const path = join16(dir, f);
|
|
6311
6465
|
let q;
|
|
6312
6466
|
try {
|
|
6313
|
-
q = JSON.parse(
|
|
6467
|
+
q = JSON.parse(readFileSync15(path, "utf8"));
|
|
6314
6468
|
} catch {
|
|
6315
6469
|
continue;
|
|
6316
6470
|
}
|
|
@@ -6319,17 +6473,17 @@ async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /
|
|
|
6319
6473
|
const apiKey = await keyFor(q.role);
|
|
6320
6474
|
if (!apiKey) continue;
|
|
6321
6475
|
await writeCounters({ ...q.target, apiKey }, q.write);
|
|
6322
|
-
|
|
6476
|
+
rmSync9(path, { force: true });
|
|
6323
6477
|
sent += 1;
|
|
6324
6478
|
log.emit(`added a queued run's spend to ${q.write.ticketReference ?? q.write.recordId} (${f})`);
|
|
6325
6479
|
} catch (e) {
|
|
6326
6480
|
if (!isRetryable(e)) {
|
|
6327
|
-
|
|
6481
|
+
rmSync9(path, { force: true });
|
|
6328
6482
|
log.warn(`dropped a queued ticket counter update (${f}): ${e.message}`);
|
|
6329
6483
|
continue;
|
|
6330
6484
|
}
|
|
6331
6485
|
const attempts = q.attempts + 1;
|
|
6332
|
-
|
|
6486
|
+
writeFileSync11(path, JSON.stringify({
|
|
6333
6487
|
...q,
|
|
6334
6488
|
attempts,
|
|
6335
6489
|
warned: true,
|
|
@@ -6532,8 +6686,8 @@ var HandoffTranscriptTracker = class {
|
|
|
6532
6686
|
};
|
|
6533
6687
|
|
|
6534
6688
|
// src/pending-handoff.ts
|
|
6535
|
-
import { existsSync as
|
|
6536
|
-
import { join as
|
|
6689
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync11, readdirSync as readdirSync7, readFileSync as readFileSync16, rmSync as rmSync10, writeFileSync as writeFileSync12 } from "fs";
|
|
6690
|
+
import { join as join19 } from "path";
|
|
6537
6691
|
|
|
6538
6692
|
// src/ticket-branch.ts
|
|
6539
6693
|
import { basename as basename6 } from "path";
|
|
@@ -7205,8 +7359,8 @@ var Tracker = class {
|
|
|
7205
7359
|
if (res.status === 404) return [];
|
|
7206
7360
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
7207
7361
|
rows = await res.json();
|
|
7208
|
-
} catch (
|
|
7209
|
-
this.onReadFailure?.(`reading ${type} events failed: ${
|
|
7362
|
+
} catch (err3) {
|
|
7363
|
+
this.onReadFailure?.(`reading ${type} events failed: ${err3 instanceof Error ? err3.message : String(err3)}`);
|
|
7210
7364
|
return [];
|
|
7211
7365
|
}
|
|
7212
7366
|
return rows.filter((e) => e.event_type === type).sort((a, b) => Date.parse(b.occurred_at ?? b.created_at ?? "") - Date.parse(a.occurred_at ?? a.created_at ?? ""));
|
|
@@ -7616,13 +7770,13 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
|
|
|
7616
7770
|
}
|
|
7617
7771
|
|
|
7618
7772
|
// src/run-worktree.ts
|
|
7619
|
-
import { existsSync as
|
|
7620
|
-
import { dirname as dirname7, join as
|
|
7773
|
+
import { existsSync as existsSync17 } from "fs";
|
|
7774
|
+
import { dirname as dirname7, join as join18 } from "path";
|
|
7621
7775
|
|
|
7622
7776
|
// src/ports.ts
|
|
7623
7777
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
7624
|
-
import { existsSync as
|
|
7625
|
-
import { join as
|
|
7778
|
+
import { existsSync as existsSync16 } from "fs";
|
|
7779
|
+
import { join as join17 } from "path";
|
|
7626
7780
|
var run = (cmd, args) => {
|
|
7627
7781
|
try {
|
|
7628
7782
|
return execFileSync4(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -7667,7 +7821,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
7667
7821
|
}
|
|
7668
7822
|
function worktreeExists(parent, prefix, n) {
|
|
7669
7823
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
7670
|
-
if (
|
|
7824
|
+
if (existsSync16(join17(parent, name, ".git"))) return true;
|
|
7671
7825
|
}
|
|
7672
7826
|
return false;
|
|
7673
7827
|
}
|
|
@@ -7750,7 +7904,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
7750
7904
|
const existing = worktreeForNumber(dir, num);
|
|
7751
7905
|
const located = locateBranchForTicket(dir, cfg, t, role);
|
|
7752
7906
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
7753
|
-
if (existing && !existing.prunable &&
|
|
7907
|
+
if (existing && !existing.prunable && existsSync17(existing.path)) {
|
|
7754
7908
|
const branch = existing.branch;
|
|
7755
7909
|
if (status(existing.path).length > 0) {
|
|
7756
7910
|
return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
|
|
@@ -7766,7 +7920,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
7766
7920
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
7767
7921
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
7768
7922
|
}
|
|
7769
|
-
const path =
|
|
7923
|
+
const path = join18(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
|
|
7770
7924
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
7771
7925
|
return { action: "cut-remote", path, branch: located.branch };
|
|
7772
7926
|
}
|
|
@@ -7820,7 +7974,7 @@ function dropEmptyRunBranch(dir, cfg, branch) {
|
|
|
7820
7974
|
if (remoteConfigured(dir, remote) && remoteBranchExists(dir, remote, branch)) return false;
|
|
7821
7975
|
if (countCommits(dir, `${baseRef}..${branch}`) !== 0) return false;
|
|
7822
7976
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
7823
|
-
if (wt &&
|
|
7977
|
+
if (wt && existsSync17(wt.path)) {
|
|
7824
7978
|
if (status(wt.path).length > 0) return false;
|
|
7825
7979
|
if (gitOk(dir, ["worktree", "remove", wt.path]) === null) return false;
|
|
7826
7980
|
}
|
|
@@ -7828,7 +7982,7 @@ function dropEmptyRunBranch(dir, cfg, branch) {
|
|
|
7828
7982
|
}
|
|
7829
7983
|
async function finishRunWorktree(dir, cfg, branch) {
|
|
7830
7984
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
7831
|
-
if (!wt || !
|
|
7985
|
+
if (!wt || !existsSync17(wt.path)) return { kind: "none" };
|
|
7832
7986
|
if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
|
|
7833
7987
|
if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
|
|
7834
7988
|
if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
|
|
@@ -7840,7 +7994,7 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
7840
7994
|
if (pids.length) await killGently(pids);
|
|
7841
7995
|
}
|
|
7842
7996
|
gitOk(dir, ["worktree", "remove", wt.path]);
|
|
7843
|
-
return
|
|
7997
|
+
return existsSync17(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
|
|
7844
7998
|
}
|
|
7845
7999
|
|
|
7846
8000
|
// src/handoff-guard.ts
|
|
@@ -8223,20 +8377,20 @@ async function guardRunStrays(route2, ship, tracker, role, workingId, since2, lo
|
|
|
8223
8377
|
|
|
8224
8378
|
// src/pending-handoff.ts
|
|
8225
8379
|
var PENDING_MAX_AGE_MS = 48 * 36e5;
|
|
8226
|
-
var pendingDirFor = (stateDir) =>
|
|
8227
|
-
var stateDirOfStream = (streamPath) =>
|
|
8380
|
+
var pendingDirFor = (stateDir) => join19(stateDir, "pending-handoffs");
|
|
8381
|
+
var stateDirOfStream = (streamPath) => join19(streamPath, "..", "..");
|
|
8228
8382
|
function writePendingHandoff(stateDir, rec) {
|
|
8229
8383
|
const dir = pendingDirFor(stateDir);
|
|
8230
|
-
|
|
8231
|
-
|
|
8384
|
+
mkdirSync11(dir, { recursive: true });
|
|
8385
|
+
writeFileSync12(join19(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
|
|
8232
8386
|
}
|
|
8233
8387
|
function listPendingHandoffs(stateDir, route2) {
|
|
8234
8388
|
const dir = pendingDirFor(stateDir);
|
|
8235
|
-
if (!
|
|
8389
|
+
if (!existsSync18(dir)) return [];
|
|
8236
8390
|
const out = [];
|
|
8237
8391
|
for (const f of readdirSync7(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
8238
8392
|
try {
|
|
8239
|
-
const rec = JSON.parse(
|
|
8393
|
+
const rec = JSON.parse(readFileSync16(join19(dir, f), "utf8"));
|
|
8240
8394
|
if (rec.version === 1 && rec.ticketRecordId && (!route2 || rec.route === route2)) out.push(rec);
|
|
8241
8395
|
} catch {
|
|
8242
8396
|
}
|
|
@@ -8244,7 +8398,7 @@ function listPendingHandoffs(stateDir, route2) {
|
|
|
8244
8398
|
return out;
|
|
8245
8399
|
}
|
|
8246
8400
|
function removePendingHandoff(stateDir, ticketRecordId) {
|
|
8247
|
-
|
|
8401
|
+
rmSync10(join19(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
|
|
8248
8402
|
}
|
|
8249
8403
|
function pendingHandoffFor(stateDir, ticketRecordId) {
|
|
8250
8404
|
return listPendingHandoffs(stateDir).find((r) => r.ticketRecordId === ticketRecordId);
|
|
@@ -8354,8 +8508,8 @@ function expandSkillMarkers(text, lookup2, opts = {}) {
|
|
|
8354
8508
|
}
|
|
8355
8509
|
|
|
8356
8510
|
// src/skills.ts
|
|
8357
|
-
import { readFileSync as
|
|
8358
|
-
import { join as
|
|
8511
|
+
import { readFileSync as readFileSync18, readdirSync as readdirSync8, existsSync as existsSync20 } from "fs";
|
|
8512
|
+
import { join as join21 } from "path";
|
|
8359
8513
|
import { parse as parseYaml } from "yaml";
|
|
8360
8514
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError3, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
8361
8515
|
|
|
@@ -8371,8 +8525,8 @@ ${additions}`;
|
|
|
8371
8525
|
}
|
|
8372
8526
|
|
|
8373
8527
|
// src/agents.ts
|
|
8374
|
-
import { readFileSync as
|
|
8375
|
-
import { join as
|
|
8528
|
+
import { readFileSync as readFileSync17, existsSync as existsSync19 } from "fs";
|
|
8529
|
+
import { join as join20 } from "path";
|
|
8376
8530
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError2, TablationApiError } from "@tablation/client";
|
|
8377
8531
|
var PERSONA_NAME = {
|
|
8378
8532
|
dev: "Developer",
|
|
@@ -8386,12 +8540,12 @@ var CREW_AGENT_COLUMN = "agent_id";
|
|
|
8386
8540
|
var AgentsSyncError = class extends Error {
|
|
8387
8541
|
};
|
|
8388
8542
|
function personaDefaultPrompt(promptsDir, role) {
|
|
8389
|
-
const briefPath =
|
|
8390
|
-
if (!
|
|
8391
|
-
if (SELF_CONTAINED_ROLES.has(role)) return
|
|
8392
|
-
const commonPath =
|
|
8393
|
-
if (!
|
|
8394
|
-
return `${
|
|
8543
|
+
const briefPath = join20(promptsDir, "personas", `lane-${role}.md`);
|
|
8544
|
+
if (!existsSync19(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
8545
|
+
if (SELF_CONTAINED_ROLES.has(role)) return readFileSync17(briefPath, "utf8");
|
|
8546
|
+
const commonPath = join20(promptsDir, "personas", "common.md");
|
|
8547
|
+
if (!existsSync19(commonPath)) return readFileSync17(briefPath, "utf8");
|
|
8548
|
+
return `${readFileSync17(commonPath, "utf8")}${readFileSync17(briefPath, "utf8")}`;
|
|
8395
8549
|
}
|
|
8396
8550
|
async function resolveAgentId(route2, role, opts) {
|
|
8397
8551
|
const cached = route2.resolved?.agentPersonas?.[role]?.agentId;
|
|
@@ -8838,7 +8992,7 @@ function describeCrewLink(o) {
|
|
|
8838
8992
|
var SkillSyncError = class extends Error {
|
|
8839
8993
|
};
|
|
8840
8994
|
function parseSkillFile(path) {
|
|
8841
|
-
const text =
|
|
8995
|
+
const text = readFileSync18(path, "utf8");
|
|
8842
8996
|
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
|
|
8843
8997
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
8844
8998
|
const frontmatter = match[1];
|
|
@@ -8851,9 +9005,9 @@ function parseSkillFile(path) {
|
|
|
8851
9005
|
return { name: meta.name, description: meta.description, prompt: body, file: path };
|
|
8852
9006
|
}
|
|
8853
9007
|
function listLocalSkills(promptsDir) {
|
|
8854
|
-
const dir =
|
|
8855
|
-
if (!
|
|
8856
|
-
return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(
|
|
9008
|
+
const dir = join21(promptsDir, "skills");
|
|
9009
|
+
if (!existsSync20(dir)) return [];
|
|
9010
|
+
return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join21(dir, f)));
|
|
8857
9011
|
}
|
|
8858
9012
|
async function syncSkills(route2, opts) {
|
|
8859
9013
|
if (!route2.resolved) {
|
|
@@ -9055,26 +9209,26 @@ var AgentError = class extends Error {
|
|
|
9055
9209
|
};
|
|
9056
9210
|
var AgentSpawnError = class extends AgentError {
|
|
9057
9211
|
};
|
|
9058
|
-
var digestPathFor = (stateDir, routeLabel, role, runId) =>
|
|
9212
|
+
var digestPathFor = (stateDir, routeLabel, role, runId) => join22(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
9059
9213
|
function planAgentRun(o) {
|
|
9060
|
-
const briefPath =
|
|
9061
|
-
if (!
|
|
9214
|
+
const briefPath = join22(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
9215
|
+
if (!existsSync21(briefPath)) {
|
|
9062
9216
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
9063
9217
|
}
|
|
9064
|
-
const commonPath =
|
|
9065
|
-
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) &&
|
|
9218
|
+
const commonPath = join22(o.route.promptsDir, "personas", "common.md");
|
|
9219
|
+
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync21(commonPath);
|
|
9066
9220
|
const now = o.now ?? (() => Date.now());
|
|
9067
9221
|
const routeLabel = routeSlug(o.route.route);
|
|
9068
9222
|
const ownDigest = o.runId ? digestPathFor(o.stateDir, routeLabel, o.role, o.runId) : void 0;
|
|
9069
|
-
const digestPath2 = ownDigest &&
|
|
9223
|
+
const digestPath2 = ownDigest && existsSync21(ownDigest) ? ownDigest : digestPathFor(o.stateDir, routeLabel, o.role);
|
|
9070
9224
|
let digest;
|
|
9071
9225
|
let ageSeconds;
|
|
9072
|
-
if (
|
|
9073
|
-
ageSeconds = Math.floor((now() -
|
|
9074
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
9226
|
+
if (existsSync21(digestPath2)) {
|
|
9227
|
+
ageSeconds = Math.floor((now() - statSync7(digestPath2).mtimeMs) / 1e3);
|
|
9228
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync19(digestPath2, "utf8");
|
|
9075
9229
|
}
|
|
9076
|
-
const common = usesCommon ?
|
|
9077
|
-
const brief =
|
|
9230
|
+
const common = usesCommon ? readFileSync19(commonPath, "utf8") : "";
|
|
9231
|
+
const brief = readFileSync19(briefPath, "utf8");
|
|
9078
9232
|
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
9079
9233
|
const effectiveBrief = appendAdditions(o.divergedPrompt ?? brief, o.personaAdditions);
|
|
9080
9234
|
const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
|
|
@@ -9095,8 +9249,8 @@ function planAgentRun(o) {
|
|
|
9095
9249
|
brief: expanded.text,
|
|
9096
9250
|
digest
|
|
9097
9251
|
});
|
|
9098
|
-
const promptSha =
|
|
9099
|
-
const streamsDir =
|
|
9252
|
+
const promptSha = createHash4("sha256").update(expanded.text).digest("hex");
|
|
9253
|
+
const streamsDir = join22(o.stateDir, "streams");
|
|
9100
9254
|
const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
|
|
9101
9255
|
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
9102
9256
|
const { toolchain, model } = seat;
|
|
@@ -9169,8 +9323,8 @@ function planAgentRun(o) {
|
|
|
9169
9323
|
digestAttached: digest !== void 0,
|
|
9170
9324
|
digestAgeSeconds: ageSeconds,
|
|
9171
9325
|
model,
|
|
9172
|
-
streamPath:
|
|
9173
|
-
eventsPath:
|
|
9326
|
+
streamPath: join22(streamsDir, `${base}.jsonl`),
|
|
9327
|
+
eventsPath: join22(streamsDir, `${base}.events.jsonl`),
|
|
9174
9328
|
ticket: o.ticket,
|
|
9175
9329
|
ticketRecordId: o.ticketRecordId ?? null,
|
|
9176
9330
|
repo: o.repo ?? null,
|
|
@@ -9201,7 +9355,7 @@ function describePlan(p) {
|
|
|
9201
9355
|
var pidPathFor = (streamPath) => `${streamPath}.pid`;
|
|
9202
9356
|
function readRunSidecar(streamPath) {
|
|
9203
9357
|
try {
|
|
9204
|
-
const sc = JSON.parse(
|
|
9358
|
+
const sc = JSON.parse(readFileSync19(pidPathFor(streamPath), "utf8"));
|
|
9205
9359
|
return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
|
|
9206
9360
|
} catch {
|
|
9207
9361
|
return null;
|
|
@@ -9237,7 +9391,7 @@ function identityOf(plan, adapterHarness) {
|
|
|
9237
9391
|
var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
|
|
9238
9392
|
function openSink(path) {
|
|
9239
9393
|
try {
|
|
9240
|
-
|
|
9394
|
+
mkdirSync12(dirname8(path), { recursive: true });
|
|
9241
9395
|
return createWriteStream(path, { flags: "a" });
|
|
9242
9396
|
} catch {
|
|
9243
9397
|
return void 0;
|
|
@@ -9419,7 +9573,7 @@ var RunObserver = class {
|
|
|
9419
9573
|
};
|
|
9420
9574
|
function writeRunMeta(id, startedAt, liveTicket, extra) {
|
|
9421
9575
|
try {
|
|
9422
|
-
|
|
9576
|
+
writeFileSync13(`${id.streamPath}.meta.json`, JSON.stringify({
|
|
9423
9577
|
route: id.route,
|
|
9424
9578
|
role: id.role,
|
|
9425
9579
|
hint: id.ticket,
|
|
@@ -9579,7 +9733,7 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9579
9733
|
return new Promise((resolve6, reject) => {
|
|
9580
9734
|
let streamFd;
|
|
9581
9735
|
try {
|
|
9582
|
-
|
|
9736
|
+
mkdirSync12(dirname8(plan.streamPath), { recursive: true });
|
|
9583
9737
|
streamFd = openSync3(plan.streamPath, "a");
|
|
9584
9738
|
} catch (e) {
|
|
9585
9739
|
void endSink(eventsSink);
|
|
@@ -9614,7 +9768,7 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9614
9768
|
promptSha: plan.promptSha
|
|
9615
9769
|
};
|
|
9616
9770
|
try {
|
|
9617
|
-
|
|
9771
|
+
writeFileSync13(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
|
|
9618
9772
|
} catch {
|
|
9619
9773
|
}
|
|
9620
9774
|
}
|
|
@@ -9625,10 +9779,10 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9625
9779
|
const tail3 = new FileTail(plan.streamPath);
|
|
9626
9780
|
const timer = setInterval(() => obs.feed(tail3.poll()), TAIL_POLL_MS);
|
|
9627
9781
|
let spawnFailed = false;
|
|
9628
|
-
child.on("error", (
|
|
9782
|
+
child.on("error", (err3) => {
|
|
9629
9783
|
spawnFailed = true;
|
|
9630
9784
|
clearInterval(timer);
|
|
9631
|
-
reject(new AgentSpawnError(`cannot run ${plan.bin}: ${
|
|
9785
|
+
reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err3.message}`));
|
|
9632
9786
|
});
|
|
9633
9787
|
child.on("close", async (code) => {
|
|
9634
9788
|
clearInterval(timer);
|
|
@@ -9639,8 +9793,8 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9639
9793
|
const { ms: ms3, handoffPending } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey, hintMoved);
|
|
9640
9794
|
resolve6({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {} });
|
|
9641
9795
|
});
|
|
9642
|
-
child.stdin?.on("error", (
|
|
9643
|
-
emit.warn(`agent closed stdin before the prompt was delivered: ${
|
|
9796
|
+
child.stdin?.on("error", (err3) => {
|
|
9797
|
+
emit.warn(`agent closed stdin before the prompt was delivered: ${err3.message}`, { step: "agent" });
|
|
9644
9798
|
});
|
|
9645
9799
|
child.stdin?.end(plan.prompt);
|
|
9646
9800
|
});
|
|
@@ -9680,7 +9834,7 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
|
9680
9834
|
}
|
|
9681
9835
|
|
|
9682
9836
|
// src/seat-keys.ts
|
|
9683
|
-
import {
|
|
9837
|
+
import { hostFromUrl as hostFromUrl2 } from "@tablation/client";
|
|
9684
9838
|
var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
|
|
9685
9839
|
function keyIsAdmin(route2) {
|
|
9686
9840
|
const role = route2.resolved?.role;
|
|
@@ -9699,7 +9853,7 @@ function slugFor(route2, agentId) {
|
|
|
9699
9853
|
function seatAgentId(route2, role) {
|
|
9700
9854
|
return route2.resolved?.agentPersonas?.[role]?.agentId;
|
|
9701
9855
|
}
|
|
9702
|
-
async function resolveReleaseKey(route2, getStore =
|
|
9856
|
+
async function resolveReleaseKey(route2, getStore = getSessionStore) {
|
|
9703
9857
|
const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route2), reason });
|
|
9704
9858
|
const agentId = route2.resolved?.releaseAgentId;
|
|
9705
9859
|
if (!agentId) return ship(keyIsAdmin(route2) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
|
|
@@ -9708,10 +9862,11 @@ async function resolveReleaseKey(route2, getStore = getSessionStore2) {
|
|
|
9708
9862
|
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
9709
9863
|
return ship(keyIsAdmin(route2) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
9710
9864
|
} catch (e) {
|
|
9711
|
-
|
|
9865
|
+
const reason = describeReadError(e);
|
|
9866
|
+
return { kind: "ship", apiKey: resolveApiKey(route2), reason, storeError: reason };
|
|
9712
9867
|
}
|
|
9713
9868
|
}
|
|
9714
|
-
async function resolveSeatKey(route2, role, getStore =
|
|
9869
|
+
async function resolveSeatKey(route2, role, getStore = getSessionStore) {
|
|
9715
9870
|
const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route2), reason });
|
|
9716
9871
|
if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
|
|
9717
9872
|
const agentId = seatAgentId(route2, role);
|
|
@@ -9721,12 +9876,13 @@ async function resolveSeatKey(route2, role, getStore = getSessionStore2) {
|
|
|
9721
9876
|
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
9722
9877
|
return ship(keyIsAdmin(route2) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
9723
9878
|
} catch (e) {
|
|
9724
|
-
|
|
9879
|
+
const reason = describeReadError(e);
|
|
9880
|
+
return { kind: "ship", apiKey: resolveApiKey(route2), reason, storeError: reason };
|
|
9725
9881
|
}
|
|
9726
9882
|
}
|
|
9727
9883
|
function safeStderrLine(e) {
|
|
9728
|
-
const
|
|
9729
|
-
const raw = typeof
|
|
9884
|
+
const err3 = e;
|
|
9885
|
+
const raw = typeof err3?.stderr === "string" && err3.stderr.trim() ? err3.stderr : typeof err3?.message === "string" ? err3.message : "";
|
|
9730
9886
|
for (const line of raw.split("\n")) {
|
|
9731
9887
|
const t = line.trim();
|
|
9732
9888
|
if (!t || /^Command failed:/i.test(t) || /(^|\s)-w(\s|$)/.test(t)) continue;
|
|
@@ -9735,6 +9891,8 @@ function safeStderrLine(e) {
|
|
|
9735
9891
|
return void 0;
|
|
9736
9892
|
}
|
|
9737
9893
|
function describeReadError(e) {
|
|
9894
|
+
const fsErr = e;
|
|
9895
|
+
if (typeof fsErr?.path === "string" && typeof fsErr.code === "string") return `key file store unreadable (${fsErr.code})`;
|
|
9738
9896
|
const code = e?.code;
|
|
9739
9897
|
const line = safeStderrLine(e);
|
|
9740
9898
|
const what = typeof code === "number" ? `security exited ${code}` : typeof code === "string" ? `security failed: ${code}` : void 0;
|
|
@@ -9763,7 +9921,7 @@ async function mintMissingSeatKeys(route2, o) {
|
|
|
9763
9921
|
];
|
|
9764
9922
|
let store;
|
|
9765
9923
|
try {
|
|
9766
|
-
store = (o.getStore ??
|
|
9924
|
+
store = (o.getStore ?? getSessionStore)();
|
|
9767
9925
|
} catch (e) {
|
|
9768
9926
|
return targets.map(({ role }) => ({
|
|
9769
9927
|
role,
|
|
@@ -9886,7 +10044,7 @@ async function revokeSeatKeys(route2, o) {
|
|
|
9886
10044
|
];
|
|
9887
10045
|
let store;
|
|
9888
10046
|
try {
|
|
9889
|
-
store = (o.getStore ??
|
|
10047
|
+
store = (o.getStore ?? getSessionStore)();
|
|
9890
10048
|
} catch (e) {
|
|
9891
10049
|
return targets.map(({ role }) => ({ role, action: "failed", detail: describeStoreError(e) }));
|
|
9892
10050
|
}
|
|
@@ -10117,7 +10275,7 @@ async function runSelftest(i, deps = realDeps) {
|
|
|
10117
10275
|
...adapter.runEnv(spec),
|
|
10118
10276
|
[KEY_VAR]: i.apiKey
|
|
10119
10277
|
};
|
|
10120
|
-
const cwd = mkdtempSync(
|
|
10278
|
+
const cwd = mkdtempSync(join23(tmpdir2(), "crew-selftest-"));
|
|
10121
10279
|
try {
|
|
10122
10280
|
const run2 = await deps.run({
|
|
10123
10281
|
bin: invocation.bin,
|
|
@@ -10139,7 +10297,7 @@ async function runSelftest(i, deps = realDeps) {
|
|
|
10139
10297
|
return finish({ ok: false, failure, detail, ...usage2 });
|
|
10140
10298
|
} finally {
|
|
10141
10299
|
try {
|
|
10142
|
-
|
|
10300
|
+
rmSync11(cwd, { recursive: true, force: true });
|
|
10143
10301
|
} catch {
|
|
10144
10302
|
}
|
|
10145
10303
|
}
|
|
@@ -10150,11 +10308,11 @@ function formatResult(r) {
|
|
|
10150
10308
|
const secs = `${(r.ms / 1e3).toFixed(1)}s`;
|
|
10151
10309
|
return r.ok ? `${r.toolchain}: PASS ${r.detail} (${secs}${tok})` : `${r.toolchain}: FAIL ${r.failure} \u2014 ${r.detail} (${secs})`;
|
|
10152
10310
|
}
|
|
10153
|
-
var fileFor2 = (stateDir) =>
|
|
10311
|
+
var fileFor2 = (stateDir) => join23(stateDir, "selftest.json");
|
|
10154
10312
|
function readSelftest(stateDir) {
|
|
10155
10313
|
try {
|
|
10156
10314
|
const p = fileFor2(stateDir);
|
|
10157
|
-
return
|
|
10315
|
+
return existsSync22(p) ? JSON.parse(readFileSync20(p, "utf8")) : {};
|
|
10158
10316
|
} catch {
|
|
10159
10317
|
return {};
|
|
10160
10318
|
}
|
|
@@ -10162,8 +10320,8 @@ function readSelftest(stateDir) {
|
|
|
10162
10320
|
function recordSelftest(stateDir, results) {
|
|
10163
10321
|
const all = readSelftest(stateDir);
|
|
10164
10322
|
for (const r of results) all[r.toolchain] = r;
|
|
10165
|
-
|
|
10166
|
-
|
|
10323
|
+
mkdirSync13(stateDir, { recursive: true });
|
|
10324
|
+
writeFileSync14(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
|
|
10167
10325
|
`);
|
|
10168
10326
|
}
|
|
10169
10327
|
function doctorSelftestLines(stored, toolchains) {
|
|
@@ -10175,7 +10333,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
10175
10333
|
}
|
|
10176
10334
|
|
|
10177
10335
|
// src/cli.ts
|
|
10178
|
-
import { resolve as resolve5, dirname as dirname22, join as
|
|
10336
|
+
import { resolve as resolve5, dirname as dirname22, join as join44, basename as basename13 } from "path";
|
|
10179
10337
|
import { fileURLToPath } from "url";
|
|
10180
10338
|
import { hostname as hostname2, homedir as homedir10 } from "os";
|
|
10181
10339
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
@@ -10220,6 +10378,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
10220
10378
|
"service",
|
|
10221
10379
|
"topic",
|
|
10222
10380
|
"assignee",
|
|
10381
|
+
"requested-by",
|
|
10223
10382
|
"issue",
|
|
10224
10383
|
"shell",
|
|
10225
10384
|
"resume"
|
|
@@ -10296,10 +10455,10 @@ function seats(roster) {
|
|
|
10296
10455
|
function holds(roster) {
|
|
10297
10456
|
return [...roster.values()].filter((m) => m.kind === "hold");
|
|
10298
10457
|
}
|
|
10299
|
-
function buildRoster(
|
|
10458
|
+
function buildRoster(configured2, crewRows) {
|
|
10300
10459
|
const rows = new Map(crewRows.map((r) => [r.id, r]));
|
|
10301
10460
|
const roster = /* @__PURE__ */ new Map();
|
|
10302
|
-
for (const c of
|
|
10461
|
+
for (const c of configured2) {
|
|
10303
10462
|
if (!c.id) continue;
|
|
10304
10463
|
const row = rows.get(c.id);
|
|
10305
10464
|
const name = (row?.name ?? "").trim();
|
|
@@ -10365,14 +10524,14 @@ function rosterMarkdown(roster, meId) {
|
|
|
10365
10524
|
}
|
|
10366
10525
|
|
|
10367
10526
|
// src/connect-resume.ts
|
|
10368
|
-
import { mkdirSync as
|
|
10369
|
-
import { dirname as dirname9, join as
|
|
10527
|
+
import { mkdirSync as mkdirSync14, readFileSync as readFileSync21, rmSync as rmSync12, writeFileSync as writeFileSync15 } from "fs";
|
|
10528
|
+
import { dirname as dirname9, join as join24 } from "path";
|
|
10370
10529
|
function lastSessionFile(stateDir) {
|
|
10371
|
-
return
|
|
10530
|
+
return join24(stateDir, "connect-session.json");
|
|
10372
10531
|
}
|
|
10373
10532
|
function readPointers(stateDir) {
|
|
10374
10533
|
try {
|
|
10375
|
-
const parsed = JSON.parse(
|
|
10534
|
+
const parsed = JSON.parse(readFileSync21(lastSessionFile(stateDir), "utf8"));
|
|
10376
10535
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
10377
10536
|
const out = {};
|
|
10378
10537
|
for (const [host, v] of Object.entries(parsed)) {
|
|
@@ -10386,8 +10545,8 @@ function readPointers(stateDir) {
|
|
|
10386
10545
|
}
|
|
10387
10546
|
function writePointers(stateDir, pointers) {
|
|
10388
10547
|
const file = lastSessionFile(stateDir);
|
|
10389
|
-
|
|
10390
|
-
|
|
10548
|
+
mkdirSync14(dirname9(file), { recursive: true });
|
|
10549
|
+
writeFileSync15(file, `${JSON.stringify(pointers, null, 2)}
|
|
10391
10550
|
`, { mode: 384 });
|
|
10392
10551
|
}
|
|
10393
10552
|
function rememberSession(stateDir, host, workspaceSlug) {
|
|
@@ -10404,7 +10563,7 @@ function forgetWorkspace(stateDir, host, workspaceSlug) {
|
|
|
10404
10563
|
const { [host]: _old, ...rest } = all;
|
|
10405
10564
|
const next = left.length ? { ...rest, [host]: left } : rest;
|
|
10406
10565
|
if (Object.keys(next).length) writePointers(stateDir, next);
|
|
10407
|
-
else
|
|
10566
|
+
else rmSync12(lastSessionFile(stateDir), { force: true });
|
|
10408
10567
|
} catch {
|
|
10409
10568
|
}
|
|
10410
10569
|
}
|
|
@@ -10444,7 +10603,7 @@ function forgetSession(stateDir, host) {
|
|
|
10444
10603
|
try {
|
|
10445
10604
|
const { [host]: _gone, ...rest } = readPointers(stateDir);
|
|
10446
10605
|
if (Object.keys(rest).length) writePointers(stateDir, rest);
|
|
10447
|
-
else
|
|
10606
|
+
else rmSync12(lastSessionFile(stateDir), { force: true });
|
|
10448
10607
|
} catch {
|
|
10449
10608
|
}
|
|
10450
10609
|
}
|
|
@@ -10472,15 +10631,15 @@ async function signOutSessions(o) {
|
|
|
10472
10631
|
}
|
|
10473
10632
|
|
|
10474
10633
|
// src/reset.ts
|
|
10475
|
-
import { existsSync as
|
|
10476
|
-
import { dirname as dirname10, join as
|
|
10634
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync15, readFileSync as readFileSync22, readdirSync as readdirSync9, rmSync as rmSync13, statSync as statSync8, writeFileSync as writeFileSync16 } from "fs";
|
|
10635
|
+
import { dirname as dirname10, join as join25 } from "path";
|
|
10477
10636
|
import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
|
|
10478
10637
|
function clonedCheckoutsFile(stateDir) {
|
|
10479
|
-
return
|
|
10638
|
+
return join25(stateDir, "cloned-checkouts.json");
|
|
10480
10639
|
}
|
|
10481
10640
|
function readClonedCheckouts(stateDir) {
|
|
10482
10641
|
try {
|
|
10483
|
-
const parsed = JSON.parse(
|
|
10642
|
+
const parsed = JSON.parse(readFileSync22(clonedCheckoutsFile(stateDir), "utf8"));
|
|
10484
10643
|
return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
|
|
10485
10644
|
} catch {
|
|
10486
10645
|
return [];
|
|
@@ -10490,8 +10649,8 @@ function recordClonedCheckout(stateDir, dir) {
|
|
|
10490
10649
|
const known = readClonedCheckouts(stateDir);
|
|
10491
10650
|
if (known.includes(dir)) return;
|
|
10492
10651
|
const file = clonedCheckoutsFile(stateDir);
|
|
10493
|
-
|
|
10494
|
-
|
|
10652
|
+
mkdirSync15(dirname10(file), { recursive: true });
|
|
10653
|
+
writeFileSync16(file, `${JSON.stringify([...known, dir], null, 2)}
|
|
10495
10654
|
`);
|
|
10496
10655
|
}
|
|
10497
10656
|
var mark = (dryRun) => dryRun ? "would-remove" : "removed";
|
|
@@ -10543,8 +10702,8 @@ async function runReset(o, fx) {
|
|
|
10543
10702
|
for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
|
|
10544
10703
|
}
|
|
10545
10704
|
steps.push(...removeState(o.stateDir, o.dryRun));
|
|
10546
|
-
if (
|
|
10547
|
-
if (!o.dryRun)
|
|
10705
|
+
if (existsSync23(o.configFile)) {
|
|
10706
|
+
if (!o.dryRun) rmSync13(o.configFile, { force: true });
|
|
10548
10707
|
steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
|
|
10549
10708
|
} else {
|
|
10550
10709
|
steps.push({ step: "config", target: o.configFile, action: "absent" });
|
|
@@ -10552,7 +10711,7 @@ async function runReset(o, fx) {
|
|
|
10552
10711
|
return steps;
|
|
10553
10712
|
}
|
|
10554
10713
|
function removeCheckout(dir, dryRun, fx) {
|
|
10555
|
-
if (!
|
|
10714
|
+
if (!existsSync23(dir)) return { step: "checkout", target: dir, action: "absent", detail: "already gone" };
|
|
10556
10715
|
const { dirty, linkedWorktrees } = fx.inspectCheckout(dir);
|
|
10557
10716
|
if (dirty.length) {
|
|
10558
10717
|
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" : ""})` };
|
|
@@ -10560,21 +10719,21 @@ function removeCheckout(dir, dryRun, fx) {
|
|
|
10560
10719
|
if (linkedWorktrees.length) {
|
|
10561
10720
|
return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
|
|
10562
10721
|
}
|
|
10563
|
-
if (!dryRun)
|
|
10722
|
+
if (!dryRun) rmSync13(dir, { recursive: true, force: true });
|
|
10564
10723
|
return { step: "checkout", target: dir, action: mark(dryRun) };
|
|
10565
10724
|
}
|
|
10566
10725
|
function removeState(stateDir, dryRun) {
|
|
10567
10726
|
const out = [];
|
|
10568
|
-
const targets = [
|
|
10727
|
+
const targets = [join25(stateDir, "resolved"), join25(stateDir, "keys"), clonedCheckoutsFile(stateDir), join25(stateDir, "connect-session.json"), join25(stateDir, "reload-requested"), join25(stateDir, "daemon.started")];
|
|
10569
10728
|
try {
|
|
10570
10729
|
for (const name of readdirSync9(stateDir)) {
|
|
10571
|
-
if (name.startsWith(".") &&
|
|
10730
|
+
if (name.startsWith(".") && statSync8(join25(stateDir, name)).isFile()) targets.push(join25(stateDir, name));
|
|
10572
10731
|
}
|
|
10573
10732
|
} catch {
|
|
10574
10733
|
}
|
|
10575
10734
|
for (const t of targets) {
|
|
10576
|
-
if (!
|
|
10577
|
-
if (!dryRun)
|
|
10735
|
+
if (!existsSync23(t)) continue;
|
|
10736
|
+
if (!dryRun) rmSync13(t, { recursive: true, force: true });
|
|
10578
10737
|
out.push({ step: "state", target: t, action: mark(dryRun) });
|
|
10579
10738
|
}
|
|
10580
10739
|
if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
|
|
@@ -10648,12 +10807,12 @@ async function checkCrewFloor(o, deps = {}) {
|
|
|
10648
10807
|
function openOutdated(state) {
|
|
10649
10808
|
return state.shipAttention().previous().find((i) => i.key === CREW_OUTDATED_KEY)?.message ?? null;
|
|
10650
10809
|
}
|
|
10651
|
-
function isUpgradeRequired(
|
|
10652
|
-
return
|
|
10810
|
+
function isUpgradeRequired(err3) {
|
|
10811
|
+
return err3 instanceof TablationApiError3 && err3.status === 426;
|
|
10653
10812
|
}
|
|
10654
|
-
async function raiseFromUpgradeRequired(o,
|
|
10813
|
+
async function raiseFromUpgradeRequired(o, err3) {
|
|
10655
10814
|
const have = sentVersion(o.ship) ?? "unknown";
|
|
10656
|
-
const floor = /(\d+\.\d+\.\d+)/.exec(
|
|
10815
|
+
const floor = /(\d+\.\d+\.\d+)/.exec(err3.message.replace(have, ""))?.[1] ?? "a newer version";
|
|
10657
10816
|
await raiseShipAttention(o, outdatedItem(have, floor));
|
|
10658
10817
|
}
|
|
10659
10818
|
|
|
@@ -11361,8 +11520,8 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
|
|
|
11361
11520
|
}
|
|
11362
11521
|
|
|
11363
11522
|
// src/poll.ts
|
|
11364
|
-
import { writeFileSync as
|
|
11365
|
-
import { join as
|
|
11523
|
+
import { writeFileSync as writeFileSync17 } from "fs";
|
|
11524
|
+
import { join as join26 } from "path";
|
|
11366
11525
|
|
|
11367
11526
|
// src/stalled.ts
|
|
11368
11527
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -11468,8 +11627,8 @@ async function decideCycle(o) {
|
|
|
11468
11627
|
const optional = async (what, read, fallback) => {
|
|
11469
11628
|
try {
|
|
11470
11629
|
return await read;
|
|
11471
|
-
} catch (
|
|
11472
|
-
emit.warn(`reading ${what} failed: ${
|
|
11630
|
+
} catch (err3) {
|
|
11631
|
+
emit.warn(`reading ${what} failed: ${err3 instanceof Error ? err3.message : String(err3)}`);
|
|
11473
11632
|
return fallback;
|
|
11474
11633
|
}
|
|
11475
11634
|
};
|
|
@@ -11669,7 +11828,7 @@ async function decideCycle(o) {
|
|
|
11669
11828
|
}
|
|
11670
11829
|
return decision;
|
|
11671
11830
|
}
|
|
11672
|
-
var digestPath = (stateDir, route2, role, runId) =>
|
|
11831
|
+
var digestPath = (stateDir, route2, role, runId) => join26(stateDir, `digest-${route2}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
11673
11832
|
function otherRunsRemoved(tickets, own) {
|
|
11674
11833
|
if (!own) return tickets;
|
|
11675
11834
|
return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
|
|
@@ -11747,7 +11906,7 @@ function writeDigest(o, d, role, stateDir, runId, own) {
|
|
|
11747
11906
|
|
|
11748
11907
|
${body}` : body;
|
|
11749
11908
|
const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
|
|
11750
|
-
|
|
11909
|
+
writeFileSync17(path, text);
|
|
11751
11910
|
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
|
|
11752
11911
|
data: { role, bytes: Buffer.byteLength(text) }
|
|
11753
11912
|
});
|
|
@@ -12230,13 +12389,13 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
|
|
|
12230
12389
|
var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
|
|
12231
12390
|
|
|
12232
12391
|
// src/connect-wizard.ts
|
|
12233
|
-
import { delimiter, dirname as dirname11, join as
|
|
12234
|
-
import { accessSync as
|
|
12392
|
+
import { delimiter, dirname as dirname11, join as join27 } from "path";
|
|
12393
|
+
import { accessSync as accessSync3, constants as constants3, existsSync as existsSync24, mkdirSync as mkdirSync16, writeFileSync as writeFileSync18 } from "fs";
|
|
12235
12394
|
import { stringify } from "yaml";
|
|
12236
12395
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
12237
12396
|
function shouldRunWizard(opts) {
|
|
12238
12397
|
if (!opts.isTTY) return false;
|
|
12239
|
-
const exists = opts.configExists ??
|
|
12398
|
+
const exists = opts.configExists ?? existsSync24;
|
|
12240
12399
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
12241
12400
|
}
|
|
12242
12401
|
function firstRunConfigPath() {
|
|
@@ -12244,7 +12403,7 @@ function firstRunConfigPath() {
|
|
|
12244
12403
|
}
|
|
12245
12404
|
function defaultIsExecutable(p) {
|
|
12246
12405
|
try {
|
|
12247
|
-
|
|
12406
|
+
accessSync3(p, constants3.X_OK);
|
|
12248
12407
|
return true;
|
|
12249
12408
|
} catch {
|
|
12250
12409
|
return false;
|
|
@@ -12253,7 +12412,7 @@ function defaultIsExecutable(p) {
|
|
|
12253
12412
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
12254
12413
|
for (const dir of pathEnv.split(delimiter)) {
|
|
12255
12414
|
if (!dir) continue;
|
|
12256
|
-
const candidate =
|
|
12415
|
+
const candidate = join27(dir, name);
|
|
12257
12416
|
if (isExecutable(candidate)) return candidate;
|
|
12258
12417
|
}
|
|
12259
12418
|
return void 0;
|
|
@@ -12314,9 +12473,9 @@ routes:
|
|
|
12314
12473
|
${routeBlock}`;
|
|
12315
12474
|
}
|
|
12316
12475
|
function writeNewConfig(path, text) {
|
|
12317
|
-
|
|
12476
|
+
mkdirSync16(dirname11(path), { recursive: true });
|
|
12318
12477
|
try {
|
|
12319
|
-
|
|
12478
|
+
writeFileSync18(path, text, { mode: 384, flag: "wx" });
|
|
12320
12479
|
return true;
|
|
12321
12480
|
} catch (e) {
|
|
12322
12481
|
if (e.code === "EEXIST") return false;
|
|
@@ -12348,9 +12507,9 @@ Wrote ${o.configPath}.
|
|
|
12348
12507
|
}
|
|
12349
12508
|
|
|
12350
12509
|
// src/connect-json.ts
|
|
12351
|
-
import { accessSync as
|
|
12510
|
+
import { accessSync as accessSync4, constants as constants4 } from "fs";
|
|
12352
12511
|
import { homedir as homedir8 } from "os";
|
|
12353
|
-
import { dirname as dirname12, join as
|
|
12512
|
+
import { dirname as dirname12, join as join28 } from "path";
|
|
12354
12513
|
import { delimiter as delimiter2 } from "path";
|
|
12355
12514
|
var CHOICE_EXIT_CODE = 3;
|
|
12356
12515
|
function createEmitter(json, write) {
|
|
@@ -12389,12 +12548,12 @@ function agentBinSearchDirs(home, pathEnv, extraPath) {
|
|
|
12389
12548
|
const dirs = [
|
|
12390
12549
|
...pathEnv.split(delimiter2),
|
|
12391
12550
|
...(extraPath ?? "").split(delimiter2),
|
|
12392
|
-
|
|
12393
|
-
|
|
12551
|
+
join28(home, ".local/bin"),
|
|
12552
|
+
join28(home, ".claude/local"),
|
|
12394
12553
|
"/opt/homebrew/bin",
|
|
12395
12554
|
"/usr/local/bin",
|
|
12396
|
-
|
|
12397
|
-
|
|
12555
|
+
join28(home, ".npm-global/bin"),
|
|
12556
|
+
join28(home, ".volta/bin")
|
|
12398
12557
|
];
|
|
12399
12558
|
return [...new Set(dirs.filter(Boolean))];
|
|
12400
12559
|
}
|
|
@@ -12405,7 +12564,7 @@ function findAgentBin(name, env, extraPath, isExec) {
|
|
|
12405
12564
|
function flagWizardAnswers(f, env) {
|
|
12406
12565
|
const isExec = env.isExecutable ?? ((p) => {
|
|
12407
12566
|
try {
|
|
12408
|
-
|
|
12567
|
+
accessSync4(p, constants4.X_OK);
|
|
12409
12568
|
return true;
|
|
12410
12569
|
} catch {
|
|
12411
12570
|
return false;
|
|
@@ -12430,8 +12589,8 @@ function flagWizardAnswers(f, env) {
|
|
|
12430
12589
|
}
|
|
12431
12590
|
|
|
12432
12591
|
// src/release-run.ts
|
|
12433
|
-
import { readFileSync as
|
|
12434
|
-
import { dirname as dirname13, join as
|
|
12592
|
+
import { readFileSync as readFileSync23, writeFileSync as writeFileSync19, existsSync as existsSync25, copyFileSync, mkdirSync as mkdirSync17, mkdtempSync as mkdtempSync2, rmSync as rmSync14 } from "fs";
|
|
12593
|
+
import { dirname as dirname13, join as join29 } from "path";
|
|
12435
12594
|
import { tmpdir as tmpdir3 } from "os";
|
|
12436
12595
|
|
|
12437
12596
|
// src/release.ts
|
|
@@ -13172,13 +13331,13 @@ async function bump(o, size) {
|
|
|
13172
13331
|
}
|
|
13173
13332
|
return printed;
|
|
13174
13333
|
}
|
|
13175
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
13334
|
+
const files = o.repo.release.versionFiles.map((f) => join29(o.cwd, f));
|
|
13176
13335
|
const first = files[0];
|
|
13177
|
-
if (!
|
|
13336
|
+
if (!existsSync25(first)) {
|
|
13178
13337
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
13179
13338
|
return null;
|
|
13180
13339
|
}
|
|
13181
|
-
const current = JSON.parse(
|
|
13340
|
+
const current = JSON.parse(readFileSync23(first, "utf8")).version;
|
|
13182
13341
|
if (!current) {
|
|
13183
13342
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
13184
13343
|
return null;
|
|
@@ -13191,27 +13350,27 @@ async function bump(o, size) {
|
|
|
13191
13350
|
return version;
|
|
13192
13351
|
}
|
|
13193
13352
|
for (const f of files) {
|
|
13194
|
-
const src =
|
|
13353
|
+
const src = readFileSync23(f, "utf8");
|
|
13195
13354
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
13196
13355
|
if (out === src) {
|
|
13197
13356
|
o.emit.error(`no version field in ${f}`);
|
|
13198
13357
|
return null;
|
|
13199
13358
|
}
|
|
13200
|
-
|
|
13359
|
+
writeFileSync19(f, out);
|
|
13201
13360
|
}
|
|
13202
13361
|
return version;
|
|
13203
13362
|
}
|
|
13204
13363
|
function writeChangelog(o, version, entries) {
|
|
13205
13364
|
const file = o.repo.release.changelog;
|
|
13206
13365
|
if (!file || entries.length === 0) return;
|
|
13207
|
-
const path =
|
|
13366
|
+
const path = join29(o.cwd, file);
|
|
13208
13367
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
13209
13368
|
if (o.dryRun) {
|
|
13210
13369
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
13211
13370
|
return;
|
|
13212
13371
|
}
|
|
13213
|
-
const existing =
|
|
13214
|
-
|
|
13372
|
+
const existing = existsSync25(path) ? readFileSync23(path, "utf8") : "# Changelog\n";
|
|
13373
|
+
writeFileSync19(path, insertChangelogSection(existing, section));
|
|
13215
13374
|
}
|
|
13216
13375
|
async function confirm(o, expected) {
|
|
13217
13376
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -13294,7 +13453,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
13294
13453
|
run2(cwd, ["worktree", "remove", "--force", dir]);
|
|
13295
13454
|
} finally {
|
|
13296
13455
|
try {
|
|
13297
|
-
|
|
13456
|
+
rmSync14(root, { recursive: true, force: true });
|
|
13298
13457
|
} finally {
|
|
13299
13458
|
run2(cwd, ["worktree", "prune"]);
|
|
13300
13459
|
run2(cwd, ["branch", "-D", branch]);
|
|
@@ -13304,8 +13463,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
13304
13463
|
function openReleaseCheckout(o) {
|
|
13305
13464
|
const { remote, base } = o.repo.branch;
|
|
13306
13465
|
const branch = `crew/release-${Date.now()}`;
|
|
13307
|
-
const root = mkdtempSync2(
|
|
13308
|
-
const dir =
|
|
13466
|
+
const root = mkdtempSync2(join29(tmpdir3(), "crew-release-"));
|
|
13467
|
+
const dir = join29(root, "checkout");
|
|
13309
13468
|
o.emit.emit(`release checkout: ${dir}`, { step: "release" });
|
|
13310
13469
|
const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
|
|
13311
13470
|
try {
|
|
@@ -13315,10 +13474,10 @@ function openReleaseCheckout(o) {
|
|
|
13315
13474
|
throw e;
|
|
13316
13475
|
}
|
|
13317
13476
|
for (const f of o.repo.worktrees.copy) {
|
|
13318
|
-
const src =
|
|
13319
|
-
if (!
|
|
13320
|
-
|
|
13321
|
-
copyFileSync(src,
|
|
13477
|
+
const src = join29(o.cwd, f);
|
|
13478
|
+
if (!existsSync25(src)) continue;
|
|
13479
|
+
mkdirSync17(dirname13(join29(dir, f)), { recursive: true });
|
|
13480
|
+
copyFileSync(src, join29(dir, f));
|
|
13322
13481
|
}
|
|
13323
13482
|
return { dir, branch, close };
|
|
13324
13483
|
}
|
|
@@ -14019,12 +14178,12 @@ Passed: ${passed}.${willClose ? " Closing this alert." : ""}`,
|
|
|
14019
14178
|
}
|
|
14020
14179
|
|
|
14021
14180
|
// src/live-runs.ts
|
|
14022
|
-
import { existsSync as
|
|
14023
|
-
import { join as
|
|
14181
|
+
import { existsSync as existsSync27, readdirSync as readdirSync11 } from "fs";
|
|
14182
|
+
import { join as join31 } from "path";
|
|
14024
14183
|
|
|
14025
14184
|
// src/interrupted.ts
|
|
14026
|
-
import { existsSync as
|
|
14027
|
-
import { join as
|
|
14185
|
+
import { existsSync as existsSync26, readdirSync as readdirSync10, readFileSync as readFileSync24 } from "fs";
|
|
14186
|
+
import { join as join30 } from "path";
|
|
14028
14187
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
14029
14188
|
function interruptedComment(b) {
|
|
14030
14189
|
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.";
|
|
@@ -14043,21 +14202,21 @@ var pidAlive = (pid) => {
|
|
|
14043
14202
|
};
|
|
14044
14203
|
function runFinished(streamPath) {
|
|
14045
14204
|
const path = `${streamPath}.meta.json`;
|
|
14046
|
-
if (!
|
|
14205
|
+
if (!existsSync26(path)) return false;
|
|
14047
14206
|
try {
|
|
14048
|
-
return JSON.parse(
|
|
14207
|
+
return JSON.parse(readFileSync24(path, "utf8")).running !== true;
|
|
14049
14208
|
} catch {
|
|
14050
14209
|
return true;
|
|
14051
14210
|
}
|
|
14052
14211
|
}
|
|
14053
14212
|
function orphanedStreams(o) {
|
|
14054
|
-
const dir =
|
|
14055
|
-
if (!
|
|
14213
|
+
const dir = join30(o.stateDir, "streams");
|
|
14214
|
+
if (!existsSync26(dir)) return [];
|
|
14056
14215
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
14057
14216
|
const out = [];
|
|
14058
14217
|
for (const name of readdirSync10(dir)) {
|
|
14059
14218
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
14060
|
-
const streamPath =
|
|
14219
|
+
const streamPath = join30(dir, name);
|
|
14061
14220
|
if (runFinished(streamPath)) continue;
|
|
14062
14221
|
const sidecar = readRunSidecar(streamPath);
|
|
14063
14222
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -14111,12 +14270,12 @@ async function reconcileRuns(o) {
|
|
|
14111
14270
|
|
|
14112
14271
|
// src/live-runs.ts
|
|
14113
14272
|
function liveRuns(stateDir, isAlive = runIsAlive) {
|
|
14114
|
-
const dir =
|
|
14115
|
-
if (!
|
|
14273
|
+
const dir = join31(stateDir, "streams");
|
|
14274
|
+
if (!existsSync27(dir)) return [];
|
|
14116
14275
|
const out = [];
|
|
14117
14276
|
for (const name of readdirSync11(dir)) {
|
|
14118
14277
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
14119
|
-
const streamPath =
|
|
14278
|
+
const streamPath = join31(dir, name);
|
|
14120
14279
|
if (runFinished(streamPath)) continue;
|
|
14121
14280
|
const sidecar = readRunSidecar(streamPath);
|
|
14122
14281
|
if (!sidecar || !isAlive(sidecar)) continue;
|
|
@@ -14450,7 +14609,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
14450
14609
|
}
|
|
14451
14610
|
|
|
14452
14611
|
// src/watch.ts
|
|
14453
|
-
import { watch as watchFile, existsSync as
|
|
14612
|
+
import { watch as watchFile, existsSync as existsSync28, statSync as statSync9, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
14454
14613
|
var E = "\x1B[";
|
|
14455
14614
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
14456
14615
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -14551,8 +14710,8 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
14551
14710
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
14552
14711
|
}
|
|
14553
14712
|
function readFrom(file, from) {
|
|
14554
|
-
if (!
|
|
14555
|
-
const size =
|
|
14713
|
+
if (!existsSync28(file)) return { events: [], offset: from };
|
|
14714
|
+
const size = statSync9(file).size;
|
|
14556
14715
|
if (size < from) return readFrom(file, 0);
|
|
14557
14716
|
if (size === from) return { events: [], offset: from };
|
|
14558
14717
|
const fd = openSync4(file, "r");
|
|
@@ -14794,19 +14953,19 @@ function nextRoles(decision, ran) {
|
|
|
14794
14953
|
}
|
|
14795
14954
|
|
|
14796
14955
|
// src/ship-name.ts
|
|
14797
|
-
import { existsSync as
|
|
14956
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync26, renameSync as renameSync3, writeFileSync as writeFileSync21 } from "fs";
|
|
14798
14957
|
import { dirname as dirname15 } from "path";
|
|
14799
14958
|
import { parseDocument as parseDocument2 } from "yaml";
|
|
14800
14959
|
|
|
14801
14960
|
// src/machine-id.ts
|
|
14802
14961
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
14803
|
-
import { createHash as
|
|
14804
|
-
import { existsSync as
|
|
14805
|
-
import { dirname as dirname14, join as
|
|
14962
|
+
import { createHash as createHash5 } from "crypto";
|
|
14963
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync25, writeFileSync as writeFileSync20 } from "fs";
|
|
14964
|
+
import { dirname as dirname14, join as join32 } from "path";
|
|
14806
14965
|
function machineIdPathFor(stateDir) {
|
|
14807
|
-
return
|
|
14966
|
+
return join32(stateDir, "machine-id");
|
|
14808
14967
|
}
|
|
14809
|
-
var sha256 = (s) =>
|
|
14968
|
+
var sha256 = (s) => createHash5("sha256").update(s).digest("hex");
|
|
14810
14969
|
function readRawMachineId(platform2 = process.platform) {
|
|
14811
14970
|
try {
|
|
14812
14971
|
if (platform2 === "darwin") {
|
|
@@ -14817,8 +14976,8 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
14817
14976
|
return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
|
|
14818
14977
|
}
|
|
14819
14978
|
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
14820
|
-
if (
|
|
14821
|
-
const id =
|
|
14979
|
+
if (existsSync29(p)) {
|
|
14980
|
+
const id = readFileSync25(p, "utf8").trim();
|
|
14822
14981
|
if (id) return id;
|
|
14823
14982
|
}
|
|
14824
14983
|
}
|
|
@@ -14829,15 +14988,15 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
14829
14988
|
function ensureMachineId(stateDir, opts = {}) {
|
|
14830
14989
|
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
14831
14990
|
const path = machineIdPathFor(stateDir);
|
|
14832
|
-
if (
|
|
14833
|
-
const cached =
|
|
14991
|
+
if (existsSync29(path)) {
|
|
14992
|
+
const cached = readFileSync25(path, "utf8").trim();
|
|
14834
14993
|
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
14835
14994
|
}
|
|
14836
14995
|
const raw = (opts.read ?? readRawMachineId)();
|
|
14837
14996
|
if (!raw) return void 0;
|
|
14838
14997
|
const hashed = sha256(raw);
|
|
14839
|
-
|
|
14840
|
-
|
|
14998
|
+
mkdirSync18(dirname14(path), { recursive: true });
|
|
14999
|
+
writeFileSync20(path, `${hashed}
|
|
14841
15000
|
`, { mode: 384 });
|
|
14842
15001
|
return hashed;
|
|
14843
15002
|
}
|
|
@@ -14908,34 +15067,34 @@ async function syncRouteName(i) {
|
|
|
14908
15067
|
return { status: "ok", ...base, action, name: i.local };
|
|
14909
15068
|
}
|
|
14910
15069
|
function readSyncedName(resolvedFile) {
|
|
14911
|
-
if (!
|
|
15070
|
+
if (!existsSync30(resolvedFile)) return void 0;
|
|
14912
15071
|
try {
|
|
14913
|
-
const v = JSON.parse(
|
|
15072
|
+
const v = JSON.parse(readFileSync26(resolvedFile, "utf8")).shipName;
|
|
14914
15073
|
return typeof v === "string" ? v : void 0;
|
|
14915
15074
|
} catch {
|
|
14916
15075
|
return void 0;
|
|
14917
15076
|
}
|
|
14918
15077
|
}
|
|
14919
15078
|
function writeSyncedName(resolvedFile, name) {
|
|
14920
|
-
if (!
|
|
15079
|
+
if (!existsSync30(resolvedFile)) return;
|
|
14921
15080
|
let raw;
|
|
14922
15081
|
try {
|
|
14923
|
-
raw = JSON.parse(
|
|
15082
|
+
raw = JSON.parse(readFileSync26(resolvedFile, "utf8"));
|
|
14924
15083
|
} catch {
|
|
14925
15084
|
return;
|
|
14926
15085
|
}
|
|
14927
15086
|
if (raw.shipName === name) return;
|
|
14928
15087
|
raw.shipName = name;
|
|
14929
|
-
|
|
15088
|
+
mkdirSync19(dirname15(resolvedFile), { recursive: true });
|
|
14930
15089
|
const tmp = `${resolvedFile}.tmp${process.pid}`;
|
|
14931
|
-
|
|
15090
|
+
writeFileSync21(tmp, `${JSON.stringify(raw, null, 2)}
|
|
14932
15091
|
`);
|
|
14933
|
-
|
|
15092
|
+
renameSync3(tmp, resolvedFile);
|
|
14934
15093
|
}
|
|
14935
15094
|
async function reconcileShipName(i) {
|
|
14936
15095
|
let local = i.ship.name;
|
|
14937
15096
|
try {
|
|
14938
|
-
local = shipNameOfYaml(
|
|
15097
|
+
local = shipNameOfYaml(readFileSync26(i.configFile, "utf8")) ?? local;
|
|
14939
15098
|
} catch {
|
|
14940
15099
|
}
|
|
14941
15100
|
const resolvedFile = resolvedPathFor(i.stateDir, i.route);
|
|
@@ -14959,8 +15118,8 @@ async function reconcileShipName(i) {
|
|
|
14959
15118
|
}
|
|
14960
15119
|
if (result.status !== "ok") return result;
|
|
14961
15120
|
if (result.action === "adopt" || result.action === "row-wins") {
|
|
14962
|
-
const text =
|
|
14963
|
-
|
|
15121
|
+
const text = readFileSync26(i.configFile, "utf8");
|
|
15122
|
+
writeFileSync21(i.configFile, setShipNameInYaml(text, result.name));
|
|
14964
15123
|
i.ship.name = result.name;
|
|
14965
15124
|
if (result.action === "row-wins") {
|
|
14966
15125
|
i.emit?.warn(
|
|
@@ -15558,7 +15717,6 @@ ${opts.area ? ` area: "${opts.area}"
|
|
|
15558
15717
|
|
|
15559
15718
|
// src/cli.ts
|
|
15560
15719
|
import {
|
|
15561
|
-
getSessionStore as getSessionStore3,
|
|
15562
15720
|
hostFromUrl as hostFromUrl4,
|
|
15563
15721
|
loginWithDeviceCode,
|
|
15564
15722
|
DeviceLoginError
|
|
@@ -15610,7 +15768,7 @@ function describeCapability(i) {
|
|
|
15610
15768
|
}
|
|
15611
15769
|
|
|
15612
15770
|
// src/agents-sync-run.ts
|
|
15613
|
-
import { existsSync as
|
|
15771
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync27, writeFileSync as writeFileSync22 } from "fs";
|
|
15614
15772
|
import { dirname as dirname16 } from "path";
|
|
15615
15773
|
async function runAgentsSync(target, o) {
|
|
15616
15774
|
const dryRun = o.dryRun ?? false;
|
|
@@ -15644,12 +15802,12 @@ async function runAgentsSync(target, o) {
|
|
|
15644
15802
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
15645
15803
|
if (!dryRun) {
|
|
15646
15804
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
15647
|
-
const raw =
|
|
15805
|
+
const raw = existsSync31(resolvedPath) ? JSON.parse(readFileSync27(resolvedPath, "utf8")) : {};
|
|
15648
15806
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
15649
15807
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
15650
15808
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
15651
|
-
|
|
15652
|
-
|
|
15809
|
+
mkdirSync20(dirname16(resolvedPath), { recursive: true });
|
|
15810
|
+
writeFileSync22(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
15653
15811
|
`);
|
|
15654
15812
|
if (target.resolved) {
|
|
15655
15813
|
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
@@ -15664,7 +15822,7 @@ async function runAgentsSync(target, o) {
|
|
|
15664
15822
|
}
|
|
15665
15823
|
|
|
15666
15824
|
// src/logbook.ts
|
|
15667
|
-
import { createHash as
|
|
15825
|
+
import { createHash as createHash6 } from "crypto";
|
|
15668
15826
|
import { TablationClient as TablationClient4 } from "@tablation/client";
|
|
15669
15827
|
var LogbookError = class extends Error {
|
|
15670
15828
|
};
|
|
@@ -15737,7 +15895,7 @@ async function showLogEntry(route2, entryId, opts = {}) {
|
|
|
15737
15895
|
};
|
|
15738
15896
|
}
|
|
15739
15897
|
const prompt = appendAdditions(match.prompt ?? "", match.prompt_additions);
|
|
15740
|
-
const actualSha =
|
|
15898
|
+
const actualSha = createHash6("sha256").update(prompt).digest("hex");
|
|
15741
15899
|
const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
|
|
15742
15900
|
return {
|
|
15743
15901
|
entry,
|
|
@@ -15753,7 +15911,7 @@ async function showLogEntry(route2, entryId, opts = {}) {
|
|
|
15753
15911
|
|
|
15754
15912
|
// src/repos-check.ts
|
|
15755
15913
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
15756
|
-
import { readFileSync as
|
|
15914
|
+
import { readFileSync as readFileSync28 } from "fs";
|
|
15757
15915
|
import { basename as basename8 } from "path";
|
|
15758
15916
|
var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
|
|
15759
15917
|
function checkHookSyntax(script, shell) {
|
|
@@ -15794,7 +15952,7 @@ function checkRepoSpec(dir, opts = {}) {
|
|
|
15794
15952
|
}
|
|
15795
15953
|
let parsed;
|
|
15796
15954
|
try {
|
|
15797
|
-
parsed = parseRepoConfig(
|
|
15955
|
+
parsed = parseRepoConfig(readFileSync28(file, "utf8"), file);
|
|
15798
15956
|
} catch (e) {
|
|
15799
15957
|
if (!(e instanceof RepoConfigError)) throw e;
|
|
15800
15958
|
return { ...empty, file, error: e.message, exitCode: 1 };
|
|
@@ -15851,7 +16009,7 @@ import { isMap as isMap2, parseDocument as parseDocument4, YAMLMap } from "yaml"
|
|
|
15851
16009
|
|
|
15852
16010
|
// src/repos-cmd.ts
|
|
15853
16011
|
import { homedir as homedir9 } from "os";
|
|
15854
|
-
import { basename as basename9, join as
|
|
16012
|
+
import { basename as basename9, join as join33, resolve as resolve3 } from "path";
|
|
15855
16013
|
import { isMap, isSeq, parseDocument as parseDocument3, stringify as stringify2 } from "yaml";
|
|
15856
16014
|
var ReposError = class extends Error {
|
|
15857
16015
|
};
|
|
@@ -16129,7 +16287,7 @@ function planRepoClone(i) {
|
|
|
16129
16287
|
}
|
|
16130
16288
|
const ship = doc.get("ship", true);
|
|
16131
16289
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
16132
|
-
const target = i.dir ? expandDir(i.dir, i.base) :
|
|
16290
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join33(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
16133
16291
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
|
|
16134
16292
|
return { ...add, remote, target };
|
|
16135
16293
|
}
|
|
@@ -16476,7 +16634,7 @@ async function runAgentSuggest(o) {
|
|
|
16476
16634
|
const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
|
|
16477
16635
|
const run2 = await new Promise((resolve6) => {
|
|
16478
16636
|
let out = "";
|
|
16479
|
-
let
|
|
16637
|
+
let err3 = "";
|
|
16480
16638
|
const child = spawn4(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
16481
16639
|
let settled = false;
|
|
16482
16640
|
const settle = (r) => {
|
|
@@ -16493,7 +16651,7 @@ async function runAgentSuggest(o) {
|
|
|
16493
16651
|
}
|
|
16494
16652
|
child.stdout.destroy();
|
|
16495
16653
|
child.stderr.destroy();
|
|
16496
|
-
settle({ code: null, out, err:
|
|
16654
|
+
settle({ code: null, out, err: err3, aborted: true });
|
|
16497
16655
|
};
|
|
16498
16656
|
if (signal.aborted) onAbort();
|
|
16499
16657
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -16501,10 +16659,10 @@ async function runAgentSuggest(o) {
|
|
|
16501
16659
|
out += d.toString();
|
|
16502
16660
|
});
|
|
16503
16661
|
child.stderr.on("data", (d) => {
|
|
16504
|
-
|
|
16662
|
+
err3 += d.toString();
|
|
16505
16663
|
});
|
|
16506
|
-
child.on("error", (e) => settle({ code: null, out, err:
|
|
16507
|
-
child.on("close", (code) => settle({ code, out, err:
|
|
16664
|
+
child.on("error", (e) => settle({ code: null, out, err: err3, spawnError: e, aborted: false }));
|
|
16665
|
+
child.on("close", (code) => settle({ code, out, err: err3, aborted: false }));
|
|
16508
16666
|
child.stdin.on("error", () => {
|
|
16509
16667
|
});
|
|
16510
16668
|
child.stdin.end(buildAgentPrompt(o.sniff));
|
|
@@ -16528,7 +16686,7 @@ ${run2.err}` });
|
|
|
16528
16686
|
}
|
|
16529
16687
|
|
|
16530
16688
|
// src/sample-ticket.ts
|
|
16531
|
-
import { basename as basename10, dirname as dirname18, join as
|
|
16689
|
+
import { basename as basename10, dirname as dirname18, join as join34 } from "path";
|
|
16532
16690
|
var SAMPLE_REPO = "crew-sample";
|
|
16533
16691
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
16534
16692
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -16551,8 +16709,8 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
16551
16709
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
16552
16710
|
`;
|
|
16553
16711
|
function sampleDir(existing, reposBase, workspace) {
|
|
16554
|
-
if (existing.length > 0) return
|
|
16555
|
-
return
|
|
16712
|
+
if (existing.length > 0) return join34(dirname18(existing[0]), SAMPLE_REPO);
|
|
16713
|
+
return join34(reposBase, workspace, SAMPLE_REPO);
|
|
16556
16714
|
}
|
|
16557
16715
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
16558
16716
|
function sampleDescription() {
|
|
@@ -16644,8 +16802,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
16644
16802
|
}
|
|
16645
16803
|
|
|
16646
16804
|
// src/worktree-sweep.ts
|
|
16647
|
-
import { existsSync as
|
|
16648
|
-
import { basename as basename11, dirname as dirname19, join as
|
|
16805
|
+
import { existsSync as existsSync32, readFileSync as readFileSync29, readdirSync as readdirSync12 } from "fs";
|
|
16806
|
+
import { basename as basename11, dirname as dirname19, join as join35, resolve as resolve4 } from "path";
|
|
16649
16807
|
function planWorktreeSweep(target, tickets, contract) {
|
|
16650
16808
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
16651
16809
|
const verified = contract.statuses.verified;
|
|
@@ -16722,7 +16880,7 @@ function siblingWorktreeDirs(target) {
|
|
|
16722
16880
|
for (const name of names) {
|
|
16723
16881
|
if (!name.startsWith(stem)) continue;
|
|
16724
16882
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
16725
|
-
const path =
|
|
16883
|
+
const path = join35(parent, name);
|
|
16726
16884
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
16727
16885
|
}
|
|
16728
16886
|
return out;
|
|
@@ -16757,7 +16915,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
16757
16915
|
let removed = 0;
|
|
16758
16916
|
let keptBranches = 0;
|
|
16759
16917
|
for (const a of actions) {
|
|
16760
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
16918
|
+
if (!dryRun && base && !a.keepBranch && existsSync32(a.path) && isHandBackMerge(a.path, base)) {
|
|
16761
16919
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
16762
16920
|
log.emit(
|
|
16763
16921
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -16765,7 +16923,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
16765
16923
|
);
|
|
16766
16924
|
}
|
|
16767
16925
|
}
|
|
16768
|
-
if (!dryRun &&
|
|
16926
|
+
if (!dryRun && existsSync32(a.path)) {
|
|
16769
16927
|
let dirty = false;
|
|
16770
16928
|
try {
|
|
16771
16929
|
dirty = status(a.path).length > 0;
|
|
@@ -16776,7 +16934,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
16776
16934
|
continue;
|
|
16777
16935
|
}
|
|
16778
16936
|
}
|
|
16779
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
16937
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync32(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
16780
16938
|
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
16781
16939
|
continue;
|
|
16782
16940
|
}
|
|
@@ -16825,7 +16983,7 @@ function isHandBackMerge(path, base) {
|
|
|
16825
16983
|
if (!msgPath) return false;
|
|
16826
16984
|
let first;
|
|
16827
16985
|
try {
|
|
16828
|
-
first =
|
|
16986
|
+
first = readFileSync29(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
16829
16987
|
} catch {
|
|
16830
16988
|
return false;
|
|
16831
16989
|
}
|
|
@@ -16930,16 +17088,16 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
16930
17088
|
}
|
|
16931
17089
|
|
|
16932
17090
|
// src/stream-sweep.ts
|
|
16933
|
-
import { existsSync as
|
|
16934
|
-
import { join as
|
|
17091
|
+
import { existsSync as existsSync33, readdirSync as readdirSync13, rmSync as rmSync15, statSync as statSync10 } from "fs";
|
|
17092
|
+
import { join as join36 } from "path";
|
|
16935
17093
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
16936
17094
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
16937
|
-
const dir =
|
|
16938
|
-
if (!
|
|
17095
|
+
const dir = join36(stateDir, "streams");
|
|
17096
|
+
if (!existsSync33(dir)) return [];
|
|
16939
17097
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
16940
|
-
return readdirSync13(dir).map((name) =>
|
|
17098
|
+
return readdirSync13(dir).map((name) => join36(dir, name)).filter((path) => {
|
|
16941
17099
|
try {
|
|
16942
|
-
return
|
|
17100
|
+
return statSync10(path).mtimeMs < cutoff;
|
|
16943
17101
|
} catch {
|
|
16944
17102
|
return false;
|
|
16945
17103
|
}
|
|
@@ -16953,7 +17111,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
16953
17111
|
continue;
|
|
16954
17112
|
}
|
|
16955
17113
|
try {
|
|
16956
|
-
|
|
17114
|
+
rmSync15(path, { force: true });
|
|
16957
17115
|
removed++;
|
|
16958
17116
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
16959
17117
|
} catch (e) {
|
|
@@ -16963,11 +17121,11 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
16963
17121
|
return removed;
|
|
16964
17122
|
}
|
|
16965
17123
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
16966
|
-
if (!
|
|
17124
|
+
if (!existsSync33(stateDir)) return [];
|
|
16967
17125
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
16968
|
-
return readdirSync13(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) =>
|
|
17126
|
+
return readdirSync13(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join36(stateDir, name)).filter((path) => {
|
|
16969
17127
|
try {
|
|
16970
|
-
return
|
|
17128
|
+
return statSync10(path).mtimeMs < cutoff;
|
|
16971
17129
|
} catch {
|
|
16972
17130
|
return false;
|
|
16973
17131
|
}
|
|
@@ -16975,30 +17133,30 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
16975
17133
|
}
|
|
16976
17134
|
|
|
16977
17135
|
// src/cli.ts
|
|
16978
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
17136
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync35, existsSync as existsSync40, mkdirSync as mkdirSync26, writeFileSync as writeFileSync27, rmSync as rmSync19, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync4, statSync as statSync14, readdirSync as readdirSync18 } from "fs";
|
|
16979
17137
|
import { createInterface } from "readline/promises";
|
|
16980
17138
|
|
|
16981
17139
|
// src/periscope-cmd.ts
|
|
16982
|
-
import { existsSync as
|
|
16983
|
-
import { join as
|
|
17140
|
+
import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync30, rmSync as rmSync16, writeFileSync as writeFileSync23 } from "fs";
|
|
17141
|
+
import { join as join37 } from "path";
|
|
16984
17142
|
var PeriscopeError = class extends Error {
|
|
16985
17143
|
};
|
|
16986
|
-
var priorDir = (stateDir) =>
|
|
16987
|
-
var priorPath = (stateDir, ticketRecordId) =>
|
|
17144
|
+
var priorDir = (stateDir) => join37(stateDir, "periscope-priors");
|
|
17145
|
+
var priorPath = (stateDir, ticketRecordId) => join37(priorDir(stateDir), `${ticketRecordId}.json`);
|
|
16988
17146
|
function removePeriscopePrior(stateDir, ticketRecordId) {
|
|
16989
|
-
|
|
17147
|
+
rmSync16(priorPath(stateDir, ticketRecordId), { force: true });
|
|
16990
17148
|
}
|
|
16991
17149
|
function writePeriscopePrior(stateDir, ticketRecordId, prior) {
|
|
16992
17150
|
try {
|
|
16993
|
-
|
|
16994
|
-
|
|
17151
|
+
mkdirSync21(priorDir(stateDir), { recursive: true });
|
|
17152
|
+
writeFileSync23(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
|
|
16995
17153
|
} catch {
|
|
16996
17154
|
}
|
|
16997
17155
|
}
|
|
16998
17156
|
function readPeriscopePrior(stateDir, ticketRecordId) {
|
|
16999
17157
|
try {
|
|
17000
17158
|
const p = priorPath(stateDir, ticketRecordId);
|
|
17001
|
-
return
|
|
17159
|
+
return existsSync34(p) ? JSON.parse(readFileSync30(p, "utf8")) : void 0;
|
|
17002
17160
|
} catch {
|
|
17003
17161
|
return void 0;
|
|
17004
17162
|
}
|
|
@@ -17006,7 +17164,7 @@ function readPeriscopePrior(stateDir, ticketRecordId) {
|
|
|
17006
17164
|
var DEFAULT_MAX_PERISCOPES2 = 3;
|
|
17007
17165
|
var DEFAULT_PERISCOPE_IDLE_MINUTES2 = 120;
|
|
17008
17166
|
function parsePeriscopeUpArgs(positional2, flags) {
|
|
17009
|
-
const usage2 = "usage: crew periscope up [[route] REPO] [--topic TEXT | --issue NNN] [--isolate] [--host HOST] [--assignee CREW_ROW_ID] [--no-open] [--json]";
|
|
17167
|
+
const usage2 = "usage: crew periscope up [[route] REPO] [--topic TEXT | --issue NNN] [--isolate] [--host HOST] [--assignee CREW_ROW_ID] [--requested-by ID] [--no-open] [--json]";
|
|
17010
17168
|
const rest = positional2.slice(2);
|
|
17011
17169
|
if (rest.length > 2) throw new PeriscopeError(usage2);
|
|
17012
17170
|
const topic = flags.topic?.trim();
|
|
@@ -17027,6 +17185,7 @@ ${usage2}`);
|
|
|
17027
17185
|
isolate: flags.isolate,
|
|
17028
17186
|
...flags.host ? { host: flags.host } : {},
|
|
17029
17187
|
...flags.assignee ? { assignee: flags.assignee } : {},
|
|
17188
|
+
...flags.requestedBy?.trim() ? { requestedBy: flags.requestedBy.trim() } : {},
|
|
17030
17189
|
json: flags.json
|
|
17031
17190
|
};
|
|
17032
17191
|
}
|
|
@@ -17390,8 +17549,8 @@ ${tail2(preview.output)}`);
|
|
|
17390
17549
|
|
|
17391
17550
|
// src/periscope-idle.ts
|
|
17392
17551
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
17393
|
-
import { statSync as
|
|
17394
|
-
import { join as
|
|
17552
|
+
import { statSync as statSync11 } from "fs";
|
|
17553
|
+
import { join as join38 } from "path";
|
|
17395
17554
|
var MAX_SCANNED_FILES = 2e4;
|
|
17396
17555
|
function newestWorktreeMtime(root) {
|
|
17397
17556
|
let listing;
|
|
@@ -17411,7 +17570,7 @@ function newestWorktreeMtime(root) {
|
|
|
17411
17570
|
if (!rel) continue;
|
|
17412
17571
|
if (++scanned > MAX_SCANNED_FILES) break;
|
|
17413
17572
|
try {
|
|
17414
|
-
const m =
|
|
17573
|
+
const m = statSync11(join38(root, rel)).mtimeMs;
|
|
17415
17574
|
if (newest === null || m > newest) newest = m;
|
|
17416
17575
|
} catch {
|
|
17417
17576
|
}
|
|
@@ -17487,18 +17646,18 @@ import { dirname as dirOf2, resolve as resolvePath } from "path";
|
|
|
17487
17646
|
|
|
17488
17647
|
// src/ssh-keys.ts
|
|
17489
17648
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
17490
|
-
import { existsSync as
|
|
17491
|
-
import { dirname as dirname20, join as
|
|
17649
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync22, readFileSync as readFileSync31, chmodSync as chmodSync3 } from "fs";
|
|
17650
|
+
import { dirname as dirname20, join as join39 } from "path";
|
|
17492
17651
|
var SshKeyError = class extends Error {
|
|
17493
17652
|
};
|
|
17494
17653
|
function shipSshKeyPathFor(stateDir) {
|
|
17495
|
-
return
|
|
17654
|
+
return join39(stateDir, "ssh", "id_ed25519");
|
|
17496
17655
|
}
|
|
17497
17656
|
function ensureShipSshKeypair(stateDir) {
|
|
17498
17657
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
17499
17658
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
17500
|
-
if (!
|
|
17501
|
-
|
|
17659
|
+
if (!existsSync35(privateKeyPath)) {
|
|
17660
|
+
mkdirSync22(dirname20(privateKeyPath), { recursive: true });
|
|
17502
17661
|
try {
|
|
17503
17662
|
execFileSync8("ssh-keygen", [
|
|
17504
17663
|
"-t",
|
|
@@ -17513,15 +17672,15 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
17513
17672
|
"-q"
|
|
17514
17673
|
], { stdio: ["ignore", "ignore", "pipe"] });
|
|
17515
17674
|
} catch (e) {
|
|
17516
|
-
const
|
|
17517
|
-
throw new SshKeyError(`ssh-keygen failed: ${
|
|
17675
|
+
const err3 = e;
|
|
17676
|
+
throw new SshKeyError(`ssh-keygen failed: ${err3.stderr?.toString().trim() || err3.message}`);
|
|
17518
17677
|
}
|
|
17519
|
-
|
|
17678
|
+
chmodSync3(privateKeyPath, 384);
|
|
17520
17679
|
}
|
|
17521
|
-
if (!
|
|
17680
|
+
if (!existsSync35(publicKeyPath)) {
|
|
17522
17681
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
17523
17682
|
}
|
|
17524
|
-
const publicKey =
|
|
17683
|
+
const publicKey = readFileSync31(publicKeyPath, "utf8").trim();
|
|
17525
17684
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
17526
17685
|
}
|
|
17527
17686
|
function sshKeygenAvailable() {
|
|
@@ -17535,14 +17694,14 @@ function sshKeygenAvailable() {
|
|
|
17535
17694
|
|
|
17536
17695
|
// src/passenger-containers.ts
|
|
17537
17696
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
17538
|
-
import { createHash as
|
|
17697
|
+
import { createHash as createHash7 } from "crypto";
|
|
17539
17698
|
var realExec = (cmd, args, opts) => {
|
|
17540
17699
|
try {
|
|
17541
17700
|
return execFileSync9(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
|
|
17542
17701
|
} catch (e) {
|
|
17543
|
-
const
|
|
17544
|
-
const why = [
|
|
17545
|
-
throw new Error(`${cmd} ${args.join(" ")}: ${why ||
|
|
17702
|
+
const err3 = e;
|
|
17703
|
+
const why = [err3.stderr, err3.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
|
|
17704
|
+
throw new Error(`${cmd} ${args.join(" ")}: ${why || err3.message.trim()}`);
|
|
17546
17705
|
}
|
|
17547
17706
|
};
|
|
17548
17707
|
var DOCKER_INFO_TIMEOUT_MS = 1e4;
|
|
@@ -17573,12 +17732,12 @@ function containerNameFor(workspaceId) {
|
|
|
17573
17732
|
return `crew-passenger-${workspaceId}`;
|
|
17574
17733
|
}
|
|
17575
17734
|
function portFor(workspaceId, base = 28765, span = 1e3) {
|
|
17576
|
-
const digest =
|
|
17735
|
+
const digest = createHash7("sha256").update(workspaceId).digest();
|
|
17577
17736
|
return base + digest.readUInt32BE(0) % span;
|
|
17578
17737
|
}
|
|
17579
17738
|
function mountsDigest(mounts, image) {
|
|
17580
17739
|
const sorted2 = [...mounts].sort((a, b) => a.containerPath.localeCompare(b.containerPath));
|
|
17581
|
-
return
|
|
17740
|
+
return createHash7("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
|
|
17582
17741
|
}
|
|
17583
17742
|
function planContainers(cfg) {
|
|
17584
17743
|
const byWorkspace = /* @__PURE__ */ new Map();
|
|
@@ -17738,9 +17897,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
17738
17897
|
|
|
17739
17898
|
// src/tunnel.ts
|
|
17740
17899
|
import { spawn as spawn5 } from "child_process";
|
|
17741
|
-
import { existsSync as
|
|
17900
|
+
import { existsSync as existsSync36, mkdirSync as mkdirSync23, readFileSync as readFileSync32, writeFileSync as writeFileSync24, unlinkSync as unlinkSync3, readdirSync as readdirSync14 } from "fs";
|
|
17742
17901
|
import { randomInt } from "crypto";
|
|
17743
|
-
import { join as
|
|
17902
|
+
import { join as join40 } from "path";
|
|
17744
17903
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
17745
17904
|
var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
17746
17905
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -17807,21 +17966,21 @@ function startTunnel(opts) {
|
|
|
17807
17966
|
};
|
|
17808
17967
|
}
|
|
17809
17968
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
17810
|
-
return
|
|
17969
|
+
return join40(stateDir, "passengers", `${workspaceId}.json`);
|
|
17811
17970
|
}
|
|
17812
17971
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
17813
17972
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
17814
|
-
if (!
|
|
17973
|
+
if (!existsSync36(path)) return void 0;
|
|
17815
17974
|
try {
|
|
17816
|
-
return JSON.parse(
|
|
17975
|
+
return JSON.parse(readFileSync32(path, "utf8"));
|
|
17817
17976
|
} catch {
|
|
17818
17977
|
return void 0;
|
|
17819
17978
|
}
|
|
17820
17979
|
}
|
|
17821
17980
|
function writePersistedTunnel(stateDir, state) {
|
|
17822
17981
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
17823
|
-
|
|
17824
|
-
|
|
17982
|
+
mkdirSync23(join40(stateDir, "passengers"), { recursive: true });
|
|
17983
|
+
writeFileSync24(path, `${JSON.stringify(state, null, 2)}
|
|
17825
17984
|
`);
|
|
17826
17985
|
}
|
|
17827
17986
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -17918,13 +18077,13 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
17918
18077
|
}
|
|
17919
18078
|
}
|
|
17920
18079
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
17921
|
-
const dir =
|
|
17922
|
-
if (!
|
|
18080
|
+
const dir = join40(stateDir, "passengers");
|
|
18081
|
+
if (!existsSync36(dir)) return [];
|
|
17923
18082
|
return readdirSync14(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
17924
18083
|
}
|
|
17925
18084
|
|
|
17926
18085
|
// src/mcp-endpoint-registry.ts
|
|
17927
|
-
import { createHash as
|
|
18086
|
+
import { createHash as createHash8 } from "crypto";
|
|
17928
18087
|
function endpointsUrl(api, workspaceId, id) {
|
|
17929
18088
|
const base = `${api.baseUrl.replace(/\/+$/, "")}/api/workspaces/${workspaceId}/mcp-endpoints`;
|
|
17930
18089
|
return id ? `${base}/${id}` : base;
|
|
@@ -17992,7 +18151,7 @@ async function probeEndpointAccess(api, workspaceId) {
|
|
|
17992
18151
|
}
|
|
17993
18152
|
var REGISTER_MCP_ENDPOINTS_HINT = "give this key's role the REGISTER_MCP_ENDPOINTS capability, e.g. via the group that holds the Crew project role";
|
|
17994
18153
|
function registrationKey(url, mcpSecret) {
|
|
17995
|
-
return
|
|
18154
|
+
return createHash8("sha256").update(`${url}
|
|
17996
18155
|
${mcpSecret ?? ""}`).digest("hex").slice(0, 16);
|
|
17997
18156
|
}
|
|
17998
18157
|
async function reconcileEndpoint(workspaceId, d) {
|
|
@@ -18040,8 +18199,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
18040
18199
|
import { createServer as createHttpServer } from "http";
|
|
18041
18200
|
import { spawn as spawn6 } from "child_process";
|
|
18042
18201
|
import { randomBytes } from "crypto";
|
|
18043
|
-
import { existsSync as
|
|
18044
|
-
import { join as
|
|
18202
|
+
import { existsSync as existsSync37, mkdirSync as mkdirSync24, readFileSync as readFileSync33, writeFileSync as writeFileSync25, unlinkSync as unlinkSync4, readdirSync as readdirSync15 } from "fs";
|
|
18203
|
+
import { join as join41 } from "path";
|
|
18045
18204
|
|
|
18046
18205
|
// src/secret-compare.ts
|
|
18047
18206
|
import { timingSafeEqual } from "crypto";
|
|
@@ -18115,8 +18274,8 @@ function runSyncDaemonFromEnv(env = process.env) {
|
|
|
18115
18274
|
const secret = requireEnv(env, "PASSENGER_SYNC_SECRET");
|
|
18116
18275
|
const targets = JSON.parse(requireEnv(env, "PASSENGER_SYNC_TARGETS"));
|
|
18117
18276
|
const server = createSyncDaemonServer(targets, secret);
|
|
18118
|
-
server.on("error", (
|
|
18119
|
-
console.error(`Passenger sync daemon failed to bind 0.0.0.0:${port}: ${
|
|
18277
|
+
server.on("error", (err3) => {
|
|
18278
|
+
console.error(`Passenger sync daemon failed to bind 0.0.0.0:${port}: ${err3 instanceof Error ? err3.message : String(err3)}`);
|
|
18120
18279
|
process.exit(1);
|
|
18121
18280
|
});
|
|
18122
18281
|
server.listen(port, "0.0.0.0", () => {
|
|
@@ -18130,21 +18289,21 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
18130
18289
|
return child;
|
|
18131
18290
|
};
|
|
18132
18291
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
18133
|
-
return
|
|
18292
|
+
return join41(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
18134
18293
|
}
|
|
18135
18294
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
18136
18295
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
18137
|
-
if (!
|
|
18296
|
+
if (!existsSync37(path)) return void 0;
|
|
18138
18297
|
try {
|
|
18139
|
-
return JSON.parse(
|
|
18298
|
+
return JSON.parse(readFileSync33(path, "utf8"));
|
|
18140
18299
|
} catch {
|
|
18141
18300
|
return void 0;
|
|
18142
18301
|
}
|
|
18143
18302
|
}
|
|
18144
18303
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
18145
18304
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
18146
|
-
|
|
18147
|
-
|
|
18305
|
+
mkdirSync24(join41(stateDir, "passengers"), { recursive: true });
|
|
18306
|
+
writeFileSync25(path, `${JSON.stringify(state, null, 2)}
|
|
18148
18307
|
`);
|
|
18149
18308
|
}
|
|
18150
18309
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -18164,8 +18323,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
18164
18323
|
}
|
|
18165
18324
|
}
|
|
18166
18325
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
18167
|
-
const dir =
|
|
18168
|
-
if (!
|
|
18326
|
+
const dir = join41(stateDir, "passengers");
|
|
18327
|
+
if (!existsSync37(dir)) return [];
|
|
18169
18328
|
const suffix = "-sync.json";
|
|
18170
18329
|
return readdirSync15(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
18171
18330
|
}
|
|
@@ -18233,28 +18392,28 @@ function codeOf(e) {
|
|
|
18233
18392
|
const code = e?.code;
|
|
18234
18393
|
return typeof code === "string" ? code : void 0;
|
|
18235
18394
|
}
|
|
18236
|
-
function isTransientError(
|
|
18237
|
-
if (!(
|
|
18238
|
-
const msg =
|
|
18395
|
+
function isTransientError(err3) {
|
|
18396
|
+
if (!(err3 instanceof Error)) return false;
|
|
18397
|
+
const msg = err3.message;
|
|
18239
18398
|
if (msg === "fetch failed" || msg.startsWith("fetch failed")) return true;
|
|
18240
|
-
for (let e =
|
|
18399
|
+
for (let e = err3, depth = 0; e && depth < 4; e = e.cause, depth++) {
|
|
18241
18400
|
const code = codeOf(e);
|
|
18242
18401
|
if (code && NETWORK_CODES.includes(code)) return true;
|
|
18243
18402
|
}
|
|
18244
18403
|
if (NETWORK_CODES.some((c) => msg.includes(c))) return true;
|
|
18245
18404
|
if (/\b(?:HTTP|status)\s*:?\s*(502|503|504)\b/i.test(msg) || /^(502|503|504)\b/.test(msg)) return true;
|
|
18246
|
-
if (
|
|
18405
|
+
if (err3 instanceof SyntaxError && /Unexpected token '?<|is not valid JSON|Unexpected end of JSON/.test(msg)) return true;
|
|
18247
18406
|
return false;
|
|
18248
18407
|
}
|
|
18249
|
-
function describeCause(
|
|
18250
|
-
const first = (
|
|
18251
|
-
const code = codeOf(
|
|
18408
|
+
function describeCause(err3) {
|
|
18409
|
+
const first = (err3 instanceof Error ? err3.message : String(err3)).split("\n")[0];
|
|
18410
|
+
const code = codeOf(err3?.cause);
|
|
18252
18411
|
return code && !first.includes(code) ? `${first} (${code})` : first;
|
|
18253
18412
|
}
|
|
18254
18413
|
|
|
18255
18414
|
// src/daemon.ts
|
|
18256
|
-
import { existsSync as
|
|
18257
|
-
import { join as
|
|
18415
|
+
import { existsSync as existsSync38, mkdirSync as mkdirSync25, readFileSync as readFileSync34, readdirSync as readdirSync16, rmSync as rmSync17, statSync as statSync12, writeFileSync as writeFileSync26 } from "fs";
|
|
18416
|
+
import { join as join42 } from "path";
|
|
18258
18417
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
18259
18418
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
18260
18419
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
@@ -18277,22 +18436,22 @@ async function runDaemonLoop(o) {
|
|
|
18277
18436
|
o.onRecovered?.({ downMs: now() - downSince });
|
|
18278
18437
|
transientRun = 0;
|
|
18279
18438
|
}
|
|
18280
|
-
} catch (
|
|
18281
|
-
if (isTransientError(
|
|
18439
|
+
} catch (err3) {
|
|
18440
|
+
if (isTransientError(err3)) {
|
|
18282
18441
|
passes++;
|
|
18283
18442
|
if (transientRun === 0) downSince = now();
|
|
18284
18443
|
transientRun++;
|
|
18285
18444
|
const wait = transientBackoffMs(transientRun);
|
|
18286
18445
|
const downMs = now() - downSince;
|
|
18287
|
-
o.onTransient?.({ err:
|
|
18288
|
-
if (transientRun === OUTAGE_ESCALATE_AFTER) o.onOutage?.({ err:
|
|
18446
|
+
o.onTransient?.({ err: err3, consecutive: transientRun, backoffMs: wait, downMs });
|
|
18447
|
+
if (transientRun === OUTAGE_ESCALATE_AFTER) o.onOutage?.({ err: err3, consecutive: transientRun, downMs });
|
|
18289
18448
|
if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
|
|
18290
18449
|
if (o.signal?.aborted) return "aborted";
|
|
18291
18450
|
await sleep(wait);
|
|
18292
18451
|
continue;
|
|
18293
18452
|
}
|
|
18294
18453
|
passes++;
|
|
18295
|
-
o.onPassError?.(
|
|
18454
|
+
o.onPassError?.(err3);
|
|
18296
18455
|
if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
|
|
18297
18456
|
if (o.signal?.aborted) return "aborted";
|
|
18298
18457
|
await sleep(backoff);
|
|
@@ -18317,7 +18476,7 @@ async function runDaemonLoop(o) {
|
|
|
18317
18476
|
function latestMtime(path) {
|
|
18318
18477
|
let stat;
|
|
18319
18478
|
try {
|
|
18320
|
-
stat =
|
|
18479
|
+
stat = statSync12(path);
|
|
18321
18480
|
} catch {
|
|
18322
18481
|
return 0;
|
|
18323
18482
|
}
|
|
@@ -18325,7 +18484,7 @@ function latestMtime(path) {
|
|
|
18325
18484
|
if (!stat.isDirectory()) return 0;
|
|
18326
18485
|
let latest = stat.mtimeMs;
|
|
18327
18486
|
for (const entry of readdirSync16(path, { withFileTypes: true })) {
|
|
18328
|
-
latest = Math.max(latest, latestMtime(
|
|
18487
|
+
latest = Math.max(latest, latestMtime(join42(path, entry.name)));
|
|
18329
18488
|
}
|
|
18330
18489
|
return latest;
|
|
18331
18490
|
}
|
|
@@ -18334,23 +18493,23 @@ function computeTreeSignature(paths) {
|
|
|
18334
18493
|
}
|
|
18335
18494
|
function makeStaleChecker(paths, opts = {}) {
|
|
18336
18495
|
const initial = computeTreeSignature(paths);
|
|
18337
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
18496
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync38(opts.reloadSentinel);
|
|
18338
18497
|
}
|
|
18339
18498
|
function reloadSentinelFor(stateDir) {
|
|
18340
|
-
return
|
|
18499
|
+
return join42(stateDir, "reload-requested");
|
|
18341
18500
|
}
|
|
18342
18501
|
function daemonStampFor(stateDir) {
|
|
18343
|
-
return
|
|
18502
|
+
return join42(stateDir, "daemon.started");
|
|
18344
18503
|
}
|
|
18345
18504
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
18346
|
-
|
|
18347
|
-
|
|
18348
|
-
|
|
18505
|
+
mkdirSync25(stateDir, { recursive: true });
|
|
18506
|
+
rmSync17(reloadSentinelFor(stateDir), { force: true });
|
|
18507
|
+
writeFileSync26(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
18349
18508
|
`);
|
|
18350
18509
|
}
|
|
18351
18510
|
function readDaemonStarted(stateDir) {
|
|
18352
18511
|
try {
|
|
18353
|
-
const v = JSON.parse(
|
|
18512
|
+
const v = JSON.parse(readFileSync34(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
18354
18513
|
return typeof v === "number" ? v : null;
|
|
18355
18514
|
} catch {
|
|
18356
18515
|
return null;
|
|
@@ -18359,7 +18518,7 @@ function readDaemonStarted(stateDir) {
|
|
|
18359
18518
|
function configChangedAfter(files, startedAt) {
|
|
18360
18519
|
for (const f of files) {
|
|
18361
18520
|
try {
|
|
18362
|
-
if (
|
|
18521
|
+
if (statSync12(f).mtimeMs > startedAt) return f;
|
|
18363
18522
|
} catch {
|
|
18364
18523
|
}
|
|
18365
18524
|
}
|
|
@@ -18411,7 +18570,7 @@ async function runOnePassUnguarded(o) {
|
|
|
18411
18570
|
await hydrateApiKeys([o.route], o.getSessionStore);
|
|
18412
18571
|
const emit = o.newEmitter();
|
|
18413
18572
|
const decide = o.decide ?? decideCycle;
|
|
18414
|
-
const
|
|
18573
|
+
const spawn9 = o.spawnRoleAgent ?? runRoleAgent;
|
|
18415
18574
|
const breaker = o.spawnBreaker ?? new SpawnBreaker();
|
|
18416
18575
|
const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
|
|
18417
18576
|
if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
|
|
@@ -18515,7 +18674,7 @@ async function runOnePassUnguarded(o) {
|
|
|
18515
18674
|
const settled = new Promise((resolve6) => {
|
|
18516
18675
|
ready = resolve6;
|
|
18517
18676
|
});
|
|
18518
|
-
const run2 =
|
|
18677
|
+
const run2 = spawn9(role, decision, ctx, { ready }).then(() => {
|
|
18519
18678
|
breaker.recordSuccess(role);
|
|
18520
18679
|
clearShipAttention(o.state, `role_parked:${role}`);
|
|
18521
18680
|
}).catch(async (e) => {
|
|
@@ -18659,6 +18818,23 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
18659
18818
|
const { route: route2, ship, emit } = ctx;
|
|
18660
18819
|
const runId = randomBytes2(3).toString("hex");
|
|
18661
18820
|
const identity = await resolveSeatKey(route2, role, ctx.getSessionStore);
|
|
18821
|
+
if (identity.kind === "ship" && identity.storeError && SEAT_KEY_ROLES.includes(role)) {
|
|
18822
|
+
emit.warn(`${role} run refused: its agent key cannot be read (${identity.storeError})`, { step: "agent", role });
|
|
18823
|
+
try {
|
|
18824
|
+
await raiseShipAttention(
|
|
18825
|
+
{ state: ctx.state, emit, route: route2, ship },
|
|
18826
|
+
seatKeyUnreadableItem(routeSlug(route2.route), role, identity.storeError)
|
|
18827
|
+
);
|
|
18828
|
+
} catch {
|
|
18829
|
+
}
|
|
18830
|
+
return;
|
|
18831
|
+
}
|
|
18832
|
+
if (SEAT_KEY_ROLES.includes(role)) {
|
|
18833
|
+
try {
|
|
18834
|
+
clearShipAttention(ctx.state, seatKeyUnreadableKey(routeSlug(route2.route), role));
|
|
18835
|
+
} catch {
|
|
18836
|
+
}
|
|
18837
|
+
}
|
|
18662
18838
|
if (identity.kind === "ship" && SEAT_KEY_ROLES.includes(role)) {
|
|
18663
18839
|
emit.warn(`${role} writes as the ship, not its own agent: ${identity.reason}`, { step: "agent", role });
|
|
18664
18840
|
} else if (identity.kind === "agent") {
|
|
@@ -18830,26 +19006,232 @@ async function buildEnvironment(route2, ship, ticket) {
|
|
|
18830
19006
|
}
|
|
18831
19007
|
|
|
18832
19008
|
// src/control-api.ts
|
|
18833
|
-
import { createHash as
|
|
19009
|
+
import { createHash as createHash9 } from "crypto";
|
|
18834
19010
|
import {
|
|
18835
|
-
chmodSync as
|
|
19011
|
+
chmodSync as chmodSync4,
|
|
18836
19012
|
closeSync as closeSync5,
|
|
18837
|
-
existsSync as
|
|
19013
|
+
existsSync as existsSync39,
|
|
18838
19014
|
fstatSync,
|
|
18839
19015
|
openSync as openSync5,
|
|
18840
19016
|
readSync as readSync3,
|
|
18841
19017
|
readdirSync as readdirSync17,
|
|
18842
|
-
rmSync as
|
|
18843
|
-
statSync as
|
|
19018
|
+
rmSync as rmSync18,
|
|
19019
|
+
statSync as statSync13,
|
|
18844
19020
|
watch
|
|
18845
19021
|
} from "fs";
|
|
18846
19022
|
import { createConnection } from "net";
|
|
18847
19023
|
import { createServer } from "http";
|
|
18848
|
-
import { basename as basename12, dirname as dirname21, join as
|
|
18849
|
-
import { execFile, spawn as
|
|
19024
|
+
import { basename as basename12, dirname as dirname21, join as join43 } from "path";
|
|
19025
|
+
import { execFile, spawn as spawn8 } from "child_process";
|
|
19026
|
+
|
|
19027
|
+
// src/periscope-control.ts
|
|
19028
|
+
import { spawn as spawn7 } from "child_process";
|
|
19029
|
+
var reply = (status2, body) => ({ status: status2, body });
|
|
19030
|
+
var err = (status2, message) => reply(status2, { error: message });
|
|
19031
|
+
var TICKET_RE = /^(?:[A-Za-z]+-)?\d+$/;
|
|
19032
|
+
var MAX_FIELD = 200;
|
|
19033
|
+
function plainText(v, field, required) {
|
|
19034
|
+
if (v === void 0 || v === null || v === "") return required ? { error: `${field} is required` } : void 0;
|
|
19035
|
+
if (typeof v !== "string") return { error: `${field} must be a string` };
|
|
19036
|
+
const t = v.trim();
|
|
19037
|
+
if (!t) return required ? { error: `${field} is required` } : void 0;
|
|
19038
|
+
if (t.length > MAX_FIELD || /[\r\n\0]/.test(t)) return { error: `${field} must be one line of at most ${MAX_FIELD} characters` };
|
|
19039
|
+
if (t.startsWith("-")) return { error: `${field} must not start with "-"` };
|
|
19040
|
+
return t;
|
|
19041
|
+
}
|
|
19042
|
+
var isErr = (v) => typeof v === "object" && v !== null && "error" in v;
|
|
19043
|
+
function pickRoute(h, named) {
|
|
19044
|
+
const hosting = h.routes.filter((r) => r.hostPeriscopes);
|
|
19045
|
+
if (named !== void 0) {
|
|
19046
|
+
const hit = hosting.find((r) => r.route === named);
|
|
19047
|
+
return hit ?? err(404, "not found");
|
|
19048
|
+
}
|
|
19049
|
+
if (hosting.length === 1) return hosting[0];
|
|
19050
|
+
return err(400, `route is required (this ship hosts periscopes on: ${hosting.map((r) => r.route).join(", ")})`);
|
|
19051
|
+
}
|
|
19052
|
+
function cliMessage(r) {
|
|
19053
|
+
const lines = r.stderr.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
19054
|
+
const plain = lines.filter((l) => !l.startsWith("{"));
|
|
19055
|
+
const last = plain[plain.length - 1] ?? lines[lines.length - 1] ?? `crew exited with status ${r.code}`;
|
|
19056
|
+
return last.replace(/^crew periscope:\s*/, "");
|
|
19057
|
+
}
|
|
19058
|
+
function failureStatus(r) {
|
|
19059
|
+
const m = cliMessage(r);
|
|
19060
|
+
if (/^no periscope for ticket /.test(m)) return 404;
|
|
19061
|
+
if (/already hosts \d+ live periscope/.test(m)) return 409;
|
|
19062
|
+
return r.code === 1 ? 422 : 500;
|
|
19063
|
+
}
|
|
19064
|
+
function lastJsonLine(stdout) {
|
|
19065
|
+
const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
19066
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
19067
|
+
try {
|
|
19068
|
+
const v = JSON.parse(lines[i]);
|
|
19069
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
19070
|
+
} catch {
|
|
19071
|
+
}
|
|
19072
|
+
}
|
|
19073
|
+
return null;
|
|
19074
|
+
}
|
|
19075
|
+
function parseLine(line) {
|
|
19076
|
+
try {
|
|
19077
|
+
const v = JSON.parse(line);
|
|
19078
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
19079
|
+
} catch {
|
|
19080
|
+
return null;
|
|
19081
|
+
}
|
|
19082
|
+
}
|
|
19083
|
+
async function execute(h, args, streaming) {
|
|
19084
|
+
const finish = (r) => {
|
|
19085
|
+
if (r.code !== 0) return { status: failureStatus(r), body: { error: cliMessage(r) } };
|
|
19086
|
+
const done = lastJsonLine(r.stdout);
|
|
19087
|
+
return done ? { status: 200, body: done } : { status: 502, body: { error: "the command finished without a result" } };
|
|
19088
|
+
};
|
|
19089
|
+
if (!streaming) {
|
|
19090
|
+
const f = finish(await h.run(args, () => {
|
|
19091
|
+
}));
|
|
19092
|
+
return reply(f.status, f.body);
|
|
19093
|
+
}
|
|
19094
|
+
return {
|
|
19095
|
+
status: 200,
|
|
19096
|
+
body: null,
|
|
19097
|
+
stream: async (write) => {
|
|
19098
|
+
const r = await h.run(args, (line) => {
|
|
19099
|
+
const ev = parseLine(line);
|
|
19100
|
+
if (ev) write(ev);
|
|
19101
|
+
});
|
|
19102
|
+
const f = finish(r);
|
|
19103
|
+
write(f.status === 200 ? f.body : { event: "error", status: f.status, message: f.body.error });
|
|
19104
|
+
}
|
|
19105
|
+
};
|
|
19106
|
+
}
|
|
19107
|
+
var isPeriscopePath = (p) => p === "/periscopes" || p.startsWith("/periscopes/");
|
|
19108
|
+
async function handlePeriscopes(h, q) {
|
|
19109
|
+
if (!h || !h.routes.some((r) => r.hostPeriscopes)) return err(404, "not found");
|
|
19110
|
+
const down = /^\/periscopes\/([^/]+)\/down$/.exec(q.path);
|
|
19111
|
+
const isUp = q.path === "/periscopes" && q.method === "POST";
|
|
19112
|
+
const isList = q.path === "/periscopes" && q.method === "GET";
|
|
19113
|
+
if (q.path === "/periscopes" && !isUp && !isList) return err(405, "method not allowed");
|
|
19114
|
+
if (down && q.method !== "POST") return err(405, "method not allowed");
|
|
19115
|
+
if (q.path !== "/periscopes" && !down) return err(404, "not found");
|
|
19116
|
+
const member = async (workspaceId) => {
|
|
19117
|
+
const v = await h.verifyIn(q.token, workspaceId);
|
|
19118
|
+
if (v === "ok") return void 0;
|
|
19119
|
+
if (v === "unavailable") return err(503, "could not verify the key right now");
|
|
19120
|
+
return err(403, "this key does not belong to the workspace of this route");
|
|
19121
|
+
};
|
|
19122
|
+
if (isList) {
|
|
19123
|
+
const named = q.query.get("route") ?? void 0;
|
|
19124
|
+
const candidates2 = named === void 0 ? h.routes.filter((r) => r.hostPeriscopes) : [pickRoute(h, named)];
|
|
19125
|
+
const rows = [];
|
|
19126
|
+
for (const c of candidates2) {
|
|
19127
|
+
if ("status" in c) return c;
|
|
19128
|
+
const denied2 = await member(c.workspaceId);
|
|
19129
|
+
if (denied2) {
|
|
19130
|
+
if (named !== void 0) return denied2;
|
|
19131
|
+
continue;
|
|
19132
|
+
}
|
|
19133
|
+
const r = await h.run(["periscope", "list", c.route, "--json"], () => {
|
|
19134
|
+
});
|
|
19135
|
+
if (r.code !== 0) return err(failureStatus(r), cliMessage(r));
|
|
19136
|
+
try {
|
|
19137
|
+
const list = JSON.parse(r.stdout);
|
|
19138
|
+
if (Array.isArray(list)) for (const e of list) rows.push({ route: c.route, ...e });
|
|
19139
|
+
} catch {
|
|
19140
|
+
return err(502, "the list command did not answer JSON");
|
|
19141
|
+
}
|
|
19142
|
+
}
|
|
19143
|
+
return reply(200, { periscopes: rows });
|
|
19144
|
+
}
|
|
19145
|
+
const b = q.body;
|
|
19146
|
+
if (!b) return err(400, "body must be a small JSON object");
|
|
19147
|
+
const routeName = plainText(b.route, "route", false);
|
|
19148
|
+
if (isErr(routeName)) return err(400, routeName.error);
|
|
19149
|
+
const picked = pickRoute(h, routeName);
|
|
19150
|
+
if ("status" in picked) return picked;
|
|
19151
|
+
const denied = await member(picked.workspaceId);
|
|
19152
|
+
if (denied) return denied;
|
|
19153
|
+
if (down) {
|
|
19154
|
+
const ticket = decodeURIComponent(down[1]);
|
|
19155
|
+
if (!TICKET_RE.test(ticket)) return err(400, "ticket must be a ticket number such as 123 or CREW-123");
|
|
19156
|
+
if (b.outcome !== "keep" && b.outcome !== "discard") return err(400, 'outcome must be "keep" or "discard"');
|
|
19157
|
+
if (b.allowEmpty !== void 0 && typeof b.allowEmpty !== "boolean") return err(400, "allowEmpty must be a boolean");
|
|
19158
|
+
return execute(h, [
|
|
19159
|
+
"periscope",
|
|
19160
|
+
"down",
|
|
19161
|
+
picked.route,
|
|
19162
|
+
ticket,
|
|
19163
|
+
`--${b.outcome}`,
|
|
19164
|
+
...b.allowEmpty === true ? ["--allow-empty"] : [],
|
|
19165
|
+
"--json"
|
|
19166
|
+
], q.wantsStream);
|
|
19167
|
+
}
|
|
19168
|
+
const repo = plainText(b.repo, "repo", true);
|
|
19169
|
+
const topic = plainText(b.topic, "topic", true);
|
|
19170
|
+
const requestedBy = plainText(b.requestedBy, "requestedBy", false);
|
|
19171
|
+
for (const v of [repo, topic, requestedBy]) if (isErr(v)) return err(400, v.error);
|
|
19172
|
+
if (b.isolate !== void 0 && typeof b.isolate !== "boolean") return err(400, "isolate must be a boolean");
|
|
19173
|
+
if (!h.previewHost) return err(409, "this ship has no ship.previewHost, so a Passenger could not reach a periscope");
|
|
19174
|
+
const live = await h.run(["periscope", "list", picked.route, "--json"], () => {
|
|
19175
|
+
});
|
|
19176
|
+
if (live.code === 0) {
|
|
19177
|
+
try {
|
|
19178
|
+
const n = JSON.parse(live.stdout).length;
|
|
19179
|
+
if (n >= h.maxPeriscopes) return err(409, `this ship already hosts ${n} live periscope(s) (ship.maxPeriscopes is ${h.maxPeriscopes}) \u2014 discard one first`);
|
|
19180
|
+
} catch {
|
|
19181
|
+
}
|
|
19182
|
+
}
|
|
19183
|
+
return execute(h, [
|
|
19184
|
+
"periscope",
|
|
19185
|
+
"up",
|
|
19186
|
+
picked.route,
|
|
19187
|
+
repo,
|
|
19188
|
+
"--topic",
|
|
19189
|
+
topic,
|
|
19190
|
+
"--host",
|
|
19191
|
+
h.previewHost,
|
|
19192
|
+
...requestedBy ? ["--requested-by", requestedBy] : [],
|
|
19193
|
+
...b.isolate === true ? ["--isolate"] : [],
|
|
19194
|
+
"--no-open",
|
|
19195
|
+
"--json"
|
|
19196
|
+
], q.wantsStream);
|
|
19197
|
+
}
|
|
19198
|
+
function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
19199
|
+
return new Promise((resolve6) => {
|
|
19200
|
+
const child = spawn7(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
19201
|
+
let stdout = "";
|
|
19202
|
+
let stderr = "";
|
|
19203
|
+
let pending = "";
|
|
19204
|
+
child.stdout.on("data", (d) => {
|
|
19205
|
+
stdout += d.toString("utf8");
|
|
19206
|
+
});
|
|
19207
|
+
child.stderr.on("data", (d) => {
|
|
19208
|
+
const text = d.toString("utf8");
|
|
19209
|
+
stderr += text;
|
|
19210
|
+
pending += text;
|
|
19211
|
+
let i;
|
|
19212
|
+
while ((i = pending.indexOf("\n")) >= 0) {
|
|
19213
|
+
const line = pending.slice(0, i).trim();
|
|
19214
|
+
pending = pending.slice(i + 1);
|
|
19215
|
+
if (line) onLine(line);
|
|
19216
|
+
}
|
|
19217
|
+
});
|
|
19218
|
+
const t = setTimeout(() => child.kill("SIGTERM"), timeoutMs);
|
|
19219
|
+
child.on("error", (e) => {
|
|
19220
|
+
clearTimeout(t);
|
|
19221
|
+
resolve6({ code: 127, stdout, stderr: `${stderr}${e.message}
|
|
19222
|
+
` });
|
|
19223
|
+
});
|
|
19224
|
+
child.on("close", (code) => {
|
|
19225
|
+
clearTimeout(t);
|
|
19226
|
+
resolve6({ code: code ?? 1, stdout, stderr });
|
|
19227
|
+
});
|
|
19228
|
+
});
|
|
19229
|
+
}
|
|
19230
|
+
|
|
19231
|
+
// src/control-api.ts
|
|
18850
19232
|
var PAUSABLE = ["dev", "design", "qa"];
|
|
18851
19233
|
var CONTROL_SOCKET_NAME = "control.sock";
|
|
18852
|
-
var controlSocketFor = (stateDir) =>
|
|
19234
|
+
var controlSocketFor = (stateDir) => join43(stateDir, CONTROL_SOCKET_NAME);
|
|
18853
19235
|
var MAX_SOCKET_PATH = 100;
|
|
18854
19236
|
var MAX_TAIL_BYTES = 1024 * 1024;
|
|
18855
19237
|
var MAX_WAIT_SECONDS = 30;
|
|
@@ -18865,7 +19247,7 @@ var ControlAuth = class {
|
|
|
18865
19247
|
if (!token) return "unauthorized";
|
|
18866
19248
|
for (const k of this.o.shipKeys()) if (k && secretsMatch(token, k)) return "ok";
|
|
18867
19249
|
const now = (this.o.now ?? Date.now)();
|
|
18868
|
-
const id =
|
|
19250
|
+
const id = createHash9("sha256").update(token).digest("hex");
|
|
18869
19251
|
const hit = this.cache.get(id);
|
|
18870
19252
|
if (hit && now - hit.at < (this.o.cacheMs ?? 6e4)) return hit.verdict;
|
|
18871
19253
|
const verdict = await this.ask(token);
|
|
@@ -18877,11 +19259,26 @@ var ControlAuth = class {
|
|
|
18877
19259
|
}
|
|
18878
19260
|
return verdict;
|
|
18879
19261
|
}
|
|
18880
|
-
|
|
19262
|
+
/**
|
|
19263
|
+
* Like `verify`, but the token must belong to `workspaceId` itself, not just
|
|
19264
|
+
* to some workspace this ship serves. The ship's own keys pass.
|
|
19265
|
+
*/
|
|
19266
|
+
async verifyIn(token, workspaceId) {
|
|
19267
|
+
if (!token) return "unauthorized";
|
|
19268
|
+
for (const k of this.o.shipKeys()) if (k && secretsMatch(token, k)) return "ok";
|
|
19269
|
+
const now = (this.o.now ?? Date.now)();
|
|
19270
|
+
const id = `${workspaceId}:${createHash9("sha256").update(token).digest("hex")}`;
|
|
19271
|
+
const hit = this.cache.get(id);
|
|
19272
|
+
if (hit && now - hit.at < (this.o.cacheMs ?? 6e4)) return hit.verdict;
|
|
19273
|
+
const verdict = await this.ask(token, [workspaceId]);
|
|
19274
|
+
if (verdict !== "unavailable") this.cache.set(id, { verdict, at: now });
|
|
19275
|
+
return verdict;
|
|
19276
|
+
}
|
|
19277
|
+
async ask(token, workspaceIds = this.o.workspaceIds) {
|
|
18881
19278
|
const doFetch = this.o.fetch ?? fetch;
|
|
18882
19279
|
const base = this.o.baseUrl.replace(/\/+$/, "");
|
|
18883
19280
|
let forbidden = false;
|
|
18884
|
-
for (const ws of
|
|
19281
|
+
for (const ws of workspaceIds) {
|
|
18885
19282
|
let res;
|
|
18886
19283
|
try {
|
|
18887
19284
|
res = await doFetch(`${base}/api/auth/me?workspaceId=${encodeURIComponent(ws)}`, {
|
|
@@ -18920,7 +19317,7 @@ function candidates(live, rotated) {
|
|
|
18920
19317
|
const out = [live];
|
|
18921
19318
|
if (!rotated) return out;
|
|
18922
19319
|
try {
|
|
18923
|
-
for (const n of readdirSync17(dirname21(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(
|
|
19320
|
+
for (const n of readdirSync17(dirname21(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join43(dirname21(live), n));
|
|
18924
19321
|
} catch {
|
|
18925
19322
|
}
|
|
18926
19323
|
return out;
|
|
@@ -18943,7 +19340,7 @@ function readRange(path, from, max) {
|
|
|
18943
19340
|
}
|
|
18944
19341
|
}
|
|
18945
19342
|
function lastLines2(path, n) {
|
|
18946
|
-
const st =
|
|
19343
|
+
const st = statSync13(path);
|
|
18947
19344
|
const cursorAt = (off) => encodeCursor({ dev: st.dev, ino: st.ino, off });
|
|
18948
19345
|
if (n <= 0 || st.size === 0) return { lines: [], cursor: cursorAt(st.size), reset: false };
|
|
18949
19346
|
let start = st.size;
|
|
@@ -18964,7 +19361,7 @@ function lastLines2(path, n) {
|
|
|
18964
19361
|
}
|
|
18965
19362
|
function readTail(live, opts) {
|
|
18966
19363
|
const max = opts.maxBytes ?? MAX_TAIL_BYTES;
|
|
18967
|
-
if (!
|
|
19364
|
+
if (!existsSync39(live)) {
|
|
18968
19365
|
return { lines: [], cursor: opts.after ?? encodeCursor({ dev: 0, ino: 0, off: 0 }), reset: false };
|
|
18969
19366
|
}
|
|
18970
19367
|
const cur = opts.after ? decodeCursor(opts.after) : null;
|
|
@@ -18977,7 +19374,7 @@ function readTail(live, opts) {
|
|
|
18977
19374
|
} else {
|
|
18978
19375
|
const found = candidates(live, opts.rotated ?? false).find((p) => {
|
|
18979
19376
|
try {
|
|
18980
|
-
const s =
|
|
19377
|
+
const s = statSync13(p);
|
|
18981
19378
|
return s.ino === cur.ino && s.dev === cur.dev;
|
|
18982
19379
|
} catch {
|
|
18983
19380
|
return false;
|
|
@@ -18988,7 +19385,7 @@ function readTail(live, opts) {
|
|
|
18988
19385
|
off = cur.off;
|
|
18989
19386
|
} else reset = true;
|
|
18990
19387
|
}
|
|
18991
|
-
let size =
|
|
19388
|
+
let size = statSync13(path).size;
|
|
18992
19389
|
if (off > size) {
|
|
18993
19390
|
off = 0;
|
|
18994
19391
|
reset = true;
|
|
@@ -18996,12 +19393,12 @@ function readTail(live, opts) {
|
|
|
18996
19393
|
if (off >= size && path !== live) {
|
|
18997
19394
|
path = live;
|
|
18998
19395
|
off = 0;
|
|
18999
|
-
size =
|
|
19396
|
+
size = statSync13(path).size;
|
|
19000
19397
|
}
|
|
19001
19398
|
const { text } = readRange(path, off, max);
|
|
19002
19399
|
const cut = text.lastIndexOf("\n");
|
|
19003
19400
|
const whole = cut < 0 ? "" : text.slice(0, cut + 1);
|
|
19004
|
-
const st =
|
|
19401
|
+
const st = statSync13(path);
|
|
19005
19402
|
return {
|
|
19006
19403
|
lines: whole.split("\n").filter((l) => l.length),
|
|
19007
19404
|
cursor: encodeCursor({ dev: st.dev, ino: st.ino, off: off + Buffer.byteLength(whole) }),
|
|
@@ -19032,8 +19429,8 @@ function waitForChange(dir, ms3, signal) {
|
|
|
19032
19429
|
}
|
|
19033
19430
|
});
|
|
19034
19431
|
}
|
|
19035
|
-
var
|
|
19036
|
-
var
|
|
19432
|
+
var reply2 = (status2, body) => ({ status: status2, body });
|
|
19433
|
+
var err2 = (status2, message) => reply2(status2, { error: message });
|
|
19037
19434
|
function pauseDoc(state) {
|
|
19038
19435
|
return {
|
|
19039
19436
|
crew: state.isPaused(),
|
|
@@ -19073,26 +19470,38 @@ async function readJsonBody(req) {
|
|
|
19073
19470
|
async function tailReply(path, q, rotated, abort) {
|
|
19074
19471
|
const after = q.get("after") ?? void 0;
|
|
19075
19472
|
const tail3 = q.has("tail") ? Number(q.get("tail")) : void 0;
|
|
19076
|
-
if (tail3 !== void 0 && !(Number.isInteger(tail3) && tail3 >= 0 && tail3 <= 1e3)) return
|
|
19473
|
+
if (tail3 !== void 0 && !(Number.isInteger(tail3) && tail3 >= 0 && tail3 <= 1e3)) return err2(400, "tail must be an integer from 0 to 1000");
|
|
19077
19474
|
const wait = q.has("wait") ? Number(q.get("wait")) : 0;
|
|
19078
|
-
if (!(Number.isFinite(wait) && wait >= 0)) return
|
|
19079
|
-
if (after !== void 0 && !decodeCursor(after)) return
|
|
19475
|
+
if (!(Number.isFinite(wait) && wait >= 0)) return err2(400, "wait must be a number of seconds");
|
|
19476
|
+
if (after !== void 0 && !decodeCursor(after)) return err2(400, "after is not a cursor this API issued");
|
|
19080
19477
|
const deadline = Date.now() + Math.min(wait, MAX_WAIT_SECONDS) * 1e3;
|
|
19081
19478
|
let r = readTail(path, { after, tail: tail3, rotated });
|
|
19082
19479
|
while (after && !r.lines.length && !r.reset && Date.now() < deadline && !abort.aborted) {
|
|
19083
19480
|
await waitForChange(dirname21(path), deadline - Date.now(), abort);
|
|
19084
19481
|
r = readTail(path, { after: r.cursor, rotated });
|
|
19085
19482
|
}
|
|
19086
|
-
return
|
|
19483
|
+
return reply2(200, r);
|
|
19087
19484
|
}
|
|
19088
19485
|
async function route(deps, method, url, req, abort) {
|
|
19089
19486
|
const p = url.pathname.replace(/\/+$/, "") || "/";
|
|
19090
19487
|
const get2 = method === "GET";
|
|
19091
19488
|
const post3 = method === "POST";
|
|
19489
|
+
if (isPeriscopePath(p)) {
|
|
19490
|
+
const token = /^Bearer (.+)$/.exec(req.headers.authorization ?? "")?.[1]?.trim() ?? "";
|
|
19491
|
+
const body = method === "POST" ? await readJsonBody(req) : {};
|
|
19492
|
+
return handlePeriscopes(deps.periscopes, {
|
|
19493
|
+
method,
|
|
19494
|
+
path: p,
|
|
19495
|
+
query: url.searchParams,
|
|
19496
|
+
token,
|
|
19497
|
+
body,
|
|
19498
|
+
wantsStream: url.searchParams.get("stream") === "1" || (req.headers.accept ?? "").includes("application/x-ndjson")
|
|
19499
|
+
});
|
|
19500
|
+
}
|
|
19092
19501
|
if (p === "/status" && get2) {
|
|
19093
19502
|
const doc = await deps.status();
|
|
19094
|
-
if (!doc) return
|
|
19095
|
-
return
|
|
19503
|
+
if (!doc) return err2(502, "could not produce the status document");
|
|
19504
|
+
return reply2(200, {
|
|
19096
19505
|
...doc,
|
|
19097
19506
|
daemon: {
|
|
19098
19507
|
alive: true,
|
|
@@ -19104,47 +19513,56 @@ async function route(deps, method, url, req, abort) {
|
|
|
19104
19513
|
});
|
|
19105
19514
|
}
|
|
19106
19515
|
if (p === "/events" && get2) return tailReply(eventFileFor(deps.stateDir), url.searchParams, true, abort);
|
|
19107
|
-
if (p === "/streams" && get2) return
|
|
19516
|
+
if (p === "/streams" && get2) return reply2(200, { streams: liveRuns(deps.stateDir).map(describeStream) });
|
|
19108
19517
|
if (p.startsWith("/streams/") && get2) {
|
|
19109
19518
|
const run2 = decodeURIComponent(p.slice("/streams/".length));
|
|
19110
|
-
if (!RUN_NAME_RE.test(run2) || run2.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run2)) return
|
|
19111
|
-
const file =
|
|
19112
|
-
if (!
|
|
19519
|
+
if (!RUN_NAME_RE.test(run2) || run2.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run2)) return err2(400, "not a run name");
|
|
19520
|
+
const file = join43(deps.stateDir, "streams", run2.endsWith(".jsonl") ? run2 : `${run2}.jsonl`);
|
|
19521
|
+
if (!existsSync39(file)) return err2(404, "no such run");
|
|
19113
19522
|
return tailReply(file, url.searchParams, false, abort);
|
|
19114
19523
|
}
|
|
19115
19524
|
if ((p === "/pause" || p === "/resume") && post3) {
|
|
19116
19525
|
const body = await readJsonBody(req);
|
|
19117
|
-
if (!body) return
|
|
19526
|
+
if (!body) return err2(400, "body must be a small JSON object");
|
|
19118
19527
|
const role = body.role;
|
|
19119
|
-
if (role !== void 0 && !PAUSABLE.includes(role)) return
|
|
19528
|
+
if (role !== void 0 && !PAUSABLE.includes(role)) return err2(400, `role must be one of ${PAUSABLE.join(", ")}`);
|
|
19120
19529
|
if (p === "/pause") deps.state.pause(role);
|
|
19121
19530
|
else deps.state.resume(role);
|
|
19122
|
-
return
|
|
19531
|
+
return reply2(200, { ok: true, pause: pauseDoc(deps.state) });
|
|
19123
19532
|
}
|
|
19124
19533
|
if ((p === "/release/pause" || p === "/release/resume") && post3) {
|
|
19125
19534
|
deps.state.setReleasePaused(p === "/release/pause");
|
|
19126
|
-
return
|
|
19535
|
+
return reply2(200, { ok: true, pause: pauseDoc(deps.state) });
|
|
19127
19536
|
}
|
|
19128
19537
|
const sched = /^\/scheduler\/(start|stop|restart)$/.exec(p);
|
|
19129
19538
|
if (sched && post3) {
|
|
19130
19539
|
const r = deps.scheduler(sched[1]);
|
|
19131
|
-
return r.accepted ?
|
|
19540
|
+
return r.accepted ? reply2(202, { accepted: true, detail: r.detail }) : err2(409, r.detail);
|
|
19132
19541
|
}
|
|
19133
19542
|
if (p === "/doctor" && get2) {
|
|
19134
19543
|
const doc = await deps.doctor();
|
|
19135
|
-
return doc ?
|
|
19544
|
+
return doc ? reply2(200, doc) : err2(502, "could not produce the doctor report");
|
|
19136
19545
|
}
|
|
19137
19546
|
const known = ["/status", "/events", "/streams", "/pause", "/resume", "/release/pause", "/release/resume", "/doctor"];
|
|
19138
|
-
if (known.includes(p) || p.startsWith("/streams/") || sched) return
|
|
19139
|
-
return
|
|
19547
|
+
if (known.includes(p) || p.startsWith("/streams/") || sched) return err2(405, "method not allowed");
|
|
19548
|
+
return err2(404, "not found");
|
|
19140
19549
|
}
|
|
19141
19550
|
function send(res, r) {
|
|
19551
|
+
if (r.stream) {
|
|
19552
|
+
res.writeHead(200, { "Content-Type": "application/x-ndjson", "Cache-Control": "no-store" });
|
|
19553
|
+
return r.stream((ev) => {
|
|
19554
|
+
if (!res.writableEnded) res.write(`${JSON.stringify(ev)}
|
|
19555
|
+
`);
|
|
19556
|
+
}).then(() => {
|
|
19557
|
+
res.end();
|
|
19558
|
+
});
|
|
19559
|
+
}
|
|
19142
19560
|
const body = JSON.stringify(r.body);
|
|
19143
19561
|
res.writeHead(r.status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), "Cache-Control": "no-store" });
|
|
19144
19562
|
res.end(body);
|
|
19145
19563
|
}
|
|
19146
19564
|
async function socketIsStale(path) {
|
|
19147
|
-
if (!
|
|
19565
|
+
if (!existsSync39(path)) return false;
|
|
19148
19566
|
return new Promise((resolve6) => {
|
|
19149
19567
|
const c = createConnection(path);
|
|
19150
19568
|
c.once("connect", () => {
|
|
@@ -19162,8 +19580,8 @@ async function startControlServer(deps) {
|
|
|
19162
19580
|
log(`control API not started: socket path is too long for a unix socket (${path})`);
|
|
19163
19581
|
return null;
|
|
19164
19582
|
}
|
|
19165
|
-
if (await socketIsStale(path))
|
|
19166
|
-
else if (
|
|
19583
|
+
if (await socketIsStale(path)) rmSync18(path, { force: true });
|
|
19584
|
+
else if (existsSync39(path)) {
|
|
19167
19585
|
log(`control API not started: ${path} is in use by another process`);
|
|
19168
19586
|
return null;
|
|
19169
19587
|
}
|
|
@@ -19174,13 +19592,13 @@ async function startControlServer(deps) {
|
|
|
19174
19592
|
try {
|
|
19175
19593
|
const m = /^Bearer (.+)$/.exec(req.headers.authorization ?? "");
|
|
19176
19594
|
const verdict = await deps.auth.verify(m?.[1]?.trim() ?? "");
|
|
19177
|
-
if (verdict === "unauthorized") return send(res,
|
|
19178
|
-
if (verdict === "forbidden") return send(res,
|
|
19179
|
-
if (verdict === "unavailable") return send(res,
|
|
19180
|
-
send(res, await route(deps, req.method ?? "GET", new URL(req.url ?? "/", "http://ship"), req, abort.signal));
|
|
19595
|
+
if (verdict === "unauthorized") return send(res, err2(401, "invalid or revoked key"));
|
|
19596
|
+
if (verdict === "forbidden") return send(res, err2(403, "this key does not belong to a workspace this ship serves"));
|
|
19597
|
+
if (verdict === "unavailable") return send(res, err2(503, "could not verify the key right now"));
|
|
19598
|
+
await send(res, await route(deps, req.method ?? "GET", new URL(req.url ?? "/", "http://ship"), req, abort.signal));
|
|
19181
19599
|
} catch (e) {
|
|
19182
19600
|
log(`control API request failed: ${e.message}`);
|
|
19183
|
-
if (!res.headersSent) send(res,
|
|
19601
|
+
if (!res.headersSent) send(res, err2(500, "internal error"));
|
|
19184
19602
|
else res.end();
|
|
19185
19603
|
}
|
|
19186
19604
|
})();
|
|
@@ -19192,7 +19610,7 @@ async function startControlServer(deps) {
|
|
|
19192
19610
|
});
|
|
19193
19611
|
server.listen(path, () => {
|
|
19194
19612
|
try {
|
|
19195
|
-
|
|
19613
|
+
chmodSync4(path, 384);
|
|
19196
19614
|
} catch {
|
|
19197
19615
|
}
|
|
19198
19616
|
server.on("error", (e) => log(`control API error: ${e.message}`));
|
|
@@ -19201,7 +19619,7 @@ async function startControlServer(deps) {
|
|
|
19201
19619
|
path,
|
|
19202
19620
|
close: () => new Promise((done) => {
|
|
19203
19621
|
server.close(() => {
|
|
19204
|
-
|
|
19622
|
+
rmSync18(path, { force: true });
|
|
19205
19623
|
done();
|
|
19206
19624
|
});
|
|
19207
19625
|
})
|
|
@@ -19210,7 +19628,7 @@ async function startControlServer(deps) {
|
|
|
19210
19628
|
});
|
|
19211
19629
|
}
|
|
19212
19630
|
var shq2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
19213
|
-
function runDetached(commands, spawnFn =
|
|
19631
|
+
function runDetached(commands, spawnFn = spawn8) {
|
|
19214
19632
|
const script = `sleep 1; ${commands.map((c) => c.map(shq2).join(" ")).join("; ")}`;
|
|
19215
19633
|
const child = spawnFn("/bin/sh", ["-c", script], { detached: true, stdio: "ignore" });
|
|
19216
19634
|
child.on("error", () => {
|
|
@@ -19678,7 +20096,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
19678
20096
|
} else {
|
|
19679
20097
|
const toleratesNoRoutes = command === "credential" || command === "forge" || command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
19680
20098
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
19681
|
-
if (configless && !
|
|
20099
|
+
if (configless && !existsSync40(findConfigFile(CREW_HOME))) {
|
|
19682
20100
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
19683
20101
|
} else {
|
|
19684
20102
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -19693,7 +20111,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
19693
20111
|
throw e;
|
|
19694
20112
|
}
|
|
19695
20113
|
if (command === "credential") {
|
|
19696
|
-
const r = runCredential(positional[1],
|
|
20114
|
+
const r = runCredential(positional[1], readFileSync35(0, "utf8"), cfg.ship.forges, cfg.ship.stateDir);
|
|
19697
20115
|
process.stdout.write(r.out);
|
|
19698
20116
|
process.stderr.write(r.err);
|
|
19699
20117
|
process.exit(r.code);
|
|
@@ -19705,9 +20123,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
19705
20123
|
forges: cfg.ship.forges,
|
|
19706
20124
|
stateDir: cfg.ship.stateDir,
|
|
19707
20125
|
configFile: cfg.configFile,
|
|
19708
|
-
readFile: (p) =>
|
|
19709
|
-
writeFile: (p, t) =>
|
|
19710
|
-
stdin: () =>
|
|
20126
|
+
readFile: (p) => readFileSync35(p, "utf8"),
|
|
20127
|
+
writeFile: (p, t) => writeFileSync27(p, t),
|
|
20128
|
+
stdin: () => readFileSync35(0, "utf8"),
|
|
19711
20129
|
env: process.env
|
|
19712
20130
|
}
|
|
19713
20131
|
);
|
|
@@ -19943,8 +20361,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
19943
20361
|
}
|
|
19944
20362
|
if (host === "linux" && hasSystemd) {
|
|
19945
20363
|
const label = labelFor(CREW_HOME, "run");
|
|
19946
|
-
const staleTimer =
|
|
19947
|
-
if (
|
|
20364
|
+
const staleTimer = join44(homedir10(), ".config", "systemd", "user", `${label}.timer`);
|
|
20365
|
+
if (existsSync40(staleTimer)) {
|
|
19948
20366
|
process.stdout.write(
|
|
19949
20367
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
19950
20368
|
`
|
|
@@ -19971,7 +20389,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
19971
20389
|
const tracker = new Tracker(c, cfg.ship);
|
|
19972
20390
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
19973
20391
|
for (const r of await resolvedRepos(c)) {
|
|
19974
|
-
if (!
|
|
20392
|
+
if (!existsSync40(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
19975
20393
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
19976
20394
|
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
19977
20395
|
}
|
|
@@ -19989,7 +20407,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
19989
20407
|
}
|
|
19990
20408
|
async function doctorFix(o) {
|
|
19991
20409
|
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))));
|
|
19992
|
-
const unitExists = (p) =>
|
|
20410
|
+
const unitExists = (p) => existsSync40(p);
|
|
19993
20411
|
const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
19994
20412
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
19995
20413
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -20006,7 +20424,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20006
20424
|
for (const a of plan.actions) {
|
|
20007
20425
|
if (a.kind === "enable") {
|
|
20008
20426
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
20009
|
-
|
|
20427
|
+
writeFileSync27(cfg.configFile, planEnable(readFileSync35(cfg.configFile, "utf8"), a.route));
|
|
20010
20428
|
enabledSound.add(a.route);
|
|
20011
20429
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
20012
20430
|
} else {
|
|
@@ -20150,6 +20568,23 @@ ${changed2.map((c) => ` - ${c}
|
|
|
20150
20568
|
}
|
|
20151
20569
|
if (!dryRun && !c.enabled) return { scope, tests: "skipped", outcome: "skipped", detail: "route not enabled" };
|
|
20152
20570
|
const releaseIdentity = await resolveReleaseKey(c);
|
|
20571
|
+
const releaseRouteSlug = routeSlug(c.route);
|
|
20572
|
+
if (releaseIdentity.kind === "ship" && releaseIdentity.storeError) {
|
|
20573
|
+
const detail = `release refused: its agent key cannot be read (${releaseIdentity.storeError})`;
|
|
20574
|
+
remit.warn(`${scope}: ${detail}`, { step: "release" });
|
|
20575
|
+
try {
|
|
20576
|
+
await raiseShipAttention(
|
|
20577
|
+
{ state: new State(cfg.ship.stateDir), emit: remit, route: c, ship: cfg.ship },
|
|
20578
|
+
seatKeyUnreadableItem(releaseRouteSlug, "release", releaseIdentity.storeError)
|
|
20579
|
+
);
|
|
20580
|
+
} catch {
|
|
20581
|
+
}
|
|
20582
|
+
return { scope, tests: "skipped", outcome: "skipped", detail };
|
|
20583
|
+
}
|
|
20584
|
+
try {
|
|
20585
|
+
clearShipAttention(new State(cfg.ship.stateDir), seatKeyUnreadableKey(releaseRouteSlug, "release"));
|
|
20586
|
+
} catch {
|
|
20587
|
+
}
|
|
20153
20588
|
if (releaseIdentity.kind === "ship") remit.warn(`release writes as the ship, not its own agent: ${releaseIdentity.reason}`, { step: "release" });
|
|
20154
20589
|
const tracker = new Tracker(c, cfg.ship, { apiKey: releaseIdentity.apiKey });
|
|
20155
20590
|
if (!dryRun) {
|
|
@@ -20599,8 +21034,8 @@ ${cu.tail.trimEnd()}
|
|
|
20599
21034
|
`swept ${r.parked} parked, ${r.restored} restored${r.failed ? `, ${r.failed} failed` : ""}${r.contended ? `, ${r.contended} contended` : ""} (${e.route.route})`,
|
|
20600
21035
|
{ step: "sweep", data: r }
|
|
20601
21036
|
);
|
|
20602
|
-
} catch (
|
|
20603
|
-
remit.warn(`could not sweep ${e.route.route}: ${
|
|
21037
|
+
} catch (err3) {
|
|
21038
|
+
remit.warn(`could not sweep ${e.route.route}: ${err3.message}`, { step: "sweep" });
|
|
20604
21039
|
}
|
|
20605
21040
|
}
|
|
20606
21041
|
process.stdout.write(renderFleet(fleet, state));
|
|
@@ -21110,27 +21545,36 @@ ${cu.tail.trimEnd()}
|
|
|
21110
21545
|
process.exit(1);
|
|
21111
21546
|
}
|
|
21112
21547
|
requireArmed("run the daemon loop");
|
|
21113
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
21548
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join44(CREW_HOME, "src"), join44(CREW_HOME, "dist"), join44(CREW_HOME, "bin")];
|
|
21114
21549
|
const configPaths = daemonConfigPaths(cfg);
|
|
21115
21550
|
watchPaths.push(...configPaths);
|
|
21116
21551
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
21117
21552
|
const spawnBreaker = new SpawnBreaker();
|
|
21118
21553
|
const controlRoutes = cfg.routes.filter((r) => r.resolved?.workspaceId);
|
|
21554
|
+
const controlAuth = new ControlAuth({
|
|
21555
|
+
baseUrl: route2.baseUrl || DEFAULT_BASE_URL,
|
|
21556
|
+
workspaceIds: [...new Set(controlRoutes.map((r) => r.resolved.workspaceId))],
|
|
21557
|
+
shipKeys: () => cfg.routes.flatMap((r) => {
|
|
21558
|
+
try {
|
|
21559
|
+
return [resolveApiKey(r)];
|
|
21560
|
+
} catch {
|
|
21561
|
+
return [];
|
|
21562
|
+
}
|
|
21563
|
+
}),
|
|
21564
|
+
userAgent: cfg.ship.userAgent
|
|
21565
|
+
});
|
|
21119
21566
|
const controlServer = await startControlServer({
|
|
21120
21567
|
stateDir: cfg.ship.stateDir,
|
|
21121
21568
|
state,
|
|
21122
|
-
auth:
|
|
21123
|
-
|
|
21124
|
-
|
|
21125
|
-
|
|
21126
|
-
|
|
21127
|
-
|
|
21128
|
-
|
|
21129
|
-
|
|
21130
|
-
|
|
21131
|
-
}),
|
|
21132
|
-
userAgent: cfg.ship.userAgent
|
|
21133
|
-
}),
|
|
21569
|
+
auth: controlAuth,
|
|
21570
|
+
// CREW-2859: Passenger periscopes, only on a route that sets hostPeriscopes.
|
|
21571
|
+
periscopes: {
|
|
21572
|
+
routes: controlRoutes.filter((r) => r.enabled).map((r) => ({ route: r.route, workspaceId: r.resolved.workspaceId, hostPeriscopes: r.hostPeriscopes === true })),
|
|
21573
|
+
previewHost: cfg.ship.previewHost,
|
|
21574
|
+
maxPeriscopes: cfg.ship.maxPeriscopes,
|
|
21575
|
+
run: runCrew,
|
|
21576
|
+
verifyIn: (token, ws) => controlAuth.verifyIn(token, ws)
|
|
21577
|
+
},
|
|
21134
21578
|
status: () => crewJson(["status", route2.route, "--json"]),
|
|
21135
21579
|
doctor: () => crewJson(["doctor", route2.route, "--json", "--offline"]),
|
|
21136
21580
|
scheduler: schedulerHandler(planDaemonControl(CREW_HOME, hostPlatform(), detectSystemd())),
|
|
@@ -21155,11 +21599,11 @@ ${cu.tail.trimEnd()}
|
|
|
21155
21599
|
`)
|
|
21156
21600
|
})
|
|
21157
21601
|
}),
|
|
21158
|
-
onTransient: ({ err:
|
|
21159
|
-
`tracker unavailable (${describeCause(
|
|
21602
|
+
onTransient: ({ err: err3, consecutive, backoffMs }) => emit.warn(
|
|
21603
|
+
`tracker unavailable (${describeCause(err3)}); attempt ${consecutive}, retrying in ${Math.round(backoffMs / 1e3)}s`
|
|
21160
21604
|
),
|
|
21161
|
-
onOutage: ({ err:
|
|
21162
|
-
const msg = `tracker unreachable for ${Math.max(1, Math.round(downMs / 6e4))} minutes: ${describeCause(
|
|
21605
|
+
onOutage: ({ err: err3, downMs }) => {
|
|
21606
|
+
const msg = `tracker unreachable for ${Math.max(1, Math.round(downMs / 6e4))} minutes: ${describeCause(err3)}`;
|
|
21163
21607
|
emit.error(msg);
|
|
21164
21608
|
void raiseShipAttention({ state, emit, route: route2, ship: cfg.ship }, {
|
|
21165
21609
|
kind: "tracker_unreachable",
|
|
@@ -21172,8 +21616,8 @@ ${cu.tail.trimEnd()}
|
|
|
21172
21616
|
clearShipAttention(state, TRACKER_OUTAGE_KEY);
|
|
21173
21617
|
emit.emit(`tracker is back after ${Math.max(1, Math.round(downMs / 1e3))}s`);
|
|
21174
21618
|
},
|
|
21175
|
-
onPassError: (
|
|
21176
|
-
`daemon pass failed, continuing: ${
|
|
21619
|
+
onPassError: (err3) => emit.error(
|
|
21620
|
+
`daemon pass failed, continuing: ${err3 instanceof Error ? err3.stack ?? err3.message : String(err3)}`
|
|
21177
21621
|
),
|
|
21178
21622
|
checkStale: makeStaleChecker(watchPaths, { reloadSentinel: reloadSentinelFor(cfg.ship.stateDir) }),
|
|
21179
21623
|
onPass: (result) => {
|
|
@@ -21362,10 +21806,10 @@ ${cu.tail.trimEnd()}
|
|
|
21362
21806
|
`);
|
|
21363
21807
|
if (!dryRun) {
|
|
21364
21808
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
21365
|
-
const raw =
|
|
21809
|
+
const raw = existsSync40(resolvedPath) ? JSON.parse(readFileSync35(resolvedPath, "utf8")) : {};
|
|
21366
21810
|
raw.agentSkills = result.agentSkills;
|
|
21367
|
-
|
|
21368
|
-
|
|
21811
|
+
mkdirSync26(dirOf2(resolvedPath), { recursive: true });
|
|
21812
|
+
writeFileSync27(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
21369
21813
|
`);
|
|
21370
21814
|
}
|
|
21371
21815
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -21388,7 +21832,7 @@ ${cu.tail.trimEnd()}
|
|
|
21388
21832
|
throw e;
|
|
21389
21833
|
}
|
|
21390
21834
|
const oldName = cfg.ship.name;
|
|
21391
|
-
|
|
21835
|
+
writeFileSync27(cfg.configFile, setShipNameInYaml(readFileSync35(cfg.configFile, "utf8"), newName));
|
|
21392
21836
|
cfg.ship.name = newName;
|
|
21393
21837
|
let renameMachineId;
|
|
21394
21838
|
try {
|
|
@@ -21435,7 +21879,7 @@ ${cu.tail.trimEnd()}
|
|
|
21435
21879
|
process.exit(2);
|
|
21436
21880
|
};
|
|
21437
21881
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
|
|
21438
|
-
const configText =
|
|
21882
|
+
const configText = readFileSync35(cfg.configFile, "utf8");
|
|
21439
21883
|
if (sub === "check") {
|
|
21440
21884
|
try {
|
|
21441
21885
|
const routeArg = value("route") ?? positional[2];
|
|
@@ -21466,8 +21910,8 @@ ${cu.tail.trimEnd()}
|
|
|
21466
21910
|
try {
|
|
21467
21911
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
21468
21912
|
const dir = repoDirOf(configText, dirname22(cfg.configFile), a.route, a.name);
|
|
21469
|
-
if (!
|
|
21470
|
-
const files = { exists: (r) =>
|
|
21913
|
+
if (!existsSync40(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
21914
|
+
const files = { exists: (r) => existsSync40(resolvePath(dir, r)), list: (r) => {
|
|
21471
21915
|
try {
|
|
21472
21916
|
return readdirSync18(resolvePath(dir, r));
|
|
21473
21917
|
} catch {
|
|
@@ -21475,7 +21919,7 @@ ${cu.tail.trimEnd()}
|
|
|
21475
21919
|
}
|
|
21476
21920
|
}, read: (r) => {
|
|
21477
21921
|
try {
|
|
21478
|
-
return
|
|
21922
|
+
return readFileSync35(resolvePath(dir, r), "utf8");
|
|
21479
21923
|
} catch {
|
|
21480
21924
|
return void 0;
|
|
21481
21925
|
}
|
|
@@ -21517,19 +21961,19 @@ ${cu.tail.trimEnd()}
|
|
|
21517
21961
|
let cleared;
|
|
21518
21962
|
if (clear2) {
|
|
21519
21963
|
const plan = clearRouteHooks(configText, a.route, a.name);
|
|
21520
|
-
if (plan.removed.length > 0)
|
|
21964
|
+
if (plan.removed.length > 0) writeFileSync27(cfg.configFile, plan.text);
|
|
21521
21965
|
cleared = { file: cfg.configFile, removed: plan.removed };
|
|
21522
21966
|
}
|
|
21523
21967
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
21524
21968
|
if (write === "route") {
|
|
21525
21969
|
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
|
|
21526
|
-
|
|
21970
|
+
writeFileSync27(cfg.configFile, plan.text);
|
|
21527
21971
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
|
|
21528
21972
|
} else if (write === "repo") {
|
|
21529
21973
|
const have = existingRepoConfig(files);
|
|
21530
21974
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
21531
21975
|
const file = resolvePath(dir, ".crew.yaml");
|
|
21532
|
-
|
|
21976
|
+
writeFileSync27(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
21533
21977
|
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
|
|
21534
21978
|
}
|
|
21535
21979
|
if (flag("json")) {
|
|
@@ -21585,7 +22029,7 @@ ${cu.tail.trimEnd()}
|
|
|
21585
22029
|
if (sub === "list" && !routeName) usage2();
|
|
21586
22030
|
if (sub === "add") {
|
|
21587
22031
|
try {
|
|
21588
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
22032
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync40(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
|
|
21589
22033
|
routeName = a.route;
|
|
21590
22034
|
addPath = a.path;
|
|
21591
22035
|
addName = a.name;
|
|
@@ -21614,16 +22058,16 @@ ${cu.tail.trimEnd()}
|
|
|
21614
22058
|
}
|
|
21615
22059
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
21616
22060
|
let resolvedRemotes;
|
|
21617
|
-
if (
|
|
22061
|
+
if (existsSync40(resolvedFile)) {
|
|
21618
22062
|
try {
|
|
21619
|
-
resolvedRemotes = JSON.parse(
|
|
22063
|
+
resolvedRemotes = JSON.parse(readFileSync35(resolvedFile, "utf8"));
|
|
21620
22064
|
} catch {
|
|
21621
22065
|
}
|
|
21622
22066
|
}
|
|
21623
22067
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
21624
22068
|
const text = configText;
|
|
21625
22069
|
const base = dirname22(cfg.configFile);
|
|
21626
|
-
const isCheckout = (d) =>
|
|
22070
|
+
const isCheckout = (d) => existsSync40(`${d}/.git`);
|
|
21627
22071
|
try {
|
|
21628
22072
|
if (sub === "list") {
|
|
21629
22073
|
if (flag("json")) {
|
|
@@ -21633,11 +22077,11 @@ ${cu.tail.trimEnd()}
|
|
|
21633
22077
|
base,
|
|
21634
22078
|
resolved: resolvedRemotes,
|
|
21635
22079
|
originOf,
|
|
21636
|
-
exists: (d) =>
|
|
22080
|
+
exists: (d) => existsSync40(d),
|
|
21637
22081
|
isCheckout,
|
|
21638
22082
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
21639
22083
|
try {
|
|
21640
|
-
return Object.keys(parseRepoConfig(
|
|
22084
|
+
return Object.keys(parseRepoConfig(readFileSync35(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
21641
22085
|
} catch {
|
|
21642
22086
|
return false;
|
|
21643
22087
|
}
|
|
@@ -21654,7 +22098,7 @@ ${cu.tail.trimEnd()}
|
|
|
21654
22098
|
for (const r of repos) {
|
|
21655
22099
|
const origin2 = originOf(r.dir);
|
|
21656
22100
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
21657
|
-
const note = !
|
|
22101
|
+
const note = !existsSync40(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
21658
22102
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
21659
22103
|
`);
|
|
21660
22104
|
}
|
|
@@ -21666,9 +22110,9 @@ ${cu.tail.trimEnd()}
|
|
|
21666
22110
|
` : `${human}
|
|
21667
22111
|
`);
|
|
21668
22112
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
21669
|
-
if (
|
|
22113
|
+
if (existsSync40(plan2.target) && (existsSync40(`${plan2.target}/.git`) || readdirSync18(plan2.target).length > 0)) {
|
|
21670
22114
|
throw new ReposError(
|
|
21671
|
-
|
|
22115
|
+
existsSync40(`${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`
|
|
21672
22116
|
);
|
|
21673
22117
|
}
|
|
21674
22118
|
if (dryRun) {
|
|
@@ -21686,14 +22130,14 @@ ${cu.tail.trimEnd()}
|
|
|
21686
22130
|
throw new ReposError(`${e.message}${authHint}
|
|
21687
22131
|
crew.yaml was not changed`);
|
|
21688
22132
|
}
|
|
21689
|
-
|
|
22133
|
+
writeFileSync27(cfg.configFile, plan2.text);
|
|
21690
22134
|
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:
|
|
21691
22135
|
`);
|
|
21692
22136
|
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}`);
|
|
21693
22137
|
break;
|
|
21694
22138
|
}
|
|
21695
22139
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
|
|
21696
|
-
const top =
|
|
22140
|
+
const top = existsSync40(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
21697
22141
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
21698
22142
|
const origin = originOf(top);
|
|
21699
22143
|
const plan = planRepoAdd({
|
|
@@ -21745,15 +22189,15 @@ ${cu.tail.trimEnd()}
|
|
|
21745
22189
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
21746
22190
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
21747
22191
|
`);
|
|
21748
|
-
if (
|
|
21749
|
-
const raw = JSON.parse(
|
|
22192
|
+
if (existsSync40(resolvedFile)) {
|
|
22193
|
+
const raw = JSON.parse(readFileSync35(resolvedFile, "utf8"));
|
|
21750
22194
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
21751
22195
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
21752
|
-
|
|
22196
|
+
writeFileSync27(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
21753
22197
|
`);
|
|
21754
22198
|
}
|
|
21755
22199
|
}
|
|
21756
|
-
|
|
22200
|
+
writeFileSync27(cfg.configFile, plan.text);
|
|
21757
22201
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
21758
22202
|
`);
|
|
21759
22203
|
} catch (e) {
|
|
@@ -21852,7 +22296,7 @@ ${reconstruction.prompt}
|
|
|
21852
22296
|
if (flag("sign-out")) {
|
|
21853
22297
|
let signOutStore;
|
|
21854
22298
|
try {
|
|
21855
|
-
signOutStore =
|
|
22299
|
+
signOutStore = getSessionStore();
|
|
21856
22300
|
} catch {
|
|
21857
22301
|
signOutStore = void 0;
|
|
21858
22302
|
}
|
|
@@ -21892,7 +22336,7 @@ ${reconstruction.prompt}
|
|
|
21892
22336
|
const givenKeySource = value("key") ?? process.env.CREW_CONNECT_KEY ? "flag" : "config";
|
|
21893
22337
|
let store;
|
|
21894
22338
|
try {
|
|
21895
|
-
store =
|
|
22339
|
+
store = getSessionStore();
|
|
21896
22340
|
} catch {
|
|
21897
22341
|
store = void 0;
|
|
21898
22342
|
}
|
|
@@ -22120,7 +22564,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22120
22564
|
process.exit(2);
|
|
22121
22565
|
}
|
|
22122
22566
|
if (found.shipNameAdopted && !dryRun) {
|
|
22123
|
-
|
|
22567
|
+
writeFileSync27(cfg.configFile, setShipNameInYaml(readFileSync35(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
22124
22568
|
cfg.ship.name = found.shipNameAdopted;
|
|
22125
22569
|
}
|
|
22126
22570
|
if (found.shipRow) {
|
|
@@ -22139,8 +22583,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22139
22583
|
}
|
|
22140
22584
|
const capabilityMisses = capabilities.filter((i) => i.status === "missing").map(missLine);
|
|
22141
22585
|
if (found.locksModelId && !dryRun) {
|
|
22142
|
-
const
|
|
22143
|
-
const repoNames = [...Object.values(found.repoNames ?? {}), ...
|
|
22586
|
+
const configured2 = cfg.routes.find((r) => r.route === route3);
|
|
22587
|
+
const repoNames = [...Object.values(found.repoNames ?? {}), ...configured2 ? reposOf(configured2).map((t) => t.name) : []].filter((n) => !!n);
|
|
22144
22588
|
try {
|
|
22145
22589
|
const made = await provisionReleaseLocks(authOpts, found.locksModelId, route3, repoNames);
|
|
22146
22590
|
for (const sc of made) process.stderr.write(`created release lock row ${sc}
|
|
@@ -22163,10 +22607,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22163
22607
|
}
|
|
22164
22608
|
}
|
|
22165
22609
|
let previous;
|
|
22166
|
-
const hadPreviousFile =
|
|
22610
|
+
const hadPreviousFile = existsSync40(resolvedPath);
|
|
22167
22611
|
if (hadPreviousFile) {
|
|
22168
22612
|
try {
|
|
22169
|
-
previous = JSON.parse(
|
|
22613
|
+
previous = JSON.parse(readFileSync35(resolvedPath, "utf8"));
|
|
22170
22614
|
} catch {
|
|
22171
22615
|
}
|
|
22172
22616
|
}
|
|
@@ -22295,12 +22739,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22295
22739
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
22296
22740
|
`);
|
|
22297
22741
|
} else {
|
|
22298
|
-
|
|
22742
|
+
mkdirSync26(dirname22(resolvedPath), { recursive: true });
|
|
22299
22743
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
22300
22744
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
22301
|
-
|
|
22745
|
+
writeFileSync27(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
22302
22746
|
`);
|
|
22303
|
-
|
|
22747
|
+
renameSync4(tmpPath, resolvedPath);
|
|
22304
22748
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
22305
22749
|
`);
|
|
22306
22750
|
}
|
|
@@ -22344,8 +22788,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22344
22788
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
22345
22789
|
`);
|
|
22346
22790
|
} else {
|
|
22347
|
-
|
|
22348
|
-
|
|
22791
|
+
mkdirSync26(dirname22(apiKeyFile), { recursive: true });
|
|
22792
|
+
writeFileSync27(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
22349
22793
|
`, { mode: 384 });
|
|
22350
22794
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
22351
22795
|
`);
|
|
@@ -23079,7 +23523,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23079
23523
|
const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
|
|
23080
23524
|
const resumed = await runPeriscopeResume({
|
|
23081
23525
|
find: async () => hit && { row: hit.row, tag: hit.tag },
|
|
23082
|
-
worktreeExists: (p) =>
|
|
23526
|
+
worktreeExists: (p) => existsSync40(p),
|
|
23083
23527
|
caps: periscopeCaps(cfg.ship),
|
|
23084
23528
|
runHook: async (name, cwd, env) => {
|
|
23085
23529
|
const script = hit?.repo.config.hooks[name];
|
|
@@ -23098,7 +23542,7 @@ ${resumed.url}
|
|
|
23098
23542
|
`);
|
|
23099
23543
|
break;
|
|
23100
23544
|
}
|
|
23101
|
-
const parsedUp = parsePeriscopeUpArgs(positional, { topic: value("topic"), issue: value("issue"), host: value("host"), assignee: value("assignee"), isolate: flag("isolate"), json: jsonP });
|
|
23545
|
+
const parsedUp = parsePeriscopeUpArgs(positional, { topic: value("topic"), issue: value("issue"), host: value("host"), assignee: value("assignee"), requestedBy: value("requested-by"), isolate: flag("isolate"), json: jsonP });
|
|
23102
23546
|
let a;
|
|
23103
23547
|
if (parsedUp.repo !== void 0) a = { ...parsedUp, repo: parsedUp.repo };
|
|
23104
23548
|
else {
|
|
@@ -23156,7 +23600,7 @@ ${where.join("\n")}`);
|
|
|
23156
23600
|
title,
|
|
23157
23601
|
description: `A periscope: a throwaway preview of ${a.repo} for a viewer, opened by \`crew periscope up\`.`,
|
|
23158
23602
|
report_type: "feature",
|
|
23159
|
-
reporter_name: operatorName,
|
|
23603
|
+
reporter_name: a.requestedBy ?? operatorName,
|
|
23160
23604
|
[cP.columns.status]: cP.statuses.draft,
|
|
23161
23605
|
[cP.columns.assignee]: pairId,
|
|
23162
23606
|
...routeP.resolved.areaId ? { [cP.columns.slice]: routeP.resolved.areaId } : {},
|
|
@@ -23173,10 +23617,10 @@ ${where.join("\n")}`);
|
|
|
23173
23617
|
},
|
|
23174
23618
|
prepare: async (path) => {
|
|
23175
23619
|
for (const f of repoP.config.worktrees.copy) {
|
|
23176
|
-
const src =
|
|
23177
|
-
if (!
|
|
23178
|
-
|
|
23179
|
-
copyFileSync2(src,
|
|
23620
|
+
const src = join44(repoP.dir, f);
|
|
23621
|
+
if (!existsSync40(src)) continue;
|
|
23622
|
+
mkdirSync26(dirOf2(join44(path, f)), { recursive: true });
|
|
23623
|
+
copyFileSync2(src, join44(path, f));
|
|
23180
23624
|
}
|
|
23181
23625
|
const setup = repoP.config.hooks.setup;
|
|
23182
23626
|
return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
|
|
@@ -23402,7 +23846,7 @@ ${where.join("\n")}`);
|
|
|
23402
23846
|
const host = hostPlatform();
|
|
23403
23847
|
let resetStore;
|
|
23404
23848
|
try {
|
|
23405
|
-
resetStore =
|
|
23849
|
+
resetStore = getSessionStore();
|
|
23406
23850
|
} catch {
|
|
23407
23851
|
resetStore = void 0;
|
|
23408
23852
|
}
|
|
@@ -23451,7 +23895,7 @@ ${where.join("\n")}`);
|
|
|
23451
23895
|
};
|
|
23452
23896
|
const droppedRoutes = [];
|
|
23453
23897
|
try {
|
|
23454
|
-
const rawCfg = parseYaml2(
|
|
23898
|
+
const rawCfg = parseYaml2(readFileSync35(cfg.configFile, "utf8"));
|
|
23455
23899
|
for (const r of rawCfg?.routes ?? []) {
|
|
23456
23900
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
23457
23901
|
try {
|
|
@@ -23493,8 +23937,8 @@ ${where.join("\n")}`);
|
|
|
23493
23937
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
23494
23938
|
break;
|
|
23495
23939
|
}
|
|
23496
|
-
|
|
23497
|
-
|
|
23940
|
+
mkdirSync26(cfg.ship.stateDir, { recursive: true });
|
|
23941
|
+
writeFileSync27(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
23498
23942
|
`);
|
|
23499
23943
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
23500
23944
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -23545,7 +23989,7 @@ ${where.join("\n")}`);
|
|
|
23545
23989
|
}
|
|
23546
23990
|
case "log": {
|
|
23547
23991
|
try {
|
|
23548
|
-
const text =
|
|
23992
|
+
const text = readFileSync35(cfg.ship.logFile, "utf8").split("\n");
|
|
23549
23993
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
23550
23994
|
`);
|
|
23551
23995
|
} catch {
|
|
@@ -23568,10 +24012,10 @@ ${where.join("\n")}`);
|
|
|
23568
24012
|
};
|
|
23569
24013
|
if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
|
|
23570
24014
|
const tracker = new Tracker(route2, cfg.ship);
|
|
23571
|
-
const configText =
|
|
24015
|
+
const configText = readFileSync35(cfg.configFile, "utf8");
|
|
23572
24016
|
const cfgBase = dirname22(cfg.configFile);
|
|
23573
|
-
const
|
|
23574
|
-
const dir =
|
|
24017
|
+
const configured2 = configuredRepos(configText, route2.route, cfgBase);
|
|
24018
|
+
const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
|
|
23575
24019
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
|
|
23576
24020
|
const rows = await tracker.repoRows();
|
|
23577
24021
|
const row = rows.find((r) => r.name === SAMPLE_REPO);
|
|
@@ -23595,9 +24039,9 @@ ${where.join("\n")}`);
|
|
|
23595
24039
|
say("dry-run", { dir, row: row?.id ?? null, closed }, `would close ${closed.join(", ") || "no tickets"}, delete the Repos row, detach and delete ${dir}`);
|
|
23596
24040
|
break;
|
|
23597
24041
|
}
|
|
23598
|
-
if (
|
|
24042
|
+
if (configured2.some((r) => r.dir === dir)) {
|
|
23599
24043
|
try {
|
|
23600
|
-
|
|
24044
|
+
writeFileSync27(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
|
|
23601
24045
|
} catch (e) {
|
|
23602
24046
|
if (e instanceof ReposError) fail(e.message);
|
|
23603
24047
|
throw e;
|
|
@@ -23605,29 +24049,29 @@ ${where.join("\n")}`);
|
|
|
23605
24049
|
}
|
|
23606
24050
|
if (row) {
|
|
23607
24051
|
await tracker.deleteRepoRow(row.id);
|
|
23608
|
-
if (
|
|
23609
|
-
const raw = JSON.parse(
|
|
24052
|
+
if (existsSync40(resolvedFile)) {
|
|
24053
|
+
const raw = JSON.parse(readFileSync35(resolvedFile, "utf8"));
|
|
23610
24054
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
23611
24055
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
23612
|
-
|
|
24056
|
+
writeFileSync27(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
23613
24057
|
`);
|
|
23614
24058
|
}
|
|
23615
24059
|
}
|
|
23616
|
-
if (
|
|
24060
|
+
if (existsSync40(join44(dir, ".git")) && basename13(dir) === SAMPLE_REPO) rmSync19(dir, { recursive: true, force: true });
|
|
23617
24061
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
23618
24062
|
break;
|
|
23619
24063
|
}
|
|
23620
24064
|
const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
23621
|
-
const fresh = !
|
|
23622
|
-
if (
|
|
24065
|
+
const fresh = !existsSync40(join44(dir, ".git"));
|
|
24066
|
+
if (existsSync40(dir) && fresh && readdirSync18(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
23623
24067
|
if (dryRun) {
|
|
23624
24068
|
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`);
|
|
23625
24069
|
break;
|
|
23626
24070
|
}
|
|
23627
24071
|
if (fresh) {
|
|
23628
|
-
|
|
23629
|
-
|
|
23630
|
-
|
|
24072
|
+
mkdirSync26(dir, { recursive: true });
|
|
24073
|
+
writeFileSync27(join44(dir, "README.md"), SAMPLE_README);
|
|
24074
|
+
writeFileSync27(join44(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
23631
24075
|
const g = (...a) => {
|
|
23632
24076
|
const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
|
|
23633
24077
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -23645,17 +24089,17 @@ ${where.join("\n")}`);
|
|
|
23645
24089
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
23646
24090
|
});
|
|
23647
24091
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
23648
|
-
if (
|
|
23649
|
-
const raw = JSON.parse(
|
|
24092
|
+
if (existsSync40(resolvedFile)) {
|
|
24093
|
+
const raw = JSON.parse(readFileSync35(resolvedFile, "utf8"));
|
|
23650
24094
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
23651
24095
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
23652
|
-
|
|
24096
|
+
writeFileSync27(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
23653
24097
|
`);
|
|
23654
24098
|
}
|
|
23655
24099
|
}
|
|
23656
|
-
if (!
|
|
24100
|
+
if (!configured2.some((r) => r.dir === dir)) {
|
|
23657
24101
|
try {
|
|
23658
|
-
|
|
24102
|
+
writeFileSync27(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
|
|
23659
24103
|
} catch (e) {
|
|
23660
24104
|
if (e instanceof ReposError) fail(e.message);
|
|
23661
24105
|
throw e;
|
|
@@ -23990,7 +24434,7 @@ watermark: ${state.watermark()}
|
|
|
23990
24434
|
}
|
|
23991
24435
|
for (const r of repos) {
|
|
23992
24436
|
const remote = r.config.branch.remote;
|
|
23993
|
-
if (
|
|
24437
|
+
if (existsSync40(r.dir)) {
|
|
23994
24438
|
pushAuth.push({
|
|
23995
24439
|
route: c.route,
|
|
23996
24440
|
repo: r.name,
|
|
@@ -24063,6 +24507,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
24063
24507
|
for (const w of roleCapWarnings(cfg.ship)) process.stdout.write(`role concurrency: WARN ${w}
|
|
24064
24508
|
`);
|
|
24065
24509
|
process.stdout.write(`periscopes: ${periscopeSummary(cfg)}
|
|
24510
|
+
`);
|
|
24511
|
+
process.stdout.write(`key store: ${describeKeyStore(cfg.ship)}
|
|
24066
24512
|
`);
|
|
24067
24513
|
const previewProblem = await previewHostProblem(cfg);
|
|
24068
24514
|
if (previewProblem) {
|
|
@@ -24073,7 +24519,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
24073
24519
|
}
|
|
24074
24520
|
try {
|
|
24075
24521
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
24076
|
-
const evSize =
|
|
24522
|
+
const evSize = existsSync40(evFile) ? statSync14(evFile).size : 0;
|
|
24077
24523
|
process.stdout.write(
|
|
24078
24524
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
24079
24525
|
` : "events log: ok\n"
|
|
@@ -24084,7 +24530,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
24084
24530
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
24085
24531
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
24086
24532
|
try {
|
|
24087
|
-
return
|
|
24533
|
+
return readFileSync35(p, "utf8");
|
|
24088
24534
|
} catch {
|
|
24089
24535
|
return null;
|
|
24090
24536
|
}
|
|
@@ -24100,7 +24546,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
24100
24546
|
const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
|
|
24101
24547
|
if (changed2) {
|
|
24102
24548
|
process.stdout.write(
|
|
24103
|
-
`daemon config: WARN ${changed2} changed at ${new Date(
|
|
24549
|
+
`daemon config: WARN ${changed2} changed at ${new Date(statSync14(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
|
|
24104
24550
|
`
|
|
24105
24551
|
);
|
|
24106
24552
|
}
|