@tablation/crew 0.99.1 → 0.100.1
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 +1038 -398
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -45,12 +45,12 @@ function periscopeSummary(cfg) {
|
|
|
45
45
|
if (routes.length === 0) return `off (no enabled route sets hostPeriscopes: true; ${limits})`;
|
|
46
46
|
return `ON for ${routes.join(", ")} (host ${cfg.ship.previewHost ?? "NOT SET (ship.previewHost)"}, ${limits})`;
|
|
47
47
|
}
|
|
48
|
-
async function previewHostProblem(cfg,
|
|
48
|
+
async function previewHostProblem(cfg, resolve7 = (h) => lookup(h)) {
|
|
49
49
|
if (periscopeRoutes(cfg).length === 0) return void 0;
|
|
50
50
|
const host = cfg.ship.previewHost;
|
|
51
51
|
if (!host) return "ship.previewHost is not set \u2014 a route hosts periscopes, so Passengers have no host to be sent to";
|
|
52
52
|
try {
|
|
53
|
-
await
|
|
53
|
+
await resolve7(host);
|
|
54
54
|
} catch {
|
|
55
55
|
return `ship.previewHost "${host}" does not resolve from this machine`;
|
|
56
56
|
}
|
|
@@ -140,11 +140,11 @@ function urlPort(url) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
function portListening(port, timeoutMs = 500) {
|
|
143
|
-
return new Promise((
|
|
143
|
+
return new Promise((resolve7) => {
|
|
144
144
|
const s = connect({ port, host: "127.0.0.1" });
|
|
145
145
|
const done = (ok) => {
|
|
146
146
|
s.destroy();
|
|
147
|
-
|
|
147
|
+
resolve7(ok);
|
|
148
148
|
};
|
|
149
149
|
s.setTimeout(timeoutMs, () => done(false));
|
|
150
150
|
s.once("connect", () => done(true));
|
|
@@ -583,8 +583,8 @@ function removeClaudeMcp(o) {
|
|
|
583
583
|
if (got.missing) return `mcp: \`${o.bin}\` not found, so Claude Code was not touched`;
|
|
584
584
|
if (got.status !== 0) return `mcp: no ${MCP_SERVER_NAME} server registered with Claude Code`;
|
|
585
585
|
if (o.dryRun) return `(dry run) would remove the ${MCP_SERVER_NAME} MCP server from Claude Code (user scope)`;
|
|
586
|
-
const
|
|
587
|
-
return
|
|
586
|
+
const rm2 = runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
|
|
587
|
+
return rm2.status === 0 ? `mcp: removed the ${MCP_SERVER_NAME} MCP server from Claude Code (user scope)` : `mcp: \`claude mcp remove\` failed (${rm2.stderr.trim() || `exit ${rm2.status}`})`;
|
|
588
588
|
}
|
|
589
589
|
|
|
590
590
|
// src/launcher.ts
|
|
@@ -620,10 +620,10 @@ function selectsCustomModelProvider(args) {
|
|
|
620
620
|
}
|
|
621
621
|
return false;
|
|
622
622
|
}
|
|
623
|
-
function parseOmlxSettings(
|
|
623
|
+
function parseOmlxSettings(text2) {
|
|
624
624
|
let raw;
|
|
625
625
|
try {
|
|
626
|
-
raw = JSON.parse(
|
|
626
|
+
raw = JSON.parse(text2);
|
|
627
627
|
} catch {
|
|
628
628
|
return void 0;
|
|
629
629
|
}
|
|
@@ -844,32 +844,32 @@ var BLOCK_RE = new RegExp(`${escapeRe(PAIR_BLOCK_START)}[\\s\\S]*?${escapeRe(PAI
|
|
|
844
844
|
function escapeRe(s) {
|
|
845
845
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
846
846
|
}
|
|
847
|
-
function hasPairBlock(
|
|
848
|
-
return BLOCK_RE.test(
|
|
847
|
+
function hasPairBlock(text2) {
|
|
848
|
+
return BLOCK_RE.test(text2);
|
|
849
849
|
}
|
|
850
|
-
function pairBlockIsBroken(
|
|
851
|
-
return !hasPairBlock(
|
|
850
|
+
function pairBlockIsBroken(text2) {
|
|
851
|
+
return !hasPairBlock(text2) && (text2.includes(PAIR_BLOCK_START) || text2.includes(PAIR_BLOCK_END));
|
|
852
852
|
}
|
|
853
|
-
function upsertPairBlock(
|
|
854
|
-
if (hasPairBlock(
|
|
853
|
+
function upsertPairBlock(text2, block) {
|
|
854
|
+
if (hasPairBlock(text2)) return text2.replace(BLOCK_RE, () => `${block}
|
|
855
855
|
`);
|
|
856
|
-
if (
|
|
856
|
+
if (text2.trim() === "") return `${block}
|
|
857
857
|
`;
|
|
858
|
-
return `${
|
|
858
|
+
return `${text2.replace(/\n*$/, "")}
|
|
859
859
|
|
|
860
860
|
${block}
|
|
861
861
|
`;
|
|
862
862
|
}
|
|
863
|
-
function removePairBlock(
|
|
864
|
-
if (!hasPairBlock(
|
|
865
|
-
return
|
|
863
|
+
function removePairBlock(text2) {
|
|
864
|
+
if (!hasPairBlock(text2)) return text2;
|
|
865
|
+
return text2.replace(new RegExp(`\\n*${BLOCK_RE.source}`), "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
|
|
866
866
|
}
|
|
867
867
|
function readIfExists(path) {
|
|
868
868
|
return existsSync5(path) ? readFileSync7(path, "utf8") : void 0;
|
|
869
869
|
}
|
|
870
|
-
function writeFileMkdir(path,
|
|
870
|
+
function writeFileMkdir(path, text2) {
|
|
871
871
|
mkdirSync4(dirname2(path), { recursive: true });
|
|
872
|
-
writeFileSync3(path,
|
|
872
|
+
writeFileSync3(path, text2);
|
|
873
873
|
}
|
|
874
874
|
function installBlockFile(path, harness, dryRun) {
|
|
875
875
|
const before = readIfExists(path) ?? "";
|
|
@@ -889,9 +889,9 @@ function removeBlockFile(path, dryRun) {
|
|
|
889
889
|
return true;
|
|
890
890
|
}
|
|
891
891
|
function blockVerdict(path, harness) {
|
|
892
|
-
const
|
|
893
|
-
if (
|
|
894
|
-
if (!
|
|
892
|
+
const text2 = readIfExists(path);
|
|
893
|
+
if (text2 === void 0 || !hasPairBlock(text2)) return { ok: false, detail: `no managed block in ${path}` };
|
|
894
|
+
if (!text2.includes(pairBlock(harness))) return { ok: false, detail: `the managed block in ${path} is out of date \u2014 run crew connect` };
|
|
895
895
|
return { ok: true, detail: "" };
|
|
896
896
|
}
|
|
897
897
|
var HEADER_RE = /^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(#.*)?$/;
|
|
@@ -913,24 +913,24 @@ function tomlTableRange(lines, name) {
|
|
|
913
913
|
return [start, end];
|
|
914
914
|
}
|
|
915
915
|
var tomlString = (v) => JSON.stringify(v);
|
|
916
|
-
function upsertTomlTable(
|
|
917
|
-
const lines =
|
|
916
|
+
function upsertTomlTable(text2, name, table2) {
|
|
917
|
+
const lines = text2.split("\n");
|
|
918
918
|
const r = tomlTableRange(lines, name);
|
|
919
919
|
if (r) {
|
|
920
920
|
lines.splice(r[0], r[1] - r[0], ...table2);
|
|
921
921
|
return lines.join("\n");
|
|
922
922
|
}
|
|
923
|
-
const body =
|
|
923
|
+
const body = text2.replace(/\n*$/, "");
|
|
924
924
|
return body === "" ? `${table2.join("\n")}
|
|
925
925
|
` : `${body}
|
|
926
926
|
|
|
927
927
|
${table2.join("\n")}
|
|
928
928
|
`;
|
|
929
929
|
}
|
|
930
|
-
function removeTomlTable(
|
|
931
|
-
const lines =
|
|
930
|
+
function removeTomlTable(text2, name) {
|
|
931
|
+
const lines = text2.split("\n");
|
|
932
932
|
let r = tomlTableRange(lines, name);
|
|
933
|
-
if (!r) return
|
|
933
|
+
if (!r) return text2;
|
|
934
934
|
while (r) {
|
|
935
935
|
let [start, end] = r;
|
|
936
936
|
if (start > 0 && lines[start - 1].trim() === "") start--;
|
|
@@ -1164,10 +1164,10 @@ var geminiPaths = (home) => ({
|
|
|
1164
1164
|
config: join8(home, ".gemini", "settings.json")
|
|
1165
1165
|
});
|
|
1166
1166
|
function readGeminiSettings(path) {
|
|
1167
|
-
const
|
|
1168
|
-
if (
|
|
1167
|
+
const text2 = readIfExists(path);
|
|
1168
|
+
if (text2 === void 0 || text2.trim() === "") return {};
|
|
1169
1169
|
try {
|
|
1170
|
-
const j = JSON.parse(
|
|
1170
|
+
const j = JSON.parse(text2);
|
|
1171
1171
|
return j && typeof j === "object" && !Array.isArray(j) ? j : void 0;
|
|
1172
1172
|
} catch {
|
|
1173
1173
|
return void 0;
|
|
@@ -1267,10 +1267,10 @@ function vscodeUserDirs(o) {
|
|
|
1267
1267
|
}
|
|
1268
1268
|
var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
|
|
1269
1269
|
function readVscodeMcp(path) {
|
|
1270
|
-
const
|
|
1271
|
-
if (
|
|
1270
|
+
const text2 = readIfExists(path);
|
|
1271
|
+
if (text2 === void 0 || text2.trim() === "") return {};
|
|
1272
1272
|
try {
|
|
1273
|
-
const j = JSON.parse(
|
|
1273
|
+
const j = JSON.parse(text2);
|
|
1274
1274
|
return j && typeof j === "object" && !Array.isArray(j) ? j : void 0;
|
|
1275
1275
|
} catch {
|
|
1276
1276
|
return void 0;
|
|
@@ -1446,9 +1446,9 @@ function mapGeminiLine(line) {
|
|
|
1446
1446
|
function createGeminiStreamMapper() {
|
|
1447
1447
|
let pending = "";
|
|
1448
1448
|
const flush = () => {
|
|
1449
|
-
const
|
|
1449
|
+
const text2 = pending;
|
|
1450
1450
|
pending = "";
|
|
1451
|
-
return
|
|
1451
|
+
return text2 ? [{ kind: "text", text: text2 }] : [];
|
|
1452
1452
|
};
|
|
1453
1453
|
return {
|
|
1454
1454
|
flush,
|
|
@@ -1758,9 +1758,9 @@ function storeForgeToken(forges, host, token, stateDir, env = process.env) {
|
|
|
1758
1758
|
if (!token.trim()) throw new ForgeError("empty token");
|
|
1759
1759
|
i.write(f.entry.token, token.trim(), { host: f.host, stateDir, env });
|
|
1760
1760
|
}
|
|
1761
|
-
function parseCredentialInput(
|
|
1761
|
+
function parseCredentialInput(text2) {
|
|
1762
1762
|
const out = {};
|
|
1763
|
-
for (const line of
|
|
1763
|
+
for (const line of text2.split("\n")) {
|
|
1764
1764
|
if (line === "") break;
|
|
1765
1765
|
const eq2 = line.indexOf("=");
|
|
1766
1766
|
if (eq2 > 0) out[line.slice(0, eq2)] = line.slice(eq2 + 1);
|
|
@@ -2148,10 +2148,10 @@ function rejectUnknown(obj, allowed, where, file) {
|
|
|
2148
2148
|
);
|
|
2149
2149
|
}
|
|
2150
2150
|
}
|
|
2151
|
-
function parseRepoConfig(
|
|
2151
|
+
function parseRepoConfig(text2, file) {
|
|
2152
2152
|
let raw;
|
|
2153
2153
|
try {
|
|
2154
|
-
raw = file.endsWith(".json") ? JSON.parse(
|
|
2154
|
+
raw = file.endsWith(".json") ? JSON.parse(text2) : parse(text2);
|
|
2155
2155
|
} catch (e) {
|
|
2156
2156
|
throw new RepoConfigError(`${file}: could not be parsed \u2014 ${e.message}`);
|
|
2157
2157
|
}
|
|
@@ -2667,9 +2667,9 @@ function parseShipEnv(v) {
|
|
|
2667
2667
|
if (typeof val !== "string" && typeof val !== "number" && typeof val !== "boolean") {
|
|
2668
2668
|
throw new ConfigError(`ship.env.${k} must be a string, number or boolean`);
|
|
2669
2669
|
}
|
|
2670
|
-
const
|
|
2671
|
-
if (/[\r\n\0]/.test(
|
|
2672
|
-
out[k] =
|
|
2670
|
+
const text2 = String(val);
|
|
2671
|
+
if (/[\r\n\0]/.test(text2)) throw new ConfigError(`ship.env.${k} must be a single line`);
|
|
2672
|
+
out[k] = text2;
|
|
2673
2673
|
}
|
|
2674
2674
|
return Object.keys(out).length ? out : void 0;
|
|
2675
2675
|
}
|
|
@@ -3639,10 +3639,10 @@ function runScript(script, opts) {
|
|
|
3639
3639
|
let output = "";
|
|
3640
3640
|
let pending = "";
|
|
3641
3641
|
const absorb = (buf) => {
|
|
3642
|
-
const
|
|
3643
|
-
output +=
|
|
3642
|
+
const text2 = buf.toString();
|
|
3643
|
+
output += text2;
|
|
3644
3644
|
if (!opts.onLine) return;
|
|
3645
|
-
pending +=
|
|
3645
|
+
pending += text2;
|
|
3646
3646
|
const lines = pending.split("\n");
|
|
3647
3647
|
pending = lines.pop() ?? "";
|
|
3648
3648
|
for (const l of lines) opts.onLine(l);
|
|
@@ -4062,9 +4062,9 @@ function planCron(ship, crewHome, job) {
|
|
|
4062
4062
|
function unitAbandonsAgents(plan, read) {
|
|
4063
4063
|
const path = plan.unitPaths[0];
|
|
4064
4064
|
if (!path || plan.mechanism !== "launchd" && plan.mechanism !== "systemd") return null;
|
|
4065
|
-
const
|
|
4066
|
-
if (
|
|
4067
|
-
return plan.mechanism === "launchd" ? /<key>AbandonProcessGroup<\/key>\s*<true\/>/.test(
|
|
4065
|
+
const text2 = read(path);
|
|
4066
|
+
if (text2 === null) return null;
|
|
4067
|
+
return plan.mechanism === "launchd" ? /<key>AbandonProcessGroup<\/key>\s*<true\/>/.test(text2) : /^KillMode=process\s*$/m.test(text2);
|
|
4068
4068
|
}
|
|
4069
4069
|
function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run", ctx = USER_INSTALL) {
|
|
4070
4070
|
if (hostShip === "macos") return planLaunchd(ship, crewHome, job);
|
|
@@ -4249,8 +4249,8 @@ function launchAgentsDir() {
|
|
|
4249
4249
|
function xmlUnescape(s) {
|
|
4250
4250
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
4251
4251
|
}
|
|
4252
|
-
function programFromPlist(
|
|
4253
|
-
const block = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(
|
|
4252
|
+
function programFromPlist(text2) {
|
|
4253
|
+
const block = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(text2);
|
|
4254
4254
|
if (!block) return null;
|
|
4255
4255
|
const args = [...block[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => xmlUnescape(m[1]));
|
|
4256
4256
|
return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
|
|
@@ -4316,9 +4316,9 @@ function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
|
|
|
4316
4316
|
function removeAllLaunchdUnits(dryRun, log, dir, loaded) {
|
|
4317
4317
|
for (const unit of listLaunchdUnits(dir, loaded)) removeLaunchdUnit(unit, dryRun, log);
|
|
4318
4318
|
}
|
|
4319
|
-
function parseLaunchctlList(
|
|
4319
|
+
function parseLaunchctlList(text2) {
|
|
4320
4320
|
const jobs = /* @__PURE__ */ new Map();
|
|
4321
|
-
for (const line of
|
|
4321
|
+
for (const line of text2.split("\n")) {
|
|
4322
4322
|
const cols = line.split(" ");
|
|
4323
4323
|
if (cols.length !== 3 || cols[2] === "Label") continue;
|
|
4324
4324
|
jobs.set(cols[2], { pid: cols[0], lastExit: cols[1] });
|
|
@@ -4342,8 +4342,8 @@ function readCrontab() {
|
|
|
4342
4342
|
const res = spawnSync4("crontab", ["-l"], { encoding: "utf8" });
|
|
4343
4343
|
return res.status === 0 ? res.stdout ?? "" : "";
|
|
4344
4344
|
}
|
|
4345
|
-
function writeCrontab(
|
|
4346
|
-
spawnSync4("crontab", ["-"], { input:
|
|
4345
|
+
function writeCrontab(text2) {
|
|
4346
|
+
spawnSync4("crontab", ["-"], { input: text2, encoding: "utf8" });
|
|
4347
4347
|
}
|
|
4348
4348
|
function addCrontabLine(marker, line) {
|
|
4349
4349
|
const existing = readCrontab();
|
|
@@ -5356,10 +5356,10 @@ function formatPushAuth(repo, r) {
|
|
|
5356
5356
|
return `${label}FAILED \u2014 ${r.detail}; ${r.hint}
|
|
5357
5357
|
`;
|
|
5358
5358
|
}
|
|
5359
|
-
async function settleRepos(items, nameOf,
|
|
5359
|
+
async function settleRepos(items, nameOf, resolve7) {
|
|
5360
5360
|
const out = await Promise.all(items.map(async (t) => {
|
|
5361
5361
|
try {
|
|
5362
|
-
return { ok: true, value: await
|
|
5362
|
+
return { ok: true, value: await resolve7(t) };
|
|
5363
5363
|
} catch (e) {
|
|
5364
5364
|
return { ok: false, repo: nameOf(t), detail: e.message };
|
|
5365
5365
|
}
|
|
@@ -6660,8 +6660,8 @@ async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /
|
|
|
6660
6660
|
|
|
6661
6661
|
// src/ticket-attribution.ts
|
|
6662
6662
|
var TICKET_KEY_RE = /\b[A-Z][A-Z0-9]{1,9}-\d+\b/g;
|
|
6663
|
-
function ticketMentions(
|
|
6664
|
-
return [...
|
|
6663
|
+
function ticketMentions(text2) {
|
|
6664
|
+
return [...text2.matchAll(TICKET_KEY_RE)].map((m) => m[0]);
|
|
6665
6665
|
}
|
|
6666
6666
|
var TicketAttributionTracker = class {
|
|
6667
6667
|
#tokensByTicket = /* @__PURE__ */ new Map();
|
|
@@ -6680,13 +6680,13 @@ var TicketAttributionTracker = class {
|
|
|
6680
6680
|
return this.#current;
|
|
6681
6681
|
}
|
|
6682
6682
|
/** Record the mentions in `text` without crediting tokens (a claim needs "which ticket" before the turn's usage line arrives). */
|
|
6683
|
-
note(
|
|
6684
|
-
const mentions = ticketMentions(
|
|
6683
|
+
note(text2) {
|
|
6684
|
+
const mentions = ticketMentions(text2);
|
|
6685
6685
|
if (mentions.length > 0) this.#current = mentions[0];
|
|
6686
6686
|
}
|
|
6687
6687
|
/** One assistant turn: its combined visible text (thinking + text blocks) and its own token cost. */
|
|
6688
|
-
add(
|
|
6689
|
-
const mentions = ticketMentions(
|
|
6688
|
+
add(text2, tokens) {
|
|
6689
|
+
const mentions = ticketMentions(text2);
|
|
6690
6690
|
if (mentions.length > 0) this.#current = mentions[0];
|
|
6691
6691
|
if (this.#current === void 0 || tokens <= 0) return;
|
|
6692
6692
|
this.#turns++;
|
|
@@ -6783,10 +6783,10 @@ function resultText(content) {
|
|
|
6783
6783
|
if (Array.isArray(content)) return content.map((c) => c && typeof c.text === "string" ? c.text : "").join("\n");
|
|
6784
6784
|
return "";
|
|
6785
6785
|
}
|
|
6786
|
-
function isTransportFailure(isError,
|
|
6786
|
+
function isTransportFailure(isError, text2) {
|
|
6787
6787
|
if (!isError) {
|
|
6788
6788
|
try {
|
|
6789
|
-
const doc = JSON.parse(
|
|
6789
|
+
const doc = JSON.parse(text2);
|
|
6790
6790
|
if (doc && typeof doc === "object") {
|
|
6791
6791
|
const code = doc.statusCode;
|
|
6792
6792
|
return typeof code === "number" && code >= 500;
|
|
@@ -6794,7 +6794,7 @@ function isTransportFailure(isError, text) {
|
|
|
6794
6794
|
} catch {
|
|
6795
6795
|
}
|
|
6796
6796
|
}
|
|
6797
|
-
return TRANSPORT_FAILURE.test(
|
|
6797
|
+
return TRANSPORT_FAILURE.test(text2);
|
|
6798
6798
|
}
|
|
6799
6799
|
var HandoffTranscriptTracker = class {
|
|
6800
6800
|
open = /* @__PURE__ */ new Map();
|
|
@@ -7218,6 +7218,12 @@ var Tracker = class {
|
|
|
7218
7218
|
if (!model) return [];
|
|
7219
7219
|
return this.client.records.list(model, { limit: 200 });
|
|
7220
7220
|
}
|
|
7221
|
+
/** Every `Repos` row with the columns the resolved file mirrors (CREW-2885). */
|
|
7222
|
+
async repoRowsForMap() {
|
|
7223
|
+
const model = this.route.resolved?.reposModelId;
|
|
7224
|
+
if (!model) return [];
|
|
7225
|
+
return this.client.records.list(model, { limit: 200 });
|
|
7226
|
+
}
|
|
7221
7227
|
/** Delete a `Repos` row (`crew sample-ticket --remove`). */
|
|
7222
7228
|
async deleteRepoRow(id) {
|
|
7223
7229
|
const model = this.route.resolved?.reposModelId;
|
|
@@ -8653,15 +8659,15 @@ async function replayPendingHandoffs(i) {
|
|
|
8653
8659
|
continue;
|
|
8654
8660
|
}
|
|
8655
8661
|
if (v.kind === "unverified") throw new Error(v.why);
|
|
8656
|
-
const
|
|
8657
|
-
if (
|
|
8658
|
-
const head = firstLine(
|
|
8662
|
+
const text2 = rec.comment ?? (rec.commentLanded ? "" : rec.finalText);
|
|
8663
|
+
if (text2) {
|
|
8664
|
+
const head = firstLine(text2);
|
|
8659
8665
|
const cc = i.contract.comments;
|
|
8660
8666
|
const seen = (await i.writer.comments()).some((c) => {
|
|
8661
8667
|
const row = c;
|
|
8662
8668
|
return row[cc.parent] === t.id && row[cc.author] === seatId && firstLine(String(row[cc.body] ?? "")) === head;
|
|
8663
8669
|
});
|
|
8664
|
-
if (!seen) await i.writer.postComment(t.id,
|
|
8670
|
+
if (!seen) await i.writer.postComment(t.id, text2, seatId);
|
|
8665
8671
|
}
|
|
8666
8672
|
await i.writer.postEvent(t.id, pushedEventBody(v.sha, v.remote, v.branch), seatId, "pushed");
|
|
8667
8673
|
await i.writer.updateTicket(t.id, rec.patch);
|
|
@@ -8690,15 +8696,15 @@ var SkillIncludeError = class extends Error {
|
|
|
8690
8696
|
var MAX_INCLUDE_DEPTH = 3;
|
|
8691
8697
|
var MAX_INCLUDED_BYTES = 2e5;
|
|
8692
8698
|
var MARKER = /\{\{[ \t]*skill:[ \t]*([^{}\r\n]*?)[ \t]*\}\}/g;
|
|
8693
|
-
function skillMarkerNames(
|
|
8699
|
+
function skillMarkerNames(text2) {
|
|
8694
8700
|
const names = [];
|
|
8695
|
-
for (const m of
|
|
8701
|
+
for (const m of text2.matchAll(MARKER)) {
|
|
8696
8702
|
const name = m[1].trim();
|
|
8697
8703
|
if (name && !names.includes(name)) names.push(name);
|
|
8698
8704
|
}
|
|
8699
8705
|
return names;
|
|
8700
8706
|
}
|
|
8701
|
-
function expandSkillMarkers(
|
|
8707
|
+
function expandSkillMarkers(text2, lookup2, opts = {}) {
|
|
8702
8708
|
const maxDepth = opts.maxDepth ?? MAX_INCLUDE_DEPTH;
|
|
8703
8709
|
const maxBytes = opts.maxBytes ?? MAX_INCLUDED_BYTES;
|
|
8704
8710
|
const warnings = [];
|
|
@@ -8725,7 +8731,7 @@ function expandSkillMarkers(text, lookup2, opts = {}) {
|
|
|
8725
8731
|
included += bytes;
|
|
8726
8732
|
return expand2(trimmed, [...chain, name]);
|
|
8727
8733
|
});
|
|
8728
|
-
return { text: expand2(
|
|
8734
|
+
return { text: expand2(text2, []), warnings };
|
|
8729
8735
|
}
|
|
8730
8736
|
|
|
8731
8737
|
// src/skills.ts
|
|
@@ -8736,9 +8742,9 @@ import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError
|
|
|
8736
8742
|
|
|
8737
8743
|
// src/prompt-additions.ts
|
|
8738
8744
|
var PROMPT_ADDITIONS_SEPARATOR = "---\n\nAdditional instructions from this workspace. Where they conflict with anything above, these take precedence.";
|
|
8739
|
-
function appendAdditions(
|
|
8740
|
-
if (typeof additions !== "string" || additions.trim() === "") return
|
|
8741
|
-
return `${
|
|
8745
|
+
function appendAdditions(text2, additions) {
|
|
8746
|
+
if (typeof additions !== "string" || additions.trim() === "") return text2;
|
|
8747
|
+
return `${text2}
|
|
8742
8748
|
|
|
8743
8749
|
${PROMPT_ADDITIONS_SEPARATOR}
|
|
8744
8750
|
|
|
@@ -9213,8 +9219,8 @@ function describeCrewLink(o) {
|
|
|
9213
9219
|
var SkillSyncError = class extends Error {
|
|
9214
9220
|
};
|
|
9215
9221
|
function parseSkillFile(path) {
|
|
9216
|
-
const
|
|
9217
|
-
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(
|
|
9222
|
+
const text2 = readFileSync18(path, "utf8");
|
|
9223
|
+
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text2);
|
|
9218
9224
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
9219
9225
|
const frontmatter = match[1];
|
|
9220
9226
|
const body = match[2];
|
|
@@ -9403,8 +9409,8 @@ function isEditedSkillRow(row, localPrompt, cached) {
|
|
|
9403
9409
|
function skillMarkerWarnings(promptsDir, personaPrompts) {
|
|
9404
9410
|
const shipped = new Set(listLocalSkills(promptsDir).map((s) => s.name));
|
|
9405
9411
|
const out = [];
|
|
9406
|
-
for (const [role,
|
|
9407
|
-
for (const name of skillMarkerNames(
|
|
9412
|
+
for (const [role, text2] of Object.entries(personaPrompts)) {
|
|
9413
|
+
for (const name of skillMarkerNames(text2 ?? "")) {
|
|
9408
9414
|
if (!shipped.has(name)) out.push(`${role}: brief includes {{skill: ${name}}} but this prompt set ships no skill with that name`);
|
|
9409
9415
|
}
|
|
9410
9416
|
}
|
|
@@ -9741,15 +9747,15 @@ var FileTail = class {
|
|
|
9741
9747
|
return "";
|
|
9742
9748
|
}
|
|
9743
9749
|
try {
|
|
9744
|
-
let
|
|
9750
|
+
let text2 = "";
|
|
9745
9751
|
const chunk = Buffer.alloc(64 * 1024);
|
|
9746
9752
|
for (; ; ) {
|
|
9747
9753
|
const n = readSync(fd, chunk, 0, chunk.length, this.offset);
|
|
9748
9754
|
if (n <= 0) break;
|
|
9749
9755
|
this.offset += n;
|
|
9750
|
-
|
|
9756
|
+
text2 += this.decoder.write(chunk.subarray(0, n));
|
|
9751
9757
|
}
|
|
9752
|
-
return
|
|
9758
|
+
return text2;
|
|
9753
9759
|
} finally {
|
|
9754
9760
|
closeSync3(fd);
|
|
9755
9761
|
}
|
|
@@ -9788,8 +9794,8 @@ var RunObserver = class {
|
|
|
9788
9794
|
this.liveTicket = id.ticket ?? void 0;
|
|
9789
9795
|
this.mapper = adapter.createStreamMapper?.() ?? { map: (p) => adapter.mapStreamLine(p), flush: () => [] };
|
|
9790
9796
|
}
|
|
9791
|
-
feed(
|
|
9792
|
-
this.buf +=
|
|
9797
|
+
feed(text2) {
|
|
9798
|
+
this.buf += text2;
|
|
9793
9799
|
const lines = this.buf.split("\n");
|
|
9794
9800
|
this.buf = lines.pop() ?? "";
|
|
9795
9801
|
for (const line of lines) this.handleLine(line);
|
|
@@ -10054,7 +10060,7 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
10054
10060
|
for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
|
|
10055
10061
|
const eventsSink = openSink(plan.eventsPath);
|
|
10056
10062
|
const id = identityOf(plan, adapter.harness);
|
|
10057
|
-
return new Promise((
|
|
10063
|
+
return new Promise((resolve7, reject) => {
|
|
10058
10064
|
let streamFd;
|
|
10059
10065
|
try {
|
|
10060
10066
|
mkdirSync12(dirname8(plan.streamPath), { recursive: true });
|
|
@@ -10138,7 +10144,7 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
10138
10144
|
plan.agentLog?.apiKey,
|
|
10139
10145
|
hintMoved
|
|
10140
10146
|
);
|
|
10141
|
-
|
|
10147
|
+
resolve7({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {}, ...killed ? { killed } : {} });
|
|
10142
10148
|
});
|
|
10143
10149
|
child.stdin?.on("error", (err3) => {
|
|
10144
10150
|
emit.warn(`agent closed stdin before the prompt was delivered: ${err3.message}`, { step: "agent" });
|
|
@@ -10471,7 +10477,7 @@ var SELFTEST_TIMEOUT_MS = 12e4;
|
|
|
10471
10477
|
var KEY_VAR = "CREW_API_KEY";
|
|
10472
10478
|
var SELFTEST_PROMPT = "This is a read-only connectivity check. Use the tracker MCP tool to read this workspace (the workspaces find-one tool, using the workspace id below) and reply with the workspace's name alone, on one line. Do not write anything, do not create files, and do not use any other tool.\n\nWorkspace id: ";
|
|
10473
10479
|
var realDeps = {
|
|
10474
|
-
run: (o) => new Promise((
|
|
10480
|
+
run: (o) => new Promise((resolve7) => {
|
|
10475
10481
|
let stdout = "";
|
|
10476
10482
|
let stderr = "";
|
|
10477
10483
|
let timedOut = false;
|
|
@@ -10479,7 +10485,7 @@ var realDeps = {
|
|
|
10479
10485
|
const done = (r) => {
|
|
10480
10486
|
if (!settled) {
|
|
10481
10487
|
settled = true;
|
|
10482
|
-
|
|
10488
|
+
resolve7(r);
|
|
10483
10489
|
}
|
|
10484
10490
|
};
|
|
10485
10491
|
const child = spawn3(o.bin, o.args, { cwd: o.cwd, env: o.env, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
@@ -10588,12 +10594,12 @@ function classify(spec, run2, seen) {
|
|
|
10588
10594
|
return fail("binary-not-found", `could not start \`${bin}\` (${run2.spawnError.message})`);
|
|
10589
10595
|
}
|
|
10590
10596
|
if (run2.timedOut) return fail("timeout", "no answer within the time limit");
|
|
10591
|
-
const
|
|
10597
|
+
const text2 = `${seen.toolOutput}
|
|
10592
10598
|
${run2.stderr}`;
|
|
10593
10599
|
if (REFUSED.test(seen.toolOutput) || seen.tools.some((t) => t.startsWith("mcp__")) && REFUSED.test(run2.stderr)) {
|
|
10594
10600
|
return fail("key-refused", "the tracker returned 401/403 to the MCP call");
|
|
10595
10601
|
}
|
|
10596
|
-
if (seen.mcpFailed || !seen.tools.some((t) => t.startsWith("mcp__")) && MCP_GONE.test(`${
|
|
10602
|
+
if (seen.mcpFailed || !seen.tools.some((t) => t.startsWith("mcp__")) && MCP_GONE.test(`${text2}
|
|
10597
10603
|
${seen.texts.join("\n")}`)) {
|
|
10598
10604
|
return fail("mcp-unavailable", "the tracker MCP tool was not available to the session");
|
|
10599
10605
|
}
|
|
@@ -10685,7 +10691,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
10685
10691
|
}
|
|
10686
10692
|
|
|
10687
10693
|
// src/cli.ts
|
|
10688
|
-
import { resolve as
|
|
10694
|
+
import { resolve as resolve6, dirname as dirname24, join as join46, basename as basename14 } from "path";
|
|
10689
10695
|
import { fileURLToPath } from "url";
|
|
10690
10696
|
import { hostname as hostname3, homedir as homedir10 } from "os";
|
|
10691
10697
|
import { spawnSync as spawnSync8 } from "child_process";
|
|
@@ -12255,13 +12261,13 @@ function writeDigest(o, d, role, stateDir, runId, own) {
|
|
|
12255
12261
|
blockerInfo: d.info
|
|
12256
12262
|
};
|
|
12257
12263
|
const body = role === "qa" ? qaDigest(input) : buildingDigest(input);
|
|
12258
|
-
const
|
|
12264
|
+
const text2 = own ? `${ownRunBanner(own)}
|
|
12259
12265
|
|
|
12260
12266
|
${body}` : body;
|
|
12261
12267
|
const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
|
|
12262
|
-
writeFileSync17(path,
|
|
12263
|
-
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(
|
|
12264
|
-
data: { role, bytes: Buffer.byteLength(
|
|
12268
|
+
writeFileSync17(path, text2);
|
|
12269
|
+
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text2)} bytes)`, {
|
|
12270
|
+
data: { role, bytes: Buffer.byteLength(text2) }
|
|
12265
12271
|
});
|
|
12266
12272
|
return true;
|
|
12267
12273
|
} catch (e) {
|
|
@@ -12366,6 +12372,115 @@ async function resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affi
|
|
|
12366
12372
|
}
|
|
12367
12373
|
}
|
|
12368
12374
|
|
|
12375
|
+
// src/repo-refresh.ts
|
|
12376
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync16, readFileSync as readFileSync23, writeFileSync as writeFileSync18 } from "fs";
|
|
12377
|
+
import { dirname as dirname11 } from "path";
|
|
12378
|
+
var REFRESH_COOLDOWN_MS = 2 * 6e4;
|
|
12379
|
+
var UNSERVABLE_PASSES_BEFORE_ATTENTION = 3;
|
|
12380
|
+
var lastAsked = /* @__PURE__ */ new Map();
|
|
12381
|
+
function unknownRepoIds(route2, repoIds) {
|
|
12382
|
+
const names = route2.resolved?.repoNames;
|
|
12383
|
+
if (!route2.resolved?.reposModelId || !names) return [];
|
|
12384
|
+
return [...new Set(repoIds.filter((id) => !!id && !(id in names)))];
|
|
12385
|
+
}
|
|
12386
|
+
function applyRepoRows(resolved, rows) {
|
|
12387
|
+
const added = [];
|
|
12388
|
+
const names = resolved.repoNames ??= {};
|
|
12389
|
+
const remotes = resolved.repoRemotes ??= {};
|
|
12390
|
+
const hosts = resolved.repoForgeHosts ??= {};
|
|
12391
|
+
for (const r of rows) {
|
|
12392
|
+
if (!(r.id in names)) added.push(r.id);
|
|
12393
|
+
names[r.id] = r.name ?? "";
|
|
12394
|
+
if (r.remote) remotes[r.id] = r.remote;
|
|
12395
|
+
else delete remotes[r.id];
|
|
12396
|
+
const h = r.forge_host?.trim().toLowerCase();
|
|
12397
|
+
if (h) hosts[r.id] = h;
|
|
12398
|
+
else delete hosts[r.id];
|
|
12399
|
+
}
|
|
12400
|
+
if (Object.keys(remotes).length === 0) delete resolved.repoRemotes;
|
|
12401
|
+
if (Object.keys(hosts).length === 0) delete resolved.repoForgeHosts;
|
|
12402
|
+
return added;
|
|
12403
|
+
}
|
|
12404
|
+
function persistRepoRows(stateDir, routeName, rows) {
|
|
12405
|
+
const path = resolvedPathFor(stateDir, routeName);
|
|
12406
|
+
if (!existsSync24(path)) return;
|
|
12407
|
+
const raw = JSON.parse(readFileSync23(path, "utf8"));
|
|
12408
|
+
applyRepoRows(raw, rows);
|
|
12409
|
+
mkdirSync16(dirname11(path), { recursive: true });
|
|
12410
|
+
writeFileSync18(path, `${JSON.stringify(raw, null, 2)}
|
|
12411
|
+
`);
|
|
12412
|
+
}
|
|
12413
|
+
async function refreshUnknownRepos(o) {
|
|
12414
|
+
const unknown = unknownRepoIds(o.route, o.tickets.map((t) => t.repo_id));
|
|
12415
|
+
if (unknown.length === 0 || !o.route.resolved) return [];
|
|
12416
|
+
const now = o.now ?? Date.now();
|
|
12417
|
+
const last = lastAsked.get(o.route.route);
|
|
12418
|
+
if (!o.force && last !== void 0 && now - last < REFRESH_COOLDOWN_MS) return [];
|
|
12419
|
+
lastAsked.set(o.route.route, now);
|
|
12420
|
+
let rows;
|
|
12421
|
+
try {
|
|
12422
|
+
rows = await o.fetchRows();
|
|
12423
|
+
} catch {
|
|
12424
|
+
return [];
|
|
12425
|
+
}
|
|
12426
|
+
const added = applyRepoRows(o.route.resolved, rows);
|
|
12427
|
+
try {
|
|
12428
|
+
persistRepoRows(o.stateDir, o.route.route, rows);
|
|
12429
|
+
} catch {
|
|
12430
|
+
}
|
|
12431
|
+
return added;
|
|
12432
|
+
}
|
|
12433
|
+
var repoUnservableKey = (route2, issueId) => `repo_unservable:${route2}:${issueId}`;
|
|
12434
|
+
function repoUnservableItem(route2, issueId, repoLabel, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
12435
|
+
return {
|
|
12436
|
+
kind: "repo_unservable",
|
|
12437
|
+
key: repoUnservableKey(route2, issueId),
|
|
12438
|
+
since: nowIso,
|
|
12439
|
+
message: `${route2}: ${issueId} is skipped \u2014 this ship has no checkout for ${repoLabel}. Resumes automatically once the route lists that repository (\`crew repos add\`) or a Repos row for it exists`
|
|
12440
|
+
};
|
|
12441
|
+
}
|
|
12442
|
+
function trackUnservable(streaks, unservable) {
|
|
12443
|
+
const now = new Set(unservable);
|
|
12444
|
+
const recovered = [];
|
|
12445
|
+
for (const id of [...streaks.keys()]) {
|
|
12446
|
+
if (!now.has(id)) {
|
|
12447
|
+
streaks.delete(id);
|
|
12448
|
+
recovered.push(id);
|
|
12449
|
+
}
|
|
12450
|
+
}
|
|
12451
|
+
const raise = [];
|
|
12452
|
+
for (const id of now) {
|
|
12453
|
+
const n = (streaks.get(id) ?? 0) + 1;
|
|
12454
|
+
streaks.set(id, n);
|
|
12455
|
+
if (n >= UNSERVABLE_PASSES_BEFORE_ATTENTION) raise.push(id);
|
|
12456
|
+
}
|
|
12457
|
+
return { raise, recovered };
|
|
12458
|
+
}
|
|
12459
|
+
var streaksByRoute = /* @__PURE__ */ new Map();
|
|
12460
|
+
async function reportUnservable(o, unservable, repoLabelOf) {
|
|
12461
|
+
const streaks = streaksByRoute.get(o.route.route) ?? /* @__PURE__ */ new Map();
|
|
12462
|
+
streaksByRoute.set(o.route.route, streaks);
|
|
12463
|
+
const { raise, recovered } = trackUnservable(streaks, unservable);
|
|
12464
|
+
for (const id of recovered) {
|
|
12465
|
+
try {
|
|
12466
|
+
clearShipAttention(o.state, repoUnservableKey(o.route.route, id));
|
|
12467
|
+
} catch {
|
|
12468
|
+
}
|
|
12469
|
+
}
|
|
12470
|
+
for (const id of raise) {
|
|
12471
|
+
try {
|
|
12472
|
+
await raiseShipAttention(o, repoUnservableItem(o.route.route, id, repoLabelOf(id)));
|
|
12473
|
+
} catch {
|
|
12474
|
+
}
|
|
12475
|
+
}
|
|
12476
|
+
}
|
|
12477
|
+
function repoLabelFor(route2, ticket) {
|
|
12478
|
+
const id = ticket?.repo_id;
|
|
12479
|
+
if (!id) return "a repository (the ticket names none)";
|
|
12480
|
+
const name = route2.resolved?.repoNames?.[id];
|
|
12481
|
+
return name ? `repository "${name}"` : `repository ${id} (no Repos row by that id)`;
|
|
12482
|
+
}
|
|
12483
|
+
|
|
12369
12484
|
// src/conflict.ts
|
|
12370
12485
|
import { StaleWriteError as StaleWriteError7 } from "@tablation/client";
|
|
12371
12486
|
var BOUNCE_MARKER = calloutHeader("WARNING", "Merge Conflict");
|
|
@@ -12742,13 +12857,13 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
|
|
|
12742
12857
|
var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
|
|
12743
12858
|
|
|
12744
12859
|
// src/connect-wizard.ts
|
|
12745
|
-
import { delimiter, dirname as
|
|
12746
|
-
import { accessSync as accessSync3, constants as constants3, existsSync as
|
|
12860
|
+
import { delimiter, dirname as dirname12, join as join27 } from "path";
|
|
12861
|
+
import { accessSync as accessSync3, constants as constants3, existsSync as existsSync25, mkdirSync as mkdirSync17, writeFileSync as writeFileSync19 } from "fs";
|
|
12747
12862
|
import { stringify } from "yaml";
|
|
12748
12863
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
12749
12864
|
function shouldRunWizard(opts) {
|
|
12750
12865
|
if (!opts.isTTY) return false;
|
|
12751
|
-
const exists = opts.configExists ??
|
|
12866
|
+
const exists = opts.configExists ?? existsSync25;
|
|
12752
12867
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
12753
12868
|
}
|
|
12754
12869
|
function firstRunConfigPath() {
|
|
@@ -12791,9 +12906,9 @@ async function runWizard(p, env) {
|
|
|
12791
12906
|
if (entered && isExec(entered)) agentBin = entered;
|
|
12792
12907
|
else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
|
|
12793
12908
|
}
|
|
12794
|
-
const nodeDir =
|
|
12909
|
+
const nodeDir = dirname12(env.nodePath);
|
|
12795
12910
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
12796
|
-
const baseDirs = [nodeDir, pnpmPath ?
|
|
12911
|
+
const baseDirs = [nodeDir, pnpmPath ? dirname12(pnpmPath) : void 0];
|
|
12797
12912
|
p.say(
|
|
12798
12913
|
`
|
|
12799
12914
|
Host Passengers lets this machine run the agent sessions that other people's
|
|
@@ -12804,7 +12919,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
|
|
|
12804
12919
|
p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
|
|
12805
12920
|
}
|
|
12806
12921
|
const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
12807
|
-
const extraPath = deriveExtraPath([...baseDirs, dockerPath ?
|
|
12922
|
+
const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname12(dockerPath) : void 0]);
|
|
12808
12923
|
if (extraPath) {
|
|
12809
12924
|
p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
|
|
12810
12925
|
}
|
|
@@ -12825,10 +12940,10 @@ function renderFullConfig(a, routeBlock) {
|
|
|
12825
12940
|
routes:
|
|
12826
12941
|
${routeBlock}`;
|
|
12827
12942
|
}
|
|
12828
|
-
function writeNewConfig(path,
|
|
12829
|
-
|
|
12943
|
+
function writeNewConfig(path, text2) {
|
|
12944
|
+
mkdirSync17(dirname12(path), { recursive: true });
|
|
12830
12945
|
try {
|
|
12831
|
-
|
|
12946
|
+
writeFileSync19(path, text2, { mode: 384, flag: "wx" });
|
|
12832
12947
|
return true;
|
|
12833
12948
|
} catch (e) {
|
|
12834
12949
|
if (e.code === "EEXIST") return false;
|
|
@@ -12862,14 +12977,14 @@ Wrote ${o.configPath}.
|
|
|
12862
12977
|
// src/connect-json.ts
|
|
12863
12978
|
import { accessSync as accessSync4, constants as constants4 } from "fs";
|
|
12864
12979
|
import { homedir as homedir8 } from "os";
|
|
12865
|
-
import { dirname as
|
|
12980
|
+
import { dirname as dirname13, join as join28 } from "path";
|
|
12866
12981
|
import { delimiter as delimiter2 } from "path";
|
|
12867
12982
|
var CHOICE_EXIT_CODE = 3;
|
|
12868
|
-
function createEmitter(
|
|
12983
|
+
function createEmitter(json2, write) {
|
|
12869
12984
|
return {
|
|
12870
|
-
json,
|
|
12985
|
+
json: json2,
|
|
12871
12986
|
emit(event, message, data = {}) {
|
|
12872
|
-
if (!
|
|
12987
|
+
if (!json2) return;
|
|
12873
12988
|
write(`${JSON.stringify({ ...data, event, message })}
|
|
12874
12989
|
`);
|
|
12875
12990
|
}
|
|
@@ -12928,7 +13043,7 @@ function flagWizardAnswers(f, env) {
|
|
|
12928
13043
|
const agentBin = f.agentBin ?? found?.bin;
|
|
12929
13044
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
12930
13045
|
const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
12931
|
-
const extraPath = f.extraPath ?? deriveExtraPath([
|
|
13046
|
+
const extraPath = f.extraPath ?? deriveExtraPath([dirname13(env.nodePath), pnpmPath ? dirname13(pnpmPath) : void 0, dockerPath ? dirname13(dockerPath) : void 0]);
|
|
12932
13047
|
return {
|
|
12933
13048
|
answers: {
|
|
12934
13049
|
name: f.shipName?.trim() || env.hostname,
|
|
@@ -12943,7 +13058,7 @@ function flagWizardAnswers(f, env) {
|
|
|
12943
13058
|
|
|
12944
13059
|
// src/provision.ts
|
|
12945
13060
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
12946
|
-
import { existsSync as
|
|
13061
|
+
import { existsSync as existsSync26, readFileSync as readFileSync24, writeFileSync as writeFileSync20 } from "fs";
|
|
12947
13062
|
import { hostname as hostname2 } from "os";
|
|
12948
13063
|
import { parseDocument as parseDocument3 } from "yaml";
|
|
12949
13064
|
|
|
@@ -12976,8 +13091,8 @@ function matchRepoName(origin, resolved) {
|
|
|
12976
13091
|
}
|
|
12977
13092
|
return void 0;
|
|
12978
13093
|
}
|
|
12979
|
-
function routeNamesOf(
|
|
12980
|
-
const routes = parseDocument(
|
|
13094
|
+
function routeNamesOf(text2) {
|
|
13095
|
+
const routes = parseDocument(text2).get("routes", true);
|
|
12981
13096
|
if (!isSeq(routes)) return [];
|
|
12982
13097
|
return routes.items.flatMap((i) => isMap(i) && typeof i.get("route") === "string" ? [i.get("route")] : []);
|
|
12983
13098
|
}
|
|
@@ -13011,8 +13126,8 @@ function routeMapOf(doc, route2) {
|
|
|
13011
13126
|
const have = isSeq(routes) ? routes.items.map((i) => isMap(i) ? String(i.get("route")) : "?").join(", ") : "none";
|
|
13012
13127
|
throw new ReposError(`no route "${route2}" in the config (have: ${have})`);
|
|
13013
13128
|
}
|
|
13014
|
-
function configuredRepos(
|
|
13015
|
-
const doc = parseDocument(
|
|
13129
|
+
function configuredRepos(text2, route2, base) {
|
|
13130
|
+
const doc = parseDocument(text2);
|
|
13016
13131
|
const m = routeMapOf(doc, route2);
|
|
13017
13132
|
const repos = m.get("repos");
|
|
13018
13133
|
if (isMap(repos)) {
|
|
@@ -13026,8 +13141,8 @@ function configuredRepos(text, route2, base) {
|
|
|
13026
13141
|
const dir = m.get("dir");
|
|
13027
13142
|
return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
13028
13143
|
}
|
|
13029
|
-
function cloneBaseOf(
|
|
13030
|
-
const m = routeMapOf(parseDocument(
|
|
13144
|
+
function cloneBaseOf(text2, route2, base, isCheckout) {
|
|
13145
|
+
const m = routeMapOf(parseDocument(text2), route2);
|
|
13031
13146
|
const repos = m.get("repos", true);
|
|
13032
13147
|
if (isMap(repos) && repos.items.length > 0) return null;
|
|
13033
13148
|
const dir = m.get("dir");
|
|
@@ -13035,8 +13150,8 @@ function cloneBaseOf(text, route2, base, isCheckout) {
|
|
|
13035
13150
|
const abs = expandDir(String(dir), base);
|
|
13036
13151
|
return isCheckout(abs) ? null : abs;
|
|
13037
13152
|
}
|
|
13038
|
-
function routeSetsHooks(
|
|
13039
|
-
const m = routeMapOf(parseDocument(
|
|
13153
|
+
function routeSetsHooks(text2, route2, name) {
|
|
13154
|
+
const m = routeMapOf(parseDocument(text2), route2);
|
|
13040
13155
|
const repos = m.get("repos", true);
|
|
13041
13156
|
let target = m;
|
|
13042
13157
|
if (isMap(repos)) target = repos.get(name, true);
|
|
@@ -13143,12 +13258,12 @@ function planRepoAdd(i) {
|
|
|
13143
13258
|
}
|
|
13144
13259
|
return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched, droppedDir };
|
|
13145
13260
|
}
|
|
13146
|
-
function planRepoRemove(
|
|
13147
|
-
const doc = parseDocument(
|
|
13261
|
+
function planRepoRemove(text2, base, route2, path) {
|
|
13262
|
+
const doc = parseDocument(text2);
|
|
13148
13263
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
13149
13264
|
const m = routeMapOf(doc, route2);
|
|
13150
13265
|
const target = expandDir(path, base);
|
|
13151
|
-
const existing = configuredRepos(
|
|
13266
|
+
const existing = configuredRepos(text2, route2, base);
|
|
13152
13267
|
const hit = existing.find((e) => e.dir === target);
|
|
13153
13268
|
if (!hit) throw new ReposError(`${target} is not configured on route ${route2}`);
|
|
13154
13269
|
const repos = m.get("repos", true);
|
|
@@ -13273,8 +13388,8 @@ async function gatherHealth(c, check) {
|
|
|
13273
13388
|
function routesInScope(routes, named) {
|
|
13274
13389
|
return named ? routes.filter((r) => r.route === named) : routes;
|
|
13275
13390
|
}
|
|
13276
|
-
function planEnable(
|
|
13277
|
-
const doc = parseDocument2(
|
|
13391
|
+
function planEnable(text2, route2) {
|
|
13392
|
+
const doc = parseDocument2(text2);
|
|
13278
13393
|
routeMapOf(doc, route2).set("enabled", true);
|
|
13279
13394
|
return String(doc);
|
|
13280
13395
|
}
|
|
@@ -13460,7 +13575,7 @@ function realProvisionDeps(crewHome) {
|
|
|
13460
13575
|
};
|
|
13461
13576
|
const configFile = () => {
|
|
13462
13577
|
const f = findConfigFile(crewHome);
|
|
13463
|
-
return
|
|
13578
|
+
return existsSync26(f) ? f : null;
|
|
13464
13579
|
};
|
|
13465
13580
|
return {
|
|
13466
13581
|
crew: spawnCrew,
|
|
@@ -13468,13 +13583,13 @@ function realProvisionDeps(crewHome) {
|
|
|
13468
13583
|
enableRoute(route2) {
|
|
13469
13584
|
const file = configFile();
|
|
13470
13585
|
if (!file) return false;
|
|
13471
|
-
const
|
|
13472
|
-
const doc = parseDocument3(
|
|
13586
|
+
const text2 = readFileSync24(file, "utf8");
|
|
13587
|
+
const doc = parseDocument3(text2);
|
|
13473
13588
|
const routes = doc.get("routes");
|
|
13474
13589
|
const entry = routes?.items?.find((r) => r.get("route") === route2);
|
|
13475
13590
|
if (!entry) throw new Error(`route ${route2} is not in ${file}`);
|
|
13476
13591
|
if (entry.get("enabled") === true) return false;
|
|
13477
|
-
|
|
13592
|
+
writeFileSync20(file, planEnable(text2, route2));
|
|
13478
13593
|
return true;
|
|
13479
13594
|
},
|
|
13480
13595
|
installState() {
|
|
@@ -13494,14 +13609,14 @@ function realProvisionDeps(crewHome) {
|
|
|
13494
13609
|
continue;
|
|
13495
13610
|
}
|
|
13496
13611
|
for (const p of plan.unitPaths) {
|
|
13497
|
-
if (!
|
|
13612
|
+
if (!existsSync26(p) || readFileSync24(p, "utf8") !== plan.unitContent[p]) unitsStale = true;
|
|
13498
13613
|
}
|
|
13499
13614
|
}
|
|
13500
13615
|
const label = labelFor(crewHome, "run");
|
|
13501
13616
|
let running = false;
|
|
13502
13617
|
let lingering = true;
|
|
13503
13618
|
if (mechanism === "systemd") {
|
|
13504
|
-
const system = ctx.root ||
|
|
13619
|
+
const system = ctx.root || existsSync26(systemUnitPathFor(label));
|
|
13505
13620
|
const ctl = system ? ["systemctl"] : ["systemctl", "--user"];
|
|
13506
13621
|
running = spawnSync6(ctl[0], [...ctl.slice(1), "is-active", "--quiet", `${label}.service`]).status === 0;
|
|
13507
13622
|
if (!system) {
|
|
@@ -13528,8 +13643,8 @@ host=${host}
|
|
|
13528
13643
|
}
|
|
13529
13644
|
|
|
13530
13645
|
// src/release-run.ts
|
|
13531
|
-
import { readFileSync as
|
|
13532
|
-
import { dirname as
|
|
13646
|
+
import { readFileSync as readFileSync25, writeFileSync as writeFileSync21, existsSync as existsSync27, copyFileSync, mkdirSync as mkdirSync18, mkdtempSync as mkdtempSync2, rmSync as rmSync14 } from "fs";
|
|
13647
|
+
import { dirname as dirname15, join as join30 } from "path";
|
|
13533
13648
|
import { tmpdir as tmpdir3 } from "os";
|
|
13534
13649
|
|
|
13535
13650
|
// src/release.ts
|
|
@@ -14272,11 +14387,11 @@ async function bump(o, size) {
|
|
|
14272
14387
|
}
|
|
14273
14388
|
const files = o.repo.release.versionFiles.map((f) => join30(o.cwd, f));
|
|
14274
14389
|
const first = files[0];
|
|
14275
|
-
if (!
|
|
14390
|
+
if (!existsSync27(first)) {
|
|
14276
14391
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
14277
14392
|
return null;
|
|
14278
14393
|
}
|
|
14279
|
-
const current = JSON.parse(
|
|
14394
|
+
const current = JSON.parse(readFileSync25(first, "utf8")).version;
|
|
14280
14395
|
if (!current) {
|
|
14281
14396
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
14282
14397
|
return null;
|
|
@@ -14289,13 +14404,13 @@ async function bump(o, size) {
|
|
|
14289
14404
|
return version;
|
|
14290
14405
|
}
|
|
14291
14406
|
for (const f of files) {
|
|
14292
|
-
const src =
|
|
14407
|
+
const src = readFileSync25(f, "utf8");
|
|
14293
14408
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
14294
14409
|
if (out === src) {
|
|
14295
14410
|
o.emit.error(`no version field in ${f}`);
|
|
14296
14411
|
return null;
|
|
14297
14412
|
}
|
|
14298
|
-
|
|
14413
|
+
writeFileSync21(f, out);
|
|
14299
14414
|
}
|
|
14300
14415
|
return version;
|
|
14301
14416
|
}
|
|
@@ -14308,8 +14423,8 @@ function writeChangelog(o, version, entries) {
|
|
|
14308
14423
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
14309
14424
|
return;
|
|
14310
14425
|
}
|
|
14311
|
-
const existing =
|
|
14312
|
-
|
|
14426
|
+
const existing = existsSync27(path) ? readFileSync25(path, "utf8") : "# Changelog\n";
|
|
14427
|
+
writeFileSync21(path, insertChangelogSection(existing, section));
|
|
14313
14428
|
}
|
|
14314
14429
|
async function confirm(o, expected) {
|
|
14315
14430
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -14414,8 +14529,8 @@ function openReleaseCheckout(o) {
|
|
|
14414
14529
|
}
|
|
14415
14530
|
for (const f of o.repo.worktrees.copy) {
|
|
14416
14531
|
const src = join30(o.cwd, f);
|
|
14417
|
-
if (!
|
|
14418
|
-
|
|
14532
|
+
if (!existsSync27(src)) continue;
|
|
14533
|
+
mkdirSync18(dirname15(join30(dir, f)), { recursive: true });
|
|
14419
14534
|
copyFileSync(src, join30(dir, f));
|
|
14420
14535
|
}
|
|
14421
14536
|
return { dir, branch, close };
|
|
@@ -15117,11 +15232,11 @@ Passed: ${passed}.${willClose ? " Closing this alert." : ""}`,
|
|
|
15117
15232
|
}
|
|
15118
15233
|
|
|
15119
15234
|
// src/live-runs.ts
|
|
15120
|
-
import { existsSync as
|
|
15235
|
+
import { existsSync as existsSync29, readdirSync as readdirSync11 } from "fs";
|
|
15121
15236
|
import { join as join32 } from "path";
|
|
15122
15237
|
|
|
15123
15238
|
// src/interrupted.ts
|
|
15124
|
-
import { existsSync as
|
|
15239
|
+
import { existsSync as existsSync28, readdirSync as readdirSync10, readFileSync as readFileSync26 } from "fs";
|
|
15125
15240
|
import { join as join31 } from "path";
|
|
15126
15241
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
15127
15242
|
function interruptedComment(b) {
|
|
@@ -15141,16 +15256,16 @@ var pidAlive = (pid) => {
|
|
|
15141
15256
|
};
|
|
15142
15257
|
function runFinished(streamPath) {
|
|
15143
15258
|
const path = `${streamPath}.meta.json`;
|
|
15144
|
-
if (!
|
|
15259
|
+
if (!existsSync28(path)) return false;
|
|
15145
15260
|
try {
|
|
15146
|
-
return JSON.parse(
|
|
15261
|
+
return JSON.parse(readFileSync26(path, "utf8")).running !== true;
|
|
15147
15262
|
} catch {
|
|
15148
15263
|
return true;
|
|
15149
15264
|
}
|
|
15150
15265
|
}
|
|
15151
15266
|
function orphanedStreams(o) {
|
|
15152
15267
|
const dir = join31(o.stateDir, "streams");
|
|
15153
|
-
if (!
|
|
15268
|
+
if (!existsSync28(dir)) return [];
|
|
15154
15269
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
15155
15270
|
const out = [];
|
|
15156
15271
|
for (const name of readdirSync10(dir)) {
|
|
@@ -15210,7 +15325,7 @@ async function reconcileRuns(o) {
|
|
|
15210
15325
|
// src/live-runs.ts
|
|
15211
15326
|
function liveRuns(stateDir, isAlive = runIsAlive) {
|
|
15212
15327
|
const dir = join32(stateDir, "streams");
|
|
15213
|
-
if (!
|
|
15328
|
+
if (!existsSync29(dir)) return [];
|
|
15214
15329
|
const out = [];
|
|
15215
15330
|
for (const name of readdirSync11(dir)) {
|
|
15216
15331
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
@@ -15549,7 +15664,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
15549
15664
|
}
|
|
15550
15665
|
|
|
15551
15666
|
// src/watch.ts
|
|
15552
|
-
import { watch as watchFile, existsSync as
|
|
15667
|
+
import { watch as watchFile, existsSync as existsSync30, statSync as statSync9, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
15553
15668
|
var E = "\x1B[";
|
|
15554
15669
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
15555
15670
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -15650,7 +15765,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
15650
15765
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
15651
15766
|
}
|
|
15652
15767
|
function readFrom(file, from) {
|
|
15653
|
-
if (!
|
|
15768
|
+
if (!existsSync30(file)) return { events: [], offset: from };
|
|
15654
15769
|
const size = statSync9(file).size;
|
|
15655
15770
|
if (size < from) return readFrom(file, 0);
|
|
15656
15771
|
if (size === from) return { events: [], offset: from };
|
|
@@ -15658,18 +15773,18 @@ function readFrom(file, from) {
|
|
|
15658
15773
|
const buf = Buffer.alloc(size - from);
|
|
15659
15774
|
readSync2(fd, buf, 0, buf.length, from);
|
|
15660
15775
|
closeSync4(fd);
|
|
15661
|
-
const
|
|
15662
|
-
const lastNewline =
|
|
15776
|
+
const text2 = buf.toString("utf8");
|
|
15777
|
+
const lastNewline = text2.lastIndexOf("\n");
|
|
15663
15778
|
if (lastNewline === -1) return { events: [], offset: from };
|
|
15664
15779
|
const events = [];
|
|
15665
|
-
for (const line of
|
|
15780
|
+
for (const line of text2.slice(0, lastNewline).split("\n")) {
|
|
15666
15781
|
if (!line.trim()) continue;
|
|
15667
15782
|
try {
|
|
15668
15783
|
events.push(JSON.parse(line));
|
|
15669
15784
|
} catch {
|
|
15670
15785
|
}
|
|
15671
15786
|
}
|
|
15672
|
-
return { events, offset: from + Buffer.byteLength(
|
|
15787
|
+
return { events, offset: from + Buffer.byteLength(text2.slice(0, lastNewline + 1), "utf8") };
|
|
15673
15788
|
}
|
|
15674
15789
|
function startWatch(o) {
|
|
15675
15790
|
const out = o.out ?? ((s) => process.stdout.write(s));
|
|
@@ -15893,15 +16008,15 @@ function nextRoles(decision, ran) {
|
|
|
15893
16008
|
}
|
|
15894
16009
|
|
|
15895
16010
|
// src/ship-name.ts
|
|
15896
|
-
import { existsSync as
|
|
15897
|
-
import { dirname as
|
|
16011
|
+
import { existsSync as existsSync32, mkdirSync as mkdirSync20, readFileSync as readFileSync28, renameSync as renameSync3, writeFileSync as writeFileSync23 } from "fs";
|
|
16012
|
+
import { dirname as dirname17 } from "path";
|
|
15898
16013
|
import { parseDocument as parseDocument5 } from "yaml";
|
|
15899
16014
|
|
|
15900
16015
|
// src/machine-id.ts
|
|
15901
16016
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
15902
16017
|
import { createHash as createHash5 } from "crypto";
|
|
15903
|
-
import { existsSync as
|
|
15904
|
-
import { dirname as
|
|
16018
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync27, writeFileSync as writeFileSync22 } from "fs";
|
|
16019
|
+
import { dirname as dirname16, join as join33 } from "path";
|
|
15905
16020
|
function machineIdPathFor(stateDir) {
|
|
15906
16021
|
return join33(stateDir, "machine-id");
|
|
15907
16022
|
}
|
|
@@ -15916,8 +16031,8 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
15916
16031
|
return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
|
|
15917
16032
|
}
|
|
15918
16033
|
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
15919
|
-
if (
|
|
15920
|
-
const id =
|
|
16034
|
+
if (existsSync31(p)) {
|
|
16035
|
+
const id = readFileSync27(p, "utf8").trim();
|
|
15921
16036
|
if (id) return id;
|
|
15922
16037
|
}
|
|
15923
16038
|
}
|
|
@@ -15928,15 +16043,15 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
15928
16043
|
function ensureMachineId(stateDir, opts = {}) {
|
|
15929
16044
|
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
15930
16045
|
const path = machineIdPathFor(stateDir);
|
|
15931
|
-
if (
|
|
15932
|
-
const cached =
|
|
16046
|
+
if (existsSync31(path)) {
|
|
16047
|
+
const cached = readFileSync27(path, "utf8").trim();
|
|
15933
16048
|
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
15934
16049
|
}
|
|
15935
16050
|
const raw = (opts.read ?? readRawMachineId)();
|
|
15936
16051
|
if (!raw) return void 0;
|
|
15937
16052
|
const hashed = sha256(raw);
|
|
15938
|
-
|
|
15939
|
-
|
|
16053
|
+
mkdirSync19(dirname16(path), { recursive: true });
|
|
16054
|
+
writeFileSync22(path, `${hashed}
|
|
15940
16055
|
`, { mode: 384 });
|
|
15941
16056
|
return hashed;
|
|
15942
16057
|
}
|
|
@@ -15964,12 +16079,12 @@ function planNameSync(local, row, lastSynced) {
|
|
|
15964
16079
|
if (rowMoved) return { drift: "row", action: "adopt" };
|
|
15965
16080
|
return { drift: "local", action: "push" };
|
|
15966
16081
|
}
|
|
15967
|
-
function shipNameOfYaml(
|
|
15968
|
-
const v = parseDocument5(
|
|
16082
|
+
function shipNameOfYaml(text2) {
|
|
16083
|
+
const v = parseDocument5(text2).getIn(["ship", "name"]);
|
|
15969
16084
|
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
15970
16085
|
}
|
|
15971
|
-
function setShipNameInYaml(
|
|
15972
|
-
const doc = parseDocument5(
|
|
16086
|
+
function setShipNameInYaml(text2, name) {
|
|
16087
|
+
const doc = parseDocument5(text2);
|
|
15973
16088
|
doc.setIn(["ship", "name"], name);
|
|
15974
16089
|
return doc.toString();
|
|
15975
16090
|
}
|
|
@@ -16007,34 +16122,34 @@ async function syncRouteName(i) {
|
|
|
16007
16122
|
return { status: "ok", ...base, action, name: i.local };
|
|
16008
16123
|
}
|
|
16009
16124
|
function readSyncedName(resolvedFile) {
|
|
16010
|
-
if (!
|
|
16125
|
+
if (!existsSync32(resolvedFile)) return void 0;
|
|
16011
16126
|
try {
|
|
16012
|
-
const v = JSON.parse(
|
|
16127
|
+
const v = JSON.parse(readFileSync28(resolvedFile, "utf8")).shipName;
|
|
16013
16128
|
return typeof v === "string" ? v : void 0;
|
|
16014
16129
|
} catch {
|
|
16015
16130
|
return void 0;
|
|
16016
16131
|
}
|
|
16017
16132
|
}
|
|
16018
16133
|
function writeSyncedName(resolvedFile, name) {
|
|
16019
|
-
if (!
|
|
16134
|
+
if (!existsSync32(resolvedFile)) return;
|
|
16020
16135
|
let raw;
|
|
16021
16136
|
try {
|
|
16022
|
-
raw = JSON.parse(
|
|
16137
|
+
raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
|
|
16023
16138
|
} catch {
|
|
16024
16139
|
return;
|
|
16025
16140
|
}
|
|
16026
16141
|
if (raw.shipName === name) return;
|
|
16027
16142
|
raw.shipName = name;
|
|
16028
|
-
|
|
16143
|
+
mkdirSync20(dirname17(resolvedFile), { recursive: true });
|
|
16029
16144
|
const tmp = `${resolvedFile}.tmp${process.pid}`;
|
|
16030
|
-
|
|
16145
|
+
writeFileSync23(tmp, `${JSON.stringify(raw, null, 2)}
|
|
16031
16146
|
`);
|
|
16032
16147
|
renameSync3(tmp, resolvedFile);
|
|
16033
16148
|
}
|
|
16034
16149
|
async function reconcileShipName(i) {
|
|
16035
16150
|
let local = i.ship.name;
|
|
16036
16151
|
try {
|
|
16037
|
-
local = shipNameOfYaml(
|
|
16152
|
+
local = shipNameOfYaml(readFileSync28(i.configFile, "utf8")) ?? local;
|
|
16038
16153
|
} catch {
|
|
16039
16154
|
}
|
|
16040
16155
|
const resolvedFile = resolvedPathFor(i.stateDir, i.route);
|
|
@@ -16058,8 +16173,8 @@ async function reconcileShipName(i) {
|
|
|
16058
16173
|
}
|
|
16059
16174
|
if (result.status !== "ok") return result;
|
|
16060
16175
|
if (result.action === "adopt" || result.action === "row-wins") {
|
|
16061
|
-
const
|
|
16062
|
-
|
|
16176
|
+
const text2 = readFileSync28(i.configFile, "utf8");
|
|
16177
|
+
writeFileSync23(i.configFile, setShipNameInYaml(text2, result.name));
|
|
16063
16178
|
i.ship.name = result.name;
|
|
16064
16179
|
if (result.action === "row-wins") {
|
|
16065
16180
|
i.emit?.warn(
|
|
@@ -16715,8 +16830,8 @@ function describeCapability(i) {
|
|
|
16715
16830
|
}
|
|
16716
16831
|
|
|
16717
16832
|
// src/agents-sync-run.ts
|
|
16718
|
-
import { existsSync as
|
|
16719
|
-
import { dirname as
|
|
16833
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as readFileSync29, writeFileSync as writeFileSync24 } from "fs";
|
|
16834
|
+
import { dirname as dirname18 } from "path";
|
|
16720
16835
|
async function runAgentsSync(target, o) {
|
|
16721
16836
|
const dryRun = o.dryRun ?? false;
|
|
16722
16837
|
const force = o.force ?? false;
|
|
@@ -16749,12 +16864,12 @@ async function runAgentsSync(target, o) {
|
|
|
16749
16864
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
16750
16865
|
if (!dryRun) {
|
|
16751
16866
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
16752
|
-
const raw =
|
|
16867
|
+
const raw = existsSync33(resolvedPath) ? JSON.parse(readFileSync29(resolvedPath, "utf8")) : {};
|
|
16753
16868
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
16754
16869
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
16755
16870
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
16756
|
-
|
|
16757
|
-
|
|
16871
|
+
mkdirSync21(dirname18(resolvedPath), { recursive: true });
|
|
16872
|
+
writeFileSync24(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
16758
16873
|
`);
|
|
16759
16874
|
if (target.resolved) {
|
|
16760
16875
|
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
@@ -16858,7 +16973,7 @@ async function showLogEntry(route2, entryId, opts = {}) {
|
|
|
16858
16973
|
|
|
16859
16974
|
// src/repos-check.ts
|
|
16860
16975
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
16861
|
-
import { readFileSync as
|
|
16976
|
+
import { readFileSync as readFileSync30 } from "fs";
|
|
16862
16977
|
import { basename as basename9 } from "path";
|
|
16863
16978
|
var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
|
|
16864
16979
|
function checkHookSyntax(script, shell) {
|
|
@@ -16899,7 +17014,7 @@ function checkRepoSpec(dir, opts = {}) {
|
|
|
16899
17014
|
}
|
|
16900
17015
|
let parsed;
|
|
16901
17016
|
try {
|
|
16902
|
-
parsed = parseRepoConfig(
|
|
17017
|
+
parsed = parseRepoConfig(readFileSync30(file, "utf8"), file);
|
|
16903
17018
|
} catch (e) {
|
|
16904
17019
|
if (!(e instanceof RepoConfigError)) throw e;
|
|
16905
17020
|
return { ...empty, file, error: e.message, exitCode: 1 };
|
|
@@ -16960,11 +17075,11 @@ function packageManager(f) {
|
|
|
16960
17075
|
return "npm";
|
|
16961
17076
|
}
|
|
16962
17077
|
function fromPackageJson(f) {
|
|
16963
|
-
const
|
|
16964
|
-
if (
|
|
17078
|
+
const text2 = f.read("package.json");
|
|
17079
|
+
if (text2 === void 0) return void 0;
|
|
16965
17080
|
let pkg;
|
|
16966
17081
|
try {
|
|
16967
|
-
pkg = JSON.parse(
|
|
17082
|
+
pkg = JSON.parse(text2);
|
|
16968
17083
|
} catch {
|
|
16969
17084
|
return void 0;
|
|
16970
17085
|
}
|
|
@@ -16987,11 +17102,11 @@ function fromPyproject(f) {
|
|
|
16987
17102
|
return f.exists("pyproject.toml") ? { test: "pytest" } : void 0;
|
|
16988
17103
|
}
|
|
16989
17104
|
function fromMakefile(f) {
|
|
16990
|
-
const
|
|
16991
|
-
if (
|
|
17105
|
+
const text2 = f.read("Makefile") ?? f.read("makefile") ?? f.read("GNUmakefile");
|
|
17106
|
+
if (text2 === void 0) return void 0;
|
|
16992
17107
|
const out = {};
|
|
16993
17108
|
for (const target of ["test", "build"]) {
|
|
16994
|
-
if (new RegExp(`^${target}\\s*:`, "m").test(
|
|
17109
|
+
if (new RegExp(`^${target}\\s*:`, "m").test(text2)) out[target] = `make ${target}`;
|
|
16995
17110
|
}
|
|
16996
17111
|
return out;
|
|
16997
17112
|
}
|
|
@@ -17084,16 +17199,16 @@ function parseHooksArgs(args, routes) {
|
|
|
17084
17199
|
if (rest.length !== 1) throw new ReposError("expected: crew repos hooks [route] <name> [--suggest] [--agent] [--write route|repo] [--recipes ID,ID] [--hook KEY=CMD] [--json]");
|
|
17085
17200
|
return { route: route2, name: rest[0] };
|
|
17086
17201
|
}
|
|
17087
|
-
function repoDirOf(
|
|
17088
|
-
const repos = configuredRepos(
|
|
17202
|
+
function repoDirOf(text2, base, route2, name) {
|
|
17203
|
+
const repos = configuredRepos(text2, route2, base);
|
|
17089
17204
|
const hit = repos.find((r) => r.name === name);
|
|
17090
17205
|
if (!hit) {
|
|
17091
17206
|
throw new ReposError(`route ${route2} has no repo named "${name}" (have: ${repos.map((r) => r.name).join(", ") || "none"})`);
|
|
17092
17207
|
}
|
|
17093
17208
|
return hit.dir;
|
|
17094
17209
|
}
|
|
17095
|
-
function planRouteHooks(
|
|
17096
|
-
const doc = parseDocument6(
|
|
17210
|
+
function planRouteHooks(text2, route2, name, hooks, opts = {}) {
|
|
17211
|
+
const doc = parseDocument6(text2);
|
|
17097
17212
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
17098
17213
|
const m = routeMapOf(doc, route2);
|
|
17099
17214
|
const repos = m.get("repos", true);
|
|
@@ -17129,19 +17244,19 @@ function planRouteHooks(text, route2, name, hooks, opts = {}) {
|
|
|
17129
17244
|
}
|
|
17130
17245
|
return { text: String(doc), written, kept, replaced };
|
|
17131
17246
|
}
|
|
17132
|
-
function clearRouteHooks(
|
|
17133
|
-
const doc = parseDocument6(
|
|
17247
|
+
function clearRouteHooks(text2, route2, name) {
|
|
17248
|
+
const doc = parseDocument6(text2);
|
|
17134
17249
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
17135
17250
|
const m = routeMapOf(doc, route2);
|
|
17136
17251
|
const repos = m.get("repos", true);
|
|
17137
17252
|
let target = m;
|
|
17138
17253
|
if (isMap2(repos)) {
|
|
17139
17254
|
const entry = repos.get(name, true);
|
|
17140
|
-
if (!isMap2(entry)) return { text, removed: [] };
|
|
17255
|
+
if (!isMap2(entry)) return { text: text2, removed: [] };
|
|
17141
17256
|
target = entry;
|
|
17142
17257
|
}
|
|
17143
17258
|
const current = target.get("hooks", true);
|
|
17144
|
-
if (!isMap2(current)) return { text, removed: [] };
|
|
17259
|
+
if (!isMap2(current)) return { text: text2, removed: [] };
|
|
17145
17260
|
const removed = current.items.map((i) => String(i.key.value ?? i.key));
|
|
17146
17261
|
target.delete("hooks");
|
|
17147
17262
|
return { text: String(doc), removed };
|
|
@@ -17165,9 +17280,9 @@ function starterRepoConfig(hooks, release) {
|
|
|
17165
17280
|
if (release.verifyMatch) lines.push(" verify:", ` match: ${release.verifyMatch}`);
|
|
17166
17281
|
lines.push("");
|
|
17167
17282
|
} else lines.push("", "release:", " # integrate: merging to the base branch is the release; nothing is deployed.", " mode: integrate", "");
|
|
17168
|
-
const
|
|
17169
|
-
parseRepoConfig(
|
|
17170
|
-
return
|
|
17283
|
+
const text2 = lines.join("\n");
|
|
17284
|
+
parseRepoConfig(text2, ".crew.yaml");
|
|
17285
|
+
return text2;
|
|
17171
17286
|
}
|
|
17172
17287
|
function existingRepoConfig(f) {
|
|
17173
17288
|
return REPO_CONFIG_FILENAMES.find((n) => f.exists(n));
|
|
@@ -17226,7 +17341,7 @@ function agentInvocation(spec, model) {
|
|
|
17226
17341
|
});
|
|
17227
17342
|
}
|
|
17228
17343
|
function parseAgentStream(out, dir) {
|
|
17229
|
-
let
|
|
17344
|
+
let text2 = "";
|
|
17230
17345
|
let isError = false;
|
|
17231
17346
|
const files = /* @__PURE__ */ new Set();
|
|
17232
17347
|
let base = dir;
|
|
@@ -17248,19 +17363,19 @@ function parseAgentStream(out, dir) {
|
|
|
17248
17363
|
if (typeof p === "string") files.add(isAbsolute2(p) ? relative(base, p) : p);
|
|
17249
17364
|
}
|
|
17250
17365
|
} else if (ev?.type === "result") {
|
|
17251
|
-
|
|
17366
|
+
text2 = typeof ev.result === "string" ? ev.result : "";
|
|
17252
17367
|
isError = ev.is_error === true;
|
|
17253
17368
|
}
|
|
17254
17369
|
}
|
|
17255
|
-
return { text, isError, filesRead: [...files].sort() };
|
|
17370
|
+
return { text: text2, isError, filesRead: [...files].sort() };
|
|
17256
17371
|
}
|
|
17257
|
-
function parseAgentHooks(
|
|
17258
|
-
const start =
|
|
17259
|
-
const end =
|
|
17372
|
+
function parseAgentHooks(text2) {
|
|
17373
|
+
const start = text2.indexOf("{");
|
|
17374
|
+
const end = text2.lastIndexOf("}");
|
|
17260
17375
|
if (start < 0 || end <= start) throw new AgentSuggestError("bad_output", "the toolchain did not answer with a JSON object");
|
|
17261
17376
|
let obj;
|
|
17262
17377
|
try {
|
|
17263
|
-
obj = JSON.parse(
|
|
17378
|
+
obj = JSON.parse(text2.slice(start, end + 1));
|
|
17264
17379
|
} catch {
|
|
17265
17380
|
throw new AgentSuggestError("bad_output", "the toolchain's answer was not valid JSON");
|
|
17266
17381
|
}
|
|
@@ -17292,7 +17407,7 @@ async function runAgentSuggest(o) {
|
|
|
17292
17407
|
const before = porcelain(o.dir);
|
|
17293
17408
|
const timeout = AbortSignal.timeout(o.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
17294
17409
|
const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
|
|
17295
|
-
const run2 = await new Promise((
|
|
17410
|
+
const run2 = await new Promise((resolve7) => {
|
|
17296
17411
|
let out = "";
|
|
17297
17412
|
let err3 = "";
|
|
17298
17413
|
const child = spawn4(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -17301,7 +17416,7 @@ async function runAgentSuggest(o) {
|
|
|
17301
17416
|
if (settled) return;
|
|
17302
17417
|
settled = true;
|
|
17303
17418
|
signal.removeEventListener("abort", onAbort);
|
|
17304
|
-
|
|
17419
|
+
resolve7(r);
|
|
17305
17420
|
};
|
|
17306
17421
|
const onAbort = () => {
|
|
17307
17422
|
try {
|
|
@@ -17346,7 +17461,7 @@ ${run2.err}` });
|
|
|
17346
17461
|
}
|
|
17347
17462
|
|
|
17348
17463
|
// src/sample-ticket.ts
|
|
17349
|
-
import { basename as basename10, dirname as
|
|
17464
|
+
import { basename as basename10, dirname as dirname19, join as join34 } from "path";
|
|
17350
17465
|
var SAMPLE_REPO = "crew-sample";
|
|
17351
17466
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
17352
17467
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -17369,7 +17484,7 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
17369
17484
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
17370
17485
|
`;
|
|
17371
17486
|
function sampleDir(existing, reposBase, workspace) {
|
|
17372
|
-
if (existing.length > 0) return join34(
|
|
17487
|
+
if (existing.length > 0) return join34(dirname19(existing[0]), SAMPLE_REPO);
|
|
17373
17488
|
return join34(reposBase, workspace, SAMPLE_REPO);
|
|
17374
17489
|
}
|
|
17375
17490
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
@@ -17414,8 +17529,8 @@ function planSampleTicket(i) {
|
|
|
17414
17529
|
}
|
|
17415
17530
|
|
|
17416
17531
|
// src/worktree-sweep.ts
|
|
17417
|
-
import { existsSync as
|
|
17418
|
-
import { basename as basename11, dirname as
|
|
17532
|
+
import { existsSync as existsSync34, readFileSync as readFileSync31, readdirSync as readdirSync12 } from "fs";
|
|
17533
|
+
import { basename as basename11, dirname as dirname20, join as join35, resolve as resolve4 } from "path";
|
|
17419
17534
|
function planWorktreeSweep(target, tickets, contract) {
|
|
17420
17535
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
17421
17536
|
const verified = contract.statuses.verified;
|
|
@@ -17479,7 +17594,7 @@ function worktreeTicketNumbers(target) {
|
|
|
17479
17594
|
return [...nums];
|
|
17480
17595
|
}
|
|
17481
17596
|
function siblingWorktreeDirs(target) {
|
|
17482
|
-
const parent =
|
|
17597
|
+
const parent = dirname20(target.dir.replace(/[/\\]+$/, ""));
|
|
17483
17598
|
const stem = `${basename11(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
17484
17599
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
17485
17600
|
let names;
|
|
@@ -17527,7 +17642,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
17527
17642
|
let removed = 0;
|
|
17528
17643
|
let keptBranches = 0;
|
|
17529
17644
|
for (const a of actions) {
|
|
17530
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
17645
|
+
if (!dryRun && base && !a.keepBranch && existsSync34(a.path) && isHandBackMerge(a.path, base)) {
|
|
17531
17646
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
17532
17647
|
log.emit(
|
|
17533
17648
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -17535,7 +17650,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
17535
17650
|
);
|
|
17536
17651
|
}
|
|
17537
17652
|
}
|
|
17538
|
-
if (!dryRun &&
|
|
17653
|
+
if (!dryRun && existsSync34(a.path)) {
|
|
17539
17654
|
let dirty = false;
|
|
17540
17655
|
try {
|
|
17541
17656
|
dirty = status(a.path).length > 0;
|
|
@@ -17546,7 +17661,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
17546
17661
|
continue;
|
|
17547
17662
|
}
|
|
17548
17663
|
}
|
|
17549
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
17664
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync34(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
17550
17665
|
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
17551
17666
|
continue;
|
|
17552
17667
|
}
|
|
@@ -17595,7 +17710,7 @@ function isHandBackMerge(path, base) {
|
|
|
17595
17710
|
if (!msgPath) return false;
|
|
17596
17711
|
let first;
|
|
17597
17712
|
try {
|
|
17598
|
-
first =
|
|
17713
|
+
first = readFileSync31(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
17599
17714
|
} catch {
|
|
17600
17715
|
return false;
|
|
17601
17716
|
}
|
|
@@ -17700,12 +17815,12 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
17700
17815
|
}
|
|
17701
17816
|
|
|
17702
17817
|
// src/stream-sweep.ts
|
|
17703
|
-
import { existsSync as
|
|
17818
|
+
import { existsSync as existsSync35, readdirSync as readdirSync13, rmSync as rmSync15, statSync as statSync10 } from "fs";
|
|
17704
17819
|
import { join as join36 } from "path";
|
|
17705
17820
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
17706
17821
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
17707
17822
|
const dir = join36(stateDir, "streams");
|
|
17708
|
-
if (!
|
|
17823
|
+
if (!existsSync35(dir)) return [];
|
|
17709
17824
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
17710
17825
|
return readdirSync13(dir).map((name) => join36(dir, name)).filter((path) => {
|
|
17711
17826
|
try {
|
|
@@ -17733,7 +17848,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
17733
17848
|
return removed;
|
|
17734
17849
|
}
|
|
17735
17850
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
17736
|
-
if (!
|
|
17851
|
+
if (!existsSync35(stateDir)) return [];
|
|
17737
17852
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
17738
17853
|
return readdirSync13(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join36(stateDir, name)).filter((path) => {
|
|
17739
17854
|
try {
|
|
@@ -17745,11 +17860,11 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
17745
17860
|
}
|
|
17746
17861
|
|
|
17747
17862
|
// src/cli.ts
|
|
17748
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
17863
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync38, existsSync as existsSync43, mkdirSync as mkdirSync28, writeFileSync as writeFileSync30, rmSync as rmSync20, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync4, statSync as statSync14, readdirSync as readdirSync19 } from "fs";
|
|
17749
17864
|
import { createInterface } from "readline/promises";
|
|
17750
17865
|
|
|
17751
17866
|
// src/periscope-cmd.ts
|
|
17752
|
-
import { existsSync as
|
|
17867
|
+
import { existsSync as existsSync36, mkdirSync as mkdirSync22, readFileSync as readFileSync32, rmSync as rmSync16, writeFileSync as writeFileSync25 } from "fs";
|
|
17753
17868
|
import { join as join37 } from "path";
|
|
17754
17869
|
var PeriscopeError = class extends Error {
|
|
17755
17870
|
};
|
|
@@ -17767,15 +17882,15 @@ function removePeriscopePrior(stateDir, ticketRecordId) {
|
|
|
17767
17882
|
}
|
|
17768
17883
|
function writePeriscopePrior(stateDir, ticketRecordId, prior) {
|
|
17769
17884
|
try {
|
|
17770
|
-
|
|
17771
|
-
|
|
17885
|
+
mkdirSync22(priorDir(stateDir), { recursive: true });
|
|
17886
|
+
writeFileSync25(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
|
|
17772
17887
|
} catch {
|
|
17773
17888
|
}
|
|
17774
17889
|
}
|
|
17775
17890
|
function readPeriscopePrior(stateDir, ticketRecordId) {
|
|
17776
17891
|
try {
|
|
17777
17892
|
const p = priorPath(stateDir, ticketRecordId);
|
|
17778
|
-
return
|
|
17893
|
+
return existsSync36(p) ? JSON.parse(readFileSync32(p, "utf8")) : void 0;
|
|
17779
17894
|
} catch {
|
|
17780
17895
|
return void 0;
|
|
17781
17896
|
}
|
|
@@ -18270,8 +18385,8 @@ import { dirname as dirOf2, resolve as resolvePath } from "path";
|
|
|
18270
18385
|
|
|
18271
18386
|
// src/ssh-keys.ts
|
|
18272
18387
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
18273
|
-
import { existsSync as
|
|
18274
|
-
import { dirname as
|
|
18388
|
+
import { existsSync as existsSync37, mkdirSync as mkdirSync23, readFileSync as readFileSync33, chmodSync as chmodSync3 } from "fs";
|
|
18389
|
+
import { dirname as dirname21, join as join39 } from "path";
|
|
18275
18390
|
var SshKeyError = class extends Error {
|
|
18276
18391
|
};
|
|
18277
18392
|
function shipSshKeyPathFor(stateDir) {
|
|
@@ -18280,8 +18395,8 @@ function shipSshKeyPathFor(stateDir) {
|
|
|
18280
18395
|
function ensureShipSshKeypair(stateDir) {
|
|
18281
18396
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
18282
18397
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
18283
|
-
if (!
|
|
18284
|
-
|
|
18398
|
+
if (!existsSync37(privateKeyPath)) {
|
|
18399
|
+
mkdirSync23(dirname21(privateKeyPath), { recursive: true });
|
|
18285
18400
|
try {
|
|
18286
18401
|
execFileSync8("ssh-keygen", [
|
|
18287
18402
|
"-t",
|
|
@@ -18301,10 +18416,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
18301
18416
|
}
|
|
18302
18417
|
chmodSync3(privateKeyPath, 384);
|
|
18303
18418
|
}
|
|
18304
|
-
if (!
|
|
18419
|
+
if (!existsSync37(publicKeyPath)) {
|
|
18305
18420
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
18306
18421
|
}
|
|
18307
|
-
const publicKey =
|
|
18422
|
+
const publicKey = readFileSync33(publicKeyPath, "utf8").trim();
|
|
18308
18423
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
18309
18424
|
}
|
|
18310
18425
|
function sshKeygenAvailable() {
|
|
@@ -18542,7 +18657,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
18542
18657
|
|
|
18543
18658
|
// src/tunnel.ts
|
|
18544
18659
|
import { spawn as spawn5 } from "child_process";
|
|
18545
|
-
import { existsSync as
|
|
18660
|
+
import { existsSync as existsSync38, mkdirSync as mkdirSync24, readFileSync as readFileSync34, writeFileSync as writeFileSync26, unlinkSync as unlinkSync3, readdirSync as readdirSync14 } from "fs";
|
|
18546
18661
|
import { randomInt } from "crypto";
|
|
18547
18662
|
import { join as join40 } from "path";
|
|
18548
18663
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
@@ -18620,17 +18735,17 @@ function tunnelStatePath(stateDir, workspaceId) {
|
|
|
18620
18735
|
}
|
|
18621
18736
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
18622
18737
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
18623
|
-
if (!
|
|
18738
|
+
if (!existsSync38(path)) return void 0;
|
|
18624
18739
|
try {
|
|
18625
|
-
return JSON.parse(
|
|
18740
|
+
return JSON.parse(readFileSync34(path, "utf8"));
|
|
18626
18741
|
} catch {
|
|
18627
18742
|
return void 0;
|
|
18628
18743
|
}
|
|
18629
18744
|
}
|
|
18630
18745
|
function writePersistedTunnel(stateDir, state) {
|
|
18631
18746
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
18632
|
-
|
|
18633
|
-
|
|
18747
|
+
mkdirSync24(join40(stateDir, "passengers"), { recursive: true });
|
|
18748
|
+
writeFileSync26(path, `${JSON.stringify(state, null, 2)}
|
|
18634
18749
|
`);
|
|
18635
18750
|
}
|
|
18636
18751
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -18733,7 +18848,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
18733
18848
|
}
|
|
18734
18849
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
18735
18850
|
const dir = join40(stateDir, "passengers");
|
|
18736
|
-
if (!
|
|
18851
|
+
if (!existsSync38(dir)) return [];
|
|
18737
18852
|
return readdirSync14(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
18738
18853
|
}
|
|
18739
18854
|
|
|
@@ -18854,7 +18969,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
18854
18969
|
import { createServer as createHttpServer } from "http";
|
|
18855
18970
|
import { spawn as spawn6 } from "child_process";
|
|
18856
18971
|
import { randomBytes } from "crypto";
|
|
18857
|
-
import { existsSync as
|
|
18972
|
+
import { existsSync as existsSync39, mkdirSync as mkdirSync25, readFileSync as readFileSync35, writeFileSync as writeFileSync27, unlinkSync as unlinkSync4, readdirSync as readdirSync15 } from "fs";
|
|
18858
18973
|
import { join as join41 } from "path";
|
|
18859
18974
|
|
|
18860
18975
|
// src/secret-compare.ts
|
|
@@ -18948,17 +19063,17 @@ function syncDaemonStatePath(stateDir, workspaceId) {
|
|
|
18948
19063
|
}
|
|
18949
19064
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
18950
19065
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
18951
|
-
if (!
|
|
19066
|
+
if (!existsSync39(path)) return void 0;
|
|
18952
19067
|
try {
|
|
18953
|
-
return JSON.parse(
|
|
19068
|
+
return JSON.parse(readFileSync35(path, "utf8"));
|
|
18954
19069
|
} catch {
|
|
18955
19070
|
return void 0;
|
|
18956
19071
|
}
|
|
18957
19072
|
}
|
|
18958
19073
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
18959
19074
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
18960
|
-
|
|
18961
|
-
|
|
19075
|
+
mkdirSync25(join41(stateDir, "passengers"), { recursive: true });
|
|
19076
|
+
writeFileSync27(path, `${JSON.stringify(state, null, 2)}
|
|
18962
19077
|
`);
|
|
18963
19078
|
}
|
|
18964
19079
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -18979,7 +19094,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
18979
19094
|
}
|
|
18980
19095
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
18981
19096
|
const dir = join41(stateDir, "passengers");
|
|
18982
|
-
if (!
|
|
19097
|
+
if (!existsSync39(dir)) return [];
|
|
18983
19098
|
const suffix = "-sync.json";
|
|
18984
19099
|
return readdirSync15(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
18985
19100
|
}
|
|
@@ -19067,13 +19182,13 @@ function describeCause(err3) {
|
|
|
19067
19182
|
}
|
|
19068
19183
|
|
|
19069
19184
|
// src/daemon.ts
|
|
19070
|
-
import { existsSync as
|
|
19185
|
+
import { existsSync as existsSync40, mkdirSync as mkdirSync26, readFileSync as readFileSync36, readdirSync as readdirSync16, rmSync as rmSync17, statSync as statSync12, writeFileSync as writeFileSync28 } from "fs";
|
|
19071
19186
|
import { join as join42 } from "path";
|
|
19072
19187
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
19073
19188
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
19074
19189
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
19075
19190
|
var ALLOWANCE_PAUSE_MS = 5 * 60 * 1e3;
|
|
19076
|
-
var defaultSleep = (ms3) => new Promise((
|
|
19191
|
+
var defaultSleep = (ms3) => new Promise((resolve7) => setTimeout(resolve7, ms3));
|
|
19077
19192
|
async function runDaemonLoop(o) {
|
|
19078
19193
|
const floor = o.floorMs ?? DEFAULT_FLOOR_MS;
|
|
19079
19194
|
const ceiling = o.ceilingMs ?? DEFAULT_CEILING_MS;
|
|
@@ -19130,15 +19245,15 @@ async function runDaemonLoop(o) {
|
|
|
19130
19245
|
}
|
|
19131
19246
|
}
|
|
19132
19247
|
function latestMtime(path) {
|
|
19133
|
-
let
|
|
19248
|
+
let stat2;
|
|
19134
19249
|
try {
|
|
19135
|
-
|
|
19250
|
+
stat2 = statSync12(path);
|
|
19136
19251
|
} catch {
|
|
19137
19252
|
return 0;
|
|
19138
19253
|
}
|
|
19139
|
-
if (
|
|
19140
|
-
if (!
|
|
19141
|
-
let latest =
|
|
19254
|
+
if (stat2.isFile()) return stat2.mtimeMs;
|
|
19255
|
+
if (!stat2.isDirectory()) return 0;
|
|
19256
|
+
let latest = stat2.mtimeMs;
|
|
19142
19257
|
for (const entry of readdirSync16(path, { withFileTypes: true })) {
|
|
19143
19258
|
latest = Math.max(latest, latestMtime(join42(path, entry.name)));
|
|
19144
19259
|
}
|
|
@@ -19149,7 +19264,7 @@ function computeTreeSignature(paths) {
|
|
|
19149
19264
|
}
|
|
19150
19265
|
function makeStaleChecker(paths, opts = {}) {
|
|
19151
19266
|
const initial = computeTreeSignature(paths);
|
|
19152
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
19267
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync40(opts.reloadSentinel);
|
|
19153
19268
|
}
|
|
19154
19269
|
function reloadSentinelFor(stateDir) {
|
|
19155
19270
|
return join42(stateDir, "reload-requested");
|
|
@@ -19158,14 +19273,14 @@ function daemonStampFor(stateDir) {
|
|
|
19158
19273
|
return join42(stateDir, "daemon.started");
|
|
19159
19274
|
}
|
|
19160
19275
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
19161
|
-
|
|
19276
|
+
mkdirSync26(stateDir, { recursive: true });
|
|
19162
19277
|
rmSync17(reloadSentinelFor(stateDir), { force: true });
|
|
19163
|
-
|
|
19278
|
+
writeFileSync28(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
19164
19279
|
`);
|
|
19165
19280
|
}
|
|
19166
19281
|
function readDaemonStarted(stateDir) {
|
|
19167
19282
|
try {
|
|
19168
|
-
const v = JSON.parse(
|
|
19283
|
+
const v = JSON.parse(readFileSync36(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
19169
19284
|
return typeof v === "number" ? v : null;
|
|
19170
19285
|
} catch {
|
|
19171
19286
|
return null;
|
|
@@ -19331,8 +19446,8 @@ async function runOnePassUnguarded(o) {
|
|
|
19331
19446
|
started.push(role);
|
|
19332
19447
|
let ready = () => {
|
|
19333
19448
|
};
|
|
19334
|
-
const settled = new Promise((
|
|
19335
|
-
ready =
|
|
19449
|
+
const settled = new Promise((resolve7) => {
|
|
19450
|
+
ready = resolve7;
|
|
19336
19451
|
});
|
|
19337
19452
|
const run2 = spawn9(role, decision, ctx, { ready }).then(() => {
|
|
19338
19453
|
breaker.recordSuccess(role);
|
|
@@ -19545,6 +19660,7 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
19545
19660
|
if (seat) {
|
|
19546
19661
|
const candidates2 = rankedCandidates(role, decision.selectionInput);
|
|
19547
19662
|
const contract = decision.selectionInput.contract ?? DEFAULT_CONTRACT;
|
|
19663
|
+
await refreshUnknownRepos({ route: route2, stateDir: ship.stateDir, fetchRows: () => tracker.repoRowsForMap(), tickets: candidates2 });
|
|
19548
19664
|
const result = await resolveTopCandidate(
|
|
19549
19665
|
tracker,
|
|
19550
19666
|
candidates2,
|
|
@@ -19571,6 +19687,11 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
19571
19687
|
role
|
|
19572
19688
|
});
|
|
19573
19689
|
}
|
|
19690
|
+
await reportUnservable(
|
|
19691
|
+
{ state: ctx.state, emit, route: route2, ship },
|
|
19692
|
+
result.unservable,
|
|
19693
|
+
(id) => repoLabelFor(route2, candidates2.find((c) => c.issue_id === id))
|
|
19694
|
+
);
|
|
19574
19695
|
if (result.held.length) {
|
|
19575
19696
|
emit.emit(`skipped ${result.held.join(", ")} \u2014 held by another live ship`, { step: "select", role });
|
|
19576
19697
|
}
|
|
@@ -19714,7 +19835,7 @@ import { createHash as createHash9 } from "crypto";
|
|
|
19714
19835
|
import {
|
|
19715
19836
|
chmodSync as chmodSync4,
|
|
19716
19837
|
closeSync as closeSync5,
|
|
19717
|
-
existsSync as
|
|
19838
|
+
existsSync as existsSync41,
|
|
19718
19839
|
fstatSync,
|
|
19719
19840
|
openSync as openSync5,
|
|
19720
19841
|
readSync as readSync3,
|
|
@@ -19725,7 +19846,7 @@ import {
|
|
|
19725
19846
|
} from "fs";
|
|
19726
19847
|
import { createConnection } from "net";
|
|
19727
19848
|
import { createServer } from "http";
|
|
19728
|
-
import { basename as basename12, dirname as
|
|
19849
|
+
import { basename as basename12, dirname as dirname22, join as join43 } from "path";
|
|
19729
19850
|
import { execFile, spawn as spawn8 } from "child_process";
|
|
19730
19851
|
|
|
19731
19852
|
// src/periscope-control.ts
|
|
@@ -19784,14 +19905,18 @@ function parseLine(line) {
|
|
|
19784
19905
|
return null;
|
|
19785
19906
|
}
|
|
19786
19907
|
}
|
|
19787
|
-
async function execute(h, args, streaming) {
|
|
19788
|
-
const
|
|
19908
|
+
async function execute(h, args, streaming, after) {
|
|
19909
|
+
const finishRaw = (r) => {
|
|
19789
19910
|
if (r.code !== 0) return { status: failureStatus(r), body: { error: cliMessage(r) } };
|
|
19790
19911
|
const done = lastJsonLine(r.stdout);
|
|
19791
19912
|
return done ? { status: 200, body: done } : { status: 502, body: { error: "the command finished without a result" } };
|
|
19792
19913
|
};
|
|
19914
|
+
const finish = async (r) => {
|
|
19915
|
+
const f = finishRaw(r);
|
|
19916
|
+
return f.status === 200 && after ? { status: 200, body: await after(f.body) } : f;
|
|
19917
|
+
};
|
|
19793
19918
|
if (!streaming) {
|
|
19794
|
-
const f = finish(await h.run(args, () => {
|
|
19919
|
+
const f = await finish(await h.run(args, () => {
|
|
19795
19920
|
}));
|
|
19796
19921
|
return reply(f.status, f.body);
|
|
19797
19922
|
}
|
|
@@ -19803,7 +19928,7 @@ async function execute(h, args, streaming) {
|
|
|
19803
19928
|
const ev = parseLine(line);
|
|
19804
19929
|
if (ev) write(ev);
|
|
19805
19930
|
});
|
|
19806
|
-
const f = finish(r);
|
|
19931
|
+
const f = await finish(r);
|
|
19807
19932
|
write(f.status === 200 ? f.body : { event: "error", status: f.status, message: f.body.error });
|
|
19808
19933
|
}
|
|
19809
19934
|
};
|
|
@@ -19867,7 +19992,10 @@ async function handlePeriscopes(h, q) {
|
|
|
19867
19992
|
`--${b.outcome}`,
|
|
19868
19993
|
...b.allowEmpty === true ? ["--allow-empty"] : [],
|
|
19869
19994
|
"--json"
|
|
19870
|
-
], q.wantsStream)
|
|
19995
|
+
], q.wantsStream, h.mcp ? async (done) => {
|
|
19996
|
+
await h.mcp.onDown(picked, done);
|
|
19997
|
+
return done;
|
|
19998
|
+
} : void 0);
|
|
19871
19999
|
}
|
|
19872
20000
|
const repo = plainText(b.repo, "repo", true);
|
|
19873
20001
|
const topic = plainText(b.topic, "topic", true);
|
|
@@ -19895,12 +20023,13 @@ async function handlePeriscopes(h, q) {
|
|
|
19895
20023
|
h.previewHost,
|
|
19896
20024
|
...requestedBy ? ["--requested-by", requestedBy] : [],
|
|
19897
20025
|
...b.isolate === true ? ["--isolate"] : [],
|
|
20026
|
+
...h.mcp ? ["--serve-mcp"] : [],
|
|
19898
20027
|
"--no-open",
|
|
19899
20028
|
"--json"
|
|
19900
|
-
], q.wantsStream);
|
|
20029
|
+
], q.wantsStream, h.mcp ? async (done) => ({ ...done, ...await h.mcp.onUp(picked, done) }) : void 0);
|
|
19901
20030
|
}
|
|
19902
20031
|
function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
19903
|
-
return new Promise((
|
|
20032
|
+
return new Promise((resolve7) => {
|
|
19904
20033
|
const child = spawn7(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
19905
20034
|
let stdout = "";
|
|
19906
20035
|
let stderr = "";
|
|
@@ -19909,9 +20038,9 @@ function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
|
19909
20038
|
stdout += d.toString("utf8");
|
|
19910
20039
|
});
|
|
19911
20040
|
child.stderr.on("data", (d) => {
|
|
19912
|
-
const
|
|
19913
|
-
stderr +=
|
|
19914
|
-
pending +=
|
|
20041
|
+
const text2 = d.toString("utf8");
|
|
20042
|
+
stderr += text2;
|
|
20043
|
+
pending += text2;
|
|
19915
20044
|
let i;
|
|
19916
20045
|
while ((i = pending.indexOf("\n")) >= 0) {
|
|
19917
20046
|
const line = pending.slice(0, i).trim();
|
|
@@ -19922,12 +20051,12 @@ function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
|
19922
20051
|
const t = setTimeout(() => child.kill("SIGTERM"), timeoutMs);
|
|
19923
20052
|
child.on("error", (e) => {
|
|
19924
20053
|
clearTimeout(t);
|
|
19925
|
-
|
|
20054
|
+
resolve7({ code: 127, stdout, stderr: `${stderr}${e.message}
|
|
19926
20055
|
` });
|
|
19927
20056
|
});
|
|
19928
20057
|
child.on("close", (code) => {
|
|
19929
20058
|
clearTimeout(t);
|
|
19930
|
-
|
|
20059
|
+
resolve7({ code: code ?? 1, stdout, stderr });
|
|
19931
20060
|
});
|
|
19932
20061
|
});
|
|
19933
20062
|
}
|
|
@@ -20021,7 +20150,7 @@ function candidates(live, rotated) {
|
|
|
20021
20150
|
const out = [live];
|
|
20022
20151
|
if (!rotated) return out;
|
|
20023
20152
|
try {
|
|
20024
|
-
for (const n of readdirSync17(
|
|
20153
|
+
for (const n of readdirSync17(dirname22(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join43(dirname22(live), n));
|
|
20025
20154
|
} catch {
|
|
20026
20155
|
}
|
|
20027
20156
|
return out;
|
|
@@ -20048,16 +20177,16 @@ function lastLines2(path, n) {
|
|
|
20048
20177
|
const cursorAt = (off) => encodeCursor({ dev: st.dev, ino: st.ino, off });
|
|
20049
20178
|
if (n <= 0 || st.size === 0) return { lines: [], cursor: cursorAt(st.size), reset: false };
|
|
20050
20179
|
let start = st.size;
|
|
20051
|
-
let
|
|
20052
|
-
while (start > 0 &&
|
|
20180
|
+
let text2 = "";
|
|
20181
|
+
while (start > 0 && text2.split("\n").length <= n + 1 && st.size - start < MAX_TAIL_BYTES) {
|
|
20053
20182
|
const step = Math.min(64 * 1024, start);
|
|
20054
20183
|
start -= step;
|
|
20055
|
-
|
|
20184
|
+
text2 = readRange(path, start, step).text + text2;
|
|
20056
20185
|
}
|
|
20057
|
-
let lines =
|
|
20186
|
+
let lines = text2.split("\n");
|
|
20058
20187
|
if (start > 0) lines = lines.slice(1);
|
|
20059
20188
|
let end = st.size;
|
|
20060
|
-
if (!
|
|
20189
|
+
if (!text2.endsWith("\n")) {
|
|
20061
20190
|
const frag = lines.pop() ?? "";
|
|
20062
20191
|
end -= Buffer.byteLength(frag);
|
|
20063
20192
|
} else lines.pop();
|
|
@@ -20065,7 +20194,7 @@ function lastLines2(path, n) {
|
|
|
20065
20194
|
}
|
|
20066
20195
|
function readTail(live, opts) {
|
|
20067
20196
|
const max = opts.maxBytes ?? MAX_TAIL_BYTES;
|
|
20068
|
-
if (!
|
|
20197
|
+
if (!existsSync41(live)) {
|
|
20069
20198
|
return { lines: [], cursor: opts.after ?? encodeCursor({ dev: 0, ino: 0, off: 0 }), reset: false };
|
|
20070
20199
|
}
|
|
20071
20200
|
const cur = opts.after ? decodeCursor(opts.after) : null;
|
|
@@ -20099,9 +20228,9 @@ function readTail(live, opts) {
|
|
|
20099
20228
|
off = 0;
|
|
20100
20229
|
size = statSync13(path).size;
|
|
20101
20230
|
}
|
|
20102
|
-
const { text } = readRange(path, off, max);
|
|
20103
|
-
const cut =
|
|
20104
|
-
const whole = cut < 0 ? "" :
|
|
20231
|
+
const { text: text2 } = readRange(path, off, max);
|
|
20232
|
+
const cut = text2.lastIndexOf("\n");
|
|
20233
|
+
const whole = cut < 0 ? "" : text2.slice(0, cut + 1);
|
|
20105
20234
|
const st = statSync13(path);
|
|
20106
20235
|
return {
|
|
20107
20236
|
lines: whole.split("\n").filter((l) => l.length),
|
|
@@ -20110,7 +20239,7 @@ function readTail(live, opts) {
|
|
|
20110
20239
|
};
|
|
20111
20240
|
}
|
|
20112
20241
|
function waitForChange(dir, ms3, signal) {
|
|
20113
|
-
return new Promise((
|
|
20242
|
+
return new Promise((resolve7) => {
|
|
20114
20243
|
let w;
|
|
20115
20244
|
const done = () => {
|
|
20116
20245
|
clearTimeout(t);
|
|
@@ -20120,7 +20249,7 @@ function waitForChange(dir, ms3, signal) {
|
|
|
20120
20249
|
w?.close();
|
|
20121
20250
|
} catch {
|
|
20122
20251
|
}
|
|
20123
|
-
|
|
20252
|
+
resolve7();
|
|
20124
20253
|
};
|
|
20125
20254
|
const t = setTimeout(done, ms3);
|
|
20126
20255
|
const poll = setInterval(done, 500);
|
|
@@ -20181,7 +20310,7 @@ async function tailReply(path, q, rotated, abort) {
|
|
|
20181
20310
|
const deadline = Date.now() + Math.min(wait, MAX_WAIT_SECONDS) * 1e3;
|
|
20182
20311
|
let r = readTail(path, { after, tail: tail3, rotated });
|
|
20183
20312
|
while (after && !r.lines.length && !r.reset && Date.now() < deadline && !abort.aborted) {
|
|
20184
|
-
await waitForChange(
|
|
20313
|
+
await waitForChange(dirname22(path), deadline - Date.now(), abort);
|
|
20185
20314
|
r = readTail(path, { after: r.cursor, rotated });
|
|
20186
20315
|
}
|
|
20187
20316
|
return reply2(200, r);
|
|
@@ -20222,7 +20351,7 @@ async function route(deps, method, url, req, abort) {
|
|
|
20222
20351
|
const run2 = decodeURIComponent(p.slice("/streams/".length));
|
|
20223
20352
|
if (!RUN_NAME_RE.test(run2) || run2.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run2)) return err2(400, "not a run name");
|
|
20224
20353
|
const file = join43(deps.stateDir, "streams", run2.endsWith(".jsonl") ? run2 : `${run2}.jsonl`);
|
|
20225
|
-
if (!
|
|
20354
|
+
if (!existsSync41(file)) return err2(404, "no such run");
|
|
20226
20355
|
return tailReply(file, url.searchParams, false, abort);
|
|
20227
20356
|
}
|
|
20228
20357
|
if ((p === "/pause" || p === "/resume") && post3) {
|
|
@@ -20266,14 +20395,14 @@ function send(res, r) {
|
|
|
20266
20395
|
res.end(body);
|
|
20267
20396
|
}
|
|
20268
20397
|
async function socketIsStale(path) {
|
|
20269
|
-
if (!
|
|
20270
|
-
return new Promise((
|
|
20398
|
+
if (!existsSync41(path)) return false;
|
|
20399
|
+
return new Promise((resolve7) => {
|
|
20271
20400
|
const c = createConnection(path);
|
|
20272
20401
|
c.once("connect", () => {
|
|
20273
20402
|
c.destroy();
|
|
20274
|
-
|
|
20403
|
+
resolve7(false);
|
|
20275
20404
|
});
|
|
20276
|
-
c.once("error", () =>
|
|
20405
|
+
c.once("error", () => resolve7(true));
|
|
20277
20406
|
});
|
|
20278
20407
|
}
|
|
20279
20408
|
async function startControlServer(deps) {
|
|
@@ -20285,7 +20414,7 @@ async function startControlServer(deps) {
|
|
|
20285
20414
|
return null;
|
|
20286
20415
|
}
|
|
20287
20416
|
if (await socketIsStale(path)) rmSync18(path, { force: true });
|
|
20288
|
-
else if (
|
|
20417
|
+
else if (existsSync41(path)) {
|
|
20289
20418
|
log(`control API not started: ${path} is in use by another process`);
|
|
20290
20419
|
return null;
|
|
20291
20420
|
}
|
|
@@ -20294,6 +20423,11 @@ async function startControlServer(deps) {
|
|
|
20294
20423
|
res.on("close", () => abort.abort());
|
|
20295
20424
|
void (async () => {
|
|
20296
20425
|
try {
|
|
20426
|
+
const mcp = /^\/periscopes\/([^/]+)\/mcp\/?$/.exec(new URL(req.url ?? "/", "http://ship").pathname);
|
|
20427
|
+
if (mcp) {
|
|
20428
|
+
if (!deps.periscopeMcp) return send(res, err2(404, "not found"));
|
|
20429
|
+
return await deps.periscopeMcp(req, res, decodeURIComponent(mcp[1]));
|
|
20430
|
+
}
|
|
20297
20431
|
const m = /^Bearer (.+)$/.exec(req.headers.authorization ?? "");
|
|
20298
20432
|
const verdict = await deps.auth.verify(m?.[1]?.trim() ?? "");
|
|
20299
20433
|
if (verdict === "unauthorized") return send(res, err2(401, "invalid or revoked key"));
|
|
@@ -20307,10 +20441,10 @@ async function startControlServer(deps) {
|
|
|
20307
20441
|
}
|
|
20308
20442
|
})();
|
|
20309
20443
|
});
|
|
20310
|
-
return new Promise((
|
|
20444
|
+
return new Promise((resolve7) => {
|
|
20311
20445
|
server.once("error", (e) => {
|
|
20312
20446
|
log(`control API not started: ${e.message}`);
|
|
20313
|
-
|
|
20447
|
+
resolve7(null);
|
|
20314
20448
|
});
|
|
20315
20449
|
server.listen(path, () => {
|
|
20316
20450
|
try {
|
|
@@ -20318,7 +20452,7 @@ async function startControlServer(deps) {
|
|
|
20318
20452
|
} catch {
|
|
20319
20453
|
}
|
|
20320
20454
|
server.on("error", (e) => log(`control API error: ${e.message}`));
|
|
20321
|
-
|
|
20455
|
+
resolve7({
|
|
20322
20456
|
server,
|
|
20323
20457
|
path,
|
|
20324
20458
|
close: () => new Promise((done) => {
|
|
@@ -20349,20 +20483,434 @@ function schedulerHandler(control, run2 = runDetached) {
|
|
|
20349
20483
|
};
|
|
20350
20484
|
}
|
|
20351
20485
|
function crewJson(args, timeoutMs = 6e4) {
|
|
20352
|
-
return new Promise((
|
|
20486
|
+
return new Promise((resolve7) => {
|
|
20353
20487
|
execFile(process.execPath, [process.argv[1] ?? "", ...args], { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (_e, stdout) => {
|
|
20354
20488
|
try {
|
|
20355
20489
|
const v = JSON.parse(String(stdout));
|
|
20356
|
-
|
|
20490
|
+
resolve7(v && typeof v === "object" && !Array.isArray(v) ? v : null);
|
|
20357
20491
|
} catch {
|
|
20358
|
-
|
|
20492
|
+
resolve7(null);
|
|
20359
20493
|
}
|
|
20360
20494
|
});
|
|
20361
20495
|
});
|
|
20362
20496
|
}
|
|
20363
20497
|
|
|
20498
|
+
// src/periscope-mcp.ts
|
|
20499
|
+
import { lstat, mkdir, readdir, readFile, readlink, realpath, rm, stat, writeFile } from "fs/promises";
|
|
20500
|
+
import { basename as basename13, dirname as dirname23, isAbsolute as isAbsolute3, join as join45, relative as relative2, resolve as resolve5, sep } from "path";
|
|
20501
|
+
import { z } from "zod";
|
|
20502
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
20503
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
20504
|
+
|
|
20505
|
+
// src/periscope-endpoints.ts
|
|
20506
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
20507
|
+
import { chmodSync as chmodSync5, existsSync as existsSync42, mkdirSync as mkdirSync27, readdirSync as readdirSync18, readFileSync as readFileSync37, rmSync as rmSync19, writeFileSync as writeFileSync29 } from "fs";
|
|
20508
|
+
import { join as join44 } from "path";
|
|
20509
|
+
var secretDir = (stateDir) => join44(stateDir, "periscope-mcp");
|
|
20510
|
+
var secretFile = (stateDir, id) => join44(secretDir(stateDir), `${id}.json`);
|
|
20511
|
+
var endpointDir = (stateDir) => join44(stateDir, "periscope-endpoints");
|
|
20512
|
+
var endpointFile = (stateDir, id) => join44(endpointDir(stateDir), `${id}.json`);
|
|
20513
|
+
function mintPeriscopeSecret(stateDir, ticketRecordId) {
|
|
20514
|
+
mkdirSync27(secretDir(stateDir), { recursive: true, mode: 448 });
|
|
20515
|
+
const secret = randomBytes3(32).toString("base64url");
|
|
20516
|
+
const file = secretFile(stateDir, ticketRecordId);
|
|
20517
|
+
writeFileSync29(file, `${JSON.stringify({ secret })}
|
|
20518
|
+
`, { mode: 384 });
|
|
20519
|
+
chmodSync5(file, 384);
|
|
20520
|
+
return secret;
|
|
20521
|
+
}
|
|
20522
|
+
function readPeriscopeSecret(stateDir, ticketRecordId) {
|
|
20523
|
+
try {
|
|
20524
|
+
const v = JSON.parse(readFileSync37(secretFile(stateDir, ticketRecordId), "utf8"));
|
|
20525
|
+
return typeof v.secret === "string" && v.secret ? v.secret : void 0;
|
|
20526
|
+
} catch {
|
|
20527
|
+
return void 0;
|
|
20528
|
+
}
|
|
20529
|
+
}
|
|
20530
|
+
function removePeriscopeSecret(stateDir, ticketRecordId) {
|
|
20531
|
+
rmSync19(secretFile(stateDir, ticketRecordId), { force: true });
|
|
20532
|
+
}
|
|
20533
|
+
function readPeriscopeEndpoint(stateDir, ticketRecordId) {
|
|
20534
|
+
try {
|
|
20535
|
+
return JSON.parse(readFileSync37(endpointFile(stateDir, ticketRecordId), "utf8"));
|
|
20536
|
+
} catch {
|
|
20537
|
+
return void 0;
|
|
20538
|
+
}
|
|
20539
|
+
}
|
|
20540
|
+
function writePeriscopeEndpoint(stateDir, rec) {
|
|
20541
|
+
mkdirSync27(endpointDir(stateDir), { recursive: true });
|
|
20542
|
+
writeFileSync29(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
|
|
20543
|
+
`);
|
|
20544
|
+
}
|
|
20545
|
+
function removePeriscopeEndpointRecord(stateDir, ticketRecordId) {
|
|
20546
|
+
rmSync19(endpointFile(stateDir, ticketRecordId), { force: true });
|
|
20547
|
+
}
|
|
20548
|
+
function listPeriscopeEndpoints(stateDir) {
|
|
20549
|
+
const dir = endpointDir(stateDir);
|
|
20550
|
+
if (!existsSync42(dir)) return [];
|
|
20551
|
+
const out = [];
|
|
20552
|
+
for (const name of readdirSync18(dir)) {
|
|
20553
|
+
if (!name.endsWith(".json")) continue;
|
|
20554
|
+
const rec = readPeriscopeEndpoint(stateDir, name.slice(0, -".json".length));
|
|
20555
|
+
if (rec && typeof rec.endpointId === "string") out.push(rec);
|
|
20556
|
+
}
|
|
20557
|
+
return out.sort((a, b) => a.registeredAt.localeCompare(b.registeredAt));
|
|
20558
|
+
}
|
|
20559
|
+
function periscopeEndpointUrl(relayUrl, ticket) {
|
|
20560
|
+
return `${relayUrl.replace(/\/+$/, "")}/control/periscopes/${encodeURIComponent(ticket)}/mcp`;
|
|
20561
|
+
}
|
|
20562
|
+
async function registerPeriscopeEndpoint(d, p) {
|
|
20563
|
+
const secret = readPeriscopeSecret(d.stateDir, p.ticketRecordId);
|
|
20564
|
+
if (!secret) return { ok: false, message: `${p.ticket} has no MCP secret (it was not opened with --mcp, or has ended)` };
|
|
20565
|
+
const api = d.apiFor(p.workspaceId);
|
|
20566
|
+
if (!api) return { ok: false, message: `no API key known for workspace ${p.workspaceId}` };
|
|
20567
|
+
const url = periscopeEndpointUrl(p.relayUrl, p.ticket);
|
|
20568
|
+
const stored = readPeriscopeEndpoint(d.stateDir, p.ticketRecordId);
|
|
20569
|
+
const out = await registerEndpoint(api, p.workspaceId, stored?.endpointId, {
|
|
20570
|
+
label: `Periscope: ${d.shipName} ${p.ticket}`,
|
|
20571
|
+
url,
|
|
20572
|
+
mcpSecret: secret
|
|
20573
|
+
});
|
|
20574
|
+
if (out.kind !== "ok") {
|
|
20575
|
+
return { ok: false, message: out.kind === "forbidden" ? "registering the endpoint was refused (403): the route key lacks REGISTER_MCP_ENDPOINTS" : out.message };
|
|
20576
|
+
}
|
|
20577
|
+
writePeriscopeEndpoint(d.stateDir, {
|
|
20578
|
+
ticket: p.ticket,
|
|
20579
|
+
ticketRecordId: p.ticketRecordId,
|
|
20580
|
+
route: p.route,
|
|
20581
|
+
workspaceId: p.workspaceId,
|
|
20582
|
+
endpointId: out.endpointId,
|
|
20583
|
+
url,
|
|
20584
|
+
registeredAt: (d.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
20585
|
+
});
|
|
20586
|
+
return { ok: true, endpointId: out.endpointId, url };
|
|
20587
|
+
}
|
|
20588
|
+
async function removeEndedPeriscopeEndpoints(d) {
|
|
20589
|
+
let removed = 0;
|
|
20590
|
+
for (const rec of listPeriscopeEndpoints(d.stateDir)) {
|
|
20591
|
+
if (readPeriscopeSecret(d.stateDir, rec.ticketRecordId)) continue;
|
|
20592
|
+
const api = d.apiFor(rec.workspaceId);
|
|
20593
|
+
if (!api) {
|
|
20594
|
+
d.warn(`no API key known for workspace ${rec.workspaceId} \u2014 cannot remove the MCP endpoint of ${rec.ticket}`);
|
|
20595
|
+
continue;
|
|
20596
|
+
}
|
|
20597
|
+
const out = await removeEndpoint(api, rec.workspaceId, rec.endpointId);
|
|
20598
|
+
if (out.kind === "ok") {
|
|
20599
|
+
removePeriscopeEndpointRecord(d.stateDir, rec.ticketRecordId);
|
|
20600
|
+
removed++;
|
|
20601
|
+
} else d.warn(`could not remove the MCP endpoint of ${rec.ticket}: ${out.kind === "forbidden" ? "403" : out.message}`);
|
|
20602
|
+
}
|
|
20603
|
+
return removed;
|
|
20604
|
+
}
|
|
20605
|
+
|
|
20606
|
+
// src/periscope-mcp.ts
|
|
20607
|
+
var PERISCOPE_HOOKS = ["test", "build", "ports", "handoff", "preview", "isolate"];
|
|
20608
|
+
var MAX_READ_BYTES = 1024 * 1024;
|
|
20609
|
+
var MAX_WRITE_BYTES = 4 * 1024 * 1024;
|
|
20610
|
+
var MAX_BODY_BYTES2 = 8 * 1024 * 1024;
|
|
20611
|
+
var MAX_HOOK_OUTPUT = 2e4;
|
|
20612
|
+
var MAX_DIFF = 2e5;
|
|
20613
|
+
var MAX_SEARCH_RESULTS = 500;
|
|
20614
|
+
var PathRefusal = class extends Error {
|
|
20615
|
+
};
|
|
20616
|
+
var isInside = (root, p) => {
|
|
20617
|
+
const rel = relative2(root, p);
|
|
20618
|
+
return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute3(rel);
|
|
20619
|
+
};
|
|
20620
|
+
async function realAncestor(p) {
|
|
20621
|
+
let cur = p;
|
|
20622
|
+
const tail3 = [];
|
|
20623
|
+
let hops = 0;
|
|
20624
|
+
for (; ; ) {
|
|
20625
|
+
try {
|
|
20626
|
+
return { real: await realpath(cur), rest: tail3.slice().reverse().join(sep) };
|
|
20627
|
+
} catch (e) {
|
|
20628
|
+
if (e.code !== "ENOENT") throw new PathRefusal(`cannot resolve ${p}`);
|
|
20629
|
+
}
|
|
20630
|
+
let isLink = false;
|
|
20631
|
+
try {
|
|
20632
|
+
isLink = (await lstat(cur)).isSymbolicLink();
|
|
20633
|
+
} catch {
|
|
20634
|
+
}
|
|
20635
|
+
if (isLink) {
|
|
20636
|
+
if (++hops > 40) throw new PathRefusal(`too many symbolic links resolving ${p}`);
|
|
20637
|
+
cur = resolve5(dirname23(cur), await readlink(cur));
|
|
20638
|
+
continue;
|
|
20639
|
+
}
|
|
20640
|
+
const parent = dirname23(cur);
|
|
20641
|
+
if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
|
|
20642
|
+
tail3.push(basename13(cur));
|
|
20643
|
+
cur = parent;
|
|
20644
|
+
}
|
|
20645
|
+
}
|
|
20646
|
+
async function resolveInWorktree(root, requested) {
|
|
20647
|
+
if (!requested || requested.includes("\0")) throw new PathRefusal("path is required");
|
|
20648
|
+
const realRoot = await realpath(root);
|
|
20649
|
+
let abs;
|
|
20650
|
+
if (isAbsolute3(requested)) {
|
|
20651
|
+
abs = resolve5(requested);
|
|
20652
|
+
if (isInside(root, abs)) abs = join45(realRoot, relative2(root, abs));
|
|
20653
|
+
} else {
|
|
20654
|
+
abs = resolve5(realRoot, requested);
|
|
20655
|
+
}
|
|
20656
|
+
if (!isInside(realRoot, abs)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
|
|
20657
|
+
const { real, rest } = await realAncestor(abs);
|
|
20658
|
+
const final = rest ? join45(real, rest) : real;
|
|
20659
|
+
if (!isInside(realRoot, final)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
|
|
20660
|
+
if (relative2(realRoot, final).split(sep).includes(".git")) throw new PathRefusal(".git is not reachable from a periscope");
|
|
20661
|
+
return final;
|
|
20662
|
+
}
|
|
20663
|
+
function globToRegExp(glob) {
|
|
20664
|
+
let re = "";
|
|
20665
|
+
for (let i = 0; i < glob.length; i++) {
|
|
20666
|
+
const c = glob[i];
|
|
20667
|
+
if (c === "*") {
|
|
20668
|
+
if (glob[i + 1] === "*") {
|
|
20669
|
+
i++;
|
|
20670
|
+
if (glob[i + 1] === "/") {
|
|
20671
|
+
i++;
|
|
20672
|
+
re += "(?:.*/)?";
|
|
20673
|
+
} else re += ".*";
|
|
20674
|
+
} else re += "[^/]*";
|
|
20675
|
+
} else if (c === "?") re += "[^/]";
|
|
20676
|
+
else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
20677
|
+
}
|
|
20678
|
+
return new RegExp(`^${re}$`);
|
|
20679
|
+
}
|
|
20680
|
+
var tailText = (s, max = MAX_HOOK_OUTPUT) => s.length > max ? `\u2026(${s.length - max} earlier characters omitted)
|
|
20681
|
+
${s.slice(-max)}` : s;
|
|
20682
|
+
var text = (t, isError = false) => ({ content: [{ type: "text", text: t }], ...isError ? { isError: true } : {} });
|
|
20683
|
+
async function hookEnv(d, sc, name) {
|
|
20684
|
+
const env = { CREW_TICKET: sc.ticket, CREW_BRANCH: sc.branch };
|
|
20685
|
+
if (name !== "ports") {
|
|
20686
|
+
const ports = await d.runHook(sc, "ports", env);
|
|
20687
|
+
if (ports) Object.assign(env, parseEnvLines(ports.output));
|
|
20688
|
+
if (sc.isolated && name !== "isolate") {
|
|
20689
|
+
const iso = await d.runHook(sc, "isolate", env);
|
|
20690
|
+
if (iso && iso.code === 0) Object.assign(env, parseEnvLines(iso.output));
|
|
20691
|
+
}
|
|
20692
|
+
}
|
|
20693
|
+
if (name === "preview") env.CREW_PREVIEW_HOST = sc.host || "localhost";
|
|
20694
|
+
return env;
|
|
20695
|
+
}
|
|
20696
|
+
function createPeriscopeMcpServer(d, sc) {
|
|
20697
|
+
const server = new McpServer({ name: `crew-periscope-${sc.ticket}`, version: "0.1.0" });
|
|
20698
|
+
const root = sc.worktree;
|
|
20699
|
+
const inTree = (p) => resolveInWorktree(root, p);
|
|
20700
|
+
const rel = async (abs) => relative2(await realpath(root), abs) || ".";
|
|
20701
|
+
server.registerTool("read_file", {
|
|
20702
|
+
title: "Read File",
|
|
20703
|
+
description: "Read a text file in this periscope's worktree. `head`/`tail` return only the first/last N lines.",
|
|
20704
|
+
inputSchema: { path: z.string(), head: z.number().int().positive().optional(), tail: z.number().int().positive().optional() },
|
|
20705
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
20706
|
+
}, async ({ path, head, tail: tail3 }) => {
|
|
20707
|
+
if (head && tail3) return text("Cannot specify both head and tail", true);
|
|
20708
|
+
const abs = await inTree(path);
|
|
20709
|
+
const st = await stat(abs);
|
|
20710
|
+
if (!st.isFile()) return text(`${path} is not a file`, true);
|
|
20711
|
+
if (st.size > MAX_READ_BYTES) return text(`${path} is ${st.size} bytes; read it in parts with head or tail (limit ${MAX_READ_BYTES})`, true);
|
|
20712
|
+
const content = await readFile(abs, "utf8");
|
|
20713
|
+
const lines = content.split("\n");
|
|
20714
|
+
return text(head ? lines.slice(0, head).join("\n") : tail3 ? lines.slice(-tail3).join("\n") : content);
|
|
20715
|
+
});
|
|
20716
|
+
server.registerTool("list_directory", {
|
|
20717
|
+
title: "List Directory",
|
|
20718
|
+
description: 'List the entries directly inside a directory of the worktree ("." is its root), each marked [FILE] or [DIR].',
|
|
20719
|
+
inputSchema: { path: z.string().default(".") },
|
|
20720
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
20721
|
+
}, async ({ path }) => {
|
|
20722
|
+
const abs = await inTree(path);
|
|
20723
|
+
const entries = (await readdir(abs, { withFileTypes: true })).filter((e) => e.name !== ".git");
|
|
20724
|
+
const lines = await Promise.all(entries.map(async (e) => {
|
|
20725
|
+
if (e.isDirectory()) return `[DIR] ${e.name}`;
|
|
20726
|
+
const size = (await lstat(join45(abs, e.name))).size;
|
|
20727
|
+
return `[FILE] ${e.name} (${size} bytes)`;
|
|
20728
|
+
}));
|
|
20729
|
+
return text(lines.join("\n") || "(empty)");
|
|
20730
|
+
});
|
|
20731
|
+
server.registerTool("search_files", {
|
|
20732
|
+
title: "Search Files",
|
|
20733
|
+
description: "Find files in the worktree whose path (relative to the worktree) matches the glob `pattern` (`*` stays within a directory, `**` crosses them). Lists files git tracks or would not ignore, so `node_modules` and build output do not appear.",
|
|
20734
|
+
inputSchema: { pattern: z.string() },
|
|
20735
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
20736
|
+
}, async ({ pattern }) => {
|
|
20737
|
+
const re = globToRegExp(pattern);
|
|
20738
|
+
const listing = git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]);
|
|
20739
|
+
const hits = listing.split("\0").filter((f) => f && re.test(f)).slice(0, MAX_SEARCH_RESULTS);
|
|
20740
|
+
return text(hits.length ? hits.join("\n") : "No matches found");
|
|
20741
|
+
});
|
|
20742
|
+
server.registerTool("write_file", {
|
|
20743
|
+
title: "Write File",
|
|
20744
|
+
description: "Create a file in the worktree, or replace its whole content. Parent directories are created.",
|
|
20745
|
+
inputSchema: { path: z.string(), content: z.string() },
|
|
20746
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
20747
|
+
}, async ({ path, content }) => {
|
|
20748
|
+
if (Buffer.byteLength(content) > MAX_WRITE_BYTES) return text(`content exceeds ${MAX_WRITE_BYTES} bytes`, true);
|
|
20749
|
+
const abs = await inTree(path);
|
|
20750
|
+
if (abs === await realpath(root)) return text("path must name a file", true);
|
|
20751
|
+
await mkdir(dirname23(abs), { recursive: true });
|
|
20752
|
+
await inTree(path);
|
|
20753
|
+
await writeFile(abs, content);
|
|
20754
|
+
return text(`Wrote ${Buffer.byteLength(content)} bytes to ${await rel(abs)}`);
|
|
20755
|
+
});
|
|
20756
|
+
server.registerTool("edit_file", {
|
|
20757
|
+
title: "Edit File",
|
|
20758
|
+
description: "Replace an exact string in a file. `oldString` must occur exactly once unless `replaceAll` is true; whitespace and line endings must match.",
|
|
20759
|
+
inputSchema: { path: z.string(), oldString: z.string().min(1), newString: z.string(), replaceAll: z.boolean().optional() },
|
|
20760
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
20761
|
+
}, async ({ path, oldString, newString, replaceAll }) => {
|
|
20762
|
+
const abs = await inTree(path);
|
|
20763
|
+
const st = await stat(abs);
|
|
20764
|
+
if (!st.isFile()) return text(`${path} is not a file`, true);
|
|
20765
|
+
if (st.size > MAX_WRITE_BYTES) return text(`${path} is too large to edit`, true);
|
|
20766
|
+
const before = await readFile(abs, "utf8");
|
|
20767
|
+
const count = before.split(oldString).length - 1;
|
|
20768
|
+
if (count === 0) return text(`oldString was not found in ${path}`, true);
|
|
20769
|
+
if (count > 1 && !replaceAll) return text(`oldString occurs ${count} times in ${path}; make it unique or set replaceAll`, true);
|
|
20770
|
+
const after = replaceAll ? before.split(oldString).join(newString) : before.replace(oldString, () => newString);
|
|
20771
|
+
await writeFile(abs, after);
|
|
20772
|
+
return text(`Edited ${await rel(abs)} (${replaceAll ? count : 1} replacement${(replaceAll ? count : 1) === 1 ? "" : "s"})`);
|
|
20773
|
+
});
|
|
20774
|
+
server.registerTool("create_directory", {
|
|
20775
|
+
title: "Create Directory",
|
|
20776
|
+
description: "Create a directory (and any missing parents) in the worktree.",
|
|
20777
|
+
inputSchema: { path: z.string() },
|
|
20778
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
20779
|
+
}, async ({ path }) => {
|
|
20780
|
+
const abs = await inTree(path);
|
|
20781
|
+
await mkdir(abs, { recursive: true });
|
|
20782
|
+
await inTree(path);
|
|
20783
|
+
return text(`Created ${await rel(abs)}`);
|
|
20784
|
+
});
|
|
20785
|
+
server.registerTool("delete_file", {
|
|
20786
|
+
title: "Delete File",
|
|
20787
|
+
description: "Delete one file (or symlink) from the worktree. Directories are not deleted.",
|
|
20788
|
+
inputSchema: { path: z.string() },
|
|
20789
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
20790
|
+
}, async ({ path }) => {
|
|
20791
|
+
const parent = await inTree(dirname23(path) === "" ? "." : dirname23(path));
|
|
20792
|
+
const abs = join45(parent, basename13(path));
|
|
20793
|
+
if (basename13(path) === ".git" || abs === parent) return text("path must name a file", true);
|
|
20794
|
+
const st = await lstat(abs).catch(() => void 0);
|
|
20795
|
+
if (!st) return text(`${path} does not exist`, true);
|
|
20796
|
+
if (st.isDirectory()) return text(`${path} is a directory; only files are deleted`, true);
|
|
20797
|
+
await rm(abs, { force: false });
|
|
20798
|
+
return text(`Deleted ${await rel(abs)}`);
|
|
20799
|
+
});
|
|
20800
|
+
server.registerTool("run_hook", {
|
|
20801
|
+
title: "Run Hook",
|
|
20802
|
+
description: `Run one of the repository's declared hooks in this worktree, with this periscope's environment, and return its output. Allowed names: ${PERISCOPE_HOOKS.join(", ")} (\`isolate\` only for a periscope opened isolated). A hook the repository does not declare is refused.`,
|
|
20803
|
+
inputSchema: { name: z.enum(PERISCOPE_HOOKS) },
|
|
20804
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
20805
|
+
}, async ({ name }) => {
|
|
20806
|
+
if (name === "isolate" && !sc.isolated) return text("isolate is allowed only for a periscope opened with --isolate", true);
|
|
20807
|
+
const env = await hookEnv(d, sc, name);
|
|
20808
|
+
const res = await d.runHook(sc, name, env);
|
|
20809
|
+
if (!res) return text(`${sc.repo} declares no \`${name}\` hook`, true);
|
|
20810
|
+
return text(`exit ${res.code}
|
|
20811
|
+
${tailText(res.output)}`, res.code !== 0);
|
|
20812
|
+
});
|
|
20813
|
+
const branchGuard = () => {
|
|
20814
|
+
const head = git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
20815
|
+
return head === sc.branch ? void 0 : `the worktree is on ${head}, not the ticket branch ${sc.branch}`;
|
|
20816
|
+
};
|
|
20817
|
+
server.registerTool("git_status", {
|
|
20818
|
+
title: "Git Status",
|
|
20819
|
+
description: "Branch and changed files (`git status --short --branch`) of this worktree.",
|
|
20820
|
+
inputSchema: {},
|
|
20821
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
20822
|
+
}, async () => text(git(root, ["status", "--short", "--branch", "--untracked-files=all"])));
|
|
20823
|
+
server.registerTool("git_diff", {
|
|
20824
|
+
title: "Git Diff",
|
|
20825
|
+
description: "The uncommitted changes against HEAD (staged and unstaged), optionally limited to one path. New untracked files appear in git_status, not here.",
|
|
20826
|
+
inputSchema: { path: z.string().optional() },
|
|
20827
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
20828
|
+
}, async ({ path }) => {
|
|
20829
|
+
const args = ["diff", "HEAD", "--no-ext-diff", "--no-color"];
|
|
20830
|
+
if (path) args.push("--", relative2(await realpath(root), await inTree(path)) || ".");
|
|
20831
|
+
const out = git(root, args);
|
|
20832
|
+
return text(out ? tailText(out, MAX_DIFF) : "(no changes)");
|
|
20833
|
+
});
|
|
20834
|
+
server.registerTool("git_commit", {
|
|
20835
|
+
title: "Git Commit",
|
|
20836
|
+
description: "Stage every change in the worktree and commit it on the ticket branch. It never pushes; ending the periscope with `--keep` does.",
|
|
20837
|
+
inputSchema: { message: z.string().min(1).max(2e3) },
|
|
20838
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
20839
|
+
}, async ({ message }) => {
|
|
20840
|
+
const wrong = branchGuard();
|
|
20841
|
+
if (wrong) return text(wrong, true);
|
|
20842
|
+
git(root, ["add", "-A"]);
|
|
20843
|
+
try {
|
|
20844
|
+
git(root, ["commit", "--no-verify", "-m", message]);
|
|
20845
|
+
} catch (e) {
|
|
20846
|
+
if (e instanceof GitError && /nothing to commit|no changes added/.test(e.message)) return text("Nothing to commit", true);
|
|
20847
|
+
throw e;
|
|
20848
|
+
}
|
|
20849
|
+
return text(`Committed ${git(root, ["rev-parse", "--short", "HEAD"])} on ${sc.branch}`);
|
|
20850
|
+
});
|
|
20851
|
+
return server;
|
|
20852
|
+
}
|
|
20853
|
+
var NOT_FOUND = { error: "not found" };
|
|
20854
|
+
var json = (res, status2, body) => {
|
|
20855
|
+
const b = JSON.stringify(body);
|
|
20856
|
+
res.writeHead(status2, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(b), "Cache-Control": "no-store" });
|
|
20857
|
+
res.end(b);
|
|
20858
|
+
};
|
|
20859
|
+
async function readBody(req) {
|
|
20860
|
+
const chunks = [];
|
|
20861
|
+
let n = 0;
|
|
20862
|
+
for await (const c of req) {
|
|
20863
|
+
n += c.length;
|
|
20864
|
+
if (n > MAX_BODY_BYTES2) return null;
|
|
20865
|
+
chunks.push(c);
|
|
20866
|
+
}
|
|
20867
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
20868
|
+
}
|
|
20869
|
+
async function handlePeriscopeMcp(d, req, res, segment) {
|
|
20870
|
+
const sc = d.find(segment);
|
|
20871
|
+
const secret = sc ? readPeriscopeSecret(d.stateDir, sc.ticketRecordId) : void 0;
|
|
20872
|
+
const presented = /^Bearer (.+)$/.exec(req.headers.authorization ?? "")?.[1]?.trim() ?? "";
|
|
20873
|
+
if (!sc || !secret || !presented || !secretsMatch(presented, secret)) return json(res, 404, NOT_FOUND);
|
|
20874
|
+
if (req.method !== "POST") {
|
|
20875
|
+
res.setHeader("Allow", "POST");
|
|
20876
|
+
return json(res, 405, { jsonrpc: "2.0", error: { code: -32e3, message: "Method not allowed." }, id: null });
|
|
20877
|
+
}
|
|
20878
|
+
const raw = await readBody(req);
|
|
20879
|
+
if (raw === null) return json(res, 413, { jsonrpc: "2.0", error: { code: -32600, message: "request body too large" }, id: null });
|
|
20880
|
+
let parsed;
|
|
20881
|
+
try {
|
|
20882
|
+
parsed = raw ? JSON.parse(raw) : void 0;
|
|
20883
|
+
} catch {
|
|
20884
|
+
return json(res, 400, { jsonrpc: "2.0", error: { code: -32700, message: "parse error" }, id: null });
|
|
20885
|
+
}
|
|
20886
|
+
const isCall = (m) => !!m && typeof m === "object" && m.method === "tools/call";
|
|
20887
|
+
if (Array.isArray(parsed) ? parsed.some(isCall) : isCall(parsed)) await d.touch(sc).catch((e) => d.log?.(`periscope ${sc.ticket} activity not recorded: ${e.message}`));
|
|
20888
|
+
try {
|
|
20889
|
+
const server = createPeriscopeMcpServer(d, sc);
|
|
20890
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
20891
|
+
await server.connect(transport);
|
|
20892
|
+
res.on("close", () => {
|
|
20893
|
+
void transport.close();
|
|
20894
|
+
void server.close();
|
|
20895
|
+
});
|
|
20896
|
+
await transport.handleRequest(req, res, parsed);
|
|
20897
|
+
} catch (e) {
|
|
20898
|
+
if (!res.headersSent) json(res, 500, { jsonrpc: "2.0", error: { code: -32603, message: e.message || "internal error" }, id: null });
|
|
20899
|
+
}
|
|
20900
|
+
}
|
|
20901
|
+
function findPeriscope(sidecars, segment) {
|
|
20902
|
+
const m = /^(?:([A-Za-z]+)-)?(\d+)$/.exec(segment);
|
|
20903
|
+
if (!m) return void 0;
|
|
20904
|
+
const num = String(Number(m[2]));
|
|
20905
|
+
return sidecars.find((sc) => {
|
|
20906
|
+
const t = /^(?:([A-Za-z]+)-)?0*(\d+)$/.exec(sc.ticket);
|
|
20907
|
+
if (!t || t[2] !== num) return false;
|
|
20908
|
+
return !m[1] || !t[1] || m[1].toLowerCase() === t[1].toLowerCase();
|
|
20909
|
+
});
|
|
20910
|
+
}
|
|
20911
|
+
|
|
20364
20912
|
// src/cli.ts
|
|
20365
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
20913
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname24(process.execPath) : resolve6(dirname24(fileURLToPath(import.meta.url)), "..");
|
|
20366
20914
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
20367
20915
|
var CYCLE_SECONDS2 = 120;
|
|
20368
20916
|
var STALE_CYCLES = 3;
|
|
@@ -20502,7 +21050,7 @@ No project here has what a route needs. Install the "${template.name}" library t
|
|
|
20502
21050
|
return installed.project.slug;
|
|
20503
21051
|
}
|
|
20504
21052
|
function usage(problem, requested = false) {
|
|
20505
|
-
const
|
|
21053
|
+
const text2 = `crew \u2014 a standing team of headless agents
|
|
20506
21054
|
|
|
20507
21055
|
crew poll [route] decide a cycle and report it; writes nothing
|
|
20508
21056
|
crew run [route] [--role R] run the winning role's session
|
|
@@ -20528,7 +21076,7 @@ function usage(problem, requested = false) {
|
|
|
20528
21076
|
crew reap [route] kill orphaned servers, drop worktrees for closed tickets
|
|
20529
21077
|
crew drop [route] NNN remove a merged ticket's worktree and branch
|
|
20530
21078
|
crew rotate-passenger-url [route] force a Host Passengers tunnel to reconnect with a fresh public URL
|
|
20531
|
-
crew periscope up [[route] REPO] [--topic TEXT|--issue NNN] [--isolate] [--host H] [--assignee ID] [--no-open] [--open] [--dry-run] [--json]
|
|
21079
|
+
crew periscope up [[route] REPO] [--topic TEXT|--issue NNN] [--isolate] [--serve-mcp] [--host H] [--assignee ID] [--no-open] [--open] [--dry-run] [--json]
|
|
20532
21080
|
crew periscope up --resume NNN restart the preview an idle periscope's server was stopped for
|
|
20533
21081
|
file a draft ticket, cut and prepare its worktree, start the repo's preview hook and record a Periscopes row (alias: start)
|
|
20534
21082
|
crew periscope down [[route] NNN] --keep|--discard [--allow-empty] [--json]
|
|
@@ -20581,12 +21129,12 @@ Options:
|
|
|
20581
21129
|
In watch: space pauses the stream, q quits.
|
|
20582
21130
|
`;
|
|
20583
21131
|
if (requested) {
|
|
20584
|
-
process.stdout.write(
|
|
21132
|
+
process.stdout.write(text2);
|
|
20585
21133
|
process.exit(0);
|
|
20586
21134
|
}
|
|
20587
21135
|
process.stderr.write(problem ? `crew: ${problem}
|
|
20588
21136
|
|
|
20589
|
-
${
|
|
21137
|
+
${text2}` : text2);
|
|
20590
21138
|
process.exit(2);
|
|
20591
21139
|
}
|
|
20592
21140
|
var argv = process.argv.slice(2);
|
|
@@ -20759,6 +21307,44 @@ if (command === "passenger-sync-daemon") {
|
|
|
20759
21307
|
return void 0;
|
|
20760
21308
|
}
|
|
20761
21309
|
};
|
|
21310
|
+
}, periscopeEndpointDeps = function() {
|
|
21311
|
+
return {
|
|
21312
|
+
stateDir: cfg.ship.stateDir,
|
|
21313
|
+
shipName: cfg.ship.name,
|
|
21314
|
+
apiFor: endpointApiFor(cfg.routes.filter((r) => r.enabled)),
|
|
21315
|
+
warn: (m) => emit.warn(m, { step: "passengers" })
|
|
21316
|
+
};
|
|
21317
|
+
}, periscopeMcpDeps = function() {
|
|
21318
|
+
const stateDir = cfg.ship.stateDir;
|
|
21319
|
+
return {
|
|
21320
|
+
stateDir,
|
|
21321
|
+
find: (segment) => findPeriscope(readPeriscopeSidecars(stateDir), segment),
|
|
21322
|
+
runHook: async (sc, name, env) => {
|
|
21323
|
+
const rt = cfg.routes.find((r) => r.route === sc.route && r.enabled);
|
|
21324
|
+
const repo = rt && (await resolvedRepos(rt)).find((r) => r.name === sc.repo);
|
|
21325
|
+
const script = repo?.config.hooks[name];
|
|
21326
|
+
if (!repo || !script) return null;
|
|
21327
|
+
return runScript(script, {
|
|
21328
|
+
cwd: sc.worktree,
|
|
21329
|
+
env,
|
|
21330
|
+
shell: resolveShell(repo.config.shell ?? cfg.ship.shell),
|
|
21331
|
+
timeoutMs: name === "preview" ? 18e4 : 15 * 6e4
|
|
21332
|
+
});
|
|
21333
|
+
},
|
|
21334
|
+
touch: async (sc) => {
|
|
21335
|
+
const now = Date.now();
|
|
21336
|
+
if (now - (periscopeTouched.get(sc.ticketRecordId) ?? 0) < PERISCOPE_TOUCH_EVERY_MS) return;
|
|
21337
|
+
periscopeTouched.set(sc.ticketRecordId, now);
|
|
21338
|
+
const rt = cfg.routes.find((r) => r.route === sc.route && r.enabled);
|
|
21339
|
+
if (!rt) return;
|
|
21340
|
+
const { idleMinutes } = periscopeCaps(cfg.ship);
|
|
21341
|
+
const at = new Date(now).toISOString();
|
|
21342
|
+
const expiresAt = new Date(now + idleMinutes * 6e4).toISOString();
|
|
21343
|
+
await new Tracker(rt, cfg.ship).updatePeriscope(sc.periscopeRecordId, { last_activity_at: at, expires_at: expiresAt });
|
|
21344
|
+
writePeriscopeSidecar(stateDir, { ...sc, lastActivityAt: at, expiresAt });
|
|
21345
|
+
},
|
|
21346
|
+
log: (m) => emit.warn(m, { step: "passengers" })
|
|
21347
|
+
};
|
|
20762
21348
|
}, endpointReconcileDeps = function(routes, relayHost) {
|
|
20763
21349
|
return {
|
|
20764
21350
|
stateDir: cfg.ship.stateDir,
|
|
@@ -20785,7 +21371,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20785
21371
|
const unsafe = baseBranchUnsafe(out);
|
|
20786
21372
|
return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
|
|
20787
21373
|
};
|
|
20788
|
-
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth;
|
|
21374
|
+
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, periscopeEndpointDeps2 = periscopeEndpointDeps, periscopeMcpDeps2 = periscopeMcpDeps, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth;
|
|
20789
21375
|
let cfg;
|
|
20790
21376
|
let firstRun;
|
|
20791
21377
|
let firstRunFromFlags = false;
|
|
@@ -20841,7 +21427,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20841
21427
|
} else {
|
|
20842
21428
|
const toleratesNoRoutes = command === "credential" || command === "forge" || command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
20843
21429
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
20844
|
-
if (configless && !
|
|
21430
|
+
if (configless && !existsSync43(findConfigFile(CREW_HOME))) {
|
|
20845
21431
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
20846
21432
|
} else {
|
|
20847
21433
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -20856,7 +21442,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20856
21442
|
throw e;
|
|
20857
21443
|
}
|
|
20858
21444
|
if (command === "credential") {
|
|
20859
|
-
const r = runCredential(positional[1],
|
|
21445
|
+
const r = runCredential(positional[1], readFileSync38(0, "utf8"), cfg.ship.forges, cfg.ship.stateDir);
|
|
20860
21446
|
process.stdout.write(r.out);
|
|
20861
21447
|
process.stderr.write(r.err);
|
|
20862
21448
|
process.exit(r.code);
|
|
@@ -20868,9 +21454,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
20868
21454
|
forges: cfg.ship.forges,
|
|
20869
21455
|
stateDir: cfg.ship.stateDir,
|
|
20870
21456
|
configFile: cfg.configFile,
|
|
20871
|
-
readFile: (p) =>
|
|
20872
|
-
writeFile: (p, t) =>
|
|
20873
|
-
stdin: () =>
|
|
21457
|
+
readFile: (p) => readFileSync38(p, "utf8"),
|
|
21458
|
+
writeFile: (p, t) => writeFileSync30(p, t),
|
|
21459
|
+
stdin: () => readFileSync38(0, "utf8"),
|
|
20874
21460
|
env: process.env
|
|
20875
21461
|
}
|
|
20876
21462
|
);
|
|
@@ -21002,6 +21588,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21002
21588
|
for (const plan of containerPlans) pending.push(reconcileEndpoint(plan.workspaceId, reconcile));
|
|
21003
21589
|
for (const plan of plans) pending.push(reconcileRelayUrl(plan.workspaceId, apiRoutes));
|
|
21004
21590
|
await Promise.all(pending);
|
|
21591
|
+
await reconcilePeriscopeEndpoints();
|
|
21005
21592
|
} catch (e) {
|
|
21006
21593
|
emit.warn(`ship tunnel sync failed: ${e.message}`, { step: "passengers" });
|
|
21007
21594
|
}
|
|
@@ -21021,6 +21608,22 @@ if (command === "passenger-sync-daemon") {
|
|
|
21021
21608
|
emit.warn(`could not write relay_url: ${e.message}`, { step: "passengers" });
|
|
21022
21609
|
}
|
|
21023
21610
|
}
|
|
21611
|
+
const periscopeTouched = /* @__PURE__ */ new Map();
|
|
21612
|
+
const PERISCOPE_TOUCH_EVERY_MS = 3e4;
|
|
21613
|
+
async function reconcilePeriscopeEndpoints() {
|
|
21614
|
+
const d = periscopeEndpointDeps();
|
|
21615
|
+
await removeEndedPeriscopeEndpoints(d);
|
|
21616
|
+
const have = new Set(listPeriscopeEndpoints(cfg.ship.stateDir).map((e) => e.ticketRecordId));
|
|
21617
|
+
for (const sc of readPeriscopeSidecars(cfg.ship.stateDir)) {
|
|
21618
|
+
if (have.has(sc.ticketRecordId) || !sc.route || !readPeriscopeSecret(cfg.ship.stateDir, sc.ticketRecordId)) continue;
|
|
21619
|
+
const rt = cfg.routes.find((r) => r.route === sc.route && r.enabled);
|
|
21620
|
+
const ws = rt?.resolved?.workspaceId;
|
|
21621
|
+
const relayUrl = rt && routeRelayUrl(rt);
|
|
21622
|
+
if (!rt || !ws || !relayUrl) continue;
|
|
21623
|
+
const out = await registerPeriscopeEndpoint(d, { ticket: sc.ticket, ticketRecordId: sc.ticketRecordId, route: sc.route, workspaceId: ws, relayUrl });
|
|
21624
|
+
if (!out.ok) d.warn(`could not register the MCP endpoint of ${sc.ticket}: ${out.message}`);
|
|
21625
|
+
}
|
|
21626
|
+
}
|
|
21024
21627
|
async function releaseFleet(opts = {}) {
|
|
21025
21628
|
if (releaseRefused()) return;
|
|
21026
21629
|
precheckTickets.clear();
|
|
@@ -21136,8 +21739,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
21136
21739
|
}
|
|
21137
21740
|
if (host === "linux" && hasSystemd) {
|
|
21138
21741
|
const label = labelFor(CREW_HOME, "run");
|
|
21139
|
-
const staleTimer =
|
|
21140
|
-
if (
|
|
21742
|
+
const staleTimer = join46(homedir10(), ".config", "systemd", "user", `${label}.timer`);
|
|
21743
|
+
if (existsSync43(staleTimer)) {
|
|
21141
21744
|
process.stdout.write(
|
|
21142
21745
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
21143
21746
|
`
|
|
@@ -21164,7 +21767,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21164
21767
|
const tracker = new Tracker(c, cfg.ship);
|
|
21165
21768
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
21166
21769
|
for (const r of await resolvedRepos(c)) {
|
|
21167
|
-
if (!
|
|
21770
|
+
if (!existsSync43(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
21168
21771
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
21169
21772
|
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
21170
21773
|
}
|
|
@@ -21182,7 +21785,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21182
21785
|
}
|
|
21183
21786
|
async function doctorFix(o) {
|
|
21184
21787
|
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))));
|
|
21185
|
-
const unitExists = (p) =>
|
|
21788
|
+
const unitExists = (p) => existsSync43(p);
|
|
21186
21789
|
const crontab = o.host === "linux" ? spawnSync8("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
21187
21790
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run", installContext()), unitExists, crontab) : true;
|
|
21188
21791
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -21199,7 +21802,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21199
21802
|
for (const a of plan.actions) {
|
|
21200
21803
|
if (a.kind === "enable") {
|
|
21201
21804
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
21202
|
-
|
|
21805
|
+
writeFileSync30(cfg.configFile, planEnable(readFileSync38(cfg.configFile, "utf8"), a.route));
|
|
21203
21806
|
enabledSound.add(a.route);
|
|
21204
21807
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
21205
21808
|
} else {
|
|
@@ -21878,6 +22481,7 @@ ${cu.tail.trimEnd()}
|
|
|
21878
22481
|
if (seat) {
|
|
21879
22482
|
const candidates2 = rankedCandidates(w.role, w.decision.selectionInput);
|
|
21880
22483
|
const contract = w.decision.selectionInput.contract ?? DEFAULT_CONTRACT;
|
|
22484
|
+
await refreshUnknownRepos({ route: w.route, stateDir: cfg.ship.stateDir, fetchRows: () => fleetTracker.repoRowsForMap(), tickets: candidates2 });
|
|
21881
22485
|
const result = await resolveTopCandidate(
|
|
21882
22486
|
fleetTracker,
|
|
21883
22487
|
candidates2,
|
|
@@ -22089,6 +22693,7 @@ ${cu.tail.trimEnd()}
|
|
|
22089
22693
|
if (seat) {
|
|
22090
22694
|
const candidates2 = rankedCandidates(current, decision.selectionInput);
|
|
22091
22695
|
const contract = decision.selectionInput.contract ?? DEFAULT_CONTRACT;
|
|
22696
|
+
await refreshUnknownRepos({ route: route2, stateDir: cfg.ship.stateDir, fetchRows: () => tracker2.repoRowsForMap(), tickets: candidates2 });
|
|
22092
22697
|
const result = await resolveTopCandidate(
|
|
22093
22698
|
tracker2,
|
|
22094
22699
|
candidates2,
|
|
@@ -22320,7 +22925,7 @@ ${cu.tail.trimEnd()}
|
|
|
22320
22925
|
process.exit(1);
|
|
22321
22926
|
}
|
|
22322
22927
|
requireArmed("run the daemon loop");
|
|
22323
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
22928
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join46(CREW_HOME, "src"), join46(CREW_HOME, "dist"), join46(CREW_HOME, "bin")];
|
|
22324
22929
|
const configPaths = daemonConfigPaths(cfg);
|
|
22325
22930
|
watchPaths.push(...configPaths);
|
|
22326
22931
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -22348,8 +22953,22 @@ ${cu.tail.trimEnd()}
|
|
|
22348
22953
|
previewHost: cfg.ship.previewHost,
|
|
22349
22954
|
maxPeriscopes: cfg.ship.maxPeriscopes,
|
|
22350
22955
|
run: runCrew,
|
|
22351
|
-
verifyIn: (token, ws) => controlAuth.verifyIn(token, ws)
|
|
22956
|
+
verifyIn: (token, ws) => controlAuth.verifyIn(token, ws),
|
|
22957
|
+
mcp: {
|
|
22958
|
+
onUp: async (r, done) => {
|
|
22959
|
+
const id = typeof done.ticketRecordId === "string" ? done.ticketRecordId : "";
|
|
22960
|
+
const ticket = typeof done.ticket === "string" ? done.ticket : "";
|
|
22961
|
+
const relayUrl = routeRelayUrl(cfg.routes.find((x) => x.route === r.route));
|
|
22962
|
+
if (!relayUrl) return { mcpEndpointId: null, mcpWarning: "this ship has no relay URL yet, so the periscope's MCP endpoint is not registered; it is retried each cycle" };
|
|
22963
|
+
const out = await registerPeriscopeEndpoint(periscopeEndpointDeps(), { ticket, ticketRecordId: id, route: r.route, workspaceId: r.workspaceId, relayUrl });
|
|
22964
|
+
return out.ok ? { mcpEndpointId: out.endpointId, mcpUrl: out.url } : { mcpEndpointId: null, mcpWarning: out.message };
|
|
22965
|
+
},
|
|
22966
|
+
onDown: async () => {
|
|
22967
|
+
await removeEndedPeriscopeEndpoints(periscopeEndpointDeps());
|
|
22968
|
+
}
|
|
22969
|
+
}
|
|
22352
22970
|
},
|
|
22971
|
+
periscopeMcp: controlRoutes.some((r) => r.enabled && r.hostPeriscopes) ? (req, res, ticket) => handlePeriscopeMcp(periscopeMcpDeps(), req, res, ticket) : void 0,
|
|
22353
22972
|
status: () => crewJson(["status", route2.route, "--json"]),
|
|
22354
22973
|
doctor: () => crewJson(["doctor", route2.route, "--json", "--offline"]),
|
|
22355
22974
|
scheduler: schedulerHandler(planDaemonControl(CREW_HOME, hostPlatform(), detectSystemd())),
|
|
@@ -22581,10 +23200,10 @@ ${cu.tail.trimEnd()}
|
|
|
22581
23200
|
`);
|
|
22582
23201
|
if (!dryRun) {
|
|
22583
23202
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
22584
|
-
const raw =
|
|
23203
|
+
const raw = existsSync43(resolvedPath) ? JSON.parse(readFileSync38(resolvedPath, "utf8")) : {};
|
|
22585
23204
|
raw.agentSkills = result.agentSkills;
|
|
22586
|
-
|
|
22587
|
-
|
|
23205
|
+
mkdirSync28(dirOf2(resolvedPath), { recursive: true });
|
|
23206
|
+
writeFileSync30(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
22588
23207
|
`);
|
|
22589
23208
|
}
|
|
22590
23209
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -22607,7 +23226,7 @@ ${cu.tail.trimEnd()}
|
|
|
22607
23226
|
throw e;
|
|
22608
23227
|
}
|
|
22609
23228
|
const oldName = cfg.ship.name;
|
|
22610
|
-
|
|
23229
|
+
writeFileSync30(cfg.configFile, setShipNameInYaml(readFileSync38(cfg.configFile, "utf8"), newName));
|
|
22611
23230
|
cfg.ship.name = newName;
|
|
22612
23231
|
let renameMachineId;
|
|
22613
23232
|
try {
|
|
@@ -22654,14 +23273,14 @@ ${cu.tail.trimEnd()}
|
|
|
22654
23273
|
process.exit(2);
|
|
22655
23274
|
};
|
|
22656
23275
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
|
|
22657
|
-
const configText =
|
|
23276
|
+
const configText = readFileSync38(cfg.configFile, "utf8");
|
|
22658
23277
|
if (sub === "check") {
|
|
22659
23278
|
try {
|
|
22660
23279
|
const routeArg = value("route") ?? positional[2];
|
|
22661
23280
|
const found = cfg.routes.find((r) => r.route === routeArg);
|
|
22662
23281
|
if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
|
|
22663
23282
|
const named2 = value("route") ? void 0 : positional[3];
|
|
22664
|
-
const dir = named2 ? repoDirOf(configText,
|
|
23283
|
+
const dir = named2 ? repoDirOf(configText, dirname24(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
|
|
22665
23284
|
const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
|
|
22666
23285
|
const o = target ? found.repoOverrides[target.name] : void 0;
|
|
22667
23286
|
runReposCheck(dir, {
|
|
@@ -22684,17 +23303,17 @@ ${cu.tail.trimEnd()}
|
|
|
22684
23303
|
if (sub === "hooks") {
|
|
22685
23304
|
try {
|
|
22686
23305
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
22687
|
-
const dir = repoDirOf(configText,
|
|
22688
|
-
if (!
|
|
22689
|
-
const files = { exists: (r) =>
|
|
23306
|
+
const dir = repoDirOf(configText, dirname24(cfg.configFile), a.route, a.name);
|
|
23307
|
+
if (!existsSync43(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
23308
|
+
const files = { exists: (r) => existsSync43(resolvePath(dir, r)), list: (r) => {
|
|
22690
23309
|
try {
|
|
22691
|
-
return
|
|
23310
|
+
return readdirSync19(resolvePath(dir, r));
|
|
22692
23311
|
} catch {
|
|
22693
23312
|
return [];
|
|
22694
23313
|
}
|
|
22695
23314
|
}, read: (r) => {
|
|
22696
23315
|
try {
|
|
22697
|
-
return
|
|
23316
|
+
return readFileSync38(resolvePath(dir, r), "utf8");
|
|
22698
23317
|
} catch {
|
|
22699
23318
|
return void 0;
|
|
22700
23319
|
}
|
|
@@ -22736,19 +23355,19 @@ ${cu.tail.trimEnd()}
|
|
|
22736
23355
|
let cleared;
|
|
22737
23356
|
if (clear2) {
|
|
22738
23357
|
const plan = clearRouteHooks(configText, a.route, a.name);
|
|
22739
|
-
if (plan.removed.length > 0)
|
|
23358
|
+
if (plan.removed.length > 0) writeFileSync30(cfg.configFile, plan.text);
|
|
22740
23359
|
cleared = { file: cfg.configFile, removed: plan.removed };
|
|
22741
23360
|
}
|
|
22742
23361
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
22743
23362
|
if (write === "route") {
|
|
22744
23363
|
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
|
|
22745
|
-
|
|
23364
|
+
writeFileSync30(cfg.configFile, plan.text);
|
|
22746
23365
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
|
|
22747
23366
|
} else if (write === "repo") {
|
|
22748
23367
|
const have = existingRepoConfig(files);
|
|
22749
23368
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
22750
23369
|
const file = resolvePath(dir, ".crew.yaml");
|
|
22751
|
-
|
|
23370
|
+
writeFileSync30(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
22752
23371
|
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
|
|
22753
23372
|
}
|
|
22754
23373
|
if (flag("json")) {
|
|
@@ -22804,7 +23423,7 @@ ${cu.tail.trimEnd()}
|
|
|
22804
23423
|
if (sub === "list" && !routeName) usage2();
|
|
22805
23424
|
if (sub === "add") {
|
|
22806
23425
|
try {
|
|
22807
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
23426
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync43(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
|
|
22808
23427
|
routeName = a.route;
|
|
22809
23428
|
addPath = a.path;
|
|
22810
23429
|
addName = a.name;
|
|
@@ -22832,67 +23451,77 @@ ${cu.tail.trimEnd()}
|
|
|
22832
23451
|
}
|
|
22833
23452
|
}
|
|
22834
23453
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
23454
|
+
if (sub === "list" || sub === "add") {
|
|
23455
|
+
const liveRoute = cfg.routes.find((r) => r.route === routeName);
|
|
23456
|
+
if (liveRoute?.resolved?.reposModelId && existsSync43(resolvedFile) && !dryRun) {
|
|
23457
|
+
try {
|
|
23458
|
+
await hydrateApiKeys([liveRoute]);
|
|
23459
|
+
persistRepoRows(cfg.ship.stateDir, liveRoute.route, await new Tracker(liveRoute, cfg.ship).repoRowsForMap());
|
|
23460
|
+
} catch {
|
|
23461
|
+
}
|
|
23462
|
+
}
|
|
23463
|
+
}
|
|
22835
23464
|
let resolvedRemotes;
|
|
22836
|
-
if (
|
|
23465
|
+
if (existsSync43(resolvedFile)) {
|
|
22837
23466
|
try {
|
|
22838
|
-
resolvedRemotes = JSON.parse(
|
|
23467
|
+
resolvedRemotes = JSON.parse(readFileSync38(resolvedFile, "utf8"));
|
|
22839
23468
|
} catch {
|
|
22840
23469
|
}
|
|
22841
23470
|
}
|
|
22842
23471
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
22843
|
-
const
|
|
22844
|
-
const base =
|
|
22845
|
-
const isCheckout = (d) =>
|
|
23472
|
+
const text2 = configText;
|
|
23473
|
+
const base = dirname24(cfg.configFile);
|
|
23474
|
+
const isCheckout = (d) => existsSync43(`${d}/.git`);
|
|
22846
23475
|
try {
|
|
22847
23476
|
if (sub === "list") {
|
|
22848
23477
|
if (flag("json")) {
|
|
22849
23478
|
const rows = listRepoRows({
|
|
22850
|
-
text,
|
|
23479
|
+
text: text2,
|
|
22851
23480
|
route: routeName,
|
|
22852
23481
|
base,
|
|
22853
23482
|
resolved: resolvedRemotes,
|
|
22854
23483
|
originOf,
|
|
22855
|
-
exists: (d) =>
|
|
23484
|
+
exists: (d) => existsSync43(d),
|
|
22856
23485
|
isCheckout,
|
|
22857
23486
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
22858
23487
|
try {
|
|
22859
|
-
return Object.keys(parseRepoConfig(
|
|
23488
|
+
return Object.keys(parseRepoConfig(readFileSync38(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
22860
23489
|
} catch {
|
|
22861
23490
|
return false;
|
|
22862
23491
|
}
|
|
22863
23492
|
})
|
|
22864
23493
|
});
|
|
22865
|
-
process.stdout.write(`${JSON.stringify({ route: routeName, repos: rows, cloneBase: cloneBaseOf(
|
|
23494
|
+
process.stdout.write(`${JSON.stringify({ route: routeName, repos: rows, cloneBase: cloneBaseOf(text2, routeName, base, isCheckout) })}
|
|
22866
23495
|
`);
|
|
22867
23496
|
break;
|
|
22868
23497
|
}
|
|
22869
|
-
const cloneBase = cloneBaseOf(
|
|
22870
|
-
const repos = configuredRepos(
|
|
23498
|
+
const cloneBase = cloneBaseOf(text2, routeName, base, isCheckout);
|
|
23499
|
+
const repos = configuredRepos(text2, routeName, base).filter((r) => r.dir !== cloneBase);
|
|
22871
23500
|
if (repos.length === 0) process.stdout.write(`route ${routeName} has no checkout configured \u2014 \`crew repos add ${routeName} <path>\`
|
|
22872
23501
|
`);
|
|
22873
23502
|
for (const r of repos) {
|
|
22874
23503
|
const origin2 = originOf(r.dir);
|
|
22875
23504
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
22876
|
-
const note = !
|
|
23505
|
+
const note = !existsSync43(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
22877
23506
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
22878
23507
|
`);
|
|
22879
23508
|
}
|
|
22880
23509
|
break;
|
|
22881
23510
|
}
|
|
22882
23511
|
if (sub === "clone") {
|
|
22883
|
-
const
|
|
22884
|
-
const say = (event, extra, human) => process.stdout.write(
|
|
23512
|
+
const json2 = flag("json");
|
|
23513
|
+
const say = (event, extra, human) => process.stdout.write(json2 ? `${JSON.stringify({ event, ...extra })}
|
|
22885
23514
|
` : `${human}
|
|
22886
23515
|
`);
|
|
22887
|
-
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
22888
|
-
if (
|
|
23516
|
+
const plan2 = planRepoClone({ text: text2, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
23517
|
+
if (existsSync43(plan2.target) && (existsSync43(`${plan2.target}/.git`) || readdirSync19(plan2.target).length > 0)) {
|
|
22889
23518
|
throw new ReposError(
|
|
22890
|
-
|
|
23519
|
+
existsSync43(`${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`
|
|
22891
23520
|
);
|
|
22892
23521
|
}
|
|
22893
23522
|
if (dryRun) {
|
|
22894
23523
|
say("dry-run", { remote: plan2.remote, target: plan2.target }, `would clone ${plan2.remote} into ${plan2.target}`);
|
|
22895
|
-
if (!
|
|
23524
|
+
if (!json2) process.stdout.write(`${plan2.block}
|
|
22896
23525
|
`);
|
|
22897
23526
|
break;
|
|
22898
23527
|
}
|
|
@@ -22905,18 +23534,18 @@ ${cu.tail.trimEnd()}
|
|
|
22905
23534
|
throw new ReposError(`${e.message}${authHint}
|
|
22906
23535
|
crew.yaml was not changed`);
|
|
22907
23536
|
}
|
|
22908
|
-
|
|
23537
|
+
writeFileSync30(cfg.configFile, plan2.text);
|
|
22909
23538
|
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:
|
|
22910
23539
|
`);
|
|
22911
23540
|
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}`);
|
|
22912
23541
|
break;
|
|
22913
23542
|
}
|
|
22914
23543
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
|
|
22915
|
-
const top =
|
|
23544
|
+
const top = existsSync43(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
22916
23545
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
22917
23546
|
const origin = originOf(top);
|
|
22918
23547
|
const plan = planRepoAdd({
|
|
22919
|
-
text,
|
|
23548
|
+
text: text2,
|
|
22920
23549
|
base,
|
|
22921
23550
|
route: routeName,
|
|
22922
23551
|
path: top,
|
|
@@ -22964,15 +23593,15 @@ ${cu.tail.trimEnd()}
|
|
|
22964
23593
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
22965
23594
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
22966
23595
|
`);
|
|
22967
|
-
if (
|
|
22968
|
-
const raw = JSON.parse(
|
|
23596
|
+
if (existsSync43(resolvedFile)) {
|
|
23597
|
+
const raw = JSON.parse(readFileSync38(resolvedFile, "utf8"));
|
|
22969
23598
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
22970
23599
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
22971
|
-
|
|
23600
|
+
writeFileSync30(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
22972
23601
|
`);
|
|
22973
23602
|
}
|
|
22974
23603
|
}
|
|
22975
|
-
|
|
23604
|
+
writeFileSync30(cfg.configFile, plan.text);
|
|
22976
23605
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
22977
23606
|
`);
|
|
22978
23607
|
} catch (e) {
|
|
@@ -23339,7 +23968,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23339
23968
|
process.exit(2);
|
|
23340
23969
|
}
|
|
23341
23970
|
if (found.shipNameAdopted && !dryRun) {
|
|
23342
|
-
|
|
23971
|
+
writeFileSync30(cfg.configFile, setShipNameInYaml(readFileSync38(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
23343
23972
|
cfg.ship.name = found.shipNameAdopted;
|
|
23344
23973
|
}
|
|
23345
23974
|
if (found.shipRow) {
|
|
@@ -23382,10 +24011,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23382
24011
|
}
|
|
23383
24012
|
}
|
|
23384
24013
|
let previous;
|
|
23385
|
-
const hadPreviousFile =
|
|
24014
|
+
const hadPreviousFile = existsSync43(resolvedPath);
|
|
23386
24015
|
if (hadPreviousFile) {
|
|
23387
24016
|
try {
|
|
23388
|
-
previous = JSON.parse(
|
|
24017
|
+
previous = JSON.parse(readFileSync38(resolvedPath, "utf8"));
|
|
23389
24018
|
} catch {
|
|
23390
24019
|
}
|
|
23391
24020
|
}
|
|
@@ -23515,10 +24144,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23515
24144
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
23516
24145
|
`);
|
|
23517
24146
|
} else {
|
|
23518
|
-
|
|
24147
|
+
mkdirSync28(dirname24(resolvedPath), { recursive: true });
|
|
23519
24148
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
23520
24149
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
23521
|
-
|
|
24150
|
+
writeFileSync30(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
23522
24151
|
`);
|
|
23523
24152
|
renameSync4(tmpPath, resolvedPath);
|
|
23524
24153
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -23564,8 +24193,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23564
24193
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
23565
24194
|
`);
|
|
23566
24195
|
} else {
|
|
23567
|
-
|
|
23568
|
-
|
|
24196
|
+
mkdirSync28(dirname24(apiKeyFile), { recursive: true });
|
|
24197
|
+
writeFileSync30(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
23569
24198
|
`, { mode: 384 });
|
|
23570
24199
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
23571
24200
|
`);
|
|
@@ -24252,6 +24881,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
24252
24881
|
},
|
|
24253
24882
|
setRowState: (id, state2) => trD.updatePeriscope(id, { state: state2 }),
|
|
24254
24883
|
forgetLocal: (id) => {
|
|
24884
|
+
removePeriscopeSecret(cfg.ship.stateDir, id);
|
|
24255
24885
|
removePeriscopeSidecar(cfg.ship.stateDir, id);
|
|
24256
24886
|
removePeriscopePrior(cfg.ship.stateDir, id);
|
|
24257
24887
|
},
|
|
@@ -24300,7 +24930,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
24300
24930
|
const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
|
|
24301
24931
|
const resumed = await runPeriscopeResume({
|
|
24302
24932
|
find: async () => hit && { row: hit.row, tag: hit.tag },
|
|
24303
|
-
worktreeExists: (p) =>
|
|
24933
|
+
worktreeExists: (p) => existsSync43(p),
|
|
24304
24934
|
caps: periscopeCaps(cfg.ship),
|
|
24305
24935
|
runHook: async (name, cwd, env) => {
|
|
24306
24936
|
const script = hit?.repo.config.hooks[name];
|
|
@@ -24394,10 +25024,10 @@ ${where.join("\n")}`);
|
|
|
24394
25024
|
},
|
|
24395
25025
|
prepare: async (path) => {
|
|
24396
25026
|
for (const f of repoP.config.worktrees.copy) {
|
|
24397
|
-
const src =
|
|
24398
|
-
if (!
|
|
24399
|
-
|
|
24400
|
-
copyFileSync2(src,
|
|
25027
|
+
const src = join46(repoP.dir, f);
|
|
25028
|
+
if (!existsSync43(src)) continue;
|
|
25029
|
+
mkdirSync28(dirOf2(join46(path, f)), { recursive: true });
|
|
25030
|
+
copyFileSync2(src, join46(path, f));
|
|
24401
25031
|
}
|
|
24402
25032
|
const setup = repoP.config.hooks.setup;
|
|
24403
25033
|
return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
|
|
@@ -24409,7 +25039,10 @@ ${where.join("\n")}`);
|
|
|
24409
25039
|
return script ? runScript(script, { cwd, env, shell: shellP, timeoutMs: name === "preview" ? 18e4 : void 0 }) : null;
|
|
24410
25040
|
},
|
|
24411
25041
|
createRow: (fields) => trackerP.createPeriscope(fields),
|
|
24412
|
-
recordLocal: (sc) =>
|
|
25042
|
+
recordLocal: (sc) => {
|
|
25043
|
+
writePeriscopeSidecar(cfg.ship.stateDir, { ...sc, route: routeP.route });
|
|
25044
|
+
if (flag("serve-mcp")) mintPeriscopeSecret(cfg.ship.stateDir, sc.ticketRecordId);
|
|
25045
|
+
},
|
|
24413
25046
|
comment: (ticketId, body) => trackerP.postComment(ticketId, body, memberId),
|
|
24414
25047
|
teardown: async (path, branch) => {
|
|
24415
25048
|
const n = Number(path.match(/(\d+)\/?$/)?.[1]);
|
|
@@ -24687,7 +25320,7 @@ ${where.join("\n")}`);
|
|
|
24687
25320
|
};
|
|
24688
25321
|
const droppedRoutes = [];
|
|
24689
25322
|
try {
|
|
24690
|
-
const rawCfg = parseYaml2(
|
|
25323
|
+
const rawCfg = parseYaml2(readFileSync38(cfg.configFile, "utf8"));
|
|
24691
25324
|
for (const r of rawCfg?.routes ?? []) {
|
|
24692
25325
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
24693
25326
|
try {
|
|
@@ -24729,8 +25362,8 @@ ${where.join("\n")}`);
|
|
|
24729
25362
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
24730
25363
|
break;
|
|
24731
25364
|
}
|
|
24732
|
-
|
|
24733
|
-
|
|
25365
|
+
mkdirSync28(cfg.ship.stateDir, { recursive: true });
|
|
25366
|
+
writeFileSync30(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
24734
25367
|
`);
|
|
24735
25368
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
24736
25369
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -24781,8 +25414,8 @@ ${where.join("\n")}`);
|
|
|
24781
25414
|
}
|
|
24782
25415
|
case "log": {
|
|
24783
25416
|
try {
|
|
24784
|
-
const
|
|
24785
|
-
process.stdout.write(`${
|
|
25417
|
+
const text2 = readFileSync38(cfg.ship.logFile, "utf8").split("\n");
|
|
25418
|
+
process.stdout.write(`${text2.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
24786
25419
|
`);
|
|
24787
25420
|
} catch {
|
|
24788
25421
|
process.stdout.write(`no log yet at ${cfg.ship.logFile}
|
|
@@ -24791,12 +25424,12 @@ ${where.join("\n")}`);
|
|
|
24791
25424
|
break;
|
|
24792
25425
|
}
|
|
24793
25426
|
case "sample-ticket": {
|
|
24794
|
-
const
|
|
24795
|
-
const say = (event, extra, human) => process.stdout.write(
|
|
25427
|
+
const json2 = flag("json");
|
|
25428
|
+
const say = (event, extra, human) => process.stdout.write(json2 ? `${JSON.stringify({ event, ...extra })}
|
|
24796
25429
|
` : `${human}
|
|
24797
25430
|
`);
|
|
24798
25431
|
const fail = (msg) => {
|
|
24799
|
-
if (
|
|
25432
|
+
if (json2) process.stdout.write(`${JSON.stringify({ event: "error", message: msg })}
|
|
24800
25433
|
`);
|
|
24801
25434
|
else process.stderr.write(`crew sample-ticket: ${msg}
|
|
24802
25435
|
`);
|
|
@@ -24804,8 +25437,8 @@ ${where.join("\n")}`);
|
|
|
24804
25437
|
};
|
|
24805
25438
|
if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
|
|
24806
25439
|
const tracker = new Tracker(route2, cfg.ship);
|
|
24807
|
-
const configText =
|
|
24808
|
-
const cfgBase =
|
|
25440
|
+
const configText = readFileSync38(cfg.configFile, "utf8");
|
|
25441
|
+
const cfgBase = dirname24(cfg.configFile);
|
|
24809
25442
|
const configured2 = configuredRepos(configText, route2.route, cfgBase);
|
|
24810
25443
|
const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
|
|
24811
25444
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
|
|
@@ -24833,7 +25466,7 @@ ${where.join("\n")}`);
|
|
|
24833
25466
|
}
|
|
24834
25467
|
if (configured2.some((r) => r.dir === dir)) {
|
|
24835
25468
|
try {
|
|
24836
|
-
|
|
25469
|
+
writeFileSync30(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
|
|
24837
25470
|
} catch (e) {
|
|
24838
25471
|
if (e instanceof ReposError) fail(e.message);
|
|
24839
25472
|
throw e;
|
|
@@ -24841,29 +25474,29 @@ ${where.join("\n")}`);
|
|
|
24841
25474
|
}
|
|
24842
25475
|
if (row) {
|
|
24843
25476
|
await tracker.deleteRepoRow(row.id);
|
|
24844
|
-
if (
|
|
24845
|
-
const raw = JSON.parse(
|
|
25477
|
+
if (existsSync43(resolvedFile)) {
|
|
25478
|
+
const raw = JSON.parse(readFileSync38(resolvedFile, "utf8"));
|
|
24846
25479
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
24847
25480
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
24848
|
-
|
|
25481
|
+
writeFileSync30(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
24849
25482
|
`);
|
|
24850
25483
|
}
|
|
24851
25484
|
}
|
|
24852
|
-
if (
|
|
25485
|
+
if (existsSync43(join46(dir, ".git")) && basename14(dir) === SAMPLE_REPO) rmSync20(dir, { recursive: true, force: true });
|
|
24853
25486
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
24854
25487
|
break;
|
|
24855
25488
|
}
|
|
24856
25489
|
const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
24857
|
-
const fresh = !
|
|
24858
|
-
if (
|
|
25490
|
+
const fresh = !existsSync43(join46(dir, ".git"));
|
|
25491
|
+
if (existsSync43(dir) && fresh && readdirSync19(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
24859
25492
|
if (dryRun) {
|
|
24860
25493
|
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`);
|
|
24861
25494
|
break;
|
|
24862
25495
|
}
|
|
24863
25496
|
if (fresh) {
|
|
24864
|
-
|
|
24865
|
-
|
|
24866
|
-
|
|
25497
|
+
mkdirSync28(dir, { recursive: true });
|
|
25498
|
+
writeFileSync30(join46(dir, "README.md"), SAMPLE_README);
|
|
25499
|
+
writeFileSync30(join46(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
24867
25500
|
const g = (...a) => {
|
|
24868
25501
|
const r = spawnSync8("git", a, { cwd: dir, encoding: "utf8" });
|
|
24869
25502
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -24881,17 +25514,17 @@ ${where.join("\n")}`);
|
|
|
24881
25514
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
24882
25515
|
});
|
|
24883
25516
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
24884
|
-
if (
|
|
24885
|
-
const raw = JSON.parse(
|
|
25517
|
+
if (existsSync43(resolvedFile)) {
|
|
25518
|
+
const raw = JSON.parse(readFileSync38(resolvedFile, "utf8"));
|
|
24886
25519
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
24887
25520
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
24888
|
-
|
|
25521
|
+
writeFileSync30(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
24889
25522
|
`);
|
|
24890
25523
|
}
|
|
24891
25524
|
}
|
|
24892
25525
|
if (!configured2.some((r) => r.dir === dir)) {
|
|
24893
25526
|
try {
|
|
24894
|
-
|
|
25527
|
+
writeFileSync30(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
|
|
24895
25528
|
} catch (e) {
|
|
24896
25529
|
if (e instanceof ReposError) fail(e.message);
|
|
24897
25530
|
throw e;
|
|
@@ -25124,7 +25757,7 @@ ${where.join("\n")}`);
|
|
|
25124
25757
|
// `synthesis-tabl-123`. `worktreePrefix` above is kept for
|
|
25125
25758
|
// config-provenance display; this is the field that actually
|
|
25126
25759
|
// describes where a worktree lands by default.
|
|
25127
|
-
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${
|
|
25760
|
+
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename14(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
|
|
25128
25761
|
worktreeParent: resolvePath(r.dir, "..")
|
|
25129
25762
|
})),
|
|
25130
25763
|
waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
|
|
@@ -25229,7 +25862,7 @@ watermark: ${state.watermark()}
|
|
|
25229
25862
|
}
|
|
25230
25863
|
for (const r of repos) {
|
|
25231
25864
|
const remote = r.config.branch.remote;
|
|
25232
|
-
if (
|
|
25865
|
+
if (existsSync43(r.dir)) {
|
|
25233
25866
|
pushAuth.push({
|
|
25234
25867
|
route: c.route,
|
|
25235
25868
|
repo: r.name,
|
|
@@ -25315,7 +25948,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
25315
25948
|
}
|
|
25316
25949
|
try {
|
|
25317
25950
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
25318
|
-
const evSize =
|
|
25951
|
+
const evSize = existsSync43(evFile) ? statSync14(evFile).size : 0;
|
|
25319
25952
|
process.stdout.write(
|
|
25320
25953
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
25321
25954
|
` : "events log: ok\n"
|
|
@@ -25326,7 +25959,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
25326
25959
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run", installContext());
|
|
25327
25960
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
25328
25961
|
try {
|
|
25329
|
-
return
|
|
25962
|
+
return readFileSync38(p, "utf8");
|
|
25330
25963
|
} catch {
|
|
25331
25964
|
return null;
|
|
25332
25965
|
}
|
|
@@ -25369,7 +26002,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
25369
26002
|
);
|
|
25370
26003
|
for (const r of await resolvedRepos(route2)) {
|
|
25371
26004
|
const { config } = r;
|
|
25372
|
-
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${
|
|
26005
|
+
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename14(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
|
|
25373
26006
|
process.stdout.write(
|
|
25374
26007
|
`${`repo ${r.name}:`.padEnd(19)}${r.dir}
|
|
25375
26008
|
worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
|
|
@@ -25756,6 +26389,11 @@ ${foreign.join("\n")}
|
|
|
25756
26389
|
);
|
|
25757
26390
|
}
|
|
25758
26391
|
}
|
|
26392
|
+
for (const e of listPeriscopeEndpoints(cfg.ship.stateDir)) {
|
|
26393
|
+
const secret = readPeriscopeSecret(cfg.ship.stateDir, e.ticketRecordId) ? "live" : "ended, awaiting removal";
|
|
26394
|
+
process.stdout.write(`periscope mcp: ${e.ticket} ${e.route} ${e.url} (${secret})
|
|
26395
|
+
`);
|
|
26396
|
+
}
|
|
25759
26397
|
if (cfg.ship.relayHost && !flag("offline")) {
|
|
25760
26398
|
const relayUrl = routeRelayUrl(route2);
|
|
25761
26399
|
if (!relayUrl) {
|
|
@@ -25803,6 +26441,8 @@ var parseTicketNumber2;
|
|
|
25803
26441
|
var routeRelayUrl2;
|
|
25804
26442
|
var passengerApiKeys2;
|
|
25805
26443
|
var endpointApiFor2;
|
|
26444
|
+
var periscopeEndpointDeps2;
|
|
26445
|
+
var periscopeMcpDeps2;
|
|
25806
26446
|
var endpointReconcileDeps2;
|
|
25807
26447
|
var releaseRefused2;
|
|
25808
26448
|
var doctorMachineId2;
|