@tablation/crew 0.72.2 → 0.73.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/README.md +1 -1
- package/dist/cli.js +445 -352
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -130,21 +130,21 @@ function formatStaleOwnLock(scope, holder) {
|
|
|
130
130
|
|
|
131
131
|
// src/selftest.ts
|
|
132
132
|
import { spawn as spawn3 } from "child_process";
|
|
133
|
-
import { existsSync as existsSync19, mkdirSync as
|
|
133
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync10, mkdtempSync, readFileSync as readFileSync16, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "fs";
|
|
134
134
|
import { tmpdir as tmpdir2 } from "os";
|
|
135
|
-
import { join as
|
|
135
|
+
import { join as join19 } from "path";
|
|
136
136
|
|
|
137
137
|
// src/agent.ts
|
|
138
138
|
import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
|
|
139
|
-
import { readFileSync as
|
|
139
|
+
import { readFileSync as readFileSync15, existsSync as existsSync18, statSync as statSync5, mkdirSync as mkdirSync9, writeFileSync as writeFileSync10, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
|
|
140
140
|
import { StringDecoder } from "string_decoder";
|
|
141
141
|
import { finished } from "stream/promises";
|
|
142
|
-
import { join as
|
|
143
|
-
import { createHash as
|
|
142
|
+
import { join as join18, dirname as dirname7, basename as basename7 } from "path";
|
|
143
|
+
import { createHash as createHash3 } from "crypto";
|
|
144
144
|
|
|
145
145
|
// src/config.ts
|
|
146
|
-
import { readFileSync as
|
|
147
|
-
import { resolve, dirname as dirname3, isAbsolute, join as
|
|
146
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
147
|
+
import { resolve, dirname as dirname3, isAbsolute, join as join8, basename as basename3 } from "path";
|
|
148
148
|
import { homedir as homedir5, tmpdir } from "os";
|
|
149
149
|
import { parse as parse2 } from "yaml";
|
|
150
150
|
|
|
@@ -294,10 +294,10 @@ var Emitter = class _Emitter {
|
|
|
294
294
|
var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
|
|
295
295
|
|
|
296
296
|
// src/pair-install.ts
|
|
297
|
-
import { existsSync as existsSync5, rmSync as rmSync4, writeFileSync as
|
|
297
|
+
import { existsSync as existsSync5, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
298
298
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
299
299
|
import { homedir as homedir4 } from "os";
|
|
300
|
-
import { join as
|
|
300
|
+
import { join as join6 } from "path";
|
|
301
301
|
|
|
302
302
|
// src/stream.ts
|
|
303
303
|
function toolTarget(name, input) {
|
|
@@ -349,9 +349,38 @@ function extractResult(line) {
|
|
|
349
349
|
|
|
350
350
|
// src/claude-mcp.ts
|
|
351
351
|
import { spawnSync } from "child_process";
|
|
352
|
-
import { readFileSync as
|
|
352
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
353
353
|
import { homedir } from "os";
|
|
354
|
+
import { join as join3 } from "path";
|
|
355
|
+
|
|
356
|
+
// src/mcp-registrations.ts
|
|
357
|
+
import { createHash } from "crypto";
|
|
358
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
354
359
|
import { join as join2 } from "path";
|
|
360
|
+
var fileFor = (stateDir) => join2(stateDir, "mcp-registrations.json");
|
|
361
|
+
var keyFingerprint = (key) => createHash("sha256").update(key).digest("hex").slice(0, 8);
|
|
362
|
+
function readMcpRegistrations(stateDir) {
|
|
363
|
+
try {
|
|
364
|
+
const j = JSON.parse(readFileSync2(fileFor(stateDir), "utf8"));
|
|
365
|
+
return Array.isArray(j) ? j : [];
|
|
366
|
+
} catch {
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function recordMcpRegistration(stateDir, r, now = /* @__PURE__ */ new Date()) {
|
|
371
|
+
const rest = readMcpRegistrations(stateDir).filter((x) => !(x.harness === r.harness && x.scope === r.scope && x.name === r.name));
|
|
372
|
+
mkdirSync2(stateDir, { recursive: true });
|
|
373
|
+
writeFileSync(fileFor(stateDir), `${JSON.stringify([...rest, { ...r, at: now.toISOString() }], null, 2)}
|
|
374
|
+
`, { mode: 384 });
|
|
375
|
+
}
|
|
376
|
+
function findMcpRegistration(stateDir, r) {
|
|
377
|
+
const fp = keyFingerprint(r.key);
|
|
378
|
+
return readMcpRegistrations(stateDir).find(
|
|
379
|
+
(x) => x.harness === r.harness && x.scope === r.scope && x.name === r.name && x.url === r.url && x.keyFingerprint === fp
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/claude-mcp.ts
|
|
355
384
|
var MCP_SERVER_NAME = "tablation";
|
|
356
385
|
var redact = (s, key) => key ? s.split(key).join("sk_***") : s;
|
|
357
386
|
function mcpUrlFor(baseUrl) {
|
|
@@ -364,7 +393,7 @@ function runClaude(bin, args) {
|
|
|
364
393
|
}
|
|
365
394
|
function userScopeEntry(home) {
|
|
366
395
|
try {
|
|
367
|
-
const j = JSON.parse(
|
|
396
|
+
const j = JSON.parse(readFileSync3(join3(home, ".claude.json"), "utf8"));
|
|
368
397
|
return j.mcpServers?.[MCP_SERVER_NAME];
|
|
369
398
|
} catch {
|
|
370
399
|
return void 0;
|
|
@@ -380,18 +409,32 @@ function inspectClaudeMcp(o) {
|
|
|
380
409
|
}
|
|
381
410
|
if (entry.url !== o.url) return { state: "different", detail: `it points at ${entry.url ?? "(no url)"}, not ${o.url}` };
|
|
382
411
|
const auth = Object.entries(entry.headers ?? {}).find(([k]) => k.toLowerCase() === "authorization")?.[1];
|
|
383
|
-
if (auth !== `Bearer ${o.key}`)
|
|
412
|
+
if (auth !== `Bearer ${o.key}`) {
|
|
413
|
+
const bearer = auth?.startsWith("Bearer ") ? auth.slice("Bearer ".length) : void 0;
|
|
414
|
+
const mine = o.stateDir && bearer ? findMcpRegistration(o.stateDir, { harness: "claude", scope: "user", name: MCP_SERVER_NAME, url: o.url, key: bearer }) : void 0;
|
|
415
|
+
if (mine) return { state: "different", detail: `crew registered an earlier key here`, crewWrote: mine.at };
|
|
416
|
+
return { state: "different", detail: `it points at ${o.url} with a key that is not one crew issued` };
|
|
417
|
+
}
|
|
384
418
|
return { state: "registered", url: o.url };
|
|
385
419
|
}
|
|
386
420
|
function ensureClaudeMcp(o) {
|
|
421
|
+
return ensureClaudeMcpResult(o).line;
|
|
422
|
+
}
|
|
423
|
+
function ensureClaudeMcpResult(o) {
|
|
424
|
+
const r = ensureInner(o);
|
|
425
|
+
return { ...r, url: o.url };
|
|
426
|
+
}
|
|
427
|
+
function ensureInner(o) {
|
|
387
428
|
const manual = `claude mcp add --transport http ${MCP_SERVER_NAME} ${o.url} --header "Authorization: Bearer <key>" --scope user`;
|
|
388
429
|
const st = inspectClaudeMcp(o);
|
|
389
|
-
if (st.state === "no-claude") return `mcp: \`${o.bin}\` not found, so Claude Code was not configured. To do it by hand: ${manual}
|
|
390
|
-
if (st.state === "registered") return `mcp: ${MCP_SERVER_NAME} already registered with Claude Code \u2192 ${st.url}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
430
|
+
if (st.state === "no-claude") return { line: `mcp: \`${o.bin}\` not found, so Claude Code was not configured. To do it by hand: ${manual}` };
|
|
431
|
+
if (st.state === "registered") return { line: `mcp: ${MCP_SERVER_NAME} already registered with Claude Code \u2192 ${st.url}`, action: "registered" };
|
|
432
|
+
const crewWrote = st.state === "different" ? st.crewWrote : void 0;
|
|
433
|
+
const ours = crewWrote !== void 0;
|
|
434
|
+
if (st.state === "different" && !o.replace && !ours) {
|
|
435
|
+
return { line: `mcp: left Claude Code's "${MCP_SERVER_NAME}" server alone \u2014 ${st.detail}. Re-run with --mcp replace to overwrite it.`, action: "kept" };
|
|
436
|
+
}
|
|
437
|
+
if (o.dryRun) return { line: `(dry run) would register the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url}` };
|
|
395
438
|
if (st.state === "different") {
|
|
396
439
|
runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
|
|
397
440
|
}
|
|
@@ -408,9 +451,21 @@ function ensureClaudeMcp(o) {
|
|
|
408
451
|
"user"
|
|
409
452
|
]);
|
|
410
453
|
if (add.status !== 0) {
|
|
411
|
-
return `mcp: \`claude mcp add\` failed (${redact(add.stderr.trim(), o.key) || `exit ${add.status}`}). To do it by hand: ${manual}
|
|
454
|
+
return { line: `mcp: \`claude mcp add\` failed (${redact(add.stderr.trim(), o.key) || `exit ${add.status}`}). To do it by hand: ${manual}` };
|
|
412
455
|
}
|
|
413
|
-
|
|
456
|
+
if (o.stateDir) {
|
|
457
|
+
try {
|
|
458
|
+
recordMcpRegistration(o.stateDir, { harness: "claude", scope: "user", name: MCP_SERVER_NAME, url: o.url, keyFingerprint: keyFingerprint(o.key) });
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (crewWrote !== void 0) {
|
|
463
|
+
return { line: `mcp: updated the key crew registered on ${crewWrote.slice(0, 10)} \u2192 ${o.url}`, action: "replaced" };
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
line: `mcp: ${st.state === "different" ? "replaced" : "registered"} the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url} (user scope, ~/.claude.json)`,
|
|
467
|
+
action: st.state === "different" ? "replaced" : "registered"
|
|
468
|
+
};
|
|
414
469
|
}
|
|
415
470
|
function removeClaudeMcp(o) {
|
|
416
471
|
const got = runClaude(o.bin, ["mcp", "get", MCP_SERVER_NAME]);
|
|
@@ -422,8 +477,8 @@ function removeClaudeMcp(o) {
|
|
|
422
477
|
}
|
|
423
478
|
|
|
424
479
|
// src/launcher.ts
|
|
425
|
-
import { basename, join as
|
|
426
|
-
import { existsSync as existsSync3, readFileSync as
|
|
480
|
+
import { basename, join as join4 } from "path";
|
|
481
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync } from "fs";
|
|
427
482
|
import { homedir as homedir2 } from "os";
|
|
428
483
|
function isOriLauncher(spec) {
|
|
429
484
|
return !!spec.launcher && basename(spec.launcher) === "ori";
|
|
@@ -432,7 +487,7 @@ function isOmlxLauncher(spec) {
|
|
|
432
487
|
return !!spec.launcher && basename(spec.launcher) === "omlx";
|
|
433
488
|
}
|
|
434
489
|
function defaultOmlxBin(home = homedir2()) {
|
|
435
|
-
return
|
|
490
|
+
return join4(home, ".omlx", "bin", "omlx");
|
|
436
491
|
}
|
|
437
492
|
function launcherExecutable(spec, home) {
|
|
438
493
|
if (!spec.launcher) return void 0;
|
|
@@ -485,8 +540,8 @@ function omlxModelPresent(model, dirs) {
|
|
|
485
540
|
});
|
|
486
541
|
}
|
|
487
542
|
function readOmlxSettings(home = homedir2()) {
|
|
488
|
-
const p =
|
|
489
|
-
return existsSync3(p) ? parseOmlxSettings(
|
|
543
|
+
const p = join4(home, ".omlx", "settings.json");
|
|
544
|
+
return existsSync3(p) ? parseOmlxSettings(readFileSync4(p, "utf8")) : void 0;
|
|
490
545
|
}
|
|
491
546
|
|
|
492
547
|
// src/capability.ts
|
|
@@ -613,17 +668,18 @@ var claudeAdapter = {
|
|
|
613
668
|
const s = inspectClaudeMcp(o);
|
|
614
669
|
return s.state === "no-claude" ? { state: "no-binary" } : s;
|
|
615
670
|
},
|
|
616
|
-
ensureMcp: ensureClaudeMcp
|
|
671
|
+
ensureMcp: ensureClaudeMcp,
|
|
672
|
+
ensureMcpResult: ensureClaudeMcpResult
|
|
617
673
|
};
|
|
618
674
|
|
|
619
675
|
// src/adapters/codex.ts
|
|
620
676
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
621
677
|
import { homedir as homedir3 } from "os";
|
|
622
|
-
import { join as
|
|
623
|
-
import { rmSync as rmSync3, writeFileSync as
|
|
678
|
+
import { join as join5 } from "path";
|
|
679
|
+
import { rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
624
680
|
|
|
625
681
|
// src/pair-files.ts
|
|
626
|
-
import { existsSync as existsSync4, mkdirSync as
|
|
682
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
|
|
627
683
|
import { dirname as dirname2 } from "path";
|
|
628
684
|
var PAIR_BLOCK_START = "<!-- crew:pair start -->";
|
|
629
685
|
var PAIR_BLOCK_END = "<!-- crew:pair end -->";
|
|
@@ -664,11 +720,11 @@ function removePairBlock(text) {
|
|
|
664
720
|
return text.replace(new RegExp(`\\n*${BLOCK_RE.source}`), "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
|
|
665
721
|
}
|
|
666
722
|
function readIfExists(path) {
|
|
667
|
-
return existsSync4(path) ?
|
|
723
|
+
return existsSync4(path) ? readFileSync5(path, "utf8") : void 0;
|
|
668
724
|
}
|
|
669
725
|
function writeFileMkdir(path, text) {
|
|
670
|
-
|
|
671
|
-
|
|
726
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
727
|
+
writeFileSync2(path, text);
|
|
672
728
|
}
|
|
673
729
|
function installBlockFile(path, harness, dryRun) {
|
|
674
730
|
const before = readIfExists(path) ?? "";
|
|
@@ -684,7 +740,7 @@ function removeBlockFile(path, dryRun) {
|
|
|
684
740
|
if (dryRun) return true;
|
|
685
741
|
const after = removePairBlock(before);
|
|
686
742
|
if (after.trim() === "") rmSync2(path);
|
|
687
|
-
else
|
|
743
|
+
else writeFileSync2(path, after);
|
|
688
744
|
return true;
|
|
689
745
|
}
|
|
690
746
|
function blockVerdict(path, harness) {
|
|
@@ -811,8 +867,8 @@ function codexTable(url) {
|
|
|
811
867
|
return [`[mcp_servers.${SERVER}]`, `url = ${tomlString(url)}`, `bearer_token_env_var = ${tomlString(PAIR_KEY_VAR)}`];
|
|
812
868
|
}
|
|
813
869
|
var codexPaths = (home) => ({
|
|
814
|
-
instructions:
|
|
815
|
-
config:
|
|
870
|
+
instructions: join5(home, ".codex", "AGENTS.md"),
|
|
871
|
+
config: join5(home, ".codex", "config.toml")
|
|
816
872
|
});
|
|
817
873
|
function codexMcpState(config, url) {
|
|
818
874
|
const lines = config.split("\n");
|
|
@@ -863,7 +919,7 @@ var codexPair = {
|
|
|
863
919
|
if (hadMcp && !o.dryRun) {
|
|
864
920
|
const after = removeTomlTable(config, `mcp_servers.${SERVER}`);
|
|
865
921
|
if (after === "") rmSync3(p.config);
|
|
866
|
-
else
|
|
922
|
+
else writeFileSync3(p.config, after);
|
|
867
923
|
}
|
|
868
924
|
const what = hadBlock && hadMcp ? `the Pair block and ${SERVER} server` : hadBlock ? "the Pair block" : `the ${SERVER} server`;
|
|
869
925
|
return hadBlock || hadMcp ? `pair(codex): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what}${kept}` : `pair(codex): nothing to remove${kept}`;
|
|
@@ -959,8 +1015,8 @@ var codexAdapter = {
|
|
|
959
1015
|
|
|
960
1016
|
// src/pair-install.ts
|
|
961
1017
|
var geminiPaths = (home) => ({
|
|
962
|
-
instructions:
|
|
963
|
-
config:
|
|
1018
|
+
instructions: join6(home, ".gemini", "GEMINI.md"),
|
|
1019
|
+
config: join6(home, ".gemini", "settings.json")
|
|
964
1020
|
});
|
|
965
1021
|
function readGeminiSettings(path) {
|
|
966
1022
|
const text = readIfExists(path);
|
|
@@ -1048,7 +1104,7 @@ var geminiPair = {
|
|
|
1048
1104
|
if (Object.keys(rest).length > 0) next.mcpServers = rest;
|
|
1049
1105
|
else delete next.mcpServers;
|
|
1050
1106
|
if (Object.keys(next).length === 0) rmSync4(p.config);
|
|
1051
|
-
else
|
|
1107
|
+
else writeFileSync4(p.config, `${JSON.stringify(next, null, 2)}
|
|
1052
1108
|
`);
|
|
1053
1109
|
}
|
|
1054
1110
|
const what = hadBlock && hadMcp ? `the Pair block and ${SERVER} server` : hadBlock ? "the Pair block" : `the ${SERVER} server`;
|
|
@@ -1059,8 +1115,8 @@ var VSCODE_FLAVOURS = ["Code", "Code - Insiders"];
|
|
|
1059
1115
|
function vscodeUserDirs(o) {
|
|
1060
1116
|
const home = o.home ?? homedir4();
|
|
1061
1117
|
const platform2 = o.platform ?? process.platform;
|
|
1062
|
-
const root = platform2 === "darwin" ?
|
|
1063
|
-
const all = VSCODE_FLAVOURS.map((f) =>
|
|
1118
|
+
const root = platform2 === "darwin" ? join6(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join6(home, "AppData", "Roaming") : join6(home, ".config");
|
|
1119
|
+
const all = VSCODE_FLAVOURS.map((f) => join6(root, f, "User"));
|
|
1064
1120
|
const present = all.filter((d) => existsSync5(d));
|
|
1065
1121
|
return present.length > 0 ? present : [all[0]];
|
|
1066
1122
|
}
|
|
@@ -1099,7 +1155,7 @@ function removeCopilotInstructions(path, dryRun) {
|
|
|
1099
1155
|
if (dryRun) return true;
|
|
1100
1156
|
const after = removePairBlock(before);
|
|
1101
1157
|
if (after.trim() === "" || after.trim() === COPILOT_FRONTMATTER.trim()) rmSync4(path);
|
|
1102
|
-
else
|
|
1158
|
+
else writeFileSync4(path, after);
|
|
1103
1159
|
return true;
|
|
1104
1160
|
}
|
|
1105
1161
|
var copilotPair = {
|
|
@@ -1108,8 +1164,8 @@ var copilotPair = {
|
|
|
1108
1164
|
installPair(o) {
|
|
1109
1165
|
const lines = [];
|
|
1110
1166
|
for (const dir of vscodeUserDirs(o)) {
|
|
1111
|
-
const mcpPath =
|
|
1112
|
-
const instrPath =
|
|
1167
|
+
const mcpPath = join6(dir, "mcp.json");
|
|
1168
|
+
const instrPath = join6(dir, "prompts", "crew-pair.instructions.md");
|
|
1113
1169
|
const settings = readVscodeMcp(mcpPath);
|
|
1114
1170
|
if (!settings) {
|
|
1115
1171
|
lines.push(`pair(copilot): ${mcpPath} is not a plain JSON object (comments are not supported) \u2014 fix it by hand; nothing was written there`);
|
|
@@ -1137,8 +1193,8 @@ var copilotPair = {
|
|
|
1137
1193
|
},
|
|
1138
1194
|
verifyPair(o) {
|
|
1139
1195
|
const verdicts = vscodeUserDirs(o).map((dir) => {
|
|
1140
|
-
const instrPath =
|
|
1141
|
-
const mcpPath =
|
|
1196
|
+
const instrPath = join6(dir, "prompts", "crew-pair.instructions.md");
|
|
1197
|
+
const mcpPath = join6(dir, "mcp.json");
|
|
1142
1198
|
const b = blockVerdict(instrPath, "copilot");
|
|
1143
1199
|
if (!b.ok) return b;
|
|
1144
1200
|
const settings = readVscodeMcp(mcpPath);
|
|
@@ -1153,8 +1209,8 @@ var copilotPair = {
|
|
|
1153
1209
|
removePair(o) {
|
|
1154
1210
|
const lines = [];
|
|
1155
1211
|
for (const dir of vscodeUserDirs(o)) {
|
|
1156
|
-
const mcpPath =
|
|
1157
|
-
const hadBlock = removeCopilotInstructions(
|
|
1212
|
+
const mcpPath = join6(dir, "mcp.json");
|
|
1213
|
+
const hadBlock = removeCopilotInstructions(join6(dir, "prompts", "crew-pair.instructions.md"), o.dryRun);
|
|
1158
1214
|
const settings = readVscodeMcp(mcpPath);
|
|
1159
1215
|
const st = settings ? copilotMcpState(settings, o.url) : { state: "missing" };
|
|
1160
1216
|
const hadMcp = st.state === "registered";
|
|
@@ -1165,7 +1221,7 @@ var copilotPair = {
|
|
|
1165
1221
|
if (Object.keys(rest).length > 0) next.servers = rest;
|
|
1166
1222
|
else delete next.servers;
|
|
1167
1223
|
if (Object.keys(next).length === 0) rmSync4(mcpPath);
|
|
1168
|
-
else
|
|
1224
|
+
else writeFileSync4(mcpPath, `${JSON.stringify(next, null, 2)}
|
|
1169
1225
|
`);
|
|
1170
1226
|
}
|
|
1171
1227
|
const what = hadBlock && hadMcp ? `the Pair instructions and ${SERVER} server` : hadBlock ? "the Pair instructions" : `the ${SERVER} server`;
|
|
@@ -1453,8 +1509,8 @@ function explain(ship, required) {
|
|
|
1453
1509
|
}
|
|
1454
1510
|
|
|
1455
1511
|
// src/repo-config.ts
|
|
1456
|
-
import { readFileSync as
|
|
1457
|
-
import { join as
|
|
1512
|
+
import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
|
|
1513
|
+
import { join as join7, basename as basename2 } from "path";
|
|
1458
1514
|
import { parse } from "yaml";
|
|
1459
1515
|
var SPEC_VERSION = 1;
|
|
1460
1516
|
var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
|
|
@@ -1537,7 +1593,7 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
|
|
|
1537
1593
|
var PROVIDERS = ["github", "buildkite", "other", "none"];
|
|
1538
1594
|
function findRepoConfig(dir) {
|
|
1539
1595
|
for (const name of REPO_CONFIG_FILENAMES) {
|
|
1540
|
-
const p =
|
|
1596
|
+
const p = join7(dir, name);
|
|
1541
1597
|
if (existsSync6(p)) return p;
|
|
1542
1598
|
}
|
|
1543
1599
|
return null;
|
|
@@ -1776,7 +1832,7 @@ function parseRepoConfig(text, file) {
|
|
|
1776
1832
|
function loadRepoConfig(dir) {
|
|
1777
1833
|
const file = findRepoConfig(dir);
|
|
1778
1834
|
if (!file) return null;
|
|
1779
|
-
return parseRepoConfig(
|
|
1835
|
+
return parseRepoConfig(readFileSync6(file, "utf8"), file);
|
|
1780
1836
|
}
|
|
1781
1837
|
function hookLabel(cfg, hook2) {
|
|
1782
1838
|
return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
|
|
@@ -2230,11 +2286,11 @@ function expand(p, base) {
|
|
|
2230
2286
|
return isAbsolute(out) ? out : resolve(base, out);
|
|
2231
2287
|
}
|
|
2232
2288
|
function resolvePromptsDir(promptSet, crewHome, base) {
|
|
2233
|
-
if (!promptSet) return
|
|
2289
|
+
if (!promptSet) return join8(crewHome, "prompts", "default");
|
|
2234
2290
|
if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
|
|
2235
2291
|
return expand(promptSet, base);
|
|
2236
2292
|
}
|
|
2237
|
-
return
|
|
2293
|
+
return join8(crewHome, "prompts", promptSet);
|
|
2238
2294
|
}
|
|
2239
2295
|
var Missing = class {
|
|
2240
2296
|
paths = [];
|
|
@@ -2258,7 +2314,7 @@ var Missing = class {
|
|
|
2258
2314
|
};
|
|
2259
2315
|
function crewVersion(crewHome) {
|
|
2260
2316
|
try {
|
|
2261
|
-
const pkg = JSON.parse(
|
|
2317
|
+
const pkg = JSON.parse(readFileSync7(join8(crewHome, "package.json"), "utf8"));
|
|
2262
2318
|
return pkg.version ?? "0.0.0";
|
|
2263
2319
|
} catch {
|
|
2264
2320
|
return "0.0.0";
|
|
@@ -2268,8 +2324,8 @@ function configSearchPath(crewHome) {
|
|
|
2268
2324
|
if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
|
|
2269
2325
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
2270
2326
|
return [
|
|
2271
|
-
...xdg ? [
|
|
2272
|
-
|
|
2327
|
+
...xdg ? [join8(xdg, "crew", "crew.yaml")] : [],
|
|
2328
|
+
join8(homedir5(), ".config", "crew", "crew.yaml"),
|
|
2273
2329
|
resolve(crewHome, "crew.yaml")
|
|
2274
2330
|
];
|
|
2275
2331
|
}
|
|
@@ -2277,17 +2333,17 @@ function findConfigFile(crewHome) {
|
|
|
2277
2333
|
const candidates = configSearchPath(crewHome);
|
|
2278
2334
|
return candidates.find((c) => existsSync7(c)) ?? candidates[candidates.length - 1];
|
|
2279
2335
|
}
|
|
2280
|
-
var defaultConfigPath = () =>
|
|
2336
|
+
var defaultConfigPath = () => join8(process.env.XDG_CONFIG_HOME ?? join8(homedir5(), ".config"), "crew", "crew.yaml");
|
|
2281
2337
|
function routeSlug(route) {
|
|
2282
2338
|
return route.replace(/\//g, "-");
|
|
2283
2339
|
}
|
|
2284
2340
|
function resolvedPathFor(stateDir, route) {
|
|
2285
2341
|
const [workspace, project] = splitRoute(route);
|
|
2286
|
-
return
|
|
2342
|
+
return join8(stateDir, "resolved", workspace, `${project}.json`);
|
|
2287
2343
|
}
|
|
2288
2344
|
function apiKeyPathFor(stateDir, route) {
|
|
2289
2345
|
const [workspace, project] = splitRoute(route);
|
|
2290
|
-
return
|
|
2346
|
+
return join8(stateDir, "keys", workspace, `${project}.env`);
|
|
2291
2347
|
}
|
|
2292
2348
|
function splitRoute(route) {
|
|
2293
2349
|
const parts = route.split("/");
|
|
@@ -2300,7 +2356,7 @@ function readResolvedFile(stateDir, route) {
|
|
|
2300
2356
|
const path = resolvedPathFor(stateDir, route);
|
|
2301
2357
|
if (!existsSync7(path)) return void 0;
|
|
2302
2358
|
try {
|
|
2303
|
-
return JSON.parse(
|
|
2359
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
2304
2360
|
} catch (e) {
|
|
2305
2361
|
throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
|
|
2306
2362
|
}
|
|
@@ -2365,12 +2421,12 @@ function loadConfig(crewHome, configFile, opts = {}) {
|
|
|
2365
2421
|
` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
|
|
2366
2422
|
|
|
2367
2423
|
Create the first of those:
|
|
2368
|
-
mkdir -p ${dirname3(defaultConfigPath())} && cp ${
|
|
2424
|
+
mkdir -p ${dirname3(defaultConfigPath())} && cp ${join8(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
|
|
2369
2425
|
);
|
|
2370
2426
|
}
|
|
2371
2427
|
let raw;
|
|
2372
2428
|
try {
|
|
2373
|
-
raw = parse2(opts.text ??
|
|
2429
|
+
raw = parse2(opts.text ?? readFileSync7(file, "utf8"));
|
|
2374
2430
|
} catch (e) {
|
|
2375
2431
|
throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
|
|
2376
2432
|
}
|
|
@@ -2448,7 +2504,7 @@ Create the first of those:
|
|
|
2448
2504
|
useNvm: shipRaw.useNvm !== false,
|
|
2449
2505
|
nvmSh: shipRaw.nvmSh,
|
|
2450
2506
|
stateDir,
|
|
2451
|
-
logFile: expand(shipRaw.logFile ??
|
|
2507
|
+
logFile: expand(shipRaw.logFile ?? join8(tmpdir(), "crew.log"), base),
|
|
2452
2508
|
userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
|
|
2453
2509
|
maxConcurrentAgents,
|
|
2454
2510
|
streamRetentionDays,
|
|
@@ -2589,7 +2645,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
|
|
|
2589
2645
|
const hasRepos = Object.keys(repoDirs).length > 0;
|
|
2590
2646
|
const reposBasePath = expand(String(c?.reposBasePath ?? raw.ship?.reposBasePath ?? DEFAULT_REPOS_BASE_PATH), base);
|
|
2591
2647
|
const dirIsCloneBase = !c?.dir && !hasRepos && c?.repos && typeof c.repos === "object" && !Array.isArray(c.repos);
|
|
2592
|
-
const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ?
|
|
2648
|
+
const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ? join8(reposBasePath, splitRoute(route)[0]) : missing.req(void 0, `${where}.dir (or ${where}.repos)`) ?? "";
|
|
2593
2649
|
const weight = c?.weight === void 0 ? void 0 : Number(c.weight);
|
|
2594
2650
|
if (weight !== void 0 && !(Number.isFinite(weight) && weight > 0)) {
|
|
2595
2651
|
missing.add(`${where}.weight must be a positive number (got "${c?.weight}")`);
|
|
@@ -2673,7 +2729,7 @@ function repoIdForName(r, name) {
|
|
|
2673
2729
|
function resolveApiKey(r) {
|
|
2674
2730
|
if (r.apiKey) return r.apiKey;
|
|
2675
2731
|
if (r.apiKeyFile && r.apiKeyVar && existsSync7(r.apiKeyFile)) {
|
|
2676
|
-
for (const line of
|
|
2732
|
+
for (const line of readFileSync7(r.apiKeyFile, "utf8").split("\n")) {
|
|
2677
2733
|
const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
|
|
2678
2734
|
if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
2679
2735
|
}
|
|
@@ -2688,7 +2744,7 @@ function credentialSource(r) {
|
|
|
2688
2744
|
if (r.apiKey) return KEYCHAIN_ELIGIBLE_ROUTES.has(r) ? "keychain" : "inline";
|
|
2689
2745
|
if (r.apiKeyFile && r.apiKeyVar && existsSync7(r.apiKeyFile)) {
|
|
2690
2746
|
const re = new RegExp(`^${r.apiKeyVar}=`);
|
|
2691
|
-
if (
|
|
2747
|
+
if (readFileSync7(r.apiKeyFile, "utf8").split("\n").some((l) => re.test(l))) return "apiKeyFile";
|
|
2692
2748
|
}
|
|
2693
2749
|
return process.env.CREW_API_KEY?.trim() ? "CREW_API_KEY" : "none";
|
|
2694
2750
|
}
|
|
@@ -2744,7 +2800,7 @@ function configuredMembers(rt) {
|
|
|
2744
2800
|
return out;
|
|
2745
2801
|
}
|
|
2746
2802
|
function defaultRepoDir(basePath, workspace, repoName) {
|
|
2747
|
-
return
|
|
2803
|
+
return join8(basePath, workspace, repoName);
|
|
2748
2804
|
}
|
|
2749
2805
|
function reposOf(r) {
|
|
2750
2806
|
const named = Object.entries(r.repos);
|
|
@@ -2777,7 +2833,7 @@ function passengerRepoTargets(r) {
|
|
|
2777
2833
|
return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
|
|
2778
2834
|
}
|
|
2779
2835
|
function passengerCheckoutDir(basePath, workspace, repoName) {
|
|
2780
|
-
return
|
|
2836
|
+
return join8(basePath, workspace, ".passenger-checkouts", repoName);
|
|
2781
2837
|
}
|
|
2782
2838
|
function passengerCheckoutTargets(r) {
|
|
2783
2839
|
if (!r.hostPassengers) return [];
|
|
@@ -3125,8 +3181,8 @@ function renderEnvironment(i) {
|
|
|
3125
3181
|
}
|
|
3126
3182
|
|
|
3127
3183
|
// src/agent-log.ts
|
|
3128
|
-
import { existsSync as existsSync8, mkdirSync as
|
|
3129
|
-
import { join as
|
|
3184
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
3185
|
+
import { join as join9 } from "path";
|
|
3130
3186
|
|
|
3131
3187
|
// src/pending-backoff.ts
|
|
3132
3188
|
var sleepBackoff = (attempts) => Math.min(6e4 * 2 ** Math.max(0, attempts - 1), 30 * 6e4);
|
|
@@ -3193,20 +3249,20 @@ async function reportAgentRun(target, run2) {
|
|
|
3193
3249
|
}
|
|
3194
3250
|
return { agentLogId: entry.id };
|
|
3195
3251
|
}
|
|
3196
|
-
var queueDir = (stateDir) =>
|
|
3252
|
+
var queueDir = (stateDir) => join9(stateDir, "pending-agent-logs");
|
|
3197
3253
|
function queueAgentRun(stateDir, name, q) {
|
|
3198
|
-
|
|
3199
|
-
|
|
3254
|
+
mkdirSync4(queueDir(stateDir), { recursive: true });
|
|
3255
|
+
writeFileSync5(join9(queueDir(stateDir), `${name}.json`), JSON.stringify(q));
|
|
3200
3256
|
}
|
|
3201
3257
|
async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
3202
3258
|
const dir = queueDir(stateDir);
|
|
3203
3259
|
if (!existsSync8(dir)) return 0;
|
|
3204
3260
|
let sent = 0;
|
|
3205
3261
|
for (const f of readdirSync2(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
3206
|
-
const path =
|
|
3262
|
+
const path = join9(dir, f);
|
|
3207
3263
|
let q;
|
|
3208
3264
|
try {
|
|
3209
|
-
q = JSON.parse(
|
|
3265
|
+
q = JSON.parse(readFileSync8(path, "utf8"));
|
|
3210
3266
|
} catch {
|
|
3211
3267
|
continue;
|
|
3212
3268
|
}
|
|
@@ -3225,7 +3281,7 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
|
|
|
3225
3281
|
continue;
|
|
3226
3282
|
}
|
|
3227
3283
|
const attempts = q.attempts + 1;
|
|
3228
|
-
|
|
3284
|
+
writeFileSync5(path, JSON.stringify({
|
|
3229
3285
|
...q,
|
|
3230
3286
|
attempts,
|
|
3231
3287
|
warned: true,
|
|
@@ -3406,25 +3462,25 @@ var HandoffTranscriptTracker = class {
|
|
|
3406
3462
|
};
|
|
3407
3463
|
|
|
3408
3464
|
// src/pending-handoff.ts
|
|
3409
|
-
import { existsSync as existsSync15, mkdirSync as
|
|
3410
|
-
import { join as
|
|
3465
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync8, readdirSync as readdirSync5, readFileSync as readFileSync12, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "fs";
|
|
3466
|
+
import { join as join15 } from "path";
|
|
3411
3467
|
|
|
3412
3468
|
// src/git.ts
|
|
3413
3469
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
3414
3470
|
import {
|
|
3415
3471
|
existsSync as existsSync9,
|
|
3416
|
-
mkdirSync as
|
|
3472
|
+
mkdirSync as mkdirSync5,
|
|
3417
3473
|
openSync as openSync2,
|
|
3418
3474
|
closeSync as closeSync2,
|
|
3419
3475
|
writeSync,
|
|
3420
3476
|
statSync as statSync2,
|
|
3421
3477
|
unlinkSync,
|
|
3422
|
-
readFileSync as
|
|
3423
|
-
writeFileSync as
|
|
3478
|
+
readFileSync as readFileSync9,
|
|
3479
|
+
writeFileSync as writeFileSync6,
|
|
3424
3480
|
readdirSync as readdirSync3,
|
|
3425
3481
|
rmSync as rmSync6
|
|
3426
3482
|
} from "fs";
|
|
3427
|
-
import { dirname as dirname4, basename as basename5, join as
|
|
3483
|
+
import { dirname as dirname4, basename as basename5, join as join10 } from "path";
|
|
3428
3484
|
var GitError = class extends Error {
|
|
3429
3485
|
};
|
|
3430
3486
|
function streamText(x) {
|
|
@@ -3808,11 +3864,11 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
|
|
|
3808
3864
|
}
|
|
3809
3865
|
for (const name of names) {
|
|
3810
3866
|
if (!name.startsWith("crew-release-")) continue;
|
|
3811
|
-
const root =
|
|
3867
|
+
const root = join10(tmp, name);
|
|
3812
3868
|
try {
|
|
3813
3869
|
const st = statSync2(root);
|
|
3814
3870
|
if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
|
|
3815
|
-
if (existsSync9(
|
|
3871
|
+
if (existsSync9(join10(root, "checkout"))) continue;
|
|
3816
3872
|
rmSync6(root, { recursive: true, force: true });
|
|
3817
3873
|
removed.push(name);
|
|
3818
3874
|
} catch {
|
|
@@ -3861,7 +3917,7 @@ function ensureRepoCheckout(dir, remote, opts = {}) {
|
|
|
3861
3917
|
`${dir} does not exist and this repo has no discovered remote to clone from \u2014 re-run \`crew connect\`, or check out the repository yourself and point \`repos:\` at it`
|
|
3862
3918
|
);
|
|
3863
3919
|
}
|
|
3864
|
-
|
|
3920
|
+
mkdirSync5(dirname4(dir), { recursive: true });
|
|
3865
3921
|
const url = cloneUrlFor(remote, opts);
|
|
3866
3922
|
try {
|
|
3867
3923
|
execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -3886,7 +3942,7 @@ function sleepSync(ms2) {
|
|
|
3886
3942
|
}
|
|
3887
3943
|
function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
3888
3944
|
const lockPath = checkoutLockPath(dir);
|
|
3889
|
-
|
|
3945
|
+
mkdirSync5(dirname4(lockPath), { recursive: true });
|
|
3890
3946
|
const deadline = Date.now() + timeoutMs;
|
|
3891
3947
|
for (; ; ) {
|
|
3892
3948
|
try {
|
|
@@ -3926,14 +3982,14 @@ function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {
|
|
|
3926
3982
|
if (!existsSync9(`${dir}/.git`) && githubRemote) {
|
|
3927
3983
|
const url = cloneUrlFor(githubRemote, clone);
|
|
3928
3984
|
const marker = cloneFailureMarker(dir);
|
|
3929
|
-
if (existsSync9(marker) &&
|
|
3930
|
-
return { action: "clone-failed", detail:
|
|
3985
|
+
if (existsSync9(marker) && readFileSync9(marker, "utf8").split("\n")[0] === url) {
|
|
3986
|
+
return { action: "clone-failed", detail: readFileSync9(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
|
|
3931
3987
|
}
|
|
3932
3988
|
try {
|
|
3933
3989
|
ensureRepoCheckout(dir, githubRemote, clone);
|
|
3934
3990
|
} catch (e) {
|
|
3935
3991
|
if (!(e instanceof GitError)) throw e;
|
|
3936
|
-
|
|
3992
|
+
writeFileSync6(marker, `${url}
|
|
3937
3993
|
${e.message}`);
|
|
3938
3994
|
return { action: "clone-failed", detail: e.message, repeat: false };
|
|
3939
3995
|
}
|
|
@@ -4354,11 +4410,11 @@ function recoveredFailureHeadlines(o, route) {
|
|
|
4354
4410
|
}
|
|
4355
4411
|
|
|
4356
4412
|
// src/install.ts
|
|
4357
|
-
import { dirname as dirname5, join as
|
|
4413
|
+
import { dirname as dirname5, join as join11, extname } from "path";
|
|
4358
4414
|
import { homedir as homedir6 } from "os";
|
|
4359
|
-
import { accessSync, constants, existsSync as existsSync10, mkdirSync as
|
|
4415
|
+
import { accessSync, constants, existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync4, readFileSync as readFileSync10, statSync as statSync3, writeFileSync as writeFileSync7, unlinkSync as unlinkSync2, realpathSync } from "fs";
|
|
4360
4416
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
4361
|
-
import { createHash } from "crypto";
|
|
4417
|
+
import { createHash as createHash2 } from "crypto";
|
|
4362
4418
|
|
|
4363
4419
|
// src/runtime-info.ts
|
|
4364
4420
|
function isCompiledBinary(moduleUrl) {
|
|
@@ -4368,7 +4424,7 @@ function isCompiledBinary(moduleUrl) {
|
|
|
4368
4424
|
// src/install.ts
|
|
4369
4425
|
function crewInvocation(crewHome) {
|
|
4370
4426
|
if (isCompiledBinary(import.meta.url)) return [process.execPath];
|
|
4371
|
-
return [process.execPath,
|
|
4427
|
+
return [process.execPath, join11(crewHome, "bin", "crew")];
|
|
4372
4428
|
}
|
|
4373
4429
|
var INTERVAL_SECONDS = 120;
|
|
4374
4430
|
function labelFor(crewHome, job) {
|
|
@@ -4379,7 +4435,7 @@ function labelFor(crewHome, job) {
|
|
|
4379
4435
|
return crewHome;
|
|
4380
4436
|
}
|
|
4381
4437
|
})();
|
|
4382
|
-
const hash =
|
|
4438
|
+
const hash = createHash2("sha1").update(real).digest("hex").slice(0, 8);
|
|
4383
4439
|
const stem = job === "release" ? "com.tablation.crew-release" : job === "passengers" ? "com.tablation.crew-passengers" : "com.tablation.crew";
|
|
4384
4440
|
return `${stem}.${hash}`;
|
|
4385
4441
|
}
|
|
@@ -4406,7 +4462,7 @@ function pathFor(ship) {
|
|
|
4406
4462
|
return parts.join(":");
|
|
4407
4463
|
}
|
|
4408
4464
|
function findOnPath(bin, searchPath) {
|
|
4409
|
-
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) =>
|
|
4465
|
+
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join11(dir, bin));
|
|
4410
4466
|
for (const candidate of candidates) {
|
|
4411
4467
|
try {
|
|
4412
4468
|
accessSync(candidate, constants.X_OK);
|
|
@@ -4416,7 +4472,7 @@ function findOnPath(bin, searchPath) {
|
|
|
4416
4472
|
}
|
|
4417
4473
|
return void 0;
|
|
4418
4474
|
}
|
|
4419
|
-
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin",
|
|
4475
|
+
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join11(homedir6(), ".docker", "bin")].join(":");
|
|
4420
4476
|
function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
4421
4477
|
const schedulerPath = pathFor(ship);
|
|
4422
4478
|
if (findOnPath("docker", schedulerPath)) return void 0;
|
|
@@ -4428,7 +4484,7 @@ function xmlEscape(s) {
|
|
|
4428
4484
|
}
|
|
4429
4485
|
function planLaunchd(ship, crewHome, job) {
|
|
4430
4486
|
const label = labelFor(crewHome, job);
|
|
4431
|
-
const unitPath =
|
|
4487
|
+
const unitPath = join11(homedir6(), "Library", "LaunchAgents", `${label}.plist`);
|
|
4432
4488
|
const crewLog = ship.logFile;
|
|
4433
4489
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
4434
4490
|
const persistent = job === "run";
|
|
@@ -4482,8 +4538,8 @@ ${scheduleKeys}
|
|
|
4482
4538
|
}
|
|
4483
4539
|
function planSystemd(ship, crewHome, job) {
|
|
4484
4540
|
const label = labelFor(crewHome, job);
|
|
4485
|
-
const unitDir =
|
|
4486
|
-
const servicePath =
|
|
4541
|
+
const unitDir = join11(homedir6(), ".config", "systemd", "user");
|
|
4542
|
+
const servicePath = join11(unitDir, `${label}.service`);
|
|
4487
4543
|
const crewLog = ship.logFile;
|
|
4488
4544
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
4489
4545
|
const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
|
|
@@ -4523,7 +4579,7 @@ WantedBy=default.target
|
|
|
4523
4579
|
schedulerLog
|
|
4524
4580
|
};
|
|
4525
4581
|
}
|
|
4526
|
-
const timerPath =
|
|
4582
|
+
const timerPath = join11(unitDir, `${label}.timer`);
|
|
4527
4583
|
const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
|
|
4528
4584
|
[Unit]
|
|
4529
4585
|
Description=${description}
|
|
@@ -4597,7 +4653,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
4597
4653
|
function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
4598
4654
|
const label = labelFor(crewHome, job);
|
|
4599
4655
|
if (hostShip === "macos") {
|
|
4600
|
-
const unitPath =
|
|
4656
|
+
const unitPath = join11(homedir6(), "Library", "LaunchAgents", `${label}.plist`);
|
|
4601
4657
|
return {
|
|
4602
4658
|
mechanism: "launchd",
|
|
4603
4659
|
unitPaths: [unitPath],
|
|
@@ -4607,18 +4663,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
4607
4663
|
};
|
|
4608
4664
|
}
|
|
4609
4665
|
if (hostShip === "linux" && hasSystemd) {
|
|
4610
|
-
const unitDir =
|
|
4666
|
+
const unitDir = join11(homedir6(), ".config", "systemd", "user");
|
|
4611
4667
|
if (job === "run") {
|
|
4612
4668
|
return {
|
|
4613
4669
|
mechanism: "systemd",
|
|
4614
|
-
unitPaths: [
|
|
4670
|
+
unitPaths: [join11(unitDir, `${label}.service`)],
|
|
4615
4671
|
crontabLine: null,
|
|
4616
4672
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
|
|
4617
4673
|
};
|
|
4618
4674
|
}
|
|
4619
4675
|
return {
|
|
4620
4676
|
mechanism: "systemd",
|
|
4621
|
-
unitPaths: [
|
|
4677
|
+
unitPaths: [join11(unitDir, `${label}.service`), join11(unitDir, `${label}.timer`)],
|
|
4622
4678
|
crontabLine: null,
|
|
4623
4679
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
|
|
4624
4680
|
};
|
|
@@ -4636,7 +4692,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
4636
4692
|
function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
4637
4693
|
const label = labelFor(crewHome, "run");
|
|
4638
4694
|
if (hostShip === "macos") {
|
|
4639
|
-
const unitPath =
|
|
4695
|
+
const unitPath = join11(homedir6(), "Library", "LaunchAgents", `${label}.plist`);
|
|
4640
4696
|
return {
|
|
4641
4697
|
mechanism: "launchd",
|
|
4642
4698
|
startCommand: ["launchctl", "load", unitPath],
|
|
@@ -4676,8 +4732,8 @@ async function applyInstall(plan, crewHome, dryRun, log) {
|
|
|
4676
4732
|
log.emit(`would write ${path}`);
|
|
4677
4733
|
continue;
|
|
4678
4734
|
}
|
|
4679
|
-
|
|
4680
|
-
|
|
4735
|
+
mkdirSync6(dirname5(path), { recursive: true });
|
|
4736
|
+
writeFileSync7(path, plan.unitContent[path]);
|
|
4681
4737
|
log.emit(`wrote ${path}`);
|
|
4682
4738
|
}
|
|
4683
4739
|
if (plan.crontabLine) {
|
|
@@ -4750,7 +4806,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
4750
4806
|
}
|
|
4751
4807
|
var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
|
|
4752
4808
|
function launchAgentsDir() {
|
|
4753
|
-
return
|
|
4809
|
+
return join11(homedir6(), "Library", "LaunchAgents");
|
|
4754
4810
|
}
|
|
4755
4811
|
function xmlUnescape(s) {
|
|
4756
4812
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
@@ -4769,10 +4825,10 @@ function loadedCrewLabels() {
|
|
|
4769
4825
|
function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
4770
4826
|
const files = existsSync10(dir) ? readdirSync4(dir) : [];
|
|
4771
4827
|
const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
|
|
4772
|
-
const plistPath =
|
|
4828
|
+
const plistPath = join11(dir, f);
|
|
4773
4829
|
let program = null;
|
|
4774
4830
|
try {
|
|
4775
|
-
program = programFromPlist(
|
|
4831
|
+
program = programFromPlist(readFileSync10(plistPath, "utf8"));
|
|
4776
4832
|
} catch {
|
|
4777
4833
|
}
|
|
4778
4834
|
return {
|
|
@@ -4785,7 +4841,7 @@ function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
|
4785
4841
|
const seen = new Set(units.map((u) => u.label));
|
|
4786
4842
|
for (const label of [...loaded].sort()) {
|
|
4787
4843
|
if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
|
|
4788
|
-
units.push({ label, plistPath:
|
|
4844
|
+
units.push({ label, plistPath: join11(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
|
|
4789
4845
|
}
|
|
4790
4846
|
return units;
|
|
4791
4847
|
}
|
|
@@ -5131,17 +5187,17 @@ var SHELL_WORDS = /* @__PURE__ */ new Set([
|
|
|
5131
5187
|
]);
|
|
5132
5188
|
|
|
5133
5189
|
// src/state.ts
|
|
5134
|
-
import { readFileSync as
|
|
5135
|
-
import { join as
|
|
5190
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync7, rmSync as rmSync7, statSync as statSync4 } from "fs";
|
|
5191
|
+
import { join as join12 } from "path";
|
|
5136
5192
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
5137
5193
|
var State = class _State {
|
|
5138
5194
|
dir;
|
|
5139
5195
|
constructor(dir) {
|
|
5140
5196
|
this.dir = dir;
|
|
5141
|
-
|
|
5197
|
+
mkdirSync7(dir, { recursive: true });
|
|
5142
5198
|
}
|
|
5143
5199
|
path(...parts) {
|
|
5144
|
-
return
|
|
5200
|
+
return join12(this.dir, ...parts);
|
|
5145
5201
|
}
|
|
5146
5202
|
/**
|
|
5147
5203
|
* The timestamp every "new since last poll" check is measured against.
|
|
@@ -5152,14 +5208,14 @@ var State = class _State {
|
|
|
5152
5208
|
*/
|
|
5153
5209
|
watermark() {
|
|
5154
5210
|
try {
|
|
5155
|
-
const v =
|
|
5211
|
+
const v = readFileSync11(this.path(".poll-watermark"), "utf8").trim();
|
|
5156
5212
|
return v || EPOCH;
|
|
5157
5213
|
} catch {
|
|
5158
5214
|
return EPOCH;
|
|
5159
5215
|
}
|
|
5160
5216
|
}
|
|
5161
5217
|
setWatermark(at) {
|
|
5162
|
-
|
|
5218
|
+
writeFileSync8(this.path(".poll-watermark"), `${at}
|
|
5163
5219
|
`);
|
|
5164
5220
|
}
|
|
5165
5221
|
/**
|
|
@@ -5186,7 +5242,7 @@ var State = class _State {
|
|
|
5186
5242
|
return new Set(roles.filter((r) => this.isRolePaused(r)));
|
|
5187
5243
|
}
|
|
5188
5244
|
pause(role) {
|
|
5189
|
-
|
|
5245
|
+
writeFileSync8(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
|
|
5190
5246
|
}
|
|
5191
5247
|
resume(role) {
|
|
5192
5248
|
rmSync7(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
|
|
@@ -5200,7 +5256,7 @@ var State = class _State {
|
|
|
5200
5256
|
return existsSync12(this.path(".release-paused"));
|
|
5201
5257
|
}
|
|
5202
5258
|
setReleasePaused(paused) {
|
|
5203
|
-
if (paused)
|
|
5259
|
+
if (paused) writeFileSync8(this.path(".release-paused"), "");
|
|
5204
5260
|
else rmSync7(this.path(".release-paused"), { force: true });
|
|
5205
5261
|
}
|
|
5206
5262
|
/**
|
|
@@ -5238,7 +5294,7 @@ var State = class _State {
|
|
|
5238
5294
|
const name = _State.safe(rawName);
|
|
5239
5295
|
const file = this.path(`.${name}.lock`);
|
|
5240
5296
|
try {
|
|
5241
|
-
const existing = Number.parseInt(
|
|
5297
|
+
const existing = Number.parseInt(readFileSync11(file, "utf8").trim(), 10);
|
|
5242
5298
|
if (Number.isFinite(existing) && existing > 0) {
|
|
5243
5299
|
try {
|
|
5244
5300
|
process.kill(existing, 0);
|
|
@@ -5248,7 +5304,7 @@ var State = class _State {
|
|
|
5248
5304
|
}
|
|
5249
5305
|
} catch {
|
|
5250
5306
|
}
|
|
5251
|
-
|
|
5307
|
+
writeFileSync8(file, `${process.pid}
|
|
5252
5308
|
`);
|
|
5253
5309
|
return {
|
|
5254
5310
|
ok: true,
|
|
@@ -5332,7 +5388,7 @@ var State = class _State {
|
|
|
5332
5388
|
const heldReported = `.release-held-${suffix}`;
|
|
5333
5389
|
const read = (f) => {
|
|
5334
5390
|
try {
|
|
5335
|
-
return
|
|
5391
|
+
return readFileSync11(this.path(f), "utf8").trim() || null;
|
|
5336
5392
|
} catch {
|
|
5337
5393
|
return null;
|
|
5338
5394
|
}
|
|
@@ -5341,7 +5397,7 @@ var State = class _State {
|
|
|
5341
5397
|
blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
|
|
5342
5398
|
noteBlocked: () => {
|
|
5343
5399
|
const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
|
|
5344
|
-
|
|
5400
|
+
writeFileSync8(this.path(blocked), `${n}
|
|
5345
5401
|
`);
|
|
5346
5402
|
return n;
|
|
5347
5403
|
},
|
|
@@ -5358,7 +5414,7 @@ var State = class _State {
|
|
|
5358
5414
|
*/
|
|
5359
5415
|
deployFailedSha: () => read(failed),
|
|
5360
5416
|
noteDeployFailed: (sha) => {
|
|
5361
|
-
|
|
5417
|
+
writeFileSync8(this.path(failed), `${sha}
|
|
5362
5418
|
`);
|
|
5363
5419
|
},
|
|
5364
5420
|
clearDeployFailed: () => {
|
|
@@ -5372,7 +5428,7 @@ var State = class _State {
|
|
|
5372
5428
|
noteTestGateFailed: (sha) => {
|
|
5373
5429
|
const [prev, n] = (read(gateFailed) ?? "").split(" ");
|
|
5374
5430
|
const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
|
|
5375
|
-
|
|
5431
|
+
writeFileSync8(this.path(gateFailed), `${sha} ${next}
|
|
5376
5432
|
`);
|
|
5377
5433
|
return next;
|
|
5378
5434
|
},
|
|
@@ -5382,13 +5438,13 @@ var State = class _State {
|
|
|
5382
5438
|
/** Whether the board was already told about this head's red gate. */
|
|
5383
5439
|
heldReported: () => {
|
|
5384
5440
|
try {
|
|
5385
|
-
return
|
|
5441
|
+
return readFileSync11(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
|
|
5386
5442
|
} catch {
|
|
5387
5443
|
return null;
|
|
5388
5444
|
}
|
|
5389
5445
|
},
|
|
5390
5446
|
noteHeldReported: (signature) => {
|
|
5391
|
-
|
|
5447
|
+
writeFileSync8(this.path(heldReported), `${signature}
|
|
5392
5448
|
`);
|
|
5393
5449
|
},
|
|
5394
5450
|
clearHeldReported: () => {
|
|
@@ -5396,7 +5452,7 @@ var State = class _State {
|
|
|
5396
5452
|
},
|
|
5397
5453
|
testGateReported: (sha) => read(gateReported) === sha,
|
|
5398
5454
|
noteTestGateReported: (sha) => {
|
|
5399
|
-
|
|
5455
|
+
writeFileSync8(this.path(gateReported), `${sha}
|
|
5400
5456
|
`);
|
|
5401
5457
|
}
|
|
5402
5458
|
};
|
|
@@ -5419,7 +5475,7 @@ var State = class _State {
|
|
|
5419
5475
|
const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
|
|
5420
5476
|
const read = (f) => {
|
|
5421
5477
|
try {
|
|
5422
|
-
return
|
|
5478
|
+
return readFileSync11(this.path(f), "utf8").trim() || null;
|
|
5423
5479
|
} catch {
|
|
5424
5480
|
return null;
|
|
5425
5481
|
}
|
|
@@ -5452,18 +5508,18 @@ var State = class _State {
|
|
|
5452
5508
|
clear2();
|
|
5453
5509
|
return;
|
|
5454
5510
|
}
|
|
5455
|
-
|
|
5511
|
+
writeFileSync8(this.path(streakFile), `${this.readInt(streakFile) + 1}
|
|
5456
5512
|
`);
|
|
5457
5513
|
const prior = read(ticketFile);
|
|
5458
5514
|
if (topTicket && topTicket === prior) {
|
|
5459
|
-
|
|
5515
|
+
writeFileSync8(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
|
|
5460
5516
|
`);
|
|
5461
5517
|
} else if (topTicket) {
|
|
5462
|
-
|
|
5518
|
+
writeFileSync8(this.path(ticketFile), `${topTicket}
|
|
5463
5519
|
`);
|
|
5464
|
-
|
|
5520
|
+
writeFileSync8(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
5465
5521
|
`);
|
|
5466
|
-
|
|
5522
|
+
writeFileSync8(this.path(ticketStreakFile), "1\n");
|
|
5467
5523
|
}
|
|
5468
5524
|
}
|
|
5469
5525
|
};
|
|
@@ -5484,14 +5540,14 @@ var State = class _State {
|
|
|
5484
5540
|
return {
|
|
5485
5541
|
previous: () => {
|
|
5486
5542
|
try {
|
|
5487
|
-
const parsed = JSON.parse(
|
|
5543
|
+
const parsed = JSON.parse(readFileSync11(this.path(file), "utf8"));
|
|
5488
5544
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
5489
5545
|
} catch {
|
|
5490
5546
|
return {};
|
|
5491
5547
|
}
|
|
5492
5548
|
},
|
|
5493
5549
|
persist: (next) => {
|
|
5494
|
-
|
|
5550
|
+
writeFileSync8(this.path(file), JSON.stringify(next));
|
|
5495
5551
|
}
|
|
5496
5552
|
};
|
|
5497
5553
|
}
|
|
@@ -5505,14 +5561,14 @@ var State = class _State {
|
|
|
5505
5561
|
return {
|
|
5506
5562
|
previous: () => {
|
|
5507
5563
|
try {
|
|
5508
|
-
const parsed = JSON.parse(
|
|
5564
|
+
const parsed = JSON.parse(readFileSync11(this.path(file), "utf8"));
|
|
5509
5565
|
return Array.isArray(parsed) ? parsed : [];
|
|
5510
5566
|
} catch {
|
|
5511
5567
|
return [];
|
|
5512
5568
|
}
|
|
5513
5569
|
},
|
|
5514
5570
|
persist: (next) => {
|
|
5515
|
-
|
|
5571
|
+
writeFileSync8(this.path(file), JSON.stringify(next));
|
|
5516
5572
|
}
|
|
5517
5573
|
};
|
|
5518
5574
|
}
|
|
@@ -5525,7 +5581,7 @@ var State = class _State {
|
|
|
5525
5581
|
const file = ".release-last-run";
|
|
5526
5582
|
return {
|
|
5527
5583
|
touch: () => {
|
|
5528
|
-
|
|
5584
|
+
writeFileSync8(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
|
|
5529
5585
|
},
|
|
5530
5586
|
at: () => {
|
|
5531
5587
|
try {
|
|
@@ -5538,7 +5594,7 @@ var State = class _State {
|
|
|
5538
5594
|
}
|
|
5539
5595
|
readInt(file) {
|
|
5540
5596
|
try {
|
|
5541
|
-
return Number.parseInt(
|
|
5597
|
+
return Number.parseInt(readFileSync11(this.path(file), "utf8").trim(), 10) || 0;
|
|
5542
5598
|
} catch {
|
|
5543
5599
|
return 0;
|
|
5544
5600
|
}
|
|
@@ -6540,12 +6596,12 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
|
|
|
6540
6596
|
|
|
6541
6597
|
// src/run-worktree.ts
|
|
6542
6598
|
import { existsSync as existsSync14 } from "fs";
|
|
6543
|
-
import { dirname as dirname6, join as
|
|
6599
|
+
import { dirname as dirname6, join as join14 } from "path";
|
|
6544
6600
|
|
|
6545
6601
|
// src/ports.ts
|
|
6546
6602
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
6547
6603
|
import { existsSync as existsSync13 } from "fs";
|
|
6548
|
-
import { join as
|
|
6604
|
+
import { join as join13 } from "path";
|
|
6549
6605
|
var run = (cmd, args) => {
|
|
6550
6606
|
try {
|
|
6551
6607
|
return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -6590,7 +6646,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
6590
6646
|
}
|
|
6591
6647
|
function worktreeExists(parent, prefix, n) {
|
|
6592
6648
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
6593
|
-
if (existsSync13(
|
|
6649
|
+
if (existsSync13(join13(parent, name, ".git"))) return true;
|
|
6594
6650
|
}
|
|
6595
6651
|
return false;
|
|
6596
6652
|
}
|
|
@@ -6682,7 +6738,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
6682
6738
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
6683
6739
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
6684
6740
|
}
|
|
6685
|
-
const path =
|
|
6741
|
+
const path = join14(dirname6(dir), worktreeForTicket(dir, cfg, t, role));
|
|
6686
6742
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
6687
6743
|
return { action: "cut-remote", path, branch: located.branch };
|
|
6688
6744
|
}
|
|
@@ -7026,12 +7082,12 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
|
|
|
7026
7082
|
|
|
7027
7083
|
// src/pending-handoff.ts
|
|
7028
7084
|
var PENDING_MAX_AGE_MS = 48 * 36e5;
|
|
7029
|
-
var pendingDirFor = (stateDir) =>
|
|
7030
|
-
var stateDirOfStream = (streamPath) =>
|
|
7085
|
+
var pendingDirFor = (stateDir) => join15(stateDir, "pending-handoffs");
|
|
7086
|
+
var stateDirOfStream = (streamPath) => join15(streamPath, "..", "..");
|
|
7031
7087
|
function writePendingHandoff(stateDir, rec) {
|
|
7032
7088
|
const dir = pendingDirFor(stateDir);
|
|
7033
|
-
|
|
7034
|
-
|
|
7089
|
+
mkdirSync8(dir, { recursive: true });
|
|
7090
|
+
writeFileSync9(join15(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
|
|
7035
7091
|
}
|
|
7036
7092
|
function listPendingHandoffs(stateDir, route) {
|
|
7037
7093
|
const dir = pendingDirFor(stateDir);
|
|
@@ -7039,7 +7095,7 @@ function listPendingHandoffs(stateDir, route) {
|
|
|
7039
7095
|
const out = [];
|
|
7040
7096
|
for (const f of readdirSync5(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
7041
7097
|
try {
|
|
7042
|
-
const rec = JSON.parse(
|
|
7098
|
+
const rec = JSON.parse(readFileSync12(join15(dir, f), "utf8"));
|
|
7043
7099
|
if (rec.version === 1 && rec.ticketRecordId && (!route || rec.route === route)) out.push(rec);
|
|
7044
7100
|
} catch {
|
|
7045
7101
|
}
|
|
@@ -7047,7 +7103,7 @@ function listPendingHandoffs(stateDir, route) {
|
|
|
7047
7103
|
return out;
|
|
7048
7104
|
}
|
|
7049
7105
|
function removePendingHandoff(stateDir, ticketRecordId) {
|
|
7050
|
-
rmSync8(
|
|
7106
|
+
rmSync8(join15(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
|
|
7051
7107
|
}
|
|
7052
7108
|
function pendingHandoffFor(stateDir, ticketRecordId) {
|
|
7053
7109
|
return listPendingHandoffs(stateDir).find((r) => r.ticketRecordId === ticketRecordId);
|
|
@@ -7157,8 +7213,8 @@ function expandSkillMarkers(text, lookup, opts = {}) {
|
|
|
7157
7213
|
}
|
|
7158
7214
|
|
|
7159
7215
|
// src/skills.ts
|
|
7160
|
-
import { readFileSync as
|
|
7161
|
-
import { join as
|
|
7216
|
+
import { readFileSync as readFileSync14, readdirSync as readdirSync6, existsSync as existsSync17 } from "fs";
|
|
7217
|
+
import { join as join17 } from "path";
|
|
7162
7218
|
import { parse as parseYaml } from "yaml";
|
|
7163
7219
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError3, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
7164
7220
|
|
|
@@ -7174,8 +7230,8 @@ ${additions}`;
|
|
|
7174
7230
|
}
|
|
7175
7231
|
|
|
7176
7232
|
// src/agents.ts
|
|
7177
|
-
import { readFileSync as
|
|
7178
|
-
import { join as
|
|
7233
|
+
import { readFileSync as readFileSync13, existsSync as existsSync16 } from "fs";
|
|
7234
|
+
import { join as join16 } from "path";
|
|
7179
7235
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError2, TablationApiError } from "@tablation/client";
|
|
7180
7236
|
var PERSONA_NAME = {
|
|
7181
7237
|
dev: "Developer",
|
|
@@ -7189,12 +7245,12 @@ var CREW_AGENT_COLUMN = "agent_id";
|
|
|
7189
7245
|
var AgentsSyncError = class extends Error {
|
|
7190
7246
|
};
|
|
7191
7247
|
function personaDefaultPrompt(promptsDir, role) {
|
|
7192
|
-
const briefPath =
|
|
7248
|
+
const briefPath = join16(promptsDir, "personas", `lane-${role}.md`);
|
|
7193
7249
|
if (!existsSync16(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
7194
|
-
if (SELF_CONTAINED_ROLES.has(role)) return
|
|
7195
|
-
const commonPath =
|
|
7196
|
-
if (!existsSync16(commonPath)) return
|
|
7197
|
-
return `${
|
|
7250
|
+
if (SELF_CONTAINED_ROLES.has(role)) return readFileSync13(briefPath, "utf8");
|
|
7251
|
+
const commonPath = join16(promptsDir, "personas", "common.md");
|
|
7252
|
+
if (!existsSync16(commonPath)) return readFileSync13(briefPath, "utf8");
|
|
7253
|
+
return `${readFileSync13(commonPath, "utf8")}${readFileSync13(briefPath, "utf8")}`;
|
|
7198
7254
|
}
|
|
7199
7255
|
async function resolveAgentId(route, role, opts) {
|
|
7200
7256
|
const cached = route.resolved?.agentPersonas?.[role]?.agentId;
|
|
@@ -7641,7 +7697,7 @@ function describeCrewLink(o) {
|
|
|
7641
7697
|
var SkillSyncError = class extends Error {
|
|
7642
7698
|
};
|
|
7643
7699
|
function parseSkillFile(path) {
|
|
7644
|
-
const text =
|
|
7700
|
+
const text = readFileSync14(path, "utf8");
|
|
7645
7701
|
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
|
|
7646
7702
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
7647
7703
|
const frontmatter = match[1];
|
|
@@ -7654,9 +7710,9 @@ function parseSkillFile(path) {
|
|
|
7654
7710
|
return { name: meta.name, description: meta.description, prompt: body, file: path };
|
|
7655
7711
|
}
|
|
7656
7712
|
function listLocalSkills(promptsDir) {
|
|
7657
|
-
const dir =
|
|
7713
|
+
const dir = join17(promptsDir, "skills");
|
|
7658
7714
|
if (!existsSync17(dir)) return [];
|
|
7659
|
-
return readdirSync6(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(
|
|
7715
|
+
return readdirSync6(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join17(dir, f)));
|
|
7660
7716
|
}
|
|
7661
7717
|
async function syncSkills(route, opts) {
|
|
7662
7718
|
if (!route.resolved) {
|
|
@@ -7859,23 +7915,23 @@ var AgentError = class extends Error {
|
|
|
7859
7915
|
var AgentSpawnError = class extends AgentError {
|
|
7860
7916
|
};
|
|
7861
7917
|
function planAgentRun(o) {
|
|
7862
|
-
const briefPath =
|
|
7918
|
+
const briefPath = join18(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
7863
7919
|
if (!existsSync18(briefPath)) {
|
|
7864
7920
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
7865
7921
|
}
|
|
7866
|
-
const commonPath =
|
|
7922
|
+
const commonPath = join18(o.route.promptsDir, "personas", "common.md");
|
|
7867
7923
|
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync18(commonPath);
|
|
7868
7924
|
const now = o.now ?? (() => Date.now());
|
|
7869
7925
|
const routeLabel = routeSlug(o.route.route);
|
|
7870
|
-
const digestPath2 =
|
|
7926
|
+
const digestPath2 = join18(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
7871
7927
|
let digest;
|
|
7872
7928
|
let ageSeconds;
|
|
7873
7929
|
if (existsSync18(digestPath2)) {
|
|
7874
7930
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
7875
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
7931
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync15(digestPath2, "utf8");
|
|
7876
7932
|
}
|
|
7877
|
-
const common = usesCommon ?
|
|
7878
|
-
const brief =
|
|
7933
|
+
const common = usesCommon ? readFileSync15(commonPath, "utf8") : "";
|
|
7934
|
+
const brief = readFileSync15(briefPath, "utf8");
|
|
7879
7935
|
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
7880
7936
|
const effectiveBrief = appendAdditions(o.divergedPrompt ?? brief, o.personaAdditions);
|
|
7881
7937
|
const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
|
|
@@ -7896,8 +7952,8 @@ function planAgentRun(o) {
|
|
|
7896
7952
|
brief: expanded.text,
|
|
7897
7953
|
digest
|
|
7898
7954
|
});
|
|
7899
|
-
const promptSha =
|
|
7900
|
-
const streamsDir =
|
|
7955
|
+
const promptSha = createHash3("sha256").update(expanded.text).digest("hex");
|
|
7956
|
+
const streamsDir = join18(o.stateDir, "streams");
|
|
7901
7957
|
const base = `${routeLabel}-${o.role}-${o.cycle}`;
|
|
7902
7958
|
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
7903
7959
|
const { toolchain, model } = seat;
|
|
@@ -7953,8 +8009,8 @@ function planAgentRun(o) {
|
|
|
7953
8009
|
digestAttached: digest !== void 0,
|
|
7954
8010
|
digestAgeSeconds: ageSeconds,
|
|
7955
8011
|
model,
|
|
7956
|
-
streamPath:
|
|
7957
|
-
eventsPath:
|
|
8012
|
+
streamPath: join18(streamsDir, `${base}.jsonl`),
|
|
8013
|
+
eventsPath: join18(streamsDir, `${base}.events.jsonl`),
|
|
7958
8014
|
ticket: o.ticket,
|
|
7959
8015
|
ticketRecordId: o.ticketRecordId ?? null,
|
|
7960
8016
|
agentLog,
|
|
@@ -7983,7 +8039,7 @@ function describePlan(p) {
|
|
|
7983
8039
|
var pidPathFor = (streamPath) => `${streamPath}.pid`;
|
|
7984
8040
|
function readRunSidecar(streamPath) {
|
|
7985
8041
|
try {
|
|
7986
|
-
const sc = JSON.parse(
|
|
8042
|
+
const sc = JSON.parse(readFileSync15(pidPathFor(streamPath), "utf8"));
|
|
7987
8043
|
return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
|
|
7988
8044
|
} catch {
|
|
7989
8045
|
return null;
|
|
@@ -8019,7 +8075,7 @@ function identityOf(plan, adapterHarness) {
|
|
|
8019
8075
|
var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
|
|
8020
8076
|
function openSink(path) {
|
|
8021
8077
|
try {
|
|
8022
|
-
|
|
8078
|
+
mkdirSync9(dirname7(path), { recursive: true });
|
|
8023
8079
|
return createWriteStream(path, { flags: "a" });
|
|
8024
8080
|
} catch {
|
|
8025
8081
|
return void 0;
|
|
@@ -8198,7 +8254,7 @@ var RunObserver = class {
|
|
|
8198
8254
|
};
|
|
8199
8255
|
function writeRunMeta(id, startedAt, liveTicket, extra) {
|
|
8200
8256
|
try {
|
|
8201
|
-
|
|
8257
|
+
writeFileSync10(`${id.streamPath}.meta.json`, JSON.stringify({
|
|
8202
8258
|
route: id.route,
|
|
8203
8259
|
role: id.role,
|
|
8204
8260
|
hint: id.ticket,
|
|
@@ -8317,7 +8373,7 @@ function spawnAgent(plan, emit) {
|
|
|
8317
8373
|
return new Promise((resolve6, reject) => {
|
|
8318
8374
|
let streamFd;
|
|
8319
8375
|
try {
|
|
8320
|
-
|
|
8376
|
+
mkdirSync9(dirname7(plan.streamPath), { recursive: true });
|
|
8321
8377
|
streamFd = openSync3(plan.streamPath, "a");
|
|
8322
8378
|
} catch (e) {
|
|
8323
8379
|
void endSink(eventsSink);
|
|
@@ -8350,7 +8406,7 @@ function spawnAgent(plan, emit) {
|
|
|
8350
8406
|
promptSha: plan.promptSha
|
|
8351
8407
|
};
|
|
8352
8408
|
try {
|
|
8353
|
-
|
|
8409
|
+
writeFileSync10(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
|
|
8354
8410
|
} catch {
|
|
8355
8411
|
}
|
|
8356
8412
|
}
|
|
@@ -8567,7 +8623,7 @@ async function runSelftest(i, deps = realDeps) {
|
|
|
8567
8623
|
...adapter.runEnv(spec),
|
|
8568
8624
|
[KEY_VAR]: i.apiKey
|
|
8569
8625
|
};
|
|
8570
|
-
const cwd = mkdtempSync(
|
|
8626
|
+
const cwd = mkdtempSync(join19(tmpdir2(), "crew-selftest-"));
|
|
8571
8627
|
try {
|
|
8572
8628
|
const run2 = await deps.run({
|
|
8573
8629
|
bin: invocation.bin,
|
|
@@ -8599,11 +8655,11 @@ function formatResult(r) {
|
|
|
8599
8655
|
const secs = `${(r.ms / 1e3).toFixed(1)}s`;
|
|
8600
8656
|
return r.ok ? `${r.toolchain}: PASS ${r.detail} (${secs}${tok})` : `${r.toolchain}: FAIL ${r.failure} \u2014 ${r.detail} (${secs})`;
|
|
8601
8657
|
}
|
|
8602
|
-
var
|
|
8658
|
+
var fileFor2 = (stateDir) => join19(stateDir, "selftest.json");
|
|
8603
8659
|
function readSelftest(stateDir) {
|
|
8604
8660
|
try {
|
|
8605
|
-
const p =
|
|
8606
|
-
return existsSync19(p) ? JSON.parse(
|
|
8661
|
+
const p = fileFor2(stateDir);
|
|
8662
|
+
return existsSync19(p) ? JSON.parse(readFileSync16(p, "utf8")) : {};
|
|
8607
8663
|
} catch {
|
|
8608
8664
|
return {};
|
|
8609
8665
|
}
|
|
@@ -8611,8 +8667,8 @@ function readSelftest(stateDir) {
|
|
|
8611
8667
|
function recordSelftest(stateDir, results) {
|
|
8612
8668
|
const all = readSelftest(stateDir);
|
|
8613
8669
|
for (const r of results) all[r.toolchain] = r;
|
|
8614
|
-
|
|
8615
|
-
|
|
8670
|
+
mkdirSync10(stateDir, { recursive: true });
|
|
8671
|
+
writeFileSync11(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
|
|
8616
8672
|
`);
|
|
8617
8673
|
}
|
|
8618
8674
|
function doctorSelftestLines(stored, toolchains) {
|
|
@@ -8624,7 +8680,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
8624
8680
|
}
|
|
8625
8681
|
|
|
8626
8682
|
// src/cli.ts
|
|
8627
|
-
import { resolve as resolve5, dirname as dirname20, join as
|
|
8683
|
+
import { resolve as resolve5, dirname as dirname20, join as join36, basename as basename11 } from "path";
|
|
8628
8684
|
import { fileURLToPath } from "url";
|
|
8629
8685
|
import { hostname as hostname2, homedir as homedir9 } from "os";
|
|
8630
8686
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
@@ -8636,6 +8692,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
8636
8692
|
"area",
|
|
8637
8693
|
"base-url",
|
|
8638
8694
|
"choose",
|
|
8695
|
+
"clear",
|
|
8639
8696
|
"dir",
|
|
8640
8697
|
"extra-path",
|
|
8641
8698
|
"harness",
|
|
@@ -8803,14 +8860,14 @@ function rosterMarkdown(roster, meId) {
|
|
|
8803
8860
|
}
|
|
8804
8861
|
|
|
8805
8862
|
// src/connect-resume.ts
|
|
8806
|
-
import { mkdirSync as
|
|
8807
|
-
import { dirname as dirname8, join as
|
|
8863
|
+
import { mkdirSync as mkdirSync11, readFileSync as readFileSync17, rmSync as rmSync10, writeFileSync as writeFileSync12 } from "fs";
|
|
8864
|
+
import { dirname as dirname8, join as join20 } from "path";
|
|
8808
8865
|
function lastSessionFile(stateDir) {
|
|
8809
|
-
return
|
|
8866
|
+
return join20(stateDir, "connect-session.json");
|
|
8810
8867
|
}
|
|
8811
8868
|
function readPointers(stateDir) {
|
|
8812
8869
|
try {
|
|
8813
|
-
const parsed = JSON.parse(
|
|
8870
|
+
const parsed = JSON.parse(readFileSync17(lastSessionFile(stateDir), "utf8"));
|
|
8814
8871
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
8815
8872
|
const out = {};
|
|
8816
8873
|
for (const [host, v] of Object.entries(parsed)) {
|
|
@@ -8824,8 +8881,8 @@ function readPointers(stateDir) {
|
|
|
8824
8881
|
}
|
|
8825
8882
|
function writePointers(stateDir, pointers) {
|
|
8826
8883
|
const file = lastSessionFile(stateDir);
|
|
8827
|
-
|
|
8828
|
-
|
|
8884
|
+
mkdirSync11(dirname8(file), { recursive: true });
|
|
8885
|
+
writeFileSync12(file, `${JSON.stringify(pointers, null, 2)}
|
|
8829
8886
|
`, { mode: 384 });
|
|
8830
8887
|
}
|
|
8831
8888
|
function rememberSession(stateDir, host, workspaceSlug) {
|
|
@@ -8881,15 +8938,15 @@ async function signOutSessions(o) {
|
|
|
8881
8938
|
}
|
|
8882
8939
|
|
|
8883
8940
|
// src/reset.ts
|
|
8884
|
-
import { existsSync as existsSync20, mkdirSync as
|
|
8885
|
-
import { dirname as dirname9, join as
|
|
8941
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync12, readFileSync as readFileSync18, readdirSync as readdirSync7, rmSync as rmSync11, statSync as statSync6, writeFileSync as writeFileSync13 } from "fs";
|
|
8942
|
+
import { dirname as dirname9, join as join21 } from "path";
|
|
8886
8943
|
import { hostFromUrl as hostFromUrl2 } from "@tablation/client";
|
|
8887
8944
|
function clonedCheckoutsFile(stateDir) {
|
|
8888
|
-
return
|
|
8945
|
+
return join21(stateDir, "cloned-checkouts.json");
|
|
8889
8946
|
}
|
|
8890
8947
|
function readClonedCheckouts(stateDir) {
|
|
8891
8948
|
try {
|
|
8892
|
-
const parsed = JSON.parse(
|
|
8949
|
+
const parsed = JSON.parse(readFileSync18(clonedCheckoutsFile(stateDir), "utf8"));
|
|
8893
8950
|
return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
|
|
8894
8951
|
} catch {
|
|
8895
8952
|
return [];
|
|
@@ -8899,8 +8956,8 @@ function recordClonedCheckout(stateDir, dir) {
|
|
|
8899
8956
|
const known = readClonedCheckouts(stateDir);
|
|
8900
8957
|
if (known.includes(dir)) return;
|
|
8901
8958
|
const file = clonedCheckoutsFile(stateDir);
|
|
8902
|
-
|
|
8903
|
-
|
|
8959
|
+
mkdirSync12(dirname9(file), { recursive: true });
|
|
8960
|
+
writeFileSync13(file, `${JSON.stringify([...known, dir], null, 2)}
|
|
8904
8961
|
`);
|
|
8905
8962
|
}
|
|
8906
8963
|
var mark = (dryRun) => dryRun ? "would-remove" : "removed";
|
|
@@ -8974,10 +9031,10 @@ function removeCheckout(dir, dryRun, fx) {
|
|
|
8974
9031
|
}
|
|
8975
9032
|
function removeState(stateDir, dryRun) {
|
|
8976
9033
|
const out = [];
|
|
8977
|
-
const targets = [
|
|
9034
|
+
const targets = [join21(stateDir, "resolved"), join21(stateDir, "keys"), clonedCheckoutsFile(stateDir), join21(stateDir, "connect-session.json"), join21(stateDir, "reload-requested"), join21(stateDir, "daemon.started")];
|
|
8978
9035
|
try {
|
|
8979
9036
|
for (const name of readdirSync7(stateDir)) {
|
|
8980
|
-
if (name.startsWith(".") && statSync6(
|
|
9037
|
+
if (name.startsWith(".") && statSync6(join21(stateDir, name)).isFile()) targets.push(join21(stateDir, name));
|
|
8981
9038
|
}
|
|
8982
9039
|
} catch {
|
|
8983
9040
|
}
|
|
@@ -9762,8 +9819,8 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
|
|
|
9762
9819
|
}
|
|
9763
9820
|
|
|
9764
9821
|
// src/poll.ts
|
|
9765
|
-
import { writeFileSync as
|
|
9766
|
-
import { join as
|
|
9822
|
+
import { writeFileSync as writeFileSync14 } from "fs";
|
|
9823
|
+
import { join as join22 } from "path";
|
|
9767
9824
|
|
|
9768
9825
|
// src/stalled.ts
|
|
9769
9826
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -10054,7 +10111,7 @@ async function decideCycle(o) {
|
|
|
10054
10111
|
}
|
|
10055
10112
|
return decision;
|
|
10056
10113
|
}
|
|
10057
|
-
var digestPath = (stateDir, route, role) =>
|
|
10114
|
+
var digestPath = (stateDir, route, role) => join22(stateDir, `digest-${route}-${role}.md`);
|
|
10058
10115
|
function writeDigest(o, d, role, stateDir) {
|
|
10059
10116
|
const me = o.route.resolved?.seats[role];
|
|
10060
10117
|
if (!me) return false;
|
|
@@ -10122,7 +10179,7 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
10122
10179
|
};
|
|
10123
10180
|
const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
|
|
10124
10181
|
const path = digestPath(stateDir, routeSlug(o.route.route), role);
|
|
10125
|
-
|
|
10182
|
+
writeFileSync14(path, text);
|
|
10126
10183
|
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
|
|
10127
10184
|
data: { role, bytes: Buffer.byteLength(text) }
|
|
10128
10185
|
});
|
|
@@ -10528,8 +10585,8 @@ async function applyStrandedVerified(writer, s, contract, operatorId, memberId,
|
|
|
10528
10585
|
}
|
|
10529
10586
|
|
|
10530
10587
|
// src/connect-wizard.ts
|
|
10531
|
-
import { delimiter, dirname as dirname10, join as
|
|
10532
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync21, mkdirSync as
|
|
10588
|
+
import { delimiter, dirname as dirname10, join as join23 } from "path";
|
|
10589
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync21, mkdirSync as mkdirSync13, writeFileSync as writeFileSync15 } from "fs";
|
|
10533
10590
|
import { stringify } from "yaml";
|
|
10534
10591
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
10535
10592
|
function shouldRunWizard(opts) {
|
|
@@ -10551,7 +10608,7 @@ function defaultIsExecutable(p) {
|
|
|
10551
10608
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
10552
10609
|
for (const dir of pathEnv.split(delimiter)) {
|
|
10553
10610
|
if (!dir) continue;
|
|
10554
|
-
const candidate =
|
|
10611
|
+
const candidate = join23(dir, name);
|
|
10555
10612
|
if (isExecutable(candidate)) return candidate;
|
|
10556
10613
|
}
|
|
10557
10614
|
return void 0;
|
|
@@ -10612,9 +10669,9 @@ routes:
|
|
|
10612
10669
|
${routeBlock}`;
|
|
10613
10670
|
}
|
|
10614
10671
|
function writeNewConfig(path, text) {
|
|
10615
|
-
|
|
10672
|
+
mkdirSync13(dirname10(path), { recursive: true });
|
|
10616
10673
|
try {
|
|
10617
|
-
|
|
10674
|
+
writeFileSync15(path, text, { mode: 384, flag: "wx" });
|
|
10618
10675
|
return true;
|
|
10619
10676
|
} catch (e) {
|
|
10620
10677
|
if (e.code === "EEXIST") return false;
|
|
@@ -10648,7 +10705,7 @@ Wrote ${o.configPath}.
|
|
|
10648
10705
|
// src/connect-json.ts
|
|
10649
10706
|
import { accessSync as accessSync3, constants as constants3 } from "fs";
|
|
10650
10707
|
import { homedir as homedir7 } from "os";
|
|
10651
|
-
import { dirname as dirname11, join as
|
|
10708
|
+
import { dirname as dirname11, join as join24 } from "path";
|
|
10652
10709
|
import { delimiter as delimiter2 } from "path";
|
|
10653
10710
|
var CHOICE_EXIT_CODE = 3;
|
|
10654
10711
|
function createEmitter(json, write) {
|
|
@@ -10687,12 +10744,12 @@ function agentBinSearchDirs(home, pathEnv, extraPath) {
|
|
|
10687
10744
|
const dirs = [
|
|
10688
10745
|
...pathEnv.split(delimiter2),
|
|
10689
10746
|
...(extraPath ?? "").split(delimiter2),
|
|
10690
|
-
|
|
10691
|
-
|
|
10747
|
+
join24(home, ".local/bin"),
|
|
10748
|
+
join24(home, ".claude/local"),
|
|
10692
10749
|
"/opt/homebrew/bin",
|
|
10693
10750
|
"/usr/local/bin",
|
|
10694
|
-
|
|
10695
|
-
|
|
10751
|
+
join24(home, ".npm-global/bin"),
|
|
10752
|
+
join24(home, ".volta/bin")
|
|
10696
10753
|
];
|
|
10697
10754
|
return [...new Set(dirs.filter(Boolean))];
|
|
10698
10755
|
}
|
|
@@ -10728,8 +10785,8 @@ function flagWizardAnswers(f, env) {
|
|
|
10728
10785
|
}
|
|
10729
10786
|
|
|
10730
10787
|
// src/release-run.ts
|
|
10731
|
-
import { readFileSync as
|
|
10732
|
-
import { dirname as dirname12, join as
|
|
10788
|
+
import { readFileSync as readFileSync19, writeFileSync as writeFileSync16, existsSync as existsSync22, copyFileSync, mkdirSync as mkdirSync14, mkdtempSync as mkdtempSync2, rmSync as rmSync12 } from "fs";
|
|
10789
|
+
import { dirname as dirname12, join as join25 } from "path";
|
|
10733
10790
|
import { tmpdir as tmpdir3 } from "os";
|
|
10734
10791
|
|
|
10735
10792
|
// src/release.ts
|
|
@@ -11447,13 +11504,13 @@ async function bump(o, size) {
|
|
|
11447
11504
|
}
|
|
11448
11505
|
return printed;
|
|
11449
11506
|
}
|
|
11450
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
11507
|
+
const files = o.repo.release.versionFiles.map((f) => join25(o.cwd, f));
|
|
11451
11508
|
const first = files[0];
|
|
11452
11509
|
if (!existsSync22(first)) {
|
|
11453
11510
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
11454
11511
|
return null;
|
|
11455
11512
|
}
|
|
11456
|
-
const current = JSON.parse(
|
|
11513
|
+
const current = JSON.parse(readFileSync19(first, "utf8")).version;
|
|
11457
11514
|
if (!current) {
|
|
11458
11515
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
11459
11516
|
return null;
|
|
@@ -11466,27 +11523,27 @@ async function bump(o, size) {
|
|
|
11466
11523
|
return version;
|
|
11467
11524
|
}
|
|
11468
11525
|
for (const f of files) {
|
|
11469
|
-
const src =
|
|
11526
|
+
const src = readFileSync19(f, "utf8");
|
|
11470
11527
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
11471
11528
|
if (out === src) {
|
|
11472
11529
|
o.emit.error(`no version field in ${f}`);
|
|
11473
11530
|
return null;
|
|
11474
11531
|
}
|
|
11475
|
-
|
|
11532
|
+
writeFileSync16(f, out);
|
|
11476
11533
|
}
|
|
11477
11534
|
return version;
|
|
11478
11535
|
}
|
|
11479
11536
|
function writeChangelog(o, version, entries) {
|
|
11480
11537
|
const file = o.repo.release.changelog;
|
|
11481
11538
|
if (!file || entries.length === 0) return;
|
|
11482
|
-
const path =
|
|
11539
|
+
const path = join25(o.cwd, file);
|
|
11483
11540
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
11484
11541
|
if (o.dryRun) {
|
|
11485
11542
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
11486
11543
|
return;
|
|
11487
11544
|
}
|
|
11488
|
-
const existing = existsSync22(path) ?
|
|
11489
|
-
|
|
11545
|
+
const existing = existsSync22(path) ? readFileSync19(path, "utf8") : "# Changelog\n";
|
|
11546
|
+
writeFileSync16(path, insertChangelogSection(existing, section));
|
|
11490
11547
|
}
|
|
11491
11548
|
async function confirm(o, expected) {
|
|
11492
11549
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -11579,8 +11636,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
11579
11636
|
function openReleaseCheckout(o) {
|
|
11580
11637
|
const { remote, base } = o.repo.branch;
|
|
11581
11638
|
const branch = `crew/release-${Date.now()}`;
|
|
11582
|
-
const root = mkdtempSync2(
|
|
11583
|
-
const dir =
|
|
11639
|
+
const root = mkdtempSync2(join25(tmpdir3(), "crew-release-"));
|
|
11640
|
+
const dir = join25(root, "checkout");
|
|
11584
11641
|
o.emit.emit(`release checkout: ${dir}`, { step: "release" });
|
|
11585
11642
|
const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
|
|
11586
11643
|
try {
|
|
@@ -11590,10 +11647,10 @@ function openReleaseCheckout(o) {
|
|
|
11590
11647
|
throw e;
|
|
11591
11648
|
}
|
|
11592
11649
|
for (const f of o.repo.worktrees.copy) {
|
|
11593
|
-
const src =
|
|
11650
|
+
const src = join25(o.cwd, f);
|
|
11594
11651
|
if (!existsSync22(src)) continue;
|
|
11595
|
-
|
|
11596
|
-
copyFileSync(src,
|
|
11652
|
+
mkdirSync14(dirname12(join25(dir, f)), { recursive: true });
|
|
11653
|
+
copyFileSync(src, join25(dir, f));
|
|
11597
11654
|
}
|
|
11598
11655
|
return { dir, branch, close };
|
|
11599
11656
|
}
|
|
@@ -12664,19 +12721,19 @@ function nextRoles(decision, ran) {
|
|
|
12664
12721
|
}
|
|
12665
12722
|
|
|
12666
12723
|
// src/ship-name.ts
|
|
12667
|
-
import { existsSync as existsSync25, mkdirSync as
|
|
12724
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync16, readFileSync as readFileSync21, renameSync as renameSync2, writeFileSync as writeFileSync18 } from "fs";
|
|
12668
12725
|
import { dirname as dirname14 } from "path";
|
|
12669
12726
|
import { parseDocument } from "yaml";
|
|
12670
12727
|
|
|
12671
12728
|
// src/machine-id.ts
|
|
12672
12729
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
12673
|
-
import { createHash as
|
|
12674
|
-
import { existsSync as existsSync24, mkdirSync as
|
|
12675
|
-
import { dirname as dirname13, join as
|
|
12730
|
+
import { createHash as createHash4 } from "crypto";
|
|
12731
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync15, readFileSync as readFileSync20, writeFileSync as writeFileSync17 } from "fs";
|
|
12732
|
+
import { dirname as dirname13, join as join26 } from "path";
|
|
12676
12733
|
function machineIdPathFor(stateDir) {
|
|
12677
|
-
return
|
|
12734
|
+
return join26(stateDir, "machine-id");
|
|
12678
12735
|
}
|
|
12679
|
-
var sha256 = (s) =>
|
|
12736
|
+
var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
|
|
12680
12737
|
function readRawMachineId(platform2 = process.platform) {
|
|
12681
12738
|
try {
|
|
12682
12739
|
if (platform2 === "darwin") {
|
|
@@ -12688,7 +12745,7 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
12688
12745
|
}
|
|
12689
12746
|
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
12690
12747
|
if (existsSync24(p)) {
|
|
12691
|
-
const id =
|
|
12748
|
+
const id = readFileSync20(p, "utf8").trim();
|
|
12692
12749
|
if (id) return id;
|
|
12693
12750
|
}
|
|
12694
12751
|
}
|
|
@@ -12700,14 +12757,14 @@ function ensureMachineId(stateDir, opts = {}) {
|
|
|
12700
12757
|
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
12701
12758
|
const path = machineIdPathFor(stateDir);
|
|
12702
12759
|
if (existsSync24(path)) {
|
|
12703
|
-
const cached =
|
|
12760
|
+
const cached = readFileSync20(path, "utf8").trim();
|
|
12704
12761
|
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
12705
12762
|
}
|
|
12706
12763
|
const raw = (opts.read ?? readRawMachineId)();
|
|
12707
12764
|
if (!raw) return void 0;
|
|
12708
12765
|
const hashed = sha256(raw);
|
|
12709
|
-
|
|
12710
|
-
|
|
12766
|
+
mkdirSync15(dirname13(path), { recursive: true });
|
|
12767
|
+
writeFileSync17(path, `${hashed}
|
|
12711
12768
|
`, { mode: 384 });
|
|
12712
12769
|
return hashed;
|
|
12713
12770
|
}
|
|
@@ -12780,7 +12837,7 @@ async function syncRouteName(i) {
|
|
|
12780
12837
|
function readSyncedName(resolvedFile) {
|
|
12781
12838
|
if (!existsSync25(resolvedFile)) return void 0;
|
|
12782
12839
|
try {
|
|
12783
|
-
const v = JSON.parse(
|
|
12840
|
+
const v = JSON.parse(readFileSync21(resolvedFile, "utf8")).shipName;
|
|
12784
12841
|
return typeof v === "string" ? v : void 0;
|
|
12785
12842
|
} catch {
|
|
12786
12843
|
return void 0;
|
|
@@ -12790,22 +12847,22 @@ function writeSyncedName(resolvedFile, name) {
|
|
|
12790
12847
|
if (!existsSync25(resolvedFile)) return;
|
|
12791
12848
|
let raw;
|
|
12792
12849
|
try {
|
|
12793
|
-
raw = JSON.parse(
|
|
12850
|
+
raw = JSON.parse(readFileSync21(resolvedFile, "utf8"));
|
|
12794
12851
|
} catch {
|
|
12795
12852
|
return;
|
|
12796
12853
|
}
|
|
12797
12854
|
if (raw.shipName === name) return;
|
|
12798
12855
|
raw.shipName = name;
|
|
12799
|
-
|
|
12856
|
+
mkdirSync16(dirname14(resolvedFile), { recursive: true });
|
|
12800
12857
|
const tmp = `${resolvedFile}.tmp${process.pid}`;
|
|
12801
|
-
|
|
12858
|
+
writeFileSync18(tmp, `${JSON.stringify(raw, null, 2)}
|
|
12802
12859
|
`);
|
|
12803
12860
|
renameSync2(tmp, resolvedFile);
|
|
12804
12861
|
}
|
|
12805
12862
|
async function reconcileShipName(i) {
|
|
12806
12863
|
let local = i.ship.name;
|
|
12807
12864
|
try {
|
|
12808
|
-
local = shipNameOfYaml(
|
|
12865
|
+
local = shipNameOfYaml(readFileSync21(i.configFile, "utf8")) ?? local;
|
|
12809
12866
|
} catch {
|
|
12810
12867
|
}
|
|
12811
12868
|
const resolvedFile = resolvedPathFor(i.stateDir, i.route);
|
|
@@ -12829,8 +12886,8 @@ async function reconcileShipName(i) {
|
|
|
12829
12886
|
}
|
|
12830
12887
|
if (result.status !== "ok") return result;
|
|
12831
12888
|
if (result.action === "adopt" || result.action === "row-wins") {
|
|
12832
|
-
const text =
|
|
12833
|
-
|
|
12889
|
+
const text = readFileSync21(i.configFile, "utf8");
|
|
12890
|
+
writeFileSync18(i.configFile, setShipNameInYaml(text, result.name));
|
|
12834
12891
|
i.ship.name = result.name;
|
|
12835
12892
|
if (result.action === "row-wins") {
|
|
12836
12893
|
i.emit?.warn(
|
|
@@ -13738,7 +13795,7 @@ function describeCapability(i) {
|
|
|
13738
13795
|
}
|
|
13739
13796
|
|
|
13740
13797
|
// src/agents-sync-run.ts
|
|
13741
|
-
import { existsSync as existsSync26, mkdirSync as
|
|
13798
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as readFileSync22, writeFileSync as writeFileSync19 } from "fs";
|
|
13742
13799
|
import { dirname as dirname15 } from "path";
|
|
13743
13800
|
async function runAgentsSync(target, o) {
|
|
13744
13801
|
const dryRun = o.dryRun ?? false;
|
|
@@ -13772,12 +13829,12 @@ async function runAgentsSync(target, o) {
|
|
|
13772
13829
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
13773
13830
|
if (!dryRun) {
|
|
13774
13831
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
13775
|
-
const raw = existsSync26(resolvedPath) ? JSON.parse(
|
|
13832
|
+
const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync22(resolvedPath, "utf8")) : {};
|
|
13776
13833
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
13777
13834
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
13778
13835
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
13779
|
-
|
|
13780
|
-
|
|
13836
|
+
mkdirSync17(dirname15(resolvedPath), { recursive: true });
|
|
13837
|
+
writeFileSync19(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
13781
13838
|
`);
|
|
13782
13839
|
if (target.resolved) {
|
|
13783
13840
|
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
@@ -13792,7 +13849,7 @@ async function runAgentsSync(target, o) {
|
|
|
13792
13849
|
}
|
|
13793
13850
|
|
|
13794
13851
|
// src/logbook.ts
|
|
13795
|
-
import { createHash as
|
|
13852
|
+
import { createHash as createHash5 } from "crypto";
|
|
13796
13853
|
import { TablationClient as TablationClient4 } from "@tablation/client";
|
|
13797
13854
|
var LogbookError = class extends Error {
|
|
13798
13855
|
};
|
|
@@ -13865,7 +13922,7 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
13865
13922
|
};
|
|
13866
13923
|
}
|
|
13867
13924
|
const prompt = appendAdditions(match.prompt ?? "", match.prompt_additions);
|
|
13868
|
-
const actualSha =
|
|
13925
|
+
const actualSha = createHash5("sha256").update(prompt).digest("hex");
|
|
13869
13926
|
const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
|
|
13870
13927
|
return {
|
|
13871
13928
|
entry,
|
|
@@ -13884,7 +13941,7 @@ import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml"
|
|
|
13884
13941
|
|
|
13885
13942
|
// src/repos-cmd.ts
|
|
13886
13943
|
import { homedir as homedir8 } from "os";
|
|
13887
|
-
import { basename as basename8, join as
|
|
13944
|
+
import { basename as basename8, join as join27, resolve as resolve3 } from "path";
|
|
13888
13945
|
import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
|
|
13889
13946
|
var ReposError = class extends Error {
|
|
13890
13947
|
};
|
|
@@ -14162,7 +14219,7 @@ function planRepoClone(i) {
|
|
|
14162
14219
|
}
|
|
14163
14220
|
const ship = doc.get("ship", true);
|
|
14164
14221
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
14165
|
-
const target = i.dir ? expandDir(i.dir, i.base) :
|
|
14222
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join27(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
14166
14223
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
|
|
14167
14224
|
return { ...add, remote, target };
|
|
14168
14225
|
}
|
|
@@ -14307,7 +14364,7 @@ function repoDirOf(text, base, route, name) {
|
|
|
14307
14364
|
}
|
|
14308
14365
|
return hit.dir;
|
|
14309
14366
|
}
|
|
14310
|
-
function planRouteHooks(text, route, name, hooks) {
|
|
14367
|
+
function planRouteHooks(text, route, name, hooks, opts = {}) {
|
|
14311
14368
|
const doc = parseDocument3(text);
|
|
14312
14369
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
14313
14370
|
const m = routeMapOf(doc, route);
|
|
@@ -14332,15 +14389,34 @@ function planRouteHooks(text, route, name, hooks) {
|
|
|
14332
14389
|
}
|
|
14333
14390
|
const written = [];
|
|
14334
14391
|
const kept = [];
|
|
14392
|
+
const replaced = [];
|
|
14335
14393
|
for (const [k, v] of Object.entries(hooks)) {
|
|
14336
14394
|
if (v === void 0) continue;
|
|
14337
|
-
if (hooksMap.has(k)) kept.push(k);
|
|
14395
|
+
if (hooksMap.has(k) && !opts.replace) kept.push(k);
|
|
14338
14396
|
else {
|
|
14397
|
+
if (hooksMap.has(k)) replaced.push(k);
|
|
14339
14398
|
hooksMap.set(k, v);
|
|
14340
14399
|
written.push(k);
|
|
14341
14400
|
}
|
|
14342
14401
|
}
|
|
14343
|
-
return { text: String(doc), written, kept };
|
|
14402
|
+
return { text: String(doc), written, kept, replaced };
|
|
14403
|
+
}
|
|
14404
|
+
function clearRouteHooks(text, route, name) {
|
|
14405
|
+
const doc = parseDocument3(text);
|
|
14406
|
+
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
14407
|
+
const m = routeMapOf(doc, route);
|
|
14408
|
+
const repos = m.get("repos", true);
|
|
14409
|
+
let target = m;
|
|
14410
|
+
if (isMap2(repos)) {
|
|
14411
|
+
const entry = repos.get(name, true);
|
|
14412
|
+
if (!isMap2(entry)) return { text, removed: [] };
|
|
14413
|
+
target = entry;
|
|
14414
|
+
}
|
|
14415
|
+
const current = target.get("hooks", true);
|
|
14416
|
+
if (!isMap2(current)) return { text, removed: [] };
|
|
14417
|
+
const removed = current.items.map((i) => String(i.key.value ?? i.key));
|
|
14418
|
+
target.delete("hooks");
|
|
14419
|
+
return { text: String(doc), removed };
|
|
14344
14420
|
}
|
|
14345
14421
|
function starterRepoConfig(hooks, release) {
|
|
14346
14422
|
const lines = [
|
|
@@ -14542,7 +14618,7 @@ ${run2.err}` });
|
|
|
14542
14618
|
}
|
|
14543
14619
|
|
|
14544
14620
|
// src/sample-ticket.ts
|
|
14545
|
-
import { basename as basename9, dirname as dirname17, join as
|
|
14621
|
+
import { basename as basename9, dirname as dirname17, join as join28 } from "path";
|
|
14546
14622
|
var SAMPLE_REPO = "crew-sample";
|
|
14547
14623
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
14548
14624
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -14565,8 +14641,8 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
14565
14641
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
14566
14642
|
`;
|
|
14567
14643
|
function sampleDir(existing, reposBase, workspace) {
|
|
14568
|
-
if (existing.length > 0) return
|
|
14569
|
-
return
|
|
14644
|
+
if (existing.length > 0) return join28(dirname17(existing[0]), SAMPLE_REPO);
|
|
14645
|
+
return join28(reposBase, workspace, SAMPLE_REPO);
|
|
14570
14646
|
}
|
|
14571
14647
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
14572
14648
|
function sampleDescription() {
|
|
@@ -14647,8 +14723,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
14647
14723
|
}
|
|
14648
14724
|
|
|
14649
14725
|
// src/worktree-sweep.ts
|
|
14650
|
-
import { existsSync as existsSync27, readFileSync as
|
|
14651
|
-
import { basename as basename10, dirname as dirname18, join as
|
|
14726
|
+
import { existsSync as existsSync27, readFileSync as readFileSync23, readdirSync as readdirSync8 } from "fs";
|
|
14727
|
+
import { basename as basename10, dirname as dirname18, join as join29, resolve as resolve4 } from "path";
|
|
14652
14728
|
function planWorktreeSweep(target, tickets, contract) {
|
|
14653
14729
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
14654
14730
|
const verified = contract.statuses.verified;
|
|
@@ -14715,7 +14791,7 @@ function siblingWorktreeDirs(target) {
|
|
|
14715
14791
|
for (const name of names) {
|
|
14716
14792
|
if (!name.startsWith(stem)) continue;
|
|
14717
14793
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
14718
|
-
const path =
|
|
14794
|
+
const path = join29(parent, name);
|
|
14719
14795
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
14720
14796
|
}
|
|
14721
14797
|
return out;
|
|
@@ -14818,7 +14894,7 @@ function isHandBackMerge(path, base) {
|
|
|
14818
14894
|
if (!msgPath) return false;
|
|
14819
14895
|
let first;
|
|
14820
14896
|
try {
|
|
14821
|
-
first =
|
|
14897
|
+
first = readFileSync23(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
14822
14898
|
} catch {
|
|
14823
14899
|
return false;
|
|
14824
14900
|
}
|
|
@@ -14924,13 +15000,13 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
14924
15000
|
|
|
14925
15001
|
// src/stream-sweep.ts
|
|
14926
15002
|
import { existsSync as existsSync28, readdirSync as readdirSync9, rmSync as rmSync13, statSync as statSync8 } from "fs";
|
|
14927
|
-
import { join as
|
|
15003
|
+
import { join as join30 } from "path";
|
|
14928
15004
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
14929
15005
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
14930
|
-
const dir =
|
|
15006
|
+
const dir = join30(stateDir, "streams");
|
|
14931
15007
|
if (!existsSync28(dir)) return [];
|
|
14932
15008
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
14933
|
-
return readdirSync9(dir).map((name) =>
|
|
15009
|
+
return readdirSync9(dir).map((name) => join30(dir, name)).filter((path) => {
|
|
14934
15010
|
try {
|
|
14935
15011
|
return statSync8(path).mtimeMs < cutoff;
|
|
14936
15012
|
} catch {
|
|
@@ -14958,7 +15034,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
14958
15034
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
14959
15035
|
if (!existsSync28(stateDir)) return [];
|
|
14960
15036
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
14961
|
-
return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) =>
|
|
15037
|
+
return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join30(stateDir, name)).filter((path) => {
|
|
14962
15038
|
try {
|
|
14963
15039
|
return statSync8(path).mtimeMs < cutoff;
|
|
14964
15040
|
} catch {
|
|
@@ -14968,24 +15044,24 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
14968
15044
|
}
|
|
14969
15045
|
|
|
14970
15046
|
// src/cli.ts
|
|
14971
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
15047
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync29, existsSync as existsSync34, mkdirSync as mkdirSync22, writeFileSync as writeFileSync23, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync14 } from "fs";
|
|
14972
15048
|
import { createInterface } from "readline/promises";
|
|
14973
15049
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
14974
15050
|
|
|
14975
15051
|
// src/ssh-keys.ts
|
|
14976
15052
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
14977
|
-
import { existsSync as existsSync29, mkdirSync as
|
|
14978
|
-
import { dirname as dirname19, join as
|
|
15053
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync24, chmodSync } from "fs";
|
|
15054
|
+
import { dirname as dirname19, join as join31 } from "path";
|
|
14979
15055
|
var SshKeyError = class extends Error {
|
|
14980
15056
|
};
|
|
14981
15057
|
function shipSshKeyPathFor(stateDir) {
|
|
14982
|
-
return
|
|
15058
|
+
return join31(stateDir, "ssh", "id_ed25519");
|
|
14983
15059
|
}
|
|
14984
15060
|
function ensureShipSshKeypair(stateDir) {
|
|
14985
15061
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
14986
15062
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
14987
15063
|
if (!existsSync29(privateKeyPath)) {
|
|
14988
|
-
|
|
15064
|
+
mkdirSync18(dirname19(privateKeyPath), { recursive: true });
|
|
14989
15065
|
try {
|
|
14990
15066
|
execFileSync6("ssh-keygen", [
|
|
14991
15067
|
"-t",
|
|
@@ -15008,7 +15084,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
15008
15084
|
if (!existsSync29(publicKeyPath)) {
|
|
15009
15085
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
15010
15086
|
}
|
|
15011
|
-
const publicKey =
|
|
15087
|
+
const publicKey = readFileSync24(publicKeyPath, "utf8").trim();
|
|
15012
15088
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
15013
15089
|
}
|
|
15014
15090
|
function sshKeygenAvailable() {
|
|
@@ -15022,7 +15098,7 @@ function sshKeygenAvailable() {
|
|
|
15022
15098
|
|
|
15023
15099
|
// src/passenger-containers.ts
|
|
15024
15100
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
15025
|
-
import { createHash as
|
|
15101
|
+
import { createHash as createHash6 } from "crypto";
|
|
15026
15102
|
var realExec = (cmd, args) => {
|
|
15027
15103
|
try {
|
|
15028
15104
|
return execFileSync7(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -15059,12 +15135,12 @@ function containerNameFor(workspaceId) {
|
|
|
15059
15135
|
return `crew-passenger-${workspaceId}`;
|
|
15060
15136
|
}
|
|
15061
15137
|
function portFor(workspaceId, base = 28765, span = 1e3) {
|
|
15062
|
-
const digest =
|
|
15138
|
+
const digest = createHash6("sha256").update(workspaceId).digest();
|
|
15063
15139
|
return base + digest.readUInt32BE(0) % span;
|
|
15064
15140
|
}
|
|
15065
15141
|
function mountsDigest(mounts, image) {
|
|
15066
15142
|
const sorted2 = [...mounts].sort((a, b) => a.containerPath.localeCompare(b.containerPath));
|
|
15067
|
-
return
|
|
15143
|
+
return createHash6("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
|
|
15068
15144
|
}
|
|
15069
15145
|
function planContainers(cfg) {
|
|
15070
15146
|
const byWorkspace = /* @__PURE__ */ new Map();
|
|
@@ -15224,9 +15300,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
15224
15300
|
|
|
15225
15301
|
// src/tunnel.ts
|
|
15226
15302
|
import { spawn as spawn5 } from "child_process";
|
|
15227
|
-
import { existsSync as existsSync30, mkdirSync as
|
|
15303
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync25, writeFileSync as writeFileSync20, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
|
|
15228
15304
|
import { randomInt } from "crypto";
|
|
15229
|
-
import { join as
|
|
15305
|
+
import { join as join32 } from "path";
|
|
15230
15306
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
15231
15307
|
var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
15232
15308
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -15293,21 +15369,21 @@ function startTunnel(opts) {
|
|
|
15293
15369
|
};
|
|
15294
15370
|
}
|
|
15295
15371
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
15296
|
-
return
|
|
15372
|
+
return join32(stateDir, "passengers", `${workspaceId}.json`);
|
|
15297
15373
|
}
|
|
15298
15374
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
15299
15375
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
15300
15376
|
if (!existsSync30(path)) return void 0;
|
|
15301
15377
|
try {
|
|
15302
|
-
return JSON.parse(
|
|
15378
|
+
return JSON.parse(readFileSync25(path, "utf8"));
|
|
15303
15379
|
} catch {
|
|
15304
15380
|
return void 0;
|
|
15305
15381
|
}
|
|
15306
15382
|
}
|
|
15307
15383
|
function writePersistedTunnel(stateDir, state) {
|
|
15308
15384
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
15309
|
-
|
|
15310
|
-
|
|
15385
|
+
mkdirSync19(join32(stateDir, "passengers"), { recursive: true });
|
|
15386
|
+
writeFileSync20(path, `${JSON.stringify(state, null, 2)}
|
|
15311
15387
|
`);
|
|
15312
15388
|
}
|
|
15313
15389
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -15404,13 +15480,13 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
15404
15480
|
}
|
|
15405
15481
|
}
|
|
15406
15482
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
15407
|
-
const dir =
|
|
15483
|
+
const dir = join32(stateDir, "passengers");
|
|
15408
15484
|
if (!existsSync30(dir)) return [];
|
|
15409
15485
|
return readdirSync10(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
15410
15486
|
}
|
|
15411
15487
|
|
|
15412
15488
|
// src/mcp-endpoint-registry.ts
|
|
15413
|
-
import { createHash as
|
|
15489
|
+
import { createHash as createHash7 } from "crypto";
|
|
15414
15490
|
function endpointsUrl(api, workspaceId, id) {
|
|
15415
15491
|
const base = `${api.baseUrl.replace(/\/+$/, "")}/api/workspaces/${workspaceId}/mcp-endpoints`;
|
|
15416
15492
|
return id ? `${base}/${id}` : base;
|
|
@@ -15478,7 +15554,7 @@ async function probeEndpointAccess(api, workspaceId) {
|
|
|
15478
15554
|
}
|
|
15479
15555
|
var REGISTER_MCP_ENDPOINTS_HINT = "give this key's role the REGISTER_MCP_ENDPOINTS capability, e.g. via the group that holds the Crew project role";
|
|
15480
15556
|
function registrationKey(url, mcpSecret) {
|
|
15481
|
-
return
|
|
15557
|
+
return createHash7("sha256").update(`${url}
|
|
15482
15558
|
${mcpSecret ?? ""}`).digest("hex").slice(0, 16);
|
|
15483
15559
|
}
|
|
15484
15560
|
async function reconcileEndpoint(workspaceId, d) {
|
|
@@ -15526,8 +15602,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
15526
15602
|
import { createServer as createHttpServer } from "http";
|
|
15527
15603
|
import { spawn as spawn6 } from "child_process";
|
|
15528
15604
|
import { randomBytes } from "crypto";
|
|
15529
|
-
import { existsSync as existsSync31, mkdirSync as
|
|
15530
|
-
import { join as
|
|
15605
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync26, writeFileSync as writeFileSync21, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
|
|
15606
|
+
import { join as join33 } from "path";
|
|
15531
15607
|
|
|
15532
15608
|
// src/secret-compare.ts
|
|
15533
15609
|
import { timingSafeEqual } from "crypto";
|
|
@@ -15616,21 +15692,21 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
15616
15692
|
return child;
|
|
15617
15693
|
};
|
|
15618
15694
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
15619
|
-
return
|
|
15695
|
+
return join33(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
15620
15696
|
}
|
|
15621
15697
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
15622
15698
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
15623
15699
|
if (!existsSync31(path)) return void 0;
|
|
15624
15700
|
try {
|
|
15625
|
-
return JSON.parse(
|
|
15701
|
+
return JSON.parse(readFileSync26(path, "utf8"));
|
|
15626
15702
|
} catch {
|
|
15627
15703
|
return void 0;
|
|
15628
15704
|
}
|
|
15629
15705
|
}
|
|
15630
15706
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
15631
15707
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
15632
|
-
|
|
15633
|
-
|
|
15708
|
+
mkdirSync20(join33(stateDir, "passengers"), { recursive: true });
|
|
15709
|
+
writeFileSync21(path, `${JSON.stringify(state, null, 2)}
|
|
15634
15710
|
`);
|
|
15635
15711
|
}
|
|
15636
15712
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -15650,7 +15726,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
15650
15726
|
}
|
|
15651
15727
|
}
|
|
15652
15728
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
15653
|
-
const dir =
|
|
15729
|
+
const dir = join33(stateDir, "passengers");
|
|
15654
15730
|
if (!existsSync31(dir)) return [];
|
|
15655
15731
|
const suffix = "-sync.json";
|
|
15656
15732
|
return readdirSync11(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
@@ -15739,12 +15815,12 @@ function describeCause(err) {
|
|
|
15739
15815
|
}
|
|
15740
15816
|
|
|
15741
15817
|
// src/daemon.ts
|
|
15742
|
-
import { existsSync as existsSync33, mkdirSync as
|
|
15743
|
-
import { join as
|
|
15818
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as readFileSync28, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync22 } from "fs";
|
|
15819
|
+
import { join as join35 } from "path";
|
|
15744
15820
|
|
|
15745
15821
|
// src/interrupted.ts
|
|
15746
|
-
import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as
|
|
15747
|
-
import { join as
|
|
15822
|
+
import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as readFileSync27 } from "fs";
|
|
15823
|
+
import { join as join34 } from "path";
|
|
15748
15824
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
15749
15825
|
function interruptedComment(b) {
|
|
15750
15826
|
const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
|
|
@@ -15765,19 +15841,19 @@ function runFinished(streamPath) {
|
|
|
15765
15841
|
const path = `${streamPath}.meta.json`;
|
|
15766
15842
|
if (!existsSync32(path)) return false;
|
|
15767
15843
|
try {
|
|
15768
|
-
return JSON.parse(
|
|
15844
|
+
return JSON.parse(readFileSync27(path, "utf8")).running !== true;
|
|
15769
15845
|
} catch {
|
|
15770
15846
|
return true;
|
|
15771
15847
|
}
|
|
15772
15848
|
}
|
|
15773
15849
|
function orphanedStreams(o) {
|
|
15774
|
-
const dir =
|
|
15850
|
+
const dir = join34(o.stateDir, "streams");
|
|
15775
15851
|
if (!existsSync32(dir)) return [];
|
|
15776
15852
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
15777
15853
|
const out = [];
|
|
15778
15854
|
for (const name of readdirSync12(dir)) {
|
|
15779
15855
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
15780
|
-
const streamPath =
|
|
15856
|
+
const streamPath = join34(dir, name);
|
|
15781
15857
|
if (runFinished(streamPath)) continue;
|
|
15782
15858
|
const sidecar = readRunSidecar(streamPath);
|
|
15783
15859
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -15897,7 +15973,7 @@ function latestMtime(path) {
|
|
|
15897
15973
|
if (!stat.isDirectory()) return 0;
|
|
15898
15974
|
let latest = stat.mtimeMs;
|
|
15899
15975
|
for (const entry of readdirSync13(path, { withFileTypes: true })) {
|
|
15900
|
-
latest = Math.max(latest, latestMtime(
|
|
15976
|
+
latest = Math.max(latest, latestMtime(join35(path, entry.name)));
|
|
15901
15977
|
}
|
|
15902
15978
|
return latest;
|
|
15903
15979
|
}
|
|
@@ -15909,20 +15985,20 @@ function makeStaleChecker(paths, opts = {}) {
|
|
|
15909
15985
|
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync33(opts.reloadSentinel);
|
|
15910
15986
|
}
|
|
15911
15987
|
function reloadSentinelFor(stateDir) {
|
|
15912
|
-
return
|
|
15988
|
+
return join35(stateDir, "reload-requested");
|
|
15913
15989
|
}
|
|
15914
15990
|
function daemonStampFor(stateDir) {
|
|
15915
|
-
return
|
|
15991
|
+
return join35(stateDir, "daemon.started");
|
|
15916
15992
|
}
|
|
15917
15993
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
15918
|
-
|
|
15994
|
+
mkdirSync21(stateDir, { recursive: true });
|
|
15919
15995
|
rmSync14(reloadSentinelFor(stateDir), { force: true });
|
|
15920
|
-
|
|
15996
|
+
writeFileSync22(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
15921
15997
|
`);
|
|
15922
15998
|
}
|
|
15923
15999
|
function readDaemonStarted(stateDir) {
|
|
15924
16000
|
try {
|
|
15925
|
-
const v = JSON.parse(
|
|
16001
|
+
const v = JSON.parse(readFileSync28(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
15926
16002
|
return typeof v === "number" ? v : null;
|
|
15927
16003
|
} catch {
|
|
15928
16004
|
return null;
|
|
@@ -16943,7 +17019,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16943
17019
|
}
|
|
16944
17020
|
if (host === "linux" && hasSystemd) {
|
|
16945
17021
|
const label = labelFor(CREW_HOME, "run");
|
|
16946
|
-
const staleTimer =
|
|
17022
|
+
const staleTimer = join36(homedir9(), ".config", "systemd", "user", `${label}.timer`);
|
|
16947
17023
|
if (existsSync34(staleTimer)) {
|
|
16948
17024
|
process.stdout.write(
|
|
16949
17025
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
@@ -17005,7 +17081,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
17005
17081
|
for (const a of plan.actions) {
|
|
17006
17082
|
if (a.kind === "enable") {
|
|
17007
17083
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
17008
|
-
|
|
17084
|
+
writeFileSync23(cfg.configFile, planEnable(readFileSync29(cfg.configFile, "utf8"), a.route));
|
|
17009
17085
|
enabledSound.add(a.route);
|
|
17010
17086
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
17011
17087
|
} else {
|
|
@@ -18014,7 +18090,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18014
18090
|
process.exit(1);
|
|
18015
18091
|
}
|
|
18016
18092
|
requireArmed("run the daemon loop");
|
|
18017
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
18093
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join36(CREW_HOME, "src"), join36(CREW_HOME, "dist"), join36(CREW_HOME, "bin")];
|
|
18018
18094
|
const configPaths = daemonConfigPaths(cfg);
|
|
18019
18095
|
watchPaths.push(...configPaths);
|
|
18020
18096
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -18233,10 +18309,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18233
18309
|
`);
|
|
18234
18310
|
if (!dryRun) {
|
|
18235
18311
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
18236
|
-
const raw = existsSync34(resolvedPath) ? JSON.parse(
|
|
18312
|
+
const raw = existsSync34(resolvedPath) ? JSON.parse(readFileSync29(resolvedPath, "utf8")) : {};
|
|
18237
18313
|
raw.agentSkills = result.agentSkills;
|
|
18238
|
-
|
|
18239
|
-
|
|
18314
|
+
mkdirSync22(dirOf(resolvedPath), { recursive: true });
|
|
18315
|
+
writeFileSync23(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
18240
18316
|
`);
|
|
18241
18317
|
}
|
|
18242
18318
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -18259,7 +18335,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18259
18335
|
throw e;
|
|
18260
18336
|
}
|
|
18261
18337
|
const oldName = cfg.ship.name;
|
|
18262
|
-
|
|
18338
|
+
writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync29(cfg.configFile, "utf8"), newName));
|
|
18263
18339
|
cfg.ship.name = newName;
|
|
18264
18340
|
let renameMachineId;
|
|
18265
18341
|
try {
|
|
@@ -18302,11 +18378,11 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18302
18378
|
case "repos": {
|
|
18303
18379
|
const sub = positional[1];
|
|
18304
18380
|
const usage2 = () => {
|
|
18305
|
-
process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project> [--json]\ncrew repos hooks [workspace/project] <name> [--suggest] [--agent] [--write route|repo] [--recipes ID,ID] [--hook KEY=CMD ...] [--json]\n");
|
|
18381
|
+
process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project> [--json]\ncrew repos hooks [workspace/project] <name> [--suggest] [--agent] [--write route|repo [--replace]] [--clear route] [--recipes ID,ID] [--hook KEY=CMD ...] [--json]\n");
|
|
18306
18382
|
process.exit(2);
|
|
18307
18383
|
};
|
|
18308
18384
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
|
|
18309
|
-
const configText =
|
|
18385
|
+
const configText = readFileSync29(cfg.configFile, "utf8");
|
|
18310
18386
|
if (sub === "hooks") {
|
|
18311
18387
|
try {
|
|
18312
18388
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
@@ -18320,7 +18396,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18320
18396
|
}
|
|
18321
18397
|
}, read: (r) => {
|
|
18322
18398
|
try {
|
|
18323
|
-
return
|
|
18399
|
+
return readFileSync29(resolvePath(dir, r), "utf8");
|
|
18324
18400
|
} catch {
|
|
18325
18401
|
return void 0;
|
|
18326
18402
|
}
|
|
@@ -18328,6 +18404,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18328
18404
|
const suggestion = suggestHooks(files);
|
|
18329
18405
|
const write = value("write");
|
|
18330
18406
|
if (write !== void 0 && write !== "route" && write !== "repo") throw new ReposError(`--write takes "route" or "repo" (got "${write}")`);
|
|
18407
|
+
const clear2 = value("clear");
|
|
18408
|
+
if (clear2 !== void 0 && clear2 !== "route") throw new ReposError(`--clear takes "route" (got "${clear2}")`);
|
|
18409
|
+
if (clear2 && (write || flag("suggest") || flag("agent"))) throw new ReposError("--clear cannot be combined with --write, --suggest or --agent");
|
|
18410
|
+
if (flag("replace") && write !== "route") throw new ReposError("--replace only applies with --write route");
|
|
18331
18411
|
const recipeIds = (value("recipes") ?? "").split(",").map((x) => x.trim()).filter((x) => x && x !== "none");
|
|
18332
18412
|
if (recipeIds.length > 0 && !write) throw new ReposError("--recipes only applies with --write route|repo");
|
|
18333
18413
|
if (flag("agent") && write) throw new ReposError("--agent is read-only; drop --write (write the sniffed hooks, then re-run --agent to compare)");
|
|
@@ -18355,20 +18435,26 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18355
18435
|
const base2 = selectRecipes(suggestion, recipeIds);
|
|
18356
18436
|
const picked = { ...base2, hooks: { ...base2.hooks, ...overrides } };
|
|
18357
18437
|
let written;
|
|
18438
|
+
let cleared;
|
|
18439
|
+
if (clear2) {
|
|
18440
|
+
const plan = clearRouteHooks(configText, a.route, a.name);
|
|
18441
|
+
if (plan.removed.length > 0) writeFileSync23(cfg.configFile, plan.text);
|
|
18442
|
+
cleared = { file: cfg.configFile, removed: plan.removed };
|
|
18443
|
+
}
|
|
18358
18444
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
18359
18445
|
if (write === "route") {
|
|
18360
|
-
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks);
|
|
18361
|
-
|
|
18362
|
-
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
|
|
18446
|
+
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
|
|
18447
|
+
writeFileSync23(cfg.configFile, plan.text);
|
|
18448
|
+
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
|
|
18363
18449
|
} else if (write === "repo") {
|
|
18364
18450
|
const have = existingRepoConfig(files);
|
|
18365
18451
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
18366
18452
|
const file = resolvePath(dir, ".crew.yaml");
|
|
18367
|
-
|
|
18368
|
-
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [] };
|
|
18453
|
+
writeFileSync23(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
18454
|
+
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
|
|
18369
18455
|
}
|
|
18370
18456
|
if (flag("json")) {
|
|
18371
|
-
process.stdout.write(`${JSON.stringify({ route: a.route, repo: a.name, dir, ...suggestion, agent, agentError, written: written ?? null })}
|
|
18457
|
+
process.stdout.write(`${JSON.stringify({ route: a.route, repo: a.name, dir, ...suggestion, agent, agentError, written: written ?? null, cleared: cleared ?? null })}
|
|
18372
18458
|
`);
|
|
18373
18459
|
} else {
|
|
18374
18460
|
if (suggestion.source) process.stdout.write(`${a.name}: proposed from ${suggestion.source}
|
|
@@ -18389,8 +18475,12 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18389
18475
|
}
|
|
18390
18476
|
if (agentError) process.stderr.write(`crew repos: --agent failed (${agentError.reason}): ${agentError.message}
|
|
18391
18477
|
`);
|
|
18478
|
+
if (cleared) process.stdout.write(cleared.removed.length ? `removed route hooks ${cleared.removed.join(", ")} from ${cleared.file}
|
|
18479
|
+
` : "no route hooks to remove\n");
|
|
18392
18480
|
if (written) {
|
|
18393
18481
|
process.stdout.write(`wrote ${written.hooks.join(", ") || "nothing new"} to ${written.file}
|
|
18482
|
+
`);
|
|
18483
|
+
if (written.replaced.length) process.stdout.write(`replaced route hooks: ${written.replaced.join(", ")}
|
|
18394
18484
|
`);
|
|
18395
18485
|
if (written.kept.length) process.stdout.write(`kept existing route hooks: ${written.kept.join(", ")}
|
|
18396
18486
|
`);
|
|
@@ -18447,7 +18537,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18447
18537
|
let resolvedRemotes;
|
|
18448
18538
|
if (existsSync34(resolvedFile)) {
|
|
18449
18539
|
try {
|
|
18450
|
-
resolvedRemotes = JSON.parse(
|
|
18540
|
+
resolvedRemotes = JSON.parse(readFileSync29(resolvedFile, "utf8"));
|
|
18451
18541
|
} catch {
|
|
18452
18542
|
}
|
|
18453
18543
|
}
|
|
@@ -18468,7 +18558,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18468
18558
|
isCheckout,
|
|
18469
18559
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
18470
18560
|
try {
|
|
18471
|
-
return Object.keys(parseRepoConfig(
|
|
18561
|
+
return Object.keys(parseRepoConfig(readFileSync29(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
18472
18562
|
} catch {
|
|
18473
18563
|
return false;
|
|
18474
18564
|
}
|
|
@@ -18517,7 +18607,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18517
18607
|
throw new ReposError(`${e.message}${authHint}
|
|
18518
18608
|
crew.yaml was not changed`);
|
|
18519
18609
|
}
|
|
18520
|
-
|
|
18610
|
+
writeFileSync23(cfg.configFile, plan2.text);
|
|
18521
18611
|
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:
|
|
18522
18612
|
`);
|
|
18523
18613
|
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}`);
|
|
@@ -18577,14 +18667,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18577
18667
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
18578
18668
|
`);
|
|
18579
18669
|
if (existsSync34(resolvedFile)) {
|
|
18580
|
-
const raw = JSON.parse(
|
|
18670
|
+
const raw = JSON.parse(readFileSync29(resolvedFile, "utf8"));
|
|
18581
18671
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
18582
18672
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
18583
|
-
|
|
18673
|
+
writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
18584
18674
|
`);
|
|
18585
18675
|
}
|
|
18586
18676
|
}
|
|
18587
|
-
|
|
18677
|
+
writeFileSync23(cfg.configFile, plan.text);
|
|
18588
18678
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
18589
18679
|
`);
|
|
18590
18680
|
} catch (e) {
|
|
@@ -18935,7 +19025,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18935
19025
|
process.exit(2);
|
|
18936
19026
|
}
|
|
18937
19027
|
if (found.shipNameAdopted && !dryRun) {
|
|
18938
|
-
|
|
19028
|
+
writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync29(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
18939
19029
|
cfg.ship.name = found.shipNameAdopted;
|
|
18940
19030
|
}
|
|
18941
19031
|
if (found.shipRow) {
|
|
@@ -18981,7 +19071,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18981
19071
|
const hadPreviousFile = existsSync34(resolvedPath);
|
|
18982
19072
|
if (hadPreviousFile) {
|
|
18983
19073
|
try {
|
|
18984
|
-
previous = JSON.parse(
|
|
19074
|
+
previous = JSON.parse(readFileSync29(resolvedPath, "utf8"));
|
|
18985
19075
|
} catch {
|
|
18986
19076
|
}
|
|
18987
19077
|
}
|
|
@@ -19101,10 +19191,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19101
19191
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
19102
19192
|
`);
|
|
19103
19193
|
} else {
|
|
19104
|
-
|
|
19194
|
+
mkdirSync22(dirname20(resolvedPath), { recursive: true });
|
|
19105
19195
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
19106
19196
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
19107
|
-
|
|
19197
|
+
writeFileSync23(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
19108
19198
|
`);
|
|
19109
19199
|
renameSync3(tmpPath, resolvedPath);
|
|
19110
19200
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -19146,8 +19236,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19146
19236
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
19147
19237
|
`);
|
|
19148
19238
|
} else {
|
|
19149
|
-
|
|
19150
|
-
|
|
19239
|
+
mkdirSync22(dirname20(apiKeyFile), { recursive: true });
|
|
19240
|
+
writeFileSync23(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
19151
19241
|
`, { mode: 384 });
|
|
19152
19242
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
19153
19243
|
`);
|
|
@@ -19183,8 +19273,11 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19183
19273
|
const id = `${tc.harness}\0${tc.bin}`;
|
|
19184
19274
|
if (seen.has(id)) continue;
|
|
19185
19275
|
seen.add(id);
|
|
19186
|
-
const
|
|
19187
|
-
|
|
19276
|
+
const adapter = adapterFor(tc.harness);
|
|
19277
|
+
const mcpUrl = mcpUrlFor(baseUrl);
|
|
19278
|
+
const mcpOpts = { bin: tc.bin, url: mcpUrl, key: apiKey, replace, dryRun, stateDir: cfg.ship.stateDir };
|
|
19279
|
+
const { line, action } = adapter.ensureMcpResult ? adapter.ensureMcpResult(mcpOpts) : { line: adapter.ensureMcp(mcpOpts), action: void 0 };
|
|
19280
|
+
connectEvents.emit("mcp_registered", line, { harness: tc.harness, bin: tc.bin, url: mcpUrl, ...action ? { action } : {} });
|
|
19188
19281
|
process.stderr.write(`${line}
|
|
19189
19282
|
`);
|
|
19190
19283
|
}
|
|
@@ -19835,7 +19928,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19835
19928
|
};
|
|
19836
19929
|
const droppedRoutes = [];
|
|
19837
19930
|
try {
|
|
19838
|
-
const rawCfg = parseYaml2(
|
|
19931
|
+
const rawCfg = parseYaml2(readFileSync29(cfg.configFile, "utf8"));
|
|
19839
19932
|
for (const r of rawCfg?.routes ?? []) {
|
|
19840
19933
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
19841
19934
|
try {
|
|
@@ -19877,8 +19970,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19877
19970
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
19878
19971
|
break;
|
|
19879
19972
|
}
|
|
19880
|
-
|
|
19881
|
-
|
|
19973
|
+
mkdirSync22(cfg.ship.stateDir, { recursive: true });
|
|
19974
|
+
writeFileSync23(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
19882
19975
|
`);
|
|
19883
19976
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
19884
19977
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -19929,7 +20022,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19929
20022
|
}
|
|
19930
20023
|
case "log": {
|
|
19931
20024
|
try {
|
|
19932
|
-
const text =
|
|
20025
|
+
const text = readFileSync29(cfg.ship.logFile, "utf8").split("\n");
|
|
19933
20026
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
19934
20027
|
`);
|
|
19935
20028
|
} catch {
|
|
@@ -19952,7 +20045,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19952
20045
|
};
|
|
19953
20046
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
19954
20047
|
const tracker = new Tracker(route, cfg.ship);
|
|
19955
|
-
const configText =
|
|
20048
|
+
const configText = readFileSync29(cfg.configFile, "utf8");
|
|
19956
20049
|
const cfgBase = dirname20(cfg.configFile);
|
|
19957
20050
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
19958
20051
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
@@ -19981,7 +20074,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19981
20074
|
}
|
|
19982
20075
|
if (configured.some((r) => r.dir === dir)) {
|
|
19983
20076
|
try {
|
|
19984
|
-
|
|
20077
|
+
writeFileSync23(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
|
|
19985
20078
|
} catch (e) {
|
|
19986
20079
|
if (e instanceof ReposError) fail(e.message);
|
|
19987
20080
|
throw e;
|
|
@@ -19990,28 +20083,28 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19990
20083
|
if (row) {
|
|
19991
20084
|
await tracker.deleteRepoRow(row.id);
|
|
19992
20085
|
if (existsSync34(resolvedFile)) {
|
|
19993
|
-
const raw = JSON.parse(
|
|
20086
|
+
const raw = JSON.parse(readFileSync29(resolvedFile, "utf8"));
|
|
19994
20087
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
19995
20088
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
19996
|
-
|
|
20089
|
+
writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
19997
20090
|
`);
|
|
19998
20091
|
}
|
|
19999
20092
|
}
|
|
20000
|
-
if (existsSync34(
|
|
20093
|
+
if (existsSync34(join36(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
|
|
20001
20094
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
20002
20095
|
break;
|
|
20003
20096
|
}
|
|
20004
20097
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
20005
|
-
const fresh = !existsSync34(
|
|
20098
|
+
const fresh = !existsSync34(join36(dir, ".git"));
|
|
20006
20099
|
if (existsSync34(dir) && fresh && readdirSync14(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
20007
20100
|
if (dryRun) {
|
|
20008
20101
|
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`);
|
|
20009
20102
|
break;
|
|
20010
20103
|
}
|
|
20011
20104
|
if (fresh) {
|
|
20012
|
-
|
|
20013
|
-
|
|
20014
|
-
|
|
20105
|
+
mkdirSync22(dir, { recursive: true });
|
|
20106
|
+
writeFileSync23(join36(dir, "README.md"), SAMPLE_README);
|
|
20107
|
+
writeFileSync23(join36(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
20015
20108
|
const g = (...a) => {
|
|
20016
20109
|
const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
|
|
20017
20110
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -20030,16 +20123,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20030
20123
|
});
|
|
20031
20124
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
20032
20125
|
if (existsSync34(resolvedFile)) {
|
|
20033
|
-
const raw = JSON.parse(
|
|
20126
|
+
const raw = JSON.parse(readFileSync29(resolvedFile, "utf8"));
|
|
20034
20127
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
20035
20128
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
20036
|
-
|
|
20129
|
+
writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
20037
20130
|
`);
|
|
20038
20131
|
}
|
|
20039
20132
|
}
|
|
20040
20133
|
if (!configured.some((r) => r.dir === dir)) {
|
|
20041
20134
|
try {
|
|
20042
|
-
|
|
20135
|
+
writeFileSync23(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
|
|
20043
20136
|
} catch (e) {
|
|
20044
20137
|
if (e instanceof ReposError) fail(e.message);
|
|
20045
20138
|
throw e;
|
|
@@ -20393,7 +20486,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
20393
20486
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
20394
20487
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
20395
20488
|
try {
|
|
20396
|
-
return
|
|
20489
|
+
return readFileSync29(p, "utf8");
|
|
20397
20490
|
} catch {
|
|
20398
20491
|
return null;
|
|
20399
20492
|
}
|