@tablation/crew 0.30.1 → 0.32.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 +618 -264
- 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",
|
|
@@ -365,7 +385,7 @@ var claudeAdapter = {
|
|
|
365
385
|
})] : [],
|
|
366
386
|
...spec.args
|
|
367
387
|
];
|
|
368
|
-
return spec.launcher ? { bin: spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
|
|
388
|
+
return spec.launcher ? { bin: spec.launcherBin ?? spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
|
|
369
389
|
},
|
|
370
390
|
runEnv(spec) {
|
|
371
391
|
return {
|
|
@@ -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
|
|
@@ -426,9 +730,15 @@ function resolveToolchain(ship, route) {
|
|
|
426
730
|
return found;
|
|
427
731
|
}
|
|
428
732
|
function toolchainVendor(spec) {
|
|
429
|
-
if (
|
|
733
|
+
if (isOriLauncher(spec)) return "openrouter";
|
|
430
734
|
return adapterFor(spec.harness).vendor;
|
|
431
735
|
}
|
|
736
|
+
function isOriLauncher(spec) {
|
|
737
|
+
return !!spec.launcher && basename(spec.launcher) === "ori";
|
|
738
|
+
}
|
|
739
|
+
function toolchainProvider(spec) {
|
|
740
|
+
return isOriLauncher(spec) ? "openrouter" : adapterFor(spec.harness).provider(spec);
|
|
741
|
+
}
|
|
432
742
|
function resolveSeat(ship, route, role, persona = {}) {
|
|
433
743
|
const warnings = [];
|
|
434
744
|
const base = resolveToolchain(ship, route);
|
|
@@ -512,8 +822,8 @@ function explain(ship, required) {
|
|
|
512
822
|
}
|
|
513
823
|
|
|
514
824
|
// src/repo-config.ts
|
|
515
|
-
import { readFileSync as
|
|
516
|
-
import { join as
|
|
825
|
+
import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
|
|
826
|
+
import { join as join4, basename as basename2 } from "path";
|
|
517
827
|
import { parse } from "yaml";
|
|
518
828
|
var SPEC_VERSION = 1;
|
|
519
829
|
var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
|
|
@@ -586,8 +896,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
|
|
|
586
896
|
var PROVIDERS = ["github", "buildkite", "other", "none"];
|
|
587
897
|
function findRepoConfig(dir) {
|
|
588
898
|
for (const name of REPO_CONFIG_FILENAMES) {
|
|
589
|
-
const p =
|
|
590
|
-
if (
|
|
899
|
+
const p = join4(dir, name);
|
|
900
|
+
if (existsSync3(p)) return p;
|
|
591
901
|
}
|
|
592
902
|
return null;
|
|
593
903
|
}
|
|
@@ -817,7 +1127,7 @@ function parseRepoConfig(text, file) {
|
|
|
817
1127
|
function loadRepoConfig(dir) {
|
|
818
1128
|
const file = findRepoConfig(dir);
|
|
819
1129
|
if (!file) return null;
|
|
820
|
-
return parseRepoConfig(
|
|
1130
|
+
return parseRepoConfig(readFileSync4(file, "utf8"), file);
|
|
821
1131
|
}
|
|
822
1132
|
function hookLabel(cfg, hook2) {
|
|
823
1133
|
return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
|
|
@@ -1084,6 +1394,7 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
|
|
|
1084
1394
|
"agent",
|
|
1085
1395
|
"toolchains",
|
|
1086
1396
|
"toolchain",
|
|
1397
|
+
"pair",
|
|
1087
1398
|
"shell",
|
|
1088
1399
|
"extraPath",
|
|
1089
1400
|
"useNvm",
|
|
@@ -1228,16 +1539,16 @@ function mergeRouteRelease(route, override) {
|
|
|
1228
1539
|
};
|
|
1229
1540
|
}
|
|
1230
1541
|
function expand(p, base) {
|
|
1231
|
-
let out = p.startsWith("~") ? p.replace(/^~/,
|
|
1232
|
-
out = out.replace(/\$\{?HOME\}?/g,
|
|
1542
|
+
let out = p.startsWith("~") ? p.replace(/^~/, homedir3()) : p;
|
|
1543
|
+
out = out.replace(/\$\{?HOME\}?/g, homedir3());
|
|
1233
1544
|
return isAbsolute(out) ? out : resolve(base, out);
|
|
1234
1545
|
}
|
|
1235
1546
|
function resolvePromptsDir(promptSet, crewHome, base) {
|
|
1236
|
-
if (!promptSet) return
|
|
1547
|
+
if (!promptSet) return join5(crewHome, "prompts", "default");
|
|
1237
1548
|
if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
|
|
1238
1549
|
return expand(promptSet, base);
|
|
1239
1550
|
}
|
|
1240
|
-
return
|
|
1551
|
+
return join5(crewHome, "prompts", promptSet);
|
|
1241
1552
|
}
|
|
1242
1553
|
var Missing = class {
|
|
1243
1554
|
paths = [];
|
|
@@ -1261,7 +1572,7 @@ var Missing = class {
|
|
|
1261
1572
|
};
|
|
1262
1573
|
function crewVersion(crewHome) {
|
|
1263
1574
|
try {
|
|
1264
|
-
const pkg = JSON.parse(
|
|
1575
|
+
const pkg = JSON.parse(readFileSync5(join5(crewHome, "package.json"), "utf8"));
|
|
1265
1576
|
return pkg.version ?? "0.0.0";
|
|
1266
1577
|
} catch {
|
|
1267
1578
|
return "0.0.0";
|
|
@@ -1271,26 +1582,26 @@ function configSearchPath(crewHome) {
|
|
|
1271
1582
|
if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
|
|
1272
1583
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
1273
1584
|
return [
|
|
1274
|
-
...xdg ? [
|
|
1275
|
-
|
|
1585
|
+
...xdg ? [join5(xdg, "crew", "crew.yaml")] : [],
|
|
1586
|
+
join5(homedir3(), ".config", "crew", "crew.yaml"),
|
|
1276
1587
|
resolve(crewHome, "crew.yaml")
|
|
1277
1588
|
];
|
|
1278
1589
|
}
|
|
1279
1590
|
function findConfigFile(crewHome) {
|
|
1280
1591
|
const candidates = configSearchPath(crewHome);
|
|
1281
|
-
return candidates.find((c) =>
|
|
1592
|
+
return candidates.find((c) => existsSync4(c)) ?? candidates[candidates.length - 1];
|
|
1282
1593
|
}
|
|
1283
|
-
var defaultConfigPath = () =>
|
|
1594
|
+
var defaultConfigPath = () => join5(process.env.XDG_CONFIG_HOME ?? join5(homedir3(), ".config"), "crew", "crew.yaml");
|
|
1284
1595
|
function routeSlug(route) {
|
|
1285
1596
|
return route.replace(/\//g, "-");
|
|
1286
1597
|
}
|
|
1287
1598
|
function resolvedPathFor(stateDir, route) {
|
|
1288
1599
|
const [workspace, project] = splitRoute(route);
|
|
1289
|
-
return
|
|
1600
|
+
return join5(stateDir, "resolved", workspace, `${project}.json`);
|
|
1290
1601
|
}
|
|
1291
1602
|
function apiKeyPathFor(stateDir, route) {
|
|
1292
1603
|
const [workspace, project] = splitRoute(route);
|
|
1293
|
-
return
|
|
1604
|
+
return join5(stateDir, "keys", workspace, `${project}.env`);
|
|
1294
1605
|
}
|
|
1295
1606
|
function splitRoute(route) {
|
|
1296
1607
|
const parts = route.split("/");
|
|
@@ -1301,9 +1612,9 @@ function splitRoute(route) {
|
|
|
1301
1612
|
}
|
|
1302
1613
|
function readResolvedFile(stateDir, route) {
|
|
1303
1614
|
const path = resolvedPathFor(stateDir, route);
|
|
1304
|
-
if (!
|
|
1615
|
+
if (!existsSync4(path)) return void 0;
|
|
1305
1616
|
try {
|
|
1306
|
-
return JSON.parse(
|
|
1617
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
1307
1618
|
} catch (e) {
|
|
1308
1619
|
throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
|
|
1309
1620
|
}
|
|
@@ -1359,23 +1670,23 @@ function parseResolved(raw, m, where) {
|
|
|
1359
1670
|
}
|
|
1360
1671
|
function loadConfig(crewHome, configFile, opts = {}) {
|
|
1361
1672
|
const file = configFile ?? findConfigFile(crewHome);
|
|
1362
|
-
if (opts.text === void 0 && !
|
|
1673
|
+
if (opts.text === void 0 && !existsSync4(file)) {
|
|
1363
1674
|
throw new ConfigError(
|
|
1364
1675
|
`no config found. Looked in:
|
|
1365
1676
|
` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
|
|
1366
1677
|
|
|
1367
1678
|
Create the first of those:
|
|
1368
|
-
mkdir -p ${
|
|
1679
|
+
mkdir -p ${dirname3(defaultConfigPath())} && cp ${join5(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
|
|
1369
1680
|
);
|
|
1370
1681
|
}
|
|
1371
1682
|
let raw;
|
|
1372
1683
|
try {
|
|
1373
|
-
raw = parse2(opts.text ??
|
|
1684
|
+
raw = parse2(opts.text ?? readFileSync5(file, "utf8"));
|
|
1374
1685
|
} catch (e) {
|
|
1375
1686
|
throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
|
|
1376
1687
|
}
|
|
1377
1688
|
if (!raw || typeof raw !== "object") throw new ConfigError(`${file} is empty or not a mapping`);
|
|
1378
|
-
const base =
|
|
1689
|
+
const base = dirname3(file);
|
|
1379
1690
|
const missing = new Missing();
|
|
1380
1691
|
rejectUnknownKeys(raw, /* @__PURE__ */ new Set(["ship", "routes"]), "top-level", file);
|
|
1381
1692
|
const shipRaw = raw.ship ?? {};
|
|
@@ -1402,6 +1713,7 @@ Create the first of those:
|
|
|
1402
1713
|
}
|
|
1403
1714
|
const toolchains = parseToolchains(shipRaw.toolchains, base, missing, file);
|
|
1404
1715
|
const toolchainNames = /* @__PURE__ */ new Set([LEGACY_TOOLCHAIN, ...toolchains.map((x) => x.name)]);
|
|
1716
|
+
const pair = parsePair(shipRaw.pair, missing);
|
|
1405
1717
|
const shipToolchain = shipRaw.toolchain === void 0 ? void 0 : String(shipRaw.toolchain);
|
|
1406
1718
|
if (shipToolchain !== void 0 && !toolchainNames.has(shipToolchain)) {
|
|
1407
1719
|
missing.add(`ship.toolchain "${shipToolchain}" is not defined (have: ${[...toolchainNames].join(", ")})`);
|
|
@@ -1441,12 +1753,13 @@ Create the first of those:
|
|
|
1441
1753
|
},
|
|
1442
1754
|
...toolchains.length > 0 ? { toolchains } : {},
|
|
1443
1755
|
...shipToolchain !== void 0 ? { toolchain: shipToolchain } : {},
|
|
1756
|
+
...pair ? { pair } : {},
|
|
1444
1757
|
shell: shipRaw.shell,
|
|
1445
1758
|
extraPath: shipRaw.extraPath,
|
|
1446
1759
|
useNvm: shipRaw.useNvm !== false,
|
|
1447
1760
|
nvmSh: shipRaw.nvmSh,
|
|
1448
1761
|
stateDir,
|
|
1449
|
-
logFile: expand(shipRaw.logFile ??
|
|
1762
|
+
logFile: expand(shipRaw.logFile ?? join5(tmpdir(), "crew.log"), base),
|
|
1450
1763
|
userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
|
|
1451
1764
|
maxConcurrentAgents,
|
|
1452
1765
|
streamRetentionDays,
|
|
@@ -1467,7 +1780,23 @@ Create the first of those:
|
|
|
1467
1780
|
function expandCommand(cmd, base) {
|
|
1468
1781
|
return cmd.includes("/") || cmd.startsWith("~") ? expand(cmd, base) : cmd;
|
|
1469
1782
|
}
|
|
1470
|
-
var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "model", "tiers", "args", "env", "maxThinkingTokens"]);
|
|
1783
|
+
var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "launcherBin", "model", "tiers", "args", "env", "maxThinkingTokens"]);
|
|
1784
|
+
function parsePair(raw, missing) {
|
|
1785
|
+
if (raw === void 0) return void 0;
|
|
1786
|
+
if (!Array.isArray(raw)) {
|
|
1787
|
+
missing.add("ship.pair must be a list of harness names");
|
|
1788
|
+
return void 0;
|
|
1789
|
+
}
|
|
1790
|
+
const out = [];
|
|
1791
|
+
for (const h of raw.map(String)) {
|
|
1792
|
+
if (!isPairHarness(h)) missing.add(`ship.pair "${h}" is not a harness Pair can be installed for (have: ${PAIR_HARNESS_NAMES.join(", ")})`);
|
|
1793
|
+
else if (!out.includes(h)) out.push(h);
|
|
1794
|
+
}
|
|
1795
|
+
return out;
|
|
1796
|
+
}
|
|
1797
|
+
function shipPair(ship) {
|
|
1798
|
+
return ship.pair ?? DEFAULT_PAIR;
|
|
1799
|
+
}
|
|
1471
1800
|
function parseToolchains(raw, base, missing, file) {
|
|
1472
1801
|
if (raw === void 0 || raw === null) return [];
|
|
1473
1802
|
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -1513,6 +1842,7 @@ function parseToolchains(raw, base, missing, file) {
|
|
|
1513
1842
|
harness,
|
|
1514
1843
|
bin: expandCommand(String(entryRaw.bin ?? adapter.defaultBin), base),
|
|
1515
1844
|
...entryRaw.launcher ? { launcher: expandCommand(String(entryRaw.launcher), base) } : {},
|
|
1845
|
+
...entryRaw.launcher && entryRaw.launcherBin ? { launcherBin: expandCommand(String(entryRaw.launcherBin), base) } : {},
|
|
1516
1846
|
model: String(entryRaw.model ?? tiers.standard?.model ?? adapter.defaultModel),
|
|
1517
1847
|
tiers,
|
|
1518
1848
|
args: Array.isArray(args) ? args.map(String) : [],
|
|
@@ -1644,8 +1974,8 @@ function repoIdForName(r, name) {
|
|
|
1644
1974
|
}
|
|
1645
1975
|
function resolveApiKey(r) {
|
|
1646
1976
|
if (r.apiKey) return r.apiKey;
|
|
1647
|
-
if (r.apiKeyFile && r.apiKeyVar &&
|
|
1648
|
-
for (const line of
|
|
1977
|
+
if (r.apiKeyFile && r.apiKeyVar && existsSync4(r.apiKeyFile)) {
|
|
1978
|
+
for (const line of readFileSync5(r.apiKeyFile, "utf8").split("\n")) {
|
|
1649
1979
|
const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
|
|
1650
1980
|
if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
1651
1981
|
}
|
|
@@ -1708,7 +2038,7 @@ function configuredMembers(rt) {
|
|
|
1708
2038
|
return out;
|
|
1709
2039
|
}
|
|
1710
2040
|
function defaultRepoDir(basePath, workspace, repoName) {
|
|
1711
|
-
return
|
|
2041
|
+
return join5(basePath, workspace, repoName);
|
|
1712
2042
|
}
|
|
1713
2043
|
function reposOf(r) {
|
|
1714
2044
|
const named = Object.entries(r.repos);
|
|
@@ -1732,7 +2062,7 @@ function passengerRepoTargets(r) {
|
|
|
1732
2062
|
return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
|
|
1733
2063
|
}
|
|
1734
2064
|
function passengerCheckoutDir(basePath, workspace, repoName) {
|
|
1735
|
-
return
|
|
2065
|
+
return join5(basePath, workspace, ".passenger-checkouts", repoName);
|
|
1736
2066
|
}
|
|
1737
2067
|
function passengerCheckoutTargets(r) {
|
|
1738
2068
|
if (!r.hostPassengers) return [];
|
|
@@ -1864,17 +2194,17 @@ function rosterMarkdown(roster, meId) {
|
|
|
1864
2194
|
}
|
|
1865
2195
|
|
|
1866
2196
|
// src/state.ts
|
|
1867
|
-
import { readFileSync as
|
|
1868
|
-
import { join as
|
|
2197
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, rmSync as rmSync3, statSync as statSync2 } from "fs";
|
|
2198
|
+
import { join as join6 } from "path";
|
|
1869
2199
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
1870
2200
|
var State = class _State {
|
|
1871
2201
|
dir;
|
|
1872
2202
|
constructor(dir) {
|
|
1873
2203
|
this.dir = dir;
|
|
1874
|
-
|
|
2204
|
+
mkdirSync3(dir, { recursive: true });
|
|
1875
2205
|
}
|
|
1876
2206
|
path(...parts) {
|
|
1877
|
-
return
|
|
2207
|
+
return join6(this.dir, ...parts);
|
|
1878
2208
|
}
|
|
1879
2209
|
/**
|
|
1880
2210
|
* The timestamp every "new since last poll" check is measured against.
|
|
@@ -1885,14 +2215,14 @@ var State = class _State {
|
|
|
1885
2215
|
*/
|
|
1886
2216
|
watermark() {
|
|
1887
2217
|
try {
|
|
1888
|
-
const v =
|
|
2218
|
+
const v = readFileSync6(this.path(".poll-watermark"), "utf8").trim();
|
|
1889
2219
|
return v || EPOCH;
|
|
1890
2220
|
} catch {
|
|
1891
2221
|
return EPOCH;
|
|
1892
2222
|
}
|
|
1893
2223
|
}
|
|
1894
2224
|
setWatermark(at) {
|
|
1895
|
-
|
|
2225
|
+
writeFileSync2(this.path(".poll-watermark"), `${at}
|
|
1896
2226
|
`);
|
|
1897
2227
|
}
|
|
1898
2228
|
/**
|
|
@@ -1910,19 +2240,19 @@ var State = class _State {
|
|
|
1910
2240
|
}
|
|
1911
2241
|
/** Whole-crew pause, and the per-role sentinels one level down. */
|
|
1912
2242
|
isPaused() {
|
|
1913
|
-
return
|
|
2243
|
+
return existsSync5(this.path(".crew-paused"));
|
|
1914
2244
|
}
|
|
1915
2245
|
isRolePaused(role) {
|
|
1916
|
-
return
|
|
2246
|
+
return existsSync5(this.path(`.role-paused-${role}`));
|
|
1917
2247
|
}
|
|
1918
2248
|
pausedRoles(roles) {
|
|
1919
2249
|
return new Set(roles.filter((r) => this.isRolePaused(r)));
|
|
1920
2250
|
}
|
|
1921
2251
|
pause(role) {
|
|
1922
|
-
|
|
2252
|
+
writeFileSync2(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
|
|
1923
2253
|
}
|
|
1924
2254
|
resume(role) {
|
|
1925
|
-
|
|
2255
|
+
rmSync3(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
|
|
1926
2256
|
}
|
|
1927
2257
|
/**
|
|
1928
2258
|
* The release phase (merge, deploy, release) has its own sentinel, one
|
|
@@ -1930,11 +2260,11 @@ var State = class _State {
|
|
|
1930
2260
|
* while agents keep working.
|
|
1931
2261
|
*/
|
|
1932
2262
|
isReleasePaused() {
|
|
1933
|
-
return
|
|
2263
|
+
return existsSync5(this.path(".release-paused"));
|
|
1934
2264
|
}
|
|
1935
2265
|
setReleasePaused(paused) {
|
|
1936
|
-
if (paused)
|
|
1937
|
-
else
|
|
2266
|
+
if (paused) writeFileSync2(this.path(".release-paused"), "");
|
|
2267
|
+
else rmSync3(this.path(".release-paused"), { force: true });
|
|
1938
2268
|
}
|
|
1939
2269
|
/**
|
|
1940
2270
|
* Why a release cycle must not run right now, or null when it may. Every
|
|
@@ -1971,7 +2301,7 @@ var State = class _State {
|
|
|
1971
2301
|
const name = _State.safe(rawName);
|
|
1972
2302
|
const file = this.path(`.${name}.lock`);
|
|
1973
2303
|
try {
|
|
1974
|
-
const existing = Number.parseInt(
|
|
2304
|
+
const existing = Number.parseInt(readFileSync6(file, "utf8").trim(), 10);
|
|
1975
2305
|
if (Number.isFinite(existing) && existing > 0) {
|
|
1976
2306
|
try {
|
|
1977
2307
|
process.kill(existing, 0);
|
|
@@ -1981,13 +2311,13 @@ var State = class _State {
|
|
|
1981
2311
|
}
|
|
1982
2312
|
} catch {
|
|
1983
2313
|
}
|
|
1984
|
-
|
|
2314
|
+
writeFileSync2(file, `${process.pid}
|
|
1985
2315
|
`);
|
|
1986
2316
|
return {
|
|
1987
2317
|
ok: true,
|
|
1988
2318
|
release: () => {
|
|
1989
2319
|
try {
|
|
1990
|
-
|
|
2320
|
+
rmSync3(file, { force: true });
|
|
1991
2321
|
} catch {
|
|
1992
2322
|
}
|
|
1993
2323
|
}
|
|
@@ -2065,7 +2395,7 @@ var State = class _State {
|
|
|
2065
2395
|
const heldReported = `.release-held-${suffix}`;
|
|
2066
2396
|
const read = (f) => {
|
|
2067
2397
|
try {
|
|
2068
|
-
return
|
|
2398
|
+
return readFileSync6(this.path(f), "utf8").trim() || null;
|
|
2069
2399
|
} catch {
|
|
2070
2400
|
return null;
|
|
2071
2401
|
}
|
|
@@ -2074,12 +2404,12 @@ var State = class _State {
|
|
|
2074
2404
|
blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
|
|
2075
2405
|
noteBlocked: () => {
|
|
2076
2406
|
const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
|
|
2077
|
-
|
|
2407
|
+
writeFileSync2(this.path(blocked), `${n}
|
|
2078
2408
|
`);
|
|
2079
2409
|
return n;
|
|
2080
2410
|
},
|
|
2081
2411
|
clearBlocked: () => {
|
|
2082
|
-
|
|
2412
|
+
rmSync3(this.path(blocked), { force: true });
|
|
2083
2413
|
},
|
|
2084
2414
|
/**
|
|
2085
2415
|
* The commit a deploy last failed on.
|
|
@@ -2091,11 +2421,11 @@ var State = class _State {
|
|
|
2091
2421
|
*/
|
|
2092
2422
|
deployFailedSha: () => read(failed),
|
|
2093
2423
|
noteDeployFailed: (sha) => {
|
|
2094
|
-
|
|
2424
|
+
writeFileSync2(this.path(failed), `${sha}
|
|
2095
2425
|
`);
|
|
2096
2426
|
},
|
|
2097
2427
|
clearDeployFailed: () => {
|
|
2098
|
-
|
|
2428
|
+
rmSync3(this.path(failed), { force: true });
|
|
2099
2429
|
},
|
|
2100
2430
|
/**
|
|
2101
2431
|
* Consecutive test-gate failures on the same head (CREW-1368). A failure
|
|
@@ -2105,31 +2435,31 @@ var State = class _State {
|
|
|
2105
2435
|
noteTestGateFailed: (sha) => {
|
|
2106
2436
|
const [prev, n] = (read(gateFailed) ?? "").split(" ");
|
|
2107
2437
|
const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
|
|
2108
|
-
|
|
2438
|
+
writeFileSync2(this.path(gateFailed), `${sha} ${next}
|
|
2109
2439
|
`);
|
|
2110
2440
|
return next;
|
|
2111
2441
|
},
|
|
2112
2442
|
clearTestGateFailed: () => {
|
|
2113
|
-
|
|
2443
|
+
rmSync3(this.path(gateFailed), { force: true });
|
|
2114
2444
|
},
|
|
2115
2445
|
/** Whether the board was already told about this head's red gate. */
|
|
2116
2446
|
heldReported: () => {
|
|
2117
2447
|
try {
|
|
2118
|
-
return
|
|
2448
|
+
return readFileSync6(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
|
|
2119
2449
|
} catch {
|
|
2120
2450
|
return null;
|
|
2121
2451
|
}
|
|
2122
2452
|
},
|
|
2123
2453
|
noteHeldReported: (signature) => {
|
|
2124
|
-
|
|
2454
|
+
writeFileSync2(this.path(heldReported), `${signature}
|
|
2125
2455
|
`);
|
|
2126
2456
|
},
|
|
2127
2457
|
clearHeldReported: () => {
|
|
2128
|
-
|
|
2458
|
+
rmSync3(this.path(heldReported), { force: true });
|
|
2129
2459
|
},
|
|
2130
2460
|
testGateReported: (sha) => read(gateReported) === sha,
|
|
2131
2461
|
noteTestGateReported: (sha) => {
|
|
2132
|
-
|
|
2462
|
+
writeFileSync2(this.path(gateReported), `${sha}
|
|
2133
2463
|
`);
|
|
2134
2464
|
}
|
|
2135
2465
|
};
|
|
@@ -2152,13 +2482,13 @@ var State = class _State {
|
|
|
2152
2482
|
const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
|
|
2153
2483
|
const read = (f) => {
|
|
2154
2484
|
try {
|
|
2155
|
-
return
|
|
2485
|
+
return readFileSync6(this.path(f), "utf8").trim() || null;
|
|
2156
2486
|
} catch {
|
|
2157
2487
|
return null;
|
|
2158
2488
|
}
|
|
2159
2489
|
};
|
|
2160
2490
|
const clear2 = () => {
|
|
2161
|
-
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile])
|
|
2491
|
+
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync3(this.path(f), { force: true });
|
|
2162
2492
|
};
|
|
2163
2493
|
return {
|
|
2164
2494
|
/** Consecutive cycles this route had actionable work and did not win. */
|
|
@@ -2185,18 +2515,18 @@ var State = class _State {
|
|
|
2185
2515
|
clear2();
|
|
2186
2516
|
return;
|
|
2187
2517
|
}
|
|
2188
|
-
|
|
2518
|
+
writeFileSync2(this.path(streakFile), `${this.readInt(streakFile) + 1}
|
|
2189
2519
|
`);
|
|
2190
2520
|
const prior = read(ticketFile);
|
|
2191
2521
|
if (topTicket && topTicket === prior) {
|
|
2192
|
-
|
|
2522
|
+
writeFileSync2(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
|
|
2193
2523
|
`);
|
|
2194
2524
|
} else if (topTicket) {
|
|
2195
|
-
|
|
2525
|
+
writeFileSync2(this.path(ticketFile), `${topTicket}
|
|
2196
2526
|
`);
|
|
2197
|
-
|
|
2527
|
+
writeFileSync2(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2198
2528
|
`);
|
|
2199
|
-
|
|
2529
|
+
writeFileSync2(this.path(ticketStreakFile), "1\n");
|
|
2200
2530
|
}
|
|
2201
2531
|
}
|
|
2202
2532
|
};
|
|
@@ -2217,14 +2547,14 @@ var State = class _State {
|
|
|
2217
2547
|
return {
|
|
2218
2548
|
previous: () => {
|
|
2219
2549
|
try {
|
|
2220
|
-
const parsed = JSON.parse(
|
|
2550
|
+
const parsed = JSON.parse(readFileSync6(this.path(file), "utf8"));
|
|
2221
2551
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
2222
2552
|
} catch {
|
|
2223
2553
|
return {};
|
|
2224
2554
|
}
|
|
2225
2555
|
},
|
|
2226
2556
|
persist: (next) => {
|
|
2227
|
-
|
|
2557
|
+
writeFileSync2(this.path(file), JSON.stringify(next));
|
|
2228
2558
|
}
|
|
2229
2559
|
};
|
|
2230
2560
|
}
|
|
@@ -2238,14 +2568,14 @@ var State = class _State {
|
|
|
2238
2568
|
return {
|
|
2239
2569
|
previous: () => {
|
|
2240
2570
|
try {
|
|
2241
|
-
const parsed = JSON.parse(
|
|
2571
|
+
const parsed = JSON.parse(readFileSync6(this.path(file), "utf8"));
|
|
2242
2572
|
return Array.isArray(parsed) ? parsed : [];
|
|
2243
2573
|
} catch {
|
|
2244
2574
|
return [];
|
|
2245
2575
|
}
|
|
2246
2576
|
},
|
|
2247
2577
|
persist: (next) => {
|
|
2248
|
-
|
|
2578
|
+
writeFileSync2(this.path(file), JSON.stringify(next));
|
|
2249
2579
|
}
|
|
2250
2580
|
};
|
|
2251
2581
|
}
|
|
@@ -2258,7 +2588,7 @@ var State = class _State {
|
|
|
2258
2588
|
const file = ".release-last-run";
|
|
2259
2589
|
return {
|
|
2260
2590
|
touch: () => {
|
|
2261
|
-
|
|
2591
|
+
writeFileSync2(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
|
|
2262
2592
|
},
|
|
2263
2593
|
at: () => {
|
|
2264
2594
|
try {
|
|
@@ -2271,7 +2601,7 @@ var State = class _State {
|
|
|
2271
2601
|
}
|
|
2272
2602
|
readInt(file) {
|
|
2273
2603
|
try {
|
|
2274
|
-
return Number.parseInt(
|
|
2604
|
+
return Number.parseInt(readFileSync6(this.path(file), "utf8").trim(), 10) || 0;
|
|
2275
2605
|
} catch {
|
|
2276
2606
|
return 0;
|
|
2277
2607
|
}
|
|
@@ -2575,9 +2905,9 @@ function describeRelease(o, route) {
|
|
|
2575
2905
|
}
|
|
2576
2906
|
|
|
2577
2907
|
// src/install.ts
|
|
2578
|
-
import { dirname as
|
|
2579
|
-
import { homedir as
|
|
2580
|
-
import { accessSync, constants, existsSync as
|
|
2908
|
+
import { dirname as dirname4, join as join7, extname } from "path";
|
|
2909
|
+
import { homedir as homedir4 } from "os";
|
|
2910
|
+
import { accessSync, constants, existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync3, unlinkSync, realpathSync } from "fs";
|
|
2581
2911
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
2582
2912
|
import { createHash } from "crypto";
|
|
2583
2913
|
|
|
@@ -2589,7 +2919,7 @@ function isCompiledBinary(moduleUrl) {
|
|
|
2589
2919
|
// src/install.ts
|
|
2590
2920
|
function crewInvocation(crewHome) {
|
|
2591
2921
|
if (isCompiledBinary(import.meta.url)) return [process.execPath];
|
|
2592
|
-
return [process.execPath,
|
|
2922
|
+
return [process.execPath, join7(crewHome, "bin", "crew")];
|
|
2593
2923
|
}
|
|
2594
2924
|
var INTERVAL_SECONDS = 120;
|
|
2595
2925
|
function labelFor(crewHome, job) {
|
|
@@ -2620,14 +2950,14 @@ function schedulerLogFor(crewLog, job) {
|
|
|
2620
2950
|
return `${stem}.${suffix}${ext || ".log"}`;
|
|
2621
2951
|
}
|
|
2622
2952
|
function pathFor(ship) {
|
|
2623
|
-
const nodeBinDir =
|
|
2953
|
+
const nodeBinDir = dirname4(process.execPath);
|
|
2624
2954
|
const parts = [nodeBinDir];
|
|
2625
2955
|
if (ship.extraPath) parts.push(ship.extraPath);
|
|
2626
2956
|
parts.push("/usr/bin", "/bin", "/usr/sbin", "/sbin");
|
|
2627
2957
|
return parts.join(":");
|
|
2628
2958
|
}
|
|
2629
2959
|
function findOnPath(bin, searchPath) {
|
|
2630
|
-
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) =>
|
|
2960
|
+
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join7(dir, bin));
|
|
2631
2961
|
for (const candidate of candidates) {
|
|
2632
2962
|
try {
|
|
2633
2963
|
accessSync(candidate, constants.X_OK);
|
|
@@ -2637,19 +2967,19 @@ function findOnPath(bin, searchPath) {
|
|
|
2637
2967
|
}
|
|
2638
2968
|
return void 0;
|
|
2639
2969
|
}
|
|
2640
|
-
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin",
|
|
2970
|
+
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join7(homedir4(), ".docker", "bin")].join(":");
|
|
2641
2971
|
function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
2642
2972
|
const schedulerPath = pathFor(ship);
|
|
2643
2973
|
if (findOnPath("docker", schedulerPath)) return void 0;
|
|
2644
2974
|
const inShell = findOnPath("docker", shellPath);
|
|
2645
|
-
return inShell ? `\`docker\` is not on the scheduler PATH (${schedulerPath}) but is at ${inShell} in your shell; add ${
|
|
2975
|
+
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
2976
|
}
|
|
2647
2977
|
function xmlEscape(s) {
|
|
2648
2978
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2649
2979
|
}
|
|
2650
2980
|
function planLaunchd(ship, crewHome, job) {
|
|
2651
2981
|
const label = labelFor(crewHome, job);
|
|
2652
|
-
const unitPath =
|
|
2982
|
+
const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2653
2983
|
const crewLog = ship.logFile;
|
|
2654
2984
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
2655
2985
|
const persistent = job === "run";
|
|
@@ -2701,8 +3031,8 @@ ${scheduleKeys}
|
|
|
2701
3031
|
}
|
|
2702
3032
|
function planSystemd(ship, crewHome, job) {
|
|
2703
3033
|
const label = labelFor(crewHome, job);
|
|
2704
|
-
const unitDir =
|
|
2705
|
-
const servicePath =
|
|
3034
|
+
const unitDir = join7(homedir4(), ".config", "systemd", "user");
|
|
3035
|
+
const servicePath = join7(unitDir, `${label}.service`);
|
|
2706
3036
|
const crewLog = ship.logFile;
|
|
2707
3037
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
2708
3038
|
const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
|
|
@@ -2739,7 +3069,7 @@ WantedBy=default.target
|
|
|
2739
3069
|
schedulerLog
|
|
2740
3070
|
};
|
|
2741
3071
|
}
|
|
2742
|
-
const timerPath =
|
|
3072
|
+
const timerPath = join7(unitDir, `${label}.timer`);
|
|
2743
3073
|
const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
|
|
2744
3074
|
[Unit]
|
|
2745
3075
|
Description=${description}
|
|
@@ -2806,7 +3136,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
2806
3136
|
function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
2807
3137
|
const label = labelFor(crewHome, job);
|
|
2808
3138
|
if (hostShip === "macos") {
|
|
2809
|
-
const unitPath =
|
|
3139
|
+
const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2810
3140
|
return {
|
|
2811
3141
|
mechanism: "launchd",
|
|
2812
3142
|
unitPaths: [unitPath],
|
|
@@ -2816,18 +3146,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
2816
3146
|
};
|
|
2817
3147
|
}
|
|
2818
3148
|
if (hostShip === "linux" && hasSystemd) {
|
|
2819
|
-
const unitDir =
|
|
3149
|
+
const unitDir = join7(homedir4(), ".config", "systemd", "user");
|
|
2820
3150
|
if (job === "run") {
|
|
2821
3151
|
return {
|
|
2822
3152
|
mechanism: "systemd",
|
|
2823
|
-
unitPaths: [
|
|
3153
|
+
unitPaths: [join7(unitDir, `${label}.service`)],
|
|
2824
3154
|
crontabLine: null,
|
|
2825
3155
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
|
|
2826
3156
|
};
|
|
2827
3157
|
}
|
|
2828
3158
|
return {
|
|
2829
3159
|
mechanism: "systemd",
|
|
2830
|
-
unitPaths: [
|
|
3160
|
+
unitPaths: [join7(unitDir, `${label}.service`), join7(unitDir, `${label}.timer`)],
|
|
2831
3161
|
crontabLine: null,
|
|
2832
3162
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
|
|
2833
3163
|
};
|
|
@@ -2845,7 +3175,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
2845
3175
|
function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
2846
3176
|
const label = labelFor(crewHome, "run");
|
|
2847
3177
|
if (hostShip === "macos") {
|
|
2848
|
-
const unitPath =
|
|
3178
|
+
const unitPath = join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2849
3179
|
return {
|
|
2850
3180
|
mechanism: "launchd",
|
|
2851
3181
|
startCommand: ["launchctl", "load", unitPath],
|
|
@@ -2885,8 +3215,8 @@ async function applyInstall(plan, crewHome, dryRun, log) {
|
|
|
2885
3215
|
log.emit(`would write ${path}`);
|
|
2886
3216
|
continue;
|
|
2887
3217
|
}
|
|
2888
|
-
|
|
2889
|
-
|
|
3218
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
3219
|
+
writeFileSync3(path, plan.unitContent[path]);
|
|
2890
3220
|
log.emit(`wrote ${path}`);
|
|
2891
3221
|
}
|
|
2892
3222
|
if (plan.crontabLine) {
|
|
@@ -2940,7 +3270,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
2940
3270
|
}
|
|
2941
3271
|
}
|
|
2942
3272
|
for (const path of plan.unitPaths) {
|
|
2943
|
-
if (!
|
|
3273
|
+
if (!existsSync6(path)) continue;
|
|
2944
3274
|
if (dryRun) {
|
|
2945
3275
|
log.emit(`would remove ${path}`);
|
|
2946
3276
|
continue;
|
|
@@ -2959,7 +3289,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
2959
3289
|
}
|
|
2960
3290
|
var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
|
|
2961
3291
|
function launchAgentsDir() {
|
|
2962
|
-
return
|
|
3292
|
+
return join7(homedir4(), "Library", "LaunchAgents");
|
|
2963
3293
|
}
|
|
2964
3294
|
function xmlUnescape(s) {
|
|
2965
3295
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
@@ -2976,25 +3306,25 @@ function loadedCrewLabels() {
|
|
|
2976
3306
|
return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
|
|
2977
3307
|
}
|
|
2978
3308
|
function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
2979
|
-
const files =
|
|
3309
|
+
const files = existsSync6(dir) ? readdirSync(dir) : [];
|
|
2980
3310
|
const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
|
|
2981
|
-
const plistPath =
|
|
3311
|
+
const plistPath = join7(dir, f);
|
|
2982
3312
|
let program = null;
|
|
2983
3313
|
try {
|
|
2984
|
-
program = programFromPlist(
|
|
3314
|
+
program = programFromPlist(readFileSync7(plistPath, "utf8"));
|
|
2985
3315
|
} catch {
|
|
2986
3316
|
}
|
|
2987
3317
|
return {
|
|
2988
3318
|
label: f.slice(0, -".plist".length),
|
|
2989
3319
|
plistPath,
|
|
2990
3320
|
program,
|
|
2991
|
-
programExists: program !== null &&
|
|
3321
|
+
programExists: program !== null && existsSync6(program)
|
|
2992
3322
|
};
|
|
2993
3323
|
});
|
|
2994
3324
|
const seen = new Set(units.map((u) => u.label));
|
|
2995
3325
|
for (const label of [...loaded].sort()) {
|
|
2996
3326
|
if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
|
|
2997
|
-
units.push({ label, plistPath:
|
|
3327
|
+
units.push({ label, plistPath: join7(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
|
|
2998
3328
|
}
|
|
2999
3329
|
return units;
|
|
3000
3330
|
}
|
|
@@ -3007,9 +3337,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
|
|
|
3007
3337
|
log.emit(`would unload and remove ${unit.label}`);
|
|
3008
3338
|
return;
|
|
3009
3339
|
}
|
|
3010
|
-
if (
|
|
3340
|
+
if (existsSync6(unit.plistPath)) spawnSync2("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
|
|
3011
3341
|
spawnSync2("launchctl", ["remove", unit.label], { encoding: "utf8" });
|
|
3012
|
-
if (
|
|
3342
|
+
if (existsSync6(unit.plistPath)) unlinkSync(unit.plistPath);
|
|
3013
3343
|
log.emit(`removed ${unit.label} (${unit.plistPath})`);
|
|
3014
3344
|
}
|
|
3015
3345
|
function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
|
|
@@ -3092,15 +3422,15 @@ import { statfsSync } from "fs";
|
|
|
3092
3422
|
// src/git.ts
|
|
3093
3423
|
import { execFileSync } from "child_process";
|
|
3094
3424
|
import {
|
|
3095
|
-
existsSync as
|
|
3096
|
-
mkdirSync as
|
|
3425
|
+
existsSync as existsSync7,
|
|
3426
|
+
mkdirSync as mkdirSync5,
|
|
3097
3427
|
openSync as openSync2,
|
|
3098
3428
|
closeSync as closeSync2,
|
|
3099
3429
|
writeSync,
|
|
3100
3430
|
statSync as statSync4,
|
|
3101
3431
|
unlinkSync as unlinkSync2
|
|
3102
3432
|
} from "fs";
|
|
3103
|
-
import { dirname as
|
|
3433
|
+
import { dirname as dirname5, basename as basename4 } from "path";
|
|
3104
3434
|
var GitError = class extends Error {
|
|
3105
3435
|
};
|
|
3106
3436
|
function gitRaw(cwd, args) {
|
|
@@ -3464,13 +3794,13 @@ function cloneUrlFor(remote) {
|
|
|
3464
3794
|
return `git@github.com:${remote}.git`;
|
|
3465
3795
|
}
|
|
3466
3796
|
function ensureRepoCheckout(dir, remote) {
|
|
3467
|
-
if (
|
|
3797
|
+
if (existsSync7(`${dir}/.git`)) return false;
|
|
3468
3798
|
if (!remote) {
|
|
3469
3799
|
throw new GitError(
|
|
3470
3800
|
`${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
3801
|
);
|
|
3472
3802
|
}
|
|
3473
|
-
|
|
3803
|
+
mkdirSync5(dirname5(dir), { recursive: true });
|
|
3474
3804
|
try {
|
|
3475
3805
|
execFileSync("git", ["clone", cloneUrlFor(remote), dir], { stdio: ["ignore", "pipe", "pipe"] });
|
|
3476
3806
|
} catch (e) {
|
|
@@ -3489,7 +3819,7 @@ function sleepSync(ms2) {
|
|
|
3489
3819
|
}
|
|
3490
3820
|
function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
3491
3821
|
const lockPath = checkoutLockPath(dir);
|
|
3492
|
-
|
|
3822
|
+
mkdirSync5(dirname5(lockPath), { recursive: true });
|
|
3493
3823
|
const deadline = Date.now() + timeoutMs;
|
|
3494
3824
|
for (; ; ) {
|
|
3495
3825
|
try {
|
|
@@ -5201,8 +5531,8 @@ function materializeBranchForTicket(dir, cfg, t, role) {
|
|
|
5201
5531
|
}
|
|
5202
5532
|
|
|
5203
5533
|
// src/poll.ts
|
|
5204
|
-
import { writeFileSync as
|
|
5205
|
-
import { join as
|
|
5534
|
+
import { writeFileSync as writeFileSync4 } from "fs";
|
|
5535
|
+
import { join as join8 } from "path";
|
|
5206
5536
|
|
|
5207
5537
|
// src/stalled.ts
|
|
5208
5538
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -5474,7 +5804,7 @@ async function decideCycle(o) {
|
|
|
5474
5804
|
}
|
|
5475
5805
|
return decision;
|
|
5476
5806
|
}
|
|
5477
|
-
var digestPath = (stateDir, route, role) =>
|
|
5807
|
+
var digestPath = (stateDir, route, role) => join8(stateDir, `digest-${route}-${role}.md`);
|
|
5478
5808
|
function writeDigest(o, d, role, stateDir) {
|
|
5479
5809
|
const me = o.route.resolved?.seats[role];
|
|
5480
5810
|
if (!me) return false;
|
|
@@ -5536,7 +5866,7 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
5536
5866
|
};
|
|
5537
5867
|
const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
|
|
5538
5868
|
const path = digestPath(stateDir, routeSlug(o.route.route), role);
|
|
5539
|
-
|
|
5869
|
+
writeFileSync4(path, text);
|
|
5540
5870
|
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
|
|
5541
5871
|
data: { role, bytes: Buffer.byteLength(text) }
|
|
5542
5872
|
});
|
|
@@ -5844,9 +6174,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
|
|
|
5844
6174
|
|
|
5845
6175
|
// src/agent.ts
|
|
5846
6176
|
import { spawn as spawn2 } from "child_process";
|
|
5847
|
-
import { readFileSync as
|
|
6177
|
+
import { readFileSync as readFileSync8, existsSync as existsSync8, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5, createWriteStream } from "fs";
|
|
5848
6178
|
import { finished } from "stream/promises";
|
|
5849
|
-
import { join as
|
|
6179
|
+
import { join as join9, dirname as dirname6 } from "path";
|
|
5850
6180
|
import { createHash as createHash2 } from "crypto";
|
|
5851
6181
|
|
|
5852
6182
|
// src/environment.ts
|
|
@@ -6244,23 +6574,23 @@ var AgentError = class extends Error {
|
|
|
6244
6574
|
var AgentSpawnError = class extends AgentError {
|
|
6245
6575
|
};
|
|
6246
6576
|
function planAgentRun(o) {
|
|
6247
|
-
const briefPath =
|
|
6248
|
-
if (!
|
|
6577
|
+
const briefPath = join9(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
6578
|
+
if (!existsSync8(briefPath)) {
|
|
6249
6579
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
6250
6580
|
}
|
|
6251
|
-
const commonPath =
|
|
6252
|
-
if (!
|
|
6581
|
+
const commonPath = join9(o.route.promptsDir, "personas", "common.md");
|
|
6582
|
+
if (!existsSync8(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
|
|
6253
6583
|
const now = o.now ?? (() => Date.now());
|
|
6254
6584
|
const routeLabel = routeSlug(o.route.route);
|
|
6255
|
-
const digestPath2 =
|
|
6585
|
+
const digestPath2 = join9(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
6256
6586
|
let digest;
|
|
6257
6587
|
let ageSeconds;
|
|
6258
|
-
if (
|
|
6588
|
+
if (existsSync8(digestPath2)) {
|
|
6259
6589
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
6260
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
6590
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync8(digestPath2, "utf8");
|
|
6261
6591
|
}
|
|
6262
|
-
const common =
|
|
6263
|
-
const brief =
|
|
6592
|
+
const common = readFileSync8(commonPath, "utf8");
|
|
6593
|
+
const brief = readFileSync8(briefPath, "utf8");
|
|
6264
6594
|
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
6265
6595
|
const effectiveBrief = o.divergedPrompt ?? brief;
|
|
6266
6596
|
const prompt = assemblePrompt({
|
|
@@ -6271,7 +6601,7 @@ function planAgentRun(o) {
|
|
|
6271
6601
|
digest
|
|
6272
6602
|
});
|
|
6273
6603
|
const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
|
|
6274
|
-
const streamsDir =
|
|
6604
|
+
const streamsDir = join9(o.stateDir, "streams");
|
|
6275
6605
|
const base = `${routeLabel}-${o.role}-${o.cycle}`;
|
|
6276
6606
|
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}));
|
|
6277
6607
|
const { toolchain, model } = seat;
|
|
@@ -6304,7 +6634,7 @@ function planAgentRun(o) {
|
|
|
6304
6634
|
args: invocation.args,
|
|
6305
6635
|
toolchain: toolchain.name,
|
|
6306
6636
|
harness: toolchain.harness,
|
|
6307
|
-
provider:
|
|
6637
|
+
provider: toolchainProvider(toolchain),
|
|
6308
6638
|
resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
|
|
6309
6639
|
warnings: seat.warnings,
|
|
6310
6640
|
prompt,
|
|
@@ -6326,8 +6656,8 @@ function planAgentRun(o) {
|
|
|
6326
6656
|
digestAttached: digest !== void 0,
|
|
6327
6657
|
digestAgeSeconds: ageSeconds,
|
|
6328
6658
|
model,
|
|
6329
|
-
streamPath:
|
|
6330
|
-
eventsPath:
|
|
6659
|
+
streamPath: join9(streamsDir, `${base}.jsonl`),
|
|
6660
|
+
eventsPath: join9(streamsDir, `${base}.events.jsonl`),
|
|
6331
6661
|
ticket: o.ticket,
|
|
6332
6662
|
agentLog,
|
|
6333
6663
|
promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
|
|
@@ -6354,7 +6684,7 @@ function describePlan(p) {
|
|
|
6354
6684
|
}
|
|
6355
6685
|
function openSink(path) {
|
|
6356
6686
|
try {
|
|
6357
|
-
|
|
6687
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
6358
6688
|
return createWriteStream(path, { flags: "a" });
|
|
6359
6689
|
} catch {
|
|
6360
6690
|
return void 0;
|
|
@@ -6463,7 +6793,7 @@ function spawnAgent(plan, emit) {
|
|
|
6463
6793
|
provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER
|
|
6464
6794
|
};
|
|
6465
6795
|
try {
|
|
6466
|
-
|
|
6796
|
+
writeFileSync5(`${plan.streamPath}.meta.json`, JSON.stringify({
|
|
6467
6797
|
route: plan.route,
|
|
6468
6798
|
role: plan.role,
|
|
6469
6799
|
ticket: plan.ticket ?? null,
|
|
@@ -6522,13 +6852,13 @@ function spawnAgent(plan, emit) {
|
|
|
6522
6852
|
}
|
|
6523
6853
|
|
|
6524
6854
|
// src/run-worktree.ts
|
|
6525
|
-
import { existsSync as
|
|
6526
|
-
import { dirname as
|
|
6855
|
+
import { existsSync as existsSync10 } from "fs";
|
|
6856
|
+
import { dirname as dirname7, join as join11 } from "path";
|
|
6527
6857
|
|
|
6528
6858
|
// src/ports.ts
|
|
6529
6859
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
6530
|
-
import { existsSync as
|
|
6531
|
-
import { join as
|
|
6860
|
+
import { existsSync as existsSync9 } from "fs";
|
|
6861
|
+
import { join as join10 } from "path";
|
|
6532
6862
|
var run = (cmd, args) => {
|
|
6533
6863
|
try {
|
|
6534
6864
|
return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -6573,7 +6903,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
6573
6903
|
}
|
|
6574
6904
|
function worktreeExists(parent, prefix, n) {
|
|
6575
6905
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
6576
|
-
if (
|
|
6906
|
+
if (existsSync9(join10(parent, name, ".git"))) return true;
|
|
6577
6907
|
}
|
|
6578
6908
|
return false;
|
|
6579
6909
|
}
|
|
@@ -6647,7 +6977,7 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
6647
6977
|
const existing = worktreeForNumber(dir, num);
|
|
6648
6978
|
const located = locateBranchForTicket(dir, cfg, t, role);
|
|
6649
6979
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
6650
|
-
if (existing && !existing.prunable &&
|
|
6980
|
+
if (existing && !existing.prunable && existsSync10(existing.path)) {
|
|
6651
6981
|
const branch = existing.branch;
|
|
6652
6982
|
if (status(existing.path).length > 0) return { action: "leave-dirty", path: existing.path, branch };
|
|
6653
6983
|
if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
|
|
@@ -6661,7 +6991,7 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
6661
6991
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
6662
6992
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
6663
6993
|
}
|
|
6664
|
-
const path =
|
|
6994
|
+
const path = join11(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
|
|
6665
6995
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
6666
6996
|
return { action: "cut-remote", path, branch: located.branch };
|
|
6667
6997
|
}
|
|
@@ -6708,7 +7038,7 @@ function worktreeHoldsUnpushed(path, remote) {
|
|
|
6708
7038
|
}
|
|
6709
7039
|
async function finishRunWorktree(dir, cfg, branch) {
|
|
6710
7040
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
6711
|
-
if (!wt || !
|
|
7041
|
+
if (!wt || !existsSync10(wt.path)) return { kind: "none" };
|
|
6712
7042
|
if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
|
|
6713
7043
|
if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
|
|
6714
7044
|
if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
|
|
@@ -6720,7 +7050,7 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
6720
7050
|
if (pids.length) await killGently(pids);
|
|
6721
7051
|
}
|
|
6722
7052
|
gitOk(dir, ["worktree", "remove", wt.path]);
|
|
6723
|
-
return
|
|
7053
|
+
return existsSync10(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
|
|
6724
7054
|
}
|
|
6725
7055
|
|
|
6726
7056
|
// src/handoff-guard.ts
|
|
@@ -6869,13 +7199,13 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
|
|
|
6869
7199
|
}
|
|
6870
7200
|
|
|
6871
7201
|
// src/connect-wizard.ts
|
|
6872
|
-
import { delimiter, dirname as
|
|
6873
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as
|
|
7202
|
+
import { delimiter, dirname as dirname8, join as join12 } from "path";
|
|
7203
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync11, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
6874
7204
|
import { stringify } from "yaml";
|
|
6875
7205
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
6876
7206
|
function shouldRunWizard(opts) {
|
|
6877
7207
|
if (!opts.isTTY) return false;
|
|
6878
|
-
const exists = opts.configExists ??
|
|
7208
|
+
const exists = opts.configExists ?? existsSync11;
|
|
6879
7209
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
6880
7210
|
}
|
|
6881
7211
|
function firstRunConfigPath() {
|
|
@@ -6892,7 +7222,7 @@ function defaultIsExecutable(p) {
|
|
|
6892
7222
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
6893
7223
|
for (const dir of pathEnv.split(delimiter)) {
|
|
6894
7224
|
if (!dir) continue;
|
|
6895
|
-
const candidate =
|
|
7225
|
+
const candidate = join12(dir, name);
|
|
6896
7226
|
if (isExecutable(candidate)) return candidate;
|
|
6897
7227
|
}
|
|
6898
7228
|
return void 0;
|
|
@@ -6918,9 +7248,9 @@ async function runWizard(p, env) {
|
|
|
6918
7248
|
if (entered && isExec(entered)) agentBin = entered;
|
|
6919
7249
|
else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
|
|
6920
7250
|
}
|
|
6921
|
-
const nodeDir =
|
|
7251
|
+
const nodeDir = dirname8(env.nodePath);
|
|
6922
7252
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
6923
|
-
const baseDirs = [nodeDir, pnpmPath ?
|
|
7253
|
+
const baseDirs = [nodeDir, pnpmPath ? dirname8(pnpmPath) : void 0];
|
|
6924
7254
|
p.say(
|
|
6925
7255
|
`
|
|
6926
7256
|
Host Passengers lets this machine run the agent sessions that other people's
|
|
@@ -6931,7 +7261,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
|
|
|
6931
7261
|
p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
|
|
6932
7262
|
}
|
|
6933
7263
|
const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
6934
|
-
const extraPath = deriveExtraPath([...baseDirs, dockerPath ?
|
|
7264
|
+
const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname8(dockerPath) : void 0]);
|
|
6935
7265
|
if (extraPath) {
|
|
6936
7266
|
p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
|
|
6937
7267
|
}
|
|
@@ -6953,9 +7283,9 @@ routes:
|
|
|
6953
7283
|
${routeBlock}`;
|
|
6954
7284
|
}
|
|
6955
7285
|
function writeNewConfig(path, text) {
|
|
6956
|
-
|
|
7286
|
+
mkdirSync7(dirname8(path), { recursive: true });
|
|
6957
7287
|
try {
|
|
6958
|
-
|
|
7288
|
+
writeFileSync6(path, text, { mode: 384, flag: "wx" });
|
|
6959
7289
|
return true;
|
|
6960
7290
|
} catch (e) {
|
|
6961
7291
|
if (e.code === "EEXIST") return false;
|
|
@@ -6987,8 +7317,8 @@ Wrote ${o.configPath}.
|
|
|
6987
7317
|
}
|
|
6988
7318
|
|
|
6989
7319
|
// src/release-run.ts
|
|
6990
|
-
import { readFileSync as
|
|
6991
|
-
import { dirname as
|
|
7320
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync12, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync4 } from "fs";
|
|
7321
|
+
import { dirname as dirname9, join as join13 } from "path";
|
|
6992
7322
|
import { tmpdir as tmpdir2 } from "os";
|
|
6993
7323
|
|
|
6994
7324
|
// src/release.ts
|
|
@@ -7646,13 +7976,13 @@ async function bump(o, size) {
|
|
|
7646
7976
|
}
|
|
7647
7977
|
return printed;
|
|
7648
7978
|
}
|
|
7649
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
7979
|
+
const files = o.repo.release.versionFiles.map((f) => join13(o.cwd, f));
|
|
7650
7980
|
const first = files[0];
|
|
7651
|
-
if (!
|
|
7981
|
+
if (!existsSync12(first)) {
|
|
7652
7982
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
7653
7983
|
return null;
|
|
7654
7984
|
}
|
|
7655
|
-
const current = JSON.parse(
|
|
7985
|
+
const current = JSON.parse(readFileSync9(first, "utf8")).version;
|
|
7656
7986
|
if (!current) {
|
|
7657
7987
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
7658
7988
|
return null;
|
|
@@ -7665,27 +7995,27 @@ async function bump(o, size) {
|
|
|
7665
7995
|
return version;
|
|
7666
7996
|
}
|
|
7667
7997
|
for (const f of files) {
|
|
7668
|
-
const src =
|
|
7998
|
+
const src = readFileSync9(f, "utf8");
|
|
7669
7999
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
7670
8000
|
if (out === src) {
|
|
7671
8001
|
o.emit.error(`no version field in ${f}`);
|
|
7672
8002
|
return null;
|
|
7673
8003
|
}
|
|
7674
|
-
|
|
8004
|
+
writeFileSync7(f, out);
|
|
7675
8005
|
}
|
|
7676
8006
|
return version;
|
|
7677
8007
|
}
|
|
7678
8008
|
function writeChangelog(o, version, entries) {
|
|
7679
8009
|
const file = o.repo.release.changelog;
|
|
7680
8010
|
if (!file || entries.length === 0) return;
|
|
7681
|
-
const path =
|
|
8011
|
+
const path = join13(o.cwd, file);
|
|
7682
8012
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
7683
8013
|
if (o.dryRun) {
|
|
7684
8014
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
7685
8015
|
return;
|
|
7686
8016
|
}
|
|
7687
|
-
const existing =
|
|
7688
|
-
|
|
8017
|
+
const existing = existsSync12(path) ? readFileSync9(path, "utf8") : "# Changelog\n";
|
|
8018
|
+
writeFileSync7(path, insertChangelogSection(existing, section));
|
|
7689
8019
|
}
|
|
7690
8020
|
async function confirm(o, expected) {
|
|
7691
8021
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -7766,11 +8096,11 @@ async function confirmLastRelease(o, decision) {
|
|
|
7766
8096
|
function openReleaseCheckout(o) {
|
|
7767
8097
|
const { remote, base } = o.repo.branch;
|
|
7768
8098
|
const branch = `crew/release-${Date.now()}`;
|
|
7769
|
-
const root = mkdtempSync(
|
|
7770
|
-
const dir =
|
|
8099
|
+
const root = mkdtempSync(join13(tmpdir2(), "crew-release-"));
|
|
8100
|
+
const dir = join13(root, "checkout");
|
|
7771
8101
|
const close = () => {
|
|
7772
8102
|
gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
|
|
7773
|
-
|
|
8103
|
+
rmSync4(root, { recursive: true, force: true });
|
|
7774
8104
|
gitOk(o.cwd, ["worktree", "prune"]);
|
|
7775
8105
|
gitOk(o.cwd, ["branch", "-D", branch]);
|
|
7776
8106
|
};
|
|
@@ -7781,10 +8111,10 @@ function openReleaseCheckout(o) {
|
|
|
7781
8111
|
throw e;
|
|
7782
8112
|
}
|
|
7783
8113
|
for (const f of o.repo.worktrees.copy) {
|
|
7784
|
-
const src =
|
|
7785
|
-
if (!
|
|
7786
|
-
|
|
7787
|
-
copyFileSync(src,
|
|
8114
|
+
const src = join13(o.cwd, f);
|
|
8115
|
+
if (!existsSync12(src)) continue;
|
|
8116
|
+
mkdirSync8(dirname9(join13(dir, f)), { recursive: true });
|
|
8117
|
+
copyFileSync(src, join13(dir, f));
|
|
7788
8118
|
}
|
|
7789
8119
|
return { dir, branch, close };
|
|
7790
8120
|
}
|
|
@@ -8253,7 +8583,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
8253
8583
|
}
|
|
8254
8584
|
|
|
8255
8585
|
// src/watch.ts
|
|
8256
|
-
import { watch as watchFile, existsSync as
|
|
8586
|
+
import { watch as watchFile, existsSync as existsSync13, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
|
|
8257
8587
|
var E = "\x1B[";
|
|
8258
8588
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
8259
8589
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -8354,7 +8684,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
8354
8684
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
8355
8685
|
}
|
|
8356
8686
|
function readFrom(file, from) {
|
|
8357
|
-
if (!
|
|
8687
|
+
if (!existsSync13(file)) return { events: [], offset: from };
|
|
8358
8688
|
const size = statSync6(file).size;
|
|
8359
8689
|
if (size < from) return readFrom(file, 0);
|
|
8360
8690
|
if (size === from) return { events: [], offset: from };
|
|
@@ -8594,8 +8924,8 @@ function nextRoles(decision, ran) {
|
|
|
8594
8924
|
}
|
|
8595
8925
|
|
|
8596
8926
|
// src/agents.ts
|
|
8597
|
-
import { readFileSync as
|
|
8598
|
-
import { join as
|
|
8927
|
+
import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
|
|
8928
|
+
import { join as join14 } from "path";
|
|
8599
8929
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
|
|
8600
8930
|
var PERSONA_NAME = {
|
|
8601
8931
|
dev: "Developer",
|
|
@@ -8609,12 +8939,12 @@ var CREW_AGENT_COLUMN = "agent_id";
|
|
|
8609
8939
|
var AgentsSyncError = class extends Error {
|
|
8610
8940
|
};
|
|
8611
8941
|
function personaDefaultPrompt(promptsDir, role) {
|
|
8612
|
-
const briefPath =
|
|
8613
|
-
if (!
|
|
8614
|
-
if (role === "pair") return
|
|
8615
|
-
const commonPath =
|
|
8616
|
-
if (!
|
|
8617
|
-
return `${
|
|
8942
|
+
const briefPath = join14(promptsDir, "personas", `lane-${role}.md`);
|
|
8943
|
+
if (!existsSync14(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
8944
|
+
if (role === "pair") return readFileSync10(briefPath, "utf8");
|
|
8945
|
+
const commonPath = join14(promptsDir, "personas", "common.md");
|
|
8946
|
+
if (!existsSync14(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
|
|
8947
|
+
return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
|
|
8618
8948
|
}
|
|
8619
8949
|
async function resolveAgentId(route, role, opts) {
|
|
8620
8950
|
const cached = route.resolved?.agentPersonas?.[role]?.agentId;
|
|
@@ -9291,14 +9621,14 @@ import {
|
|
|
9291
9621
|
} from "@tablation/client";
|
|
9292
9622
|
|
|
9293
9623
|
// src/skills.ts
|
|
9294
|
-
import { readFileSync as
|
|
9295
|
-
import { join as
|
|
9624
|
+
import { readFileSync as readFileSync11, readdirSync as readdirSync2, existsSync as existsSync15 } from "fs";
|
|
9625
|
+
import { join as join15 } from "path";
|
|
9296
9626
|
import { parse as parseYaml } from "yaml";
|
|
9297
9627
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
9298
9628
|
var SkillSyncError = class extends Error {
|
|
9299
9629
|
};
|
|
9300
9630
|
function parseSkillFile(path) {
|
|
9301
|
-
const text =
|
|
9631
|
+
const text = readFileSync11(path, "utf8");
|
|
9302
9632
|
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
|
|
9303
9633
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
9304
9634
|
const frontmatter = match[1];
|
|
@@ -9311,9 +9641,9 @@ function parseSkillFile(path) {
|
|
|
9311
9641
|
return { name: meta.name, description: meta.description, prompt: body, file: path };
|
|
9312
9642
|
}
|
|
9313
9643
|
function listLocalSkills(promptsDir) {
|
|
9314
|
-
const dir =
|
|
9315
|
-
if (!
|
|
9316
|
-
return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(
|
|
9644
|
+
const dir = join15(promptsDir, "skills");
|
|
9645
|
+
if (!existsSync15(dir)) return [];
|
|
9646
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join15(dir, f)));
|
|
9317
9647
|
}
|
|
9318
9648
|
async function syncSkills(route, opts) {
|
|
9319
9649
|
if (!route.resolved) {
|
|
@@ -9492,7 +9822,7 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
9492
9822
|
}
|
|
9493
9823
|
|
|
9494
9824
|
// src/repos-cmd.ts
|
|
9495
|
-
import { homedir as
|
|
9825
|
+
import { homedir as homedir5 } from "os";
|
|
9496
9826
|
import { basename as basename7, resolve as resolve3 } from "path";
|
|
9497
9827
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
9498
9828
|
var ReposError = class extends Error {
|
|
@@ -9539,7 +9869,7 @@ function parseAddArgs(args, routes, isDir) {
|
|
|
9539
9869
|
return { route, path: "." };
|
|
9540
9870
|
}
|
|
9541
9871
|
function expandDir(p, base) {
|
|
9542
|
-
const home = p === "~" ?
|
|
9872
|
+
const home = p === "~" ? homedir5() : p.startsWith("~/") ? `${homedir5()}/${p.slice(2)}` : p;
|
|
9543
9873
|
return resolve3(base, home);
|
|
9544
9874
|
}
|
|
9545
9875
|
function routeMapOf(doc, route) {
|
|
@@ -9674,7 +10004,7 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
9674
10004
|
}
|
|
9675
10005
|
|
|
9676
10006
|
// src/worktree-sweep.ts
|
|
9677
|
-
import { existsSync as
|
|
10007
|
+
import { existsSync as existsSync16 } from "fs";
|
|
9678
10008
|
import { basename as basename8 } from "path";
|
|
9679
10009
|
function planWorktreeSweep(target, tickets, contract) {
|
|
9680
10010
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
@@ -9702,7 +10032,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
9702
10032
|
let removed = 0;
|
|
9703
10033
|
let keptBranches = 0;
|
|
9704
10034
|
for (const a of actions) {
|
|
9705
|
-
if (!dryRun &&
|
|
10035
|
+
if (!dryRun && existsSync16(a.path)) {
|
|
9706
10036
|
let dirty = false;
|
|
9707
10037
|
try {
|
|
9708
10038
|
dirty = status(a.path).length > 0;
|
|
@@ -9716,7 +10046,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
9716
10046
|
continue;
|
|
9717
10047
|
}
|
|
9718
10048
|
}
|
|
9719
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
10049
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync16(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
9720
10050
|
log.warn(
|
|
9721
10051
|
`not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
|
|
9722
10052
|
{ ticket: a.ticket.issue_id, step: "worktree" }
|
|
@@ -9797,14 +10127,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
9797
10127
|
}
|
|
9798
10128
|
|
|
9799
10129
|
// src/stream-sweep.ts
|
|
9800
|
-
import { existsSync as
|
|
9801
|
-
import { join as
|
|
10130
|
+
import { existsSync as existsSync17, readdirSync as readdirSync3, rmSync as rmSync5, statSync as statSync7 } from "fs";
|
|
10131
|
+
import { join as join16 } from "path";
|
|
9802
10132
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
9803
10133
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
9804
|
-
const dir =
|
|
9805
|
-
if (!
|
|
10134
|
+
const dir = join16(stateDir, "streams");
|
|
10135
|
+
if (!existsSync17(dir)) return [];
|
|
9806
10136
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
9807
|
-
return readdirSync3(dir).map((name) =>
|
|
10137
|
+
return readdirSync3(dir).map((name) => join16(dir, name)).filter((path) => {
|
|
9808
10138
|
try {
|
|
9809
10139
|
return statSync7(path).mtimeMs < cutoff;
|
|
9810
10140
|
} catch {
|
|
@@ -9820,7 +10150,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
9820
10150
|
continue;
|
|
9821
10151
|
}
|
|
9822
10152
|
try {
|
|
9823
|
-
|
|
10153
|
+
rmSync5(path, { force: true });
|
|
9824
10154
|
removed++;
|
|
9825
10155
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
9826
10156
|
} catch (e) {
|
|
@@ -9830,9 +10160,9 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
9830
10160
|
return removed;
|
|
9831
10161
|
}
|
|
9832
10162
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
9833
|
-
if (!
|
|
10163
|
+
if (!existsSync17(stateDir)) return [];
|
|
9834
10164
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
9835
|
-
return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) =>
|
|
10165
|
+
return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join16(stateDir, name)).filter((path) => {
|
|
9836
10166
|
try {
|
|
9837
10167
|
return statSync7(path).mtimeMs < cutoff;
|
|
9838
10168
|
} catch {
|
|
@@ -9842,24 +10172,24 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
9842
10172
|
}
|
|
9843
10173
|
|
|
9844
10174
|
// src/cli.ts
|
|
9845
|
-
import { readFileSync as
|
|
10175
|
+
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
10176
|
import { createInterface } from "readline/promises";
|
|
9847
10177
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
9848
10178
|
|
|
9849
10179
|
// src/ssh-keys.ts
|
|
9850
10180
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
9851
|
-
import { existsSync as
|
|
9852
|
-
import { dirname as
|
|
10181
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, chmodSync } from "fs";
|
|
10182
|
+
import { dirname as dirname11, join as join17 } from "path";
|
|
9853
10183
|
var SshKeyError = class extends Error {
|
|
9854
10184
|
};
|
|
9855
10185
|
function shipSshKeyPathFor(stateDir) {
|
|
9856
|
-
return
|
|
10186
|
+
return join17(stateDir, "ssh", "id_ed25519");
|
|
9857
10187
|
}
|
|
9858
10188
|
function ensureShipSshKeypair(stateDir) {
|
|
9859
10189
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
9860
10190
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
9861
|
-
if (!
|
|
9862
|
-
|
|
10191
|
+
if (!existsSync18(privateKeyPath)) {
|
|
10192
|
+
mkdirSync9(dirname11(privateKeyPath), { recursive: true });
|
|
9863
10193
|
try {
|
|
9864
10194
|
execFileSync3("ssh-keygen", [
|
|
9865
10195
|
"-t",
|
|
@@ -9879,10 +10209,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
9879
10209
|
}
|
|
9880
10210
|
chmodSync(privateKeyPath, 384);
|
|
9881
10211
|
}
|
|
9882
|
-
if (!
|
|
10212
|
+
if (!existsSync18(publicKeyPath)) {
|
|
9883
10213
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
9884
10214
|
}
|
|
9885
|
-
const publicKey =
|
|
10215
|
+
const publicKey = readFileSync12(publicKeyPath, "utf8").trim();
|
|
9886
10216
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
9887
10217
|
}
|
|
9888
10218
|
function sshKeygenAvailable() {
|
|
@@ -10096,9 +10426,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
10096
10426
|
|
|
10097
10427
|
// src/tunnel.ts
|
|
10098
10428
|
import { spawn as spawn3 } from "child_process";
|
|
10099
|
-
import { existsSync as
|
|
10429
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3, readdirSync as readdirSync4 } from "fs";
|
|
10100
10430
|
import { randomInt } from "crypto";
|
|
10101
|
-
import { join as
|
|
10431
|
+
import { join as join18 } from "path";
|
|
10102
10432
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
10103
10433
|
var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
10104
10434
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -10165,21 +10495,21 @@ function startTunnel(opts) {
|
|
|
10165
10495
|
};
|
|
10166
10496
|
}
|
|
10167
10497
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
10168
|
-
return
|
|
10498
|
+
return join18(stateDir, "passengers", `${workspaceId}.json`);
|
|
10169
10499
|
}
|
|
10170
10500
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
10171
10501
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
10172
|
-
if (!
|
|
10502
|
+
if (!existsSync19(path)) return void 0;
|
|
10173
10503
|
try {
|
|
10174
|
-
return JSON.parse(
|
|
10504
|
+
return JSON.parse(readFileSync13(path, "utf8"));
|
|
10175
10505
|
} catch {
|
|
10176
10506
|
return void 0;
|
|
10177
10507
|
}
|
|
10178
10508
|
}
|
|
10179
10509
|
function writePersistedTunnel(stateDir, state) {
|
|
10180
10510
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
10181
|
-
|
|
10182
|
-
|
|
10511
|
+
mkdirSync10(join18(stateDir, "passengers"), { recursive: true });
|
|
10512
|
+
writeFileSync8(path, `${JSON.stringify(state, null, 2)}
|
|
10183
10513
|
`);
|
|
10184
10514
|
}
|
|
10185
10515
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -10276,8 +10606,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
10276
10606
|
}
|
|
10277
10607
|
}
|
|
10278
10608
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
10279
|
-
const dir =
|
|
10280
|
-
if (!
|
|
10609
|
+
const dir = join18(stateDir, "passengers");
|
|
10610
|
+
if (!existsSync19(dir)) return [];
|
|
10281
10611
|
return readdirSync4(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
10282
10612
|
}
|
|
10283
10613
|
|
|
@@ -10398,8 +10728,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
10398
10728
|
import { createServer as createHttpServer } from "http";
|
|
10399
10729
|
import { spawn as spawn4 } from "child_process";
|
|
10400
10730
|
import { randomBytes } from "crypto";
|
|
10401
|
-
import { existsSync as
|
|
10402
|
-
import { join as
|
|
10731
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
|
|
10732
|
+
import { join as join19 } from "path";
|
|
10403
10733
|
|
|
10404
10734
|
// src/secret-compare.ts
|
|
10405
10735
|
import { timingSafeEqual } from "crypto";
|
|
@@ -10488,21 +10818,21 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
10488
10818
|
return child;
|
|
10489
10819
|
};
|
|
10490
10820
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
10491
|
-
return
|
|
10821
|
+
return join19(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
10492
10822
|
}
|
|
10493
10823
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
10494
10824
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
10495
|
-
if (!
|
|
10825
|
+
if (!existsSync20(path)) return void 0;
|
|
10496
10826
|
try {
|
|
10497
|
-
return JSON.parse(
|
|
10827
|
+
return JSON.parse(readFileSync14(path, "utf8"));
|
|
10498
10828
|
} catch {
|
|
10499
10829
|
return void 0;
|
|
10500
10830
|
}
|
|
10501
10831
|
}
|
|
10502
10832
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
10503
10833
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
10504
|
-
|
|
10505
|
-
|
|
10834
|
+
mkdirSync11(join19(stateDir, "passengers"), { recursive: true });
|
|
10835
|
+
writeFileSync9(path, `${JSON.stringify(state, null, 2)}
|
|
10506
10836
|
`);
|
|
10507
10837
|
}
|
|
10508
10838
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -10522,8 +10852,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
10522
10852
|
}
|
|
10523
10853
|
}
|
|
10524
10854
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
10525
|
-
const dir =
|
|
10526
|
-
if (!
|
|
10855
|
+
const dir = join19(stateDir, "passengers");
|
|
10856
|
+
if (!existsSync20(dir)) return [];
|
|
10527
10857
|
const suffix = "-sync.json";
|
|
10528
10858
|
return readdirSync5(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
10529
10859
|
}
|
|
@@ -10581,7 +10911,7 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
10581
10911
|
|
|
10582
10912
|
// src/daemon.ts
|
|
10583
10913
|
import { readdirSync as readdirSync6, statSync as statSync8 } from "fs";
|
|
10584
|
-
import { join as
|
|
10914
|
+
import { join as join20 } from "path";
|
|
10585
10915
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
10586
10916
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
10587
10917
|
var defaultSleep = (ms2) => new Promise((resolve5) => setTimeout(resolve5, ms2));
|
|
@@ -10620,7 +10950,7 @@ function latestMtime(path) {
|
|
|
10620
10950
|
if (!stat.isDirectory()) return 0;
|
|
10621
10951
|
let latest = stat.mtimeMs;
|
|
10622
10952
|
for (const entry of readdirSync6(path, { withFileTypes: true })) {
|
|
10623
|
-
latest = Math.max(latest, latestMtime(
|
|
10953
|
+
latest = Math.max(latest, latestMtime(join20(path, entry.name)));
|
|
10624
10954
|
}
|
|
10625
10955
|
return latest;
|
|
10626
10956
|
}
|
|
@@ -10906,7 +11236,7 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
10906
11236
|
}
|
|
10907
11237
|
|
|
10908
11238
|
// src/cli.ts
|
|
10909
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
11239
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname12(process.execPath) : resolve4(dirname12(fileURLToPath(import.meta.url)), "..");
|
|
10910
11240
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
10911
11241
|
var CYCLE_SECONDS2 = 120;
|
|
10912
11242
|
var STALE_CYCLES = 3;
|
|
@@ -11040,8 +11370,9 @@ function usage() {
|
|
|
11040
11370
|
crew log [route] tail the log
|
|
11041
11371
|
crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
|
|
11042
11372
|
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)
|
|
11373
|
+
crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
|
|
11043
11374
|
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)
|
|
11375
|
+
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
11376
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
11046
11377
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
|
|
11047
11378
|
crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches
|
|
@@ -11080,7 +11411,7 @@ var value = (name) => {
|
|
|
11080
11411
|
var positional = argv.filter((a, i) => !a.startsWith("--") && !argv[i - 1]?.startsWith("--"));
|
|
11081
11412
|
var command = positional[0];
|
|
11082
11413
|
if (!command) usage();
|
|
11083
|
-
var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "install", "uninstall", "release", "merge", "deploy"]);
|
|
11414
|
+
var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "disconnect", "install", "uninstall", "release", "merge", "deploy"]);
|
|
11084
11415
|
var STATE_WRITING_SUBCOMMANDS = {
|
|
11085
11416
|
agents: /* @__PURE__ */ new Set(["sync"]),
|
|
11086
11417
|
skills: /* @__PURE__ */ new Set(["sync"])
|
|
@@ -11190,7 +11521,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
11190
11521
|
const releaseFleetWide = ["merge", "deploy", "release"].includes(command) && flag("fleet");
|
|
11191
11522
|
const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
|
|
11192
11523
|
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;
|
|
11524
|
+
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
11525
|
let route;
|
|
11195
11526
|
try {
|
|
11196
11527
|
route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
|
|
@@ -11396,8 +11727,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
11396
11727
|
}
|
|
11397
11728
|
if (host === "linux" && hasSystemd) {
|
|
11398
11729
|
const label = labelFor(CREW_HOME, "run");
|
|
11399
|
-
const staleTimer =
|
|
11400
|
-
if (
|
|
11730
|
+
const staleTimer = join21(homedir6(), ".config", "systemd", "user", `${label}.timer`);
|
|
11731
|
+
if (existsSync21(staleTimer)) {
|
|
11401
11732
|
process.stdout.write(
|
|
11402
11733
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
11403
11734
|
`
|
|
@@ -11423,7 +11754,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
11423
11754
|
const tracker = new Tracker(c, cfg.ship);
|
|
11424
11755
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
11425
11756
|
for (const r of await resolvedRepos(c)) {
|
|
11426
|
-
if (!
|
|
11757
|
+
if (!existsSync21(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
11427
11758
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
11428
11759
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
11429
11760
|
}
|
|
@@ -11441,7 +11772,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
11441
11772
|
}
|
|
11442
11773
|
async function doctorFix(o) {
|
|
11443
11774
|
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) =>
|
|
11775
|
+
const unitExists = (p) => existsSync21(p);
|
|
11445
11776
|
const crontab = o.host === "linux" ? spawnSync3("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
11446
11777
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
11447
11778
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -11458,7 +11789,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
11458
11789
|
for (const a of plan.actions) {
|
|
11459
11790
|
if (a.kind === "enable") {
|
|
11460
11791
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
11461
|
-
|
|
11792
|
+
writeFileSync10(cfg.configFile, planEnable(readFileSync15(cfg.configFile, "utf8"), a.route));
|
|
11462
11793
|
enabledSound.add(a.route);
|
|
11463
11794
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
11464
11795
|
} else {
|
|
@@ -12355,7 +12686,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12355
12686
|
process.exit(1);
|
|
12356
12687
|
}
|
|
12357
12688
|
requireArmed("run the daemon loop");
|
|
12358
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
12689
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join21(CREW_HOME, "src"), join21(CREW_HOME, "dist"), join21(CREW_HOME, "bin")];
|
|
12359
12690
|
const spawnBreaker = new SpawnBreaker();
|
|
12360
12691
|
const stopReason = await runDaemonLoop({
|
|
12361
12692
|
runPass: () => runOnePass({
|
|
@@ -12441,7 +12772,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12441
12772
|
}
|
|
12442
12773
|
try {
|
|
12443
12774
|
const prompt = await currentPersonaPrompt(promptTarget, role, { userAgent: cfg.ship.userAgent });
|
|
12444
|
-
process.stdout.write(prompt);
|
|
12775
|
+
process.stdout.write(prompt + (role === "pair" ? harnessPromptNote(value("harness")) : ""));
|
|
12445
12776
|
} catch (e) {
|
|
12446
12777
|
if (e instanceof AgentsSyncError) {
|
|
12447
12778
|
process.stderr.write(`crew agents prompt: ${e.message}
|
|
@@ -12504,11 +12835,11 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12504
12835
|
`);
|
|
12505
12836
|
if (!dryRun) {
|
|
12506
12837
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
12507
|
-
const raw =
|
|
12838
|
+
const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
|
|
12508
12839
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
12509
12840
|
raw.agentSkills = skillResult.agentSkills;
|
|
12510
|
-
|
|
12511
|
-
|
|
12841
|
+
mkdirSync12(dirOf(resolvedPath), { recursive: true });
|
|
12842
|
+
writeFileSync10(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
12512
12843
|
`);
|
|
12513
12844
|
}
|
|
12514
12845
|
if (personaResult.outcomes.some((o) => o.action === "diverged") || skillResult.outcomes.some((o) => o.action === "diverged")) {
|
|
@@ -12553,10 +12884,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12553
12884
|
`);
|
|
12554
12885
|
if (!dryRun) {
|
|
12555
12886
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
12556
|
-
const raw =
|
|
12887
|
+
const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
|
|
12557
12888
|
raw.agentSkills = result.agentSkills;
|
|
12558
|
-
|
|
12559
|
-
|
|
12889
|
+
mkdirSync12(dirOf(resolvedPath), { recursive: true });
|
|
12890
|
+
writeFileSync10(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
12560
12891
|
`);
|
|
12561
12892
|
}
|
|
12562
12893
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -12569,14 +12900,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12569
12900
|
process.exit(2);
|
|
12570
12901
|
};
|
|
12571
12902
|
if (sub !== "add" && sub !== "list") usage2();
|
|
12572
|
-
const configText =
|
|
12903
|
+
const configText = readFileSync15(cfg.configFile, "utf8");
|
|
12573
12904
|
let routeName = positional[2];
|
|
12574
12905
|
let addPath = ".";
|
|
12575
12906
|
let addName;
|
|
12576
12907
|
if (sub === "list" && !routeName) usage2();
|
|
12577
12908
|
if (sub === "add") {
|
|
12578
12909
|
try {
|
|
12579
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
12910
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync21(resolvePath(p.replace(/^~(?=\/|$)/, homedir6()))));
|
|
12580
12911
|
routeName = a.route;
|
|
12581
12912
|
addPath = a.path;
|
|
12582
12913
|
addName = a.name;
|
|
@@ -12591,15 +12922,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12591
12922
|
}
|
|
12592
12923
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
12593
12924
|
let resolvedRemotes;
|
|
12594
|
-
if (
|
|
12925
|
+
if (existsSync21(resolvedFile)) {
|
|
12595
12926
|
try {
|
|
12596
|
-
resolvedRemotes = JSON.parse(
|
|
12927
|
+
resolvedRemotes = JSON.parse(readFileSync15(resolvedFile, "utf8"));
|
|
12597
12928
|
} catch {
|
|
12598
12929
|
}
|
|
12599
12930
|
}
|
|
12600
12931
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
12601
12932
|
const text = configText;
|
|
12602
|
-
const base =
|
|
12933
|
+
const base = dirname12(cfg.configFile);
|
|
12603
12934
|
try {
|
|
12604
12935
|
if (sub === "list") {
|
|
12605
12936
|
const repos = configuredRepos(text, routeName, base);
|
|
@@ -12608,14 +12939,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12608
12939
|
for (const r of repos) {
|
|
12609
12940
|
const origin2 = originOf(r.dir);
|
|
12610
12941
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
12611
|
-
const note = !
|
|
12942
|
+
const note = !existsSync21(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
12612
12943
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
12613
12944
|
`);
|
|
12614
12945
|
}
|
|
12615
12946
|
break;
|
|
12616
12947
|
}
|
|
12617
|
-
const target = resolvePath(addPath.replace(/^~(?=\/|$)/,
|
|
12618
|
-
const top =
|
|
12948
|
+
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir6()));
|
|
12949
|
+
const top = existsSync21(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
12619
12950
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
12620
12951
|
const origin = originOf(top);
|
|
12621
12952
|
const plan = planRepoAdd({
|
|
@@ -12663,15 +12994,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12663
12994
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
12664
12995
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
12665
12996
|
`);
|
|
12666
|
-
if (
|
|
12667
|
-
const raw = JSON.parse(
|
|
12997
|
+
if (existsSync21(resolvedFile)) {
|
|
12998
|
+
const raw = JSON.parse(readFileSync15(resolvedFile, "utf8"));
|
|
12668
12999
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
12669
13000
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
12670
|
-
|
|
13001
|
+
writeFileSync10(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
12671
13002
|
`);
|
|
12672
13003
|
}
|
|
12673
13004
|
}
|
|
12674
|
-
|
|
13005
|
+
writeFileSync10(cfg.configFile, plan.text);
|
|
12675
13006
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
12676
13007
|
`);
|
|
12677
13008
|
} catch (e) {
|
|
@@ -12910,10 +13241,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
12910
13241
|
operator = await pickOperator(found.holds);
|
|
12911
13242
|
}
|
|
12912
13243
|
let previous;
|
|
12913
|
-
const hadPreviousFile =
|
|
13244
|
+
const hadPreviousFile = existsSync21(resolvedPath);
|
|
12914
13245
|
if (hadPreviousFile) {
|
|
12915
13246
|
try {
|
|
12916
|
-
previous = JSON.parse(
|
|
13247
|
+
previous = JSON.parse(readFileSync15(resolvedPath, "utf8"));
|
|
12917
13248
|
} catch {
|
|
12918
13249
|
}
|
|
12919
13250
|
}
|
|
@@ -12974,10 +13305,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
12974
13305
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
12975
13306
|
`);
|
|
12976
13307
|
} else {
|
|
12977
|
-
|
|
13308
|
+
mkdirSync12(dirname12(resolvedPath), { recursive: true });
|
|
12978
13309
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
12979
13310
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
12980
|
-
|
|
13311
|
+
writeFileSync10(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
12981
13312
|
`);
|
|
12982
13313
|
renameSync2(tmpPath, resolvedPath);
|
|
12983
13314
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -12991,8 +13322,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
12991
13322
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
12992
13323
|
`);
|
|
12993
13324
|
} else {
|
|
12994
|
-
|
|
12995
|
-
|
|
13325
|
+
mkdirSync12(dirname12(apiKeyFile), { recursive: true });
|
|
13326
|
+
writeFileSync10(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
12996
13327
|
`, { mode: 384 });
|
|
12997
13328
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
12998
13329
|
`);
|
|
@@ -13001,7 +13332,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13001
13332
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
13002
13333
|
const wizard = firstRun;
|
|
13003
13334
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
13004
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
13335
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join21(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
13005
13336
|
const routeBlock = renderConnection(
|
|
13006
13337
|
found,
|
|
13007
13338
|
route2,
|
|
@@ -13029,6 +13360,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13029
13360
|
if (seen.has(id)) continue;
|
|
13030
13361
|
seen.add(id);
|
|
13031
13362
|
process.stderr.write(`${adapterFor(tc.harness).ensureMcp({ bin: tc.bin, url: mcpUrlFor(baseUrl), key: apiKey, replace, dryRun })}
|
|
13363
|
+
`);
|
|
13364
|
+
}
|
|
13365
|
+
for (const h of shipPair(cfg.ship).filter((x) => x !== "claude")) {
|
|
13366
|
+
process.stderr.write(`${pairInstallerFor(h).installPair({ url: mcpUrlFor(baseUrl), replace, dryRun })}
|
|
13032
13367
|
`);
|
|
13033
13368
|
}
|
|
13034
13369
|
}
|
|
@@ -13482,6 +13817,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13482
13817
|
`);
|
|
13483
13818
|
break;
|
|
13484
13819
|
}
|
|
13820
|
+
case "disconnect": {
|
|
13821
|
+
const url = mcpUrlFor(value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL);
|
|
13822
|
+
for (const h of shipPair(cfg.ship)) {
|
|
13823
|
+
process.stderr.write(`${pairInstallerFor(h).removePair({ url, bin: cfg.ship.agent.bin, dryRun })}
|
|
13824
|
+
`);
|
|
13825
|
+
}
|
|
13826
|
+
break;
|
|
13827
|
+
}
|
|
13485
13828
|
case "install": {
|
|
13486
13829
|
if (!await installScheduler({ replace: flag("replace") })) process.exit(2);
|
|
13487
13830
|
break;
|
|
@@ -13521,7 +13864,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13521
13864
|
}
|
|
13522
13865
|
case "log": {
|
|
13523
13866
|
try {
|
|
13524
|
-
const text =
|
|
13867
|
+
const text = readFileSync15(cfg.ship.logFile, "utf8").split("\n");
|
|
13525
13868
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
13526
13869
|
`);
|
|
13527
13870
|
} catch {
|
|
@@ -13729,7 +14072,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
13729
14072
|
);
|
|
13730
14073
|
try {
|
|
13731
14074
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
13732
|
-
const evSize =
|
|
14075
|
+
const evSize = existsSync21(evFile) ? statSync9(evFile).size : 0;
|
|
13733
14076
|
process.stdout.write(
|
|
13734
14077
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
13735
14078
|
` : "events log: ok\n"
|
|
@@ -13853,15 +14196,19 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
13853
14196
|
let allBinsFound = true;
|
|
13854
14197
|
for (const tc of shipToolchains(cfg.ship)) {
|
|
13855
14198
|
const adapter = adapterFor(tc.harness);
|
|
13856
|
-
const binPath = findOnPath(tc.launcher ?? tc.bin, schedulerPath);
|
|
14199
|
+
const binPath = findOnPath(tc.launcherBin ?? tc.launcher ?? tc.bin, schedulerPath);
|
|
13857
14200
|
if (!binPath) allBinsFound = false;
|
|
13858
14201
|
const label = tc.name === defaultToolchain ? "agent binary: " : `toolchain ${tc.name}:`.padEnd(18);
|
|
13859
14202
|
process.stdout.write(
|
|
13860
|
-
`${label} ${tc.launcher ?? tc.bin} \u2014 ${binPath ? `OK (${binPath})` : "NOT FOUND on the scheduler PATH"}${tc.name === defaultToolchain ? "" : ` [${adapter.displayName}]`}
|
|
14203
|
+
`${label} ${tc.launcherBin ?? tc.launcher ?? tc.bin} \u2014 ${binPath ? `OK (${binPath})` : "NOT FOUND on the scheduler PATH"}${tc.name === defaultToolchain ? "" : ` [${adapter.displayName}]`}
|
|
13861
14204
|
`
|
|
13862
14205
|
);
|
|
13863
14206
|
for (const limit of adapter.limitations) process.stdout.write(` cannot enforce: ${limit}
|
|
13864
14207
|
`);
|
|
14208
|
+
if (isOriLauncher(tc)) {
|
|
14209
|
+
process.stdout.write(` ori: headless auth under the scheduler is unverified \u2014 run \`ori login\` as the scheduler user, then confirm a run records provider openrouter (see docs/toolchains.md)
|
|
14210
|
+
`);
|
|
14211
|
+
}
|
|
13865
14212
|
}
|
|
13866
14213
|
process.stdout.write(`git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler PATH"}
|
|
13867
14214
|
`);
|
|
@@ -13889,6 +14236,13 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
13889
14236
|
);
|
|
13890
14237
|
}
|
|
13891
14238
|
}
|
|
14239
|
+
for (const h of shipPair(cfg.ship).filter((x) => x !== "claude")) {
|
|
14240
|
+
const inst = pairInstallerFor(h);
|
|
14241
|
+
const v = inst.verifyPair({ url: mcpUrlFor(route.baseUrl) });
|
|
14242
|
+
process.stdout.write(`pair ${h}:`.padEnd(19) + (v.ok ? ` installed (${v.detail})
|
|
14243
|
+
` : ` NOT installed \u2014 ${v.detail}; run crew connect
|
|
14244
|
+
`));
|
|
14245
|
+
}
|
|
13892
14246
|
const shipVendors = new Set(shipToolchains(cfg.ship).map(toolchainVendor));
|
|
13893
14247
|
if (route.apiKey && route.resolved) {
|
|
13894
14248
|
let flagged = 0;
|