@tablation/crew 0.99.0 → 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +894 -299
- 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
|
}
|
|
@@ -83,6 +83,25 @@ function routePeriscopeProblems(o) {
|
|
|
83
83
|
function previewHostFinding(message) {
|
|
84
84
|
return /does not resolve/.test(message) ? { severity: "attention", kind: "preview_host_unresolvable", message } : { severity: "blocker", kind: "preview_host", message };
|
|
85
85
|
}
|
|
86
|
+
var refId = (v) => typeof v === "string" ? v : v && typeof v === "object" && typeof v.id === "string" ? v.id : void 0;
|
|
87
|
+
function duplicateLivePeriscopes(rows, tagOf, shipName2, route2) {
|
|
88
|
+
const byTicket = /* @__PURE__ */ new Map();
|
|
89
|
+
for (const r of rows) {
|
|
90
|
+
const t = refId(r.issue_id);
|
|
91
|
+
if (t) byTicket.set(t, [...byTicket.get(t) ?? [], r]);
|
|
92
|
+
}
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const [t, rs] of byTicket) {
|
|
95
|
+
if (rs.length < 2) continue;
|
|
96
|
+
out.push({
|
|
97
|
+
severity: "attention",
|
|
98
|
+
kind: "periscope_duplicate_live",
|
|
99
|
+
...route2 ? { route: route2 } : {},
|
|
100
|
+
message: `${tagOf(t)} has ${rs.length} live Periscopes rows (${rs.map((r) => `${r.id} on ${shipName2(r.ship_id)}${r.url ? ` ${r.url}` : ""}`).join("; ")}) \u2014 the periscope_url rollup assumes one; end all but one with \`crew periscope down\``
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
86
105
|
|
|
87
106
|
// src/periscope-state.ts
|
|
88
107
|
import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
@@ -121,11 +140,11 @@ function urlPort(url) {
|
|
|
121
140
|
}
|
|
122
141
|
}
|
|
123
142
|
function portListening(port, timeoutMs = 500) {
|
|
124
|
-
return new Promise((
|
|
143
|
+
return new Promise((resolve7) => {
|
|
125
144
|
const s = connect({ port, host: "127.0.0.1" });
|
|
126
145
|
const done = (ok) => {
|
|
127
146
|
s.destroy();
|
|
128
|
-
|
|
147
|
+
resolve7(ok);
|
|
129
148
|
};
|
|
130
149
|
s.setTimeout(timeoutMs, () => done(false));
|
|
131
150
|
s.once("connect", () => done(true));
|
|
@@ -564,8 +583,8 @@ function removeClaudeMcp(o) {
|
|
|
564
583
|
if (got.missing) return `mcp: \`${o.bin}\` not found, so Claude Code was not touched`;
|
|
565
584
|
if (got.status !== 0) return `mcp: no ${MCP_SERVER_NAME} server registered with Claude Code`;
|
|
566
585
|
if (o.dryRun) return `(dry run) would remove the ${MCP_SERVER_NAME} MCP server from Claude Code (user scope)`;
|
|
567
|
-
const
|
|
568
|
-
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}`})`;
|
|
569
588
|
}
|
|
570
589
|
|
|
571
590
|
// src/launcher.ts
|
|
@@ -601,10 +620,10 @@ function selectsCustomModelProvider(args) {
|
|
|
601
620
|
}
|
|
602
621
|
return false;
|
|
603
622
|
}
|
|
604
|
-
function parseOmlxSettings(
|
|
623
|
+
function parseOmlxSettings(text2) {
|
|
605
624
|
let raw;
|
|
606
625
|
try {
|
|
607
|
-
raw = JSON.parse(
|
|
626
|
+
raw = JSON.parse(text2);
|
|
608
627
|
} catch {
|
|
609
628
|
return void 0;
|
|
610
629
|
}
|
|
@@ -825,32 +844,32 @@ var BLOCK_RE = new RegExp(`${escapeRe(PAIR_BLOCK_START)}[\\s\\S]*?${escapeRe(PAI
|
|
|
825
844
|
function escapeRe(s) {
|
|
826
845
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
827
846
|
}
|
|
828
|
-
function hasPairBlock(
|
|
829
|
-
return BLOCK_RE.test(
|
|
847
|
+
function hasPairBlock(text2) {
|
|
848
|
+
return BLOCK_RE.test(text2);
|
|
830
849
|
}
|
|
831
|
-
function pairBlockIsBroken(
|
|
832
|
-
return !hasPairBlock(
|
|
850
|
+
function pairBlockIsBroken(text2) {
|
|
851
|
+
return !hasPairBlock(text2) && (text2.includes(PAIR_BLOCK_START) || text2.includes(PAIR_BLOCK_END));
|
|
833
852
|
}
|
|
834
|
-
function upsertPairBlock(
|
|
835
|
-
if (hasPairBlock(
|
|
853
|
+
function upsertPairBlock(text2, block) {
|
|
854
|
+
if (hasPairBlock(text2)) return text2.replace(BLOCK_RE, () => `${block}
|
|
836
855
|
`);
|
|
837
|
-
if (
|
|
856
|
+
if (text2.trim() === "") return `${block}
|
|
838
857
|
`;
|
|
839
|
-
return `${
|
|
858
|
+
return `${text2.replace(/\n*$/, "")}
|
|
840
859
|
|
|
841
860
|
${block}
|
|
842
861
|
`;
|
|
843
862
|
}
|
|
844
|
-
function removePairBlock(
|
|
845
|
-
if (!hasPairBlock(
|
|
846
|
-
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");
|
|
847
866
|
}
|
|
848
867
|
function readIfExists(path) {
|
|
849
868
|
return existsSync5(path) ? readFileSync7(path, "utf8") : void 0;
|
|
850
869
|
}
|
|
851
|
-
function writeFileMkdir(path,
|
|
870
|
+
function writeFileMkdir(path, text2) {
|
|
852
871
|
mkdirSync4(dirname2(path), { recursive: true });
|
|
853
|
-
writeFileSync3(path,
|
|
872
|
+
writeFileSync3(path, text2);
|
|
854
873
|
}
|
|
855
874
|
function installBlockFile(path, harness, dryRun) {
|
|
856
875
|
const before = readIfExists(path) ?? "";
|
|
@@ -870,9 +889,9 @@ function removeBlockFile(path, dryRun) {
|
|
|
870
889
|
return true;
|
|
871
890
|
}
|
|
872
891
|
function blockVerdict(path, harness) {
|
|
873
|
-
const
|
|
874
|
-
if (
|
|
875
|
-
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` };
|
|
876
895
|
return { ok: true, detail: "" };
|
|
877
896
|
}
|
|
878
897
|
var HEADER_RE = /^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(#.*)?$/;
|
|
@@ -894,24 +913,24 @@ function tomlTableRange(lines, name) {
|
|
|
894
913
|
return [start, end];
|
|
895
914
|
}
|
|
896
915
|
var tomlString = (v) => JSON.stringify(v);
|
|
897
|
-
function upsertTomlTable(
|
|
898
|
-
const lines =
|
|
916
|
+
function upsertTomlTable(text2, name, table2) {
|
|
917
|
+
const lines = text2.split("\n");
|
|
899
918
|
const r = tomlTableRange(lines, name);
|
|
900
919
|
if (r) {
|
|
901
920
|
lines.splice(r[0], r[1] - r[0], ...table2);
|
|
902
921
|
return lines.join("\n");
|
|
903
922
|
}
|
|
904
|
-
const body =
|
|
923
|
+
const body = text2.replace(/\n*$/, "");
|
|
905
924
|
return body === "" ? `${table2.join("\n")}
|
|
906
925
|
` : `${body}
|
|
907
926
|
|
|
908
927
|
${table2.join("\n")}
|
|
909
928
|
`;
|
|
910
929
|
}
|
|
911
|
-
function removeTomlTable(
|
|
912
|
-
const lines =
|
|
930
|
+
function removeTomlTable(text2, name) {
|
|
931
|
+
const lines = text2.split("\n");
|
|
913
932
|
let r = tomlTableRange(lines, name);
|
|
914
|
-
if (!r) return
|
|
933
|
+
if (!r) return text2;
|
|
915
934
|
while (r) {
|
|
916
935
|
let [start, end] = r;
|
|
917
936
|
if (start > 0 && lines[start - 1].trim() === "") start--;
|
|
@@ -1145,10 +1164,10 @@ var geminiPaths = (home) => ({
|
|
|
1145
1164
|
config: join8(home, ".gemini", "settings.json")
|
|
1146
1165
|
});
|
|
1147
1166
|
function readGeminiSettings(path) {
|
|
1148
|
-
const
|
|
1149
|
-
if (
|
|
1167
|
+
const text2 = readIfExists(path);
|
|
1168
|
+
if (text2 === void 0 || text2.trim() === "") return {};
|
|
1150
1169
|
try {
|
|
1151
|
-
const j = JSON.parse(
|
|
1170
|
+
const j = JSON.parse(text2);
|
|
1152
1171
|
return j && typeof j === "object" && !Array.isArray(j) ? j : void 0;
|
|
1153
1172
|
} catch {
|
|
1154
1173
|
return void 0;
|
|
@@ -1248,10 +1267,10 @@ function vscodeUserDirs(o) {
|
|
|
1248
1267
|
}
|
|
1249
1268
|
var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
|
|
1250
1269
|
function readVscodeMcp(path) {
|
|
1251
|
-
const
|
|
1252
|
-
if (
|
|
1270
|
+
const text2 = readIfExists(path);
|
|
1271
|
+
if (text2 === void 0 || text2.trim() === "") return {};
|
|
1253
1272
|
try {
|
|
1254
|
-
const j = JSON.parse(
|
|
1273
|
+
const j = JSON.parse(text2);
|
|
1255
1274
|
return j && typeof j === "object" && !Array.isArray(j) ? j : void 0;
|
|
1256
1275
|
} catch {
|
|
1257
1276
|
return void 0;
|
|
@@ -1427,9 +1446,9 @@ function mapGeminiLine(line) {
|
|
|
1427
1446
|
function createGeminiStreamMapper() {
|
|
1428
1447
|
let pending = "";
|
|
1429
1448
|
const flush = () => {
|
|
1430
|
-
const
|
|
1449
|
+
const text2 = pending;
|
|
1431
1450
|
pending = "";
|
|
1432
|
-
return
|
|
1451
|
+
return text2 ? [{ kind: "text", text: text2 }] : [];
|
|
1433
1452
|
};
|
|
1434
1453
|
return {
|
|
1435
1454
|
flush,
|
|
@@ -1739,9 +1758,9 @@ function storeForgeToken(forges, host, token, stateDir, env = process.env) {
|
|
|
1739
1758
|
if (!token.trim()) throw new ForgeError("empty token");
|
|
1740
1759
|
i.write(f.entry.token, token.trim(), { host: f.host, stateDir, env });
|
|
1741
1760
|
}
|
|
1742
|
-
function parseCredentialInput(
|
|
1761
|
+
function parseCredentialInput(text2) {
|
|
1743
1762
|
const out = {};
|
|
1744
|
-
for (const line of
|
|
1763
|
+
for (const line of text2.split("\n")) {
|
|
1745
1764
|
if (line === "") break;
|
|
1746
1765
|
const eq2 = line.indexOf("=");
|
|
1747
1766
|
if (eq2 > 0) out[line.slice(0, eq2)] = line.slice(eq2 + 1);
|
|
@@ -2129,10 +2148,10 @@ function rejectUnknown(obj, allowed, where, file) {
|
|
|
2129
2148
|
);
|
|
2130
2149
|
}
|
|
2131
2150
|
}
|
|
2132
|
-
function parseRepoConfig(
|
|
2151
|
+
function parseRepoConfig(text2, file) {
|
|
2133
2152
|
let raw;
|
|
2134
2153
|
try {
|
|
2135
|
-
raw = file.endsWith(".json") ? JSON.parse(
|
|
2154
|
+
raw = file.endsWith(".json") ? JSON.parse(text2) : parse(text2);
|
|
2136
2155
|
} catch (e) {
|
|
2137
2156
|
throw new RepoConfigError(`${file}: could not be parsed \u2014 ${e.message}`);
|
|
2138
2157
|
}
|
|
@@ -2648,9 +2667,9 @@ function parseShipEnv(v) {
|
|
|
2648
2667
|
if (typeof val !== "string" && typeof val !== "number" && typeof val !== "boolean") {
|
|
2649
2668
|
throw new ConfigError(`ship.env.${k} must be a string, number or boolean`);
|
|
2650
2669
|
}
|
|
2651
|
-
const
|
|
2652
|
-
if (/[\r\n\0]/.test(
|
|
2653
|
-
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;
|
|
2654
2673
|
}
|
|
2655
2674
|
return Object.keys(out).length ? out : void 0;
|
|
2656
2675
|
}
|
|
@@ -3620,10 +3639,10 @@ function runScript(script, opts) {
|
|
|
3620
3639
|
let output = "";
|
|
3621
3640
|
let pending = "";
|
|
3622
3641
|
const absorb = (buf) => {
|
|
3623
|
-
const
|
|
3624
|
-
output +=
|
|
3642
|
+
const text2 = buf.toString();
|
|
3643
|
+
output += text2;
|
|
3625
3644
|
if (!opts.onLine) return;
|
|
3626
|
-
pending +=
|
|
3645
|
+
pending += text2;
|
|
3627
3646
|
const lines = pending.split("\n");
|
|
3628
3647
|
pending = lines.pop() ?? "";
|
|
3629
3648
|
for (const l of lines) opts.onLine(l);
|
|
@@ -4043,9 +4062,9 @@ function planCron(ship, crewHome, job) {
|
|
|
4043
4062
|
function unitAbandonsAgents(plan, read) {
|
|
4044
4063
|
const path = plan.unitPaths[0];
|
|
4045
4064
|
if (!path || plan.mechanism !== "launchd" && plan.mechanism !== "systemd") return null;
|
|
4046
|
-
const
|
|
4047
|
-
if (
|
|
4048
|
-
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);
|
|
4049
4068
|
}
|
|
4050
4069
|
function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run", ctx = USER_INSTALL) {
|
|
4051
4070
|
if (hostShip === "macos") return planLaunchd(ship, crewHome, job);
|
|
@@ -4230,8 +4249,8 @@ function launchAgentsDir() {
|
|
|
4230
4249
|
function xmlUnescape(s) {
|
|
4231
4250
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
4232
4251
|
}
|
|
4233
|
-
function programFromPlist(
|
|
4234
|
-
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);
|
|
4235
4254
|
if (!block) return null;
|
|
4236
4255
|
const args = [...block[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => xmlUnescape(m[1]));
|
|
4237
4256
|
return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
|
|
@@ -4297,9 +4316,9 @@ function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
|
|
|
4297
4316
|
function removeAllLaunchdUnits(dryRun, log, dir, loaded) {
|
|
4298
4317
|
for (const unit of listLaunchdUnits(dir, loaded)) removeLaunchdUnit(unit, dryRun, log);
|
|
4299
4318
|
}
|
|
4300
|
-
function parseLaunchctlList(
|
|
4319
|
+
function parseLaunchctlList(text2) {
|
|
4301
4320
|
const jobs = /* @__PURE__ */ new Map();
|
|
4302
|
-
for (const line of
|
|
4321
|
+
for (const line of text2.split("\n")) {
|
|
4303
4322
|
const cols = line.split(" ");
|
|
4304
4323
|
if (cols.length !== 3 || cols[2] === "Label") continue;
|
|
4305
4324
|
jobs.set(cols[2], { pid: cols[0], lastExit: cols[1] });
|
|
@@ -4323,8 +4342,8 @@ function readCrontab() {
|
|
|
4323
4342
|
const res = spawnSync4("crontab", ["-l"], { encoding: "utf8" });
|
|
4324
4343
|
return res.status === 0 ? res.stdout ?? "" : "";
|
|
4325
4344
|
}
|
|
4326
|
-
function writeCrontab(
|
|
4327
|
-
spawnSync4("crontab", ["-"], { input:
|
|
4345
|
+
function writeCrontab(text2) {
|
|
4346
|
+
spawnSync4("crontab", ["-"], { input: text2, encoding: "utf8" });
|
|
4328
4347
|
}
|
|
4329
4348
|
function addCrontabLine(marker, line) {
|
|
4330
4349
|
const existing = readCrontab();
|
|
@@ -5337,10 +5356,10 @@ function formatPushAuth(repo, r) {
|
|
|
5337
5356
|
return `${label}FAILED \u2014 ${r.detail}; ${r.hint}
|
|
5338
5357
|
`;
|
|
5339
5358
|
}
|
|
5340
|
-
async function settleRepos(items, nameOf,
|
|
5359
|
+
async function settleRepos(items, nameOf, resolve7) {
|
|
5341
5360
|
const out = await Promise.all(items.map(async (t) => {
|
|
5342
5361
|
try {
|
|
5343
|
-
return { ok: true, value: await
|
|
5362
|
+
return { ok: true, value: await resolve7(t) };
|
|
5344
5363
|
} catch (e) {
|
|
5345
5364
|
return { ok: false, repo: nameOf(t), detail: e.message };
|
|
5346
5365
|
}
|
|
@@ -6641,8 +6660,8 @@ async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /
|
|
|
6641
6660
|
|
|
6642
6661
|
// src/ticket-attribution.ts
|
|
6643
6662
|
var TICKET_KEY_RE = /\b[A-Z][A-Z0-9]{1,9}-\d+\b/g;
|
|
6644
|
-
function ticketMentions(
|
|
6645
|
-
return [...
|
|
6663
|
+
function ticketMentions(text2) {
|
|
6664
|
+
return [...text2.matchAll(TICKET_KEY_RE)].map((m) => m[0]);
|
|
6646
6665
|
}
|
|
6647
6666
|
var TicketAttributionTracker = class {
|
|
6648
6667
|
#tokensByTicket = /* @__PURE__ */ new Map();
|
|
@@ -6661,13 +6680,13 @@ var TicketAttributionTracker = class {
|
|
|
6661
6680
|
return this.#current;
|
|
6662
6681
|
}
|
|
6663
6682
|
/** Record the mentions in `text` without crediting tokens (a claim needs "which ticket" before the turn's usage line arrives). */
|
|
6664
|
-
note(
|
|
6665
|
-
const mentions = ticketMentions(
|
|
6683
|
+
note(text2) {
|
|
6684
|
+
const mentions = ticketMentions(text2);
|
|
6666
6685
|
if (mentions.length > 0) this.#current = mentions[0];
|
|
6667
6686
|
}
|
|
6668
6687
|
/** One assistant turn: its combined visible text (thinking + text blocks) and its own token cost. */
|
|
6669
|
-
add(
|
|
6670
|
-
const mentions = ticketMentions(
|
|
6688
|
+
add(text2, tokens) {
|
|
6689
|
+
const mentions = ticketMentions(text2);
|
|
6671
6690
|
if (mentions.length > 0) this.#current = mentions[0];
|
|
6672
6691
|
if (this.#current === void 0 || tokens <= 0) return;
|
|
6673
6692
|
this.#turns++;
|
|
@@ -6764,10 +6783,10 @@ function resultText(content) {
|
|
|
6764
6783
|
if (Array.isArray(content)) return content.map((c) => c && typeof c.text === "string" ? c.text : "").join("\n");
|
|
6765
6784
|
return "";
|
|
6766
6785
|
}
|
|
6767
|
-
function isTransportFailure(isError,
|
|
6786
|
+
function isTransportFailure(isError, text2) {
|
|
6768
6787
|
if (!isError) {
|
|
6769
6788
|
try {
|
|
6770
|
-
const doc = JSON.parse(
|
|
6789
|
+
const doc = JSON.parse(text2);
|
|
6771
6790
|
if (doc && typeof doc === "object") {
|
|
6772
6791
|
const code = doc.statusCode;
|
|
6773
6792
|
return typeof code === "number" && code >= 500;
|
|
@@ -6775,7 +6794,7 @@ function isTransportFailure(isError, text) {
|
|
|
6775
6794
|
} catch {
|
|
6776
6795
|
}
|
|
6777
6796
|
}
|
|
6778
|
-
return TRANSPORT_FAILURE.test(
|
|
6797
|
+
return TRANSPORT_FAILURE.test(text2);
|
|
6779
6798
|
}
|
|
6780
6799
|
var HandoffTranscriptTracker = class {
|
|
6781
6800
|
open = /* @__PURE__ */ new Map();
|
|
@@ -7672,6 +7691,30 @@ var Tracker = class {
|
|
|
7672
7691
|
limit: 200
|
|
7673
7692
|
});
|
|
7674
7693
|
}
|
|
7694
|
+
/**
|
|
7695
|
+
* Every `live` Periscopes row on any ship (CREW-2871), for the one-per-issue
|
|
7696
|
+
* guard and doctor. Empty without the table.
|
|
7697
|
+
*/
|
|
7698
|
+
async livePeriscopesAllShips() {
|
|
7699
|
+
const modelId = this.route.resolved?.periscopesModelId;
|
|
7700
|
+
if (!modelId) return [];
|
|
7701
|
+
return this.client.records.list(modelId, {
|
|
7702
|
+
filters: encodeFilters([{ columnName: "state", operator: "EQ", value: "live" }]),
|
|
7703
|
+
limit: 500
|
|
7704
|
+
});
|
|
7705
|
+
}
|
|
7706
|
+
/** The `live` Periscopes rows of one ticket, on any ship (CREW-2871). */
|
|
7707
|
+
async livePeriscopesForTicket(ticketRecordId) {
|
|
7708
|
+
const modelId = this.route.resolved?.periscopesModelId;
|
|
7709
|
+
if (!modelId) return [];
|
|
7710
|
+
return this.client.records.list(modelId, {
|
|
7711
|
+
filters: encodeFilters([
|
|
7712
|
+
{ columnName: "issue_id", operator: "EQ", value: ticketRecordId },
|
|
7713
|
+
{ columnName: "state", operator: "EQ", value: "live" }
|
|
7714
|
+
]),
|
|
7715
|
+
limit: 50
|
|
7716
|
+
});
|
|
7717
|
+
}
|
|
7675
7718
|
/** Patches one Periscopes row (EPIC-077). */
|
|
7676
7719
|
async updatePeriscope(rowId, fields) {
|
|
7677
7720
|
const modelId = this.route.resolved?.periscopesModelId;
|
|
@@ -7696,12 +7739,12 @@ var Tracker = class {
|
|
|
7696
7739
|
const conds = [{ columnName: "ship_id", operator: "EQ", value: shipId }];
|
|
7697
7740
|
if (states?.length) conds.push({ columnName: "state", operator: "IN", value: states });
|
|
7698
7741
|
const rows = await this.client.records.list(modelId, { filters: encodeFilters(conds), limit: 200 });
|
|
7699
|
-
const
|
|
7700
|
-
const ids = [...new Set(rows.map((r) =>
|
|
7742
|
+
const refId2 = (v) => typeof v === "string" ? v : v && typeof v === "object" && typeof v.id === "string" ? v.id : void 0;
|
|
7743
|
+
const ids = [...new Set(rows.map((r) => refId2(r.issue_id)).filter((x) => !!x))];
|
|
7701
7744
|
const tickets = new Map((await this.ticketsByIds(ids)).map((t) => [t.id, t]));
|
|
7702
7745
|
const out = [];
|
|
7703
7746
|
for (const row of rows) {
|
|
7704
|
-
const t = tickets.get(
|
|
7747
|
+
const t = tickets.get(refId2(row.issue_id) ?? "");
|
|
7705
7748
|
if (t) out.push({ row, ticket: t });
|
|
7706
7749
|
}
|
|
7707
7750
|
return out.sort((a, b) => String(b.row.started_at ?? "").localeCompare(String(a.row.started_at ?? "")));
|
|
@@ -8610,15 +8653,15 @@ async function replayPendingHandoffs(i) {
|
|
|
8610
8653
|
continue;
|
|
8611
8654
|
}
|
|
8612
8655
|
if (v.kind === "unverified") throw new Error(v.why);
|
|
8613
|
-
const
|
|
8614
|
-
if (
|
|
8615
|
-
const head = firstLine(
|
|
8656
|
+
const text2 = rec.comment ?? (rec.commentLanded ? "" : rec.finalText);
|
|
8657
|
+
if (text2) {
|
|
8658
|
+
const head = firstLine(text2);
|
|
8616
8659
|
const cc = i.contract.comments;
|
|
8617
8660
|
const seen = (await i.writer.comments()).some((c) => {
|
|
8618
8661
|
const row = c;
|
|
8619
8662
|
return row[cc.parent] === t.id && row[cc.author] === seatId && firstLine(String(row[cc.body] ?? "")) === head;
|
|
8620
8663
|
});
|
|
8621
|
-
if (!seen) await i.writer.postComment(t.id,
|
|
8664
|
+
if (!seen) await i.writer.postComment(t.id, text2, seatId);
|
|
8622
8665
|
}
|
|
8623
8666
|
await i.writer.postEvent(t.id, pushedEventBody(v.sha, v.remote, v.branch), seatId, "pushed");
|
|
8624
8667
|
await i.writer.updateTicket(t.id, rec.patch);
|
|
@@ -8647,15 +8690,15 @@ var SkillIncludeError = class extends Error {
|
|
|
8647
8690
|
var MAX_INCLUDE_DEPTH = 3;
|
|
8648
8691
|
var MAX_INCLUDED_BYTES = 2e5;
|
|
8649
8692
|
var MARKER = /\{\{[ \t]*skill:[ \t]*([^{}\r\n]*?)[ \t]*\}\}/g;
|
|
8650
|
-
function skillMarkerNames(
|
|
8693
|
+
function skillMarkerNames(text2) {
|
|
8651
8694
|
const names = [];
|
|
8652
|
-
for (const m of
|
|
8695
|
+
for (const m of text2.matchAll(MARKER)) {
|
|
8653
8696
|
const name = m[1].trim();
|
|
8654
8697
|
if (name && !names.includes(name)) names.push(name);
|
|
8655
8698
|
}
|
|
8656
8699
|
return names;
|
|
8657
8700
|
}
|
|
8658
|
-
function expandSkillMarkers(
|
|
8701
|
+
function expandSkillMarkers(text2, lookup2, opts = {}) {
|
|
8659
8702
|
const maxDepth = opts.maxDepth ?? MAX_INCLUDE_DEPTH;
|
|
8660
8703
|
const maxBytes = opts.maxBytes ?? MAX_INCLUDED_BYTES;
|
|
8661
8704
|
const warnings = [];
|
|
@@ -8682,7 +8725,7 @@ function expandSkillMarkers(text, lookup2, opts = {}) {
|
|
|
8682
8725
|
included += bytes;
|
|
8683
8726
|
return expand2(trimmed, [...chain, name]);
|
|
8684
8727
|
});
|
|
8685
|
-
return { text: expand2(
|
|
8728
|
+
return { text: expand2(text2, []), warnings };
|
|
8686
8729
|
}
|
|
8687
8730
|
|
|
8688
8731
|
// src/skills.ts
|
|
@@ -8693,9 +8736,9 @@ import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError
|
|
|
8693
8736
|
|
|
8694
8737
|
// src/prompt-additions.ts
|
|
8695
8738
|
var PROMPT_ADDITIONS_SEPARATOR = "---\n\nAdditional instructions from this workspace. Where they conflict with anything above, these take precedence.";
|
|
8696
|
-
function appendAdditions(
|
|
8697
|
-
if (typeof additions !== "string" || additions.trim() === "") return
|
|
8698
|
-
return `${
|
|
8739
|
+
function appendAdditions(text2, additions) {
|
|
8740
|
+
if (typeof additions !== "string" || additions.trim() === "") return text2;
|
|
8741
|
+
return `${text2}
|
|
8699
8742
|
|
|
8700
8743
|
${PROMPT_ADDITIONS_SEPARATOR}
|
|
8701
8744
|
|
|
@@ -9170,8 +9213,8 @@ function describeCrewLink(o) {
|
|
|
9170
9213
|
var SkillSyncError = class extends Error {
|
|
9171
9214
|
};
|
|
9172
9215
|
function parseSkillFile(path) {
|
|
9173
|
-
const
|
|
9174
|
-
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(
|
|
9216
|
+
const text2 = readFileSync18(path, "utf8");
|
|
9217
|
+
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text2);
|
|
9175
9218
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
9176
9219
|
const frontmatter = match[1];
|
|
9177
9220
|
const body = match[2];
|
|
@@ -9360,8 +9403,8 @@ function isEditedSkillRow(row, localPrompt, cached) {
|
|
|
9360
9403
|
function skillMarkerWarnings(promptsDir, personaPrompts) {
|
|
9361
9404
|
const shipped = new Set(listLocalSkills(promptsDir).map((s) => s.name));
|
|
9362
9405
|
const out = [];
|
|
9363
|
-
for (const [role,
|
|
9364
|
-
for (const name of skillMarkerNames(
|
|
9406
|
+
for (const [role, text2] of Object.entries(personaPrompts)) {
|
|
9407
|
+
for (const name of skillMarkerNames(text2 ?? "")) {
|
|
9365
9408
|
if (!shipped.has(name)) out.push(`${role}: brief includes {{skill: ${name}}} but this prompt set ships no skill with that name`);
|
|
9366
9409
|
}
|
|
9367
9410
|
}
|
|
@@ -9698,15 +9741,15 @@ var FileTail = class {
|
|
|
9698
9741
|
return "";
|
|
9699
9742
|
}
|
|
9700
9743
|
try {
|
|
9701
|
-
let
|
|
9744
|
+
let text2 = "";
|
|
9702
9745
|
const chunk = Buffer.alloc(64 * 1024);
|
|
9703
9746
|
for (; ; ) {
|
|
9704
9747
|
const n = readSync(fd, chunk, 0, chunk.length, this.offset);
|
|
9705
9748
|
if (n <= 0) break;
|
|
9706
9749
|
this.offset += n;
|
|
9707
|
-
|
|
9750
|
+
text2 += this.decoder.write(chunk.subarray(0, n));
|
|
9708
9751
|
}
|
|
9709
|
-
return
|
|
9752
|
+
return text2;
|
|
9710
9753
|
} finally {
|
|
9711
9754
|
closeSync3(fd);
|
|
9712
9755
|
}
|
|
@@ -9745,8 +9788,8 @@ var RunObserver = class {
|
|
|
9745
9788
|
this.liveTicket = id.ticket ?? void 0;
|
|
9746
9789
|
this.mapper = adapter.createStreamMapper?.() ?? { map: (p) => adapter.mapStreamLine(p), flush: () => [] };
|
|
9747
9790
|
}
|
|
9748
|
-
feed(
|
|
9749
|
-
this.buf +=
|
|
9791
|
+
feed(text2) {
|
|
9792
|
+
this.buf += text2;
|
|
9750
9793
|
const lines = this.buf.split("\n");
|
|
9751
9794
|
this.buf = lines.pop() ?? "";
|
|
9752
9795
|
for (const line of lines) this.handleLine(line);
|
|
@@ -10011,7 +10054,7 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
10011
10054
|
for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
|
|
10012
10055
|
const eventsSink = openSink(plan.eventsPath);
|
|
10013
10056
|
const id = identityOf(plan, adapter.harness);
|
|
10014
|
-
return new Promise((
|
|
10057
|
+
return new Promise((resolve7, reject) => {
|
|
10015
10058
|
let streamFd;
|
|
10016
10059
|
try {
|
|
10017
10060
|
mkdirSync12(dirname8(plan.streamPath), { recursive: true });
|
|
@@ -10095,7 +10138,7 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
10095
10138
|
plan.agentLog?.apiKey,
|
|
10096
10139
|
hintMoved
|
|
10097
10140
|
);
|
|
10098
|
-
|
|
10141
|
+
resolve7({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {}, ...killed ? { killed } : {} });
|
|
10099
10142
|
});
|
|
10100
10143
|
child.stdin?.on("error", (err3) => {
|
|
10101
10144
|
emit.warn(`agent closed stdin before the prompt was delivered: ${err3.message}`, { step: "agent" });
|
|
@@ -10428,7 +10471,7 @@ var SELFTEST_TIMEOUT_MS = 12e4;
|
|
|
10428
10471
|
var KEY_VAR = "CREW_API_KEY";
|
|
10429
10472
|
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: ";
|
|
10430
10473
|
var realDeps = {
|
|
10431
|
-
run: (o) => new Promise((
|
|
10474
|
+
run: (o) => new Promise((resolve7) => {
|
|
10432
10475
|
let stdout = "";
|
|
10433
10476
|
let stderr = "";
|
|
10434
10477
|
let timedOut = false;
|
|
@@ -10436,7 +10479,7 @@ var realDeps = {
|
|
|
10436
10479
|
const done = (r) => {
|
|
10437
10480
|
if (!settled) {
|
|
10438
10481
|
settled = true;
|
|
10439
|
-
|
|
10482
|
+
resolve7(r);
|
|
10440
10483
|
}
|
|
10441
10484
|
};
|
|
10442
10485
|
const child = spawn3(o.bin, o.args, { cwd: o.cwd, env: o.env, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
@@ -10545,12 +10588,12 @@ function classify(spec, run2, seen) {
|
|
|
10545
10588
|
return fail("binary-not-found", `could not start \`${bin}\` (${run2.spawnError.message})`);
|
|
10546
10589
|
}
|
|
10547
10590
|
if (run2.timedOut) return fail("timeout", "no answer within the time limit");
|
|
10548
|
-
const
|
|
10591
|
+
const text2 = `${seen.toolOutput}
|
|
10549
10592
|
${run2.stderr}`;
|
|
10550
10593
|
if (REFUSED.test(seen.toolOutput) || seen.tools.some((t) => t.startsWith("mcp__")) && REFUSED.test(run2.stderr)) {
|
|
10551
10594
|
return fail("key-refused", "the tracker returned 401/403 to the MCP call");
|
|
10552
10595
|
}
|
|
10553
|
-
if (seen.mcpFailed || !seen.tools.some((t) => t.startsWith("mcp__")) && MCP_GONE.test(`${
|
|
10596
|
+
if (seen.mcpFailed || !seen.tools.some((t) => t.startsWith("mcp__")) && MCP_GONE.test(`${text2}
|
|
10554
10597
|
${seen.texts.join("\n")}`)) {
|
|
10555
10598
|
return fail("mcp-unavailable", "the tracker MCP tool was not available to the session");
|
|
10556
10599
|
}
|
|
@@ -10642,7 +10685,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
10642
10685
|
}
|
|
10643
10686
|
|
|
10644
10687
|
// src/cli.ts
|
|
10645
|
-
import { resolve as
|
|
10688
|
+
import { resolve as resolve6, dirname as dirname23, join as join46, basename as basename14 } from "path";
|
|
10646
10689
|
import { fileURLToPath } from "url";
|
|
10647
10690
|
import { hostname as hostname3, homedir as homedir10 } from "os";
|
|
10648
10691
|
import { spawnSync as spawnSync8 } from "child_process";
|
|
@@ -12212,13 +12255,13 @@ function writeDigest(o, d, role, stateDir, runId, own) {
|
|
|
12212
12255
|
blockerInfo: d.info
|
|
12213
12256
|
};
|
|
12214
12257
|
const body = role === "qa" ? qaDigest(input) : buildingDigest(input);
|
|
12215
|
-
const
|
|
12258
|
+
const text2 = own ? `${ownRunBanner(own)}
|
|
12216
12259
|
|
|
12217
12260
|
${body}` : body;
|
|
12218
12261
|
const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
|
|
12219
|
-
writeFileSync17(path,
|
|
12220
|
-
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(
|
|
12221
|
-
data: { role, bytes: Buffer.byteLength(
|
|
12262
|
+
writeFileSync17(path, text2);
|
|
12263
|
+
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text2)} bytes)`, {
|
|
12264
|
+
data: { role, bytes: Buffer.byteLength(text2) }
|
|
12222
12265
|
});
|
|
12223
12266
|
return true;
|
|
12224
12267
|
} catch (e) {
|
|
@@ -12782,10 +12825,10 @@ function renderFullConfig(a, routeBlock) {
|
|
|
12782
12825
|
routes:
|
|
12783
12826
|
${routeBlock}`;
|
|
12784
12827
|
}
|
|
12785
|
-
function writeNewConfig(path,
|
|
12828
|
+
function writeNewConfig(path, text2) {
|
|
12786
12829
|
mkdirSync16(dirname11(path), { recursive: true });
|
|
12787
12830
|
try {
|
|
12788
|
-
writeFileSync18(path,
|
|
12831
|
+
writeFileSync18(path, text2, { mode: 384, flag: "wx" });
|
|
12789
12832
|
return true;
|
|
12790
12833
|
} catch (e) {
|
|
12791
12834
|
if (e.code === "EEXIST") return false;
|
|
@@ -12822,11 +12865,11 @@ import { homedir as homedir8 } from "os";
|
|
|
12822
12865
|
import { dirname as dirname12, join as join28 } from "path";
|
|
12823
12866
|
import { delimiter as delimiter2 } from "path";
|
|
12824
12867
|
var CHOICE_EXIT_CODE = 3;
|
|
12825
|
-
function createEmitter(
|
|
12868
|
+
function createEmitter(json2, write) {
|
|
12826
12869
|
return {
|
|
12827
|
-
json,
|
|
12870
|
+
json: json2,
|
|
12828
12871
|
emit(event, message, data = {}) {
|
|
12829
|
-
if (!
|
|
12872
|
+
if (!json2) return;
|
|
12830
12873
|
write(`${JSON.stringify({ ...data, event, message })}
|
|
12831
12874
|
`);
|
|
12832
12875
|
}
|
|
@@ -12933,8 +12976,8 @@ function matchRepoName(origin, resolved) {
|
|
|
12933
12976
|
}
|
|
12934
12977
|
return void 0;
|
|
12935
12978
|
}
|
|
12936
|
-
function routeNamesOf(
|
|
12937
|
-
const routes = parseDocument(
|
|
12979
|
+
function routeNamesOf(text2) {
|
|
12980
|
+
const routes = parseDocument(text2).get("routes", true);
|
|
12938
12981
|
if (!isSeq(routes)) return [];
|
|
12939
12982
|
return routes.items.flatMap((i) => isMap(i) && typeof i.get("route") === "string" ? [i.get("route")] : []);
|
|
12940
12983
|
}
|
|
@@ -12968,8 +13011,8 @@ function routeMapOf(doc, route2) {
|
|
|
12968
13011
|
const have = isSeq(routes) ? routes.items.map((i) => isMap(i) ? String(i.get("route")) : "?").join(", ") : "none";
|
|
12969
13012
|
throw new ReposError(`no route "${route2}" in the config (have: ${have})`);
|
|
12970
13013
|
}
|
|
12971
|
-
function configuredRepos(
|
|
12972
|
-
const doc = parseDocument(
|
|
13014
|
+
function configuredRepos(text2, route2, base) {
|
|
13015
|
+
const doc = parseDocument(text2);
|
|
12973
13016
|
const m = routeMapOf(doc, route2);
|
|
12974
13017
|
const repos = m.get("repos");
|
|
12975
13018
|
if (isMap(repos)) {
|
|
@@ -12983,8 +13026,8 @@ function configuredRepos(text, route2, base) {
|
|
|
12983
13026
|
const dir = m.get("dir");
|
|
12984
13027
|
return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
12985
13028
|
}
|
|
12986
|
-
function cloneBaseOf(
|
|
12987
|
-
const m = routeMapOf(parseDocument(
|
|
13029
|
+
function cloneBaseOf(text2, route2, base, isCheckout) {
|
|
13030
|
+
const m = routeMapOf(parseDocument(text2), route2);
|
|
12988
13031
|
const repos = m.get("repos", true);
|
|
12989
13032
|
if (isMap(repos) && repos.items.length > 0) return null;
|
|
12990
13033
|
const dir = m.get("dir");
|
|
@@ -12992,8 +13035,8 @@ function cloneBaseOf(text, route2, base, isCheckout) {
|
|
|
12992
13035
|
const abs = expandDir(String(dir), base);
|
|
12993
13036
|
return isCheckout(abs) ? null : abs;
|
|
12994
13037
|
}
|
|
12995
|
-
function routeSetsHooks(
|
|
12996
|
-
const m = routeMapOf(parseDocument(
|
|
13038
|
+
function routeSetsHooks(text2, route2, name) {
|
|
13039
|
+
const m = routeMapOf(parseDocument(text2), route2);
|
|
12997
13040
|
const repos = m.get("repos", true);
|
|
12998
13041
|
let target = m;
|
|
12999
13042
|
if (isMap(repos)) target = repos.get(name, true);
|
|
@@ -13100,12 +13143,12 @@ function planRepoAdd(i) {
|
|
|
13100
13143
|
}
|
|
13101
13144
|
return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched, droppedDir };
|
|
13102
13145
|
}
|
|
13103
|
-
function planRepoRemove(
|
|
13104
|
-
const doc = parseDocument(
|
|
13146
|
+
function planRepoRemove(text2, base, route2, path) {
|
|
13147
|
+
const doc = parseDocument(text2);
|
|
13105
13148
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
13106
13149
|
const m = routeMapOf(doc, route2);
|
|
13107
13150
|
const target = expandDir(path, base);
|
|
13108
|
-
const existing = configuredRepos(
|
|
13151
|
+
const existing = configuredRepos(text2, route2, base);
|
|
13109
13152
|
const hit = existing.find((e) => e.dir === target);
|
|
13110
13153
|
if (!hit) throw new ReposError(`${target} is not configured on route ${route2}`);
|
|
13111
13154
|
const repos = m.get("repos", true);
|
|
@@ -13230,8 +13273,8 @@ async function gatherHealth(c, check) {
|
|
|
13230
13273
|
function routesInScope(routes, named) {
|
|
13231
13274
|
return named ? routes.filter((r) => r.route === named) : routes;
|
|
13232
13275
|
}
|
|
13233
|
-
function planEnable(
|
|
13234
|
-
const doc = parseDocument2(
|
|
13276
|
+
function planEnable(text2, route2) {
|
|
13277
|
+
const doc = parseDocument2(text2);
|
|
13235
13278
|
routeMapOf(doc, route2).set("enabled", true);
|
|
13236
13279
|
return String(doc);
|
|
13237
13280
|
}
|
|
@@ -13425,13 +13468,13 @@ function realProvisionDeps(crewHome) {
|
|
|
13425
13468
|
enableRoute(route2) {
|
|
13426
13469
|
const file = configFile();
|
|
13427
13470
|
if (!file) return false;
|
|
13428
|
-
const
|
|
13429
|
-
const doc = parseDocument3(
|
|
13471
|
+
const text2 = readFileSync23(file, "utf8");
|
|
13472
|
+
const doc = parseDocument3(text2);
|
|
13430
13473
|
const routes = doc.get("routes");
|
|
13431
13474
|
const entry = routes?.items?.find((r) => r.get("route") === route2);
|
|
13432
13475
|
if (!entry) throw new Error(`route ${route2} is not in ${file}`);
|
|
13433
13476
|
if (entry.get("enabled") === true) return false;
|
|
13434
|
-
writeFileSync19(file, planEnable(
|
|
13477
|
+
writeFileSync19(file, planEnable(text2, route2));
|
|
13435
13478
|
return true;
|
|
13436
13479
|
},
|
|
13437
13480
|
installState() {
|
|
@@ -15615,18 +15658,18 @@ function readFrom(file, from) {
|
|
|
15615
15658
|
const buf = Buffer.alloc(size - from);
|
|
15616
15659
|
readSync2(fd, buf, 0, buf.length, from);
|
|
15617
15660
|
closeSync4(fd);
|
|
15618
|
-
const
|
|
15619
|
-
const lastNewline =
|
|
15661
|
+
const text2 = buf.toString("utf8");
|
|
15662
|
+
const lastNewline = text2.lastIndexOf("\n");
|
|
15620
15663
|
if (lastNewline === -1) return { events: [], offset: from };
|
|
15621
15664
|
const events = [];
|
|
15622
|
-
for (const line of
|
|
15665
|
+
for (const line of text2.slice(0, lastNewline).split("\n")) {
|
|
15623
15666
|
if (!line.trim()) continue;
|
|
15624
15667
|
try {
|
|
15625
15668
|
events.push(JSON.parse(line));
|
|
15626
15669
|
} catch {
|
|
15627
15670
|
}
|
|
15628
15671
|
}
|
|
15629
|
-
return { events, offset: from + Buffer.byteLength(
|
|
15672
|
+
return { events, offset: from + Buffer.byteLength(text2.slice(0, lastNewline + 1), "utf8") };
|
|
15630
15673
|
}
|
|
15631
15674
|
function startWatch(o) {
|
|
15632
15675
|
const out = o.out ?? ((s) => process.stdout.write(s));
|
|
@@ -15921,12 +15964,12 @@ function planNameSync(local, row, lastSynced) {
|
|
|
15921
15964
|
if (rowMoved) return { drift: "row", action: "adopt" };
|
|
15922
15965
|
return { drift: "local", action: "push" };
|
|
15923
15966
|
}
|
|
15924
|
-
function shipNameOfYaml(
|
|
15925
|
-
const v = parseDocument5(
|
|
15967
|
+
function shipNameOfYaml(text2) {
|
|
15968
|
+
const v = parseDocument5(text2).getIn(["ship", "name"]);
|
|
15926
15969
|
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
15927
15970
|
}
|
|
15928
|
-
function setShipNameInYaml(
|
|
15929
|
-
const doc = parseDocument5(
|
|
15971
|
+
function setShipNameInYaml(text2, name) {
|
|
15972
|
+
const doc = parseDocument5(text2);
|
|
15930
15973
|
doc.setIn(["ship", "name"], name);
|
|
15931
15974
|
return doc.toString();
|
|
15932
15975
|
}
|
|
@@ -16015,8 +16058,8 @@ async function reconcileShipName(i) {
|
|
|
16015
16058
|
}
|
|
16016
16059
|
if (result.status !== "ok") return result;
|
|
16017
16060
|
if (result.action === "adopt" || result.action === "row-wins") {
|
|
16018
|
-
const
|
|
16019
|
-
writeFileSync22(i.configFile, setShipNameInYaml(
|
|
16061
|
+
const text2 = readFileSync27(i.configFile, "utf8");
|
|
16062
|
+
writeFileSync22(i.configFile, setShipNameInYaml(text2, result.name));
|
|
16020
16063
|
i.ship.name = result.name;
|
|
16021
16064
|
if (result.action === "row-wins") {
|
|
16022
16065
|
i.emit?.warn(
|
|
@@ -16917,11 +16960,11 @@ function packageManager(f) {
|
|
|
16917
16960
|
return "npm";
|
|
16918
16961
|
}
|
|
16919
16962
|
function fromPackageJson(f) {
|
|
16920
|
-
const
|
|
16921
|
-
if (
|
|
16963
|
+
const text2 = f.read("package.json");
|
|
16964
|
+
if (text2 === void 0) return void 0;
|
|
16922
16965
|
let pkg;
|
|
16923
16966
|
try {
|
|
16924
|
-
pkg = JSON.parse(
|
|
16967
|
+
pkg = JSON.parse(text2);
|
|
16925
16968
|
} catch {
|
|
16926
16969
|
return void 0;
|
|
16927
16970
|
}
|
|
@@ -16944,11 +16987,11 @@ function fromPyproject(f) {
|
|
|
16944
16987
|
return f.exists("pyproject.toml") ? { test: "pytest" } : void 0;
|
|
16945
16988
|
}
|
|
16946
16989
|
function fromMakefile(f) {
|
|
16947
|
-
const
|
|
16948
|
-
if (
|
|
16990
|
+
const text2 = f.read("Makefile") ?? f.read("makefile") ?? f.read("GNUmakefile");
|
|
16991
|
+
if (text2 === void 0) return void 0;
|
|
16949
16992
|
const out = {};
|
|
16950
16993
|
for (const target of ["test", "build"]) {
|
|
16951
|
-
if (new RegExp(`^${target}\\s*:`, "m").test(
|
|
16994
|
+
if (new RegExp(`^${target}\\s*:`, "m").test(text2)) out[target] = `make ${target}`;
|
|
16952
16995
|
}
|
|
16953
16996
|
return out;
|
|
16954
16997
|
}
|
|
@@ -17041,16 +17084,16 @@ function parseHooksArgs(args, routes) {
|
|
|
17041
17084
|
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]");
|
|
17042
17085
|
return { route: route2, name: rest[0] };
|
|
17043
17086
|
}
|
|
17044
|
-
function repoDirOf(
|
|
17045
|
-
const repos = configuredRepos(
|
|
17087
|
+
function repoDirOf(text2, base, route2, name) {
|
|
17088
|
+
const repos = configuredRepos(text2, route2, base);
|
|
17046
17089
|
const hit = repos.find((r) => r.name === name);
|
|
17047
17090
|
if (!hit) {
|
|
17048
17091
|
throw new ReposError(`route ${route2} has no repo named "${name}" (have: ${repos.map((r) => r.name).join(", ") || "none"})`);
|
|
17049
17092
|
}
|
|
17050
17093
|
return hit.dir;
|
|
17051
17094
|
}
|
|
17052
|
-
function planRouteHooks(
|
|
17053
|
-
const doc = parseDocument6(
|
|
17095
|
+
function planRouteHooks(text2, route2, name, hooks, opts = {}) {
|
|
17096
|
+
const doc = parseDocument6(text2);
|
|
17054
17097
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
17055
17098
|
const m = routeMapOf(doc, route2);
|
|
17056
17099
|
const repos = m.get("repos", true);
|
|
@@ -17086,19 +17129,19 @@ function planRouteHooks(text, route2, name, hooks, opts = {}) {
|
|
|
17086
17129
|
}
|
|
17087
17130
|
return { text: String(doc), written, kept, replaced };
|
|
17088
17131
|
}
|
|
17089
|
-
function clearRouteHooks(
|
|
17090
|
-
const doc = parseDocument6(
|
|
17132
|
+
function clearRouteHooks(text2, route2, name) {
|
|
17133
|
+
const doc = parseDocument6(text2);
|
|
17091
17134
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
17092
17135
|
const m = routeMapOf(doc, route2);
|
|
17093
17136
|
const repos = m.get("repos", true);
|
|
17094
17137
|
let target = m;
|
|
17095
17138
|
if (isMap2(repos)) {
|
|
17096
17139
|
const entry = repos.get(name, true);
|
|
17097
|
-
if (!isMap2(entry)) return { text, removed: [] };
|
|
17140
|
+
if (!isMap2(entry)) return { text: text2, removed: [] };
|
|
17098
17141
|
target = entry;
|
|
17099
17142
|
}
|
|
17100
17143
|
const current = target.get("hooks", true);
|
|
17101
|
-
if (!isMap2(current)) return { text, removed: [] };
|
|
17144
|
+
if (!isMap2(current)) return { text: text2, removed: [] };
|
|
17102
17145
|
const removed = current.items.map((i) => String(i.key.value ?? i.key));
|
|
17103
17146
|
target.delete("hooks");
|
|
17104
17147
|
return { text: String(doc), removed };
|
|
@@ -17122,9 +17165,9 @@ function starterRepoConfig(hooks, release) {
|
|
|
17122
17165
|
if (release.verifyMatch) lines.push(" verify:", ` match: ${release.verifyMatch}`);
|
|
17123
17166
|
lines.push("");
|
|
17124
17167
|
} else lines.push("", "release:", " # integrate: merging to the base branch is the release; nothing is deployed.", " mode: integrate", "");
|
|
17125
|
-
const
|
|
17126
|
-
parseRepoConfig(
|
|
17127
|
-
return
|
|
17168
|
+
const text2 = lines.join("\n");
|
|
17169
|
+
parseRepoConfig(text2, ".crew.yaml");
|
|
17170
|
+
return text2;
|
|
17128
17171
|
}
|
|
17129
17172
|
function existingRepoConfig(f) {
|
|
17130
17173
|
return REPO_CONFIG_FILENAMES.find((n) => f.exists(n));
|
|
@@ -17183,7 +17226,7 @@ function agentInvocation(spec, model) {
|
|
|
17183
17226
|
});
|
|
17184
17227
|
}
|
|
17185
17228
|
function parseAgentStream(out, dir) {
|
|
17186
|
-
let
|
|
17229
|
+
let text2 = "";
|
|
17187
17230
|
let isError = false;
|
|
17188
17231
|
const files = /* @__PURE__ */ new Set();
|
|
17189
17232
|
let base = dir;
|
|
@@ -17205,19 +17248,19 @@ function parseAgentStream(out, dir) {
|
|
|
17205
17248
|
if (typeof p === "string") files.add(isAbsolute2(p) ? relative(base, p) : p);
|
|
17206
17249
|
}
|
|
17207
17250
|
} else if (ev?.type === "result") {
|
|
17208
|
-
|
|
17251
|
+
text2 = typeof ev.result === "string" ? ev.result : "";
|
|
17209
17252
|
isError = ev.is_error === true;
|
|
17210
17253
|
}
|
|
17211
17254
|
}
|
|
17212
|
-
return { text, isError, filesRead: [...files].sort() };
|
|
17255
|
+
return { text: text2, isError, filesRead: [...files].sort() };
|
|
17213
17256
|
}
|
|
17214
|
-
function parseAgentHooks(
|
|
17215
|
-
const start =
|
|
17216
|
-
const end =
|
|
17257
|
+
function parseAgentHooks(text2) {
|
|
17258
|
+
const start = text2.indexOf("{");
|
|
17259
|
+
const end = text2.lastIndexOf("}");
|
|
17217
17260
|
if (start < 0 || end <= start) throw new AgentSuggestError("bad_output", "the toolchain did not answer with a JSON object");
|
|
17218
17261
|
let obj;
|
|
17219
17262
|
try {
|
|
17220
|
-
obj = JSON.parse(
|
|
17263
|
+
obj = JSON.parse(text2.slice(start, end + 1));
|
|
17221
17264
|
} catch {
|
|
17222
17265
|
throw new AgentSuggestError("bad_output", "the toolchain's answer was not valid JSON");
|
|
17223
17266
|
}
|
|
@@ -17249,7 +17292,7 @@ async function runAgentSuggest(o) {
|
|
|
17249
17292
|
const before = porcelain(o.dir);
|
|
17250
17293
|
const timeout = AbortSignal.timeout(o.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
17251
17294
|
const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
|
|
17252
|
-
const run2 = await new Promise((
|
|
17295
|
+
const run2 = await new Promise((resolve7) => {
|
|
17253
17296
|
let out = "";
|
|
17254
17297
|
let err3 = "";
|
|
17255
17298
|
const child = spawn4(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -17258,7 +17301,7 @@ async function runAgentSuggest(o) {
|
|
|
17258
17301
|
if (settled) return;
|
|
17259
17302
|
settled = true;
|
|
17260
17303
|
signal.removeEventListener("abort", onAbort);
|
|
17261
|
-
|
|
17304
|
+
resolve7(r);
|
|
17262
17305
|
};
|
|
17263
17306
|
const onAbort = () => {
|
|
17264
17307
|
try {
|
|
@@ -17702,7 +17745,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
17702
17745
|
}
|
|
17703
17746
|
|
|
17704
17747
|
// src/cli.ts
|
|
17705
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
17748
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync37, existsSync as existsSync42, mkdirSync as mkdirSync27, writeFileSync as writeFileSync29, rmSync as rmSync20, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync4, statSync as statSync14, readdirSync as readdirSync19 } from "fs";
|
|
17706
17749
|
import { createInterface } from "readline/promises";
|
|
17707
17750
|
|
|
17708
17751
|
// src/periscope-cmd.ts
|
|
@@ -17710,6 +17753,13 @@ import { existsSync as existsSync35, mkdirSync as mkdirSync21, readFileSync as r
|
|
|
17710
17753
|
import { join as join37 } from "path";
|
|
17711
17754
|
var PeriscopeError = class extends Error {
|
|
17712
17755
|
};
|
|
17756
|
+
var LivePeriscopeError = class extends PeriscopeError {
|
|
17757
|
+
data;
|
|
17758
|
+
constructor(tag, number, live) {
|
|
17759
|
+
super(`${tag} already has a live periscope on ${live.ship}: ${live.url} (worktree ${live.worktree}) \u2014 run \`crew periscope down ${number} --keep|--discard\` on ${live.ship}, or resume it there with \`crew periscope up --resume ${number}\``);
|
|
17760
|
+
this.data = { refused: "live_periscope_exists", ...live };
|
|
17761
|
+
}
|
|
17762
|
+
};
|
|
17713
17763
|
var priorDir = (stateDir) => join37(stateDir, "periscope-priors");
|
|
17714
17764
|
var priorPath = (stateDir, ticketRecordId) => join37(priorDir(stateDir), `${ticketRecordId}.json`);
|
|
17715
17765
|
function removePeriscopePrior(stateDir, ticketRecordId) {
|
|
@@ -17776,6 +17826,10 @@ function periscopeCaps(l) {
|
|
|
17776
17826
|
idleMinutes: l.periscopeIdleMinutes ?? DEFAULT_PERISCOPE_IDLE_MINUTES2
|
|
17777
17827
|
};
|
|
17778
17828
|
}
|
|
17829
|
+
async function assertNoLivePeriscope(e, t, number) {
|
|
17830
|
+
const live = (await e.liveRows(t.id))[0];
|
|
17831
|
+
if (live) throw new LivePeriscopeError(t.tag, number, live);
|
|
17832
|
+
}
|
|
17779
17833
|
async function existingRefusal(e, t) {
|
|
17780
17834
|
if (t.closed) return `${t.tag} is ${t.status}; a closed ticket cannot be periscoped`;
|
|
17781
17835
|
if (e.repoId && t.repoId !== e.repoId) return `${t.tag} belongs to a different repository than ${e.repoName}`;
|
|
@@ -17834,6 +17888,7 @@ async function runPeriscopeUp(d, args) {
|
|
|
17834
17888
|
if (!ex) throw new PeriscopeError("--issue is not supported here");
|
|
17835
17889
|
prior = await ex.load(args.issue);
|
|
17836
17890
|
if (!prior) throw new PeriscopeError(`no ticket numbered ${args.issue} on this route`);
|
|
17891
|
+
await assertNoLivePeriscope(ex, prior, args.issue);
|
|
17837
17892
|
const refused = await existingRefusal(ex, prior);
|
|
17838
17893
|
if (refused) throw new PeriscopeError(refused);
|
|
17839
17894
|
const unsafe = d.baseProblem();
|
|
@@ -19018,7 +19073,7 @@ import { randomBytes as randomBytes2 } from "crypto";
|
|
|
19018
19073
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
19019
19074
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
19020
19075
|
var ALLOWANCE_PAUSE_MS = 5 * 60 * 1e3;
|
|
19021
|
-
var defaultSleep = (ms3) => new Promise((
|
|
19076
|
+
var defaultSleep = (ms3) => new Promise((resolve7) => setTimeout(resolve7, ms3));
|
|
19022
19077
|
async function runDaemonLoop(o) {
|
|
19023
19078
|
const floor = o.floorMs ?? DEFAULT_FLOOR_MS;
|
|
19024
19079
|
const ceiling = o.ceilingMs ?? DEFAULT_CEILING_MS;
|
|
@@ -19075,15 +19130,15 @@ async function runDaemonLoop(o) {
|
|
|
19075
19130
|
}
|
|
19076
19131
|
}
|
|
19077
19132
|
function latestMtime(path) {
|
|
19078
|
-
let
|
|
19133
|
+
let stat2;
|
|
19079
19134
|
try {
|
|
19080
|
-
|
|
19135
|
+
stat2 = statSync12(path);
|
|
19081
19136
|
} catch {
|
|
19082
19137
|
return 0;
|
|
19083
19138
|
}
|
|
19084
|
-
if (
|
|
19085
|
-
if (!
|
|
19086
|
-
let latest =
|
|
19139
|
+
if (stat2.isFile()) return stat2.mtimeMs;
|
|
19140
|
+
if (!stat2.isDirectory()) return 0;
|
|
19141
|
+
let latest = stat2.mtimeMs;
|
|
19087
19142
|
for (const entry of readdirSync16(path, { withFileTypes: true })) {
|
|
19088
19143
|
latest = Math.max(latest, latestMtime(join42(path, entry.name)));
|
|
19089
19144
|
}
|
|
@@ -19276,8 +19331,8 @@ async function runOnePassUnguarded(o) {
|
|
|
19276
19331
|
started.push(role);
|
|
19277
19332
|
let ready = () => {
|
|
19278
19333
|
};
|
|
19279
|
-
const settled = new Promise((
|
|
19280
|
-
ready =
|
|
19334
|
+
const settled = new Promise((resolve7) => {
|
|
19335
|
+
ready = resolve7;
|
|
19281
19336
|
});
|
|
19282
19337
|
const run2 = spawn9(role, decision, ctx, { ready }).then(() => {
|
|
19283
19338
|
breaker.recordSuccess(role);
|
|
@@ -19729,14 +19784,18 @@ function parseLine(line) {
|
|
|
19729
19784
|
return null;
|
|
19730
19785
|
}
|
|
19731
19786
|
}
|
|
19732
|
-
async function execute(h, args, streaming) {
|
|
19733
|
-
const
|
|
19787
|
+
async function execute(h, args, streaming, after) {
|
|
19788
|
+
const finishRaw = (r) => {
|
|
19734
19789
|
if (r.code !== 0) return { status: failureStatus(r), body: { error: cliMessage(r) } };
|
|
19735
19790
|
const done = lastJsonLine(r.stdout);
|
|
19736
19791
|
return done ? { status: 200, body: done } : { status: 502, body: { error: "the command finished without a result" } };
|
|
19737
19792
|
};
|
|
19793
|
+
const finish = async (r) => {
|
|
19794
|
+
const f = finishRaw(r);
|
|
19795
|
+
return f.status === 200 && after ? { status: 200, body: await after(f.body) } : f;
|
|
19796
|
+
};
|
|
19738
19797
|
if (!streaming) {
|
|
19739
|
-
const f = finish(await h.run(args, () => {
|
|
19798
|
+
const f = await finish(await h.run(args, () => {
|
|
19740
19799
|
}));
|
|
19741
19800
|
return reply(f.status, f.body);
|
|
19742
19801
|
}
|
|
@@ -19748,7 +19807,7 @@ async function execute(h, args, streaming) {
|
|
|
19748
19807
|
const ev = parseLine(line);
|
|
19749
19808
|
if (ev) write(ev);
|
|
19750
19809
|
});
|
|
19751
|
-
const f = finish(r);
|
|
19810
|
+
const f = await finish(r);
|
|
19752
19811
|
write(f.status === 200 ? f.body : { event: "error", status: f.status, message: f.body.error });
|
|
19753
19812
|
}
|
|
19754
19813
|
};
|
|
@@ -19812,7 +19871,10 @@ async function handlePeriscopes(h, q) {
|
|
|
19812
19871
|
`--${b.outcome}`,
|
|
19813
19872
|
...b.allowEmpty === true ? ["--allow-empty"] : [],
|
|
19814
19873
|
"--json"
|
|
19815
|
-
], q.wantsStream)
|
|
19874
|
+
], q.wantsStream, h.mcp ? async (done) => {
|
|
19875
|
+
await h.mcp.onDown(picked, done);
|
|
19876
|
+
return done;
|
|
19877
|
+
} : void 0);
|
|
19816
19878
|
}
|
|
19817
19879
|
const repo = plainText(b.repo, "repo", true);
|
|
19818
19880
|
const topic = plainText(b.topic, "topic", true);
|
|
@@ -19840,12 +19902,13 @@ async function handlePeriscopes(h, q) {
|
|
|
19840
19902
|
h.previewHost,
|
|
19841
19903
|
...requestedBy ? ["--requested-by", requestedBy] : [],
|
|
19842
19904
|
...b.isolate === true ? ["--isolate"] : [],
|
|
19905
|
+
...h.mcp ? ["--serve-mcp"] : [],
|
|
19843
19906
|
"--no-open",
|
|
19844
19907
|
"--json"
|
|
19845
|
-
], q.wantsStream);
|
|
19908
|
+
], q.wantsStream, h.mcp ? async (done) => ({ ...done, ...await h.mcp.onUp(picked, done) }) : void 0);
|
|
19846
19909
|
}
|
|
19847
19910
|
function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
19848
|
-
return new Promise((
|
|
19911
|
+
return new Promise((resolve7) => {
|
|
19849
19912
|
const child = spawn7(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
19850
19913
|
let stdout = "";
|
|
19851
19914
|
let stderr = "";
|
|
@@ -19854,9 +19917,9 @@ function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
|
19854
19917
|
stdout += d.toString("utf8");
|
|
19855
19918
|
});
|
|
19856
19919
|
child.stderr.on("data", (d) => {
|
|
19857
|
-
const
|
|
19858
|
-
stderr +=
|
|
19859
|
-
pending +=
|
|
19920
|
+
const text2 = d.toString("utf8");
|
|
19921
|
+
stderr += text2;
|
|
19922
|
+
pending += text2;
|
|
19860
19923
|
let i;
|
|
19861
19924
|
while ((i = pending.indexOf("\n")) >= 0) {
|
|
19862
19925
|
const line = pending.slice(0, i).trim();
|
|
@@ -19867,12 +19930,12 @@ function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
|
19867
19930
|
const t = setTimeout(() => child.kill("SIGTERM"), timeoutMs);
|
|
19868
19931
|
child.on("error", (e) => {
|
|
19869
19932
|
clearTimeout(t);
|
|
19870
|
-
|
|
19933
|
+
resolve7({ code: 127, stdout, stderr: `${stderr}${e.message}
|
|
19871
19934
|
` });
|
|
19872
19935
|
});
|
|
19873
19936
|
child.on("close", (code) => {
|
|
19874
19937
|
clearTimeout(t);
|
|
19875
|
-
|
|
19938
|
+
resolve7({ code: code ?? 1, stdout, stderr });
|
|
19876
19939
|
});
|
|
19877
19940
|
});
|
|
19878
19941
|
}
|
|
@@ -19993,16 +20056,16 @@ function lastLines2(path, n) {
|
|
|
19993
20056
|
const cursorAt = (off) => encodeCursor({ dev: st.dev, ino: st.ino, off });
|
|
19994
20057
|
if (n <= 0 || st.size === 0) return { lines: [], cursor: cursorAt(st.size), reset: false };
|
|
19995
20058
|
let start = st.size;
|
|
19996
|
-
let
|
|
19997
|
-
while (start > 0 &&
|
|
20059
|
+
let text2 = "";
|
|
20060
|
+
while (start > 0 && text2.split("\n").length <= n + 1 && st.size - start < MAX_TAIL_BYTES) {
|
|
19998
20061
|
const step = Math.min(64 * 1024, start);
|
|
19999
20062
|
start -= step;
|
|
20000
|
-
|
|
20063
|
+
text2 = readRange(path, start, step).text + text2;
|
|
20001
20064
|
}
|
|
20002
|
-
let lines =
|
|
20065
|
+
let lines = text2.split("\n");
|
|
20003
20066
|
if (start > 0) lines = lines.slice(1);
|
|
20004
20067
|
let end = st.size;
|
|
20005
|
-
if (!
|
|
20068
|
+
if (!text2.endsWith("\n")) {
|
|
20006
20069
|
const frag = lines.pop() ?? "";
|
|
20007
20070
|
end -= Buffer.byteLength(frag);
|
|
20008
20071
|
} else lines.pop();
|
|
@@ -20044,9 +20107,9 @@ function readTail(live, opts) {
|
|
|
20044
20107
|
off = 0;
|
|
20045
20108
|
size = statSync13(path).size;
|
|
20046
20109
|
}
|
|
20047
|
-
const { text } = readRange(path, off, max);
|
|
20048
|
-
const cut =
|
|
20049
|
-
const whole = cut < 0 ? "" :
|
|
20110
|
+
const { text: text2 } = readRange(path, off, max);
|
|
20111
|
+
const cut = text2.lastIndexOf("\n");
|
|
20112
|
+
const whole = cut < 0 ? "" : text2.slice(0, cut + 1);
|
|
20050
20113
|
const st = statSync13(path);
|
|
20051
20114
|
return {
|
|
20052
20115
|
lines: whole.split("\n").filter((l) => l.length),
|
|
@@ -20055,7 +20118,7 @@ function readTail(live, opts) {
|
|
|
20055
20118
|
};
|
|
20056
20119
|
}
|
|
20057
20120
|
function waitForChange(dir, ms3, signal) {
|
|
20058
|
-
return new Promise((
|
|
20121
|
+
return new Promise((resolve7) => {
|
|
20059
20122
|
let w;
|
|
20060
20123
|
const done = () => {
|
|
20061
20124
|
clearTimeout(t);
|
|
@@ -20065,7 +20128,7 @@ function waitForChange(dir, ms3, signal) {
|
|
|
20065
20128
|
w?.close();
|
|
20066
20129
|
} catch {
|
|
20067
20130
|
}
|
|
20068
|
-
|
|
20131
|
+
resolve7();
|
|
20069
20132
|
};
|
|
20070
20133
|
const t = setTimeout(done, ms3);
|
|
20071
20134
|
const poll = setInterval(done, 500);
|
|
@@ -20212,13 +20275,13 @@ function send(res, r) {
|
|
|
20212
20275
|
}
|
|
20213
20276
|
async function socketIsStale(path) {
|
|
20214
20277
|
if (!existsSync40(path)) return false;
|
|
20215
|
-
return new Promise((
|
|
20278
|
+
return new Promise((resolve7) => {
|
|
20216
20279
|
const c = createConnection(path);
|
|
20217
20280
|
c.once("connect", () => {
|
|
20218
20281
|
c.destroy();
|
|
20219
|
-
|
|
20282
|
+
resolve7(false);
|
|
20220
20283
|
});
|
|
20221
|
-
c.once("error", () =>
|
|
20284
|
+
c.once("error", () => resolve7(true));
|
|
20222
20285
|
});
|
|
20223
20286
|
}
|
|
20224
20287
|
async function startControlServer(deps) {
|
|
@@ -20239,6 +20302,11 @@ async function startControlServer(deps) {
|
|
|
20239
20302
|
res.on("close", () => abort.abort());
|
|
20240
20303
|
void (async () => {
|
|
20241
20304
|
try {
|
|
20305
|
+
const mcp = /^\/periscopes\/([^/]+)\/mcp\/?$/.exec(new URL(req.url ?? "/", "http://ship").pathname);
|
|
20306
|
+
if (mcp) {
|
|
20307
|
+
if (!deps.periscopeMcp) return send(res, err2(404, "not found"));
|
|
20308
|
+
return await deps.periscopeMcp(req, res, decodeURIComponent(mcp[1]));
|
|
20309
|
+
}
|
|
20242
20310
|
const m = /^Bearer (.+)$/.exec(req.headers.authorization ?? "");
|
|
20243
20311
|
const verdict = await deps.auth.verify(m?.[1]?.trim() ?? "");
|
|
20244
20312
|
if (verdict === "unauthorized") return send(res, err2(401, "invalid or revoked key"));
|
|
@@ -20252,10 +20320,10 @@ async function startControlServer(deps) {
|
|
|
20252
20320
|
}
|
|
20253
20321
|
})();
|
|
20254
20322
|
});
|
|
20255
|
-
return new Promise((
|
|
20323
|
+
return new Promise((resolve7) => {
|
|
20256
20324
|
server.once("error", (e) => {
|
|
20257
20325
|
log(`control API not started: ${e.message}`);
|
|
20258
|
-
|
|
20326
|
+
resolve7(null);
|
|
20259
20327
|
});
|
|
20260
20328
|
server.listen(path, () => {
|
|
20261
20329
|
try {
|
|
@@ -20263,7 +20331,7 @@ async function startControlServer(deps) {
|
|
|
20263
20331
|
} catch {
|
|
20264
20332
|
}
|
|
20265
20333
|
server.on("error", (e) => log(`control API error: ${e.message}`));
|
|
20266
|
-
|
|
20334
|
+
resolve7({
|
|
20267
20335
|
server,
|
|
20268
20336
|
path,
|
|
20269
20337
|
close: () => new Promise((done) => {
|
|
@@ -20294,23 +20362,456 @@ function schedulerHandler(control, run2 = runDetached) {
|
|
|
20294
20362
|
};
|
|
20295
20363
|
}
|
|
20296
20364
|
function crewJson(args, timeoutMs = 6e4) {
|
|
20297
|
-
return new Promise((
|
|
20365
|
+
return new Promise((resolve7) => {
|
|
20298
20366
|
execFile(process.execPath, [process.argv[1] ?? "", ...args], { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (_e, stdout) => {
|
|
20299
20367
|
try {
|
|
20300
20368
|
const v = JSON.parse(String(stdout));
|
|
20301
|
-
|
|
20369
|
+
resolve7(v && typeof v === "object" && !Array.isArray(v) ? v : null);
|
|
20302
20370
|
} catch {
|
|
20303
|
-
|
|
20371
|
+
resolve7(null);
|
|
20304
20372
|
}
|
|
20305
20373
|
});
|
|
20306
20374
|
});
|
|
20307
20375
|
}
|
|
20308
20376
|
|
|
20377
|
+
// src/periscope-mcp.ts
|
|
20378
|
+
import { lstat, mkdir, readdir, readFile, readlink, realpath, rm, stat, writeFile } from "fs/promises";
|
|
20379
|
+
import { basename as basename13, dirname as dirname22, isAbsolute as isAbsolute3, join as join45, relative as relative2, resolve as resolve5, sep } from "path";
|
|
20380
|
+
import { z } from "zod";
|
|
20381
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
20382
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
20383
|
+
|
|
20384
|
+
// src/periscope-endpoints.ts
|
|
20385
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
20386
|
+
import { chmodSync as chmodSync5, existsSync as existsSync41, mkdirSync as mkdirSync26, readdirSync as readdirSync18, readFileSync as readFileSync36, rmSync as rmSync19, writeFileSync as writeFileSync28 } from "fs";
|
|
20387
|
+
import { join as join44 } from "path";
|
|
20388
|
+
var secretDir = (stateDir) => join44(stateDir, "periscope-mcp");
|
|
20389
|
+
var secretFile = (stateDir, id) => join44(secretDir(stateDir), `${id}.json`);
|
|
20390
|
+
var endpointDir = (stateDir) => join44(stateDir, "periscope-endpoints");
|
|
20391
|
+
var endpointFile = (stateDir, id) => join44(endpointDir(stateDir), `${id}.json`);
|
|
20392
|
+
function mintPeriscopeSecret(stateDir, ticketRecordId) {
|
|
20393
|
+
mkdirSync26(secretDir(stateDir), { recursive: true, mode: 448 });
|
|
20394
|
+
const secret = randomBytes3(32).toString("base64url");
|
|
20395
|
+
const file = secretFile(stateDir, ticketRecordId);
|
|
20396
|
+
writeFileSync28(file, `${JSON.stringify({ secret })}
|
|
20397
|
+
`, { mode: 384 });
|
|
20398
|
+
chmodSync5(file, 384);
|
|
20399
|
+
return secret;
|
|
20400
|
+
}
|
|
20401
|
+
function readPeriscopeSecret(stateDir, ticketRecordId) {
|
|
20402
|
+
try {
|
|
20403
|
+
const v = JSON.parse(readFileSync36(secretFile(stateDir, ticketRecordId), "utf8"));
|
|
20404
|
+
return typeof v.secret === "string" && v.secret ? v.secret : void 0;
|
|
20405
|
+
} catch {
|
|
20406
|
+
return void 0;
|
|
20407
|
+
}
|
|
20408
|
+
}
|
|
20409
|
+
function removePeriscopeSecret(stateDir, ticketRecordId) {
|
|
20410
|
+
rmSync19(secretFile(stateDir, ticketRecordId), { force: true });
|
|
20411
|
+
}
|
|
20412
|
+
function readPeriscopeEndpoint(stateDir, ticketRecordId) {
|
|
20413
|
+
try {
|
|
20414
|
+
return JSON.parse(readFileSync36(endpointFile(stateDir, ticketRecordId), "utf8"));
|
|
20415
|
+
} catch {
|
|
20416
|
+
return void 0;
|
|
20417
|
+
}
|
|
20418
|
+
}
|
|
20419
|
+
function writePeriscopeEndpoint(stateDir, rec) {
|
|
20420
|
+
mkdirSync26(endpointDir(stateDir), { recursive: true });
|
|
20421
|
+
writeFileSync28(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
|
|
20422
|
+
`);
|
|
20423
|
+
}
|
|
20424
|
+
function removePeriscopeEndpointRecord(stateDir, ticketRecordId) {
|
|
20425
|
+
rmSync19(endpointFile(stateDir, ticketRecordId), { force: true });
|
|
20426
|
+
}
|
|
20427
|
+
function listPeriscopeEndpoints(stateDir) {
|
|
20428
|
+
const dir = endpointDir(stateDir);
|
|
20429
|
+
if (!existsSync41(dir)) return [];
|
|
20430
|
+
const out = [];
|
|
20431
|
+
for (const name of readdirSync18(dir)) {
|
|
20432
|
+
if (!name.endsWith(".json")) continue;
|
|
20433
|
+
const rec = readPeriscopeEndpoint(stateDir, name.slice(0, -".json".length));
|
|
20434
|
+
if (rec && typeof rec.endpointId === "string") out.push(rec);
|
|
20435
|
+
}
|
|
20436
|
+
return out.sort((a, b) => a.registeredAt.localeCompare(b.registeredAt));
|
|
20437
|
+
}
|
|
20438
|
+
function periscopeEndpointUrl(relayUrl, ticket) {
|
|
20439
|
+
return `${relayUrl.replace(/\/+$/, "")}/control/periscopes/${encodeURIComponent(ticket)}/mcp`;
|
|
20440
|
+
}
|
|
20441
|
+
async function registerPeriscopeEndpoint(d, p) {
|
|
20442
|
+
const secret = readPeriscopeSecret(d.stateDir, p.ticketRecordId);
|
|
20443
|
+
if (!secret) return { ok: false, message: `${p.ticket} has no MCP secret (it was not opened with --mcp, or has ended)` };
|
|
20444
|
+
const api = d.apiFor(p.workspaceId);
|
|
20445
|
+
if (!api) return { ok: false, message: `no API key known for workspace ${p.workspaceId}` };
|
|
20446
|
+
const url = periscopeEndpointUrl(p.relayUrl, p.ticket);
|
|
20447
|
+
const stored = readPeriscopeEndpoint(d.stateDir, p.ticketRecordId);
|
|
20448
|
+
const out = await registerEndpoint(api, p.workspaceId, stored?.endpointId, {
|
|
20449
|
+
label: `Periscope: ${d.shipName} ${p.ticket}`,
|
|
20450
|
+
url,
|
|
20451
|
+
mcpSecret: secret
|
|
20452
|
+
});
|
|
20453
|
+
if (out.kind !== "ok") {
|
|
20454
|
+
return { ok: false, message: out.kind === "forbidden" ? "registering the endpoint was refused (403): the route key lacks REGISTER_MCP_ENDPOINTS" : out.message };
|
|
20455
|
+
}
|
|
20456
|
+
writePeriscopeEndpoint(d.stateDir, {
|
|
20457
|
+
ticket: p.ticket,
|
|
20458
|
+
ticketRecordId: p.ticketRecordId,
|
|
20459
|
+
route: p.route,
|
|
20460
|
+
workspaceId: p.workspaceId,
|
|
20461
|
+
endpointId: out.endpointId,
|
|
20462
|
+
url,
|
|
20463
|
+
registeredAt: (d.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
20464
|
+
});
|
|
20465
|
+
return { ok: true, endpointId: out.endpointId, url };
|
|
20466
|
+
}
|
|
20467
|
+
async function removeEndedPeriscopeEndpoints(d) {
|
|
20468
|
+
let removed = 0;
|
|
20469
|
+
for (const rec of listPeriscopeEndpoints(d.stateDir)) {
|
|
20470
|
+
if (readPeriscopeSecret(d.stateDir, rec.ticketRecordId)) continue;
|
|
20471
|
+
const api = d.apiFor(rec.workspaceId);
|
|
20472
|
+
if (!api) {
|
|
20473
|
+
d.warn(`no API key known for workspace ${rec.workspaceId} \u2014 cannot remove the MCP endpoint of ${rec.ticket}`);
|
|
20474
|
+
continue;
|
|
20475
|
+
}
|
|
20476
|
+
const out = await removeEndpoint(api, rec.workspaceId, rec.endpointId);
|
|
20477
|
+
if (out.kind === "ok") {
|
|
20478
|
+
removePeriscopeEndpointRecord(d.stateDir, rec.ticketRecordId);
|
|
20479
|
+
removed++;
|
|
20480
|
+
} else d.warn(`could not remove the MCP endpoint of ${rec.ticket}: ${out.kind === "forbidden" ? "403" : out.message}`);
|
|
20481
|
+
}
|
|
20482
|
+
return removed;
|
|
20483
|
+
}
|
|
20484
|
+
|
|
20485
|
+
// src/periscope-mcp.ts
|
|
20486
|
+
var PERISCOPE_HOOKS = ["test", "build", "ports", "handoff", "preview", "isolate"];
|
|
20487
|
+
var MAX_READ_BYTES = 1024 * 1024;
|
|
20488
|
+
var MAX_WRITE_BYTES = 4 * 1024 * 1024;
|
|
20489
|
+
var MAX_BODY_BYTES2 = 8 * 1024 * 1024;
|
|
20490
|
+
var MAX_HOOK_OUTPUT = 2e4;
|
|
20491
|
+
var MAX_DIFF = 2e5;
|
|
20492
|
+
var MAX_SEARCH_RESULTS = 500;
|
|
20493
|
+
var PathRefusal = class extends Error {
|
|
20494
|
+
};
|
|
20495
|
+
var isInside = (root, p) => {
|
|
20496
|
+
const rel = relative2(root, p);
|
|
20497
|
+
return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute3(rel);
|
|
20498
|
+
};
|
|
20499
|
+
async function realAncestor(p) {
|
|
20500
|
+
let cur = p;
|
|
20501
|
+
const tail3 = [];
|
|
20502
|
+
let hops = 0;
|
|
20503
|
+
for (; ; ) {
|
|
20504
|
+
try {
|
|
20505
|
+
return { real: await realpath(cur), rest: tail3.slice().reverse().join(sep) };
|
|
20506
|
+
} catch (e) {
|
|
20507
|
+
if (e.code !== "ENOENT") throw new PathRefusal(`cannot resolve ${p}`);
|
|
20508
|
+
}
|
|
20509
|
+
let isLink = false;
|
|
20510
|
+
try {
|
|
20511
|
+
isLink = (await lstat(cur)).isSymbolicLink();
|
|
20512
|
+
} catch {
|
|
20513
|
+
}
|
|
20514
|
+
if (isLink) {
|
|
20515
|
+
if (++hops > 40) throw new PathRefusal(`too many symbolic links resolving ${p}`);
|
|
20516
|
+
cur = resolve5(dirname22(cur), await readlink(cur));
|
|
20517
|
+
continue;
|
|
20518
|
+
}
|
|
20519
|
+
const parent = dirname22(cur);
|
|
20520
|
+
if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
|
|
20521
|
+
tail3.push(basename13(cur));
|
|
20522
|
+
cur = parent;
|
|
20523
|
+
}
|
|
20524
|
+
}
|
|
20525
|
+
async function resolveInWorktree(root, requested) {
|
|
20526
|
+
if (!requested || requested.includes("\0")) throw new PathRefusal("path is required");
|
|
20527
|
+
const realRoot = await realpath(root);
|
|
20528
|
+
let abs;
|
|
20529
|
+
if (isAbsolute3(requested)) {
|
|
20530
|
+
abs = resolve5(requested);
|
|
20531
|
+
if (isInside(root, abs)) abs = join45(realRoot, relative2(root, abs));
|
|
20532
|
+
} else {
|
|
20533
|
+
abs = resolve5(realRoot, requested);
|
|
20534
|
+
}
|
|
20535
|
+
if (!isInside(realRoot, abs)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
|
|
20536
|
+
const { real, rest } = await realAncestor(abs);
|
|
20537
|
+
const final = rest ? join45(real, rest) : real;
|
|
20538
|
+
if (!isInside(realRoot, final)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
|
|
20539
|
+
if (relative2(realRoot, final).split(sep).includes(".git")) throw new PathRefusal(".git is not reachable from a periscope");
|
|
20540
|
+
return final;
|
|
20541
|
+
}
|
|
20542
|
+
function globToRegExp(glob) {
|
|
20543
|
+
let re = "";
|
|
20544
|
+
for (let i = 0; i < glob.length; i++) {
|
|
20545
|
+
const c = glob[i];
|
|
20546
|
+
if (c === "*") {
|
|
20547
|
+
if (glob[i + 1] === "*") {
|
|
20548
|
+
i++;
|
|
20549
|
+
if (glob[i + 1] === "/") {
|
|
20550
|
+
i++;
|
|
20551
|
+
re += "(?:.*/)?";
|
|
20552
|
+
} else re += ".*";
|
|
20553
|
+
} else re += "[^/]*";
|
|
20554
|
+
} else if (c === "?") re += "[^/]";
|
|
20555
|
+
else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
20556
|
+
}
|
|
20557
|
+
return new RegExp(`^${re}$`);
|
|
20558
|
+
}
|
|
20559
|
+
var tailText = (s, max = MAX_HOOK_OUTPUT) => s.length > max ? `\u2026(${s.length - max} earlier characters omitted)
|
|
20560
|
+
${s.slice(-max)}` : s;
|
|
20561
|
+
var text = (t, isError = false) => ({ content: [{ type: "text", text: t }], ...isError ? { isError: true } : {} });
|
|
20562
|
+
async function hookEnv(d, sc, name) {
|
|
20563
|
+
const env = { CREW_TICKET: sc.ticket, CREW_BRANCH: sc.branch };
|
|
20564
|
+
if (name !== "ports") {
|
|
20565
|
+
const ports = await d.runHook(sc, "ports", env);
|
|
20566
|
+
if (ports) Object.assign(env, parseEnvLines(ports.output));
|
|
20567
|
+
if (sc.isolated && name !== "isolate") {
|
|
20568
|
+
const iso = await d.runHook(sc, "isolate", env);
|
|
20569
|
+
if (iso && iso.code === 0) Object.assign(env, parseEnvLines(iso.output));
|
|
20570
|
+
}
|
|
20571
|
+
}
|
|
20572
|
+
if (name === "preview") env.CREW_PREVIEW_HOST = sc.host || "localhost";
|
|
20573
|
+
return env;
|
|
20574
|
+
}
|
|
20575
|
+
function createPeriscopeMcpServer(d, sc) {
|
|
20576
|
+
const server = new McpServer({ name: `crew-periscope-${sc.ticket}`, version: "0.1.0" });
|
|
20577
|
+
const root = sc.worktree;
|
|
20578
|
+
const inTree = (p) => resolveInWorktree(root, p);
|
|
20579
|
+
const rel = async (abs) => relative2(await realpath(root), abs) || ".";
|
|
20580
|
+
server.registerTool("read_file", {
|
|
20581
|
+
title: "Read File",
|
|
20582
|
+
description: "Read a text file in this periscope's worktree. `head`/`tail` return only the first/last N lines.",
|
|
20583
|
+
inputSchema: { path: z.string(), head: z.number().int().positive().optional(), tail: z.number().int().positive().optional() },
|
|
20584
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
20585
|
+
}, async ({ path, head, tail: tail3 }) => {
|
|
20586
|
+
if (head && tail3) return text("Cannot specify both head and tail", true);
|
|
20587
|
+
const abs = await inTree(path);
|
|
20588
|
+
const st = await stat(abs);
|
|
20589
|
+
if (!st.isFile()) return text(`${path} is not a file`, true);
|
|
20590
|
+
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);
|
|
20591
|
+
const content = await readFile(abs, "utf8");
|
|
20592
|
+
const lines = content.split("\n");
|
|
20593
|
+
return text(head ? lines.slice(0, head).join("\n") : tail3 ? lines.slice(-tail3).join("\n") : content);
|
|
20594
|
+
});
|
|
20595
|
+
server.registerTool("list_directory", {
|
|
20596
|
+
title: "List Directory",
|
|
20597
|
+
description: 'List the entries directly inside a directory of the worktree ("." is its root), each marked [FILE] or [DIR].',
|
|
20598
|
+
inputSchema: { path: z.string().default(".") },
|
|
20599
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
20600
|
+
}, async ({ path }) => {
|
|
20601
|
+
const abs = await inTree(path);
|
|
20602
|
+
const entries = (await readdir(abs, { withFileTypes: true })).filter((e) => e.name !== ".git");
|
|
20603
|
+
const lines = await Promise.all(entries.map(async (e) => {
|
|
20604
|
+
if (e.isDirectory()) return `[DIR] ${e.name}`;
|
|
20605
|
+
const size = (await lstat(join45(abs, e.name))).size;
|
|
20606
|
+
return `[FILE] ${e.name} (${size} bytes)`;
|
|
20607
|
+
}));
|
|
20608
|
+
return text(lines.join("\n") || "(empty)");
|
|
20609
|
+
});
|
|
20610
|
+
server.registerTool("search_files", {
|
|
20611
|
+
title: "Search Files",
|
|
20612
|
+
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.",
|
|
20613
|
+
inputSchema: { pattern: z.string() },
|
|
20614
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
20615
|
+
}, async ({ pattern }) => {
|
|
20616
|
+
const re = globToRegExp(pattern);
|
|
20617
|
+
const listing = git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]);
|
|
20618
|
+
const hits = listing.split("\0").filter((f) => f && re.test(f)).slice(0, MAX_SEARCH_RESULTS);
|
|
20619
|
+
return text(hits.length ? hits.join("\n") : "No matches found");
|
|
20620
|
+
});
|
|
20621
|
+
server.registerTool("write_file", {
|
|
20622
|
+
title: "Write File",
|
|
20623
|
+
description: "Create a file in the worktree, or replace its whole content. Parent directories are created.",
|
|
20624
|
+
inputSchema: { path: z.string(), content: z.string() },
|
|
20625
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
20626
|
+
}, async ({ path, content }) => {
|
|
20627
|
+
if (Buffer.byteLength(content) > MAX_WRITE_BYTES) return text(`content exceeds ${MAX_WRITE_BYTES} bytes`, true);
|
|
20628
|
+
const abs = await inTree(path);
|
|
20629
|
+
if (abs === await realpath(root)) return text("path must name a file", true);
|
|
20630
|
+
await mkdir(dirname22(abs), { recursive: true });
|
|
20631
|
+
await inTree(path);
|
|
20632
|
+
await writeFile(abs, content);
|
|
20633
|
+
return text(`Wrote ${Buffer.byteLength(content)} bytes to ${await rel(abs)}`);
|
|
20634
|
+
});
|
|
20635
|
+
server.registerTool("edit_file", {
|
|
20636
|
+
title: "Edit File",
|
|
20637
|
+
description: "Replace an exact string in a file. `oldString` must occur exactly once unless `replaceAll` is true; whitespace and line endings must match.",
|
|
20638
|
+
inputSchema: { path: z.string(), oldString: z.string().min(1), newString: z.string(), replaceAll: z.boolean().optional() },
|
|
20639
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
20640
|
+
}, async ({ path, oldString, newString, replaceAll }) => {
|
|
20641
|
+
const abs = await inTree(path);
|
|
20642
|
+
const st = await stat(abs);
|
|
20643
|
+
if (!st.isFile()) return text(`${path} is not a file`, true);
|
|
20644
|
+
if (st.size > MAX_WRITE_BYTES) return text(`${path} is too large to edit`, true);
|
|
20645
|
+
const before = await readFile(abs, "utf8");
|
|
20646
|
+
const count = before.split(oldString).length - 1;
|
|
20647
|
+
if (count === 0) return text(`oldString was not found in ${path}`, true);
|
|
20648
|
+
if (count > 1 && !replaceAll) return text(`oldString occurs ${count} times in ${path}; make it unique or set replaceAll`, true);
|
|
20649
|
+
const after = replaceAll ? before.split(oldString).join(newString) : before.replace(oldString, () => newString);
|
|
20650
|
+
await writeFile(abs, after);
|
|
20651
|
+
return text(`Edited ${await rel(abs)} (${replaceAll ? count : 1} replacement${(replaceAll ? count : 1) === 1 ? "" : "s"})`);
|
|
20652
|
+
});
|
|
20653
|
+
server.registerTool("create_directory", {
|
|
20654
|
+
title: "Create Directory",
|
|
20655
|
+
description: "Create a directory (and any missing parents) in the worktree.",
|
|
20656
|
+
inputSchema: { path: z.string() },
|
|
20657
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
20658
|
+
}, async ({ path }) => {
|
|
20659
|
+
const abs = await inTree(path);
|
|
20660
|
+
await mkdir(abs, { recursive: true });
|
|
20661
|
+
await inTree(path);
|
|
20662
|
+
return text(`Created ${await rel(abs)}`);
|
|
20663
|
+
});
|
|
20664
|
+
server.registerTool("delete_file", {
|
|
20665
|
+
title: "Delete File",
|
|
20666
|
+
description: "Delete one file (or symlink) from the worktree. Directories are not deleted.",
|
|
20667
|
+
inputSchema: { path: z.string() },
|
|
20668
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
20669
|
+
}, async ({ path }) => {
|
|
20670
|
+
const parent = await inTree(dirname22(path) === "" ? "." : dirname22(path));
|
|
20671
|
+
const abs = join45(parent, basename13(path));
|
|
20672
|
+
if (basename13(path) === ".git" || abs === parent) return text("path must name a file", true);
|
|
20673
|
+
const st = await lstat(abs).catch(() => void 0);
|
|
20674
|
+
if (!st) return text(`${path} does not exist`, true);
|
|
20675
|
+
if (st.isDirectory()) return text(`${path} is a directory; only files are deleted`, true);
|
|
20676
|
+
await rm(abs, { force: false });
|
|
20677
|
+
return text(`Deleted ${await rel(abs)}`);
|
|
20678
|
+
});
|
|
20679
|
+
server.registerTool("run_hook", {
|
|
20680
|
+
title: "Run Hook",
|
|
20681
|
+
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.`,
|
|
20682
|
+
inputSchema: { name: z.enum(PERISCOPE_HOOKS) },
|
|
20683
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
20684
|
+
}, async ({ name }) => {
|
|
20685
|
+
if (name === "isolate" && !sc.isolated) return text("isolate is allowed only for a periscope opened with --isolate", true);
|
|
20686
|
+
const env = await hookEnv(d, sc, name);
|
|
20687
|
+
const res = await d.runHook(sc, name, env);
|
|
20688
|
+
if (!res) return text(`${sc.repo} declares no \`${name}\` hook`, true);
|
|
20689
|
+
return text(`exit ${res.code}
|
|
20690
|
+
${tailText(res.output)}`, res.code !== 0);
|
|
20691
|
+
});
|
|
20692
|
+
const branchGuard = () => {
|
|
20693
|
+
const head = git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
20694
|
+
return head === sc.branch ? void 0 : `the worktree is on ${head}, not the ticket branch ${sc.branch}`;
|
|
20695
|
+
};
|
|
20696
|
+
server.registerTool("git_status", {
|
|
20697
|
+
title: "Git Status",
|
|
20698
|
+
description: "Branch and changed files (`git status --short --branch`) of this worktree.",
|
|
20699
|
+
inputSchema: {},
|
|
20700
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
20701
|
+
}, async () => text(git(root, ["status", "--short", "--branch", "--untracked-files=all"])));
|
|
20702
|
+
server.registerTool("git_diff", {
|
|
20703
|
+
title: "Git Diff",
|
|
20704
|
+
description: "The uncommitted changes against HEAD (staged and unstaged), optionally limited to one path. New untracked files appear in git_status, not here.",
|
|
20705
|
+
inputSchema: { path: z.string().optional() },
|
|
20706
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
20707
|
+
}, async ({ path }) => {
|
|
20708
|
+
const args = ["diff", "HEAD", "--no-ext-diff", "--no-color"];
|
|
20709
|
+
if (path) args.push("--", relative2(await realpath(root), await inTree(path)) || ".");
|
|
20710
|
+
const out = git(root, args);
|
|
20711
|
+
return text(out ? tailText(out, MAX_DIFF) : "(no changes)");
|
|
20712
|
+
});
|
|
20713
|
+
server.registerTool("git_commit", {
|
|
20714
|
+
title: "Git Commit",
|
|
20715
|
+
description: "Stage every change in the worktree and commit it on the ticket branch. It never pushes; ending the periscope with `--keep` does.",
|
|
20716
|
+
inputSchema: { message: z.string().min(1).max(2e3) },
|
|
20717
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
20718
|
+
}, async ({ message }) => {
|
|
20719
|
+
const wrong = branchGuard();
|
|
20720
|
+
if (wrong) return text(wrong, true);
|
|
20721
|
+
git(root, ["add", "-A"]);
|
|
20722
|
+
try {
|
|
20723
|
+
git(root, ["commit", "--no-verify", "-m", message]);
|
|
20724
|
+
} catch (e) {
|
|
20725
|
+
if (e instanceof GitError && /nothing to commit|no changes added/.test(e.message)) return text("Nothing to commit", true);
|
|
20726
|
+
throw e;
|
|
20727
|
+
}
|
|
20728
|
+
return text(`Committed ${git(root, ["rev-parse", "--short", "HEAD"])} on ${sc.branch}`);
|
|
20729
|
+
});
|
|
20730
|
+
return server;
|
|
20731
|
+
}
|
|
20732
|
+
var NOT_FOUND = { error: "not found" };
|
|
20733
|
+
var json = (res, status2, body) => {
|
|
20734
|
+
const b = JSON.stringify(body);
|
|
20735
|
+
res.writeHead(status2, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(b), "Cache-Control": "no-store" });
|
|
20736
|
+
res.end(b);
|
|
20737
|
+
};
|
|
20738
|
+
async function readBody(req) {
|
|
20739
|
+
const chunks = [];
|
|
20740
|
+
let n = 0;
|
|
20741
|
+
for await (const c of req) {
|
|
20742
|
+
n += c.length;
|
|
20743
|
+
if (n > MAX_BODY_BYTES2) return null;
|
|
20744
|
+
chunks.push(c);
|
|
20745
|
+
}
|
|
20746
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
20747
|
+
}
|
|
20748
|
+
async function handlePeriscopeMcp(d, req, res, segment) {
|
|
20749
|
+
const sc = d.find(segment);
|
|
20750
|
+
const secret = sc ? readPeriscopeSecret(d.stateDir, sc.ticketRecordId) : void 0;
|
|
20751
|
+
const presented = /^Bearer (.+)$/.exec(req.headers.authorization ?? "")?.[1]?.trim() ?? "";
|
|
20752
|
+
if (!sc || !secret || !presented || !secretsMatch(presented, secret)) return json(res, 404, NOT_FOUND);
|
|
20753
|
+
if (req.method !== "POST") {
|
|
20754
|
+
res.setHeader("Allow", "POST");
|
|
20755
|
+
return json(res, 405, { jsonrpc: "2.0", error: { code: -32e3, message: "Method not allowed." }, id: null });
|
|
20756
|
+
}
|
|
20757
|
+
const raw = await readBody(req);
|
|
20758
|
+
if (raw === null) return json(res, 413, { jsonrpc: "2.0", error: { code: -32600, message: "request body too large" }, id: null });
|
|
20759
|
+
let parsed;
|
|
20760
|
+
try {
|
|
20761
|
+
parsed = raw ? JSON.parse(raw) : void 0;
|
|
20762
|
+
} catch {
|
|
20763
|
+
return json(res, 400, { jsonrpc: "2.0", error: { code: -32700, message: "parse error" }, id: null });
|
|
20764
|
+
}
|
|
20765
|
+
const isCall = (m) => !!m && typeof m === "object" && m.method === "tools/call";
|
|
20766
|
+
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}`));
|
|
20767
|
+
try {
|
|
20768
|
+
const server = createPeriscopeMcpServer(d, sc);
|
|
20769
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
20770
|
+
await server.connect(transport);
|
|
20771
|
+
res.on("close", () => {
|
|
20772
|
+
void transport.close();
|
|
20773
|
+
void server.close();
|
|
20774
|
+
});
|
|
20775
|
+
await transport.handleRequest(req, res, parsed);
|
|
20776
|
+
} catch (e) {
|
|
20777
|
+
if (!res.headersSent) json(res, 500, { jsonrpc: "2.0", error: { code: -32603, message: e.message || "internal error" }, id: null });
|
|
20778
|
+
}
|
|
20779
|
+
}
|
|
20780
|
+
function findPeriscope(sidecars, segment) {
|
|
20781
|
+
const m = /^(?:([A-Za-z]+)-)?(\d+)$/.exec(segment);
|
|
20782
|
+
if (!m) return void 0;
|
|
20783
|
+
const num = String(Number(m[2]));
|
|
20784
|
+
return sidecars.find((sc) => {
|
|
20785
|
+
const t = /^(?:([A-Za-z]+)-)?0*(\d+)$/.exec(sc.ticket);
|
|
20786
|
+
if (!t || t[2] !== num) return false;
|
|
20787
|
+
return !m[1] || !t[1] || m[1].toLowerCase() === t[1].toLowerCase();
|
|
20788
|
+
});
|
|
20789
|
+
}
|
|
20790
|
+
|
|
20309
20791
|
// src/cli.ts
|
|
20310
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
20792
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname23(process.execPath) : resolve6(dirname23(fileURLToPath(import.meta.url)), "..");
|
|
20311
20793
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
20312
20794
|
var CYCLE_SECONDS2 = 120;
|
|
20313
20795
|
var STALE_CYCLES = 3;
|
|
20796
|
+
async function duplicateLivePeriscopeProblems(route2, ship) {
|
|
20797
|
+
try {
|
|
20798
|
+
const tr = new Tracker(route2, ship);
|
|
20799
|
+
const rows = await tr.livePeriscopesAllShips();
|
|
20800
|
+
const refOf = (v) => typeof v === "string" ? v : v?.id;
|
|
20801
|
+
const counts = /* @__PURE__ */ new Map();
|
|
20802
|
+
for (const r of rows) {
|
|
20803
|
+
const t = refOf(r.issue_id);
|
|
20804
|
+
if (t) counts.set(t, (counts.get(t) ?? 0) + 1);
|
|
20805
|
+
}
|
|
20806
|
+
const dupIds = [...counts].filter(([, n]) => n > 1).map(([t]) => t);
|
|
20807
|
+
if (dupIds.length === 0) return [];
|
|
20808
|
+
const tags = new Map((await tr.ticketsByIds(dupIds)).map((t) => [t.id, t.issue_tag ?? t.issue_id]));
|
|
20809
|
+
const ships = new Map((await tr.shipRows()).map((x) => [x.id, x.name ?? x.id]));
|
|
20810
|
+
return duplicateLivePeriscopes(rows, (id) => tags.get(id) ?? id, (id) => id ? ships.get(id) ?? id : "an unknown ship", route2.route);
|
|
20811
|
+
} catch {
|
|
20812
|
+
return [];
|
|
20813
|
+
}
|
|
20814
|
+
}
|
|
20314
20815
|
function renderShipLine(s, issueLabelById) {
|
|
20315
20816
|
const name = s.name ?? "(unnamed)";
|
|
20316
20817
|
if (!s.last_seen) return `${name.padEnd(20)} never seen`;
|
|
@@ -20428,7 +20929,7 @@ No project here has what a route needs. Install the "${template.name}" library t
|
|
|
20428
20929
|
return installed.project.slug;
|
|
20429
20930
|
}
|
|
20430
20931
|
function usage(problem, requested = false) {
|
|
20431
|
-
const
|
|
20932
|
+
const text2 = `crew \u2014 a standing team of headless agents
|
|
20432
20933
|
|
|
20433
20934
|
crew poll [route] decide a cycle and report it; writes nothing
|
|
20434
20935
|
crew run [route] [--role R] run the winning role's session
|
|
@@ -20454,7 +20955,7 @@ function usage(problem, requested = false) {
|
|
|
20454
20955
|
crew reap [route] kill orphaned servers, drop worktrees for closed tickets
|
|
20455
20956
|
crew drop [route] NNN remove a merged ticket's worktree and branch
|
|
20456
20957
|
crew rotate-passenger-url [route] force a Host Passengers tunnel to reconnect with a fresh public URL
|
|
20457
|
-
crew periscope up [[route] REPO] [--topic TEXT|--issue NNN] [--isolate] [--host H] [--assignee ID] [--no-open] [--open] [--dry-run] [--json]
|
|
20958
|
+
crew periscope up [[route] REPO] [--topic TEXT|--issue NNN] [--isolate] [--serve-mcp] [--host H] [--assignee ID] [--no-open] [--open] [--dry-run] [--json]
|
|
20458
20959
|
crew periscope up --resume NNN restart the preview an idle periscope's server was stopped for
|
|
20459
20960
|
file a draft ticket, cut and prepare its worktree, start the repo's preview hook and record a Periscopes row (alias: start)
|
|
20460
20961
|
crew periscope down [[route] NNN] --keep|--discard [--allow-empty] [--json]
|
|
@@ -20507,12 +21008,12 @@ Options:
|
|
|
20507
21008
|
In watch: space pauses the stream, q quits.
|
|
20508
21009
|
`;
|
|
20509
21010
|
if (requested) {
|
|
20510
|
-
process.stdout.write(
|
|
21011
|
+
process.stdout.write(text2);
|
|
20511
21012
|
process.exit(0);
|
|
20512
21013
|
}
|
|
20513
21014
|
process.stderr.write(problem ? `crew: ${problem}
|
|
20514
21015
|
|
|
20515
|
-
${
|
|
21016
|
+
${text2}` : text2);
|
|
20516
21017
|
process.exit(2);
|
|
20517
21018
|
}
|
|
20518
21019
|
var argv = process.argv.slice(2);
|
|
@@ -20685,6 +21186,44 @@ if (command === "passenger-sync-daemon") {
|
|
|
20685
21186
|
return void 0;
|
|
20686
21187
|
}
|
|
20687
21188
|
};
|
|
21189
|
+
}, periscopeEndpointDeps = function() {
|
|
21190
|
+
return {
|
|
21191
|
+
stateDir: cfg.ship.stateDir,
|
|
21192
|
+
shipName: cfg.ship.name,
|
|
21193
|
+
apiFor: endpointApiFor(cfg.routes.filter((r) => r.enabled)),
|
|
21194
|
+
warn: (m) => emit.warn(m, { step: "passengers" })
|
|
21195
|
+
};
|
|
21196
|
+
}, periscopeMcpDeps = function() {
|
|
21197
|
+
const stateDir = cfg.ship.stateDir;
|
|
21198
|
+
return {
|
|
21199
|
+
stateDir,
|
|
21200
|
+
find: (segment) => findPeriscope(readPeriscopeSidecars(stateDir), segment),
|
|
21201
|
+
runHook: async (sc, name, env) => {
|
|
21202
|
+
const rt = cfg.routes.find((r) => r.route === sc.route && r.enabled);
|
|
21203
|
+
const repo = rt && (await resolvedRepos(rt)).find((r) => r.name === sc.repo);
|
|
21204
|
+
const script = repo?.config.hooks[name];
|
|
21205
|
+
if (!repo || !script) return null;
|
|
21206
|
+
return runScript(script, {
|
|
21207
|
+
cwd: sc.worktree,
|
|
21208
|
+
env,
|
|
21209
|
+
shell: resolveShell(repo.config.shell ?? cfg.ship.shell),
|
|
21210
|
+
timeoutMs: name === "preview" ? 18e4 : 15 * 6e4
|
|
21211
|
+
});
|
|
21212
|
+
},
|
|
21213
|
+
touch: async (sc) => {
|
|
21214
|
+
const now = Date.now();
|
|
21215
|
+
if (now - (periscopeTouched.get(sc.ticketRecordId) ?? 0) < PERISCOPE_TOUCH_EVERY_MS) return;
|
|
21216
|
+
periscopeTouched.set(sc.ticketRecordId, now);
|
|
21217
|
+
const rt = cfg.routes.find((r) => r.route === sc.route && r.enabled);
|
|
21218
|
+
if (!rt) return;
|
|
21219
|
+
const { idleMinutes } = periscopeCaps(cfg.ship);
|
|
21220
|
+
const at = new Date(now).toISOString();
|
|
21221
|
+
const expiresAt = new Date(now + idleMinutes * 6e4).toISOString();
|
|
21222
|
+
await new Tracker(rt, cfg.ship).updatePeriscope(sc.periscopeRecordId, { last_activity_at: at, expires_at: expiresAt });
|
|
21223
|
+
writePeriscopeSidecar(stateDir, { ...sc, lastActivityAt: at, expiresAt });
|
|
21224
|
+
},
|
|
21225
|
+
log: (m) => emit.warn(m, { step: "passengers" })
|
|
21226
|
+
};
|
|
20688
21227
|
}, endpointReconcileDeps = function(routes, relayHost) {
|
|
20689
21228
|
return {
|
|
20690
21229
|
stateDir: cfg.ship.stateDir,
|
|
@@ -20711,7 +21250,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20711
21250
|
const unsafe = baseBranchUnsafe(out);
|
|
20712
21251
|
return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
|
|
20713
21252
|
};
|
|
20714
|
-
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth;
|
|
21253
|
+
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, periscopeEndpointDeps2 = periscopeEndpointDeps, periscopeMcpDeps2 = periscopeMcpDeps, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth;
|
|
20715
21254
|
let cfg;
|
|
20716
21255
|
let firstRun;
|
|
20717
21256
|
let firstRunFromFlags = false;
|
|
@@ -20767,7 +21306,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20767
21306
|
} else {
|
|
20768
21307
|
const toleratesNoRoutes = command === "credential" || command === "forge" || command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
20769
21308
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
20770
|
-
if (configless && !
|
|
21309
|
+
if (configless && !existsSync42(findConfigFile(CREW_HOME))) {
|
|
20771
21310
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
20772
21311
|
} else {
|
|
20773
21312
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -20782,7 +21321,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20782
21321
|
throw e;
|
|
20783
21322
|
}
|
|
20784
21323
|
if (command === "credential") {
|
|
20785
|
-
const r = runCredential(positional[1],
|
|
21324
|
+
const r = runCredential(positional[1], readFileSync37(0, "utf8"), cfg.ship.forges, cfg.ship.stateDir);
|
|
20786
21325
|
process.stdout.write(r.out);
|
|
20787
21326
|
process.stderr.write(r.err);
|
|
20788
21327
|
process.exit(r.code);
|
|
@@ -20794,9 +21333,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
20794
21333
|
forges: cfg.ship.forges,
|
|
20795
21334
|
stateDir: cfg.ship.stateDir,
|
|
20796
21335
|
configFile: cfg.configFile,
|
|
20797
|
-
readFile: (p) =>
|
|
20798
|
-
writeFile: (p, t) =>
|
|
20799
|
-
stdin: () =>
|
|
21336
|
+
readFile: (p) => readFileSync37(p, "utf8"),
|
|
21337
|
+
writeFile: (p, t) => writeFileSync29(p, t),
|
|
21338
|
+
stdin: () => readFileSync37(0, "utf8"),
|
|
20800
21339
|
env: process.env
|
|
20801
21340
|
}
|
|
20802
21341
|
);
|
|
@@ -20928,6 +21467,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20928
21467
|
for (const plan of containerPlans) pending.push(reconcileEndpoint(plan.workspaceId, reconcile));
|
|
20929
21468
|
for (const plan of plans) pending.push(reconcileRelayUrl(plan.workspaceId, apiRoutes));
|
|
20930
21469
|
await Promise.all(pending);
|
|
21470
|
+
await reconcilePeriscopeEndpoints();
|
|
20931
21471
|
} catch (e) {
|
|
20932
21472
|
emit.warn(`ship tunnel sync failed: ${e.message}`, { step: "passengers" });
|
|
20933
21473
|
}
|
|
@@ -20947,6 +21487,22 @@ if (command === "passenger-sync-daemon") {
|
|
|
20947
21487
|
emit.warn(`could not write relay_url: ${e.message}`, { step: "passengers" });
|
|
20948
21488
|
}
|
|
20949
21489
|
}
|
|
21490
|
+
const periscopeTouched = /* @__PURE__ */ new Map();
|
|
21491
|
+
const PERISCOPE_TOUCH_EVERY_MS = 3e4;
|
|
21492
|
+
async function reconcilePeriscopeEndpoints() {
|
|
21493
|
+
const d = periscopeEndpointDeps();
|
|
21494
|
+
await removeEndedPeriscopeEndpoints(d);
|
|
21495
|
+
const have = new Set(listPeriscopeEndpoints(cfg.ship.stateDir).map((e) => e.ticketRecordId));
|
|
21496
|
+
for (const sc of readPeriscopeSidecars(cfg.ship.stateDir)) {
|
|
21497
|
+
if (have.has(sc.ticketRecordId) || !sc.route || !readPeriscopeSecret(cfg.ship.stateDir, sc.ticketRecordId)) continue;
|
|
21498
|
+
const rt = cfg.routes.find((r) => r.route === sc.route && r.enabled);
|
|
21499
|
+
const ws = rt?.resolved?.workspaceId;
|
|
21500
|
+
const relayUrl = rt && routeRelayUrl(rt);
|
|
21501
|
+
if (!rt || !ws || !relayUrl) continue;
|
|
21502
|
+
const out = await registerPeriscopeEndpoint(d, { ticket: sc.ticket, ticketRecordId: sc.ticketRecordId, route: sc.route, workspaceId: ws, relayUrl });
|
|
21503
|
+
if (!out.ok) d.warn(`could not register the MCP endpoint of ${sc.ticket}: ${out.message}`);
|
|
21504
|
+
}
|
|
21505
|
+
}
|
|
20950
21506
|
async function releaseFleet(opts = {}) {
|
|
20951
21507
|
if (releaseRefused()) return;
|
|
20952
21508
|
precheckTickets.clear();
|
|
@@ -21062,8 +21618,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
21062
21618
|
}
|
|
21063
21619
|
if (host === "linux" && hasSystemd) {
|
|
21064
21620
|
const label = labelFor(CREW_HOME, "run");
|
|
21065
|
-
const staleTimer =
|
|
21066
|
-
if (
|
|
21621
|
+
const staleTimer = join46(homedir10(), ".config", "systemd", "user", `${label}.timer`);
|
|
21622
|
+
if (existsSync42(staleTimer)) {
|
|
21067
21623
|
process.stdout.write(
|
|
21068
21624
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
21069
21625
|
`
|
|
@@ -21090,7 +21646,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21090
21646
|
const tracker = new Tracker(c, cfg.ship);
|
|
21091
21647
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
21092
21648
|
for (const r of await resolvedRepos(c)) {
|
|
21093
|
-
if (!
|
|
21649
|
+
if (!existsSync42(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
21094
21650
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
21095
21651
|
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
21096
21652
|
}
|
|
@@ -21108,7 +21664,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21108
21664
|
}
|
|
21109
21665
|
async function doctorFix(o) {
|
|
21110
21666
|
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))));
|
|
21111
|
-
const unitExists = (p) =>
|
|
21667
|
+
const unitExists = (p) => existsSync42(p);
|
|
21112
21668
|
const crontab = o.host === "linux" ? spawnSync8("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
21113
21669
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run", installContext()), unitExists, crontab) : true;
|
|
21114
21670
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -21125,7 +21681,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21125
21681
|
for (const a of plan.actions) {
|
|
21126
21682
|
if (a.kind === "enable") {
|
|
21127
21683
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
21128
|
-
|
|
21684
|
+
writeFileSync29(cfg.configFile, planEnable(readFileSync37(cfg.configFile, "utf8"), a.route));
|
|
21129
21685
|
enabledSound.add(a.route);
|
|
21130
21686
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
21131
21687
|
} else {
|
|
@@ -22246,7 +22802,7 @@ ${cu.tail.trimEnd()}
|
|
|
22246
22802
|
process.exit(1);
|
|
22247
22803
|
}
|
|
22248
22804
|
requireArmed("run the daemon loop");
|
|
22249
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
22805
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join46(CREW_HOME, "src"), join46(CREW_HOME, "dist"), join46(CREW_HOME, "bin")];
|
|
22250
22806
|
const configPaths = daemonConfigPaths(cfg);
|
|
22251
22807
|
watchPaths.push(...configPaths);
|
|
22252
22808
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -22274,8 +22830,22 @@ ${cu.tail.trimEnd()}
|
|
|
22274
22830
|
previewHost: cfg.ship.previewHost,
|
|
22275
22831
|
maxPeriscopes: cfg.ship.maxPeriscopes,
|
|
22276
22832
|
run: runCrew,
|
|
22277
|
-
verifyIn: (token, ws) => controlAuth.verifyIn(token, ws)
|
|
22833
|
+
verifyIn: (token, ws) => controlAuth.verifyIn(token, ws),
|
|
22834
|
+
mcp: {
|
|
22835
|
+
onUp: async (r, done) => {
|
|
22836
|
+
const id = typeof done.ticketRecordId === "string" ? done.ticketRecordId : "";
|
|
22837
|
+
const ticket = typeof done.ticket === "string" ? done.ticket : "";
|
|
22838
|
+
const relayUrl = routeRelayUrl(cfg.routes.find((x) => x.route === r.route));
|
|
22839
|
+
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" };
|
|
22840
|
+
const out = await registerPeriscopeEndpoint(periscopeEndpointDeps(), { ticket, ticketRecordId: id, route: r.route, workspaceId: r.workspaceId, relayUrl });
|
|
22841
|
+
return out.ok ? { mcpEndpointId: out.endpointId, mcpUrl: out.url } : { mcpEndpointId: null, mcpWarning: out.message };
|
|
22842
|
+
},
|
|
22843
|
+
onDown: async () => {
|
|
22844
|
+
await removeEndedPeriscopeEndpoints(periscopeEndpointDeps());
|
|
22845
|
+
}
|
|
22846
|
+
}
|
|
22278
22847
|
},
|
|
22848
|
+
periscopeMcp: controlRoutes.some((r) => r.enabled && r.hostPeriscopes) ? (req, res, ticket) => handlePeriscopeMcp(periscopeMcpDeps(), req, res, ticket) : void 0,
|
|
22279
22849
|
status: () => crewJson(["status", route2.route, "--json"]),
|
|
22280
22850
|
doctor: () => crewJson(["doctor", route2.route, "--json", "--offline"]),
|
|
22281
22851
|
scheduler: schedulerHandler(planDaemonControl(CREW_HOME, hostPlatform(), detectSystemd())),
|
|
@@ -22507,10 +23077,10 @@ ${cu.tail.trimEnd()}
|
|
|
22507
23077
|
`);
|
|
22508
23078
|
if (!dryRun) {
|
|
22509
23079
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
22510
|
-
const raw =
|
|
23080
|
+
const raw = existsSync42(resolvedPath) ? JSON.parse(readFileSync37(resolvedPath, "utf8")) : {};
|
|
22511
23081
|
raw.agentSkills = result.agentSkills;
|
|
22512
|
-
|
|
22513
|
-
|
|
23082
|
+
mkdirSync27(dirOf2(resolvedPath), { recursive: true });
|
|
23083
|
+
writeFileSync29(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
22514
23084
|
`);
|
|
22515
23085
|
}
|
|
22516
23086
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -22533,7 +23103,7 @@ ${cu.tail.trimEnd()}
|
|
|
22533
23103
|
throw e;
|
|
22534
23104
|
}
|
|
22535
23105
|
const oldName = cfg.ship.name;
|
|
22536
|
-
|
|
23106
|
+
writeFileSync29(cfg.configFile, setShipNameInYaml(readFileSync37(cfg.configFile, "utf8"), newName));
|
|
22537
23107
|
cfg.ship.name = newName;
|
|
22538
23108
|
let renameMachineId;
|
|
22539
23109
|
try {
|
|
@@ -22580,14 +23150,14 @@ ${cu.tail.trimEnd()}
|
|
|
22580
23150
|
process.exit(2);
|
|
22581
23151
|
};
|
|
22582
23152
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
|
|
22583
|
-
const configText =
|
|
23153
|
+
const configText = readFileSync37(cfg.configFile, "utf8");
|
|
22584
23154
|
if (sub === "check") {
|
|
22585
23155
|
try {
|
|
22586
23156
|
const routeArg = value("route") ?? positional[2];
|
|
22587
23157
|
const found = cfg.routes.find((r) => r.route === routeArg);
|
|
22588
23158
|
if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
|
|
22589
23159
|
const named2 = value("route") ? void 0 : positional[3];
|
|
22590
|
-
const dir = named2 ? repoDirOf(configText,
|
|
23160
|
+
const dir = named2 ? repoDirOf(configText, dirname23(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
|
|
22591
23161
|
const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
|
|
22592
23162
|
const o = target ? found.repoOverrides[target.name] : void 0;
|
|
22593
23163
|
runReposCheck(dir, {
|
|
@@ -22610,17 +23180,17 @@ ${cu.tail.trimEnd()}
|
|
|
22610
23180
|
if (sub === "hooks") {
|
|
22611
23181
|
try {
|
|
22612
23182
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
22613
|
-
const dir = repoDirOf(configText,
|
|
22614
|
-
if (!
|
|
22615
|
-
const files = { exists: (r) =>
|
|
23183
|
+
const dir = repoDirOf(configText, dirname23(cfg.configFile), a.route, a.name);
|
|
23184
|
+
if (!existsSync42(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
23185
|
+
const files = { exists: (r) => existsSync42(resolvePath(dir, r)), list: (r) => {
|
|
22616
23186
|
try {
|
|
22617
|
-
return
|
|
23187
|
+
return readdirSync19(resolvePath(dir, r));
|
|
22618
23188
|
} catch {
|
|
22619
23189
|
return [];
|
|
22620
23190
|
}
|
|
22621
23191
|
}, read: (r) => {
|
|
22622
23192
|
try {
|
|
22623
|
-
return
|
|
23193
|
+
return readFileSync37(resolvePath(dir, r), "utf8");
|
|
22624
23194
|
} catch {
|
|
22625
23195
|
return void 0;
|
|
22626
23196
|
}
|
|
@@ -22662,19 +23232,19 @@ ${cu.tail.trimEnd()}
|
|
|
22662
23232
|
let cleared;
|
|
22663
23233
|
if (clear2) {
|
|
22664
23234
|
const plan = clearRouteHooks(configText, a.route, a.name);
|
|
22665
|
-
if (plan.removed.length > 0)
|
|
23235
|
+
if (plan.removed.length > 0) writeFileSync29(cfg.configFile, plan.text);
|
|
22666
23236
|
cleared = { file: cfg.configFile, removed: plan.removed };
|
|
22667
23237
|
}
|
|
22668
23238
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
22669
23239
|
if (write === "route") {
|
|
22670
23240
|
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
|
|
22671
|
-
|
|
23241
|
+
writeFileSync29(cfg.configFile, plan.text);
|
|
22672
23242
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
|
|
22673
23243
|
} else if (write === "repo") {
|
|
22674
23244
|
const have = existingRepoConfig(files);
|
|
22675
23245
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
22676
23246
|
const file = resolvePath(dir, ".crew.yaml");
|
|
22677
|
-
|
|
23247
|
+
writeFileSync29(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
22678
23248
|
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
|
|
22679
23249
|
}
|
|
22680
23250
|
if (flag("json")) {
|
|
@@ -22730,7 +23300,7 @@ ${cu.tail.trimEnd()}
|
|
|
22730
23300
|
if (sub === "list" && !routeName) usage2();
|
|
22731
23301
|
if (sub === "add") {
|
|
22732
23302
|
try {
|
|
22733
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
23303
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync42(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
|
|
22734
23304
|
routeName = a.route;
|
|
22735
23305
|
addPath = a.path;
|
|
22736
23306
|
addName = a.name;
|
|
@@ -22759,66 +23329,66 @@ ${cu.tail.trimEnd()}
|
|
|
22759
23329
|
}
|
|
22760
23330
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
22761
23331
|
let resolvedRemotes;
|
|
22762
|
-
if (
|
|
23332
|
+
if (existsSync42(resolvedFile)) {
|
|
22763
23333
|
try {
|
|
22764
|
-
resolvedRemotes = JSON.parse(
|
|
23334
|
+
resolvedRemotes = JSON.parse(readFileSync37(resolvedFile, "utf8"));
|
|
22765
23335
|
} catch {
|
|
22766
23336
|
}
|
|
22767
23337
|
}
|
|
22768
23338
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
22769
|
-
const
|
|
22770
|
-
const base =
|
|
22771
|
-
const isCheckout = (d) =>
|
|
23339
|
+
const text2 = configText;
|
|
23340
|
+
const base = dirname23(cfg.configFile);
|
|
23341
|
+
const isCheckout = (d) => existsSync42(`${d}/.git`);
|
|
22772
23342
|
try {
|
|
22773
23343
|
if (sub === "list") {
|
|
22774
23344
|
if (flag("json")) {
|
|
22775
23345
|
const rows = listRepoRows({
|
|
22776
|
-
text,
|
|
23346
|
+
text: text2,
|
|
22777
23347
|
route: routeName,
|
|
22778
23348
|
base,
|
|
22779
23349
|
resolved: resolvedRemotes,
|
|
22780
23350
|
originOf,
|
|
22781
|
-
exists: (d) =>
|
|
23351
|
+
exists: (d) => existsSync42(d),
|
|
22782
23352
|
isCheckout,
|
|
22783
23353
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
22784
23354
|
try {
|
|
22785
|
-
return Object.keys(parseRepoConfig(
|
|
23355
|
+
return Object.keys(parseRepoConfig(readFileSync37(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
22786
23356
|
} catch {
|
|
22787
23357
|
return false;
|
|
22788
23358
|
}
|
|
22789
23359
|
})
|
|
22790
23360
|
});
|
|
22791
|
-
process.stdout.write(`${JSON.stringify({ route: routeName, repos: rows, cloneBase: cloneBaseOf(
|
|
23361
|
+
process.stdout.write(`${JSON.stringify({ route: routeName, repos: rows, cloneBase: cloneBaseOf(text2, routeName, base, isCheckout) })}
|
|
22792
23362
|
`);
|
|
22793
23363
|
break;
|
|
22794
23364
|
}
|
|
22795
|
-
const cloneBase = cloneBaseOf(
|
|
22796
|
-
const repos = configuredRepos(
|
|
23365
|
+
const cloneBase = cloneBaseOf(text2, routeName, base, isCheckout);
|
|
23366
|
+
const repos = configuredRepos(text2, routeName, base).filter((r) => r.dir !== cloneBase);
|
|
22797
23367
|
if (repos.length === 0) process.stdout.write(`route ${routeName} has no checkout configured \u2014 \`crew repos add ${routeName} <path>\`
|
|
22798
23368
|
`);
|
|
22799
23369
|
for (const r of repos) {
|
|
22800
23370
|
const origin2 = originOf(r.dir);
|
|
22801
23371
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
22802
|
-
const note = !
|
|
23372
|
+
const note = !existsSync42(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
22803
23373
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
22804
23374
|
`);
|
|
22805
23375
|
}
|
|
22806
23376
|
break;
|
|
22807
23377
|
}
|
|
22808
23378
|
if (sub === "clone") {
|
|
22809
|
-
const
|
|
22810
|
-
const say = (event, extra, human) => process.stdout.write(
|
|
23379
|
+
const json2 = flag("json");
|
|
23380
|
+
const say = (event, extra, human) => process.stdout.write(json2 ? `${JSON.stringify({ event, ...extra })}
|
|
22811
23381
|
` : `${human}
|
|
22812
23382
|
`);
|
|
22813
|
-
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
22814
|
-
if (
|
|
23383
|
+
const plan2 = planRepoClone({ text: text2, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
23384
|
+
if (existsSync42(plan2.target) && (existsSync42(`${plan2.target}/.git`) || readdirSync19(plan2.target).length > 0)) {
|
|
22815
23385
|
throw new ReposError(
|
|
22816
|
-
|
|
23386
|
+
existsSync42(`${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`
|
|
22817
23387
|
);
|
|
22818
23388
|
}
|
|
22819
23389
|
if (dryRun) {
|
|
22820
23390
|
say("dry-run", { remote: plan2.remote, target: plan2.target }, `would clone ${plan2.remote} into ${plan2.target}`);
|
|
22821
|
-
if (!
|
|
23391
|
+
if (!json2) process.stdout.write(`${plan2.block}
|
|
22822
23392
|
`);
|
|
22823
23393
|
break;
|
|
22824
23394
|
}
|
|
@@ -22831,18 +23401,18 @@ ${cu.tail.trimEnd()}
|
|
|
22831
23401
|
throw new ReposError(`${e.message}${authHint}
|
|
22832
23402
|
crew.yaml was not changed`);
|
|
22833
23403
|
}
|
|
22834
|
-
|
|
23404
|
+
writeFileSync29(cfg.configFile, plan2.text);
|
|
22835
23405
|
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:
|
|
22836
23406
|
`);
|
|
22837
23407
|
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}`);
|
|
22838
23408
|
break;
|
|
22839
23409
|
}
|
|
22840
23410
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
|
|
22841
|
-
const top =
|
|
23411
|
+
const top = existsSync42(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
22842
23412
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
22843
23413
|
const origin = originOf(top);
|
|
22844
23414
|
const plan = planRepoAdd({
|
|
22845
|
-
text,
|
|
23415
|
+
text: text2,
|
|
22846
23416
|
base,
|
|
22847
23417
|
route: routeName,
|
|
22848
23418
|
path: top,
|
|
@@ -22890,15 +23460,15 @@ ${cu.tail.trimEnd()}
|
|
|
22890
23460
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
22891
23461
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
22892
23462
|
`);
|
|
22893
|
-
if (
|
|
22894
|
-
const raw = JSON.parse(
|
|
23463
|
+
if (existsSync42(resolvedFile)) {
|
|
23464
|
+
const raw = JSON.parse(readFileSync37(resolvedFile, "utf8"));
|
|
22895
23465
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
22896
23466
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
22897
|
-
|
|
23467
|
+
writeFileSync29(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
22898
23468
|
`);
|
|
22899
23469
|
}
|
|
22900
23470
|
}
|
|
22901
|
-
|
|
23471
|
+
writeFileSync29(cfg.configFile, plan.text);
|
|
22902
23472
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
22903
23473
|
`);
|
|
22904
23474
|
} catch (e) {
|
|
@@ -23265,7 +23835,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23265
23835
|
process.exit(2);
|
|
23266
23836
|
}
|
|
23267
23837
|
if (found.shipNameAdopted && !dryRun) {
|
|
23268
|
-
|
|
23838
|
+
writeFileSync29(cfg.configFile, setShipNameInYaml(readFileSync37(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
23269
23839
|
cfg.ship.name = found.shipNameAdopted;
|
|
23270
23840
|
}
|
|
23271
23841
|
if (found.shipRow) {
|
|
@@ -23308,10 +23878,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23308
23878
|
}
|
|
23309
23879
|
}
|
|
23310
23880
|
let previous;
|
|
23311
|
-
const hadPreviousFile =
|
|
23881
|
+
const hadPreviousFile = existsSync42(resolvedPath);
|
|
23312
23882
|
if (hadPreviousFile) {
|
|
23313
23883
|
try {
|
|
23314
|
-
previous = JSON.parse(
|
|
23884
|
+
previous = JSON.parse(readFileSync37(resolvedPath, "utf8"));
|
|
23315
23885
|
} catch {
|
|
23316
23886
|
}
|
|
23317
23887
|
}
|
|
@@ -23441,10 +24011,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23441
24011
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
23442
24012
|
`);
|
|
23443
24013
|
} else {
|
|
23444
|
-
|
|
24014
|
+
mkdirSync27(dirname23(resolvedPath), { recursive: true });
|
|
23445
24015
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
23446
24016
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
23447
|
-
|
|
24017
|
+
writeFileSync29(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
23448
24018
|
`);
|
|
23449
24019
|
renameSync4(tmpPath, resolvedPath);
|
|
23450
24020
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -23490,8 +24060,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23490
24060
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
23491
24061
|
`);
|
|
23492
24062
|
} else {
|
|
23493
|
-
|
|
23494
|
-
|
|
24063
|
+
mkdirSync27(dirname23(apiKeyFile), { recursive: true });
|
|
24064
|
+
writeFileSync29(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
23495
24065
|
`, { mode: 384 });
|
|
23496
24066
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
23497
24067
|
`);
|
|
@@ -24178,6 +24748,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
24178
24748
|
},
|
|
24179
24749
|
setRowState: (id, state2) => trD.updatePeriscope(id, { state: state2 }),
|
|
24180
24750
|
forgetLocal: (id) => {
|
|
24751
|
+
removePeriscopeSecret(cfg.ship.stateDir, id);
|
|
24181
24752
|
removePeriscopeSidecar(cfg.ship.stateDir, id);
|
|
24182
24753
|
removePeriscopePrior(cfg.ship.stateDir, id);
|
|
24183
24754
|
},
|
|
@@ -24226,7 +24797,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
24226
24797
|
const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
|
|
24227
24798
|
const resumed = await runPeriscopeResume({
|
|
24228
24799
|
find: async () => hit && { row: hit.row, tag: hit.tag },
|
|
24229
|
-
worktreeExists: (p) =>
|
|
24800
|
+
worktreeExists: (p) => existsSync42(p),
|
|
24230
24801
|
caps: periscopeCaps(cfg.ship),
|
|
24231
24802
|
runHook: async (name, cwd, env) => {
|
|
24232
24803
|
const script = hit?.repo.config.hooks[name];
|
|
@@ -24320,10 +24891,10 @@ ${where.join("\n")}`);
|
|
|
24320
24891
|
},
|
|
24321
24892
|
prepare: async (path) => {
|
|
24322
24893
|
for (const f of repoP.config.worktrees.copy) {
|
|
24323
|
-
const src =
|
|
24324
|
-
if (!
|
|
24325
|
-
|
|
24326
|
-
copyFileSync2(src,
|
|
24894
|
+
const src = join46(repoP.dir, f);
|
|
24895
|
+
if (!existsSync42(src)) continue;
|
|
24896
|
+
mkdirSync27(dirOf2(join46(path, f)), { recursive: true });
|
|
24897
|
+
copyFileSync2(src, join46(path, f));
|
|
24327
24898
|
}
|
|
24328
24899
|
const setup = repoP.config.hooks.setup;
|
|
24329
24900
|
return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
|
|
@@ -24335,7 +24906,10 @@ ${where.join("\n")}`);
|
|
|
24335
24906
|
return script ? runScript(script, { cwd, env, shell: shellP, timeoutMs: name === "preview" ? 18e4 : void 0 }) : null;
|
|
24336
24907
|
},
|
|
24337
24908
|
createRow: (fields) => trackerP.createPeriscope(fields),
|
|
24338
|
-
recordLocal: (sc) =>
|
|
24909
|
+
recordLocal: (sc) => {
|
|
24910
|
+
writePeriscopeSidecar(cfg.ship.stateDir, { ...sc, route: routeP.route });
|
|
24911
|
+
if (flag("serve-mcp")) mintPeriscopeSecret(cfg.ship.stateDir, sc.ticketRecordId);
|
|
24912
|
+
},
|
|
24339
24913
|
comment: (ticketId, body) => trackerP.postComment(ticketId, body, memberId),
|
|
24340
24914
|
teardown: async (path, branch) => {
|
|
24341
24915
|
const n = Number(path.match(/(\d+)\/?$/)?.[1]);
|
|
@@ -24371,6 +24945,10 @@ ${where.join("\n")}`);
|
|
|
24371
24945
|
const row = crewP.find((r) => r.id === assigneeId);
|
|
24372
24946
|
return row?.agent_id ? row.name ?? assigneeId : void 0;
|
|
24373
24947
|
},
|
|
24948
|
+
liveRows: async (ticketRecordId) => {
|
|
24949
|
+
const [rows, ships] = [await trackerP.livePeriscopesForTicket(ticketRecordId), await trackerP.shipRows()];
|
|
24950
|
+
return rows.map((r) => ({ ship: ships.find((x) => x.id === r.ship_id)?.name ?? r.ship_id ?? "an unknown ship", url: r.url ?? "", worktree: r.worktree }));
|
|
24951
|
+
},
|
|
24374
24952
|
hasLiveRun: (ticketRecordId) => liveRuns(cfg.ship.stateDir).some((r) => r.ticketRecordId === ticketRecordId),
|
|
24375
24953
|
hold: async (id) => {
|
|
24376
24954
|
const auth = await trackerP.authorshipProbe();
|
|
@@ -24394,6 +24972,7 @@ ${where.join("\n")}`);
|
|
|
24394
24972
|
if (a.issue) {
|
|
24395
24973
|
const t = await deps.existing.load(a.issue);
|
|
24396
24974
|
if (!t) throw new PeriscopeError(`no ticket numbered ${a.issue} on this route`);
|
|
24975
|
+
await assertNoLivePeriscope(deps.existing, t, a.issue);
|
|
24397
24976
|
const refused = await existingRefusal(deps.existing, t);
|
|
24398
24977
|
if (refused) throw new PeriscopeError(refused);
|
|
24399
24978
|
say("dry-run", `would hold ${t.tag} (status ${t.status} stays) for ${pairId}, cut or reuse its worktree for ${a.repo}, run setup${a.isolate ? ", isolate" : ""}, ports, handoff and preview on ${deps.host}, and record a Periscopes row`);
|
|
@@ -24417,6 +24996,8 @@ ${where.join("\n")}`);
|
|
|
24417
24996
|
} catch (e) {
|
|
24418
24997
|
if (e instanceof PeriscopeError || e instanceof ConfigError) {
|
|
24419
24998
|
process.stderr.write(`crew periscope: ${e.message}
|
|
24999
|
+
`);
|
|
25000
|
+
if (e instanceof LivePeriscopeError && jsonP) process.stdout.write(`${JSON.stringify(e.data)}
|
|
24420
25001
|
`);
|
|
24421
25002
|
process.exit(1);
|
|
24422
25003
|
}
|
|
@@ -24606,7 +25187,7 @@ ${where.join("\n")}`);
|
|
|
24606
25187
|
};
|
|
24607
25188
|
const droppedRoutes = [];
|
|
24608
25189
|
try {
|
|
24609
|
-
const rawCfg = parseYaml2(
|
|
25190
|
+
const rawCfg = parseYaml2(readFileSync37(cfg.configFile, "utf8"));
|
|
24610
25191
|
for (const r of rawCfg?.routes ?? []) {
|
|
24611
25192
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
24612
25193
|
try {
|
|
@@ -24648,8 +25229,8 @@ ${where.join("\n")}`);
|
|
|
24648
25229
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
24649
25230
|
break;
|
|
24650
25231
|
}
|
|
24651
|
-
|
|
24652
|
-
|
|
25232
|
+
mkdirSync27(cfg.ship.stateDir, { recursive: true });
|
|
25233
|
+
writeFileSync29(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
24653
25234
|
`);
|
|
24654
25235
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
24655
25236
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -24700,8 +25281,8 @@ ${where.join("\n")}`);
|
|
|
24700
25281
|
}
|
|
24701
25282
|
case "log": {
|
|
24702
25283
|
try {
|
|
24703
|
-
const
|
|
24704
|
-
process.stdout.write(`${
|
|
25284
|
+
const text2 = readFileSync37(cfg.ship.logFile, "utf8").split("\n");
|
|
25285
|
+
process.stdout.write(`${text2.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
24705
25286
|
`);
|
|
24706
25287
|
} catch {
|
|
24707
25288
|
process.stdout.write(`no log yet at ${cfg.ship.logFile}
|
|
@@ -24710,12 +25291,12 @@ ${where.join("\n")}`);
|
|
|
24710
25291
|
break;
|
|
24711
25292
|
}
|
|
24712
25293
|
case "sample-ticket": {
|
|
24713
|
-
const
|
|
24714
|
-
const say = (event, extra, human) => process.stdout.write(
|
|
25294
|
+
const json2 = flag("json");
|
|
25295
|
+
const say = (event, extra, human) => process.stdout.write(json2 ? `${JSON.stringify({ event, ...extra })}
|
|
24715
25296
|
` : `${human}
|
|
24716
25297
|
`);
|
|
24717
25298
|
const fail = (msg) => {
|
|
24718
|
-
if (
|
|
25299
|
+
if (json2) process.stdout.write(`${JSON.stringify({ event: "error", message: msg })}
|
|
24719
25300
|
`);
|
|
24720
25301
|
else process.stderr.write(`crew sample-ticket: ${msg}
|
|
24721
25302
|
`);
|
|
@@ -24723,8 +25304,8 @@ ${where.join("\n")}`);
|
|
|
24723
25304
|
};
|
|
24724
25305
|
if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
|
|
24725
25306
|
const tracker = new Tracker(route2, cfg.ship);
|
|
24726
|
-
const configText =
|
|
24727
|
-
const cfgBase =
|
|
25307
|
+
const configText = readFileSync37(cfg.configFile, "utf8");
|
|
25308
|
+
const cfgBase = dirname23(cfg.configFile);
|
|
24728
25309
|
const configured2 = configuredRepos(configText, route2.route, cfgBase);
|
|
24729
25310
|
const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
|
|
24730
25311
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
|
|
@@ -24752,7 +25333,7 @@ ${where.join("\n")}`);
|
|
|
24752
25333
|
}
|
|
24753
25334
|
if (configured2.some((r) => r.dir === dir)) {
|
|
24754
25335
|
try {
|
|
24755
|
-
|
|
25336
|
+
writeFileSync29(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
|
|
24756
25337
|
} catch (e) {
|
|
24757
25338
|
if (e instanceof ReposError) fail(e.message);
|
|
24758
25339
|
throw e;
|
|
@@ -24760,29 +25341,29 @@ ${where.join("\n")}`);
|
|
|
24760
25341
|
}
|
|
24761
25342
|
if (row) {
|
|
24762
25343
|
await tracker.deleteRepoRow(row.id);
|
|
24763
|
-
if (
|
|
24764
|
-
const raw = JSON.parse(
|
|
25344
|
+
if (existsSync42(resolvedFile)) {
|
|
25345
|
+
const raw = JSON.parse(readFileSync37(resolvedFile, "utf8"));
|
|
24765
25346
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
24766
25347
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
24767
|
-
|
|
25348
|
+
writeFileSync29(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
24768
25349
|
`);
|
|
24769
25350
|
}
|
|
24770
25351
|
}
|
|
24771
|
-
if (
|
|
25352
|
+
if (existsSync42(join46(dir, ".git")) && basename14(dir) === SAMPLE_REPO) rmSync20(dir, { recursive: true, force: true });
|
|
24772
25353
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
24773
25354
|
break;
|
|
24774
25355
|
}
|
|
24775
25356
|
const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
24776
|
-
const fresh = !
|
|
24777
|
-
if (
|
|
25357
|
+
const fresh = !existsSync42(join46(dir, ".git"));
|
|
25358
|
+
if (existsSync42(dir) && fresh && readdirSync19(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
24778
25359
|
if (dryRun) {
|
|
24779
25360
|
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`);
|
|
24780
25361
|
break;
|
|
24781
25362
|
}
|
|
24782
25363
|
if (fresh) {
|
|
24783
|
-
|
|
24784
|
-
|
|
24785
|
-
|
|
25364
|
+
mkdirSync27(dir, { recursive: true });
|
|
25365
|
+
writeFileSync29(join46(dir, "README.md"), SAMPLE_README);
|
|
25366
|
+
writeFileSync29(join46(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
24786
25367
|
const g = (...a) => {
|
|
24787
25368
|
const r = spawnSync8("git", a, { cwd: dir, encoding: "utf8" });
|
|
24788
25369
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -24800,17 +25381,17 @@ ${where.join("\n")}`);
|
|
|
24800
25381
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
24801
25382
|
});
|
|
24802
25383
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
24803
|
-
if (
|
|
24804
|
-
const raw = JSON.parse(
|
|
25384
|
+
if (existsSync42(resolvedFile)) {
|
|
25385
|
+
const raw = JSON.parse(readFileSync37(resolvedFile, "utf8"));
|
|
24805
25386
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
24806
25387
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
24807
|
-
|
|
25388
|
+
writeFileSync29(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
24808
25389
|
`);
|
|
24809
25390
|
}
|
|
24810
25391
|
}
|
|
24811
25392
|
if (!configured2.some((r) => r.dir === dir)) {
|
|
24812
25393
|
try {
|
|
24813
|
-
|
|
25394
|
+
writeFileSync29(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
|
|
24814
25395
|
} catch (e) {
|
|
24815
25396
|
if (e instanceof ReposError) fail(e.message);
|
|
24816
25397
|
throw e;
|
|
@@ -25043,7 +25624,7 @@ ${where.join("\n")}`);
|
|
|
25043
25624
|
// `synthesis-tabl-123`. `worktreePrefix` above is kept for
|
|
25044
25625
|
// config-provenance display; this is the field that actually
|
|
25045
25626
|
// describes where a worktree lands by default.
|
|
25046
|
-
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${
|
|
25627
|
+
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename14(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
|
|
25047
25628
|
worktreeParent: resolvePath(r.dir, "..")
|
|
25048
25629
|
})),
|
|
25049
25630
|
waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
|
|
@@ -25148,7 +25729,7 @@ watermark: ${state.watermark()}
|
|
|
25148
25729
|
}
|
|
25149
25730
|
for (const r of repos) {
|
|
25150
25731
|
const remote = r.config.branch.remote;
|
|
25151
|
-
if (
|
|
25732
|
+
if (existsSync42(r.dir)) {
|
|
25152
25733
|
pushAuth.push({
|
|
25153
25734
|
route: c.route,
|
|
25154
25735
|
repo: r.name,
|
|
@@ -25206,6 +25787,7 @@ watermark: ${state.watermark()}
|
|
|
25206
25787
|
const { settled } = await settleRepos(reposOf(route2), (t) => t.name, (t) => resolveOneRepo(route2, t));
|
|
25207
25788
|
const tablePresent = flag("offline") ? void 0 : (await new Tracker(route2, cfg.ship).periscopesStatus().catch(() => ({ present: true }))).present;
|
|
25208
25789
|
problems.push(...routePeriscopeProblems({ route: route2.route, hosts: true, repos: settled.map((r) => ({ name: r.name, hasPreviewHook: !!r.config.hooks.preview })), tablePresent }));
|
|
25790
|
+
if (!flag("offline")) problems.push(...await duplicateLivePeriscopeProblems(route2, cfg.ship));
|
|
25209
25791
|
}
|
|
25210
25792
|
problems.push(...attentionProblems(openShipAttention(state), problems));
|
|
25211
25793
|
if (problems.some((p) => p.severity === "blocker")) process.exitCode = 1;
|
|
@@ -25233,7 +25815,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
25233
25815
|
}
|
|
25234
25816
|
try {
|
|
25235
25817
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
25236
|
-
const evSize =
|
|
25818
|
+
const evSize = existsSync42(evFile) ? statSync14(evFile).size : 0;
|
|
25237
25819
|
process.stdout.write(
|
|
25238
25820
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
25239
25821
|
` : "events log: ok\n"
|
|
@@ -25244,7 +25826,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
25244
25826
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run", installContext());
|
|
25245
25827
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
25246
25828
|
try {
|
|
25247
|
-
return
|
|
25829
|
+
return readFileSync37(p, "utf8");
|
|
25248
25830
|
} catch {
|
|
25249
25831
|
return null;
|
|
25250
25832
|
}
|
|
@@ -25287,7 +25869,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
25287
25869
|
);
|
|
25288
25870
|
for (const r of await resolvedRepos(route2)) {
|
|
25289
25871
|
const { config } = r;
|
|
25290
|
-
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${
|
|
25872
|
+
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename14(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
|
|
25291
25873
|
process.stdout.write(
|
|
25292
25874
|
`${`repo ${r.name}:`.padEnd(19)}${r.dir}
|
|
25293
25875
|
worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
|
|
@@ -25351,6 +25933,12 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
25351
25933
|
process.stdout.write(`periscopes: ${f.severity.toUpperCase()} ${f.message}
|
|
25352
25934
|
`);
|
|
25353
25935
|
}
|
|
25936
|
+
if (!flag("offline")) {
|
|
25937
|
+
for (const f of await duplicateLivePeriscopeProblems(route2, cfg.ship)) {
|
|
25938
|
+
process.stdout.write(`periscopes: ${f.severity.toUpperCase()} ${f.message}
|
|
25939
|
+
`);
|
|
25940
|
+
}
|
|
25941
|
+
}
|
|
25354
25942
|
}
|
|
25355
25943
|
process.stdout.write(
|
|
25356
25944
|
!periscopes.present ? "periscopes: Periscopes table absent \u2014 this workspace cannot host periscopes\n" : periscopes.missing.length === 0 ? `periscopes: Periscopes table present (${periscopes.modelId})
|
|
@@ -25668,6 +26256,11 @@ ${foreign.join("\n")}
|
|
|
25668
26256
|
);
|
|
25669
26257
|
}
|
|
25670
26258
|
}
|
|
26259
|
+
for (const e of listPeriscopeEndpoints(cfg.ship.stateDir)) {
|
|
26260
|
+
const secret = readPeriscopeSecret(cfg.ship.stateDir, e.ticketRecordId) ? "live" : "ended, awaiting removal";
|
|
26261
|
+
process.stdout.write(`periscope mcp: ${e.ticket} ${e.route} ${e.url} (${secret})
|
|
26262
|
+
`);
|
|
26263
|
+
}
|
|
25671
26264
|
if (cfg.ship.relayHost && !flag("offline")) {
|
|
25672
26265
|
const relayUrl = routeRelayUrl(route2);
|
|
25673
26266
|
if (!relayUrl) {
|
|
@@ -25715,6 +26308,8 @@ var parseTicketNumber2;
|
|
|
25715
26308
|
var routeRelayUrl2;
|
|
25716
26309
|
var passengerApiKeys2;
|
|
25717
26310
|
var endpointApiFor2;
|
|
26311
|
+
var periscopeEndpointDeps2;
|
|
26312
|
+
var periscopeMcpDeps2;
|
|
25718
26313
|
var endpointReconcileDeps2;
|
|
25719
26314
|
var releaseRefused2;
|
|
25720
26315
|
var doctorMachineId2;
|