@tablation/crew 0.30.1 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +601 -258
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { resolve as resolve4, dirname as
|
|
4
|
+
import { resolve as resolve4, dirname as dirname12, join as join21, basename as basename9 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
-
import { hostname as hostname2, homedir as
|
|
6
|
+
import { hostname as hostname2, homedir as homedir6 } from "os";
|
|
7
7
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
10
|
-
import { readFileSync as
|
|
11
|
-
import { resolve, dirname as
|
|
12
|
-
import { homedir as
|
|
10
|
+
import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
|
|
11
|
+
import { resolve, dirname as dirname3, isAbsolute, join as join5, basename as basename3 } from "path";
|
|
12
|
+
import { homedir as homedir3, tmpdir } from "os";
|
|
13
13
|
import { parse as parse2 } from "yaml";
|
|
14
14
|
|
|
15
15
|
// src/events.ts
|
|
@@ -157,8 +157,10 @@ var Emitter = class _Emitter {
|
|
|
157
157
|
};
|
|
158
158
|
var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
|
|
159
159
|
|
|
160
|
-
// src/
|
|
161
|
-
import {
|
|
160
|
+
// src/pair-install.ts
|
|
161
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync } from "fs";
|
|
162
|
+
import { homedir as homedir2 } from "os";
|
|
163
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
162
164
|
|
|
163
165
|
// src/stream.ts
|
|
164
166
|
function toolTarget(name, input) {
|
|
@@ -273,6 +275,14 @@ function ensureClaudeMcp(o) {
|
|
|
273
275
|
}
|
|
274
276
|
return `mcp: registered the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url} (user scope, ~/.claude.json)`;
|
|
275
277
|
}
|
|
278
|
+
function removeClaudeMcp(o) {
|
|
279
|
+
const got = runClaude(o.bin, ["mcp", "get", MCP_SERVER_NAME]);
|
|
280
|
+
if (got.missing) return `mcp: \`${o.bin}\` not found, so Claude Code was not touched`;
|
|
281
|
+
if (got.status !== 0) return `mcp: no ${MCP_SERVER_NAME} server registered with Claude Code`;
|
|
282
|
+
if (o.dryRun) return `(dry run) would remove the ${MCP_SERVER_NAME} MCP server from Claude Code (user scope)`;
|
|
283
|
+
const rm = runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
|
|
284
|
+
return rm.status === 0 ? `mcp: removed the ${MCP_SERVER_NAME} MCP server from Claude Code (user scope)` : `mcp: \`claude mcp remove\` failed (${rm.stderr.trim() || `exit ${rm.status}`})`;
|
|
285
|
+
}
|
|
276
286
|
|
|
277
287
|
// src/capability.ts
|
|
278
288
|
function profileFor(role) {
|
|
@@ -312,6 +322,16 @@ function claudeToolsFor(p) {
|
|
|
312
322
|
function allowedTools(role) {
|
|
313
323
|
return claudeToolsFor(profileFor(role));
|
|
314
324
|
}
|
|
325
|
+
var claudePair = {
|
|
326
|
+
harness: "claude",
|
|
327
|
+
displayName: "Claude Code",
|
|
328
|
+
installPair: (o) => ensureClaudeMcp({ bin: o.bin ?? "claude", url: o.url, key: o.key ?? "", home: o.home, replace: o.replace, dryRun: o.dryRun }),
|
|
329
|
+
verifyPair(o) {
|
|
330
|
+
const s = inspectClaudeMcp({ bin: o.bin ?? "claude", url: o.url, key: o.key ?? "", home: o.home });
|
|
331
|
+
return s.state === "registered" ? { ok: true, detail: s.url } : s.state === "different" ? { ok: false, detail: s.detail } : { ok: false, detail: s.state === "no-claude" ? "claude binary not found" : "tablation MCP server not registered" };
|
|
332
|
+
},
|
|
333
|
+
removePair: (o) => removeClaudeMcp({ bin: o.bin ?? "claude", dryRun: o.dryRun })
|
|
334
|
+
};
|
|
315
335
|
var claudeAdapter = {
|
|
316
336
|
harness: "claude",
|
|
317
337
|
displayName: "Claude Code",
|
|
@@ -388,7 +408,291 @@ var claudeAdapter = {
|
|
|
388
408
|
ensureMcp: ensureClaudeMcp
|
|
389
409
|
};
|
|
390
410
|
|
|
411
|
+
// src/pair-install.ts
|
|
412
|
+
var PAIR_BLOCK_START = "<!-- crew:pair start -->";
|
|
413
|
+
var PAIR_BLOCK_END = "<!-- crew:pair end -->";
|
|
414
|
+
var PAIR_KEY_VAR = "CREW_API_KEY";
|
|
415
|
+
var SERVER = "tablation";
|
|
416
|
+
function pairBlock(harness) {
|
|
417
|
+
return [
|
|
418
|
+
PAIR_BLOCK_START,
|
|
419
|
+
"## Tablation Pair (managed by `crew connect` \u2014 edit nothing between the markers)",
|
|
420
|
+
"",
|
|
421
|
+
"At the start of every session, run `crew agents prompt pair --harness " + harness + "` and follow what it prints.",
|
|
422
|
+
`The Tablation tracker tools come from the \`${SERVER}\` MCP server; its key is read from \`$${PAIR_KEY_VAR}\`.`,
|
|
423
|
+
PAIR_BLOCK_END
|
|
424
|
+
].join("\n");
|
|
425
|
+
}
|
|
426
|
+
var BLOCK_RE = new RegExp(`${escapeRe(PAIR_BLOCK_START)}[\\s\\S]*?${escapeRe(PAIR_BLOCK_END)}\\n?`);
|
|
427
|
+
function escapeRe(s) {
|
|
428
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
429
|
+
}
|
|
430
|
+
function hasPairBlock(text) {
|
|
431
|
+
return BLOCK_RE.test(text);
|
|
432
|
+
}
|
|
433
|
+
function pairBlockIsBroken(text) {
|
|
434
|
+
return !hasPairBlock(text) && (text.includes(PAIR_BLOCK_START) || text.includes(PAIR_BLOCK_END));
|
|
435
|
+
}
|
|
436
|
+
function upsertPairBlock(text, block) {
|
|
437
|
+
if (hasPairBlock(text)) return text.replace(BLOCK_RE, () => `${block}
|
|
438
|
+
`);
|
|
439
|
+
if (text.trim() === "") return `${block}
|
|
440
|
+
`;
|
|
441
|
+
return `${text.replace(/\n*$/, "")}
|
|
442
|
+
|
|
443
|
+
${block}
|
|
444
|
+
`;
|
|
445
|
+
}
|
|
446
|
+
function removePairBlock(text) {
|
|
447
|
+
if (!hasPairBlock(text)) return text;
|
|
448
|
+
return text.replace(new RegExp(`\\n*${BLOCK_RE.source}`), "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
|
|
449
|
+
}
|
|
450
|
+
function readIfExists(path) {
|
|
451
|
+
return existsSync2(path) ? readFileSync3(path, "utf8") : void 0;
|
|
452
|
+
}
|
|
453
|
+
function writeFileMkdir(path, text) {
|
|
454
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
455
|
+
writeFileSync(path, text);
|
|
456
|
+
}
|
|
457
|
+
function installBlockFile(path, harness, dryRun) {
|
|
458
|
+
const before = readIfExists(path) ?? "";
|
|
459
|
+
if (pairBlockIsBroken(before)) return { changed: false, error: `${path} has an unmatched ${PAIR_BLOCK_START}/${PAIR_BLOCK_END} marker \u2014 fix it by hand` };
|
|
460
|
+
const after = upsertPairBlock(before, pairBlock(harness));
|
|
461
|
+
if (after === before) return { changed: false };
|
|
462
|
+
if (!dryRun) writeFileMkdir(path, after);
|
|
463
|
+
return { changed: true };
|
|
464
|
+
}
|
|
465
|
+
function removeBlockFile(path, dryRun) {
|
|
466
|
+
const before = readIfExists(path);
|
|
467
|
+
if (before === void 0 || !hasPairBlock(before)) return false;
|
|
468
|
+
if (dryRun) return true;
|
|
469
|
+
const after = removePairBlock(before);
|
|
470
|
+
if (after.trim() === "") rmSync2(path);
|
|
471
|
+
else writeFileSync(path, after);
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
function blockVerdict(path, harness) {
|
|
475
|
+
const text = readIfExists(path);
|
|
476
|
+
if (text === void 0 || !hasPairBlock(text)) return { ok: false, detail: `no managed block in ${path}` };
|
|
477
|
+
if (!text.includes(pairBlock(harness))) return { ok: false, detail: `the managed block in ${path} is out of date \u2014 run crew connect` };
|
|
478
|
+
return { ok: true, detail: "" };
|
|
479
|
+
}
|
|
480
|
+
var HEADER_RE = /^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(#.*)?$/;
|
|
481
|
+
var tableName = (raw) => raw.replace(/["']/g, "").replace(/\s*\.\s*/g, ".");
|
|
482
|
+
function tomlTableRange(lines, name) {
|
|
483
|
+
const start = lines.findIndex((l) => {
|
|
484
|
+
const m = HEADER_RE.exec(l);
|
|
485
|
+
return m !== null && tableName(m[1]) === name;
|
|
486
|
+
});
|
|
487
|
+
if (start < 0) return void 0;
|
|
488
|
+
let end = lines.length;
|
|
489
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
490
|
+
if (HEADER_RE.test(lines[i])) {
|
|
491
|
+
end = i;
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
while (end > start + 1 && lines[end - 1].trim() === "") end--;
|
|
496
|
+
return [start, end];
|
|
497
|
+
}
|
|
498
|
+
var tomlString = (v) => JSON.stringify(v);
|
|
499
|
+
function codexTable(url) {
|
|
500
|
+
return [`[mcp_servers.${SERVER}]`, `url = ${tomlString(url)}`, `bearer_token_env_var = ${tomlString(PAIR_KEY_VAR)}`];
|
|
501
|
+
}
|
|
502
|
+
function upsertTomlTable(text, name, table2) {
|
|
503
|
+
const lines = text.split("\n");
|
|
504
|
+
const r = tomlTableRange(lines, name);
|
|
505
|
+
if (r) {
|
|
506
|
+
lines.splice(r[0], r[1] - r[0], ...table2);
|
|
507
|
+
return lines.join("\n");
|
|
508
|
+
}
|
|
509
|
+
const body = text.replace(/\n*$/, "");
|
|
510
|
+
return body === "" ? `${table2.join("\n")}
|
|
511
|
+
` : `${body}
|
|
512
|
+
|
|
513
|
+
${table2.join("\n")}
|
|
514
|
+
`;
|
|
515
|
+
}
|
|
516
|
+
function removeTomlTable(text, name) {
|
|
517
|
+
const lines = text.split("\n");
|
|
518
|
+
let r = tomlTableRange(lines, name);
|
|
519
|
+
if (!r) return text;
|
|
520
|
+
while (r) {
|
|
521
|
+
let [start, end] = r;
|
|
522
|
+
if (start > 0 && lines[start - 1].trim() === "") start--;
|
|
523
|
+
lines.splice(start, end - start);
|
|
524
|
+
const childName = lines.map((l) => HEADER_RE.exec(l)).map((m) => m ? tableName(m[1]) : void 0).find((n) => n !== void 0 && n.startsWith(`${name}.`));
|
|
525
|
+
r = childName === void 0 ? void 0 : tomlTableRange(lines, childName);
|
|
526
|
+
}
|
|
527
|
+
const out = lines.join("\n").replace(/^\n+/, "");
|
|
528
|
+
return out.trim() === "" ? "" : out.replace(/\n*$/, "\n");
|
|
529
|
+
}
|
|
530
|
+
function tomlValue(lines, range, key) {
|
|
531
|
+
for (let i = range[0] + 1; i < range[1]; i++) {
|
|
532
|
+
const m = new RegExp(`^\\s*${key}\\s*=\\s*"((?:[^"\\\\]|\\\\.)*)"`).exec(lines[i]);
|
|
533
|
+
if (m) return m[1];
|
|
534
|
+
}
|
|
535
|
+
return void 0;
|
|
536
|
+
}
|
|
537
|
+
var codexPaths = (home) => ({
|
|
538
|
+
instructions: join3(home, ".codex", "AGENTS.md"),
|
|
539
|
+
config: join3(home, ".codex", "config.toml")
|
|
540
|
+
});
|
|
541
|
+
function codexMcpState(config, url) {
|
|
542
|
+
const lines = config.split("\n");
|
|
543
|
+
const r = tomlTableRange(lines, `mcp_servers.${SERVER}`);
|
|
544
|
+
if (!r) return { state: "missing" };
|
|
545
|
+
const got = tomlValue(lines, r, "url");
|
|
546
|
+
const env = tomlValue(lines, r, "bearer_token_env_var");
|
|
547
|
+
if (got !== url) return { state: "different", detail: `it points at ${got ?? "(no url)"}, not ${url}` };
|
|
548
|
+
if (env !== PAIR_KEY_VAR) return { state: "different", detail: `it does not read its key from $${PAIR_KEY_VAR}` };
|
|
549
|
+
return { state: "registered" };
|
|
550
|
+
}
|
|
551
|
+
var codexPair = {
|
|
552
|
+
harness: "codex",
|
|
553
|
+
displayName: "Codex",
|
|
554
|
+
installPair(o) {
|
|
555
|
+
const p = codexPaths(o.home ?? homedir2());
|
|
556
|
+
const config = readIfExists(p.config) ?? "";
|
|
557
|
+
const st = codexMcpState(config, o.url);
|
|
558
|
+
if (st.state === "different" && !o.replace) {
|
|
559
|
+
return `pair(codex): left ${p.config} alone \u2014 a "${SERVER}" server exists and ${st.detail}. Re-run with --mcp replace to overwrite it.`;
|
|
560
|
+
}
|
|
561
|
+
const block = installBlockFile(p.instructions, "codex", o.dryRun);
|
|
562
|
+
if (block.error) return `pair(codex): ${block.error}`;
|
|
563
|
+
let mcpChanged = false;
|
|
564
|
+
if (st.state !== "registered") {
|
|
565
|
+
mcpChanged = true;
|
|
566
|
+
if (!o.dryRun) writeFileMkdir(p.config, upsertTomlTable(config, `mcp_servers.${SERVER}`, codexTable(o.url)));
|
|
567
|
+
}
|
|
568
|
+
if (!block.changed && !mcpChanged) return `pair(codex): already installed (${p.instructions}, ${p.config})`;
|
|
569
|
+
return `pair(codex): ${o.dryRun ? "(dry run) would install" : "installed"} the Pair block in ${p.instructions} and ${SERVER} in ${p.config} (key read from $${PAIR_KEY_VAR})`;
|
|
570
|
+
},
|
|
571
|
+
verifyPair(o) {
|
|
572
|
+
const p = codexPaths(o.home ?? homedir2());
|
|
573
|
+
const b = blockVerdict(p.instructions, "codex");
|
|
574
|
+
if (!b.ok) return b;
|
|
575
|
+
const st = codexMcpState(readIfExists(p.config) ?? "", o.url);
|
|
576
|
+
if (st.state === "missing") return { ok: false, detail: `no ${SERVER} server in ${p.config}` };
|
|
577
|
+
if (st.state === "different") return { ok: false, detail: `${SERVER} in ${p.config} ${st.detail}` };
|
|
578
|
+
return { ok: true, detail: `${p.instructions}, ${p.config}` };
|
|
579
|
+
},
|
|
580
|
+
removePair(o) {
|
|
581
|
+
const p = codexPaths(o.home ?? homedir2());
|
|
582
|
+
const hadBlock = removeBlockFile(p.instructions, o.dryRun);
|
|
583
|
+
const config = readIfExists(p.config);
|
|
584
|
+
const st = codexMcpState(config ?? "", o.url);
|
|
585
|
+
const hadMcp = config !== void 0 && st.state === "registered";
|
|
586
|
+
const kept = st.state === "different" ? `; left the existing ${SERVER} server alone \u2014 ${st.detail}` : "";
|
|
587
|
+
if (hadMcp && !o.dryRun) {
|
|
588
|
+
const after = removeTomlTable(config, `mcp_servers.${SERVER}`);
|
|
589
|
+
if (after === "") rmSync2(p.config);
|
|
590
|
+
else writeFileSync(p.config, after);
|
|
591
|
+
}
|
|
592
|
+
const what = hadBlock && hadMcp ? `the Pair block and ${SERVER} server` : hadBlock ? "the Pair block" : `the ${SERVER} server`;
|
|
593
|
+
return hadBlock || hadMcp ? `pair(codex): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what}${kept}` : `pair(codex): nothing to remove${kept}`;
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
var geminiPaths = (home) => ({
|
|
597
|
+
instructions: join3(home, ".gemini", "GEMINI.md"),
|
|
598
|
+
config: join3(home, ".gemini", "settings.json")
|
|
599
|
+
});
|
|
600
|
+
function readGeminiSettings(path) {
|
|
601
|
+
const text = readIfExists(path);
|
|
602
|
+
if (text === void 0 || text.trim() === "") return {};
|
|
603
|
+
try {
|
|
604
|
+
const j = JSON.parse(text);
|
|
605
|
+
return j && typeof j === "object" && !Array.isArray(j) ? j : void 0;
|
|
606
|
+
} catch {
|
|
607
|
+
return void 0;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
var geminiEntry = (url) => ({ httpUrl: url, headers: { Authorization: `Bearer $${PAIR_KEY_VAR}` } });
|
|
611
|
+
function geminiMcpState(s, url) {
|
|
612
|
+
const e = s.mcpServers?.[SERVER];
|
|
613
|
+
if (!e) return { state: "missing" };
|
|
614
|
+
if (e.httpUrl !== url) return { state: "different", detail: `it points at ${e.httpUrl ?? "(no httpUrl)"}, not ${url}` };
|
|
615
|
+
if (e.headers?.Authorization !== `Bearer $${PAIR_KEY_VAR}`) return { state: "different", detail: `it does not read its key from $${PAIR_KEY_VAR}` };
|
|
616
|
+
return { state: "registered" };
|
|
617
|
+
}
|
|
618
|
+
var geminiPair = {
|
|
619
|
+
harness: "gemini",
|
|
620
|
+
displayName: "Gemini CLI",
|
|
621
|
+
installPair(o) {
|
|
622
|
+
const p = geminiPaths(o.home ?? homedir2());
|
|
623
|
+
const settings = readGeminiSettings(p.config);
|
|
624
|
+
if (!settings) return `pair(gemini): ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
|
|
625
|
+
const st = geminiMcpState(settings, o.url);
|
|
626
|
+
if (st.state === "different" && !o.replace) {
|
|
627
|
+
return `pair(gemini): left ${p.config} alone \u2014 a "${SERVER}" server exists and ${st.detail}. Re-run with --mcp replace to overwrite it.`;
|
|
628
|
+
}
|
|
629
|
+
const block = installBlockFile(p.instructions, "gemini", o.dryRun);
|
|
630
|
+
if (block.error) return `pair(gemini): ${block.error}`;
|
|
631
|
+
const mcpChanged = st.state !== "registered";
|
|
632
|
+
if (mcpChanged && !o.dryRun) {
|
|
633
|
+
const next = { ...settings, mcpServers: { ...settings.mcpServers, [SERVER]: geminiEntry(o.url) } };
|
|
634
|
+
writeFileMkdir(p.config, `${JSON.stringify(next, null, 2)}
|
|
635
|
+
`);
|
|
636
|
+
}
|
|
637
|
+
if (!block.changed && !mcpChanged) return `pair(gemini): already installed (${p.instructions}, ${p.config})`;
|
|
638
|
+
return `pair(gemini): ${o.dryRun ? "(dry run) would install" : "installed"} the Pair block in ${p.instructions} and ${SERVER} in ${p.config} (key read from $${PAIR_KEY_VAR})`;
|
|
639
|
+
},
|
|
640
|
+
verifyPair(o) {
|
|
641
|
+
const p = geminiPaths(o.home ?? homedir2());
|
|
642
|
+
const b = blockVerdict(p.instructions, "gemini");
|
|
643
|
+
if (!b.ok) return b;
|
|
644
|
+
const settings = readGeminiSettings(p.config);
|
|
645
|
+
if (!settings) return { ok: false, detail: `${p.config} is not a JSON object` };
|
|
646
|
+
const st = geminiMcpState(settings, o.url);
|
|
647
|
+
if (st.state === "missing") return { ok: false, detail: `no ${SERVER} server in ${p.config}` };
|
|
648
|
+
if (st.state === "different") return { ok: false, detail: `${SERVER} in ${p.config} ${st.detail}` };
|
|
649
|
+
return { ok: true, detail: `${p.instructions}, ${p.config}` };
|
|
650
|
+
},
|
|
651
|
+
removePair(o) {
|
|
652
|
+
const p = geminiPaths(o.home ?? homedir2());
|
|
653
|
+
const hadBlock = removeBlockFile(p.instructions, o.dryRun);
|
|
654
|
+
const settings = readGeminiSettings(p.config);
|
|
655
|
+
const st = settings ? geminiMcpState(settings, o.url) : { state: "missing" };
|
|
656
|
+
const hadMcp = st.state === "registered";
|
|
657
|
+
const kept = st.state === "different" ? `; left the existing ${SERVER} server alone \u2014 ${st.detail}` : "";
|
|
658
|
+
if (hadMcp && settings && !o.dryRun) {
|
|
659
|
+
const { [SERVER]: _gone, ...rest } = settings.mcpServers ?? {};
|
|
660
|
+
const next = { ...settings };
|
|
661
|
+
if (Object.keys(rest).length > 0) next.mcpServers = rest;
|
|
662
|
+
else delete next.mcpServers;
|
|
663
|
+
if (Object.keys(next).length === 0) rmSync2(p.config);
|
|
664
|
+
else writeFileSync(p.config, `${JSON.stringify(next, null, 2)}
|
|
665
|
+
`);
|
|
666
|
+
}
|
|
667
|
+
const what = hadBlock && hadMcp ? `the Pair block and ${SERVER} server` : hadBlock ? "the Pair block" : `the ${SERVER} server`;
|
|
668
|
+
return hadBlock || hadMcp ? `pair(gemini): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what}${kept}` : `pair(gemini): nothing to remove${kept}`;
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
var PAIR_INSTALLERS = {
|
|
672
|
+
[claudePair.harness]: claudePair,
|
|
673
|
+
[codexPair.harness]: codexPair,
|
|
674
|
+
[geminiPair.harness]: geminiPair
|
|
675
|
+
};
|
|
676
|
+
var PAIR_HARNESS_NAMES = Object.keys(PAIR_INSTALLERS);
|
|
677
|
+
var DEFAULT_PAIR = ["claude"];
|
|
678
|
+
function isPairHarness(name) {
|
|
679
|
+
return Object.hasOwn(PAIR_INSTALLERS, name);
|
|
680
|
+
}
|
|
681
|
+
function pairInstallerFor(harness) {
|
|
682
|
+
const i = Object.hasOwn(PAIR_INSTALLERS, harness) ? PAIR_INSTALLERS[harness] : void 0;
|
|
683
|
+
if (!i) throw new Error(`no Pair installer for "${harness}" (have: ${PAIR_HARNESS_NAMES.join(", ")})`);
|
|
684
|
+
return i;
|
|
685
|
+
}
|
|
686
|
+
function harnessPromptNote(harness) {
|
|
687
|
+
if (!harness || !isPairHarness(harness)) return "";
|
|
688
|
+
return `
|
|
689
|
+
|
|
690
|
+
(This Pair session is running in ${pairInstallerFor(harness).displayName}.)
|
|
691
|
+
`;
|
|
692
|
+
}
|
|
693
|
+
|
|
391
694
|
// src/toolchain.ts
|
|
695
|
+
import { basename } from "path";
|
|
392
696
|
var TIERS = ["light", "standard", "deep"];
|
|
393
697
|
var ADAPTERS = {
|
|
394
698
|
[claudeAdapter.harness]: claudeAdapter
|
|
@@ -512,8 +816,8 @@ function explain(ship, required) {
|
|
|
512
816
|
}
|
|
513
817
|
|
|
514
818
|
// src/repo-config.ts
|
|
515
|
-
import { readFileSync as
|
|
516
|
-
import { join as
|
|
819
|
+
import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
|
|
820
|
+
import { join as join4, basename as basename2 } from "path";
|
|
517
821
|
import { parse } from "yaml";
|
|
518
822
|
var SPEC_VERSION = 1;
|
|
519
823
|
var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
|
|
@@ -586,8 +890,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
|
|
|
586
890
|
var PROVIDERS = ["github", "buildkite", "other", "none"];
|
|
587
891
|
function findRepoConfig(dir) {
|
|
588
892
|
for (const name of REPO_CONFIG_FILENAMES) {
|
|
589
|
-
const p =
|
|
590
|
-
if (
|
|
893
|
+
const p = join4(dir, name);
|
|
894
|
+
if (existsSync3(p)) return p;
|
|
591
895
|
}
|
|
592
896
|
return null;
|
|
593
897
|
}
|
|
@@ -817,7 +1121,7 @@ function parseRepoConfig(text, file) {
|
|
|
817
1121
|
function loadRepoConfig(dir) {
|
|
818
1122
|
const file = findRepoConfig(dir);
|
|
819
1123
|
if (!file) return null;
|
|
820
|
-
return parseRepoConfig(
|
|
1124
|
+
return parseRepoConfig(readFileSync4(file, "utf8"), file);
|
|
821
1125
|
}
|
|
822
1126
|
function hookLabel(cfg, hook2) {
|
|
823
1127
|
return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
|
|
@@ -1084,6 +1388,7 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
|
|
|
1084
1388
|
"agent",
|
|
1085
1389
|
"toolchains",
|
|
1086
1390
|
"toolchain",
|
|
1391
|
+
"pair",
|
|
1087
1392
|
"shell",
|
|
1088
1393
|
"extraPath",
|
|
1089
1394
|
"useNvm",
|
|
@@ -1228,16 +1533,16 @@ function mergeRouteRelease(route, override) {
|
|
|
1228
1533
|
};
|
|
1229
1534
|
}
|
|
1230
1535
|
function expand(p, base) {
|
|
1231
|
-
let out = p.startsWith("~") ? p.replace(/^~/,
|
|
1232
|
-
out = out.replace(/\$\{?HOME\}?/g,
|
|
1536
|
+
let out = p.startsWith("~") ? p.replace(/^~/, homedir3()) : p;
|
|
1537
|
+
out = out.replace(/\$\{?HOME\}?/g, homedir3());
|
|
1233
1538
|
return isAbsolute(out) ? out : resolve(base, out);
|
|
1234
1539
|
}
|
|
1235
1540
|
function resolvePromptsDir(promptSet, crewHome, base) {
|
|
1236
|
-
if (!promptSet) return
|
|
1541
|
+
if (!promptSet) return join5(crewHome, "prompts", "default");
|
|
1237
1542
|
if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
|
|
1238
1543
|
return expand(promptSet, base);
|
|
1239
1544
|
}
|
|
1240
|
-
return
|
|
1545
|
+
return join5(crewHome, "prompts", promptSet);
|
|
1241
1546
|
}
|
|
1242
1547
|
var Missing = class {
|
|
1243
1548
|
paths = [];
|
|
@@ -1261,7 +1566,7 @@ var Missing = class {
|
|
|
1261
1566
|
};
|
|
1262
1567
|
function crewVersion(crewHome) {
|
|
1263
1568
|
try {
|
|
1264
|
-
const pkg = JSON.parse(
|
|
1569
|
+
const pkg = JSON.parse(readFileSync5(join5(crewHome, "package.json"), "utf8"));
|
|
1265
1570
|
return pkg.version ?? "0.0.0";
|
|
1266
1571
|
} catch {
|
|
1267
1572
|
return "0.0.0";
|
|
@@ -1271,26 +1576,26 @@ function configSearchPath(crewHome) {
|
|
|
1271
1576
|
if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
|
|
1272
1577
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
1273
1578
|
return [
|
|
1274
|
-
...xdg ? [
|
|
1275
|
-
|
|
1579
|
+
...xdg ? [join5(xdg, "crew", "crew.yaml")] : [],
|
|
1580
|
+
join5(homedir3(), ".config", "crew", "crew.yaml"),
|
|
1276
1581
|
resolve(crewHome, "crew.yaml")
|
|
1277
1582
|
];
|
|
1278
1583
|
}
|
|
1279
1584
|
function findConfigFile(crewHome) {
|
|
1280
1585
|
const candidates = configSearchPath(crewHome);
|
|
1281
|
-
return candidates.find((c) =>
|
|
1586
|
+
return candidates.find((c) => existsSync4(c)) ?? candidates[candidates.length - 1];
|
|
1282
1587
|
}
|
|
1283
|
-
var defaultConfigPath = () =>
|
|
1588
|
+
var defaultConfigPath = () => join5(process.env.XDG_CONFIG_HOME ?? join5(homedir3(), ".config"), "crew", "crew.yaml");
|
|
1284
1589
|
function routeSlug(route) {
|
|
1285
1590
|
return route.replace(/\//g, "-");
|
|
1286
1591
|
}
|
|
1287
1592
|
function resolvedPathFor(stateDir, route) {
|
|
1288
1593
|
const [workspace, project] = splitRoute(route);
|
|
1289
|
-
return
|
|
1594
|
+
return join5(stateDir, "resolved", workspace, `${project}.json`);
|
|
1290
1595
|
}
|
|
1291
1596
|
function apiKeyPathFor(stateDir, route) {
|
|
1292
1597
|
const [workspace, project] = splitRoute(route);
|
|
1293
|
-
return
|
|
1598
|
+
return join5(stateDir, "keys", workspace, `${project}.env`);
|
|
1294
1599
|
}
|
|
1295
1600
|
function splitRoute(route) {
|
|
1296
1601
|
const parts = route.split("/");
|
|
@@ -1301,9 +1606,9 @@ function splitRoute(route) {
|
|
|
1301
1606
|
}
|
|
1302
1607
|
function readResolvedFile(stateDir, route) {
|
|
1303
1608
|
const path = resolvedPathFor(stateDir, route);
|
|
1304
|
-
if (!
|
|
1609
|
+
if (!existsSync4(path)) return void 0;
|
|
1305
1610
|
try {
|
|
1306
|
-
return JSON.parse(
|
|
1611
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
1307
1612
|
} catch (e) {
|
|
1308
1613
|
throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
|
|
1309
1614
|
}
|
|
@@ -1359,23 +1664,23 @@ function parseResolved(raw, m, where) {
|
|
|
1359
1664
|
}
|
|
1360
1665
|
function loadConfig(crewHome, configFile, opts = {}) {
|
|
1361
1666
|
const file = configFile ?? findConfigFile(crewHome);
|
|
1362
|
-
if (opts.text === void 0 && !
|
|
1667
|
+
if (opts.text === void 0 && !existsSync4(file)) {
|
|
1363
1668
|
throw new ConfigError(
|
|
1364
1669
|
`no config found. Looked in:
|
|
1365
1670
|
` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
|
|
1366
1671
|
|
|
1367
1672
|
Create the first of those:
|
|
1368
|
-
mkdir -p ${
|
|
1673
|
+
mkdir -p ${dirname3(defaultConfigPath())} && cp ${join5(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
|
|
1369
1674
|
);
|
|
1370
1675
|
}
|
|
1371
1676
|
let raw;
|
|
1372
1677
|
try {
|
|
1373
|
-
raw = parse2(opts.text ??
|
|
1678
|
+
raw = parse2(opts.text ?? readFileSync5(file, "utf8"));
|
|
1374
1679
|
} catch (e) {
|
|
1375
1680
|
throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
|
|
1376
1681
|
}
|
|
1377
1682
|
if (!raw || typeof raw !== "object") throw new ConfigError(`${file} is empty or not a mapping`);
|
|
1378
|
-
const base =
|
|
1683
|
+
const base = dirname3(file);
|
|
1379
1684
|
const missing = new Missing();
|
|
1380
1685
|
rejectUnknownKeys(raw, /* @__PURE__ */ new Set(["ship", "routes"]), "top-level", file);
|
|
1381
1686
|
const shipRaw = raw.ship ?? {};
|
|
@@ -1402,6 +1707,7 @@ Create the first of those:
|
|
|
1402
1707
|
}
|
|
1403
1708
|
const toolchains = parseToolchains(shipRaw.toolchains, base, missing, file);
|
|
1404
1709
|
const toolchainNames = /* @__PURE__ */ new Set([LEGACY_TOOLCHAIN, ...toolchains.map((x) => x.name)]);
|
|
1710
|
+
const pair = parsePair(shipRaw.pair, missing);
|
|
1405
1711
|
const shipToolchain = shipRaw.toolchain === void 0 ? void 0 : String(shipRaw.toolchain);
|
|
1406
1712
|
if (shipToolchain !== void 0 && !toolchainNames.has(shipToolchain)) {
|
|
1407
1713
|
missing.add(`ship.toolchain "${shipToolchain}" is not defined (have: ${[...toolchainNames].join(", ")})`);
|
|
@@ -1441,12 +1747,13 @@ Create the first of those:
|
|
|
1441
1747
|
},
|
|
1442
1748
|
...toolchains.length > 0 ? { toolchains } : {},
|
|
1443
1749
|
...shipToolchain !== void 0 ? { toolchain: shipToolchain } : {},
|
|
1750
|
+
...pair ? { pair } : {},
|
|
1444
1751
|
shell: shipRaw.shell,
|
|
1445
1752
|
extraPath: shipRaw.extraPath,
|
|
1446
1753
|
useNvm: shipRaw.useNvm !== false,
|
|
1447
1754
|
nvmSh: shipRaw.nvmSh,
|
|
1448
1755
|
stateDir,
|
|
1449
|
-
logFile: expand(shipRaw.logFile ??
|
|
1756
|
+
logFile: expand(shipRaw.logFile ?? join5(tmpdir(), "crew.log"), base),
|
|
1450
1757
|
userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
|
|
1451
1758
|
maxConcurrentAgents,
|
|
1452
1759
|
streamRetentionDays,
|
|
@@ -1468,6 +1775,22 @@ function expandCommand(cmd, base) {
|
|
|
1468
1775
|
return cmd.includes("/") || cmd.startsWith("~") ? expand(cmd, base) : cmd;
|
|
1469
1776
|
}
|
|
1470
1777
|
var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "model", "tiers", "args", "env", "maxThinkingTokens"]);
|
|
1778
|
+
function parsePair(raw, missing) {
|
|
1779
|
+
if (raw === void 0) return void 0;
|
|
1780
|
+
if (!Array.isArray(raw)) {
|
|
1781
|
+
missing.add("ship.pair must be a list of harness names");
|
|
1782
|
+
return void 0;
|
|
1783
|
+
}
|
|
1784
|
+
const out = [];
|
|
1785
|
+
for (const h of raw.map(String)) {
|
|
1786
|
+
if (!isPairHarness(h)) missing.add(`ship.pair "${h}" is not a harness Pair can be installed for (have: ${PAIR_HARNESS_NAMES.join(", ")})`);
|
|
1787
|
+
else if (!out.includes(h)) out.push(h);
|
|
1788
|
+
}
|
|
1789
|
+
return out;
|
|
1790
|
+
}
|
|
1791
|
+
function shipPair(ship) {
|
|
1792
|
+
return ship.pair ?? DEFAULT_PAIR;
|
|
1793
|
+
}
|
|
1471
1794
|
function parseToolchains(raw, base, missing, file) {
|
|
1472
1795
|
if (raw === void 0 || raw === null) return [];
|
|
1473
1796
|
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -1644,8 +1967,8 @@ function repoIdForName(r, name) {
|
|
|
1644
1967
|
}
|
|
1645
1968
|
function resolveApiKey(r) {
|
|
1646
1969
|
if (r.apiKey) return r.apiKey;
|
|
1647
|
-
if (r.apiKeyFile && r.apiKeyVar &&
|
|
1648
|
-
for (const line of
|
|
1970
|
+
if (r.apiKeyFile && r.apiKeyVar && existsSync4(r.apiKeyFile)) {
|
|
1971
|
+
for (const line of readFileSync5(r.apiKeyFile, "utf8").split("\n")) {
|
|
1649
1972
|
const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
|
|
1650
1973
|
if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
1651
1974
|
}
|
|
@@ -1708,7 +2031,7 @@ function configuredMembers(rt) {
|
|
|
1708
2031
|
return out;
|
|
1709
2032
|
}
|
|
1710
2033
|
function defaultRepoDir(basePath, workspace, repoName) {
|
|
1711
|
-
return
|
|
2034
|
+
return join5(basePath, workspace, repoName);
|
|
1712
2035
|
}
|
|
1713
2036
|
function reposOf(r) {
|
|
1714
2037
|
const named = Object.entries(r.repos);
|
|
@@ -1732,7 +2055,7 @@ function passengerRepoTargets(r) {
|
|
|
1732
2055
|
return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
|
|
1733
2056
|
}
|
|
1734
2057
|
function passengerCheckoutDir(basePath, workspace, repoName) {
|
|
1735
|
-
return
|
|
2058
|
+
return join5(basePath, workspace, ".passenger-checkouts", repoName);
|
|
1736
2059
|
}
|
|
1737
2060
|
function passengerCheckoutTargets(r) {
|
|
1738
2061
|
if (!r.hostPassengers) return [];
|
|
@@ -1864,17 +2187,17 @@ function rosterMarkdown(roster, meId) {
|
|
|
1864
2187
|
}
|
|
1865
2188
|
|
|
1866
2189
|
// src/state.ts
|
|
1867
|
-
import { readFileSync as
|
|
1868
|
-
import { join as
|
|
2190
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, rmSync as rmSync3, statSync as statSync2 } from "fs";
|
|
2191
|
+
import { join as join6 } from "path";
|
|
1869
2192
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
1870
2193
|
var State = class _State {
|
|
1871
2194
|
dir;
|
|
1872
2195
|
constructor(dir) {
|
|
1873
2196
|
this.dir = dir;
|
|
1874
|
-
|
|
2197
|
+
mkdirSync3(dir, { recursive: true });
|
|
1875
2198
|
}
|
|
1876
2199
|
path(...parts) {
|
|
1877
|
-
return
|
|
2200
|
+
return join6(this.dir, ...parts);
|
|
1878
2201
|
}
|
|
1879
2202
|
/**
|
|
1880
2203
|
* The timestamp every "new since last poll" check is measured against.
|
|
@@ -1885,14 +2208,14 @@ var State = class _State {
|
|
|
1885
2208
|
*/
|
|
1886
2209
|
watermark() {
|
|
1887
2210
|
try {
|
|
1888
|
-
const v =
|
|
2211
|
+
const v = readFileSync6(this.path(".poll-watermark"), "utf8").trim();
|
|
1889
2212
|
return v || EPOCH;
|
|
1890
2213
|
} catch {
|
|
1891
2214
|
return EPOCH;
|
|
1892
2215
|
}
|
|
1893
2216
|
}
|
|
1894
2217
|
setWatermark(at) {
|
|
1895
|
-
|
|
2218
|
+
writeFileSync2(this.path(".poll-watermark"), `${at}
|
|
1896
2219
|
`);
|
|
1897
2220
|
}
|
|
1898
2221
|
/**
|
|
@@ -1910,19 +2233,19 @@ var State = class _State {
|
|
|
1910
2233
|
}
|
|
1911
2234
|
/** Whole-crew pause, and the per-role sentinels one level down. */
|
|
1912
2235
|
isPaused() {
|
|
1913
|
-
return
|
|
2236
|
+
return existsSync5(this.path(".crew-paused"));
|
|
1914
2237
|
}
|
|
1915
2238
|
isRolePaused(role) {
|
|
1916
|
-
return
|
|
2239
|
+
return existsSync5(this.path(`.role-paused-${role}`));
|
|
1917
2240
|
}
|
|
1918
2241
|
pausedRoles(roles) {
|
|
1919
2242
|
return new Set(roles.filter((r) => this.isRolePaused(r)));
|
|
1920
2243
|
}
|
|
1921
2244
|
pause(role) {
|
|
1922
|
-
|
|
2245
|
+
writeFileSync2(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
|
|
1923
2246
|
}
|
|
1924
2247
|
resume(role) {
|
|
1925
|
-
|
|
2248
|
+
rmSync3(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
|
|
1926
2249
|
}
|
|
1927
2250
|
/**
|
|
1928
2251
|
* The release phase (merge, deploy, release) has its own sentinel, one
|
|
@@ -1930,11 +2253,11 @@ var State = class _State {
|
|
|
1930
2253
|
* while agents keep working.
|
|
1931
2254
|
*/
|
|
1932
2255
|
isReleasePaused() {
|
|
1933
|
-
return
|
|
2256
|
+
return existsSync5(this.path(".release-paused"));
|
|
1934
2257
|
}
|
|
1935
2258
|
setReleasePaused(paused) {
|
|
1936
|
-
if (paused)
|
|
1937
|
-
else
|
|
2259
|
+
if (paused) writeFileSync2(this.path(".release-paused"), "");
|
|
2260
|
+
else rmSync3(this.path(".release-paused"), { force: true });
|
|
1938
2261
|
}
|
|
1939
2262
|
/**
|
|
1940
2263
|
* Why a release cycle must not run right now, or null when it may. Every
|
|
@@ -1971,7 +2294,7 @@ var State = class _State {
|
|
|
1971
2294
|
const name = _State.safe(rawName);
|
|
1972
2295
|
const file = this.path(`.${name}.lock`);
|
|
1973
2296
|
try {
|
|
1974
|
-
const existing = Number.parseInt(
|
|
2297
|
+
const existing = Number.parseInt(readFileSync6(file, "utf8").trim(), 10);
|
|
1975
2298
|
if (Number.isFinite(existing) && existing > 0) {
|
|
1976
2299
|
try {
|
|
1977
2300
|
process.kill(existing, 0);
|
|
@@ -1981,13 +2304,13 @@ var State = class _State {
|
|
|
1981
2304
|
}
|
|
1982
2305
|
} catch {
|
|
1983
2306
|
}
|
|
1984
|
-
|
|
2307
|
+
writeFileSync2(file, `${process.pid}
|
|
1985
2308
|
`);
|
|
1986
2309
|
return {
|
|
1987
2310
|
ok: true,
|
|
1988
2311
|
release: () => {
|
|
1989
2312
|
try {
|
|
1990
|
-
|
|
2313
|
+
rmSync3(file, { force: true });
|
|
1991
2314
|
} catch {
|
|
1992
2315
|
}
|
|
1993
2316
|
}
|
|
@@ -2065,7 +2388,7 @@ var State = class _State {
|
|
|
2065
2388
|
const heldReported = `.release-held-${suffix}`;
|
|
2066
2389
|
const read = (f) => {
|
|
2067
2390
|
try {
|
|
2068
|
-
return
|
|
2391
|
+
return readFileSync6(this.path(f), "utf8").trim() || null;
|
|
2069
2392
|
} catch {
|
|
2070
2393
|
return null;
|
|
2071
2394
|
}
|
|
@@ -2074,12 +2397,12 @@ var State = class _State {
|
|
|
2074
2397
|
blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
|
|
2075
2398
|
noteBlocked: () => {
|
|
2076
2399
|
const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
|
|
2077
|
-
|
|
2400
|
+
writeFileSync2(this.path(blocked), `${n}
|
|
2078
2401
|
`);
|
|
2079
2402
|
return n;
|
|
2080
2403
|
},
|
|
2081
2404
|
clearBlocked: () => {
|
|
2082
|
-
|
|
2405
|
+
rmSync3(this.path(blocked), { force: true });
|
|
2083
2406
|
},
|
|
2084
2407
|
/**
|
|
2085
2408
|
* The commit a deploy last failed on.
|
|
@@ -2091,11 +2414,11 @@ var State = class _State {
|
|
|
2091
2414
|
*/
|
|
2092
2415
|
deployFailedSha: () => read(failed),
|
|
2093
2416
|
noteDeployFailed: (sha) => {
|
|
2094
|
-
|
|
2417
|
+
writeFileSync2(this.path(failed), `${sha}
|
|
2095
2418
|
`);
|
|
2096
2419
|
},
|
|
2097
2420
|
clearDeployFailed: () => {
|
|
2098
|
-
|
|
2421
|
+
rmSync3(this.path(failed), { force: true });
|
|
2099
2422
|
},
|
|
2100
2423
|
/**
|
|
2101
2424
|
* Consecutive test-gate failures on the same head (CREW-1368). A failure
|
|
@@ -2105,31 +2428,31 @@ var State = class _State {
|
|
|
2105
2428
|
noteTestGateFailed: (sha) => {
|
|
2106
2429
|
const [prev, n] = (read(gateFailed) ?? "").split(" ");
|
|
2107
2430
|
const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
|
|
2108
|
-
|
|
2431
|
+
writeFileSync2(this.path(gateFailed), `${sha} ${next}
|
|
2109
2432
|
`);
|
|
2110
2433
|
return next;
|
|
2111
2434
|
},
|
|
2112
2435
|
clearTestGateFailed: () => {
|
|
2113
|
-
|
|
2436
|
+
rmSync3(this.path(gateFailed), { force: true });
|
|
2114
2437
|
},
|
|
2115
2438
|
/** Whether the board was already told about this head's red gate. */
|
|
2116
2439
|
heldReported: () => {
|
|
2117
2440
|
try {
|
|
2118
|
-
return
|
|
2441
|
+
return readFileSync6(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
|
|
2119
2442
|
} catch {
|
|
2120
2443
|
return null;
|
|
2121
2444
|
}
|
|
2122
2445
|
},
|
|
2123
2446
|
noteHeldReported: (signature) => {
|
|
2124
|
-
|
|
2447
|
+
writeFileSync2(this.path(heldReported), `${signature}
|
|
2125
2448
|
`);
|
|
2126
2449
|
},
|
|
2127
2450
|
clearHeldReported: () => {
|
|
2128
|
-
|
|
2451
|
+
rmSync3(this.path(heldReported), { force: true });
|
|
2129
2452
|
},
|
|
2130
2453
|
testGateReported: (sha) => read(gateReported) === sha,
|
|
2131
2454
|
noteTestGateReported: (sha) => {
|
|
2132
|
-
|
|
2455
|
+
writeFileSync2(this.path(gateReported), `${sha}
|
|
2133
2456
|
`);
|
|
2134
2457
|
}
|
|
2135
2458
|
};
|
|
@@ -2152,13 +2475,13 @@ var State = class _State {
|
|
|
2152
2475
|
const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
|
|
2153
2476
|
const read = (f) => {
|
|
2154
2477
|
try {
|
|
2155
|
-
return
|
|
2478
|
+
return readFileSync6(this.path(f), "utf8").trim() || null;
|
|
2156
2479
|
} catch {
|
|
2157
2480
|
return null;
|
|
2158
2481
|
}
|
|
2159
2482
|
};
|
|
2160
2483
|
const clear2 = () => {
|
|
2161
|
-
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile])
|
|
2484
|
+
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync3(this.path(f), { force: true });
|
|
2162
2485
|
};
|
|
2163
2486
|
return {
|
|
2164
2487
|
/** Consecutive cycles this route had actionable work and did not win. */
|
|
@@ -2185,18 +2508,18 @@ var State = class _State {
|
|
|
2185
2508
|
clear2();
|
|
2186
2509
|
return;
|
|
2187
2510
|
}
|
|
2188
|
-
|
|
2511
|
+
writeFileSync2(this.path(streakFile), `${this.readInt(streakFile) + 1}
|
|
2189
2512
|
`);
|
|
2190
2513
|
const prior = read(ticketFile);
|
|
2191
2514
|
if (topTicket && topTicket === prior) {
|
|
2192
|
-
|
|
2515
|
+
writeFileSync2(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
|
|
2193
2516
|
`);
|
|
2194
2517
|
} else if (topTicket) {
|
|
2195
|
-
|
|
2518
|
+
writeFileSync2(this.path(ticketFile), `${topTicket}
|
|
2196
2519
|
`);
|
|
2197
|
-
|
|
2520
|
+
writeFileSync2(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2198
2521
|
`);
|
|
2199
|
-
|
|
2522
|
+
writeFileSync2(this.path(ticketStreakFile), "1\n");
|
|
2200
2523
|
}
|
|
2201
2524
|
}
|
|
2202
2525
|
};
|
|
@@ -2217,14 +2540,14 @@ var State = class _State {
|
|
|
2217
2540
|
return {
|
|
2218
2541
|
previous: () => {
|
|
2219
2542
|
try {
|
|
2220
|
-
const parsed = JSON.parse(
|
|
2543
|
+
const parsed = JSON.parse(readFileSync6(this.path(file), "utf8"));
|
|
2221
2544
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
2222
2545
|
} catch {
|
|
2223
2546
|
return {};
|
|
2224
2547
|
}
|
|
2225
2548
|
},
|
|
2226
2549
|
persist: (next) => {
|
|
2227
|
-
|
|
2550
|
+
writeFileSync2(this.path(file), JSON.stringify(next));
|
|
2228
2551
|
}
|
|
2229
2552
|
};
|
|
2230
2553
|
}
|
|
@@ -2238,14 +2561,14 @@ var State = class _State {
|
|
|
2238
2561
|
return {
|
|
2239
2562
|
previous: () => {
|
|
2240
2563
|
try {
|
|
2241
|
-
const parsed = JSON.parse(
|
|
2564
|
+
const parsed = JSON.parse(readFileSync6(this.path(file), "utf8"));
|
|
2242
2565
|
return Array.isArray(parsed) ? parsed : [];
|
|
2243
2566
|
} catch {
|
|
2244
2567
|
return [];
|
|
2245
2568
|
}
|
|
2246
2569
|
},
|
|
2247
2570
|
persist: (next) => {
|
|
2248
|
-
|
|
2571
|
+
writeFileSync2(this.path(file), JSON.stringify(next));
|
|
2249
2572
|
}
|
|
2250
2573
|
};
|
|
2251
2574
|
}
|
|
@@ -2258,7 +2581,7 @@ var State = class _State {
|
|
|
2258
2581
|
const file = ".release-last-run";
|
|
2259
2582
|
return {
|
|
2260
2583
|
touch: () => {
|
|
2261
|
-
|
|
2584
|
+
writeFileSync2(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
|
|
2262
2585
|
},
|
|
2263
2586
|
at: () => {
|
|
2264
2587
|
try {
|
|
@@ -2271,7 +2594,7 @@ var State = class _State {
|
|
|
2271
2594
|
}
|
|
2272
2595
|
readInt(file) {
|
|
2273
2596
|
try {
|
|
2274
|
-
return Number.parseInt(
|
|
2597
|
+
return Number.parseInt(readFileSync6(this.path(file), "utf8").trim(), 10) || 0;
|
|
2275
2598
|
} catch {
|
|
2276
2599
|
return 0;
|
|
2277
2600
|
}
|
|
@@ -2575,9 +2898,9 @@ function describeRelease(o, route) {
|
|
|
2575
2898
|
}
|
|
2576
2899
|
|
|
2577
2900
|
// src/install.ts
|
|
2578
|
-
import { dirname as
|
|
2579
|
-
import { homedir as
|
|
2580
|
-
import { accessSync, constants, existsSync as
|
|
2901
|
+
import { dirname as dirname4, join as join7, extname } from "path";
|
|
2902
|
+
import { homedir as homedir4 } from "os";
|
|
2903
|
+
import { accessSync, constants, existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync3, unlinkSync, realpathSync } from "fs";
|
|
2581
2904
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
2582
2905
|
import { createHash } from "crypto";
|
|
2583
2906
|
|
|
@@ -2589,7 +2912,7 @@ function isCompiledBinary(moduleUrl) {
|
|
|
2589
2912
|
// src/install.ts
|
|
2590
2913
|
function crewInvocation(crewHome) {
|
|
2591
2914
|
if (isCompiledBinary(import.meta.url)) return [process.execPath];
|
|
2592
|
-
return [process.execPath,
|
|
2915
|
+
return [process.execPath, join7(crewHome, "bin", "crew")];
|
|
2593
2916
|
}
|
|
2594
2917
|
var INTERVAL_SECONDS = 120;
|
|
2595
2918
|
function labelFor(crewHome, job) {
|
|
@@ -2620,14 +2943,14 @@ function schedulerLogFor(crewLog, job) {
|
|
|
2620
2943
|
return `${stem}.${suffix}${ext || ".log"}`;
|
|
2621
2944
|
}
|
|
2622
2945
|
function pathFor(ship) {
|
|
2623
|
-
const nodeBinDir =
|
|
2946
|
+
const nodeBinDir = dirname4(process.execPath);
|
|
2624
2947
|
const parts = [nodeBinDir];
|
|
2625
2948
|
if (ship.extraPath) parts.push(ship.extraPath);
|
|
2626
2949
|
parts.push("/usr/bin", "/bin", "/usr/sbin", "/sbin");
|
|
2627
2950
|
return parts.join(":");
|
|
2628
2951
|
}
|
|
2629
2952
|
function findOnPath(bin, searchPath) {
|
|
2630
|
-
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) =>
|
|
2953
|
+
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join7(dir, bin));
|
|
2631
2954
|
for (const candidate of candidates) {
|
|
2632
2955
|
try {
|
|
2633
2956
|
accessSync(candidate, constants.X_OK);
|
|
@@ -2637,19 +2960,19 @@ function findOnPath(bin, searchPath) {
|
|
|
2637
2960
|
}
|
|
2638
2961
|
return void 0;
|
|
2639
2962
|
}
|
|
2640
|
-
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin",
|
|
2963
|
+
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join7(homedir4(), ".docker", "bin")].join(":");
|
|
2641
2964
|
function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
2642
2965
|
const schedulerPath = pathFor(ship);
|
|
2643
2966
|
if (findOnPath("docker", schedulerPath)) return void 0;
|
|
2644
2967
|
const inShell = findOnPath("docker", shellPath);
|
|
2645
|
-
return inShell ? `\`docker\` is not on the scheduler PATH (${schedulerPath}) but is at ${inShell} in your shell; add ${
|
|
2968
|
+
return inShell ? `\`docker\` is not on the scheduler PATH (${schedulerPath}) but is at ${inShell} in your shell; add ${dirname4(inShell)} to ship.extraPath in crew.yaml and run \`crew install\`` : `\`docker\` is not on the scheduler PATH (${schedulerPath}) or your shell PATH; install Docker, then add its directory to ship.extraPath in crew.yaml and run \`crew install\``;
|
|
2646
2969
|
}
|
|
2647
2970
|
function xmlEscape(s) {
|
|
2648
2971
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2649
2972
|
}
|
|
2650
2973
|
function planLaunchd(ship, crewHome, job) {
|
|
2651
2974
|
const label = labelFor(crewHome, job);
|
|
2652
|
-
const unitPath =
|
|
2975
|
+
const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2653
2976
|
const crewLog = ship.logFile;
|
|
2654
2977
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
2655
2978
|
const persistent = job === "run";
|
|
@@ -2701,8 +3024,8 @@ ${scheduleKeys}
|
|
|
2701
3024
|
}
|
|
2702
3025
|
function planSystemd(ship, crewHome, job) {
|
|
2703
3026
|
const label = labelFor(crewHome, job);
|
|
2704
|
-
const unitDir =
|
|
2705
|
-
const servicePath =
|
|
3027
|
+
const unitDir = join7(homedir4(), ".config", "systemd", "user");
|
|
3028
|
+
const servicePath = join7(unitDir, `${label}.service`);
|
|
2706
3029
|
const crewLog = ship.logFile;
|
|
2707
3030
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
2708
3031
|
const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
|
|
@@ -2739,7 +3062,7 @@ WantedBy=default.target
|
|
|
2739
3062
|
schedulerLog
|
|
2740
3063
|
};
|
|
2741
3064
|
}
|
|
2742
|
-
const timerPath =
|
|
3065
|
+
const timerPath = join7(unitDir, `${label}.timer`);
|
|
2743
3066
|
const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
|
|
2744
3067
|
[Unit]
|
|
2745
3068
|
Description=${description}
|
|
@@ -2806,7 +3129,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
2806
3129
|
function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
2807
3130
|
const label = labelFor(crewHome, job);
|
|
2808
3131
|
if (hostShip === "macos") {
|
|
2809
|
-
const unitPath =
|
|
3132
|
+
const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2810
3133
|
return {
|
|
2811
3134
|
mechanism: "launchd",
|
|
2812
3135
|
unitPaths: [unitPath],
|
|
@@ -2816,18 +3139,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
2816
3139
|
};
|
|
2817
3140
|
}
|
|
2818
3141
|
if (hostShip === "linux" && hasSystemd) {
|
|
2819
|
-
const unitDir =
|
|
3142
|
+
const unitDir = join7(homedir4(), ".config", "systemd", "user");
|
|
2820
3143
|
if (job === "run") {
|
|
2821
3144
|
return {
|
|
2822
3145
|
mechanism: "systemd",
|
|
2823
|
-
unitPaths: [
|
|
3146
|
+
unitPaths: [join7(unitDir, `${label}.service`)],
|
|
2824
3147
|
crontabLine: null,
|
|
2825
3148
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
|
|
2826
3149
|
};
|
|
2827
3150
|
}
|
|
2828
3151
|
return {
|
|
2829
3152
|
mechanism: "systemd",
|
|
2830
|
-
unitPaths: [
|
|
3153
|
+
unitPaths: [join7(unitDir, `${label}.service`), join7(unitDir, `${label}.timer`)],
|
|
2831
3154
|
crontabLine: null,
|
|
2832
3155
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
|
|
2833
3156
|
};
|
|
@@ -2845,7 +3168,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
2845
3168
|
function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
2846
3169
|
const label = labelFor(crewHome, "run");
|
|
2847
3170
|
if (hostShip === "macos") {
|
|
2848
|
-
const unitPath =
|
|
3171
|
+
const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2849
3172
|
return {
|
|
2850
3173
|
mechanism: "launchd",
|
|
2851
3174
|
startCommand: ["launchctl", "load", unitPath],
|
|
@@ -2885,8 +3208,8 @@ async function applyInstall(plan, crewHome, dryRun, log) {
|
|
|
2885
3208
|
log.emit(`would write ${path}`);
|
|
2886
3209
|
continue;
|
|
2887
3210
|
}
|
|
2888
|
-
|
|
2889
|
-
|
|
3211
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
3212
|
+
writeFileSync3(path, plan.unitContent[path]);
|
|
2890
3213
|
log.emit(`wrote ${path}`);
|
|
2891
3214
|
}
|
|
2892
3215
|
if (plan.crontabLine) {
|
|
@@ -2940,7 +3263,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
2940
3263
|
}
|
|
2941
3264
|
}
|
|
2942
3265
|
for (const path of plan.unitPaths) {
|
|
2943
|
-
if (!
|
|
3266
|
+
if (!existsSync6(path)) continue;
|
|
2944
3267
|
if (dryRun) {
|
|
2945
3268
|
log.emit(`would remove ${path}`);
|
|
2946
3269
|
continue;
|
|
@@ -2959,7 +3282,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
2959
3282
|
}
|
|
2960
3283
|
var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
|
|
2961
3284
|
function launchAgentsDir() {
|
|
2962
|
-
return
|
|
3285
|
+
return join7(homedir4(), "Library", "LaunchAgents");
|
|
2963
3286
|
}
|
|
2964
3287
|
function xmlUnescape(s) {
|
|
2965
3288
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
@@ -2976,25 +3299,25 @@ function loadedCrewLabels() {
|
|
|
2976
3299
|
return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
|
|
2977
3300
|
}
|
|
2978
3301
|
function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
2979
|
-
const files =
|
|
3302
|
+
const files = existsSync6(dir) ? readdirSync(dir) : [];
|
|
2980
3303
|
const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
|
|
2981
|
-
const plistPath =
|
|
3304
|
+
const plistPath = join7(dir, f);
|
|
2982
3305
|
let program = null;
|
|
2983
3306
|
try {
|
|
2984
|
-
program = programFromPlist(
|
|
3307
|
+
program = programFromPlist(readFileSync7(plistPath, "utf8"));
|
|
2985
3308
|
} catch {
|
|
2986
3309
|
}
|
|
2987
3310
|
return {
|
|
2988
3311
|
label: f.slice(0, -".plist".length),
|
|
2989
3312
|
plistPath,
|
|
2990
3313
|
program,
|
|
2991
|
-
programExists: program !== null &&
|
|
3314
|
+
programExists: program !== null && existsSync6(program)
|
|
2992
3315
|
};
|
|
2993
3316
|
});
|
|
2994
3317
|
const seen = new Set(units.map((u) => u.label));
|
|
2995
3318
|
for (const label of [...loaded].sort()) {
|
|
2996
3319
|
if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
|
|
2997
|
-
units.push({ label, plistPath:
|
|
3320
|
+
units.push({ label, plistPath: join7(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
|
|
2998
3321
|
}
|
|
2999
3322
|
return units;
|
|
3000
3323
|
}
|
|
@@ -3007,9 +3330,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
|
|
|
3007
3330
|
log.emit(`would unload and remove ${unit.label}`);
|
|
3008
3331
|
return;
|
|
3009
3332
|
}
|
|
3010
|
-
if (
|
|
3333
|
+
if (existsSync6(unit.plistPath)) spawnSync2("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
|
|
3011
3334
|
spawnSync2("launchctl", ["remove", unit.label], { encoding: "utf8" });
|
|
3012
|
-
if (
|
|
3335
|
+
if (existsSync6(unit.plistPath)) unlinkSync(unit.plistPath);
|
|
3013
3336
|
log.emit(`removed ${unit.label} (${unit.plistPath})`);
|
|
3014
3337
|
}
|
|
3015
3338
|
function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
|
|
@@ -3092,15 +3415,15 @@ import { statfsSync } from "fs";
|
|
|
3092
3415
|
// src/git.ts
|
|
3093
3416
|
import { execFileSync } from "child_process";
|
|
3094
3417
|
import {
|
|
3095
|
-
existsSync as
|
|
3096
|
-
mkdirSync as
|
|
3418
|
+
existsSync as existsSync7,
|
|
3419
|
+
mkdirSync as mkdirSync5,
|
|
3097
3420
|
openSync as openSync2,
|
|
3098
3421
|
closeSync as closeSync2,
|
|
3099
3422
|
writeSync,
|
|
3100
3423
|
statSync as statSync4,
|
|
3101
3424
|
unlinkSync as unlinkSync2
|
|
3102
3425
|
} from "fs";
|
|
3103
|
-
import { dirname as
|
|
3426
|
+
import { dirname as dirname5, basename as basename4 } from "path";
|
|
3104
3427
|
var GitError = class extends Error {
|
|
3105
3428
|
};
|
|
3106
3429
|
function gitRaw(cwd, args) {
|
|
@@ -3464,13 +3787,13 @@ function cloneUrlFor(remote) {
|
|
|
3464
3787
|
return `git@github.com:${remote}.git`;
|
|
3465
3788
|
}
|
|
3466
3789
|
function ensureRepoCheckout(dir, remote) {
|
|
3467
|
-
if (
|
|
3790
|
+
if (existsSync7(`${dir}/.git`)) return false;
|
|
3468
3791
|
if (!remote) {
|
|
3469
3792
|
throw new GitError(
|
|
3470
3793
|
`${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`
|
|
3471
3794
|
);
|
|
3472
3795
|
}
|
|
3473
|
-
|
|
3796
|
+
mkdirSync5(dirname5(dir), { recursive: true });
|
|
3474
3797
|
try {
|
|
3475
3798
|
execFileSync("git", ["clone", cloneUrlFor(remote), dir], { stdio: ["ignore", "pipe", "pipe"] });
|
|
3476
3799
|
} catch (e) {
|
|
@@ -3489,7 +3812,7 @@ function sleepSync(ms2) {
|
|
|
3489
3812
|
}
|
|
3490
3813
|
function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
3491
3814
|
const lockPath = checkoutLockPath(dir);
|
|
3492
|
-
|
|
3815
|
+
mkdirSync5(dirname5(lockPath), { recursive: true });
|
|
3493
3816
|
const deadline = Date.now() + timeoutMs;
|
|
3494
3817
|
for (; ; ) {
|
|
3495
3818
|
try {
|
|
@@ -5201,8 +5524,8 @@ function materializeBranchForTicket(dir, cfg, t, role) {
|
|
|
5201
5524
|
}
|
|
5202
5525
|
|
|
5203
5526
|
// src/poll.ts
|
|
5204
|
-
import { writeFileSync as
|
|
5205
|
-
import { join as
|
|
5527
|
+
import { writeFileSync as writeFileSync4 } from "fs";
|
|
5528
|
+
import { join as join8 } from "path";
|
|
5206
5529
|
|
|
5207
5530
|
// src/stalled.ts
|
|
5208
5531
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -5474,7 +5797,7 @@ async function decideCycle(o) {
|
|
|
5474
5797
|
}
|
|
5475
5798
|
return decision;
|
|
5476
5799
|
}
|
|
5477
|
-
var digestPath = (stateDir, route, role) =>
|
|
5800
|
+
var digestPath = (stateDir, route, role) => join8(stateDir, `digest-${route}-${role}.md`);
|
|
5478
5801
|
function writeDigest(o, d, role, stateDir) {
|
|
5479
5802
|
const me = o.route.resolved?.seats[role];
|
|
5480
5803
|
if (!me) return false;
|
|
@@ -5536,7 +5859,7 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
5536
5859
|
};
|
|
5537
5860
|
const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
|
|
5538
5861
|
const path = digestPath(stateDir, routeSlug(o.route.route), role);
|
|
5539
|
-
|
|
5862
|
+
writeFileSync4(path, text);
|
|
5540
5863
|
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
|
|
5541
5864
|
data: { role, bytes: Buffer.byteLength(text) }
|
|
5542
5865
|
});
|
|
@@ -5844,9 +6167,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
|
|
|
5844
6167
|
|
|
5845
6168
|
// src/agent.ts
|
|
5846
6169
|
import { spawn as spawn2 } from "child_process";
|
|
5847
|
-
import { readFileSync as
|
|
6170
|
+
import { readFileSync as readFileSync8, existsSync as existsSync8, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5, createWriteStream } from "fs";
|
|
5848
6171
|
import { finished } from "stream/promises";
|
|
5849
|
-
import { join as
|
|
6172
|
+
import { join as join9, dirname as dirname6 } from "path";
|
|
5850
6173
|
import { createHash as createHash2 } from "crypto";
|
|
5851
6174
|
|
|
5852
6175
|
// src/environment.ts
|
|
@@ -6244,23 +6567,23 @@ var AgentError = class extends Error {
|
|
|
6244
6567
|
var AgentSpawnError = class extends AgentError {
|
|
6245
6568
|
};
|
|
6246
6569
|
function planAgentRun(o) {
|
|
6247
|
-
const briefPath =
|
|
6248
|
-
if (!
|
|
6570
|
+
const briefPath = join9(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
6571
|
+
if (!existsSync8(briefPath)) {
|
|
6249
6572
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
6250
6573
|
}
|
|
6251
|
-
const commonPath =
|
|
6252
|
-
if (!
|
|
6574
|
+
const commonPath = join9(o.route.promptsDir, "personas", "common.md");
|
|
6575
|
+
if (!existsSync8(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
|
|
6253
6576
|
const now = o.now ?? (() => Date.now());
|
|
6254
6577
|
const routeLabel = routeSlug(o.route.route);
|
|
6255
|
-
const digestPath2 =
|
|
6578
|
+
const digestPath2 = join9(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
6256
6579
|
let digest;
|
|
6257
6580
|
let ageSeconds;
|
|
6258
|
-
if (
|
|
6581
|
+
if (existsSync8(digestPath2)) {
|
|
6259
6582
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
6260
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
6583
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync8(digestPath2, "utf8");
|
|
6261
6584
|
}
|
|
6262
|
-
const common =
|
|
6263
|
-
const brief =
|
|
6585
|
+
const common = readFileSync8(commonPath, "utf8");
|
|
6586
|
+
const brief = readFileSync8(briefPath, "utf8");
|
|
6264
6587
|
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
6265
6588
|
const effectiveBrief = o.divergedPrompt ?? brief;
|
|
6266
6589
|
const prompt = assemblePrompt({
|
|
@@ -6271,7 +6594,7 @@ function planAgentRun(o) {
|
|
|
6271
6594
|
digest
|
|
6272
6595
|
});
|
|
6273
6596
|
const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
|
|
6274
|
-
const streamsDir =
|
|
6597
|
+
const streamsDir = join9(o.stateDir, "streams");
|
|
6275
6598
|
const base = `${routeLabel}-${o.role}-${o.cycle}`;
|
|
6276
6599
|
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}));
|
|
6277
6600
|
const { toolchain, model } = seat;
|
|
@@ -6326,8 +6649,8 @@ function planAgentRun(o) {
|
|
|
6326
6649
|
digestAttached: digest !== void 0,
|
|
6327
6650
|
digestAgeSeconds: ageSeconds,
|
|
6328
6651
|
model,
|
|
6329
|
-
streamPath:
|
|
6330
|
-
eventsPath:
|
|
6652
|
+
streamPath: join9(streamsDir, `${base}.jsonl`),
|
|
6653
|
+
eventsPath: join9(streamsDir, `${base}.events.jsonl`),
|
|
6331
6654
|
ticket: o.ticket,
|
|
6332
6655
|
agentLog,
|
|
6333
6656
|
promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
|
|
@@ -6354,7 +6677,7 @@ function describePlan(p) {
|
|
|
6354
6677
|
}
|
|
6355
6678
|
function openSink(path) {
|
|
6356
6679
|
try {
|
|
6357
|
-
|
|
6680
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
6358
6681
|
return createWriteStream(path, { flags: "a" });
|
|
6359
6682
|
} catch {
|
|
6360
6683
|
return void 0;
|
|
@@ -6463,7 +6786,7 @@ function spawnAgent(plan, emit) {
|
|
|
6463
6786
|
provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER
|
|
6464
6787
|
};
|
|
6465
6788
|
try {
|
|
6466
|
-
|
|
6789
|
+
writeFileSync5(`${plan.streamPath}.meta.json`, JSON.stringify({
|
|
6467
6790
|
route: plan.route,
|
|
6468
6791
|
role: plan.role,
|
|
6469
6792
|
ticket: plan.ticket ?? null,
|
|
@@ -6522,13 +6845,13 @@ function spawnAgent(plan, emit) {
|
|
|
6522
6845
|
}
|
|
6523
6846
|
|
|
6524
6847
|
// src/run-worktree.ts
|
|
6525
|
-
import { existsSync as
|
|
6526
|
-
import { dirname as
|
|
6848
|
+
import { existsSync as existsSync10 } from "fs";
|
|
6849
|
+
import { dirname as dirname7, join as join11 } from "path";
|
|
6527
6850
|
|
|
6528
6851
|
// src/ports.ts
|
|
6529
6852
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
6530
|
-
import { existsSync as
|
|
6531
|
-
import { join as
|
|
6853
|
+
import { existsSync as existsSync9 } from "fs";
|
|
6854
|
+
import { join as join10 } from "path";
|
|
6532
6855
|
var run = (cmd, args) => {
|
|
6533
6856
|
try {
|
|
6534
6857
|
return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -6573,7 +6896,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
6573
6896
|
}
|
|
6574
6897
|
function worktreeExists(parent, prefix, n) {
|
|
6575
6898
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
6576
|
-
if (
|
|
6899
|
+
if (existsSync9(join10(parent, name, ".git"))) return true;
|
|
6577
6900
|
}
|
|
6578
6901
|
return false;
|
|
6579
6902
|
}
|
|
@@ -6647,7 +6970,7 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
6647
6970
|
const existing = worktreeForNumber(dir, num);
|
|
6648
6971
|
const located = locateBranchForTicket(dir, cfg, t, role);
|
|
6649
6972
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
6650
|
-
if (existing && !existing.prunable &&
|
|
6973
|
+
if (existing && !existing.prunable && existsSync10(existing.path)) {
|
|
6651
6974
|
const branch = existing.branch;
|
|
6652
6975
|
if (status(existing.path).length > 0) return { action: "leave-dirty", path: existing.path, branch };
|
|
6653
6976
|
if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
|
|
@@ -6661,7 +6984,7 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
6661
6984
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
6662
6985
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
6663
6986
|
}
|
|
6664
|
-
const path =
|
|
6987
|
+
const path = join11(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
|
|
6665
6988
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
6666
6989
|
return { action: "cut-remote", path, branch: located.branch };
|
|
6667
6990
|
}
|
|
@@ -6708,7 +7031,7 @@ function worktreeHoldsUnpushed(path, remote) {
|
|
|
6708
7031
|
}
|
|
6709
7032
|
async function finishRunWorktree(dir, cfg, branch) {
|
|
6710
7033
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
6711
|
-
if (!wt || !
|
|
7034
|
+
if (!wt || !existsSync10(wt.path)) return { kind: "none" };
|
|
6712
7035
|
if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
|
|
6713
7036
|
if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
|
|
6714
7037
|
if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
|
|
@@ -6720,7 +7043,7 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
6720
7043
|
if (pids.length) await killGently(pids);
|
|
6721
7044
|
}
|
|
6722
7045
|
gitOk(dir, ["worktree", "remove", wt.path]);
|
|
6723
|
-
return
|
|
7046
|
+
return existsSync10(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
|
|
6724
7047
|
}
|
|
6725
7048
|
|
|
6726
7049
|
// src/handoff-guard.ts
|
|
@@ -6869,13 +7192,13 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
|
|
|
6869
7192
|
}
|
|
6870
7193
|
|
|
6871
7194
|
// src/connect-wizard.ts
|
|
6872
|
-
import { delimiter, dirname as
|
|
6873
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as
|
|
7195
|
+
import { delimiter, dirname as dirname8, join as join12 } from "path";
|
|
7196
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync11, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
6874
7197
|
import { stringify } from "yaml";
|
|
6875
7198
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
6876
7199
|
function shouldRunWizard(opts) {
|
|
6877
7200
|
if (!opts.isTTY) return false;
|
|
6878
|
-
const exists = opts.configExists ??
|
|
7201
|
+
const exists = opts.configExists ?? existsSync11;
|
|
6879
7202
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
6880
7203
|
}
|
|
6881
7204
|
function firstRunConfigPath() {
|
|
@@ -6892,7 +7215,7 @@ function defaultIsExecutable(p) {
|
|
|
6892
7215
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
6893
7216
|
for (const dir of pathEnv.split(delimiter)) {
|
|
6894
7217
|
if (!dir) continue;
|
|
6895
|
-
const candidate =
|
|
7218
|
+
const candidate = join12(dir, name);
|
|
6896
7219
|
if (isExecutable(candidate)) return candidate;
|
|
6897
7220
|
}
|
|
6898
7221
|
return void 0;
|
|
@@ -6918,9 +7241,9 @@ async function runWizard(p, env) {
|
|
|
6918
7241
|
if (entered && isExec(entered)) agentBin = entered;
|
|
6919
7242
|
else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
|
|
6920
7243
|
}
|
|
6921
|
-
const nodeDir =
|
|
7244
|
+
const nodeDir = dirname8(env.nodePath);
|
|
6922
7245
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
6923
|
-
const baseDirs = [nodeDir, pnpmPath ?
|
|
7246
|
+
const baseDirs = [nodeDir, pnpmPath ? dirname8(pnpmPath) : void 0];
|
|
6924
7247
|
p.say(
|
|
6925
7248
|
`
|
|
6926
7249
|
Host Passengers lets this machine run the agent sessions that other people's
|
|
@@ -6931,7 +7254,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
|
|
|
6931
7254
|
p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
|
|
6932
7255
|
}
|
|
6933
7256
|
const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
6934
|
-
const extraPath = deriveExtraPath([...baseDirs, dockerPath ?
|
|
7257
|
+
const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname8(dockerPath) : void 0]);
|
|
6935
7258
|
if (extraPath) {
|
|
6936
7259
|
p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
|
|
6937
7260
|
}
|
|
@@ -6953,9 +7276,9 @@ routes:
|
|
|
6953
7276
|
${routeBlock}`;
|
|
6954
7277
|
}
|
|
6955
7278
|
function writeNewConfig(path, text) {
|
|
6956
|
-
|
|
7279
|
+
mkdirSync7(dirname8(path), { recursive: true });
|
|
6957
7280
|
try {
|
|
6958
|
-
|
|
7281
|
+
writeFileSync6(path, text, { mode: 384, flag: "wx" });
|
|
6959
7282
|
return true;
|
|
6960
7283
|
} catch (e) {
|
|
6961
7284
|
if (e.code === "EEXIST") return false;
|
|
@@ -6987,8 +7310,8 @@ Wrote ${o.configPath}.
|
|
|
6987
7310
|
}
|
|
6988
7311
|
|
|
6989
7312
|
// src/release-run.ts
|
|
6990
|
-
import { readFileSync as
|
|
6991
|
-
import { dirname as
|
|
7313
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync12, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync4 } from "fs";
|
|
7314
|
+
import { dirname as dirname9, join as join13 } from "path";
|
|
6992
7315
|
import { tmpdir as tmpdir2 } from "os";
|
|
6993
7316
|
|
|
6994
7317
|
// src/release.ts
|
|
@@ -7646,13 +7969,13 @@ async function bump(o, size) {
|
|
|
7646
7969
|
}
|
|
7647
7970
|
return printed;
|
|
7648
7971
|
}
|
|
7649
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
7972
|
+
const files = o.repo.release.versionFiles.map((f) => join13(o.cwd, f));
|
|
7650
7973
|
const first = files[0];
|
|
7651
|
-
if (!
|
|
7974
|
+
if (!existsSync12(first)) {
|
|
7652
7975
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
7653
7976
|
return null;
|
|
7654
7977
|
}
|
|
7655
|
-
const current = JSON.parse(
|
|
7978
|
+
const current = JSON.parse(readFileSync9(first, "utf8")).version;
|
|
7656
7979
|
if (!current) {
|
|
7657
7980
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
7658
7981
|
return null;
|
|
@@ -7665,27 +7988,27 @@ async function bump(o, size) {
|
|
|
7665
7988
|
return version;
|
|
7666
7989
|
}
|
|
7667
7990
|
for (const f of files) {
|
|
7668
|
-
const src =
|
|
7991
|
+
const src = readFileSync9(f, "utf8");
|
|
7669
7992
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
7670
7993
|
if (out === src) {
|
|
7671
7994
|
o.emit.error(`no version field in ${f}`);
|
|
7672
7995
|
return null;
|
|
7673
7996
|
}
|
|
7674
|
-
|
|
7997
|
+
writeFileSync7(f, out);
|
|
7675
7998
|
}
|
|
7676
7999
|
return version;
|
|
7677
8000
|
}
|
|
7678
8001
|
function writeChangelog(o, version, entries) {
|
|
7679
8002
|
const file = o.repo.release.changelog;
|
|
7680
8003
|
if (!file || entries.length === 0) return;
|
|
7681
|
-
const path =
|
|
8004
|
+
const path = join13(o.cwd, file);
|
|
7682
8005
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
7683
8006
|
if (o.dryRun) {
|
|
7684
8007
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
7685
8008
|
return;
|
|
7686
8009
|
}
|
|
7687
|
-
const existing =
|
|
7688
|
-
|
|
8010
|
+
const existing = existsSync12(path) ? readFileSync9(path, "utf8") : "# Changelog\n";
|
|
8011
|
+
writeFileSync7(path, insertChangelogSection(existing, section));
|
|
7689
8012
|
}
|
|
7690
8013
|
async function confirm(o, expected) {
|
|
7691
8014
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -7766,11 +8089,11 @@ async function confirmLastRelease(o, decision) {
|
|
|
7766
8089
|
function openReleaseCheckout(o) {
|
|
7767
8090
|
const { remote, base } = o.repo.branch;
|
|
7768
8091
|
const branch = `crew/release-${Date.now()}`;
|
|
7769
|
-
const root = mkdtempSync(
|
|
7770
|
-
const dir =
|
|
8092
|
+
const root = mkdtempSync(join13(tmpdir2(), "crew-release-"));
|
|
8093
|
+
const dir = join13(root, "checkout");
|
|
7771
8094
|
const close = () => {
|
|
7772
8095
|
gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
|
|
7773
|
-
|
|
8096
|
+
rmSync4(root, { recursive: true, force: true });
|
|
7774
8097
|
gitOk(o.cwd, ["worktree", "prune"]);
|
|
7775
8098
|
gitOk(o.cwd, ["branch", "-D", branch]);
|
|
7776
8099
|
};
|
|
@@ -7781,10 +8104,10 @@ function openReleaseCheckout(o) {
|
|
|
7781
8104
|
throw e;
|
|
7782
8105
|
}
|
|
7783
8106
|
for (const f of o.repo.worktrees.copy) {
|
|
7784
|
-
const src =
|
|
7785
|
-
if (!
|
|
7786
|
-
|
|
7787
|
-
copyFileSync(src,
|
|
8107
|
+
const src = join13(o.cwd, f);
|
|
8108
|
+
if (!existsSync12(src)) continue;
|
|
8109
|
+
mkdirSync8(dirname9(join13(dir, f)), { recursive: true });
|
|
8110
|
+
copyFileSync(src, join13(dir, f));
|
|
7788
8111
|
}
|
|
7789
8112
|
return { dir, branch, close };
|
|
7790
8113
|
}
|
|
@@ -8253,7 +8576,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
8253
8576
|
}
|
|
8254
8577
|
|
|
8255
8578
|
// src/watch.ts
|
|
8256
|
-
import { watch as watchFile, existsSync as
|
|
8579
|
+
import { watch as watchFile, existsSync as existsSync13, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
|
|
8257
8580
|
var E = "\x1B[";
|
|
8258
8581
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
8259
8582
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -8354,7 +8677,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
8354
8677
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
8355
8678
|
}
|
|
8356
8679
|
function readFrom(file, from) {
|
|
8357
|
-
if (!
|
|
8680
|
+
if (!existsSync13(file)) return { events: [], offset: from };
|
|
8358
8681
|
const size = statSync6(file).size;
|
|
8359
8682
|
if (size < from) return readFrom(file, 0);
|
|
8360
8683
|
if (size === from) return { events: [], offset: from };
|
|
@@ -8594,8 +8917,8 @@ function nextRoles(decision, ran) {
|
|
|
8594
8917
|
}
|
|
8595
8918
|
|
|
8596
8919
|
// src/agents.ts
|
|
8597
|
-
import { readFileSync as
|
|
8598
|
-
import { join as
|
|
8920
|
+
import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
|
|
8921
|
+
import { join as join14 } from "path";
|
|
8599
8922
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
|
|
8600
8923
|
var PERSONA_NAME = {
|
|
8601
8924
|
dev: "Developer",
|
|
@@ -8609,12 +8932,12 @@ var CREW_AGENT_COLUMN = "agent_id";
|
|
|
8609
8932
|
var AgentsSyncError = class extends Error {
|
|
8610
8933
|
};
|
|
8611
8934
|
function personaDefaultPrompt(promptsDir, role) {
|
|
8612
|
-
const briefPath =
|
|
8613
|
-
if (!
|
|
8614
|
-
if (role === "pair") return
|
|
8615
|
-
const commonPath =
|
|
8616
|
-
if (!
|
|
8617
|
-
return `${
|
|
8935
|
+
const briefPath = join14(promptsDir, "personas", `lane-${role}.md`);
|
|
8936
|
+
if (!existsSync14(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
8937
|
+
if (role === "pair") return readFileSync10(briefPath, "utf8");
|
|
8938
|
+
const commonPath = join14(promptsDir, "personas", "common.md");
|
|
8939
|
+
if (!existsSync14(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
|
|
8940
|
+
return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
|
|
8618
8941
|
}
|
|
8619
8942
|
async function resolveAgentId(route, role, opts) {
|
|
8620
8943
|
const cached = route.resolved?.agentPersonas?.[role]?.agentId;
|
|
@@ -9291,14 +9614,14 @@ import {
|
|
|
9291
9614
|
} from "@tablation/client";
|
|
9292
9615
|
|
|
9293
9616
|
// src/skills.ts
|
|
9294
|
-
import { readFileSync as
|
|
9295
|
-
import { join as
|
|
9617
|
+
import { readFileSync as readFileSync11, readdirSync as readdirSync2, existsSync as existsSync15 } from "fs";
|
|
9618
|
+
import { join as join15 } from "path";
|
|
9296
9619
|
import { parse as parseYaml } from "yaml";
|
|
9297
9620
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
9298
9621
|
var SkillSyncError = class extends Error {
|
|
9299
9622
|
};
|
|
9300
9623
|
function parseSkillFile(path) {
|
|
9301
|
-
const text =
|
|
9624
|
+
const text = readFileSync11(path, "utf8");
|
|
9302
9625
|
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
|
|
9303
9626
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
9304
9627
|
const frontmatter = match[1];
|
|
@@ -9311,9 +9634,9 @@ function parseSkillFile(path) {
|
|
|
9311
9634
|
return { name: meta.name, description: meta.description, prompt: body, file: path };
|
|
9312
9635
|
}
|
|
9313
9636
|
function listLocalSkills(promptsDir) {
|
|
9314
|
-
const dir =
|
|
9315
|
-
if (!
|
|
9316
|
-
return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(
|
|
9637
|
+
const dir = join15(promptsDir, "skills");
|
|
9638
|
+
if (!existsSync15(dir)) return [];
|
|
9639
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join15(dir, f)));
|
|
9317
9640
|
}
|
|
9318
9641
|
async function syncSkills(route, opts) {
|
|
9319
9642
|
if (!route.resolved) {
|
|
@@ -9492,7 +9815,7 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
9492
9815
|
}
|
|
9493
9816
|
|
|
9494
9817
|
// src/repos-cmd.ts
|
|
9495
|
-
import { homedir as
|
|
9818
|
+
import { homedir as homedir5 } from "os";
|
|
9496
9819
|
import { basename as basename7, resolve as resolve3 } from "path";
|
|
9497
9820
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
9498
9821
|
var ReposError = class extends Error {
|
|
@@ -9539,7 +9862,7 @@ function parseAddArgs(args, routes, isDir) {
|
|
|
9539
9862
|
return { route, path: "." };
|
|
9540
9863
|
}
|
|
9541
9864
|
function expandDir(p, base) {
|
|
9542
|
-
const home = p === "~" ?
|
|
9865
|
+
const home = p === "~" ? homedir5() : p.startsWith("~/") ? `${homedir5()}/${p.slice(2)}` : p;
|
|
9543
9866
|
return resolve3(base, home);
|
|
9544
9867
|
}
|
|
9545
9868
|
function routeMapOf(doc, route) {
|
|
@@ -9674,7 +9997,7 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
9674
9997
|
}
|
|
9675
9998
|
|
|
9676
9999
|
// src/worktree-sweep.ts
|
|
9677
|
-
import { existsSync as
|
|
10000
|
+
import { existsSync as existsSync16 } from "fs";
|
|
9678
10001
|
import { basename as basename8 } from "path";
|
|
9679
10002
|
function planWorktreeSweep(target, tickets, contract) {
|
|
9680
10003
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
@@ -9702,7 +10025,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
9702
10025
|
let removed = 0;
|
|
9703
10026
|
let keptBranches = 0;
|
|
9704
10027
|
for (const a of actions) {
|
|
9705
|
-
if (!dryRun &&
|
|
10028
|
+
if (!dryRun && existsSync16(a.path)) {
|
|
9706
10029
|
let dirty = false;
|
|
9707
10030
|
try {
|
|
9708
10031
|
dirty = status(a.path).length > 0;
|
|
@@ -9716,7 +10039,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
9716
10039
|
continue;
|
|
9717
10040
|
}
|
|
9718
10041
|
}
|
|
9719
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
10042
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync16(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
9720
10043
|
log.warn(
|
|
9721
10044
|
`not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
|
|
9722
10045
|
{ ticket: a.ticket.issue_id, step: "worktree" }
|
|
@@ -9797,14 +10120,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
9797
10120
|
}
|
|
9798
10121
|
|
|
9799
10122
|
// src/stream-sweep.ts
|
|
9800
|
-
import { existsSync as
|
|
9801
|
-
import { join as
|
|
10123
|
+
import { existsSync as existsSync17, readdirSync as readdirSync3, rmSync as rmSync5, statSync as statSync7 } from "fs";
|
|
10124
|
+
import { join as join16 } from "path";
|
|
9802
10125
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
9803
10126
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
9804
|
-
const dir =
|
|
9805
|
-
if (!
|
|
10127
|
+
const dir = join16(stateDir, "streams");
|
|
10128
|
+
if (!existsSync17(dir)) return [];
|
|
9806
10129
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
9807
|
-
return readdirSync3(dir).map((name) =>
|
|
10130
|
+
return readdirSync3(dir).map((name) => join16(dir, name)).filter((path) => {
|
|
9808
10131
|
try {
|
|
9809
10132
|
return statSync7(path).mtimeMs < cutoff;
|
|
9810
10133
|
} catch {
|
|
@@ -9820,7 +10143,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
9820
10143
|
continue;
|
|
9821
10144
|
}
|
|
9822
10145
|
try {
|
|
9823
|
-
|
|
10146
|
+
rmSync5(path, { force: true });
|
|
9824
10147
|
removed++;
|
|
9825
10148
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
9826
10149
|
} catch (e) {
|
|
@@ -9830,9 +10153,9 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
9830
10153
|
return removed;
|
|
9831
10154
|
}
|
|
9832
10155
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
9833
|
-
if (!
|
|
10156
|
+
if (!existsSync17(stateDir)) return [];
|
|
9834
10157
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
9835
|
-
return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) =>
|
|
10158
|
+
return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join16(stateDir, name)).filter((path) => {
|
|
9836
10159
|
try {
|
|
9837
10160
|
return statSync7(path).mtimeMs < cutoff;
|
|
9838
10161
|
} catch {
|
|
@@ -9842,24 +10165,24 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
9842
10165
|
}
|
|
9843
10166
|
|
|
9844
10167
|
// src/cli.ts
|
|
9845
|
-
import { readFileSync as
|
|
10168
|
+
import { readFileSync as readFileSync15, existsSync as existsSync21, mkdirSync as mkdirSync12, writeFileSync as writeFileSync10, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
|
|
9846
10169
|
import { createInterface } from "readline/promises";
|
|
9847
10170
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
9848
10171
|
|
|
9849
10172
|
// src/ssh-keys.ts
|
|
9850
10173
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
9851
|
-
import { existsSync as
|
|
9852
|
-
import { dirname as
|
|
10174
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, chmodSync } from "fs";
|
|
10175
|
+
import { dirname as dirname11, join as join17 } from "path";
|
|
9853
10176
|
var SshKeyError = class extends Error {
|
|
9854
10177
|
};
|
|
9855
10178
|
function shipSshKeyPathFor(stateDir) {
|
|
9856
|
-
return
|
|
10179
|
+
return join17(stateDir, "ssh", "id_ed25519");
|
|
9857
10180
|
}
|
|
9858
10181
|
function ensureShipSshKeypair(stateDir) {
|
|
9859
10182
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
9860
10183
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
9861
|
-
if (!
|
|
9862
|
-
|
|
10184
|
+
if (!existsSync18(privateKeyPath)) {
|
|
10185
|
+
mkdirSync9(dirname11(privateKeyPath), { recursive: true });
|
|
9863
10186
|
try {
|
|
9864
10187
|
execFileSync3("ssh-keygen", [
|
|
9865
10188
|
"-t",
|
|
@@ -9879,10 +10202,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
9879
10202
|
}
|
|
9880
10203
|
chmodSync(privateKeyPath, 384);
|
|
9881
10204
|
}
|
|
9882
|
-
if (!
|
|
10205
|
+
if (!existsSync18(publicKeyPath)) {
|
|
9883
10206
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
9884
10207
|
}
|
|
9885
|
-
const publicKey =
|
|
10208
|
+
const publicKey = readFileSync12(publicKeyPath, "utf8").trim();
|
|
9886
10209
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
9887
10210
|
}
|
|
9888
10211
|
function sshKeygenAvailable() {
|
|
@@ -10096,9 +10419,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
10096
10419
|
|
|
10097
10420
|
// src/tunnel.ts
|
|
10098
10421
|
import { spawn as spawn3 } from "child_process";
|
|
10099
|
-
import { existsSync as
|
|
10422
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3, readdirSync as readdirSync4 } from "fs";
|
|
10100
10423
|
import { randomInt } from "crypto";
|
|
10101
|
-
import { join as
|
|
10424
|
+
import { join as join18 } from "path";
|
|
10102
10425
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
10103
10426
|
var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
10104
10427
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -10165,21 +10488,21 @@ function startTunnel(opts) {
|
|
|
10165
10488
|
};
|
|
10166
10489
|
}
|
|
10167
10490
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
10168
|
-
return
|
|
10491
|
+
return join18(stateDir, "passengers", `${workspaceId}.json`);
|
|
10169
10492
|
}
|
|
10170
10493
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
10171
10494
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
10172
|
-
if (!
|
|
10495
|
+
if (!existsSync19(path)) return void 0;
|
|
10173
10496
|
try {
|
|
10174
|
-
return JSON.parse(
|
|
10497
|
+
return JSON.parse(readFileSync13(path, "utf8"));
|
|
10175
10498
|
} catch {
|
|
10176
10499
|
return void 0;
|
|
10177
10500
|
}
|
|
10178
10501
|
}
|
|
10179
10502
|
function writePersistedTunnel(stateDir, state) {
|
|
10180
10503
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
10181
|
-
|
|
10182
|
-
|
|
10504
|
+
mkdirSync10(join18(stateDir, "passengers"), { recursive: true });
|
|
10505
|
+
writeFileSync8(path, `${JSON.stringify(state, null, 2)}
|
|
10183
10506
|
`);
|
|
10184
10507
|
}
|
|
10185
10508
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -10276,8 +10599,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
10276
10599
|
}
|
|
10277
10600
|
}
|
|
10278
10601
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
10279
|
-
const dir =
|
|
10280
|
-
if (!
|
|
10602
|
+
const dir = join18(stateDir, "passengers");
|
|
10603
|
+
if (!existsSync19(dir)) return [];
|
|
10281
10604
|
return readdirSync4(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
10282
10605
|
}
|
|
10283
10606
|
|
|
@@ -10398,8 +10721,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
10398
10721
|
import { createServer as createHttpServer } from "http";
|
|
10399
10722
|
import { spawn as spawn4 } from "child_process";
|
|
10400
10723
|
import { randomBytes } from "crypto";
|
|
10401
|
-
import { existsSync as
|
|
10402
|
-
import { join as
|
|
10724
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
|
|
10725
|
+
import { join as join19 } from "path";
|
|
10403
10726
|
|
|
10404
10727
|
// src/secret-compare.ts
|
|
10405
10728
|
import { timingSafeEqual } from "crypto";
|
|
@@ -10488,21 +10811,21 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
10488
10811
|
return child;
|
|
10489
10812
|
};
|
|
10490
10813
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
10491
|
-
return
|
|
10814
|
+
return join19(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
10492
10815
|
}
|
|
10493
10816
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
10494
10817
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
10495
|
-
if (!
|
|
10818
|
+
if (!existsSync20(path)) return void 0;
|
|
10496
10819
|
try {
|
|
10497
|
-
return JSON.parse(
|
|
10820
|
+
return JSON.parse(readFileSync14(path, "utf8"));
|
|
10498
10821
|
} catch {
|
|
10499
10822
|
return void 0;
|
|
10500
10823
|
}
|
|
10501
10824
|
}
|
|
10502
10825
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
10503
10826
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
10504
|
-
|
|
10505
|
-
|
|
10827
|
+
mkdirSync11(join19(stateDir, "passengers"), { recursive: true });
|
|
10828
|
+
writeFileSync9(path, `${JSON.stringify(state, null, 2)}
|
|
10506
10829
|
`);
|
|
10507
10830
|
}
|
|
10508
10831
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -10522,8 +10845,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
10522
10845
|
}
|
|
10523
10846
|
}
|
|
10524
10847
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
10525
|
-
const dir =
|
|
10526
|
-
if (!
|
|
10848
|
+
const dir = join19(stateDir, "passengers");
|
|
10849
|
+
if (!existsSync20(dir)) return [];
|
|
10527
10850
|
const suffix = "-sync.json";
|
|
10528
10851
|
return readdirSync5(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
10529
10852
|
}
|
|
@@ -10581,7 +10904,7 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
10581
10904
|
|
|
10582
10905
|
// src/daemon.ts
|
|
10583
10906
|
import { readdirSync as readdirSync6, statSync as statSync8 } from "fs";
|
|
10584
|
-
import { join as
|
|
10907
|
+
import { join as join20 } from "path";
|
|
10585
10908
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
10586
10909
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
10587
10910
|
var defaultSleep = (ms2) => new Promise((resolve5) => setTimeout(resolve5, ms2));
|
|
@@ -10620,7 +10943,7 @@ function latestMtime(path) {
|
|
|
10620
10943
|
if (!stat.isDirectory()) return 0;
|
|
10621
10944
|
let latest = stat.mtimeMs;
|
|
10622
10945
|
for (const entry of readdirSync6(path, { withFileTypes: true })) {
|
|
10623
|
-
latest = Math.max(latest, latestMtime(
|
|
10946
|
+
latest = Math.max(latest, latestMtime(join20(path, entry.name)));
|
|
10624
10947
|
}
|
|
10625
10948
|
return latest;
|
|
10626
10949
|
}
|
|
@@ -10906,7 +11229,7 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
10906
11229
|
}
|
|
10907
11230
|
|
|
10908
11231
|
// src/cli.ts
|
|
10909
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
11232
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname12(process.execPath) : resolve4(dirname12(fileURLToPath(import.meta.url)), "..");
|
|
10910
11233
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
10911
11234
|
var CYCLE_SECONDS2 = 120;
|
|
10912
11235
|
var STALE_CYCLES = 3;
|
|
@@ -11040,8 +11363,9 @@ function usage() {
|
|
|
11040
11363
|
crew log [route] tail the log
|
|
11041
11364
|
crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
|
|
11042
11365
|
crew connect resolve a workspace's ids into a crew.yaml block (--no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one)
|
|
11366
|
+
crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
|
|
11043
11367
|
crew agents sync [route] [--force] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona)
|
|
11044
|
-
crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook)
|
|
11368
|
+
crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook; --harness H adds which CLI it serves)
|
|
11045
11369
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
11046
11370
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
|
|
11047
11371
|
crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches
|
|
@@ -11080,7 +11404,7 @@ var value = (name) => {
|
|
|
11080
11404
|
var positional = argv.filter((a, i) => !a.startsWith("--") && !argv[i - 1]?.startsWith("--"));
|
|
11081
11405
|
var command = positional[0];
|
|
11082
11406
|
if (!command) usage();
|
|
11083
|
-
var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "install", "uninstall", "release", "merge", "deploy"]);
|
|
11407
|
+
var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "disconnect", "install", "uninstall", "release", "merge", "deploy"]);
|
|
11084
11408
|
var STATE_WRITING_SUBCOMMANDS = {
|
|
11085
11409
|
agents: /* @__PURE__ */ new Set(["sync"]),
|
|
11086
11410
|
skills: /* @__PURE__ */ new Set(["sync"])
|
|
@@ -11190,7 +11514,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
11190
11514
|
const releaseFleetWide = ["merge", "deploy", "release"].includes(command) && flag("fleet");
|
|
11191
11515
|
const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
|
|
11192
11516
|
const named = command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
|
|
11193
|
-
const fleetWide = command === "inbox" || command === "connect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || releaseFleetWide || command === "pause" || command === "resume" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1;
|
|
11517
|
+
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || releaseFleetWide || command === "pause" || command === "resume" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1;
|
|
11194
11518
|
let route;
|
|
11195
11519
|
try {
|
|
11196
11520
|
route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
|
|
@@ -11396,8 +11720,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
11396
11720
|
}
|
|
11397
11721
|
if (host === "linux" && hasSystemd) {
|
|
11398
11722
|
const label = labelFor(CREW_HOME, "run");
|
|
11399
|
-
const staleTimer =
|
|
11400
|
-
if (
|
|
11723
|
+
const staleTimer = join21(homedir6(), ".config", "systemd", "user", `${label}.timer`);
|
|
11724
|
+
if (existsSync21(staleTimer)) {
|
|
11401
11725
|
process.stdout.write(
|
|
11402
11726
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
11403
11727
|
`
|
|
@@ -11423,7 +11747,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
11423
11747
|
const tracker = new Tracker(c, cfg.ship);
|
|
11424
11748
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
11425
11749
|
for (const r of await resolvedRepos(c)) {
|
|
11426
|
-
if (!
|
|
11750
|
+
if (!existsSync21(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
11427
11751
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
11428
11752
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
11429
11753
|
}
|
|
@@ -11441,7 +11765,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
11441
11765
|
}
|
|
11442
11766
|
async function doctorFix(o) {
|
|
11443
11767
|
const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
|
|
11444
|
-
const unitExists = (p) =>
|
|
11768
|
+
const unitExists = (p) => existsSync21(p);
|
|
11445
11769
|
const crontab = o.host === "linux" ? spawnSync3("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
11446
11770
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
11447
11771
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -11458,7 +11782,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
11458
11782
|
for (const a of plan.actions) {
|
|
11459
11783
|
if (a.kind === "enable") {
|
|
11460
11784
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
11461
|
-
|
|
11785
|
+
writeFileSync10(cfg.configFile, planEnable(readFileSync15(cfg.configFile, "utf8"), a.route));
|
|
11462
11786
|
enabledSound.add(a.route);
|
|
11463
11787
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
11464
11788
|
} else {
|
|
@@ -12355,7 +12679,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12355
12679
|
process.exit(1);
|
|
12356
12680
|
}
|
|
12357
12681
|
requireArmed("run the daemon loop");
|
|
12358
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
12682
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join21(CREW_HOME, "src"), join21(CREW_HOME, "dist"), join21(CREW_HOME, "bin")];
|
|
12359
12683
|
const spawnBreaker = new SpawnBreaker();
|
|
12360
12684
|
const stopReason = await runDaemonLoop({
|
|
12361
12685
|
runPass: () => runOnePass({
|
|
@@ -12441,7 +12765,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12441
12765
|
}
|
|
12442
12766
|
try {
|
|
12443
12767
|
const prompt = await currentPersonaPrompt(promptTarget, role, { userAgent: cfg.ship.userAgent });
|
|
12444
|
-
process.stdout.write(prompt);
|
|
12768
|
+
process.stdout.write(prompt + (role === "pair" ? harnessPromptNote(value("harness")) : ""));
|
|
12445
12769
|
} catch (e) {
|
|
12446
12770
|
if (e instanceof AgentsSyncError) {
|
|
12447
12771
|
process.stderr.write(`crew agents prompt: ${e.message}
|
|
@@ -12504,11 +12828,11 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12504
12828
|
`);
|
|
12505
12829
|
if (!dryRun) {
|
|
12506
12830
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
12507
|
-
const raw =
|
|
12831
|
+
const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
|
|
12508
12832
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
12509
12833
|
raw.agentSkills = skillResult.agentSkills;
|
|
12510
|
-
|
|
12511
|
-
|
|
12834
|
+
mkdirSync12(dirOf(resolvedPath), { recursive: true });
|
|
12835
|
+
writeFileSync10(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
12512
12836
|
`);
|
|
12513
12837
|
}
|
|
12514
12838
|
if (personaResult.outcomes.some((o) => o.action === "diverged") || skillResult.outcomes.some((o) => o.action === "diverged")) {
|
|
@@ -12553,10 +12877,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12553
12877
|
`);
|
|
12554
12878
|
if (!dryRun) {
|
|
12555
12879
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
12556
|
-
const raw =
|
|
12880
|
+
const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
|
|
12557
12881
|
raw.agentSkills = result.agentSkills;
|
|
12558
|
-
|
|
12559
|
-
|
|
12882
|
+
mkdirSync12(dirOf(resolvedPath), { recursive: true });
|
|
12883
|
+
writeFileSync10(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
12560
12884
|
`);
|
|
12561
12885
|
}
|
|
12562
12886
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -12569,14 +12893,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12569
12893
|
process.exit(2);
|
|
12570
12894
|
};
|
|
12571
12895
|
if (sub !== "add" && sub !== "list") usage2();
|
|
12572
|
-
const configText =
|
|
12896
|
+
const configText = readFileSync15(cfg.configFile, "utf8");
|
|
12573
12897
|
let routeName = positional[2];
|
|
12574
12898
|
let addPath = ".";
|
|
12575
12899
|
let addName;
|
|
12576
12900
|
if (sub === "list" && !routeName) usage2();
|
|
12577
12901
|
if (sub === "add") {
|
|
12578
12902
|
try {
|
|
12579
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
12903
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync21(resolvePath(p.replace(/^~(?=\/|$)/, homedir6()))));
|
|
12580
12904
|
routeName = a.route;
|
|
12581
12905
|
addPath = a.path;
|
|
12582
12906
|
addName = a.name;
|
|
@@ -12591,15 +12915,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12591
12915
|
}
|
|
12592
12916
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
12593
12917
|
let resolvedRemotes;
|
|
12594
|
-
if (
|
|
12918
|
+
if (existsSync21(resolvedFile)) {
|
|
12595
12919
|
try {
|
|
12596
|
-
resolvedRemotes = JSON.parse(
|
|
12920
|
+
resolvedRemotes = JSON.parse(readFileSync15(resolvedFile, "utf8"));
|
|
12597
12921
|
} catch {
|
|
12598
12922
|
}
|
|
12599
12923
|
}
|
|
12600
12924
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
12601
12925
|
const text = configText;
|
|
12602
|
-
const base =
|
|
12926
|
+
const base = dirname12(cfg.configFile);
|
|
12603
12927
|
try {
|
|
12604
12928
|
if (sub === "list") {
|
|
12605
12929
|
const repos = configuredRepos(text, routeName, base);
|
|
@@ -12608,14 +12932,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12608
12932
|
for (const r of repos) {
|
|
12609
12933
|
const origin2 = originOf(r.dir);
|
|
12610
12934
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
12611
|
-
const note = !
|
|
12935
|
+
const note = !existsSync21(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
12612
12936
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
12613
12937
|
`);
|
|
12614
12938
|
}
|
|
12615
12939
|
break;
|
|
12616
12940
|
}
|
|
12617
|
-
const target = resolvePath(addPath.replace(/^~(?=\/|$)/,
|
|
12618
|
-
const top =
|
|
12941
|
+
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir6()));
|
|
12942
|
+
const top = existsSync21(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
12619
12943
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
12620
12944
|
const origin = originOf(top);
|
|
12621
12945
|
const plan = planRepoAdd({
|
|
@@ -12663,15 +12987,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12663
12987
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
12664
12988
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
12665
12989
|
`);
|
|
12666
|
-
if (
|
|
12667
|
-
const raw = JSON.parse(
|
|
12990
|
+
if (existsSync21(resolvedFile)) {
|
|
12991
|
+
const raw = JSON.parse(readFileSync15(resolvedFile, "utf8"));
|
|
12668
12992
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
12669
12993
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
12670
|
-
|
|
12994
|
+
writeFileSync10(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
12671
12995
|
`);
|
|
12672
12996
|
}
|
|
12673
12997
|
}
|
|
12674
|
-
|
|
12998
|
+
writeFileSync10(cfg.configFile, plan.text);
|
|
12675
12999
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
12676
13000
|
`);
|
|
12677
13001
|
} catch (e) {
|
|
@@ -12910,10 +13234,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
12910
13234
|
operator = await pickOperator(found.holds);
|
|
12911
13235
|
}
|
|
12912
13236
|
let previous;
|
|
12913
|
-
const hadPreviousFile =
|
|
13237
|
+
const hadPreviousFile = existsSync21(resolvedPath);
|
|
12914
13238
|
if (hadPreviousFile) {
|
|
12915
13239
|
try {
|
|
12916
|
-
previous = JSON.parse(
|
|
13240
|
+
previous = JSON.parse(readFileSync15(resolvedPath, "utf8"));
|
|
12917
13241
|
} catch {
|
|
12918
13242
|
}
|
|
12919
13243
|
}
|
|
@@ -12974,10 +13298,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
12974
13298
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
12975
13299
|
`);
|
|
12976
13300
|
} else {
|
|
12977
|
-
|
|
13301
|
+
mkdirSync12(dirname12(resolvedPath), { recursive: true });
|
|
12978
13302
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
12979
13303
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
12980
|
-
|
|
13304
|
+
writeFileSync10(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
12981
13305
|
`);
|
|
12982
13306
|
renameSync2(tmpPath, resolvedPath);
|
|
12983
13307
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -12991,8 +13315,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
12991
13315
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
12992
13316
|
`);
|
|
12993
13317
|
} else {
|
|
12994
|
-
|
|
12995
|
-
|
|
13318
|
+
mkdirSync12(dirname12(apiKeyFile), { recursive: true });
|
|
13319
|
+
writeFileSync10(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
12996
13320
|
`, { mode: 384 });
|
|
12997
13321
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
12998
13322
|
`);
|
|
@@ -13001,7 +13325,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13001
13325
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
13002
13326
|
const wizard = firstRun;
|
|
13003
13327
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
13004
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
13328
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join21(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
13005
13329
|
const routeBlock = renderConnection(
|
|
13006
13330
|
found,
|
|
13007
13331
|
route2,
|
|
@@ -13029,6 +13353,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13029
13353
|
if (seen.has(id)) continue;
|
|
13030
13354
|
seen.add(id);
|
|
13031
13355
|
process.stderr.write(`${adapterFor(tc.harness).ensureMcp({ bin: tc.bin, url: mcpUrlFor(baseUrl), key: apiKey, replace, dryRun })}
|
|
13356
|
+
`);
|
|
13357
|
+
}
|
|
13358
|
+
for (const h of shipPair(cfg.ship).filter((x) => x !== "claude")) {
|
|
13359
|
+
process.stderr.write(`${pairInstallerFor(h).installPair({ url: mcpUrlFor(baseUrl), replace, dryRun })}
|
|
13032
13360
|
`);
|
|
13033
13361
|
}
|
|
13034
13362
|
}
|
|
@@ -13482,6 +13810,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13482
13810
|
`);
|
|
13483
13811
|
break;
|
|
13484
13812
|
}
|
|
13813
|
+
case "disconnect": {
|
|
13814
|
+
const url = mcpUrlFor(value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL);
|
|
13815
|
+
for (const h of shipPair(cfg.ship)) {
|
|
13816
|
+
process.stderr.write(`${pairInstallerFor(h).removePair({ url, bin: cfg.ship.agent.bin, dryRun })}
|
|
13817
|
+
`);
|
|
13818
|
+
}
|
|
13819
|
+
break;
|
|
13820
|
+
}
|
|
13485
13821
|
case "install": {
|
|
13486
13822
|
if (!await installScheduler({ replace: flag("replace") })) process.exit(2);
|
|
13487
13823
|
break;
|
|
@@ -13521,7 +13857,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13521
13857
|
}
|
|
13522
13858
|
case "log": {
|
|
13523
13859
|
try {
|
|
13524
|
-
const text =
|
|
13860
|
+
const text = readFileSync15(cfg.ship.logFile, "utf8").split("\n");
|
|
13525
13861
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
13526
13862
|
`);
|
|
13527
13863
|
} catch {
|
|
@@ -13729,7 +14065,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
13729
14065
|
);
|
|
13730
14066
|
try {
|
|
13731
14067
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
13732
|
-
const evSize =
|
|
14068
|
+
const evSize = existsSync21(evFile) ? statSync9(evFile).size : 0;
|
|
13733
14069
|
process.stdout.write(
|
|
13734
14070
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
13735
14071
|
` : "events log: ok\n"
|
|
@@ -13889,6 +14225,13 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
13889
14225
|
);
|
|
13890
14226
|
}
|
|
13891
14227
|
}
|
|
14228
|
+
for (const h of shipPair(cfg.ship).filter((x) => x !== "claude")) {
|
|
14229
|
+
const inst = pairInstallerFor(h);
|
|
14230
|
+
const v = inst.verifyPair({ url: mcpUrlFor(route.baseUrl) });
|
|
14231
|
+
process.stdout.write(`pair ${h}:`.padEnd(19) + (v.ok ? ` installed (${v.detail})
|
|
14232
|
+
` : ` NOT installed \u2014 ${v.detail}; run crew connect
|
|
14233
|
+
`));
|
|
14234
|
+
}
|
|
13892
14235
|
const shipVendors = new Set(shipToolchains(cfg.ship).map(toolchainVendor));
|
|
13893
14236
|
if (route.apiKey && route.resolved) {
|
|
13894
14237
|
let flagged = 0;
|