@tablation/crew 0.99.1 → 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 +803 -296
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -45,12 +45,12 @@ function periscopeSummary(cfg) {
|
|
|
45
45
|
if (routes.length === 0) return `off (no enabled route sets hostPeriscopes: true; ${limits})`;
|
|
46
46
|
return `ON for ${routes.join(", ")} (host ${cfg.ship.previewHost ?? "NOT SET (ship.previewHost)"}, ${limits})`;
|
|
47
47
|
}
|
|
48
|
-
async function previewHostProblem(cfg,
|
|
48
|
+
async function previewHostProblem(cfg, resolve7 = (h) => lookup(h)) {
|
|
49
49
|
if (periscopeRoutes(cfg).length === 0) return void 0;
|
|
50
50
|
const host = cfg.ship.previewHost;
|
|
51
51
|
if (!host) return "ship.previewHost is not set \u2014 a route hosts periscopes, so Passengers have no host to be sent to";
|
|
52
52
|
try {
|
|
53
|
-
await
|
|
53
|
+
await resolve7(host);
|
|
54
54
|
} catch {
|
|
55
55
|
return `ship.previewHost "${host}" does not resolve from this machine`;
|
|
56
56
|
}
|
|
@@ -140,11 +140,11 @@ function urlPort(url) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
function portListening(port, timeoutMs = 500) {
|
|
143
|
-
return new Promise((
|
|
143
|
+
return new Promise((resolve7) => {
|
|
144
144
|
const s = connect({ port, host: "127.0.0.1" });
|
|
145
145
|
const done = (ok) => {
|
|
146
146
|
s.destroy();
|
|
147
|
-
|
|
147
|
+
resolve7(ok);
|
|
148
148
|
};
|
|
149
149
|
s.setTimeout(timeoutMs, () => done(false));
|
|
150
150
|
s.once("connect", () => done(true));
|
|
@@ -583,8 +583,8 @@ function removeClaudeMcp(o) {
|
|
|
583
583
|
if (got.missing) return `mcp: \`${o.bin}\` not found, so Claude Code was not touched`;
|
|
584
584
|
if (got.status !== 0) return `mcp: no ${MCP_SERVER_NAME} server registered with Claude Code`;
|
|
585
585
|
if (o.dryRun) return `(dry run) would remove the ${MCP_SERVER_NAME} MCP server from Claude Code (user scope)`;
|
|
586
|
-
const
|
|
587
|
-
return
|
|
586
|
+
const rm2 = runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
|
|
587
|
+
return rm2.status === 0 ? `mcp: removed the ${MCP_SERVER_NAME} MCP server from Claude Code (user scope)` : `mcp: \`claude mcp remove\` failed (${rm2.stderr.trim() || `exit ${rm2.status}`})`;
|
|
588
588
|
}
|
|
589
589
|
|
|
590
590
|
// src/launcher.ts
|
|
@@ -620,10 +620,10 @@ function selectsCustomModelProvider(args) {
|
|
|
620
620
|
}
|
|
621
621
|
return false;
|
|
622
622
|
}
|
|
623
|
-
function parseOmlxSettings(
|
|
623
|
+
function parseOmlxSettings(text2) {
|
|
624
624
|
let raw;
|
|
625
625
|
try {
|
|
626
|
-
raw = JSON.parse(
|
|
626
|
+
raw = JSON.parse(text2);
|
|
627
627
|
} catch {
|
|
628
628
|
return void 0;
|
|
629
629
|
}
|
|
@@ -844,32 +844,32 @@ var BLOCK_RE = new RegExp(`${escapeRe(PAIR_BLOCK_START)}[\\s\\S]*?${escapeRe(PAI
|
|
|
844
844
|
function escapeRe(s) {
|
|
845
845
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
846
846
|
}
|
|
847
|
-
function hasPairBlock(
|
|
848
|
-
return BLOCK_RE.test(
|
|
847
|
+
function hasPairBlock(text2) {
|
|
848
|
+
return BLOCK_RE.test(text2);
|
|
849
849
|
}
|
|
850
|
-
function pairBlockIsBroken(
|
|
851
|
-
return !hasPairBlock(
|
|
850
|
+
function pairBlockIsBroken(text2) {
|
|
851
|
+
return !hasPairBlock(text2) && (text2.includes(PAIR_BLOCK_START) || text2.includes(PAIR_BLOCK_END));
|
|
852
852
|
}
|
|
853
|
-
function upsertPairBlock(
|
|
854
|
-
if (hasPairBlock(
|
|
853
|
+
function upsertPairBlock(text2, block) {
|
|
854
|
+
if (hasPairBlock(text2)) return text2.replace(BLOCK_RE, () => `${block}
|
|
855
855
|
`);
|
|
856
|
-
if (
|
|
856
|
+
if (text2.trim() === "") return `${block}
|
|
857
857
|
`;
|
|
858
|
-
return `${
|
|
858
|
+
return `${text2.replace(/\n*$/, "")}
|
|
859
859
|
|
|
860
860
|
${block}
|
|
861
861
|
`;
|
|
862
862
|
}
|
|
863
|
-
function removePairBlock(
|
|
864
|
-
if (!hasPairBlock(
|
|
865
|
-
return
|
|
863
|
+
function removePairBlock(text2) {
|
|
864
|
+
if (!hasPairBlock(text2)) return text2;
|
|
865
|
+
return text2.replace(new RegExp(`\\n*${BLOCK_RE.source}`), "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
|
|
866
866
|
}
|
|
867
867
|
function readIfExists(path) {
|
|
868
868
|
return existsSync5(path) ? readFileSync7(path, "utf8") : void 0;
|
|
869
869
|
}
|
|
870
|
-
function writeFileMkdir(path,
|
|
870
|
+
function writeFileMkdir(path, text2) {
|
|
871
871
|
mkdirSync4(dirname2(path), { recursive: true });
|
|
872
|
-
writeFileSync3(path,
|
|
872
|
+
writeFileSync3(path, text2);
|
|
873
873
|
}
|
|
874
874
|
function installBlockFile(path, harness, dryRun) {
|
|
875
875
|
const before = readIfExists(path) ?? "";
|
|
@@ -889,9 +889,9 @@ function removeBlockFile(path, dryRun) {
|
|
|
889
889
|
return true;
|
|
890
890
|
}
|
|
891
891
|
function blockVerdict(path, harness) {
|
|
892
|
-
const
|
|
893
|
-
if (
|
|
894
|
-
if (!
|
|
892
|
+
const text2 = readIfExists(path);
|
|
893
|
+
if (text2 === void 0 || !hasPairBlock(text2)) return { ok: false, detail: `no managed block in ${path}` };
|
|
894
|
+
if (!text2.includes(pairBlock(harness))) return { ok: false, detail: `the managed block in ${path} is out of date \u2014 run crew connect` };
|
|
895
895
|
return { ok: true, detail: "" };
|
|
896
896
|
}
|
|
897
897
|
var HEADER_RE = /^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(#.*)?$/;
|
|
@@ -913,24 +913,24 @@ function tomlTableRange(lines, name) {
|
|
|
913
913
|
return [start, end];
|
|
914
914
|
}
|
|
915
915
|
var tomlString = (v) => JSON.stringify(v);
|
|
916
|
-
function upsertTomlTable(
|
|
917
|
-
const lines =
|
|
916
|
+
function upsertTomlTable(text2, name, table2) {
|
|
917
|
+
const lines = text2.split("\n");
|
|
918
918
|
const r = tomlTableRange(lines, name);
|
|
919
919
|
if (r) {
|
|
920
920
|
lines.splice(r[0], r[1] - r[0], ...table2);
|
|
921
921
|
return lines.join("\n");
|
|
922
922
|
}
|
|
923
|
-
const body =
|
|
923
|
+
const body = text2.replace(/\n*$/, "");
|
|
924
924
|
return body === "" ? `${table2.join("\n")}
|
|
925
925
|
` : `${body}
|
|
926
926
|
|
|
927
927
|
${table2.join("\n")}
|
|
928
928
|
`;
|
|
929
929
|
}
|
|
930
|
-
function removeTomlTable(
|
|
931
|
-
const lines =
|
|
930
|
+
function removeTomlTable(text2, name) {
|
|
931
|
+
const lines = text2.split("\n");
|
|
932
932
|
let r = tomlTableRange(lines, name);
|
|
933
|
-
if (!r) return
|
|
933
|
+
if (!r) return text2;
|
|
934
934
|
while (r) {
|
|
935
935
|
let [start, end] = r;
|
|
936
936
|
if (start > 0 && lines[start - 1].trim() === "") start--;
|
|
@@ -1164,10 +1164,10 @@ var geminiPaths = (home) => ({
|
|
|
1164
1164
|
config: join8(home, ".gemini", "settings.json")
|
|
1165
1165
|
});
|
|
1166
1166
|
function readGeminiSettings(path) {
|
|
1167
|
-
const
|
|
1168
|
-
if (
|
|
1167
|
+
const text2 = readIfExists(path);
|
|
1168
|
+
if (text2 === void 0 || text2.trim() === "") return {};
|
|
1169
1169
|
try {
|
|
1170
|
-
const j = JSON.parse(
|
|
1170
|
+
const j = JSON.parse(text2);
|
|
1171
1171
|
return j && typeof j === "object" && !Array.isArray(j) ? j : void 0;
|
|
1172
1172
|
} catch {
|
|
1173
1173
|
return void 0;
|
|
@@ -1267,10 +1267,10 @@ function vscodeUserDirs(o) {
|
|
|
1267
1267
|
}
|
|
1268
1268
|
var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
|
|
1269
1269
|
function readVscodeMcp(path) {
|
|
1270
|
-
const
|
|
1271
|
-
if (
|
|
1270
|
+
const text2 = readIfExists(path);
|
|
1271
|
+
if (text2 === void 0 || text2.trim() === "") return {};
|
|
1272
1272
|
try {
|
|
1273
|
-
const j = JSON.parse(
|
|
1273
|
+
const j = JSON.parse(text2);
|
|
1274
1274
|
return j && typeof j === "object" && !Array.isArray(j) ? j : void 0;
|
|
1275
1275
|
} catch {
|
|
1276
1276
|
return void 0;
|
|
@@ -1446,9 +1446,9 @@ function mapGeminiLine(line) {
|
|
|
1446
1446
|
function createGeminiStreamMapper() {
|
|
1447
1447
|
let pending = "";
|
|
1448
1448
|
const flush = () => {
|
|
1449
|
-
const
|
|
1449
|
+
const text2 = pending;
|
|
1450
1450
|
pending = "";
|
|
1451
|
-
return
|
|
1451
|
+
return text2 ? [{ kind: "text", text: text2 }] : [];
|
|
1452
1452
|
};
|
|
1453
1453
|
return {
|
|
1454
1454
|
flush,
|
|
@@ -1758,9 +1758,9 @@ function storeForgeToken(forges, host, token, stateDir, env = process.env) {
|
|
|
1758
1758
|
if (!token.trim()) throw new ForgeError("empty token");
|
|
1759
1759
|
i.write(f.entry.token, token.trim(), { host: f.host, stateDir, env });
|
|
1760
1760
|
}
|
|
1761
|
-
function parseCredentialInput(
|
|
1761
|
+
function parseCredentialInput(text2) {
|
|
1762
1762
|
const out = {};
|
|
1763
|
-
for (const line of
|
|
1763
|
+
for (const line of text2.split("\n")) {
|
|
1764
1764
|
if (line === "") break;
|
|
1765
1765
|
const eq2 = line.indexOf("=");
|
|
1766
1766
|
if (eq2 > 0) out[line.slice(0, eq2)] = line.slice(eq2 + 1);
|
|
@@ -2148,10 +2148,10 @@ function rejectUnknown(obj, allowed, where, file) {
|
|
|
2148
2148
|
);
|
|
2149
2149
|
}
|
|
2150
2150
|
}
|
|
2151
|
-
function parseRepoConfig(
|
|
2151
|
+
function parseRepoConfig(text2, file) {
|
|
2152
2152
|
let raw;
|
|
2153
2153
|
try {
|
|
2154
|
-
raw = file.endsWith(".json") ? JSON.parse(
|
|
2154
|
+
raw = file.endsWith(".json") ? JSON.parse(text2) : parse(text2);
|
|
2155
2155
|
} catch (e) {
|
|
2156
2156
|
throw new RepoConfigError(`${file}: could not be parsed \u2014 ${e.message}`);
|
|
2157
2157
|
}
|
|
@@ -2667,9 +2667,9 @@ function parseShipEnv(v) {
|
|
|
2667
2667
|
if (typeof val !== "string" && typeof val !== "number" && typeof val !== "boolean") {
|
|
2668
2668
|
throw new ConfigError(`ship.env.${k} must be a string, number or boolean`);
|
|
2669
2669
|
}
|
|
2670
|
-
const
|
|
2671
|
-
if (/[\r\n\0]/.test(
|
|
2672
|
-
out[k] =
|
|
2670
|
+
const text2 = String(val);
|
|
2671
|
+
if (/[\r\n\0]/.test(text2)) throw new ConfigError(`ship.env.${k} must be a single line`);
|
|
2672
|
+
out[k] = text2;
|
|
2673
2673
|
}
|
|
2674
2674
|
return Object.keys(out).length ? out : void 0;
|
|
2675
2675
|
}
|
|
@@ -3639,10 +3639,10 @@ function runScript(script, opts) {
|
|
|
3639
3639
|
let output = "";
|
|
3640
3640
|
let pending = "";
|
|
3641
3641
|
const absorb = (buf) => {
|
|
3642
|
-
const
|
|
3643
|
-
output +=
|
|
3642
|
+
const text2 = buf.toString();
|
|
3643
|
+
output += text2;
|
|
3644
3644
|
if (!opts.onLine) return;
|
|
3645
|
-
pending +=
|
|
3645
|
+
pending += text2;
|
|
3646
3646
|
const lines = pending.split("\n");
|
|
3647
3647
|
pending = lines.pop() ?? "";
|
|
3648
3648
|
for (const l of lines) opts.onLine(l);
|
|
@@ -4062,9 +4062,9 @@ function planCron(ship, crewHome, job) {
|
|
|
4062
4062
|
function unitAbandonsAgents(plan, read) {
|
|
4063
4063
|
const path = plan.unitPaths[0];
|
|
4064
4064
|
if (!path || plan.mechanism !== "launchd" && plan.mechanism !== "systemd") return null;
|
|
4065
|
-
const
|
|
4066
|
-
if (
|
|
4067
|
-
return plan.mechanism === "launchd" ? /<key>AbandonProcessGroup<\/key>\s*<true\/>/.test(
|
|
4065
|
+
const text2 = read(path);
|
|
4066
|
+
if (text2 === null) return null;
|
|
4067
|
+
return plan.mechanism === "launchd" ? /<key>AbandonProcessGroup<\/key>\s*<true\/>/.test(text2) : /^KillMode=process\s*$/m.test(text2);
|
|
4068
4068
|
}
|
|
4069
4069
|
function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run", ctx = USER_INSTALL) {
|
|
4070
4070
|
if (hostShip === "macos") return planLaunchd(ship, crewHome, job);
|
|
@@ -4249,8 +4249,8 @@ function launchAgentsDir() {
|
|
|
4249
4249
|
function xmlUnescape(s) {
|
|
4250
4250
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
4251
4251
|
}
|
|
4252
|
-
function programFromPlist(
|
|
4253
|
-
const block = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(
|
|
4252
|
+
function programFromPlist(text2) {
|
|
4253
|
+
const block = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(text2);
|
|
4254
4254
|
if (!block) return null;
|
|
4255
4255
|
const args = [...block[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => xmlUnescape(m[1]));
|
|
4256
4256
|
return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
|
|
@@ -4316,9 +4316,9 @@ function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
|
|
|
4316
4316
|
function removeAllLaunchdUnits(dryRun, log, dir, loaded) {
|
|
4317
4317
|
for (const unit of listLaunchdUnits(dir, loaded)) removeLaunchdUnit(unit, dryRun, log);
|
|
4318
4318
|
}
|
|
4319
|
-
function parseLaunchctlList(
|
|
4319
|
+
function parseLaunchctlList(text2) {
|
|
4320
4320
|
const jobs = /* @__PURE__ */ new Map();
|
|
4321
|
-
for (const line of
|
|
4321
|
+
for (const line of text2.split("\n")) {
|
|
4322
4322
|
const cols = line.split(" ");
|
|
4323
4323
|
if (cols.length !== 3 || cols[2] === "Label") continue;
|
|
4324
4324
|
jobs.set(cols[2], { pid: cols[0], lastExit: cols[1] });
|
|
@@ -4342,8 +4342,8 @@ function readCrontab() {
|
|
|
4342
4342
|
const res = spawnSync4("crontab", ["-l"], { encoding: "utf8" });
|
|
4343
4343
|
return res.status === 0 ? res.stdout ?? "" : "";
|
|
4344
4344
|
}
|
|
4345
|
-
function writeCrontab(
|
|
4346
|
-
spawnSync4("crontab", ["-"], { input:
|
|
4345
|
+
function writeCrontab(text2) {
|
|
4346
|
+
spawnSync4("crontab", ["-"], { input: text2, encoding: "utf8" });
|
|
4347
4347
|
}
|
|
4348
4348
|
function addCrontabLine(marker, line) {
|
|
4349
4349
|
const existing = readCrontab();
|
|
@@ -5356,10 +5356,10 @@ function formatPushAuth(repo, r) {
|
|
|
5356
5356
|
return `${label}FAILED \u2014 ${r.detail}; ${r.hint}
|
|
5357
5357
|
`;
|
|
5358
5358
|
}
|
|
5359
|
-
async function settleRepos(items, nameOf,
|
|
5359
|
+
async function settleRepos(items, nameOf, resolve7) {
|
|
5360
5360
|
const out = await Promise.all(items.map(async (t) => {
|
|
5361
5361
|
try {
|
|
5362
|
-
return { ok: true, value: await
|
|
5362
|
+
return { ok: true, value: await resolve7(t) };
|
|
5363
5363
|
} catch (e) {
|
|
5364
5364
|
return { ok: false, repo: nameOf(t), detail: e.message };
|
|
5365
5365
|
}
|
|
@@ -6660,8 +6660,8 @@ async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /
|
|
|
6660
6660
|
|
|
6661
6661
|
// src/ticket-attribution.ts
|
|
6662
6662
|
var TICKET_KEY_RE = /\b[A-Z][A-Z0-9]{1,9}-\d+\b/g;
|
|
6663
|
-
function ticketMentions(
|
|
6664
|
-
return [...
|
|
6663
|
+
function ticketMentions(text2) {
|
|
6664
|
+
return [...text2.matchAll(TICKET_KEY_RE)].map((m) => m[0]);
|
|
6665
6665
|
}
|
|
6666
6666
|
var TicketAttributionTracker = class {
|
|
6667
6667
|
#tokensByTicket = /* @__PURE__ */ new Map();
|
|
@@ -6680,13 +6680,13 @@ var TicketAttributionTracker = class {
|
|
|
6680
6680
|
return this.#current;
|
|
6681
6681
|
}
|
|
6682
6682
|
/** Record the mentions in `text` without crediting tokens (a claim needs "which ticket" before the turn's usage line arrives). */
|
|
6683
|
-
note(
|
|
6684
|
-
const mentions = ticketMentions(
|
|
6683
|
+
note(text2) {
|
|
6684
|
+
const mentions = ticketMentions(text2);
|
|
6685
6685
|
if (mentions.length > 0) this.#current = mentions[0];
|
|
6686
6686
|
}
|
|
6687
6687
|
/** One assistant turn: its combined visible text (thinking + text blocks) and its own token cost. */
|
|
6688
|
-
add(
|
|
6689
|
-
const mentions = ticketMentions(
|
|
6688
|
+
add(text2, tokens) {
|
|
6689
|
+
const mentions = ticketMentions(text2);
|
|
6690
6690
|
if (mentions.length > 0) this.#current = mentions[0];
|
|
6691
6691
|
if (this.#current === void 0 || tokens <= 0) return;
|
|
6692
6692
|
this.#turns++;
|
|
@@ -6783,10 +6783,10 @@ function resultText(content) {
|
|
|
6783
6783
|
if (Array.isArray(content)) return content.map((c) => c && typeof c.text === "string" ? c.text : "").join("\n");
|
|
6784
6784
|
return "";
|
|
6785
6785
|
}
|
|
6786
|
-
function isTransportFailure(isError,
|
|
6786
|
+
function isTransportFailure(isError, text2) {
|
|
6787
6787
|
if (!isError) {
|
|
6788
6788
|
try {
|
|
6789
|
-
const doc = JSON.parse(
|
|
6789
|
+
const doc = JSON.parse(text2);
|
|
6790
6790
|
if (doc && typeof doc === "object") {
|
|
6791
6791
|
const code = doc.statusCode;
|
|
6792
6792
|
return typeof code === "number" && code >= 500;
|
|
@@ -6794,7 +6794,7 @@ function isTransportFailure(isError, text) {
|
|
|
6794
6794
|
} catch {
|
|
6795
6795
|
}
|
|
6796
6796
|
}
|
|
6797
|
-
return TRANSPORT_FAILURE.test(
|
|
6797
|
+
return TRANSPORT_FAILURE.test(text2);
|
|
6798
6798
|
}
|
|
6799
6799
|
var HandoffTranscriptTracker = class {
|
|
6800
6800
|
open = /* @__PURE__ */ new Map();
|
|
@@ -8653,15 +8653,15 @@ async function replayPendingHandoffs(i) {
|
|
|
8653
8653
|
continue;
|
|
8654
8654
|
}
|
|
8655
8655
|
if (v.kind === "unverified") throw new Error(v.why);
|
|
8656
|
-
const
|
|
8657
|
-
if (
|
|
8658
|
-
const head = firstLine(
|
|
8656
|
+
const text2 = rec.comment ?? (rec.commentLanded ? "" : rec.finalText);
|
|
8657
|
+
if (text2) {
|
|
8658
|
+
const head = firstLine(text2);
|
|
8659
8659
|
const cc = i.contract.comments;
|
|
8660
8660
|
const seen = (await i.writer.comments()).some((c) => {
|
|
8661
8661
|
const row = c;
|
|
8662
8662
|
return row[cc.parent] === t.id && row[cc.author] === seatId && firstLine(String(row[cc.body] ?? "")) === head;
|
|
8663
8663
|
});
|
|
8664
|
-
if (!seen) await i.writer.postComment(t.id,
|
|
8664
|
+
if (!seen) await i.writer.postComment(t.id, text2, seatId);
|
|
8665
8665
|
}
|
|
8666
8666
|
await i.writer.postEvent(t.id, pushedEventBody(v.sha, v.remote, v.branch), seatId, "pushed");
|
|
8667
8667
|
await i.writer.updateTicket(t.id, rec.patch);
|
|
@@ -8690,15 +8690,15 @@ var SkillIncludeError = class extends Error {
|
|
|
8690
8690
|
var MAX_INCLUDE_DEPTH = 3;
|
|
8691
8691
|
var MAX_INCLUDED_BYTES = 2e5;
|
|
8692
8692
|
var MARKER = /\{\{[ \t]*skill:[ \t]*([^{}\r\n]*?)[ \t]*\}\}/g;
|
|
8693
|
-
function skillMarkerNames(
|
|
8693
|
+
function skillMarkerNames(text2) {
|
|
8694
8694
|
const names = [];
|
|
8695
|
-
for (const m of
|
|
8695
|
+
for (const m of text2.matchAll(MARKER)) {
|
|
8696
8696
|
const name = m[1].trim();
|
|
8697
8697
|
if (name && !names.includes(name)) names.push(name);
|
|
8698
8698
|
}
|
|
8699
8699
|
return names;
|
|
8700
8700
|
}
|
|
8701
|
-
function expandSkillMarkers(
|
|
8701
|
+
function expandSkillMarkers(text2, lookup2, opts = {}) {
|
|
8702
8702
|
const maxDepth = opts.maxDepth ?? MAX_INCLUDE_DEPTH;
|
|
8703
8703
|
const maxBytes = opts.maxBytes ?? MAX_INCLUDED_BYTES;
|
|
8704
8704
|
const warnings = [];
|
|
@@ -8725,7 +8725,7 @@ function expandSkillMarkers(text, lookup2, opts = {}) {
|
|
|
8725
8725
|
included += bytes;
|
|
8726
8726
|
return expand2(trimmed, [...chain, name]);
|
|
8727
8727
|
});
|
|
8728
|
-
return { text: expand2(
|
|
8728
|
+
return { text: expand2(text2, []), warnings };
|
|
8729
8729
|
}
|
|
8730
8730
|
|
|
8731
8731
|
// src/skills.ts
|
|
@@ -8736,9 +8736,9 @@ import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError
|
|
|
8736
8736
|
|
|
8737
8737
|
// src/prompt-additions.ts
|
|
8738
8738
|
var PROMPT_ADDITIONS_SEPARATOR = "---\n\nAdditional instructions from this workspace. Where they conflict with anything above, these take precedence.";
|
|
8739
|
-
function appendAdditions(
|
|
8740
|
-
if (typeof additions !== "string" || additions.trim() === "") return
|
|
8741
|
-
return `${
|
|
8739
|
+
function appendAdditions(text2, additions) {
|
|
8740
|
+
if (typeof additions !== "string" || additions.trim() === "") return text2;
|
|
8741
|
+
return `${text2}
|
|
8742
8742
|
|
|
8743
8743
|
${PROMPT_ADDITIONS_SEPARATOR}
|
|
8744
8744
|
|
|
@@ -9213,8 +9213,8 @@ function describeCrewLink(o) {
|
|
|
9213
9213
|
var SkillSyncError = class extends Error {
|
|
9214
9214
|
};
|
|
9215
9215
|
function parseSkillFile(path) {
|
|
9216
|
-
const
|
|
9217
|
-
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);
|
|
9218
9218
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
9219
9219
|
const frontmatter = match[1];
|
|
9220
9220
|
const body = match[2];
|
|
@@ -9403,8 +9403,8 @@ function isEditedSkillRow(row, localPrompt, cached) {
|
|
|
9403
9403
|
function skillMarkerWarnings(promptsDir, personaPrompts) {
|
|
9404
9404
|
const shipped = new Set(listLocalSkills(promptsDir).map((s) => s.name));
|
|
9405
9405
|
const out = [];
|
|
9406
|
-
for (const [role,
|
|
9407
|
-
for (const name of skillMarkerNames(
|
|
9406
|
+
for (const [role, text2] of Object.entries(personaPrompts)) {
|
|
9407
|
+
for (const name of skillMarkerNames(text2 ?? "")) {
|
|
9408
9408
|
if (!shipped.has(name)) out.push(`${role}: brief includes {{skill: ${name}}} but this prompt set ships no skill with that name`);
|
|
9409
9409
|
}
|
|
9410
9410
|
}
|
|
@@ -9741,15 +9741,15 @@ var FileTail = class {
|
|
|
9741
9741
|
return "";
|
|
9742
9742
|
}
|
|
9743
9743
|
try {
|
|
9744
|
-
let
|
|
9744
|
+
let text2 = "";
|
|
9745
9745
|
const chunk = Buffer.alloc(64 * 1024);
|
|
9746
9746
|
for (; ; ) {
|
|
9747
9747
|
const n = readSync(fd, chunk, 0, chunk.length, this.offset);
|
|
9748
9748
|
if (n <= 0) break;
|
|
9749
9749
|
this.offset += n;
|
|
9750
|
-
|
|
9750
|
+
text2 += this.decoder.write(chunk.subarray(0, n));
|
|
9751
9751
|
}
|
|
9752
|
-
return
|
|
9752
|
+
return text2;
|
|
9753
9753
|
} finally {
|
|
9754
9754
|
closeSync3(fd);
|
|
9755
9755
|
}
|
|
@@ -9788,8 +9788,8 @@ var RunObserver = class {
|
|
|
9788
9788
|
this.liveTicket = id.ticket ?? void 0;
|
|
9789
9789
|
this.mapper = adapter.createStreamMapper?.() ?? { map: (p) => adapter.mapStreamLine(p), flush: () => [] };
|
|
9790
9790
|
}
|
|
9791
|
-
feed(
|
|
9792
|
-
this.buf +=
|
|
9791
|
+
feed(text2) {
|
|
9792
|
+
this.buf += text2;
|
|
9793
9793
|
const lines = this.buf.split("\n");
|
|
9794
9794
|
this.buf = lines.pop() ?? "";
|
|
9795
9795
|
for (const line of lines) this.handleLine(line);
|
|
@@ -10054,7 +10054,7 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
10054
10054
|
for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
|
|
10055
10055
|
const eventsSink = openSink(plan.eventsPath);
|
|
10056
10056
|
const id = identityOf(plan, adapter.harness);
|
|
10057
|
-
return new Promise((
|
|
10057
|
+
return new Promise((resolve7, reject) => {
|
|
10058
10058
|
let streamFd;
|
|
10059
10059
|
try {
|
|
10060
10060
|
mkdirSync12(dirname8(plan.streamPath), { recursive: true });
|
|
@@ -10138,7 +10138,7 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
10138
10138
|
plan.agentLog?.apiKey,
|
|
10139
10139
|
hintMoved
|
|
10140
10140
|
);
|
|
10141
|
-
|
|
10141
|
+
resolve7({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {}, ...killed ? { killed } : {} });
|
|
10142
10142
|
});
|
|
10143
10143
|
child.stdin?.on("error", (err3) => {
|
|
10144
10144
|
emit.warn(`agent closed stdin before the prompt was delivered: ${err3.message}`, { step: "agent" });
|
|
@@ -10471,7 +10471,7 @@ var SELFTEST_TIMEOUT_MS = 12e4;
|
|
|
10471
10471
|
var KEY_VAR = "CREW_API_KEY";
|
|
10472
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: ";
|
|
10473
10473
|
var realDeps = {
|
|
10474
|
-
run: (o) => new Promise((
|
|
10474
|
+
run: (o) => new Promise((resolve7) => {
|
|
10475
10475
|
let stdout = "";
|
|
10476
10476
|
let stderr = "";
|
|
10477
10477
|
let timedOut = false;
|
|
@@ -10479,7 +10479,7 @@ var realDeps = {
|
|
|
10479
10479
|
const done = (r) => {
|
|
10480
10480
|
if (!settled) {
|
|
10481
10481
|
settled = true;
|
|
10482
|
-
|
|
10482
|
+
resolve7(r);
|
|
10483
10483
|
}
|
|
10484
10484
|
};
|
|
10485
10485
|
const child = spawn3(o.bin, o.args, { cwd: o.cwd, env: o.env, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
@@ -10588,12 +10588,12 @@ function classify(spec, run2, seen) {
|
|
|
10588
10588
|
return fail("binary-not-found", `could not start \`${bin}\` (${run2.spawnError.message})`);
|
|
10589
10589
|
}
|
|
10590
10590
|
if (run2.timedOut) return fail("timeout", "no answer within the time limit");
|
|
10591
|
-
const
|
|
10591
|
+
const text2 = `${seen.toolOutput}
|
|
10592
10592
|
${run2.stderr}`;
|
|
10593
10593
|
if (REFUSED.test(seen.toolOutput) || seen.tools.some((t) => t.startsWith("mcp__")) && REFUSED.test(run2.stderr)) {
|
|
10594
10594
|
return fail("key-refused", "the tracker returned 401/403 to the MCP call");
|
|
10595
10595
|
}
|
|
10596
|
-
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}
|
|
10597
10597
|
${seen.texts.join("\n")}`)) {
|
|
10598
10598
|
return fail("mcp-unavailable", "the tracker MCP tool was not available to the session");
|
|
10599
10599
|
}
|
|
@@ -10685,7 +10685,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
10685
10685
|
}
|
|
10686
10686
|
|
|
10687
10687
|
// src/cli.ts
|
|
10688
|
-
import { resolve as
|
|
10688
|
+
import { resolve as resolve6, dirname as dirname23, join as join46, basename as basename14 } from "path";
|
|
10689
10689
|
import { fileURLToPath } from "url";
|
|
10690
10690
|
import { hostname as hostname3, homedir as homedir10 } from "os";
|
|
10691
10691
|
import { spawnSync as spawnSync8 } from "child_process";
|
|
@@ -12255,13 +12255,13 @@ function writeDigest(o, d, role, stateDir, runId, own) {
|
|
|
12255
12255
|
blockerInfo: d.info
|
|
12256
12256
|
};
|
|
12257
12257
|
const body = role === "qa" ? qaDigest(input) : buildingDigest(input);
|
|
12258
|
-
const
|
|
12258
|
+
const text2 = own ? `${ownRunBanner(own)}
|
|
12259
12259
|
|
|
12260
12260
|
${body}` : body;
|
|
12261
12261
|
const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
|
|
12262
|
-
writeFileSync17(path,
|
|
12263
|
-
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(
|
|
12264
|
-
data: { role, bytes: Buffer.byteLength(
|
|
12262
|
+
writeFileSync17(path, text2);
|
|
12263
|
+
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text2)} bytes)`, {
|
|
12264
|
+
data: { role, bytes: Buffer.byteLength(text2) }
|
|
12265
12265
|
});
|
|
12266
12266
|
return true;
|
|
12267
12267
|
} catch (e) {
|
|
@@ -12825,10 +12825,10 @@ function renderFullConfig(a, routeBlock) {
|
|
|
12825
12825
|
routes:
|
|
12826
12826
|
${routeBlock}`;
|
|
12827
12827
|
}
|
|
12828
|
-
function writeNewConfig(path,
|
|
12828
|
+
function writeNewConfig(path, text2) {
|
|
12829
12829
|
mkdirSync16(dirname11(path), { recursive: true });
|
|
12830
12830
|
try {
|
|
12831
|
-
writeFileSync18(path,
|
|
12831
|
+
writeFileSync18(path, text2, { mode: 384, flag: "wx" });
|
|
12832
12832
|
return true;
|
|
12833
12833
|
} catch (e) {
|
|
12834
12834
|
if (e.code === "EEXIST") return false;
|
|
@@ -12865,11 +12865,11 @@ import { homedir as homedir8 } from "os";
|
|
|
12865
12865
|
import { dirname as dirname12, join as join28 } from "path";
|
|
12866
12866
|
import { delimiter as delimiter2 } from "path";
|
|
12867
12867
|
var CHOICE_EXIT_CODE = 3;
|
|
12868
|
-
function createEmitter(
|
|
12868
|
+
function createEmitter(json2, write) {
|
|
12869
12869
|
return {
|
|
12870
|
-
json,
|
|
12870
|
+
json: json2,
|
|
12871
12871
|
emit(event, message, data = {}) {
|
|
12872
|
-
if (!
|
|
12872
|
+
if (!json2) return;
|
|
12873
12873
|
write(`${JSON.stringify({ ...data, event, message })}
|
|
12874
12874
|
`);
|
|
12875
12875
|
}
|
|
@@ -12976,8 +12976,8 @@ function matchRepoName(origin, resolved) {
|
|
|
12976
12976
|
}
|
|
12977
12977
|
return void 0;
|
|
12978
12978
|
}
|
|
12979
|
-
function routeNamesOf(
|
|
12980
|
-
const routes = parseDocument(
|
|
12979
|
+
function routeNamesOf(text2) {
|
|
12980
|
+
const routes = parseDocument(text2).get("routes", true);
|
|
12981
12981
|
if (!isSeq(routes)) return [];
|
|
12982
12982
|
return routes.items.flatMap((i) => isMap(i) && typeof i.get("route") === "string" ? [i.get("route")] : []);
|
|
12983
12983
|
}
|
|
@@ -13011,8 +13011,8 @@ function routeMapOf(doc, route2) {
|
|
|
13011
13011
|
const have = isSeq(routes) ? routes.items.map((i) => isMap(i) ? String(i.get("route")) : "?").join(", ") : "none";
|
|
13012
13012
|
throw new ReposError(`no route "${route2}" in the config (have: ${have})`);
|
|
13013
13013
|
}
|
|
13014
|
-
function configuredRepos(
|
|
13015
|
-
const doc = parseDocument(
|
|
13014
|
+
function configuredRepos(text2, route2, base) {
|
|
13015
|
+
const doc = parseDocument(text2);
|
|
13016
13016
|
const m = routeMapOf(doc, route2);
|
|
13017
13017
|
const repos = m.get("repos");
|
|
13018
13018
|
if (isMap(repos)) {
|
|
@@ -13026,8 +13026,8 @@ function configuredRepos(text, route2, base) {
|
|
|
13026
13026
|
const dir = m.get("dir");
|
|
13027
13027
|
return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
13028
13028
|
}
|
|
13029
|
-
function cloneBaseOf(
|
|
13030
|
-
const m = routeMapOf(parseDocument(
|
|
13029
|
+
function cloneBaseOf(text2, route2, base, isCheckout) {
|
|
13030
|
+
const m = routeMapOf(parseDocument(text2), route2);
|
|
13031
13031
|
const repos = m.get("repos", true);
|
|
13032
13032
|
if (isMap(repos) && repos.items.length > 0) return null;
|
|
13033
13033
|
const dir = m.get("dir");
|
|
@@ -13035,8 +13035,8 @@ function cloneBaseOf(text, route2, base, isCheckout) {
|
|
|
13035
13035
|
const abs = expandDir(String(dir), base);
|
|
13036
13036
|
return isCheckout(abs) ? null : abs;
|
|
13037
13037
|
}
|
|
13038
|
-
function routeSetsHooks(
|
|
13039
|
-
const m = routeMapOf(parseDocument(
|
|
13038
|
+
function routeSetsHooks(text2, route2, name) {
|
|
13039
|
+
const m = routeMapOf(parseDocument(text2), route2);
|
|
13040
13040
|
const repos = m.get("repos", true);
|
|
13041
13041
|
let target = m;
|
|
13042
13042
|
if (isMap(repos)) target = repos.get(name, true);
|
|
@@ -13143,12 +13143,12 @@ function planRepoAdd(i) {
|
|
|
13143
13143
|
}
|
|
13144
13144
|
return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched, droppedDir };
|
|
13145
13145
|
}
|
|
13146
|
-
function planRepoRemove(
|
|
13147
|
-
const doc = parseDocument(
|
|
13146
|
+
function planRepoRemove(text2, base, route2, path) {
|
|
13147
|
+
const doc = parseDocument(text2);
|
|
13148
13148
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
13149
13149
|
const m = routeMapOf(doc, route2);
|
|
13150
13150
|
const target = expandDir(path, base);
|
|
13151
|
-
const existing = configuredRepos(
|
|
13151
|
+
const existing = configuredRepos(text2, route2, base);
|
|
13152
13152
|
const hit = existing.find((e) => e.dir === target);
|
|
13153
13153
|
if (!hit) throw new ReposError(`${target} is not configured on route ${route2}`);
|
|
13154
13154
|
const repos = m.get("repos", true);
|
|
@@ -13273,8 +13273,8 @@ async function gatherHealth(c, check) {
|
|
|
13273
13273
|
function routesInScope(routes, named) {
|
|
13274
13274
|
return named ? routes.filter((r) => r.route === named) : routes;
|
|
13275
13275
|
}
|
|
13276
|
-
function planEnable(
|
|
13277
|
-
const doc = parseDocument2(
|
|
13276
|
+
function planEnable(text2, route2) {
|
|
13277
|
+
const doc = parseDocument2(text2);
|
|
13278
13278
|
routeMapOf(doc, route2).set("enabled", true);
|
|
13279
13279
|
return String(doc);
|
|
13280
13280
|
}
|
|
@@ -13468,13 +13468,13 @@ function realProvisionDeps(crewHome) {
|
|
|
13468
13468
|
enableRoute(route2) {
|
|
13469
13469
|
const file = configFile();
|
|
13470
13470
|
if (!file) return false;
|
|
13471
|
-
const
|
|
13472
|
-
const doc = parseDocument3(
|
|
13471
|
+
const text2 = readFileSync23(file, "utf8");
|
|
13472
|
+
const doc = parseDocument3(text2);
|
|
13473
13473
|
const routes = doc.get("routes");
|
|
13474
13474
|
const entry = routes?.items?.find((r) => r.get("route") === route2);
|
|
13475
13475
|
if (!entry) throw new Error(`route ${route2} is not in ${file}`);
|
|
13476
13476
|
if (entry.get("enabled") === true) return false;
|
|
13477
|
-
writeFileSync19(file, planEnable(
|
|
13477
|
+
writeFileSync19(file, planEnable(text2, route2));
|
|
13478
13478
|
return true;
|
|
13479
13479
|
},
|
|
13480
13480
|
installState() {
|
|
@@ -15658,18 +15658,18 @@ function readFrom(file, from) {
|
|
|
15658
15658
|
const buf = Buffer.alloc(size - from);
|
|
15659
15659
|
readSync2(fd, buf, 0, buf.length, from);
|
|
15660
15660
|
closeSync4(fd);
|
|
15661
|
-
const
|
|
15662
|
-
const lastNewline =
|
|
15661
|
+
const text2 = buf.toString("utf8");
|
|
15662
|
+
const lastNewline = text2.lastIndexOf("\n");
|
|
15663
15663
|
if (lastNewline === -1) return { events: [], offset: from };
|
|
15664
15664
|
const events = [];
|
|
15665
|
-
for (const line of
|
|
15665
|
+
for (const line of text2.slice(0, lastNewline).split("\n")) {
|
|
15666
15666
|
if (!line.trim()) continue;
|
|
15667
15667
|
try {
|
|
15668
15668
|
events.push(JSON.parse(line));
|
|
15669
15669
|
} catch {
|
|
15670
15670
|
}
|
|
15671
15671
|
}
|
|
15672
|
-
return { events, offset: from + Buffer.byteLength(
|
|
15672
|
+
return { events, offset: from + Buffer.byteLength(text2.slice(0, lastNewline + 1), "utf8") };
|
|
15673
15673
|
}
|
|
15674
15674
|
function startWatch(o) {
|
|
15675
15675
|
const out = o.out ?? ((s) => process.stdout.write(s));
|
|
@@ -15964,12 +15964,12 @@ function planNameSync(local, row, lastSynced) {
|
|
|
15964
15964
|
if (rowMoved) return { drift: "row", action: "adopt" };
|
|
15965
15965
|
return { drift: "local", action: "push" };
|
|
15966
15966
|
}
|
|
15967
|
-
function shipNameOfYaml(
|
|
15968
|
-
const v = parseDocument5(
|
|
15967
|
+
function shipNameOfYaml(text2) {
|
|
15968
|
+
const v = parseDocument5(text2).getIn(["ship", "name"]);
|
|
15969
15969
|
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
15970
15970
|
}
|
|
15971
|
-
function setShipNameInYaml(
|
|
15972
|
-
const doc = parseDocument5(
|
|
15971
|
+
function setShipNameInYaml(text2, name) {
|
|
15972
|
+
const doc = parseDocument5(text2);
|
|
15973
15973
|
doc.setIn(["ship", "name"], name);
|
|
15974
15974
|
return doc.toString();
|
|
15975
15975
|
}
|
|
@@ -16058,8 +16058,8 @@ async function reconcileShipName(i) {
|
|
|
16058
16058
|
}
|
|
16059
16059
|
if (result.status !== "ok") return result;
|
|
16060
16060
|
if (result.action === "adopt" || result.action === "row-wins") {
|
|
16061
|
-
const
|
|
16062
|
-
writeFileSync22(i.configFile, setShipNameInYaml(
|
|
16061
|
+
const text2 = readFileSync27(i.configFile, "utf8");
|
|
16062
|
+
writeFileSync22(i.configFile, setShipNameInYaml(text2, result.name));
|
|
16063
16063
|
i.ship.name = result.name;
|
|
16064
16064
|
if (result.action === "row-wins") {
|
|
16065
16065
|
i.emit?.warn(
|
|
@@ -16960,11 +16960,11 @@ function packageManager(f) {
|
|
|
16960
16960
|
return "npm";
|
|
16961
16961
|
}
|
|
16962
16962
|
function fromPackageJson(f) {
|
|
16963
|
-
const
|
|
16964
|
-
if (
|
|
16963
|
+
const text2 = f.read("package.json");
|
|
16964
|
+
if (text2 === void 0) return void 0;
|
|
16965
16965
|
let pkg;
|
|
16966
16966
|
try {
|
|
16967
|
-
pkg = JSON.parse(
|
|
16967
|
+
pkg = JSON.parse(text2);
|
|
16968
16968
|
} catch {
|
|
16969
16969
|
return void 0;
|
|
16970
16970
|
}
|
|
@@ -16987,11 +16987,11 @@ function fromPyproject(f) {
|
|
|
16987
16987
|
return f.exists("pyproject.toml") ? { test: "pytest" } : void 0;
|
|
16988
16988
|
}
|
|
16989
16989
|
function fromMakefile(f) {
|
|
16990
|
-
const
|
|
16991
|
-
if (
|
|
16990
|
+
const text2 = f.read("Makefile") ?? f.read("makefile") ?? f.read("GNUmakefile");
|
|
16991
|
+
if (text2 === void 0) return void 0;
|
|
16992
16992
|
const out = {};
|
|
16993
16993
|
for (const target of ["test", "build"]) {
|
|
16994
|
-
if (new RegExp(`^${target}\\s*:`, "m").test(
|
|
16994
|
+
if (new RegExp(`^${target}\\s*:`, "m").test(text2)) out[target] = `make ${target}`;
|
|
16995
16995
|
}
|
|
16996
16996
|
return out;
|
|
16997
16997
|
}
|
|
@@ -17084,16 +17084,16 @@ function parseHooksArgs(args, routes) {
|
|
|
17084
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]");
|
|
17085
17085
|
return { route: route2, name: rest[0] };
|
|
17086
17086
|
}
|
|
17087
|
-
function repoDirOf(
|
|
17088
|
-
const repos = configuredRepos(
|
|
17087
|
+
function repoDirOf(text2, base, route2, name) {
|
|
17088
|
+
const repos = configuredRepos(text2, route2, base);
|
|
17089
17089
|
const hit = repos.find((r) => r.name === name);
|
|
17090
17090
|
if (!hit) {
|
|
17091
17091
|
throw new ReposError(`route ${route2} has no repo named "${name}" (have: ${repos.map((r) => r.name).join(", ") || "none"})`);
|
|
17092
17092
|
}
|
|
17093
17093
|
return hit.dir;
|
|
17094
17094
|
}
|
|
17095
|
-
function planRouteHooks(
|
|
17096
|
-
const doc = parseDocument6(
|
|
17095
|
+
function planRouteHooks(text2, route2, name, hooks, opts = {}) {
|
|
17096
|
+
const doc = parseDocument6(text2);
|
|
17097
17097
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
17098
17098
|
const m = routeMapOf(doc, route2);
|
|
17099
17099
|
const repos = m.get("repos", true);
|
|
@@ -17129,19 +17129,19 @@ function planRouteHooks(text, route2, name, hooks, opts = {}) {
|
|
|
17129
17129
|
}
|
|
17130
17130
|
return { text: String(doc), written, kept, replaced };
|
|
17131
17131
|
}
|
|
17132
|
-
function clearRouteHooks(
|
|
17133
|
-
const doc = parseDocument6(
|
|
17132
|
+
function clearRouteHooks(text2, route2, name) {
|
|
17133
|
+
const doc = parseDocument6(text2);
|
|
17134
17134
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
17135
17135
|
const m = routeMapOf(doc, route2);
|
|
17136
17136
|
const repos = m.get("repos", true);
|
|
17137
17137
|
let target = m;
|
|
17138
17138
|
if (isMap2(repos)) {
|
|
17139
17139
|
const entry = repos.get(name, true);
|
|
17140
|
-
if (!isMap2(entry)) return { text, removed: [] };
|
|
17140
|
+
if (!isMap2(entry)) return { text: text2, removed: [] };
|
|
17141
17141
|
target = entry;
|
|
17142
17142
|
}
|
|
17143
17143
|
const current = target.get("hooks", true);
|
|
17144
|
-
if (!isMap2(current)) return { text, removed: [] };
|
|
17144
|
+
if (!isMap2(current)) return { text: text2, removed: [] };
|
|
17145
17145
|
const removed = current.items.map((i) => String(i.key.value ?? i.key));
|
|
17146
17146
|
target.delete("hooks");
|
|
17147
17147
|
return { text: String(doc), removed };
|
|
@@ -17165,9 +17165,9 @@ function starterRepoConfig(hooks, release) {
|
|
|
17165
17165
|
if (release.verifyMatch) lines.push(" verify:", ` match: ${release.verifyMatch}`);
|
|
17166
17166
|
lines.push("");
|
|
17167
17167
|
} else lines.push("", "release:", " # integrate: merging to the base branch is the release; nothing is deployed.", " mode: integrate", "");
|
|
17168
|
-
const
|
|
17169
|
-
parseRepoConfig(
|
|
17170
|
-
return
|
|
17168
|
+
const text2 = lines.join("\n");
|
|
17169
|
+
parseRepoConfig(text2, ".crew.yaml");
|
|
17170
|
+
return text2;
|
|
17171
17171
|
}
|
|
17172
17172
|
function existingRepoConfig(f) {
|
|
17173
17173
|
return REPO_CONFIG_FILENAMES.find((n) => f.exists(n));
|
|
@@ -17226,7 +17226,7 @@ function agentInvocation(spec, model) {
|
|
|
17226
17226
|
});
|
|
17227
17227
|
}
|
|
17228
17228
|
function parseAgentStream(out, dir) {
|
|
17229
|
-
let
|
|
17229
|
+
let text2 = "";
|
|
17230
17230
|
let isError = false;
|
|
17231
17231
|
const files = /* @__PURE__ */ new Set();
|
|
17232
17232
|
let base = dir;
|
|
@@ -17248,19 +17248,19 @@ function parseAgentStream(out, dir) {
|
|
|
17248
17248
|
if (typeof p === "string") files.add(isAbsolute2(p) ? relative(base, p) : p);
|
|
17249
17249
|
}
|
|
17250
17250
|
} else if (ev?.type === "result") {
|
|
17251
|
-
|
|
17251
|
+
text2 = typeof ev.result === "string" ? ev.result : "";
|
|
17252
17252
|
isError = ev.is_error === true;
|
|
17253
17253
|
}
|
|
17254
17254
|
}
|
|
17255
|
-
return { text, isError, filesRead: [...files].sort() };
|
|
17255
|
+
return { text: text2, isError, filesRead: [...files].sort() };
|
|
17256
17256
|
}
|
|
17257
|
-
function parseAgentHooks(
|
|
17258
|
-
const start =
|
|
17259
|
-
const end =
|
|
17257
|
+
function parseAgentHooks(text2) {
|
|
17258
|
+
const start = text2.indexOf("{");
|
|
17259
|
+
const end = text2.lastIndexOf("}");
|
|
17260
17260
|
if (start < 0 || end <= start) throw new AgentSuggestError("bad_output", "the toolchain did not answer with a JSON object");
|
|
17261
17261
|
let obj;
|
|
17262
17262
|
try {
|
|
17263
|
-
obj = JSON.parse(
|
|
17263
|
+
obj = JSON.parse(text2.slice(start, end + 1));
|
|
17264
17264
|
} catch {
|
|
17265
17265
|
throw new AgentSuggestError("bad_output", "the toolchain's answer was not valid JSON");
|
|
17266
17266
|
}
|
|
@@ -17292,7 +17292,7 @@ async function runAgentSuggest(o) {
|
|
|
17292
17292
|
const before = porcelain(o.dir);
|
|
17293
17293
|
const timeout = AbortSignal.timeout(o.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
17294
17294
|
const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
|
|
17295
|
-
const run2 = await new Promise((
|
|
17295
|
+
const run2 = await new Promise((resolve7) => {
|
|
17296
17296
|
let out = "";
|
|
17297
17297
|
let err3 = "";
|
|
17298
17298
|
const child = spawn4(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -17301,7 +17301,7 @@ async function runAgentSuggest(o) {
|
|
|
17301
17301
|
if (settled) return;
|
|
17302
17302
|
settled = true;
|
|
17303
17303
|
signal.removeEventListener("abort", onAbort);
|
|
17304
|
-
|
|
17304
|
+
resolve7(r);
|
|
17305
17305
|
};
|
|
17306
17306
|
const onAbort = () => {
|
|
17307
17307
|
try {
|
|
@@ -17745,7 +17745,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
17745
17745
|
}
|
|
17746
17746
|
|
|
17747
17747
|
// src/cli.ts
|
|
17748
|
-
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";
|
|
17749
17749
|
import { createInterface } from "readline/promises";
|
|
17750
17750
|
|
|
17751
17751
|
// src/periscope-cmd.ts
|
|
@@ -19073,7 +19073,7 @@ import { randomBytes as randomBytes2 } from "crypto";
|
|
|
19073
19073
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
19074
19074
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
19075
19075
|
var ALLOWANCE_PAUSE_MS = 5 * 60 * 1e3;
|
|
19076
|
-
var defaultSleep = (ms3) => new Promise((
|
|
19076
|
+
var defaultSleep = (ms3) => new Promise((resolve7) => setTimeout(resolve7, ms3));
|
|
19077
19077
|
async function runDaemonLoop(o) {
|
|
19078
19078
|
const floor = o.floorMs ?? DEFAULT_FLOOR_MS;
|
|
19079
19079
|
const ceiling = o.ceilingMs ?? DEFAULT_CEILING_MS;
|
|
@@ -19130,15 +19130,15 @@ async function runDaemonLoop(o) {
|
|
|
19130
19130
|
}
|
|
19131
19131
|
}
|
|
19132
19132
|
function latestMtime(path) {
|
|
19133
|
-
let
|
|
19133
|
+
let stat2;
|
|
19134
19134
|
try {
|
|
19135
|
-
|
|
19135
|
+
stat2 = statSync12(path);
|
|
19136
19136
|
} catch {
|
|
19137
19137
|
return 0;
|
|
19138
19138
|
}
|
|
19139
|
-
if (
|
|
19140
|
-
if (!
|
|
19141
|
-
let latest =
|
|
19139
|
+
if (stat2.isFile()) return stat2.mtimeMs;
|
|
19140
|
+
if (!stat2.isDirectory()) return 0;
|
|
19141
|
+
let latest = stat2.mtimeMs;
|
|
19142
19142
|
for (const entry of readdirSync16(path, { withFileTypes: true })) {
|
|
19143
19143
|
latest = Math.max(latest, latestMtime(join42(path, entry.name)));
|
|
19144
19144
|
}
|
|
@@ -19331,8 +19331,8 @@ async function runOnePassUnguarded(o) {
|
|
|
19331
19331
|
started.push(role);
|
|
19332
19332
|
let ready = () => {
|
|
19333
19333
|
};
|
|
19334
|
-
const settled = new Promise((
|
|
19335
|
-
ready =
|
|
19334
|
+
const settled = new Promise((resolve7) => {
|
|
19335
|
+
ready = resolve7;
|
|
19336
19336
|
});
|
|
19337
19337
|
const run2 = spawn9(role, decision, ctx, { ready }).then(() => {
|
|
19338
19338
|
breaker.recordSuccess(role);
|
|
@@ -19784,14 +19784,18 @@ function parseLine(line) {
|
|
|
19784
19784
|
return null;
|
|
19785
19785
|
}
|
|
19786
19786
|
}
|
|
19787
|
-
async function execute(h, args, streaming) {
|
|
19788
|
-
const
|
|
19787
|
+
async function execute(h, args, streaming, after) {
|
|
19788
|
+
const finishRaw = (r) => {
|
|
19789
19789
|
if (r.code !== 0) return { status: failureStatus(r), body: { error: cliMessage(r) } };
|
|
19790
19790
|
const done = lastJsonLine(r.stdout);
|
|
19791
19791
|
return done ? { status: 200, body: done } : { status: 502, body: { error: "the command finished without a result" } };
|
|
19792
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
|
+
};
|
|
19793
19797
|
if (!streaming) {
|
|
19794
|
-
const f = finish(await h.run(args, () => {
|
|
19798
|
+
const f = await finish(await h.run(args, () => {
|
|
19795
19799
|
}));
|
|
19796
19800
|
return reply(f.status, f.body);
|
|
19797
19801
|
}
|
|
@@ -19803,7 +19807,7 @@ async function execute(h, args, streaming) {
|
|
|
19803
19807
|
const ev = parseLine(line);
|
|
19804
19808
|
if (ev) write(ev);
|
|
19805
19809
|
});
|
|
19806
|
-
const f = finish(r);
|
|
19810
|
+
const f = await finish(r);
|
|
19807
19811
|
write(f.status === 200 ? f.body : { event: "error", status: f.status, message: f.body.error });
|
|
19808
19812
|
}
|
|
19809
19813
|
};
|
|
@@ -19867,7 +19871,10 @@ async function handlePeriscopes(h, q) {
|
|
|
19867
19871
|
`--${b.outcome}`,
|
|
19868
19872
|
...b.allowEmpty === true ? ["--allow-empty"] : [],
|
|
19869
19873
|
"--json"
|
|
19870
|
-
], q.wantsStream)
|
|
19874
|
+
], q.wantsStream, h.mcp ? async (done) => {
|
|
19875
|
+
await h.mcp.onDown(picked, done);
|
|
19876
|
+
return done;
|
|
19877
|
+
} : void 0);
|
|
19871
19878
|
}
|
|
19872
19879
|
const repo = plainText(b.repo, "repo", true);
|
|
19873
19880
|
const topic = plainText(b.topic, "topic", true);
|
|
@@ -19895,12 +19902,13 @@ async function handlePeriscopes(h, q) {
|
|
|
19895
19902
|
h.previewHost,
|
|
19896
19903
|
...requestedBy ? ["--requested-by", requestedBy] : [],
|
|
19897
19904
|
...b.isolate === true ? ["--isolate"] : [],
|
|
19905
|
+
...h.mcp ? ["--serve-mcp"] : [],
|
|
19898
19906
|
"--no-open",
|
|
19899
19907
|
"--json"
|
|
19900
|
-
], q.wantsStream);
|
|
19908
|
+
], q.wantsStream, h.mcp ? async (done) => ({ ...done, ...await h.mcp.onUp(picked, done) }) : void 0);
|
|
19901
19909
|
}
|
|
19902
19910
|
function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
19903
|
-
return new Promise((
|
|
19911
|
+
return new Promise((resolve7) => {
|
|
19904
19912
|
const child = spawn7(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
19905
19913
|
let stdout = "";
|
|
19906
19914
|
let stderr = "";
|
|
@@ -19909,9 +19917,9 @@ function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
|
19909
19917
|
stdout += d.toString("utf8");
|
|
19910
19918
|
});
|
|
19911
19919
|
child.stderr.on("data", (d) => {
|
|
19912
|
-
const
|
|
19913
|
-
stderr +=
|
|
19914
|
-
pending +=
|
|
19920
|
+
const text2 = d.toString("utf8");
|
|
19921
|
+
stderr += text2;
|
|
19922
|
+
pending += text2;
|
|
19915
19923
|
let i;
|
|
19916
19924
|
while ((i = pending.indexOf("\n")) >= 0) {
|
|
19917
19925
|
const line = pending.slice(0, i).trim();
|
|
@@ -19922,12 +19930,12 @@ function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
|
19922
19930
|
const t = setTimeout(() => child.kill("SIGTERM"), timeoutMs);
|
|
19923
19931
|
child.on("error", (e) => {
|
|
19924
19932
|
clearTimeout(t);
|
|
19925
|
-
|
|
19933
|
+
resolve7({ code: 127, stdout, stderr: `${stderr}${e.message}
|
|
19926
19934
|
` });
|
|
19927
19935
|
});
|
|
19928
19936
|
child.on("close", (code) => {
|
|
19929
19937
|
clearTimeout(t);
|
|
19930
|
-
|
|
19938
|
+
resolve7({ code: code ?? 1, stdout, stderr });
|
|
19931
19939
|
});
|
|
19932
19940
|
});
|
|
19933
19941
|
}
|
|
@@ -20048,16 +20056,16 @@ function lastLines2(path, n) {
|
|
|
20048
20056
|
const cursorAt = (off) => encodeCursor({ dev: st.dev, ino: st.ino, off });
|
|
20049
20057
|
if (n <= 0 || st.size === 0) return { lines: [], cursor: cursorAt(st.size), reset: false };
|
|
20050
20058
|
let start = st.size;
|
|
20051
|
-
let
|
|
20052
|
-
while (start > 0 &&
|
|
20059
|
+
let text2 = "";
|
|
20060
|
+
while (start > 0 && text2.split("\n").length <= n + 1 && st.size - start < MAX_TAIL_BYTES) {
|
|
20053
20061
|
const step = Math.min(64 * 1024, start);
|
|
20054
20062
|
start -= step;
|
|
20055
|
-
|
|
20063
|
+
text2 = readRange(path, start, step).text + text2;
|
|
20056
20064
|
}
|
|
20057
|
-
let lines =
|
|
20065
|
+
let lines = text2.split("\n");
|
|
20058
20066
|
if (start > 0) lines = lines.slice(1);
|
|
20059
20067
|
let end = st.size;
|
|
20060
|
-
if (!
|
|
20068
|
+
if (!text2.endsWith("\n")) {
|
|
20061
20069
|
const frag = lines.pop() ?? "";
|
|
20062
20070
|
end -= Buffer.byteLength(frag);
|
|
20063
20071
|
} else lines.pop();
|
|
@@ -20099,9 +20107,9 @@ function readTail(live, opts) {
|
|
|
20099
20107
|
off = 0;
|
|
20100
20108
|
size = statSync13(path).size;
|
|
20101
20109
|
}
|
|
20102
|
-
const { text } = readRange(path, off, max);
|
|
20103
|
-
const cut =
|
|
20104
|
-
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);
|
|
20105
20113
|
const st = statSync13(path);
|
|
20106
20114
|
return {
|
|
20107
20115
|
lines: whole.split("\n").filter((l) => l.length),
|
|
@@ -20110,7 +20118,7 @@ function readTail(live, opts) {
|
|
|
20110
20118
|
};
|
|
20111
20119
|
}
|
|
20112
20120
|
function waitForChange(dir, ms3, signal) {
|
|
20113
|
-
return new Promise((
|
|
20121
|
+
return new Promise((resolve7) => {
|
|
20114
20122
|
let w;
|
|
20115
20123
|
const done = () => {
|
|
20116
20124
|
clearTimeout(t);
|
|
@@ -20120,7 +20128,7 @@ function waitForChange(dir, ms3, signal) {
|
|
|
20120
20128
|
w?.close();
|
|
20121
20129
|
} catch {
|
|
20122
20130
|
}
|
|
20123
|
-
|
|
20131
|
+
resolve7();
|
|
20124
20132
|
};
|
|
20125
20133
|
const t = setTimeout(done, ms3);
|
|
20126
20134
|
const poll = setInterval(done, 500);
|
|
@@ -20267,13 +20275,13 @@ function send(res, r) {
|
|
|
20267
20275
|
}
|
|
20268
20276
|
async function socketIsStale(path) {
|
|
20269
20277
|
if (!existsSync40(path)) return false;
|
|
20270
|
-
return new Promise((
|
|
20278
|
+
return new Promise((resolve7) => {
|
|
20271
20279
|
const c = createConnection(path);
|
|
20272
20280
|
c.once("connect", () => {
|
|
20273
20281
|
c.destroy();
|
|
20274
|
-
|
|
20282
|
+
resolve7(false);
|
|
20275
20283
|
});
|
|
20276
|
-
c.once("error", () =>
|
|
20284
|
+
c.once("error", () => resolve7(true));
|
|
20277
20285
|
});
|
|
20278
20286
|
}
|
|
20279
20287
|
async function startControlServer(deps) {
|
|
@@ -20294,6 +20302,11 @@ async function startControlServer(deps) {
|
|
|
20294
20302
|
res.on("close", () => abort.abort());
|
|
20295
20303
|
void (async () => {
|
|
20296
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
|
+
}
|
|
20297
20310
|
const m = /^Bearer (.+)$/.exec(req.headers.authorization ?? "");
|
|
20298
20311
|
const verdict = await deps.auth.verify(m?.[1]?.trim() ?? "");
|
|
20299
20312
|
if (verdict === "unauthorized") return send(res, err2(401, "invalid or revoked key"));
|
|
@@ -20307,10 +20320,10 @@ async function startControlServer(deps) {
|
|
|
20307
20320
|
}
|
|
20308
20321
|
})();
|
|
20309
20322
|
});
|
|
20310
|
-
return new Promise((
|
|
20323
|
+
return new Promise((resolve7) => {
|
|
20311
20324
|
server.once("error", (e) => {
|
|
20312
20325
|
log(`control API not started: ${e.message}`);
|
|
20313
|
-
|
|
20326
|
+
resolve7(null);
|
|
20314
20327
|
});
|
|
20315
20328
|
server.listen(path, () => {
|
|
20316
20329
|
try {
|
|
@@ -20318,7 +20331,7 @@ async function startControlServer(deps) {
|
|
|
20318
20331
|
} catch {
|
|
20319
20332
|
}
|
|
20320
20333
|
server.on("error", (e) => log(`control API error: ${e.message}`));
|
|
20321
|
-
|
|
20334
|
+
resolve7({
|
|
20322
20335
|
server,
|
|
20323
20336
|
path,
|
|
20324
20337
|
close: () => new Promise((done) => {
|
|
@@ -20349,20 +20362,434 @@ function schedulerHandler(control, run2 = runDetached) {
|
|
|
20349
20362
|
};
|
|
20350
20363
|
}
|
|
20351
20364
|
function crewJson(args, timeoutMs = 6e4) {
|
|
20352
|
-
return new Promise((
|
|
20365
|
+
return new Promise((resolve7) => {
|
|
20353
20366
|
execFile(process.execPath, [process.argv[1] ?? "", ...args], { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (_e, stdout) => {
|
|
20354
20367
|
try {
|
|
20355
20368
|
const v = JSON.parse(String(stdout));
|
|
20356
|
-
|
|
20369
|
+
resolve7(v && typeof v === "object" && !Array.isArray(v) ? v : null);
|
|
20357
20370
|
} catch {
|
|
20358
|
-
|
|
20371
|
+
resolve7(null);
|
|
20359
20372
|
}
|
|
20360
20373
|
});
|
|
20361
20374
|
});
|
|
20362
20375
|
}
|
|
20363
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
|
+
|
|
20364
20791
|
// src/cli.ts
|
|
20365
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
20792
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname23(process.execPath) : resolve6(dirname23(fileURLToPath(import.meta.url)), "..");
|
|
20366
20793
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
20367
20794
|
var CYCLE_SECONDS2 = 120;
|
|
20368
20795
|
var STALE_CYCLES = 3;
|
|
@@ -20502,7 +20929,7 @@ No project here has what a route needs. Install the "${template.name}" library t
|
|
|
20502
20929
|
return installed.project.slug;
|
|
20503
20930
|
}
|
|
20504
20931
|
function usage(problem, requested = false) {
|
|
20505
|
-
const
|
|
20932
|
+
const text2 = `crew \u2014 a standing team of headless agents
|
|
20506
20933
|
|
|
20507
20934
|
crew poll [route] decide a cycle and report it; writes nothing
|
|
20508
20935
|
crew run [route] [--role R] run the winning role's session
|
|
@@ -20528,7 +20955,7 @@ function usage(problem, requested = false) {
|
|
|
20528
20955
|
crew reap [route] kill orphaned servers, drop worktrees for closed tickets
|
|
20529
20956
|
crew drop [route] NNN remove a merged ticket's worktree and branch
|
|
20530
20957
|
crew rotate-passenger-url [route] force a Host Passengers tunnel to reconnect with a fresh public URL
|
|
20531
|
-
crew periscope up [[route] REPO] [--topic TEXT|--issue NNN] [--isolate] [--host H] [--assignee ID] [--no-open] [--open] [--dry-run] [--json]
|
|
20958
|
+
crew periscope up [[route] REPO] [--topic TEXT|--issue NNN] [--isolate] [--serve-mcp] [--host H] [--assignee ID] [--no-open] [--open] [--dry-run] [--json]
|
|
20532
20959
|
crew periscope up --resume NNN restart the preview an idle periscope's server was stopped for
|
|
20533
20960
|
file a draft ticket, cut and prepare its worktree, start the repo's preview hook and record a Periscopes row (alias: start)
|
|
20534
20961
|
crew periscope down [[route] NNN] --keep|--discard [--allow-empty] [--json]
|
|
@@ -20581,12 +21008,12 @@ Options:
|
|
|
20581
21008
|
In watch: space pauses the stream, q quits.
|
|
20582
21009
|
`;
|
|
20583
21010
|
if (requested) {
|
|
20584
|
-
process.stdout.write(
|
|
21011
|
+
process.stdout.write(text2);
|
|
20585
21012
|
process.exit(0);
|
|
20586
21013
|
}
|
|
20587
21014
|
process.stderr.write(problem ? `crew: ${problem}
|
|
20588
21015
|
|
|
20589
|
-
${
|
|
21016
|
+
${text2}` : text2);
|
|
20590
21017
|
process.exit(2);
|
|
20591
21018
|
}
|
|
20592
21019
|
var argv = process.argv.slice(2);
|
|
@@ -20759,6 +21186,44 @@ if (command === "passenger-sync-daemon") {
|
|
|
20759
21186
|
return void 0;
|
|
20760
21187
|
}
|
|
20761
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
|
+
};
|
|
20762
21227
|
}, endpointReconcileDeps = function(routes, relayHost) {
|
|
20763
21228
|
return {
|
|
20764
21229
|
stateDir: cfg.ship.stateDir,
|
|
@@ -20785,7 +21250,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20785
21250
|
const unsafe = baseBranchUnsafe(out);
|
|
20786
21251
|
return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
|
|
20787
21252
|
};
|
|
20788
|
-
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;
|
|
20789
21254
|
let cfg;
|
|
20790
21255
|
let firstRun;
|
|
20791
21256
|
let firstRunFromFlags = false;
|
|
@@ -20841,7 +21306,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20841
21306
|
} else {
|
|
20842
21307
|
const toleratesNoRoutes = command === "credential" || command === "forge" || command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
20843
21308
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
20844
|
-
if (configless && !
|
|
21309
|
+
if (configless && !existsSync42(findConfigFile(CREW_HOME))) {
|
|
20845
21310
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
20846
21311
|
} else {
|
|
20847
21312
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -20856,7 +21321,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20856
21321
|
throw e;
|
|
20857
21322
|
}
|
|
20858
21323
|
if (command === "credential") {
|
|
20859
|
-
const r = runCredential(positional[1],
|
|
21324
|
+
const r = runCredential(positional[1], readFileSync37(0, "utf8"), cfg.ship.forges, cfg.ship.stateDir);
|
|
20860
21325
|
process.stdout.write(r.out);
|
|
20861
21326
|
process.stderr.write(r.err);
|
|
20862
21327
|
process.exit(r.code);
|
|
@@ -20868,9 +21333,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
20868
21333
|
forges: cfg.ship.forges,
|
|
20869
21334
|
stateDir: cfg.ship.stateDir,
|
|
20870
21335
|
configFile: cfg.configFile,
|
|
20871
|
-
readFile: (p) =>
|
|
20872
|
-
writeFile: (p, t) =>
|
|
20873
|
-
stdin: () =>
|
|
21336
|
+
readFile: (p) => readFileSync37(p, "utf8"),
|
|
21337
|
+
writeFile: (p, t) => writeFileSync29(p, t),
|
|
21338
|
+
stdin: () => readFileSync37(0, "utf8"),
|
|
20874
21339
|
env: process.env
|
|
20875
21340
|
}
|
|
20876
21341
|
);
|
|
@@ -21002,6 +21467,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21002
21467
|
for (const plan of containerPlans) pending.push(reconcileEndpoint(plan.workspaceId, reconcile));
|
|
21003
21468
|
for (const plan of plans) pending.push(reconcileRelayUrl(plan.workspaceId, apiRoutes));
|
|
21004
21469
|
await Promise.all(pending);
|
|
21470
|
+
await reconcilePeriscopeEndpoints();
|
|
21005
21471
|
} catch (e) {
|
|
21006
21472
|
emit.warn(`ship tunnel sync failed: ${e.message}`, { step: "passengers" });
|
|
21007
21473
|
}
|
|
@@ -21021,6 +21487,22 @@ if (command === "passenger-sync-daemon") {
|
|
|
21021
21487
|
emit.warn(`could not write relay_url: ${e.message}`, { step: "passengers" });
|
|
21022
21488
|
}
|
|
21023
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
|
+
}
|
|
21024
21506
|
async function releaseFleet(opts = {}) {
|
|
21025
21507
|
if (releaseRefused()) return;
|
|
21026
21508
|
precheckTickets.clear();
|
|
@@ -21136,8 +21618,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
21136
21618
|
}
|
|
21137
21619
|
if (host === "linux" && hasSystemd) {
|
|
21138
21620
|
const label = labelFor(CREW_HOME, "run");
|
|
21139
|
-
const staleTimer =
|
|
21140
|
-
if (
|
|
21621
|
+
const staleTimer = join46(homedir10(), ".config", "systemd", "user", `${label}.timer`);
|
|
21622
|
+
if (existsSync42(staleTimer)) {
|
|
21141
21623
|
process.stdout.write(
|
|
21142
21624
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
21143
21625
|
`
|
|
@@ -21164,7 +21646,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21164
21646
|
const tracker = new Tracker(c, cfg.ship);
|
|
21165
21647
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
21166
21648
|
for (const r of await resolvedRepos(c)) {
|
|
21167
|
-
if (!
|
|
21649
|
+
if (!existsSync42(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
21168
21650
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
21169
21651
|
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
21170
21652
|
}
|
|
@@ -21182,7 +21664,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21182
21664
|
}
|
|
21183
21665
|
async function doctorFix(o) {
|
|
21184
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))));
|
|
21185
|
-
const unitExists = (p) =>
|
|
21667
|
+
const unitExists = (p) => existsSync42(p);
|
|
21186
21668
|
const crontab = o.host === "linux" ? spawnSync8("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
21187
21669
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run", installContext()), unitExists, crontab) : true;
|
|
21188
21670
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -21199,7 +21681,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
21199
21681
|
for (const a of plan.actions) {
|
|
21200
21682
|
if (a.kind === "enable") {
|
|
21201
21683
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
21202
|
-
|
|
21684
|
+
writeFileSync29(cfg.configFile, planEnable(readFileSync37(cfg.configFile, "utf8"), a.route));
|
|
21203
21685
|
enabledSound.add(a.route);
|
|
21204
21686
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
21205
21687
|
} else {
|
|
@@ -22320,7 +22802,7 @@ ${cu.tail.trimEnd()}
|
|
|
22320
22802
|
process.exit(1);
|
|
22321
22803
|
}
|
|
22322
22804
|
requireArmed("run the daemon loop");
|
|
22323
|
-
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")];
|
|
22324
22806
|
const configPaths = daemonConfigPaths(cfg);
|
|
22325
22807
|
watchPaths.push(...configPaths);
|
|
22326
22808
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -22348,8 +22830,22 @@ ${cu.tail.trimEnd()}
|
|
|
22348
22830
|
previewHost: cfg.ship.previewHost,
|
|
22349
22831
|
maxPeriscopes: cfg.ship.maxPeriscopes,
|
|
22350
22832
|
run: runCrew,
|
|
22351
|
-
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
|
+
}
|
|
22352
22847
|
},
|
|
22848
|
+
periscopeMcp: controlRoutes.some((r) => r.enabled && r.hostPeriscopes) ? (req, res, ticket) => handlePeriscopeMcp(periscopeMcpDeps(), req, res, ticket) : void 0,
|
|
22353
22849
|
status: () => crewJson(["status", route2.route, "--json"]),
|
|
22354
22850
|
doctor: () => crewJson(["doctor", route2.route, "--json", "--offline"]),
|
|
22355
22851
|
scheduler: schedulerHandler(planDaemonControl(CREW_HOME, hostPlatform(), detectSystemd())),
|
|
@@ -22581,10 +23077,10 @@ ${cu.tail.trimEnd()}
|
|
|
22581
23077
|
`);
|
|
22582
23078
|
if (!dryRun) {
|
|
22583
23079
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
22584
|
-
const raw =
|
|
23080
|
+
const raw = existsSync42(resolvedPath) ? JSON.parse(readFileSync37(resolvedPath, "utf8")) : {};
|
|
22585
23081
|
raw.agentSkills = result.agentSkills;
|
|
22586
|
-
|
|
22587
|
-
|
|
23082
|
+
mkdirSync27(dirOf2(resolvedPath), { recursive: true });
|
|
23083
|
+
writeFileSync29(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
22588
23084
|
`);
|
|
22589
23085
|
}
|
|
22590
23086
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -22607,7 +23103,7 @@ ${cu.tail.trimEnd()}
|
|
|
22607
23103
|
throw e;
|
|
22608
23104
|
}
|
|
22609
23105
|
const oldName = cfg.ship.name;
|
|
22610
|
-
|
|
23106
|
+
writeFileSync29(cfg.configFile, setShipNameInYaml(readFileSync37(cfg.configFile, "utf8"), newName));
|
|
22611
23107
|
cfg.ship.name = newName;
|
|
22612
23108
|
let renameMachineId;
|
|
22613
23109
|
try {
|
|
@@ -22654,14 +23150,14 @@ ${cu.tail.trimEnd()}
|
|
|
22654
23150
|
process.exit(2);
|
|
22655
23151
|
};
|
|
22656
23152
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
|
|
22657
|
-
const configText =
|
|
23153
|
+
const configText = readFileSync37(cfg.configFile, "utf8");
|
|
22658
23154
|
if (sub === "check") {
|
|
22659
23155
|
try {
|
|
22660
23156
|
const routeArg = value("route") ?? positional[2];
|
|
22661
23157
|
const found = cfg.routes.find((r) => r.route === routeArg);
|
|
22662
23158
|
if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
|
|
22663
23159
|
const named2 = value("route") ? void 0 : positional[3];
|
|
22664
|
-
const dir = named2 ? repoDirOf(configText,
|
|
23160
|
+
const dir = named2 ? repoDirOf(configText, dirname23(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
|
|
22665
23161
|
const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
|
|
22666
23162
|
const o = target ? found.repoOverrides[target.name] : void 0;
|
|
22667
23163
|
runReposCheck(dir, {
|
|
@@ -22684,17 +23180,17 @@ ${cu.tail.trimEnd()}
|
|
|
22684
23180
|
if (sub === "hooks") {
|
|
22685
23181
|
try {
|
|
22686
23182
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
22687
|
-
const dir = repoDirOf(configText,
|
|
22688
|
-
if (!
|
|
22689
|
-
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) => {
|
|
22690
23186
|
try {
|
|
22691
|
-
return
|
|
23187
|
+
return readdirSync19(resolvePath(dir, r));
|
|
22692
23188
|
} catch {
|
|
22693
23189
|
return [];
|
|
22694
23190
|
}
|
|
22695
23191
|
}, read: (r) => {
|
|
22696
23192
|
try {
|
|
22697
|
-
return
|
|
23193
|
+
return readFileSync37(resolvePath(dir, r), "utf8");
|
|
22698
23194
|
} catch {
|
|
22699
23195
|
return void 0;
|
|
22700
23196
|
}
|
|
@@ -22736,19 +23232,19 @@ ${cu.tail.trimEnd()}
|
|
|
22736
23232
|
let cleared;
|
|
22737
23233
|
if (clear2) {
|
|
22738
23234
|
const plan = clearRouteHooks(configText, a.route, a.name);
|
|
22739
|
-
if (plan.removed.length > 0)
|
|
23235
|
+
if (plan.removed.length > 0) writeFileSync29(cfg.configFile, plan.text);
|
|
22740
23236
|
cleared = { file: cfg.configFile, removed: plan.removed };
|
|
22741
23237
|
}
|
|
22742
23238
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
22743
23239
|
if (write === "route") {
|
|
22744
23240
|
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
|
|
22745
|
-
|
|
23241
|
+
writeFileSync29(cfg.configFile, plan.text);
|
|
22746
23242
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
|
|
22747
23243
|
} else if (write === "repo") {
|
|
22748
23244
|
const have = existingRepoConfig(files);
|
|
22749
23245
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
22750
23246
|
const file = resolvePath(dir, ".crew.yaml");
|
|
22751
|
-
|
|
23247
|
+
writeFileSync29(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
22752
23248
|
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
|
|
22753
23249
|
}
|
|
22754
23250
|
if (flag("json")) {
|
|
@@ -22804,7 +23300,7 @@ ${cu.tail.trimEnd()}
|
|
|
22804
23300
|
if (sub === "list" && !routeName) usage2();
|
|
22805
23301
|
if (sub === "add") {
|
|
22806
23302
|
try {
|
|
22807
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
23303
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync42(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
|
|
22808
23304
|
routeName = a.route;
|
|
22809
23305
|
addPath = a.path;
|
|
22810
23306
|
addName = a.name;
|
|
@@ -22833,66 +23329,66 @@ ${cu.tail.trimEnd()}
|
|
|
22833
23329
|
}
|
|
22834
23330
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
22835
23331
|
let resolvedRemotes;
|
|
22836
|
-
if (
|
|
23332
|
+
if (existsSync42(resolvedFile)) {
|
|
22837
23333
|
try {
|
|
22838
|
-
resolvedRemotes = JSON.parse(
|
|
23334
|
+
resolvedRemotes = JSON.parse(readFileSync37(resolvedFile, "utf8"));
|
|
22839
23335
|
} catch {
|
|
22840
23336
|
}
|
|
22841
23337
|
}
|
|
22842
23338
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
22843
|
-
const
|
|
22844
|
-
const base =
|
|
22845
|
-
const isCheckout = (d) =>
|
|
23339
|
+
const text2 = configText;
|
|
23340
|
+
const base = dirname23(cfg.configFile);
|
|
23341
|
+
const isCheckout = (d) => existsSync42(`${d}/.git`);
|
|
22846
23342
|
try {
|
|
22847
23343
|
if (sub === "list") {
|
|
22848
23344
|
if (flag("json")) {
|
|
22849
23345
|
const rows = listRepoRows({
|
|
22850
|
-
text,
|
|
23346
|
+
text: text2,
|
|
22851
23347
|
route: routeName,
|
|
22852
23348
|
base,
|
|
22853
23349
|
resolved: resolvedRemotes,
|
|
22854
23350
|
originOf,
|
|
22855
|
-
exists: (d) =>
|
|
23351
|
+
exists: (d) => existsSync42(d),
|
|
22856
23352
|
isCheckout,
|
|
22857
23353
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
22858
23354
|
try {
|
|
22859
|
-
return Object.keys(parseRepoConfig(
|
|
23355
|
+
return Object.keys(parseRepoConfig(readFileSync37(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
22860
23356
|
} catch {
|
|
22861
23357
|
return false;
|
|
22862
23358
|
}
|
|
22863
23359
|
})
|
|
22864
23360
|
});
|
|
22865
|
-
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) })}
|
|
22866
23362
|
`);
|
|
22867
23363
|
break;
|
|
22868
23364
|
}
|
|
22869
|
-
const cloneBase = cloneBaseOf(
|
|
22870
|
-
const repos = configuredRepos(
|
|
23365
|
+
const cloneBase = cloneBaseOf(text2, routeName, base, isCheckout);
|
|
23366
|
+
const repos = configuredRepos(text2, routeName, base).filter((r) => r.dir !== cloneBase);
|
|
22871
23367
|
if (repos.length === 0) process.stdout.write(`route ${routeName} has no checkout configured \u2014 \`crew repos add ${routeName} <path>\`
|
|
22872
23368
|
`);
|
|
22873
23369
|
for (const r of repos) {
|
|
22874
23370
|
const origin2 = originOf(r.dir);
|
|
22875
23371
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
22876
|
-
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)"}`;
|
|
22877
23373
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
22878
23374
|
`);
|
|
22879
23375
|
}
|
|
22880
23376
|
break;
|
|
22881
23377
|
}
|
|
22882
23378
|
if (sub === "clone") {
|
|
22883
|
-
const
|
|
22884
|
-
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 })}
|
|
22885
23381
|
` : `${human}
|
|
22886
23382
|
`);
|
|
22887
|
-
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
22888
|
-
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)) {
|
|
22889
23385
|
throw new ReposError(
|
|
22890
|
-
|
|
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`
|
|
22891
23387
|
);
|
|
22892
23388
|
}
|
|
22893
23389
|
if (dryRun) {
|
|
22894
23390
|
say("dry-run", { remote: plan2.remote, target: plan2.target }, `would clone ${plan2.remote} into ${plan2.target}`);
|
|
22895
|
-
if (!
|
|
23391
|
+
if (!json2) process.stdout.write(`${plan2.block}
|
|
22896
23392
|
`);
|
|
22897
23393
|
break;
|
|
22898
23394
|
}
|
|
@@ -22905,18 +23401,18 @@ ${cu.tail.trimEnd()}
|
|
|
22905
23401
|
throw new ReposError(`${e.message}${authHint}
|
|
22906
23402
|
crew.yaml was not changed`);
|
|
22907
23403
|
}
|
|
22908
|
-
|
|
23404
|
+
writeFileSync29(cfg.configFile, plan2.text);
|
|
22909
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:
|
|
22910
23406
|
`);
|
|
22911
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}`);
|
|
22912
23408
|
break;
|
|
22913
23409
|
}
|
|
22914
23410
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
|
|
22915
|
-
const top =
|
|
23411
|
+
const top = existsSync42(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
22916
23412
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
22917
23413
|
const origin = originOf(top);
|
|
22918
23414
|
const plan = planRepoAdd({
|
|
22919
|
-
text,
|
|
23415
|
+
text: text2,
|
|
22920
23416
|
base,
|
|
22921
23417
|
route: routeName,
|
|
22922
23418
|
path: top,
|
|
@@ -22964,15 +23460,15 @@ ${cu.tail.trimEnd()}
|
|
|
22964
23460
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
22965
23461
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
22966
23462
|
`);
|
|
22967
|
-
if (
|
|
22968
|
-
const raw = JSON.parse(
|
|
23463
|
+
if (existsSync42(resolvedFile)) {
|
|
23464
|
+
const raw = JSON.parse(readFileSync37(resolvedFile, "utf8"));
|
|
22969
23465
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
22970
23466
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
22971
|
-
|
|
23467
|
+
writeFileSync29(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
22972
23468
|
`);
|
|
22973
23469
|
}
|
|
22974
23470
|
}
|
|
22975
|
-
|
|
23471
|
+
writeFileSync29(cfg.configFile, plan.text);
|
|
22976
23472
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
22977
23473
|
`);
|
|
22978
23474
|
} catch (e) {
|
|
@@ -23339,7 +23835,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23339
23835
|
process.exit(2);
|
|
23340
23836
|
}
|
|
23341
23837
|
if (found.shipNameAdopted && !dryRun) {
|
|
23342
|
-
|
|
23838
|
+
writeFileSync29(cfg.configFile, setShipNameInYaml(readFileSync37(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
23343
23839
|
cfg.ship.name = found.shipNameAdopted;
|
|
23344
23840
|
}
|
|
23345
23841
|
if (found.shipRow) {
|
|
@@ -23382,10 +23878,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23382
23878
|
}
|
|
23383
23879
|
}
|
|
23384
23880
|
let previous;
|
|
23385
|
-
const hadPreviousFile =
|
|
23881
|
+
const hadPreviousFile = existsSync42(resolvedPath);
|
|
23386
23882
|
if (hadPreviousFile) {
|
|
23387
23883
|
try {
|
|
23388
|
-
previous = JSON.parse(
|
|
23884
|
+
previous = JSON.parse(readFileSync37(resolvedPath, "utf8"));
|
|
23389
23885
|
} catch {
|
|
23390
23886
|
}
|
|
23391
23887
|
}
|
|
@@ -23515,10 +24011,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23515
24011
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
23516
24012
|
`);
|
|
23517
24013
|
} else {
|
|
23518
|
-
|
|
24014
|
+
mkdirSync27(dirname23(resolvedPath), { recursive: true });
|
|
23519
24015
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
23520
24016
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
23521
|
-
|
|
24017
|
+
writeFileSync29(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
23522
24018
|
`);
|
|
23523
24019
|
renameSync4(tmpPath, resolvedPath);
|
|
23524
24020
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -23564,8 +24060,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23564
24060
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
23565
24061
|
`);
|
|
23566
24062
|
} else {
|
|
23567
|
-
|
|
23568
|
-
|
|
24063
|
+
mkdirSync27(dirname23(apiKeyFile), { recursive: true });
|
|
24064
|
+
writeFileSync29(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
23569
24065
|
`, { mode: 384 });
|
|
23570
24066
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
23571
24067
|
`);
|
|
@@ -24252,6 +24748,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
24252
24748
|
},
|
|
24253
24749
|
setRowState: (id, state2) => trD.updatePeriscope(id, { state: state2 }),
|
|
24254
24750
|
forgetLocal: (id) => {
|
|
24751
|
+
removePeriscopeSecret(cfg.ship.stateDir, id);
|
|
24255
24752
|
removePeriscopeSidecar(cfg.ship.stateDir, id);
|
|
24256
24753
|
removePeriscopePrior(cfg.ship.stateDir, id);
|
|
24257
24754
|
},
|
|
@@ -24300,7 +24797,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
24300
24797
|
const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
|
|
24301
24798
|
const resumed = await runPeriscopeResume({
|
|
24302
24799
|
find: async () => hit && { row: hit.row, tag: hit.tag },
|
|
24303
|
-
worktreeExists: (p) =>
|
|
24800
|
+
worktreeExists: (p) => existsSync42(p),
|
|
24304
24801
|
caps: periscopeCaps(cfg.ship),
|
|
24305
24802
|
runHook: async (name, cwd, env) => {
|
|
24306
24803
|
const script = hit?.repo.config.hooks[name];
|
|
@@ -24394,10 +24891,10 @@ ${where.join("\n")}`);
|
|
|
24394
24891
|
},
|
|
24395
24892
|
prepare: async (path) => {
|
|
24396
24893
|
for (const f of repoP.config.worktrees.copy) {
|
|
24397
|
-
const src =
|
|
24398
|
-
if (!
|
|
24399
|
-
|
|
24400
|
-
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));
|
|
24401
24898
|
}
|
|
24402
24899
|
const setup = repoP.config.hooks.setup;
|
|
24403
24900
|
return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
|
|
@@ -24409,7 +24906,10 @@ ${where.join("\n")}`);
|
|
|
24409
24906
|
return script ? runScript(script, { cwd, env, shell: shellP, timeoutMs: name === "preview" ? 18e4 : void 0 }) : null;
|
|
24410
24907
|
},
|
|
24411
24908
|
createRow: (fields) => trackerP.createPeriscope(fields),
|
|
24412
|
-
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
|
+
},
|
|
24413
24913
|
comment: (ticketId, body) => trackerP.postComment(ticketId, body, memberId),
|
|
24414
24914
|
teardown: async (path, branch) => {
|
|
24415
24915
|
const n = Number(path.match(/(\d+)\/?$/)?.[1]);
|
|
@@ -24687,7 +25187,7 @@ ${where.join("\n")}`);
|
|
|
24687
25187
|
};
|
|
24688
25188
|
const droppedRoutes = [];
|
|
24689
25189
|
try {
|
|
24690
|
-
const rawCfg = parseYaml2(
|
|
25190
|
+
const rawCfg = parseYaml2(readFileSync37(cfg.configFile, "utf8"));
|
|
24691
25191
|
for (const r of rawCfg?.routes ?? []) {
|
|
24692
25192
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
24693
25193
|
try {
|
|
@@ -24729,8 +25229,8 @@ ${where.join("\n")}`);
|
|
|
24729
25229
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
24730
25230
|
break;
|
|
24731
25231
|
}
|
|
24732
|
-
|
|
24733
|
-
|
|
25232
|
+
mkdirSync27(cfg.ship.stateDir, { recursive: true });
|
|
25233
|
+
writeFileSync29(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
24734
25234
|
`);
|
|
24735
25235
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
24736
25236
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -24781,8 +25281,8 @@ ${where.join("\n")}`);
|
|
|
24781
25281
|
}
|
|
24782
25282
|
case "log": {
|
|
24783
25283
|
try {
|
|
24784
|
-
const
|
|
24785
|
-
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")}
|
|
24786
25286
|
`);
|
|
24787
25287
|
} catch {
|
|
24788
25288
|
process.stdout.write(`no log yet at ${cfg.ship.logFile}
|
|
@@ -24791,12 +25291,12 @@ ${where.join("\n")}`);
|
|
|
24791
25291
|
break;
|
|
24792
25292
|
}
|
|
24793
25293
|
case "sample-ticket": {
|
|
24794
|
-
const
|
|
24795
|
-
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 })}
|
|
24796
25296
|
` : `${human}
|
|
24797
25297
|
`);
|
|
24798
25298
|
const fail = (msg) => {
|
|
24799
|
-
if (
|
|
25299
|
+
if (json2) process.stdout.write(`${JSON.stringify({ event: "error", message: msg })}
|
|
24800
25300
|
`);
|
|
24801
25301
|
else process.stderr.write(`crew sample-ticket: ${msg}
|
|
24802
25302
|
`);
|
|
@@ -24804,8 +25304,8 @@ ${where.join("\n")}`);
|
|
|
24804
25304
|
};
|
|
24805
25305
|
if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
|
|
24806
25306
|
const tracker = new Tracker(route2, cfg.ship);
|
|
24807
|
-
const configText =
|
|
24808
|
-
const cfgBase =
|
|
25307
|
+
const configText = readFileSync37(cfg.configFile, "utf8");
|
|
25308
|
+
const cfgBase = dirname23(cfg.configFile);
|
|
24809
25309
|
const configured2 = configuredRepos(configText, route2.route, cfgBase);
|
|
24810
25310
|
const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
|
|
24811
25311
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
|
|
@@ -24833,7 +25333,7 @@ ${where.join("\n")}`);
|
|
|
24833
25333
|
}
|
|
24834
25334
|
if (configured2.some((r) => r.dir === dir)) {
|
|
24835
25335
|
try {
|
|
24836
|
-
|
|
25336
|
+
writeFileSync29(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
|
|
24837
25337
|
} catch (e) {
|
|
24838
25338
|
if (e instanceof ReposError) fail(e.message);
|
|
24839
25339
|
throw e;
|
|
@@ -24841,29 +25341,29 @@ ${where.join("\n")}`);
|
|
|
24841
25341
|
}
|
|
24842
25342
|
if (row) {
|
|
24843
25343
|
await tracker.deleteRepoRow(row.id);
|
|
24844
|
-
if (
|
|
24845
|
-
const raw = JSON.parse(
|
|
25344
|
+
if (existsSync42(resolvedFile)) {
|
|
25345
|
+
const raw = JSON.parse(readFileSync37(resolvedFile, "utf8"));
|
|
24846
25346
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
24847
25347
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
24848
|
-
|
|
25348
|
+
writeFileSync29(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
24849
25349
|
`);
|
|
24850
25350
|
}
|
|
24851
25351
|
}
|
|
24852
|
-
if (
|
|
25352
|
+
if (existsSync42(join46(dir, ".git")) && basename14(dir) === SAMPLE_REPO) rmSync20(dir, { recursive: true, force: true });
|
|
24853
25353
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
24854
25354
|
break;
|
|
24855
25355
|
}
|
|
24856
25356
|
const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
24857
|
-
const fresh = !
|
|
24858
|
-
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`);
|
|
24859
25359
|
if (dryRun) {
|
|
24860
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`);
|
|
24861
25361
|
break;
|
|
24862
25362
|
}
|
|
24863
25363
|
if (fresh) {
|
|
24864
|
-
|
|
24865
|
-
|
|
24866
|
-
|
|
25364
|
+
mkdirSync27(dir, { recursive: true });
|
|
25365
|
+
writeFileSync29(join46(dir, "README.md"), SAMPLE_README);
|
|
25366
|
+
writeFileSync29(join46(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
24867
25367
|
const g = (...a) => {
|
|
24868
25368
|
const r = spawnSync8("git", a, { cwd: dir, encoding: "utf8" });
|
|
24869
25369
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -24881,17 +25381,17 @@ ${where.join("\n")}`);
|
|
|
24881
25381
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
24882
25382
|
});
|
|
24883
25383
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
24884
|
-
if (
|
|
24885
|
-
const raw = JSON.parse(
|
|
25384
|
+
if (existsSync42(resolvedFile)) {
|
|
25385
|
+
const raw = JSON.parse(readFileSync37(resolvedFile, "utf8"));
|
|
24886
25386
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
24887
25387
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
24888
|
-
|
|
25388
|
+
writeFileSync29(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
24889
25389
|
`);
|
|
24890
25390
|
}
|
|
24891
25391
|
}
|
|
24892
25392
|
if (!configured2.some((r) => r.dir === dir)) {
|
|
24893
25393
|
try {
|
|
24894
|
-
|
|
25394
|
+
writeFileSync29(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
|
|
24895
25395
|
} catch (e) {
|
|
24896
25396
|
if (e instanceof ReposError) fail(e.message);
|
|
24897
25397
|
throw e;
|
|
@@ -25124,7 +25624,7 @@ ${where.join("\n")}`);
|
|
|
25124
25624
|
// `synthesis-tabl-123`. `worktreePrefix` above is kept for
|
|
25125
25625
|
// config-provenance display; this is the field that actually
|
|
25126
25626
|
// describes where a worktree lands by default.
|
|
25127
|
-
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>`),
|
|
25128
25628
|
worktreeParent: resolvePath(r.dir, "..")
|
|
25129
25629
|
})),
|
|
25130
25630
|
waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
|
|
@@ -25229,7 +25729,7 @@ watermark: ${state.watermark()}
|
|
|
25229
25729
|
}
|
|
25230
25730
|
for (const r of repos) {
|
|
25231
25731
|
const remote = r.config.branch.remote;
|
|
25232
|
-
if (
|
|
25732
|
+
if (existsSync42(r.dir)) {
|
|
25233
25733
|
pushAuth.push({
|
|
25234
25734
|
route: c.route,
|
|
25235
25735
|
repo: r.name,
|
|
@@ -25315,7 +25815,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
25315
25815
|
}
|
|
25316
25816
|
try {
|
|
25317
25817
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
25318
|
-
const evSize =
|
|
25818
|
+
const evSize = existsSync42(evFile) ? statSync14(evFile).size : 0;
|
|
25319
25819
|
process.stdout.write(
|
|
25320
25820
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
25321
25821
|
` : "events log: ok\n"
|
|
@@ -25326,7 +25826,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
25326
25826
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run", installContext());
|
|
25327
25827
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
25328
25828
|
try {
|
|
25329
|
-
return
|
|
25829
|
+
return readFileSync37(p, "utf8");
|
|
25330
25830
|
} catch {
|
|
25331
25831
|
return null;
|
|
25332
25832
|
}
|
|
@@ -25369,7 +25869,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
25369
25869
|
);
|
|
25370
25870
|
for (const r of await resolvedRepos(route2)) {
|
|
25371
25871
|
const { config } = r;
|
|
25372
|
-
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)}`);
|
|
25373
25873
|
process.stdout.write(
|
|
25374
25874
|
`${`repo ${r.name}:`.padEnd(19)}${r.dir}
|
|
25375
25875
|
worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
|
|
@@ -25756,6 +26256,11 @@ ${foreign.join("\n")}
|
|
|
25756
26256
|
);
|
|
25757
26257
|
}
|
|
25758
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
|
+
}
|
|
25759
26264
|
if (cfg.ship.relayHost && !flag("offline")) {
|
|
25760
26265
|
const relayUrl = routeRelayUrl(route2);
|
|
25761
26266
|
if (!relayUrl) {
|
|
@@ -25803,6 +26308,8 @@ var parseTicketNumber2;
|
|
|
25803
26308
|
var routeRelayUrl2;
|
|
25804
26309
|
var passengerApiKeys2;
|
|
25805
26310
|
var endpointApiFor2;
|
|
26311
|
+
var periscopeEndpointDeps2;
|
|
26312
|
+
var periscopeMcpDeps2;
|
|
25806
26313
|
var endpointReconcileDeps2;
|
|
25807
26314
|
var releaseRefused2;
|
|
25808
26315
|
var doctorMachineId2;
|