@tablation/crew 0.35.0 → 0.37.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/crew.example.yaml +1 -1
- package/dist/cli.js +510 -269
- 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 dirname12, join as
|
|
4
|
+
import { resolve as resolve4, dirname as dirname12, join as join22, basename as basename9 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
-
import { hostname as hostname2, homedir as
|
|
7
|
-
import { spawnSync as
|
|
6
|
+
import { hostname as hostname2, homedir as homedir7 } from "os";
|
|
7
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
10
|
-
import { readFileSync as readFileSync5, existsSync as
|
|
11
|
-
import { resolve, dirname as dirname3, isAbsolute, join as
|
|
12
|
-
import { homedir as
|
|
10
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
11
|
+
import { resolve, dirname as dirname3, isAbsolute, join as join6, basename as basename3 } from "path";
|
|
12
|
+
import { homedir as homedir4, tmpdir } from "os";
|
|
13
13
|
import { parse as parse2 } from "yaml";
|
|
14
14
|
|
|
15
15
|
// src/events.ts
|
|
@@ -158,10 +158,10 @@ var Emitter = class _Emitter {
|
|
|
158
158
|
var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
|
|
159
159
|
|
|
160
160
|
// src/pair-install.ts
|
|
161
|
-
import { existsSync as
|
|
162
|
-
import { spawnSync as
|
|
163
|
-
import { homedir as
|
|
164
|
-
import {
|
|
161
|
+
import { existsSync as existsSync3, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
162
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
163
|
+
import { homedir as homedir3 } from "os";
|
|
164
|
+
import { join as join4 } from "path";
|
|
165
165
|
|
|
166
166
|
// src/stream.ts
|
|
167
167
|
function toolTarget(name, input) {
|
|
@@ -409,7 +409,15 @@ var claudeAdapter = {
|
|
|
409
409
|
ensureMcp: ensureClaudeMcp
|
|
410
410
|
};
|
|
411
411
|
|
|
412
|
-
// src/
|
|
412
|
+
// src/adapters/codex.ts
|
|
413
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
414
|
+
import { homedir as homedir2 } from "os";
|
|
415
|
+
import { join as join3 } from "path";
|
|
416
|
+
import { rmSync as rmSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
417
|
+
|
|
418
|
+
// src/pair-files.ts
|
|
419
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync, rmSync as rmSync2 } from "fs";
|
|
420
|
+
import { dirname as dirname2 } from "path";
|
|
413
421
|
var PAIR_BLOCK_START = "<!-- crew:pair start -->";
|
|
414
422
|
var PAIR_BLOCK_END = "<!-- crew:pair end -->";
|
|
415
423
|
var PAIR_KEY_VAR = "CREW_API_KEY";
|
|
@@ -497,9 +505,6 @@ function tomlTableRange(lines, name) {
|
|
|
497
505
|
return [start, end];
|
|
498
506
|
}
|
|
499
507
|
var tomlString = (v) => JSON.stringify(v);
|
|
500
|
-
function codexTable(url) {
|
|
501
|
-
return [`[mcp_servers.${SERVER}]`, `url = ${tomlString(url)}`, `bearer_token_env_var = ${tomlString(PAIR_KEY_VAR)}`];
|
|
502
|
-
}
|
|
503
508
|
function upsertTomlTable(text, name, table2) {
|
|
504
509
|
const lines = text.split("\n");
|
|
505
510
|
const r = tomlTableRange(lines, name);
|
|
@@ -535,6 +540,69 @@ function tomlValue(lines, range, key) {
|
|
|
535
540
|
}
|
|
536
541
|
return void 0;
|
|
537
542
|
}
|
|
543
|
+
|
|
544
|
+
// src/adapters/codex.ts
|
|
545
|
+
var CODEX_BILLING_VARS_TO_UNSET = [
|
|
546
|
+
"OPENAI_API_KEY",
|
|
547
|
+
"OPENAI_BASE_URL",
|
|
548
|
+
"OPENAI_API_BASE",
|
|
549
|
+
"OPENAI_ORG_ID",
|
|
550
|
+
"OPENAI_PROJECT",
|
|
551
|
+
"CODEX_API_KEY"
|
|
552
|
+
];
|
|
553
|
+
function codexSandboxFor(p) {
|
|
554
|
+
return p.level === "read" ? "read-only" : "danger-full-access";
|
|
555
|
+
}
|
|
556
|
+
var REASONING_EFFORTS = ["minimal", "low", "medium", "high"];
|
|
557
|
+
function commandTarget(command2) {
|
|
558
|
+
if (!command2) return void 0;
|
|
559
|
+
const wrapped = /^(?:\S*\/)?(?:ba|z|da)?sh\s+-\w*c\s+(['"]?)([\s\S]*)\1$/.exec(command2.trim());
|
|
560
|
+
const inner = (wrapped ? wrapped[2] : command2).trim();
|
|
561
|
+
return inner.split(/\s+/)[0] || void 0;
|
|
562
|
+
}
|
|
563
|
+
function mapCodexLine(line) {
|
|
564
|
+
const l = line;
|
|
565
|
+
if (!l || l.type !== "item.completed" || !l.item) return [];
|
|
566
|
+
const it = l.item;
|
|
567
|
+
switch (it.type) {
|
|
568
|
+
case "reasoning":
|
|
569
|
+
return it.text ? [{ kind: "thought", text: it.text }] : [];
|
|
570
|
+
case "agent_message":
|
|
571
|
+
return it.text ? [{ kind: "text", text: it.text }] : [];
|
|
572
|
+
case "command_execution":
|
|
573
|
+
return [{ kind: "tool", tool: "command", target: commandTarget(it.command) }];
|
|
574
|
+
case "file_change":
|
|
575
|
+
return [{ kind: "tool", tool: "file_change", target: it.changes?.[0]?.path }];
|
|
576
|
+
case "mcp_tool_call":
|
|
577
|
+
return [{ kind: "tool", tool: `mcp__${it.server ?? "mcp"}__${it.tool ?? "tool"}` }];
|
|
578
|
+
case "web_search":
|
|
579
|
+
return [{ kind: "tool", tool: "web_search", target: it.query }];
|
|
580
|
+
default:
|
|
581
|
+
return [];
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function extractCodexResult(line) {
|
|
585
|
+
const l = line;
|
|
586
|
+
if (!l) return void 0;
|
|
587
|
+
if (l.type === "thread.started" && l.thread_id) return { sessionId: l.thread_id };
|
|
588
|
+
if (l.type === "turn.completed" && l.usage) {
|
|
589
|
+
return {
|
|
590
|
+
inputTokens: l.usage.input_tokens,
|
|
591
|
+
outputTokens: l.usage.output_tokens,
|
|
592
|
+
cacheReadTokens: l.usage.cached_input_tokens,
|
|
593
|
+
cacheWriteTokens: l.usage.cache_write_input_tokens
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
return void 0;
|
|
597
|
+
}
|
|
598
|
+
function extractCodexTurnTokens(line) {
|
|
599
|
+
const l = line;
|
|
600
|
+
if (!l || l.type !== "turn.completed" || !l.usage) return void 0;
|
|
601
|
+
return (l.usage.input_tokens ?? 0) + (l.usage.output_tokens ?? 0);
|
|
602
|
+
}
|
|
603
|
+
function codexTable(url) {
|
|
604
|
+
return [`[mcp_servers.${SERVER}]`, `url = ${tomlString(url)}`, `bearer_token_env_var = ${tomlString(PAIR_KEY_VAR)}`];
|
|
605
|
+
}
|
|
538
606
|
var codexPaths = (home) => ({
|
|
539
607
|
instructions: join3(home, ".codex", "AGENTS.md"),
|
|
540
608
|
config: join3(home, ".codex", "config.toml")
|
|
@@ -587,16 +655,105 @@ var codexPair = {
|
|
|
587
655
|
const kept = st.state === "different" ? `; left the existing ${SERVER} server alone \u2014 ${st.detail}` : "";
|
|
588
656
|
if (hadMcp && !o.dryRun) {
|
|
589
657
|
const after = removeTomlTable(config, `mcp_servers.${SERVER}`);
|
|
590
|
-
if (after === "")
|
|
591
|
-
else
|
|
658
|
+
if (after === "") rmSync3(p.config);
|
|
659
|
+
else writeFileSync2(p.config, after);
|
|
592
660
|
}
|
|
593
661
|
const what = hadBlock && hadMcp ? `the Pair block and ${SERVER} server` : hadBlock ? "the Pair block" : `the ${SERVER} server`;
|
|
594
662
|
return hadBlock || hadMcp ? `pair(codex): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what}${kept}` : `pair(codex): nothing to remove${kept}`;
|
|
595
663
|
}
|
|
596
664
|
};
|
|
665
|
+
function binMissing(bin) {
|
|
666
|
+
const r = spawnSync2(bin, ["--version"], { encoding: "utf8", timeout: 3e4 });
|
|
667
|
+
return r.error?.code === "ENOENT";
|
|
668
|
+
}
|
|
669
|
+
var codexAdapter = {
|
|
670
|
+
harness: "codex",
|
|
671
|
+
displayName: "Codex",
|
|
672
|
+
vendor: "openai",
|
|
673
|
+
ownsModel: (model) => /^(gpt-|o\d|codex-)/.test(model),
|
|
674
|
+
// The tier's reasoning is Codex's effort level; anything else is ignored.
|
|
675
|
+
applyReasoning: (spec, reasoning) => REASONING_EFFORTS.includes(reasoning) ? { ...spec, reasoningEffort: reasoning } : spec,
|
|
676
|
+
defaultBin: "codex",
|
|
677
|
+
// What Codex CLI itself starts with today (the `gpt-5-codex` family the first
|
|
678
|
+
// cut named now answers 404 "does not exist or you do not have access" from
|
|
679
|
+
// the Responses API for a 2026 API key, which the recording for CREW-1520
|
|
680
|
+
// hit first). An operator pins something else per toolchain entry or tier.
|
|
681
|
+
defaultModel: "gpt-5.5",
|
|
682
|
+
defaultTiers: {
|
|
683
|
+
light: { model: "gpt-5.4-mini", reasoning: "low" },
|
|
684
|
+
standard: { model: "gpt-5.5" },
|
|
685
|
+
deep: { model: "gpt-5.5", reasoning: "high" }
|
|
686
|
+
},
|
|
687
|
+
limitations: [
|
|
688
|
+
"no per-tool allow or deny list, and a push (even a forced one) is not blocked",
|
|
689
|
+
"the write (QA) and edit (dev, design) profiles run with no filesystem sandbox (danger-full-access): Codex's workspace-write keeps every .git directory read-only, so a sandboxed seat could not cut a worktree or commit, and a headless run cannot approve the escalation",
|
|
690
|
+
"the read profile (triage) runs in the read-only sandbox, which has no network, so it can reach the tracker only through the MCP tools, not curl",
|
|
691
|
+
"the design canvas and skills are unavailable",
|
|
692
|
+
"no cost is reported (tokens only)"
|
|
693
|
+
],
|
|
694
|
+
invocation(spec, o) {
|
|
695
|
+
const profile = profileFor(o.role);
|
|
696
|
+
const mode = codexSandboxFor(profile);
|
|
697
|
+
const cfg = (k, v) => ["-c", `${k}=${tomlString(v)}`];
|
|
698
|
+
const args = [
|
|
699
|
+
"exec",
|
|
700
|
+
"--json",
|
|
701
|
+
"--skip-git-repo-check",
|
|
702
|
+
"--sandbox",
|
|
703
|
+
mode,
|
|
704
|
+
"--model",
|
|
705
|
+
o.model,
|
|
706
|
+
// Headless: nobody can answer an approval prompt.
|
|
707
|
+
"-c",
|
|
708
|
+
'approval_policy="never"',
|
|
709
|
+
...spec.reasoningEffort ? cfg("model_reasoning_effort", spec.reasoningEffort) : [],
|
|
710
|
+
// The tracker MCP for this run only, as -c overrides: the config file is
|
|
711
|
+
// untouched and the key is named by variable, never written (§7).
|
|
712
|
+
// `default_tools_approval_mode="approve"` is load-bearing: Codex asks
|
|
713
|
+
// before an MCP tool call by default, and under `approval_policy="never"`
|
|
714
|
+
// that question is auto-rejected ("MCP tool call requires approval, but
|
|
715
|
+
// approval policy is never" — seen on the first real recording), which
|
|
716
|
+
// would cut a headless seat off from the tracker entirely.
|
|
717
|
+
...o.mcp ? [
|
|
718
|
+
...cfg(`mcp_servers.${SERVER}.url`, o.mcp.url),
|
|
719
|
+
...cfg(`mcp_servers.${SERVER}.bearer_token_env_var`, o.mcp.keyVar),
|
|
720
|
+
...cfg(`mcp_servers.${SERVER}.default_tools_approval_mode`, "approve")
|
|
721
|
+
] : [],
|
|
722
|
+
...spec.args,
|
|
723
|
+
// Prompt on stdin.
|
|
724
|
+
"-"
|
|
725
|
+
];
|
|
726
|
+
return spec.launcher ? { bin: spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
|
|
727
|
+
},
|
|
728
|
+
runEnv: (spec) => ({ ...spec.env }),
|
|
729
|
+
provider: () => "openai",
|
|
730
|
+
unsetEnv: CODEX_BILLING_VARS_TO_UNSET,
|
|
731
|
+
mapStreamLine: mapCodexLine,
|
|
732
|
+
extractResult: extractCodexResult,
|
|
733
|
+
extractTurnTokens: extractCodexTurnTokens,
|
|
734
|
+
inspectMcp(o) {
|
|
735
|
+
if (binMissing(o.bin)) return { state: "no-binary" };
|
|
736
|
+
const st = codexMcpState(readIfExists(codexPaths(o.home ?? homedir2()).config) ?? "", o.url);
|
|
737
|
+
return st.state === "registered" ? { state: "registered", url: o.url } : st;
|
|
738
|
+
},
|
|
739
|
+
ensureMcp(o) {
|
|
740
|
+
const p = codexPaths(o.home ?? homedir2());
|
|
741
|
+
const config = readIfExists(p.config) ?? "";
|
|
742
|
+
const st = codexMcpState(config, o.url);
|
|
743
|
+
if (st.state === "registered") return `mcp: ${SERVER} already registered with Codex \u2192 ${o.url}`;
|
|
744
|
+
if (st.state === "different" && !o.replace) {
|
|
745
|
+
return `mcp: left Codex's "${SERVER}" server alone \u2014 ${st.detail}. Re-run with --mcp replace to overwrite it.`;
|
|
746
|
+
}
|
|
747
|
+
if (o.dryRun) return `(dry run) would register the ${SERVER} MCP server with Codex \u2192 ${o.url}`;
|
|
748
|
+
writeFileMkdir(p.config, upsertTomlTable(config, `mcp_servers.${SERVER}`, codexTable(o.url)));
|
|
749
|
+
return `mcp: registered the ${SERVER} MCP server with Codex \u2192 ${o.url} (${p.config}; key read from $${PAIR_KEY_VAR})`;
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// src/pair-install.ts
|
|
597
754
|
var geminiPaths = (home) => ({
|
|
598
|
-
instructions:
|
|
599
|
-
config:
|
|
755
|
+
instructions: join4(home, ".gemini", "GEMINI.md"),
|
|
756
|
+
config: join4(home, ".gemini", "settings.json")
|
|
600
757
|
});
|
|
601
758
|
function readGeminiSettings(path) {
|
|
602
759
|
const text = readIfExists(path);
|
|
@@ -617,14 +774,14 @@ function geminiMcpState(s, url) {
|
|
|
617
774
|
return { state: "registered" };
|
|
618
775
|
}
|
|
619
776
|
function inspectGeminiMcp(o) {
|
|
620
|
-
const p = geminiPaths(o.home ??
|
|
777
|
+
const p = geminiPaths(o.home ?? homedir3());
|
|
621
778
|
const settings = readGeminiSettings(p.config);
|
|
622
779
|
if (!settings) return { state: "different", detail: `${p.config} is not a JSON object` };
|
|
623
780
|
const st = geminiMcpState(settings, o.url);
|
|
624
781
|
return st.state === "registered" ? { state: "registered", url: o.url } : st;
|
|
625
782
|
}
|
|
626
783
|
function ensureGeminiMcp(o) {
|
|
627
|
-
const p = geminiPaths(o.home ??
|
|
784
|
+
const p = geminiPaths(o.home ?? homedir3());
|
|
628
785
|
const settings = readGeminiSettings(p.config);
|
|
629
786
|
if (!settings) return `gemini: ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
|
|
630
787
|
const st = geminiMcpState(settings, o.url);
|
|
@@ -642,7 +799,7 @@ var geminiPair = {
|
|
|
642
799
|
harness: "gemini",
|
|
643
800
|
displayName: "Gemini CLI",
|
|
644
801
|
installPair(o) {
|
|
645
|
-
const p = geminiPaths(o.home ??
|
|
802
|
+
const p = geminiPaths(o.home ?? homedir3());
|
|
646
803
|
const settings = readGeminiSettings(p.config);
|
|
647
804
|
if (!settings) return `pair(gemini): ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
|
|
648
805
|
const st = geminiMcpState(settings, o.url);
|
|
@@ -661,7 +818,7 @@ var geminiPair = {
|
|
|
661
818
|
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})`;
|
|
662
819
|
},
|
|
663
820
|
verifyPair(o) {
|
|
664
|
-
const p = geminiPaths(o.home ??
|
|
821
|
+
const p = geminiPaths(o.home ?? homedir3());
|
|
665
822
|
const b = blockVerdict(p.instructions, "gemini");
|
|
666
823
|
if (!b.ok) return b;
|
|
667
824
|
const settings = readGeminiSettings(p.config);
|
|
@@ -672,7 +829,7 @@ var geminiPair = {
|
|
|
672
829
|
return { ok: true, detail: `${p.instructions}, ${p.config}` };
|
|
673
830
|
},
|
|
674
831
|
removePair(o) {
|
|
675
|
-
const p = geminiPaths(o.home ??
|
|
832
|
+
const p = geminiPaths(o.home ?? homedir3());
|
|
676
833
|
const hadBlock = removeBlockFile(p.instructions, o.dryRun);
|
|
677
834
|
const settings = readGeminiSettings(p.config);
|
|
678
835
|
const st = settings ? geminiMcpState(settings, o.url) : { state: "missing" };
|
|
@@ -683,8 +840,8 @@ var geminiPair = {
|
|
|
683
840
|
const next = { ...settings };
|
|
684
841
|
if (Object.keys(rest).length > 0) next.mcpServers = rest;
|
|
685
842
|
else delete next.mcpServers;
|
|
686
|
-
if (Object.keys(next).length === 0)
|
|
687
|
-
else
|
|
843
|
+
if (Object.keys(next).length === 0) rmSync4(p.config);
|
|
844
|
+
else writeFileSync3(p.config, `${JSON.stringify(next, null, 2)}
|
|
688
845
|
`);
|
|
689
846
|
}
|
|
690
847
|
const what = hadBlock && hadMcp ? `the Pair block and ${SERVER} server` : hadBlock ? "the Pair block" : `the ${SERVER} server`;
|
|
@@ -693,11 +850,11 @@ var geminiPair = {
|
|
|
693
850
|
};
|
|
694
851
|
var VSCODE_FLAVOURS = ["Code", "Code - Insiders"];
|
|
695
852
|
function vscodeUserDirs(o) {
|
|
696
|
-
const home = o.home ??
|
|
853
|
+
const home = o.home ?? homedir3();
|
|
697
854
|
const platform2 = o.platform ?? process.platform;
|
|
698
|
-
const root = platform2 === "darwin" ?
|
|
699
|
-
const all = VSCODE_FLAVOURS.map((f) =>
|
|
700
|
-
const present = all.filter((d) =>
|
|
855
|
+
const root = platform2 === "darwin" ? join4(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join4(home, "AppData", "Roaming") : join4(home, ".config");
|
|
856
|
+
const all = VSCODE_FLAVOURS.map((f) => join4(root, f, "User"));
|
|
857
|
+
const present = all.filter((d) => existsSync3(d));
|
|
701
858
|
return present.length > 0 ? present : [all[0]];
|
|
702
859
|
}
|
|
703
860
|
var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
|
|
@@ -734,8 +891,8 @@ function removeCopilotInstructions(path, dryRun) {
|
|
|
734
891
|
if (before === void 0 || !hasPairBlock(before)) return false;
|
|
735
892
|
if (dryRun) return true;
|
|
736
893
|
const after = removePairBlock(before);
|
|
737
|
-
if (after.trim() === "" || after.trim() === COPILOT_FRONTMATTER.trim())
|
|
738
|
-
else
|
|
894
|
+
if (after.trim() === "" || after.trim() === COPILOT_FRONTMATTER.trim()) rmSync4(path);
|
|
895
|
+
else writeFileSync3(path, after);
|
|
739
896
|
return true;
|
|
740
897
|
}
|
|
741
898
|
var copilotPair = {
|
|
@@ -744,8 +901,8 @@ var copilotPair = {
|
|
|
744
901
|
installPair(o) {
|
|
745
902
|
const lines = [];
|
|
746
903
|
for (const dir of vscodeUserDirs(o)) {
|
|
747
|
-
const mcpPath =
|
|
748
|
-
const instrPath =
|
|
904
|
+
const mcpPath = join4(dir, "mcp.json");
|
|
905
|
+
const instrPath = join4(dir, "prompts", "crew-pair.instructions.md");
|
|
749
906
|
const settings = readVscodeMcp(mcpPath);
|
|
750
907
|
if (!settings) {
|
|
751
908
|
lines.push(`pair(copilot): ${mcpPath} is not a plain JSON object (comments are not supported) \u2014 fix it by hand; nothing was written there`);
|
|
@@ -773,8 +930,8 @@ var copilotPair = {
|
|
|
773
930
|
},
|
|
774
931
|
verifyPair(o) {
|
|
775
932
|
const verdicts = vscodeUserDirs(o).map((dir) => {
|
|
776
|
-
const instrPath =
|
|
777
|
-
const mcpPath =
|
|
933
|
+
const instrPath = join4(dir, "prompts", "crew-pair.instructions.md");
|
|
934
|
+
const mcpPath = join4(dir, "mcp.json");
|
|
778
935
|
const b = blockVerdict(instrPath, "copilot");
|
|
779
936
|
if (!b.ok) return b;
|
|
780
937
|
const settings = readVscodeMcp(mcpPath);
|
|
@@ -789,8 +946,8 @@ var copilotPair = {
|
|
|
789
946
|
removePair(o) {
|
|
790
947
|
const lines = [];
|
|
791
948
|
for (const dir of vscodeUserDirs(o)) {
|
|
792
|
-
const mcpPath =
|
|
793
|
-
const hadBlock = removeCopilotInstructions(
|
|
949
|
+
const mcpPath = join4(dir, "mcp.json");
|
|
950
|
+
const hadBlock = removeCopilotInstructions(join4(dir, "prompts", "crew-pair.instructions.md"), o.dryRun);
|
|
794
951
|
const settings = readVscodeMcp(mcpPath);
|
|
795
952
|
const st = settings ? copilotMcpState(settings, o.url) : { state: "missing" };
|
|
796
953
|
const hadMcp = st.state === "registered";
|
|
@@ -800,8 +957,8 @@ var copilotPair = {
|
|
|
800
957
|
const next = { ...settings };
|
|
801
958
|
if (Object.keys(rest).length > 0) next.servers = rest;
|
|
802
959
|
else delete next.servers;
|
|
803
|
-
if (Object.keys(next).length === 0)
|
|
804
|
-
else
|
|
960
|
+
if (Object.keys(next).length === 0) rmSync4(mcpPath);
|
|
961
|
+
else writeFileSync3(mcpPath, `${JSON.stringify(next, null, 2)}
|
|
805
962
|
`);
|
|
806
963
|
}
|
|
807
964
|
const what = hadBlock && hadMcp ? `the Pair instructions and ${SERVER} server` : hadBlock ? "the Pair instructions" : `the ${SERVER} server`;
|
|
@@ -817,7 +974,7 @@ var copilotPair = {
|
|
|
817
974
|
envWarning(o) {
|
|
818
975
|
if ((o.platform ?? process.platform) !== "darwin") return void 0;
|
|
819
976
|
const get2 = o.loginEnv ?? ((name) => {
|
|
820
|
-
const r =
|
|
977
|
+
const r = spawnSync3("launchctl", ["getenv", name], { encoding: "utf8" });
|
|
821
978
|
return r.status === 0 ? r.stdout.trim() : void 0;
|
|
822
979
|
});
|
|
823
980
|
if (get2(PAIR_KEY_VAR)) return void 0;
|
|
@@ -963,6 +1120,7 @@ var geminiAdapter = {
|
|
|
963
1120
|
var TIERS = ["light", "standard", "deep"];
|
|
964
1121
|
var ADAPTERS = {
|
|
965
1122
|
[claudeAdapter.harness]: claudeAdapter,
|
|
1123
|
+
[codexAdapter.harness]: codexAdapter,
|
|
966
1124
|
[geminiAdapter.harness]: geminiAdapter
|
|
967
1125
|
};
|
|
968
1126
|
var HARNESS_NAMES = Object.keys(ADAPTERS);
|
|
@@ -1091,8 +1249,8 @@ function explain(ship, required) {
|
|
|
1091
1249
|
}
|
|
1092
1250
|
|
|
1093
1251
|
// src/repo-config.ts
|
|
1094
|
-
import { readFileSync as readFileSync4, existsSync as
|
|
1095
|
-
import { join as
|
|
1252
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
1253
|
+
import { join as join5, basename as basename2 } from "path";
|
|
1096
1254
|
import { parse } from "yaml";
|
|
1097
1255
|
var SPEC_VERSION = 1;
|
|
1098
1256
|
var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
|
|
@@ -1165,8 +1323,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
|
|
|
1165
1323
|
var PROVIDERS = ["github", "buildkite", "other", "none"];
|
|
1166
1324
|
function findRepoConfig(dir) {
|
|
1167
1325
|
for (const name of REPO_CONFIG_FILENAMES) {
|
|
1168
|
-
const p =
|
|
1169
|
-
if (
|
|
1326
|
+
const p = join5(dir, name);
|
|
1327
|
+
if (existsSync4(p)) return p;
|
|
1170
1328
|
}
|
|
1171
1329
|
return null;
|
|
1172
1330
|
}
|
|
@@ -1808,16 +1966,16 @@ function mergeRouteRelease(route, override) {
|
|
|
1808
1966
|
};
|
|
1809
1967
|
}
|
|
1810
1968
|
function expand(p, base) {
|
|
1811
|
-
let out = p.startsWith("~") ? p.replace(/^~/,
|
|
1812
|
-
out = out.replace(/\$\{?HOME\}?/g,
|
|
1969
|
+
let out = p.startsWith("~") ? p.replace(/^~/, homedir4()) : p;
|
|
1970
|
+
out = out.replace(/\$\{?HOME\}?/g, homedir4());
|
|
1813
1971
|
return isAbsolute(out) ? out : resolve(base, out);
|
|
1814
1972
|
}
|
|
1815
1973
|
function resolvePromptsDir(promptSet, crewHome, base) {
|
|
1816
|
-
if (!promptSet) return
|
|
1974
|
+
if (!promptSet) return join6(crewHome, "prompts", "default");
|
|
1817
1975
|
if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
|
|
1818
1976
|
return expand(promptSet, base);
|
|
1819
1977
|
}
|
|
1820
|
-
return
|
|
1978
|
+
return join6(crewHome, "prompts", promptSet);
|
|
1821
1979
|
}
|
|
1822
1980
|
var Missing = class {
|
|
1823
1981
|
paths = [];
|
|
@@ -1841,7 +1999,7 @@ var Missing = class {
|
|
|
1841
1999
|
};
|
|
1842
2000
|
function crewVersion(crewHome) {
|
|
1843
2001
|
try {
|
|
1844
|
-
const pkg = JSON.parse(readFileSync5(
|
|
2002
|
+
const pkg = JSON.parse(readFileSync5(join6(crewHome, "package.json"), "utf8"));
|
|
1845
2003
|
return pkg.version ?? "0.0.0";
|
|
1846
2004
|
} catch {
|
|
1847
2005
|
return "0.0.0";
|
|
@@ -1851,26 +2009,26 @@ function configSearchPath(crewHome) {
|
|
|
1851
2009
|
if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
|
|
1852
2010
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
1853
2011
|
return [
|
|
1854
|
-
...xdg ? [
|
|
1855
|
-
|
|
2012
|
+
...xdg ? [join6(xdg, "crew", "crew.yaml")] : [],
|
|
2013
|
+
join6(homedir4(), ".config", "crew", "crew.yaml"),
|
|
1856
2014
|
resolve(crewHome, "crew.yaml")
|
|
1857
2015
|
];
|
|
1858
2016
|
}
|
|
1859
2017
|
function findConfigFile(crewHome) {
|
|
1860
2018
|
const candidates = configSearchPath(crewHome);
|
|
1861
|
-
return candidates.find((c) =>
|
|
2019
|
+
return candidates.find((c) => existsSync5(c)) ?? candidates[candidates.length - 1];
|
|
1862
2020
|
}
|
|
1863
|
-
var defaultConfigPath = () =>
|
|
2021
|
+
var defaultConfigPath = () => join6(process.env.XDG_CONFIG_HOME ?? join6(homedir4(), ".config"), "crew", "crew.yaml");
|
|
1864
2022
|
function routeSlug(route) {
|
|
1865
2023
|
return route.replace(/\//g, "-");
|
|
1866
2024
|
}
|
|
1867
2025
|
function resolvedPathFor(stateDir, route) {
|
|
1868
2026
|
const [workspace, project] = splitRoute(route);
|
|
1869
|
-
return
|
|
2027
|
+
return join6(stateDir, "resolved", workspace, `${project}.json`);
|
|
1870
2028
|
}
|
|
1871
2029
|
function apiKeyPathFor(stateDir, route) {
|
|
1872
2030
|
const [workspace, project] = splitRoute(route);
|
|
1873
|
-
return
|
|
2031
|
+
return join6(stateDir, "keys", workspace, `${project}.env`);
|
|
1874
2032
|
}
|
|
1875
2033
|
function splitRoute(route) {
|
|
1876
2034
|
const parts = route.split("/");
|
|
@@ -1881,7 +2039,7 @@ function splitRoute(route) {
|
|
|
1881
2039
|
}
|
|
1882
2040
|
function readResolvedFile(stateDir, route) {
|
|
1883
2041
|
const path = resolvedPathFor(stateDir, route);
|
|
1884
|
-
if (!
|
|
2042
|
+
if (!existsSync5(path)) return void 0;
|
|
1885
2043
|
try {
|
|
1886
2044
|
return JSON.parse(readFileSync5(path, "utf8"));
|
|
1887
2045
|
} catch (e) {
|
|
@@ -1939,13 +2097,13 @@ function parseResolved(raw, m, where) {
|
|
|
1939
2097
|
}
|
|
1940
2098
|
function loadConfig(crewHome, configFile, opts = {}) {
|
|
1941
2099
|
const file = configFile ?? findConfigFile(crewHome);
|
|
1942
|
-
if (opts.text === void 0 && !
|
|
2100
|
+
if (opts.text === void 0 && !existsSync5(file)) {
|
|
1943
2101
|
throw new ConfigError(
|
|
1944
2102
|
`no config found. Looked in:
|
|
1945
2103
|
` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
|
|
1946
2104
|
|
|
1947
2105
|
Create the first of those:
|
|
1948
|
-
mkdir -p ${dirname3(defaultConfigPath())} && cp ${
|
|
2106
|
+
mkdir -p ${dirname3(defaultConfigPath())} && cp ${join6(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
|
|
1949
2107
|
);
|
|
1950
2108
|
}
|
|
1951
2109
|
let raw;
|
|
@@ -2028,7 +2186,7 @@ Create the first of those:
|
|
|
2028
2186
|
useNvm: shipRaw.useNvm !== false,
|
|
2029
2187
|
nvmSh: shipRaw.nvmSh,
|
|
2030
2188
|
stateDir,
|
|
2031
|
-
logFile: expand(shipRaw.logFile ??
|
|
2189
|
+
logFile: expand(shipRaw.logFile ?? join6(tmpdir(), "crew.log"), base),
|
|
2032
2190
|
userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
|
|
2033
2191
|
maxConcurrentAgents,
|
|
2034
2192
|
streamRetentionDays,
|
|
@@ -2248,7 +2406,7 @@ function repoIdForName(r, name) {
|
|
|
2248
2406
|
}
|
|
2249
2407
|
function resolveApiKey(r) {
|
|
2250
2408
|
if (r.apiKey) return r.apiKey;
|
|
2251
|
-
if (r.apiKeyFile && r.apiKeyVar &&
|
|
2409
|
+
if (r.apiKeyFile && r.apiKeyVar && existsSync5(r.apiKeyFile)) {
|
|
2252
2410
|
for (const line of readFileSync5(r.apiKeyFile, "utf8").split("\n")) {
|
|
2253
2411
|
const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
|
|
2254
2412
|
if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
@@ -2312,7 +2470,7 @@ function configuredMembers(rt) {
|
|
|
2312
2470
|
return out;
|
|
2313
2471
|
}
|
|
2314
2472
|
function defaultRepoDir(basePath, workspace, repoName) {
|
|
2315
|
-
return
|
|
2473
|
+
return join6(basePath, workspace, repoName);
|
|
2316
2474
|
}
|
|
2317
2475
|
function reposOf(r) {
|
|
2318
2476
|
const named = Object.entries(r.repos);
|
|
@@ -2336,7 +2494,7 @@ function passengerRepoTargets(r) {
|
|
|
2336
2494
|
return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
|
|
2337
2495
|
}
|
|
2338
2496
|
function passengerCheckoutDir(basePath, workspace, repoName) {
|
|
2339
|
-
return
|
|
2497
|
+
return join6(basePath, workspace, ".passenger-checkouts", repoName);
|
|
2340
2498
|
}
|
|
2341
2499
|
function passengerCheckoutTargets(r) {
|
|
2342
2500
|
if (!r.hostPassengers) return [];
|
|
@@ -2468,8 +2626,8 @@ function rosterMarkdown(roster, meId) {
|
|
|
2468
2626
|
}
|
|
2469
2627
|
|
|
2470
2628
|
// src/state.ts
|
|
2471
|
-
import { readFileSync as readFileSync6, writeFileSync as
|
|
2472
|
-
import { join as
|
|
2629
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync as rmSync5, statSync as statSync2 } from "fs";
|
|
2630
|
+
import { join as join7 } from "path";
|
|
2473
2631
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
2474
2632
|
var State = class _State {
|
|
2475
2633
|
dir;
|
|
@@ -2478,7 +2636,7 @@ var State = class _State {
|
|
|
2478
2636
|
mkdirSync3(dir, { recursive: true });
|
|
2479
2637
|
}
|
|
2480
2638
|
path(...parts) {
|
|
2481
|
-
return
|
|
2639
|
+
return join7(this.dir, ...parts);
|
|
2482
2640
|
}
|
|
2483
2641
|
/**
|
|
2484
2642
|
* The timestamp every "new since last poll" check is measured against.
|
|
@@ -2496,7 +2654,7 @@ var State = class _State {
|
|
|
2496
2654
|
}
|
|
2497
2655
|
}
|
|
2498
2656
|
setWatermark(at) {
|
|
2499
|
-
|
|
2657
|
+
writeFileSync4(this.path(".poll-watermark"), `${at}
|
|
2500
2658
|
`);
|
|
2501
2659
|
}
|
|
2502
2660
|
/**
|
|
@@ -2514,19 +2672,19 @@ var State = class _State {
|
|
|
2514
2672
|
}
|
|
2515
2673
|
/** Whole-crew pause, and the per-role sentinels one level down. */
|
|
2516
2674
|
isPaused() {
|
|
2517
|
-
return
|
|
2675
|
+
return existsSync6(this.path(".crew-paused"));
|
|
2518
2676
|
}
|
|
2519
2677
|
isRolePaused(role) {
|
|
2520
|
-
return
|
|
2678
|
+
return existsSync6(this.path(`.role-paused-${role}`));
|
|
2521
2679
|
}
|
|
2522
2680
|
pausedRoles(roles) {
|
|
2523
2681
|
return new Set(roles.filter((r) => this.isRolePaused(r)));
|
|
2524
2682
|
}
|
|
2525
2683
|
pause(role) {
|
|
2526
|
-
|
|
2684
|
+
writeFileSync4(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
|
|
2527
2685
|
}
|
|
2528
2686
|
resume(role) {
|
|
2529
|
-
|
|
2687
|
+
rmSync5(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
|
|
2530
2688
|
}
|
|
2531
2689
|
/**
|
|
2532
2690
|
* The release phase (merge, deploy, release) has its own sentinel, one
|
|
@@ -2534,11 +2692,11 @@ var State = class _State {
|
|
|
2534
2692
|
* while agents keep working.
|
|
2535
2693
|
*/
|
|
2536
2694
|
isReleasePaused() {
|
|
2537
|
-
return
|
|
2695
|
+
return existsSync6(this.path(".release-paused"));
|
|
2538
2696
|
}
|
|
2539
2697
|
setReleasePaused(paused) {
|
|
2540
|
-
if (paused)
|
|
2541
|
-
else
|
|
2698
|
+
if (paused) writeFileSync4(this.path(".release-paused"), "");
|
|
2699
|
+
else rmSync5(this.path(".release-paused"), { force: true });
|
|
2542
2700
|
}
|
|
2543
2701
|
/**
|
|
2544
2702
|
* Why a release cycle must not run right now, or null when it may. Every
|
|
@@ -2585,13 +2743,13 @@ var State = class _State {
|
|
|
2585
2743
|
}
|
|
2586
2744
|
} catch {
|
|
2587
2745
|
}
|
|
2588
|
-
|
|
2746
|
+
writeFileSync4(file, `${process.pid}
|
|
2589
2747
|
`);
|
|
2590
2748
|
return {
|
|
2591
2749
|
ok: true,
|
|
2592
2750
|
release: () => {
|
|
2593
2751
|
try {
|
|
2594
|
-
|
|
2752
|
+
rmSync5(file, { force: true });
|
|
2595
2753
|
} catch {
|
|
2596
2754
|
}
|
|
2597
2755
|
}
|
|
@@ -2678,12 +2836,12 @@ var State = class _State {
|
|
|
2678
2836
|
blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
|
|
2679
2837
|
noteBlocked: () => {
|
|
2680
2838
|
const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
|
|
2681
|
-
|
|
2839
|
+
writeFileSync4(this.path(blocked), `${n}
|
|
2682
2840
|
`);
|
|
2683
2841
|
return n;
|
|
2684
2842
|
},
|
|
2685
2843
|
clearBlocked: () => {
|
|
2686
|
-
|
|
2844
|
+
rmSync5(this.path(blocked), { force: true });
|
|
2687
2845
|
},
|
|
2688
2846
|
/**
|
|
2689
2847
|
* The commit a deploy last failed on.
|
|
@@ -2695,11 +2853,11 @@ var State = class _State {
|
|
|
2695
2853
|
*/
|
|
2696
2854
|
deployFailedSha: () => read(failed),
|
|
2697
2855
|
noteDeployFailed: (sha) => {
|
|
2698
|
-
|
|
2856
|
+
writeFileSync4(this.path(failed), `${sha}
|
|
2699
2857
|
`);
|
|
2700
2858
|
},
|
|
2701
2859
|
clearDeployFailed: () => {
|
|
2702
|
-
|
|
2860
|
+
rmSync5(this.path(failed), { force: true });
|
|
2703
2861
|
},
|
|
2704
2862
|
/**
|
|
2705
2863
|
* Consecutive test-gate failures on the same head (CREW-1368). A failure
|
|
@@ -2709,12 +2867,12 @@ var State = class _State {
|
|
|
2709
2867
|
noteTestGateFailed: (sha) => {
|
|
2710
2868
|
const [prev, n] = (read(gateFailed) ?? "").split(" ");
|
|
2711
2869
|
const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
|
|
2712
|
-
|
|
2870
|
+
writeFileSync4(this.path(gateFailed), `${sha} ${next}
|
|
2713
2871
|
`);
|
|
2714
2872
|
return next;
|
|
2715
2873
|
},
|
|
2716
2874
|
clearTestGateFailed: () => {
|
|
2717
|
-
|
|
2875
|
+
rmSync5(this.path(gateFailed), { force: true });
|
|
2718
2876
|
},
|
|
2719
2877
|
/** Whether the board was already told about this head's red gate. */
|
|
2720
2878
|
heldReported: () => {
|
|
@@ -2725,15 +2883,15 @@ var State = class _State {
|
|
|
2725
2883
|
}
|
|
2726
2884
|
},
|
|
2727
2885
|
noteHeldReported: (signature) => {
|
|
2728
|
-
|
|
2886
|
+
writeFileSync4(this.path(heldReported), `${signature}
|
|
2729
2887
|
`);
|
|
2730
2888
|
},
|
|
2731
2889
|
clearHeldReported: () => {
|
|
2732
|
-
|
|
2890
|
+
rmSync5(this.path(heldReported), { force: true });
|
|
2733
2891
|
},
|
|
2734
2892
|
testGateReported: (sha) => read(gateReported) === sha,
|
|
2735
2893
|
noteTestGateReported: (sha) => {
|
|
2736
|
-
|
|
2894
|
+
writeFileSync4(this.path(gateReported), `${sha}
|
|
2737
2895
|
`);
|
|
2738
2896
|
}
|
|
2739
2897
|
};
|
|
@@ -2762,7 +2920,7 @@ var State = class _State {
|
|
|
2762
2920
|
}
|
|
2763
2921
|
};
|
|
2764
2922
|
const clear2 = () => {
|
|
2765
|
-
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile])
|
|
2923
|
+
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync5(this.path(f), { force: true });
|
|
2766
2924
|
};
|
|
2767
2925
|
return {
|
|
2768
2926
|
/** Consecutive cycles this route had actionable work and did not win. */
|
|
@@ -2789,18 +2947,18 @@ var State = class _State {
|
|
|
2789
2947
|
clear2();
|
|
2790
2948
|
return;
|
|
2791
2949
|
}
|
|
2792
|
-
|
|
2950
|
+
writeFileSync4(this.path(streakFile), `${this.readInt(streakFile) + 1}
|
|
2793
2951
|
`);
|
|
2794
2952
|
const prior = read(ticketFile);
|
|
2795
2953
|
if (topTicket && topTicket === prior) {
|
|
2796
|
-
|
|
2954
|
+
writeFileSync4(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
|
|
2797
2955
|
`);
|
|
2798
2956
|
} else if (topTicket) {
|
|
2799
|
-
|
|
2957
|
+
writeFileSync4(this.path(ticketFile), `${topTicket}
|
|
2800
2958
|
`);
|
|
2801
|
-
|
|
2959
|
+
writeFileSync4(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2802
2960
|
`);
|
|
2803
|
-
|
|
2961
|
+
writeFileSync4(this.path(ticketStreakFile), "1\n");
|
|
2804
2962
|
}
|
|
2805
2963
|
}
|
|
2806
2964
|
};
|
|
@@ -2828,7 +2986,7 @@ var State = class _State {
|
|
|
2828
2986
|
}
|
|
2829
2987
|
},
|
|
2830
2988
|
persist: (next) => {
|
|
2831
|
-
|
|
2989
|
+
writeFileSync4(this.path(file), JSON.stringify(next));
|
|
2832
2990
|
}
|
|
2833
2991
|
};
|
|
2834
2992
|
}
|
|
@@ -2849,7 +3007,7 @@ var State = class _State {
|
|
|
2849
3007
|
}
|
|
2850
3008
|
},
|
|
2851
3009
|
persist: (next) => {
|
|
2852
|
-
|
|
3010
|
+
writeFileSync4(this.path(file), JSON.stringify(next));
|
|
2853
3011
|
}
|
|
2854
3012
|
};
|
|
2855
3013
|
}
|
|
@@ -2862,7 +3020,7 @@ var State = class _State {
|
|
|
2862
3020
|
const file = ".release-last-run";
|
|
2863
3021
|
return {
|
|
2864
3022
|
touch: () => {
|
|
2865
|
-
|
|
3023
|
+
writeFileSync4(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
|
|
2866
3024
|
},
|
|
2867
3025
|
at: () => {
|
|
2868
3026
|
try {
|
|
@@ -3179,10 +3337,10 @@ function describeRelease(o, route) {
|
|
|
3179
3337
|
}
|
|
3180
3338
|
|
|
3181
3339
|
// src/install.ts
|
|
3182
|
-
import { dirname as dirname4, join as
|
|
3183
|
-
import { homedir as
|
|
3184
|
-
import { accessSync, constants, existsSync as
|
|
3185
|
-
import { spawnSync as
|
|
3340
|
+
import { dirname as dirname4, join as join8, extname } from "path";
|
|
3341
|
+
import { homedir as homedir5 } from "os";
|
|
3342
|
+
import { accessSync, constants, existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync5, unlinkSync, realpathSync } from "fs";
|
|
3343
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
3186
3344
|
import { createHash } from "crypto";
|
|
3187
3345
|
|
|
3188
3346
|
// src/runtime-info.ts
|
|
@@ -3193,7 +3351,7 @@ function isCompiledBinary(moduleUrl) {
|
|
|
3193
3351
|
// src/install.ts
|
|
3194
3352
|
function crewInvocation(crewHome) {
|
|
3195
3353
|
if (isCompiledBinary(import.meta.url)) return [process.execPath];
|
|
3196
|
-
return [process.execPath,
|
|
3354
|
+
return [process.execPath, join8(crewHome, "bin", "crew")];
|
|
3197
3355
|
}
|
|
3198
3356
|
var INTERVAL_SECONDS = 120;
|
|
3199
3357
|
function labelFor(crewHome, job) {
|
|
@@ -3231,7 +3389,7 @@ function pathFor(ship) {
|
|
|
3231
3389
|
return parts.join(":");
|
|
3232
3390
|
}
|
|
3233
3391
|
function findOnPath(bin, searchPath) {
|
|
3234
|
-
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) =>
|
|
3392
|
+
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join8(dir, bin));
|
|
3235
3393
|
for (const candidate of candidates) {
|
|
3236
3394
|
try {
|
|
3237
3395
|
accessSync(candidate, constants.X_OK);
|
|
@@ -3241,7 +3399,7 @@ function findOnPath(bin, searchPath) {
|
|
|
3241
3399
|
}
|
|
3242
3400
|
return void 0;
|
|
3243
3401
|
}
|
|
3244
|
-
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin",
|
|
3402
|
+
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join8(homedir5(), ".docker", "bin")].join(":");
|
|
3245
3403
|
function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
3246
3404
|
const schedulerPath = pathFor(ship);
|
|
3247
3405
|
if (findOnPath("docker", schedulerPath)) return void 0;
|
|
@@ -3253,7 +3411,7 @@ function xmlEscape(s) {
|
|
|
3253
3411
|
}
|
|
3254
3412
|
function planLaunchd(ship, crewHome, job) {
|
|
3255
3413
|
const label = labelFor(crewHome, job);
|
|
3256
|
-
const unitPath =
|
|
3414
|
+
const unitPath = join8(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3257
3415
|
const crewLog = ship.logFile;
|
|
3258
3416
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
3259
3417
|
const persistent = job === "run";
|
|
@@ -3305,8 +3463,8 @@ ${scheduleKeys}
|
|
|
3305
3463
|
}
|
|
3306
3464
|
function planSystemd(ship, crewHome, job) {
|
|
3307
3465
|
const label = labelFor(crewHome, job);
|
|
3308
|
-
const unitDir =
|
|
3309
|
-
const servicePath =
|
|
3466
|
+
const unitDir = join8(homedir5(), ".config", "systemd", "user");
|
|
3467
|
+
const servicePath = join8(unitDir, `${label}.service`);
|
|
3310
3468
|
const crewLog = ship.logFile;
|
|
3311
3469
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
3312
3470
|
const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
|
|
@@ -3343,7 +3501,7 @@ WantedBy=default.target
|
|
|
3343
3501
|
schedulerLog
|
|
3344
3502
|
};
|
|
3345
3503
|
}
|
|
3346
|
-
const timerPath =
|
|
3504
|
+
const timerPath = join8(unitDir, `${label}.timer`);
|
|
3347
3505
|
const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
|
|
3348
3506
|
[Unit]
|
|
3349
3507
|
Description=${description}
|
|
@@ -3410,7 +3568,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3410
3568
|
function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
3411
3569
|
const label = labelFor(crewHome, job);
|
|
3412
3570
|
if (hostShip === "macos") {
|
|
3413
|
-
const unitPath =
|
|
3571
|
+
const unitPath = join8(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3414
3572
|
return {
|
|
3415
3573
|
mechanism: "launchd",
|
|
3416
3574
|
unitPaths: [unitPath],
|
|
@@ -3420,18 +3578,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3420
3578
|
};
|
|
3421
3579
|
}
|
|
3422
3580
|
if (hostShip === "linux" && hasSystemd) {
|
|
3423
|
-
const unitDir =
|
|
3581
|
+
const unitDir = join8(homedir5(), ".config", "systemd", "user");
|
|
3424
3582
|
if (job === "run") {
|
|
3425
3583
|
return {
|
|
3426
3584
|
mechanism: "systemd",
|
|
3427
|
-
unitPaths: [
|
|
3585
|
+
unitPaths: [join8(unitDir, `${label}.service`)],
|
|
3428
3586
|
crontabLine: null,
|
|
3429
3587
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
|
|
3430
3588
|
};
|
|
3431
3589
|
}
|
|
3432
3590
|
return {
|
|
3433
3591
|
mechanism: "systemd",
|
|
3434
|
-
unitPaths: [
|
|
3592
|
+
unitPaths: [join8(unitDir, `${label}.service`), join8(unitDir, `${label}.timer`)],
|
|
3435
3593
|
crontabLine: null,
|
|
3436
3594
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
|
|
3437
3595
|
};
|
|
@@ -3449,7 +3607,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3449
3607
|
function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
3450
3608
|
const label = labelFor(crewHome, "run");
|
|
3451
3609
|
if (hostShip === "macos") {
|
|
3452
|
-
const unitPath =
|
|
3610
|
+
const unitPath = join8(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3453
3611
|
return {
|
|
3454
3612
|
mechanism: "launchd",
|
|
3455
3613
|
startCommand: ["launchctl", "load", unitPath],
|
|
@@ -3473,7 +3631,7 @@ function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
|
3473
3631
|
}
|
|
3474
3632
|
function runAll(commands, cwd, log) {
|
|
3475
3633
|
for (const [bin, ...args] of commands) {
|
|
3476
|
-
const res =
|
|
3634
|
+
const res = spawnSync4(bin, args, { cwd, encoding: "utf8" });
|
|
3477
3635
|
if (res.error || res.status !== 0) {
|
|
3478
3636
|
log.warn(`${bin} ${args.join(" ")} exited ${res.status ?? "error"}`, {
|
|
3479
3637
|
output: `${res.stdout ?? ""}${res.stderr ?? ""}${res.error ? String(res.error) : ""}`
|
|
@@ -3490,7 +3648,7 @@ async function applyInstall(plan, crewHome, dryRun, log) {
|
|
|
3490
3648
|
continue;
|
|
3491
3649
|
}
|
|
3492
3650
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
3493
|
-
|
|
3651
|
+
writeFileSync5(path, plan.unitContent[path]);
|
|
3494
3652
|
log.emit(`wrote ${path}`);
|
|
3495
3653
|
}
|
|
3496
3654
|
if (plan.crontabLine) {
|
|
@@ -3538,13 +3696,13 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
3538
3696
|
if (dryRun) {
|
|
3539
3697
|
log.emit(`would run: launchctl remove ${plan.removeLabel}`);
|
|
3540
3698
|
} else {
|
|
3541
|
-
if (
|
|
3699
|
+
if (spawnSync4("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
|
|
3542
3700
|
log.emit(`removed loaded job ${plan.removeLabel}`);
|
|
3543
3701
|
}
|
|
3544
3702
|
}
|
|
3545
3703
|
}
|
|
3546
3704
|
for (const path of plan.unitPaths) {
|
|
3547
|
-
if (!
|
|
3705
|
+
if (!existsSync7(path)) continue;
|
|
3548
3706
|
if (dryRun) {
|
|
3549
3707
|
log.emit(`would remove ${path}`);
|
|
3550
3708
|
continue;
|
|
@@ -3563,7 +3721,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
3563
3721
|
}
|
|
3564
3722
|
var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
|
|
3565
3723
|
function launchAgentsDir() {
|
|
3566
|
-
return
|
|
3724
|
+
return join8(homedir5(), "Library", "LaunchAgents");
|
|
3567
3725
|
}
|
|
3568
3726
|
function xmlUnescape(s) {
|
|
3569
3727
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
@@ -3575,14 +3733,14 @@ function programFromPlist(text) {
|
|
|
3575
3733
|
return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
|
|
3576
3734
|
}
|
|
3577
3735
|
function loadedCrewLabels() {
|
|
3578
|
-
const list =
|
|
3736
|
+
const list = spawnSync4("launchctl", ["list"], { encoding: "utf8" });
|
|
3579
3737
|
if (list.status !== 0) return [];
|
|
3580
3738
|
return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
|
|
3581
3739
|
}
|
|
3582
3740
|
function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
3583
|
-
const files =
|
|
3741
|
+
const files = existsSync7(dir) ? readdirSync(dir) : [];
|
|
3584
3742
|
const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
|
|
3585
|
-
const plistPath =
|
|
3743
|
+
const plistPath = join8(dir, f);
|
|
3586
3744
|
let program = null;
|
|
3587
3745
|
try {
|
|
3588
3746
|
program = programFromPlist(readFileSync7(plistPath, "utf8"));
|
|
@@ -3592,13 +3750,13 @@ function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
|
3592
3750
|
label: f.slice(0, -".plist".length),
|
|
3593
3751
|
plistPath,
|
|
3594
3752
|
program,
|
|
3595
|
-
programExists: program !== null &&
|
|
3753
|
+
programExists: program !== null && existsSync7(program)
|
|
3596
3754
|
};
|
|
3597
3755
|
});
|
|
3598
3756
|
const seen = new Set(units.map((u) => u.label));
|
|
3599
3757
|
for (const label of [...loaded].sort()) {
|
|
3600
3758
|
if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
|
|
3601
|
-
units.push({ label, plistPath:
|
|
3759
|
+
units.push({ label, plistPath: join8(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
|
|
3602
3760
|
}
|
|
3603
3761
|
return units;
|
|
3604
3762
|
}
|
|
@@ -3611,9 +3769,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
|
|
|
3611
3769
|
log.emit(`would unload and remove ${unit.label}`);
|
|
3612
3770
|
return;
|
|
3613
3771
|
}
|
|
3614
|
-
if (
|
|
3615
|
-
|
|
3616
|
-
if (
|
|
3772
|
+
if (existsSync7(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
|
|
3773
|
+
spawnSync4("launchctl", ["remove", unit.label], { encoding: "utf8" });
|
|
3774
|
+
if (existsSync7(unit.plistPath)) unlinkSync(unit.plistPath);
|
|
3617
3775
|
log.emit(`removed ${unit.label} (${unit.plistPath})`);
|
|
3618
3776
|
}
|
|
3619
3777
|
function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
|
|
@@ -3645,7 +3803,7 @@ function parseLaunchctlList(text) {
|
|
|
3645
3803
|
return jobs;
|
|
3646
3804
|
}
|
|
3647
3805
|
function describeForeignLaunchdUnits(crewHome, dir) {
|
|
3648
|
-
const list =
|
|
3806
|
+
const list = spawnSync4("launchctl", ["list"], { encoding: "utf8" });
|
|
3649
3807
|
const jobs = parseLaunchctlList(list.status === 0 ? list.stdout ?? "" : "");
|
|
3650
3808
|
const foreign = foreignLaunchdUnits(crewHome, dir, jobs.keys());
|
|
3651
3809
|
return foreign.map((u) => {
|
|
@@ -3658,11 +3816,11 @@ function describeForeignLaunchdUnits(crewHome, dir) {
|
|
|
3658
3816
|
});
|
|
3659
3817
|
}
|
|
3660
3818
|
function readCrontab() {
|
|
3661
|
-
const res =
|
|
3819
|
+
const res = spawnSync4("crontab", ["-l"], { encoding: "utf8" });
|
|
3662
3820
|
return res.status === 0 ? res.stdout ?? "" : "";
|
|
3663
3821
|
}
|
|
3664
3822
|
function writeCrontab(text) {
|
|
3665
|
-
|
|
3823
|
+
spawnSync4("crontab", ["-"], { input: text, encoding: "utf8" });
|
|
3666
3824
|
}
|
|
3667
3825
|
function addCrontabLine(marker, line) {
|
|
3668
3826
|
const existing = readCrontab();
|
|
@@ -3686,7 +3844,7 @@ function removeCrontabLine(marker) {
|
|
|
3686
3844
|
}
|
|
3687
3845
|
function detectSystemd() {
|
|
3688
3846
|
if (process.platform !== "linux") return false;
|
|
3689
|
-
const res =
|
|
3847
|
+
const res = spawnSync4("systemctl", ["--user", "status"], { encoding: "utf8" });
|
|
3690
3848
|
return !res.error;
|
|
3691
3849
|
}
|
|
3692
3850
|
|
|
@@ -3696,7 +3854,7 @@ import { statfsSync } from "fs";
|
|
|
3696
3854
|
// src/git.ts
|
|
3697
3855
|
import { execFileSync } from "child_process";
|
|
3698
3856
|
import {
|
|
3699
|
-
existsSync as
|
|
3857
|
+
existsSync as existsSync8,
|
|
3700
3858
|
mkdirSync as mkdirSync5,
|
|
3701
3859
|
openSync as openSync2,
|
|
3702
3860
|
closeSync as closeSync2,
|
|
@@ -4068,7 +4226,7 @@ function cloneUrlFor(remote) {
|
|
|
4068
4226
|
return `git@github.com:${remote}.git`;
|
|
4069
4227
|
}
|
|
4070
4228
|
function ensureRepoCheckout(dir, remote) {
|
|
4071
|
-
if (
|
|
4229
|
+
if (existsSync8(`${dir}/.git`)) return false;
|
|
4072
4230
|
if (!remote) {
|
|
4073
4231
|
throw new GitError(
|
|
4074
4232
|
`${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`
|
|
@@ -5805,8 +5963,8 @@ function materializeBranchForTicket(dir, cfg, t, role) {
|
|
|
5805
5963
|
}
|
|
5806
5964
|
|
|
5807
5965
|
// src/poll.ts
|
|
5808
|
-
import { writeFileSync as
|
|
5809
|
-
import { join as
|
|
5966
|
+
import { writeFileSync as writeFileSync6 } from "fs";
|
|
5967
|
+
import { join as join9 } from "path";
|
|
5810
5968
|
|
|
5811
5969
|
// src/stalled.ts
|
|
5812
5970
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -6078,7 +6236,7 @@ async function decideCycle(o) {
|
|
|
6078
6236
|
}
|
|
6079
6237
|
return decision;
|
|
6080
6238
|
}
|
|
6081
|
-
var digestPath = (stateDir, route, role) =>
|
|
6239
|
+
var digestPath = (stateDir, route, role) => join9(stateDir, `digest-${route}-${role}.md`);
|
|
6082
6240
|
function writeDigest(o, d, role, stateDir) {
|
|
6083
6241
|
const me = o.route.resolved?.seats[role];
|
|
6084
6242
|
if (!me) return false;
|
|
@@ -6140,7 +6298,7 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
6140
6298
|
};
|
|
6141
6299
|
const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
|
|
6142
6300
|
const path = digestPath(stateDir, routeSlug(o.route.route), role);
|
|
6143
|
-
|
|
6301
|
+
writeFileSync6(path, text);
|
|
6144
6302
|
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
|
|
6145
6303
|
data: { role, bytes: Buffer.byteLength(text) }
|
|
6146
6304
|
});
|
|
@@ -6448,9 +6606,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
|
|
|
6448
6606
|
|
|
6449
6607
|
// src/agent.ts
|
|
6450
6608
|
import { spawn as spawn2 } from "child_process";
|
|
6451
|
-
import { readFileSync as readFileSync8, existsSync as
|
|
6609
|
+
import { readFileSync as readFileSync8, existsSync as existsSync9, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, createWriteStream } from "fs";
|
|
6452
6610
|
import { finished } from "stream/promises";
|
|
6453
|
-
import { join as
|
|
6611
|
+
import { join as join10, dirname as dirname6 } from "path";
|
|
6454
6612
|
import { createHash as createHash2 } from "crypto";
|
|
6455
6613
|
|
|
6456
6614
|
// src/environment.ts
|
|
@@ -6848,18 +7006,18 @@ var AgentError = class extends Error {
|
|
|
6848
7006
|
var AgentSpawnError = class extends AgentError {
|
|
6849
7007
|
};
|
|
6850
7008
|
function planAgentRun(o) {
|
|
6851
|
-
const briefPath =
|
|
6852
|
-
if (!
|
|
7009
|
+
const briefPath = join10(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
7010
|
+
if (!existsSync9(briefPath)) {
|
|
6853
7011
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
6854
7012
|
}
|
|
6855
|
-
const commonPath =
|
|
6856
|
-
if (!
|
|
7013
|
+
const commonPath = join10(o.route.promptsDir, "personas", "common.md");
|
|
7014
|
+
if (!existsSync9(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
|
|
6857
7015
|
const now = o.now ?? (() => Date.now());
|
|
6858
7016
|
const routeLabel = routeSlug(o.route.route);
|
|
6859
|
-
const digestPath2 =
|
|
7017
|
+
const digestPath2 = join10(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
6860
7018
|
let digest;
|
|
6861
7019
|
let ageSeconds;
|
|
6862
|
-
if (
|
|
7020
|
+
if (existsSync9(digestPath2)) {
|
|
6863
7021
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
6864
7022
|
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync8(digestPath2, "utf8");
|
|
6865
7023
|
}
|
|
@@ -6875,7 +7033,7 @@ function planAgentRun(o) {
|
|
|
6875
7033
|
digest
|
|
6876
7034
|
});
|
|
6877
7035
|
const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
|
|
6878
|
-
const streamsDir =
|
|
7036
|
+
const streamsDir = join10(o.stateDir, "streams");
|
|
6879
7037
|
const base = `${routeLabel}-${o.role}-${o.cycle}`;
|
|
6880
7038
|
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
6881
7039
|
const { toolchain, model } = seat;
|
|
@@ -6931,8 +7089,8 @@ function planAgentRun(o) {
|
|
|
6931
7089
|
digestAttached: digest !== void 0,
|
|
6932
7090
|
digestAgeSeconds: ageSeconds,
|
|
6933
7091
|
model,
|
|
6934
|
-
streamPath:
|
|
6935
|
-
eventsPath:
|
|
7092
|
+
streamPath: join10(streamsDir, `${base}.jsonl`),
|
|
7093
|
+
eventsPath: join10(streamsDir, `${base}.events.jsonl`),
|
|
6936
7094
|
ticket: o.ticket,
|
|
6937
7095
|
agentLog,
|
|
6938
7096
|
promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
|
|
@@ -7002,8 +7160,8 @@ function spawnAgent(plan, emit) {
|
|
|
7002
7160
|
return;
|
|
7003
7161
|
}
|
|
7004
7162
|
try {
|
|
7005
|
-
const
|
|
7006
|
-
if (
|
|
7163
|
+
const got = adapter.extractResult(parsed);
|
|
7164
|
+
if (got) result = { ...result, ...Object.fromEntries(Object.entries(got).filter(([, v]) => v !== void 0)) };
|
|
7007
7165
|
consume(parsed, mapper.map(parsed));
|
|
7008
7166
|
} catch {
|
|
7009
7167
|
}
|
|
@@ -7079,7 +7237,7 @@ function spawnAgent(plan, emit) {
|
|
|
7079
7237
|
provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER
|
|
7080
7238
|
};
|
|
7081
7239
|
try {
|
|
7082
|
-
|
|
7240
|
+
writeFileSync7(`${plan.streamPath}.meta.json`, JSON.stringify({
|
|
7083
7241
|
route: plan.route,
|
|
7084
7242
|
role: plan.role,
|
|
7085
7243
|
ticket: plan.ticket ?? null,
|
|
@@ -7138,13 +7296,13 @@ function spawnAgent(plan, emit) {
|
|
|
7138
7296
|
}
|
|
7139
7297
|
|
|
7140
7298
|
// src/run-worktree.ts
|
|
7141
|
-
import { existsSync as
|
|
7142
|
-
import { dirname as dirname7, join as
|
|
7299
|
+
import { existsSync as existsSync11 } from "fs";
|
|
7300
|
+
import { dirname as dirname7, join as join12 } from "path";
|
|
7143
7301
|
|
|
7144
7302
|
// src/ports.ts
|
|
7145
7303
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
7146
|
-
import { existsSync as
|
|
7147
|
-
import { join as
|
|
7304
|
+
import { existsSync as existsSync10 } from "fs";
|
|
7305
|
+
import { join as join11 } from "path";
|
|
7148
7306
|
var run = (cmd, args) => {
|
|
7149
7307
|
try {
|
|
7150
7308
|
return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -7189,7 +7347,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
7189
7347
|
}
|
|
7190
7348
|
function worktreeExists(parent, prefix, n) {
|
|
7191
7349
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
7192
|
-
if (
|
|
7350
|
+
if (existsSync10(join11(parent, name, ".git"))) return true;
|
|
7193
7351
|
}
|
|
7194
7352
|
return false;
|
|
7195
7353
|
}
|
|
@@ -7263,7 +7421,7 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
7263
7421
|
const existing = worktreeForNumber(dir, num);
|
|
7264
7422
|
const located = locateBranchForTicket(dir, cfg, t, role);
|
|
7265
7423
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
7266
|
-
if (existing && !existing.prunable &&
|
|
7424
|
+
if (existing && !existing.prunable && existsSync11(existing.path)) {
|
|
7267
7425
|
const branch = existing.branch;
|
|
7268
7426
|
if (status(existing.path).length > 0) return { action: "leave-dirty", path: existing.path, branch };
|
|
7269
7427
|
if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
|
|
@@ -7277,7 +7435,7 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
7277
7435
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
7278
7436
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
7279
7437
|
}
|
|
7280
|
-
const path =
|
|
7438
|
+
const path = join12(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
|
|
7281
7439
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
7282
7440
|
return { action: "cut-remote", path, branch: located.branch };
|
|
7283
7441
|
}
|
|
@@ -7324,7 +7482,7 @@ function worktreeHoldsUnpushed(path, remote) {
|
|
|
7324
7482
|
}
|
|
7325
7483
|
async function finishRunWorktree(dir, cfg, branch) {
|
|
7326
7484
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
7327
|
-
if (!wt || !
|
|
7485
|
+
if (!wt || !existsSync11(wt.path)) return { kind: "none" };
|
|
7328
7486
|
if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
|
|
7329
7487
|
if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
|
|
7330
7488
|
if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
|
|
@@ -7336,7 +7494,7 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
7336
7494
|
if (pids.length) await killGently(pids);
|
|
7337
7495
|
}
|
|
7338
7496
|
gitOk(dir, ["worktree", "remove", wt.path]);
|
|
7339
|
-
return
|
|
7497
|
+
return existsSync11(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
|
|
7340
7498
|
}
|
|
7341
7499
|
|
|
7342
7500
|
// src/handoff-guard.ts
|
|
@@ -7485,13 +7643,13 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
|
|
|
7485
7643
|
}
|
|
7486
7644
|
|
|
7487
7645
|
// src/connect-wizard.ts
|
|
7488
|
-
import { delimiter, dirname as dirname8, join as
|
|
7489
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as
|
|
7646
|
+
import { delimiter, dirname as dirname8, join as join13 } from "path";
|
|
7647
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync12, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
|
|
7490
7648
|
import { stringify } from "yaml";
|
|
7491
7649
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
7492
7650
|
function shouldRunWizard(opts) {
|
|
7493
7651
|
if (!opts.isTTY) return false;
|
|
7494
|
-
const exists = opts.configExists ??
|
|
7652
|
+
const exists = opts.configExists ?? existsSync12;
|
|
7495
7653
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
7496
7654
|
}
|
|
7497
7655
|
function firstRunConfigPath() {
|
|
@@ -7508,7 +7666,7 @@ function defaultIsExecutable(p) {
|
|
|
7508
7666
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
7509
7667
|
for (const dir of pathEnv.split(delimiter)) {
|
|
7510
7668
|
if (!dir) continue;
|
|
7511
|
-
const candidate =
|
|
7669
|
+
const candidate = join13(dir, name);
|
|
7512
7670
|
if (isExecutable(candidate)) return candidate;
|
|
7513
7671
|
}
|
|
7514
7672
|
return void 0;
|
|
@@ -7571,7 +7729,7 @@ ${routeBlock}`;
|
|
|
7571
7729
|
function writeNewConfig(path, text) {
|
|
7572
7730
|
mkdirSync7(dirname8(path), { recursive: true });
|
|
7573
7731
|
try {
|
|
7574
|
-
|
|
7732
|
+
writeFileSync8(path, text, { mode: 384, flag: "wx" });
|
|
7575
7733
|
return true;
|
|
7576
7734
|
} catch (e) {
|
|
7577
7735
|
if (e.code === "EEXIST") return false;
|
|
@@ -7603,8 +7761,8 @@ Wrote ${o.configPath}.
|
|
|
7603
7761
|
}
|
|
7604
7762
|
|
|
7605
7763
|
// src/release-run.ts
|
|
7606
|
-
import { readFileSync as readFileSync9, writeFileSync as
|
|
7607
|
-
import { dirname as dirname9, join as
|
|
7764
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync13, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
|
|
7765
|
+
import { dirname as dirname9, join as join14 } from "path";
|
|
7608
7766
|
import { tmpdir as tmpdir2 } from "os";
|
|
7609
7767
|
|
|
7610
7768
|
// src/release.ts
|
|
@@ -8262,9 +8420,9 @@ async function bump(o, size) {
|
|
|
8262
8420
|
}
|
|
8263
8421
|
return printed;
|
|
8264
8422
|
}
|
|
8265
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
8423
|
+
const files = o.repo.release.versionFiles.map((f) => join14(o.cwd, f));
|
|
8266
8424
|
const first = files[0];
|
|
8267
|
-
if (!
|
|
8425
|
+
if (!existsSync13(first)) {
|
|
8268
8426
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
8269
8427
|
return null;
|
|
8270
8428
|
}
|
|
@@ -8287,21 +8445,21 @@ async function bump(o, size) {
|
|
|
8287
8445
|
o.emit.error(`no version field in ${f}`);
|
|
8288
8446
|
return null;
|
|
8289
8447
|
}
|
|
8290
|
-
|
|
8448
|
+
writeFileSync9(f, out);
|
|
8291
8449
|
}
|
|
8292
8450
|
return version;
|
|
8293
8451
|
}
|
|
8294
8452
|
function writeChangelog(o, version, entries) {
|
|
8295
8453
|
const file = o.repo.release.changelog;
|
|
8296
8454
|
if (!file || entries.length === 0) return;
|
|
8297
|
-
const path =
|
|
8455
|
+
const path = join14(o.cwd, file);
|
|
8298
8456
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
8299
8457
|
if (o.dryRun) {
|
|
8300
8458
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
8301
8459
|
return;
|
|
8302
8460
|
}
|
|
8303
|
-
const existing =
|
|
8304
|
-
|
|
8461
|
+
const existing = existsSync13(path) ? readFileSync9(path, "utf8") : "# Changelog\n";
|
|
8462
|
+
writeFileSync9(path, insertChangelogSection(existing, section));
|
|
8305
8463
|
}
|
|
8306
8464
|
async function confirm(o, expected) {
|
|
8307
8465
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -8382,11 +8540,11 @@ async function confirmLastRelease(o, decision) {
|
|
|
8382
8540
|
function openReleaseCheckout(o) {
|
|
8383
8541
|
const { remote, base } = o.repo.branch;
|
|
8384
8542
|
const branch = `crew/release-${Date.now()}`;
|
|
8385
|
-
const root = mkdtempSync(
|
|
8386
|
-
const dir =
|
|
8543
|
+
const root = mkdtempSync(join14(tmpdir2(), "crew-release-"));
|
|
8544
|
+
const dir = join14(root, "checkout");
|
|
8387
8545
|
const close = () => {
|
|
8388
8546
|
gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
|
|
8389
|
-
|
|
8547
|
+
rmSync6(root, { recursive: true, force: true });
|
|
8390
8548
|
gitOk(o.cwd, ["worktree", "prune"]);
|
|
8391
8549
|
gitOk(o.cwd, ["branch", "-D", branch]);
|
|
8392
8550
|
};
|
|
@@ -8397,10 +8555,10 @@ function openReleaseCheckout(o) {
|
|
|
8397
8555
|
throw e;
|
|
8398
8556
|
}
|
|
8399
8557
|
for (const f of o.repo.worktrees.copy) {
|
|
8400
|
-
const src =
|
|
8401
|
-
if (!
|
|
8402
|
-
mkdirSync8(dirname9(
|
|
8403
|
-
copyFileSync(src,
|
|
8558
|
+
const src = join14(o.cwd, f);
|
|
8559
|
+
if (!existsSync13(src)) continue;
|
|
8560
|
+
mkdirSync8(dirname9(join14(dir, f)), { recursive: true });
|
|
8561
|
+
copyFileSync(src, join14(dir, f));
|
|
8404
8562
|
}
|
|
8405
8563
|
return { dir, branch, close };
|
|
8406
8564
|
}
|
|
@@ -8869,7 +9027,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
8869
9027
|
}
|
|
8870
9028
|
|
|
8871
9029
|
// src/watch.ts
|
|
8872
|
-
import { watch as watchFile, existsSync as
|
|
9030
|
+
import { watch as watchFile, existsSync as existsSync14, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
|
|
8873
9031
|
var E = "\x1B[";
|
|
8874
9032
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
8875
9033
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -8970,7 +9128,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
8970
9128
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
8971
9129
|
}
|
|
8972
9130
|
function readFrom(file, from) {
|
|
8973
|
-
if (!
|
|
9131
|
+
if (!existsSync14(file)) return { events: [], offset: from };
|
|
8974
9132
|
const size = statSync6(file).size;
|
|
8975
9133
|
if (size < from) return readFrom(file, 0);
|
|
8976
9134
|
if (size === from) return { events: [], offset: from };
|
|
@@ -9210,8 +9368,8 @@ function nextRoles(decision, ran) {
|
|
|
9210
9368
|
}
|
|
9211
9369
|
|
|
9212
9370
|
// src/agents.ts
|
|
9213
|
-
import { readFileSync as readFileSync10, existsSync as
|
|
9214
|
-
import { join as
|
|
9371
|
+
import { readFileSync as readFileSync10, existsSync as existsSync15 } from "fs";
|
|
9372
|
+
import { join as join15 } from "path";
|
|
9215
9373
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
|
|
9216
9374
|
var PERSONA_NAME = {
|
|
9217
9375
|
dev: "Developer",
|
|
@@ -9225,11 +9383,11 @@ var CREW_AGENT_COLUMN = "agent_id";
|
|
|
9225
9383
|
var AgentsSyncError = class extends Error {
|
|
9226
9384
|
};
|
|
9227
9385
|
function personaDefaultPrompt(promptsDir, role) {
|
|
9228
|
-
const briefPath =
|
|
9229
|
-
if (!
|
|
9386
|
+
const briefPath = join15(promptsDir, "personas", `lane-${role}.md`);
|
|
9387
|
+
if (!existsSync15(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
9230
9388
|
if (role === "pair") return readFileSync10(briefPath, "utf8");
|
|
9231
|
-
const commonPath =
|
|
9232
|
-
if (!
|
|
9389
|
+
const commonPath = join15(promptsDir, "personas", "common.md");
|
|
9390
|
+
if (!existsSync15(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
|
|
9233
9391
|
return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
|
|
9234
9392
|
}
|
|
9235
9393
|
async function resolveAgentId(route, role, opts) {
|
|
@@ -9907,8 +10065,8 @@ import {
|
|
|
9907
10065
|
} from "@tablation/client";
|
|
9908
10066
|
|
|
9909
10067
|
// src/skills.ts
|
|
9910
|
-
import { readFileSync as readFileSync11, readdirSync as readdirSync2, existsSync as
|
|
9911
|
-
import { join as
|
|
10068
|
+
import { readFileSync as readFileSync11, readdirSync as readdirSync2, existsSync as existsSync16 } from "fs";
|
|
10069
|
+
import { join as join16 } from "path";
|
|
9912
10070
|
import { parse as parseYaml } from "yaml";
|
|
9913
10071
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
9914
10072
|
var SkillSyncError = class extends Error {
|
|
@@ -9927,9 +10085,9 @@ function parseSkillFile(path) {
|
|
|
9927
10085
|
return { name: meta.name, description: meta.description, prompt: body, file: path };
|
|
9928
10086
|
}
|
|
9929
10087
|
function listLocalSkills(promptsDir) {
|
|
9930
|
-
const dir =
|
|
9931
|
-
if (!
|
|
9932
|
-
return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(
|
|
10088
|
+
const dir = join16(promptsDir, "skills");
|
|
10089
|
+
if (!existsSync16(dir)) return [];
|
|
10090
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join16(dir, f)));
|
|
9933
10091
|
}
|
|
9934
10092
|
async function syncSkills(route, opts) {
|
|
9935
10093
|
if (!route.resolved) {
|
|
@@ -10108,7 +10266,7 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
10108
10266
|
}
|
|
10109
10267
|
|
|
10110
10268
|
// src/repos-cmd.ts
|
|
10111
|
-
import { homedir as
|
|
10269
|
+
import { homedir as homedir6 } from "os";
|
|
10112
10270
|
import { basename as basename7, resolve as resolve3 } from "path";
|
|
10113
10271
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
10114
10272
|
var ReposError = class extends Error {
|
|
@@ -10155,7 +10313,7 @@ function parseAddArgs(args, routes, isDir) {
|
|
|
10155
10313
|
return { route, path: "." };
|
|
10156
10314
|
}
|
|
10157
10315
|
function expandDir(p, base) {
|
|
10158
|
-
const home = p === "~" ?
|
|
10316
|
+
const home = p === "~" ? homedir6() : p.startsWith("~/") ? `${homedir6()}/${p.slice(2)}` : p;
|
|
10159
10317
|
return resolve3(base, home);
|
|
10160
10318
|
}
|
|
10161
10319
|
function routeMapOf(doc, route) {
|
|
@@ -10290,7 +10448,7 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
10290
10448
|
}
|
|
10291
10449
|
|
|
10292
10450
|
// src/worktree-sweep.ts
|
|
10293
|
-
import { existsSync as
|
|
10451
|
+
import { existsSync as existsSync17 } from "fs";
|
|
10294
10452
|
import { basename as basename8 } from "path";
|
|
10295
10453
|
function planWorktreeSweep(target, tickets, contract) {
|
|
10296
10454
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
@@ -10318,7 +10476,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
10318
10476
|
let removed = 0;
|
|
10319
10477
|
let keptBranches = 0;
|
|
10320
10478
|
for (const a of actions) {
|
|
10321
|
-
if (!dryRun &&
|
|
10479
|
+
if (!dryRun && existsSync17(a.path)) {
|
|
10322
10480
|
let dirty = false;
|
|
10323
10481
|
try {
|
|
10324
10482
|
dirty = status(a.path).length > 0;
|
|
@@ -10332,7 +10490,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
10332
10490
|
continue;
|
|
10333
10491
|
}
|
|
10334
10492
|
}
|
|
10335
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
10493
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync17(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
10336
10494
|
log.warn(
|
|
10337
10495
|
`not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
|
|
10338
10496
|
{ ticket: a.ticket.issue_id, step: "worktree" }
|
|
@@ -10413,14 +10571,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
10413
10571
|
}
|
|
10414
10572
|
|
|
10415
10573
|
// src/stream-sweep.ts
|
|
10416
|
-
import { existsSync as
|
|
10417
|
-
import { join as
|
|
10574
|
+
import { existsSync as existsSync18, readdirSync as readdirSync3, rmSync as rmSync7, statSync as statSync7 } from "fs";
|
|
10575
|
+
import { join as join17 } from "path";
|
|
10418
10576
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
10419
10577
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
10420
|
-
const dir =
|
|
10421
|
-
if (!
|
|
10578
|
+
const dir = join17(stateDir, "streams");
|
|
10579
|
+
if (!existsSync18(dir)) return [];
|
|
10422
10580
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
10423
|
-
return readdirSync3(dir).map((name) =>
|
|
10581
|
+
return readdirSync3(dir).map((name) => join17(dir, name)).filter((path) => {
|
|
10424
10582
|
try {
|
|
10425
10583
|
return statSync7(path).mtimeMs < cutoff;
|
|
10426
10584
|
} catch {
|
|
@@ -10436,7 +10594,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
10436
10594
|
continue;
|
|
10437
10595
|
}
|
|
10438
10596
|
try {
|
|
10439
|
-
|
|
10597
|
+
rmSync7(path, { force: true });
|
|
10440
10598
|
removed++;
|
|
10441
10599
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
10442
10600
|
} catch (e) {
|
|
@@ -10446,9 +10604,9 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
10446
10604
|
return removed;
|
|
10447
10605
|
}
|
|
10448
10606
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
10449
|
-
if (!
|
|
10607
|
+
if (!existsSync18(stateDir)) return [];
|
|
10450
10608
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
10451
|
-
return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) =>
|
|
10609
|
+
return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join17(stateDir, name)).filter((path) => {
|
|
10452
10610
|
try {
|
|
10453
10611
|
return statSync7(path).mtimeMs < cutoff;
|
|
10454
10612
|
} catch {
|
|
@@ -10458,23 +10616,23 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
10458
10616
|
}
|
|
10459
10617
|
|
|
10460
10618
|
// src/cli.ts
|
|
10461
|
-
import { readFileSync as
|
|
10619
|
+
import { readFileSync as readFileSync16, existsSync as existsSync23, mkdirSync as mkdirSync13, writeFileSync as writeFileSync13, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
|
|
10462
10620
|
import { createInterface } from "readline/promises";
|
|
10463
10621
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
10464
10622
|
|
|
10465
10623
|
// src/ssh-keys.ts
|
|
10466
10624
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
10467
|
-
import { existsSync as
|
|
10468
|
-
import { dirname as dirname11, join as
|
|
10625
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync9, readFileSync as readFileSync12, chmodSync } from "fs";
|
|
10626
|
+
import { dirname as dirname11, join as join18 } from "path";
|
|
10469
10627
|
var SshKeyError = class extends Error {
|
|
10470
10628
|
};
|
|
10471
10629
|
function shipSshKeyPathFor(stateDir) {
|
|
10472
|
-
return
|
|
10630
|
+
return join18(stateDir, "ssh", "id_ed25519");
|
|
10473
10631
|
}
|
|
10474
10632
|
function ensureShipSshKeypair(stateDir) {
|
|
10475
10633
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
10476
10634
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
10477
|
-
if (!
|
|
10635
|
+
if (!existsSync19(privateKeyPath)) {
|
|
10478
10636
|
mkdirSync9(dirname11(privateKeyPath), { recursive: true });
|
|
10479
10637
|
try {
|
|
10480
10638
|
execFileSync3("ssh-keygen", [
|
|
@@ -10495,7 +10653,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
10495
10653
|
}
|
|
10496
10654
|
chmodSync(privateKeyPath, 384);
|
|
10497
10655
|
}
|
|
10498
|
-
if (!
|
|
10656
|
+
if (!existsSync19(publicKeyPath)) {
|
|
10499
10657
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
10500
10658
|
}
|
|
10501
10659
|
const publicKey = readFileSync12(publicKeyPath, "utf8").trim();
|
|
@@ -10712,9 +10870,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
10712
10870
|
|
|
10713
10871
|
// src/tunnel.ts
|
|
10714
10872
|
import { spawn as spawn3 } from "child_process";
|
|
10715
|
-
import { existsSync as
|
|
10873
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3, readdirSync as readdirSync4 } from "fs";
|
|
10716
10874
|
import { randomInt } from "crypto";
|
|
10717
|
-
import { join as
|
|
10875
|
+
import { join as join19 } from "path";
|
|
10718
10876
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
10719
10877
|
var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
10720
10878
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -10781,11 +10939,11 @@ function startTunnel(opts) {
|
|
|
10781
10939
|
};
|
|
10782
10940
|
}
|
|
10783
10941
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
10784
|
-
return
|
|
10942
|
+
return join19(stateDir, "passengers", `${workspaceId}.json`);
|
|
10785
10943
|
}
|
|
10786
10944
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
10787
10945
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
10788
|
-
if (!
|
|
10946
|
+
if (!existsSync20(path)) return void 0;
|
|
10789
10947
|
try {
|
|
10790
10948
|
return JSON.parse(readFileSync13(path, "utf8"));
|
|
10791
10949
|
} catch {
|
|
@@ -10794,8 +10952,8 @@ function readPersistedTunnel(stateDir, workspaceId) {
|
|
|
10794
10952
|
}
|
|
10795
10953
|
function writePersistedTunnel(stateDir, state) {
|
|
10796
10954
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
10797
|
-
mkdirSync10(
|
|
10798
|
-
|
|
10955
|
+
mkdirSync10(join19(stateDir, "passengers"), { recursive: true });
|
|
10956
|
+
writeFileSync10(path, `${JSON.stringify(state, null, 2)}
|
|
10799
10957
|
`);
|
|
10800
10958
|
}
|
|
10801
10959
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -10892,8 +11050,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
10892
11050
|
}
|
|
10893
11051
|
}
|
|
10894
11052
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
10895
|
-
const dir =
|
|
10896
|
-
if (!
|
|
11053
|
+
const dir = join19(stateDir, "passengers");
|
|
11054
|
+
if (!existsSync20(dir)) return [];
|
|
10897
11055
|
return readdirSync4(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
10898
11056
|
}
|
|
10899
11057
|
|
|
@@ -11014,8 +11172,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
11014
11172
|
import { createServer as createHttpServer } from "http";
|
|
11015
11173
|
import { spawn as spawn4 } from "child_process";
|
|
11016
11174
|
import { randomBytes } from "crypto";
|
|
11017
|
-
import { existsSync as
|
|
11018
|
-
import { join as
|
|
11175
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
|
|
11176
|
+
import { join as join20 } from "path";
|
|
11019
11177
|
|
|
11020
11178
|
// src/secret-compare.ts
|
|
11021
11179
|
import { timingSafeEqual } from "crypto";
|
|
@@ -11104,11 +11262,11 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
11104
11262
|
return child;
|
|
11105
11263
|
};
|
|
11106
11264
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
11107
|
-
return
|
|
11265
|
+
return join20(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
11108
11266
|
}
|
|
11109
11267
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
11110
11268
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
11111
|
-
if (!
|
|
11269
|
+
if (!existsSync21(path)) return void 0;
|
|
11112
11270
|
try {
|
|
11113
11271
|
return JSON.parse(readFileSync14(path, "utf8"));
|
|
11114
11272
|
} catch {
|
|
@@ -11117,8 +11275,8 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
|
11117
11275
|
}
|
|
11118
11276
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
11119
11277
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
11120
|
-
mkdirSync11(
|
|
11121
|
-
|
|
11278
|
+
mkdirSync11(join20(stateDir, "passengers"), { recursive: true });
|
|
11279
|
+
writeFileSync11(path, `${JSON.stringify(state, null, 2)}
|
|
11122
11280
|
`);
|
|
11123
11281
|
}
|
|
11124
11282
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -11138,8 +11296,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
11138
11296
|
}
|
|
11139
11297
|
}
|
|
11140
11298
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
11141
|
-
const dir =
|
|
11142
|
-
if (!
|
|
11299
|
+
const dir = join20(stateDir, "passengers");
|
|
11300
|
+
if (!existsSync21(dir)) return [];
|
|
11143
11301
|
const suffix = "-sync.json";
|
|
11144
11302
|
return readdirSync5(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
11145
11303
|
}
|
|
@@ -11196,8 +11354,8 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
11196
11354
|
}
|
|
11197
11355
|
|
|
11198
11356
|
// src/daemon.ts
|
|
11199
|
-
import { readdirSync as readdirSync6, statSync as statSync8 } from "fs";
|
|
11200
|
-
import { join as
|
|
11357
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync15, readdirSync as readdirSync6, rmSync as rmSync8, statSync as statSync8, writeFileSync as writeFileSync12 } from "fs";
|
|
11358
|
+
import { join as join21 } from "path";
|
|
11201
11359
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
11202
11360
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
11203
11361
|
var defaultSleep = (ms2) => new Promise((resolve5) => setTimeout(resolve5, ms2));
|
|
@@ -11236,16 +11394,45 @@ function latestMtime(path) {
|
|
|
11236
11394
|
if (!stat.isDirectory()) return 0;
|
|
11237
11395
|
let latest = stat.mtimeMs;
|
|
11238
11396
|
for (const entry of readdirSync6(path, { withFileTypes: true })) {
|
|
11239
|
-
latest = Math.max(latest, latestMtime(
|
|
11397
|
+
latest = Math.max(latest, latestMtime(join21(path, entry.name)));
|
|
11240
11398
|
}
|
|
11241
11399
|
return latest;
|
|
11242
11400
|
}
|
|
11243
11401
|
function computeTreeSignature(paths) {
|
|
11244
11402
|
return paths.map((p) => String(latestMtime(p))).join(":");
|
|
11245
11403
|
}
|
|
11246
|
-
function makeStaleChecker(paths) {
|
|
11404
|
+
function makeStaleChecker(paths, opts = {}) {
|
|
11247
11405
|
const initial = computeTreeSignature(paths);
|
|
11248
|
-
return () => computeTreeSignature(paths) !== initial;
|
|
11406
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync22(opts.reloadSentinel);
|
|
11407
|
+
}
|
|
11408
|
+
function reloadSentinelFor(stateDir) {
|
|
11409
|
+
return join21(stateDir, "reload-requested");
|
|
11410
|
+
}
|
|
11411
|
+
function daemonStampFor(stateDir) {
|
|
11412
|
+
return join21(stateDir, "daemon.started");
|
|
11413
|
+
}
|
|
11414
|
+
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
11415
|
+
mkdirSync12(stateDir, { recursive: true });
|
|
11416
|
+
rmSync8(reloadSentinelFor(stateDir), { force: true });
|
|
11417
|
+
writeFileSync12(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
11418
|
+
`);
|
|
11419
|
+
}
|
|
11420
|
+
function readDaemonStarted(stateDir) {
|
|
11421
|
+
try {
|
|
11422
|
+
const v = JSON.parse(readFileSync15(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
11423
|
+
return typeof v === "number" ? v : null;
|
|
11424
|
+
} catch {
|
|
11425
|
+
return null;
|
|
11426
|
+
}
|
|
11427
|
+
}
|
|
11428
|
+
function configChangedAfter(files, startedAt) {
|
|
11429
|
+
for (const f of files) {
|
|
11430
|
+
try {
|
|
11431
|
+
if (statSync8(f).mtimeMs > startedAt) return f;
|
|
11432
|
+
} catch {
|
|
11433
|
+
}
|
|
11434
|
+
}
|
|
11435
|
+
return null;
|
|
11249
11436
|
}
|
|
11250
11437
|
var SPAWN_PARK_AFTER = 3;
|
|
11251
11438
|
var SpawnBreaker = class {
|
|
@@ -11646,6 +11833,7 @@ function usage() {
|
|
|
11646
11833
|
crew watch [route] live view of what the crew is doing
|
|
11647
11834
|
crew status [route] [--json] paused/running state; --json for a machine reader
|
|
11648
11835
|
crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
|
|
11836
|
+
crew reload make the running daemon re-read crew.yaml (--now: restart it immediately)
|
|
11649
11837
|
crew doctor [route] [--fix] preflight; --fix enables clean routes and offers crew install
|
|
11650
11838
|
crew ports [route] which checkout owns which ports, and what is up
|
|
11651
11839
|
crew reap [route] kill orphaned servers, drop worktrees for closed tickets
|
|
@@ -11700,7 +11888,12 @@ var value = (name) => {
|
|
|
11700
11888
|
var positional = argv.filter((a, i) => !a.startsWith("--") && !argv[i - 1]?.startsWith("--"));
|
|
11701
11889
|
var command = positional[0];
|
|
11702
11890
|
if (!command) usage();
|
|
11703
|
-
|
|
11891
|
+
function daemonConfigPaths(c) {
|
|
11892
|
+
const files = [c.configFile];
|
|
11893
|
+
for (const r of c.routes) if (r.apiKeyFile && !files.includes(r.apiKeyFile)) files.push(r.apiKeyFile);
|
|
11894
|
+
return files;
|
|
11895
|
+
}
|
|
11896
|
+
var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "disconnect", "install", "uninstall", "reload", "release", "merge", "deploy"]);
|
|
11704
11897
|
var STATE_WRITING_SUBCOMMANDS = {
|
|
11705
11898
|
agents: /* @__PURE__ */ new Set(["sync"]),
|
|
11706
11899
|
skills: /* @__PURE__ */ new Set(["sync"])
|
|
@@ -11810,7 +12003,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
11810
12003
|
const releaseFleetWide = ["merge", "deploy", "release"].includes(command) && flag("fleet");
|
|
11811
12004
|
const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
|
|
11812
12005
|
const named = command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
|
|
11813
|
-
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;
|
|
12006
|
+
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1;
|
|
11814
12007
|
let route;
|
|
11815
12008
|
try {
|
|
11816
12009
|
route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
|
|
@@ -12016,14 +12209,14 @@ if (command === "passenger-sync-daemon") {
|
|
|
12016
12209
|
}
|
|
12017
12210
|
if (host === "linux" && hasSystemd) {
|
|
12018
12211
|
const label = labelFor(CREW_HOME, "run");
|
|
12019
|
-
const staleTimer =
|
|
12020
|
-
if (
|
|
12212
|
+
const staleTimer = join22(homedir7(), ".config", "systemd", "user", `${label}.timer`);
|
|
12213
|
+
if (existsSync23(staleTimer)) {
|
|
12021
12214
|
process.stdout.write(
|
|
12022
12215
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
12023
12216
|
`
|
|
12024
12217
|
);
|
|
12025
12218
|
if (!dryRun) {
|
|
12026
|
-
|
|
12219
|
+
spawnSync5("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
|
|
12027
12220
|
try {
|
|
12028
12221
|
unlinkSync5(staleTimer);
|
|
12029
12222
|
} catch {
|
|
@@ -12043,7 +12236,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
12043
12236
|
const tracker = new Tracker(c, cfg.ship);
|
|
12044
12237
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
12045
12238
|
for (const r of await resolvedRepos(c)) {
|
|
12046
|
-
if (!
|
|
12239
|
+
if (!existsSync23(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
12047
12240
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
12048
12241
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
12049
12242
|
}
|
|
@@ -12061,8 +12254,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
12061
12254
|
}
|
|
12062
12255
|
async function doctorFix(o) {
|
|
12063
12256
|
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))));
|
|
12064
|
-
const unitExists = (p) =>
|
|
12065
|
-
const crontab = o.host === "linux" ?
|
|
12257
|
+
const unitExists = (p) => existsSync23(p);
|
|
12258
|
+
const crontab = o.host === "linux" ? spawnSync5("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
12066
12259
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
12067
12260
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
12068
12261
|
for (const n of plan.notes) process.stdout.write(`fix: ${n}
|
|
@@ -12078,7 +12271,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
12078
12271
|
for (const a of plan.actions) {
|
|
12079
12272
|
if (a.kind === "enable") {
|
|
12080
12273
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
12081
|
-
|
|
12274
|
+
writeFileSync13(cfg.configFile, planEnable(readFileSync16(cfg.configFile, "utf8"), a.route));
|
|
12082
12275
|
enabledSound.add(a.route);
|
|
12083
12276
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
12084
12277
|
} else {
|
|
@@ -12941,7 +13134,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12941
13134
|
}
|
|
12942
13135
|
let failed = false;
|
|
12943
13136
|
for (const cmd2 of commands) {
|
|
12944
|
-
const res2 =
|
|
13137
|
+
const res2 = spawnSync5(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
12945
13138
|
if (res2.stdout) process.stdout.write(res2.stdout);
|
|
12946
13139
|
if (res2.stderr) process.stderr.write(res2.stderr);
|
|
12947
13140
|
if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
|
|
@@ -12959,7 +13152,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12959
13152
|
`);
|
|
12960
13153
|
break;
|
|
12961
13154
|
}
|
|
12962
|
-
const res =
|
|
13155
|
+
const res = spawnSync5(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
12963
13156
|
if (res.stdout) process.stdout.write(res.stdout);
|
|
12964
13157
|
if (res.stderr) process.stderr.write(res.stderr);
|
|
12965
13158
|
if (daemonSub !== "status" && (res.error || res.status !== 0)) {
|
|
@@ -12982,7 +13175,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12982
13175
|
process.exit(1);
|
|
12983
13176
|
}
|
|
12984
13177
|
requireArmed("run the daemon loop");
|
|
12985
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
13178
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join22(CREW_HOME, "src"), join22(CREW_HOME, "dist"), join22(CREW_HOME, "bin")];
|
|
13179
|
+
const configPaths = daemonConfigPaths(cfg);
|
|
13180
|
+
watchPaths.push(...configPaths);
|
|
13181
|
+
noteDaemonStarted(cfg.ship.stateDir);
|
|
12986
13182
|
const spawnBreaker = new SpawnBreaker();
|
|
12987
13183
|
const stopReason = await runDaemonLoop({
|
|
12988
13184
|
runPass: () => runOnePass({
|
|
@@ -13000,14 +13196,16 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13000
13196
|
`)
|
|
13001
13197
|
})
|
|
13002
13198
|
}),
|
|
13003
|
-
checkStale: makeStaleChecker(watchPaths),
|
|
13199
|
+
checkStale: makeStaleChecker(watchPaths, { reloadSentinel: reloadSentinelFor(cfg.ship.stateDir) }),
|
|
13004
13200
|
onPass: (result) => {
|
|
13005
13201
|
if (result.started.length) emit.emit(`daemon pass started: ${result.started.join(", ")}`);
|
|
13006
13202
|
for (const s of result.skipped) emit.emit(`daemon pass skipped ${s.role} \u2014 ${s.reason}`);
|
|
13007
13203
|
}
|
|
13008
13204
|
});
|
|
13009
13205
|
if (stopReason === "stale") {
|
|
13010
|
-
emit.emit(
|
|
13206
|
+
emit.emit(
|
|
13207
|
+
"crew daemon: installed tree, crew.yaml or a reload request changed on disk \u2014 exiting cleanly so the OS-level restart relaunches with the update"
|
|
13208
|
+
);
|
|
13011
13209
|
process.exit(0);
|
|
13012
13210
|
}
|
|
13013
13211
|
break;
|
|
@@ -13131,11 +13329,11 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13131
13329
|
`);
|
|
13132
13330
|
if (!dryRun) {
|
|
13133
13331
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
13134
|
-
const raw =
|
|
13332
|
+
const raw = existsSync23(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
|
|
13135
13333
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
13136
13334
|
raw.agentSkills = skillResult.agentSkills;
|
|
13137
|
-
|
|
13138
|
-
|
|
13335
|
+
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
13336
|
+
writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
13139
13337
|
`);
|
|
13140
13338
|
}
|
|
13141
13339
|
if (personaResult.outcomes.some((o) => o.action === "diverged") || skillResult.outcomes.some((o) => o.action === "diverged")) {
|
|
@@ -13180,10 +13378,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13180
13378
|
`);
|
|
13181
13379
|
if (!dryRun) {
|
|
13182
13380
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
13183
|
-
const raw =
|
|
13381
|
+
const raw = existsSync23(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
|
|
13184
13382
|
raw.agentSkills = result.agentSkills;
|
|
13185
|
-
|
|
13186
|
-
|
|
13383
|
+
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
13384
|
+
writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
13187
13385
|
`);
|
|
13188
13386
|
}
|
|
13189
13387
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -13196,14 +13394,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13196
13394
|
process.exit(2);
|
|
13197
13395
|
};
|
|
13198
13396
|
if (sub !== "add" && sub !== "list") usage2();
|
|
13199
|
-
const configText =
|
|
13397
|
+
const configText = readFileSync16(cfg.configFile, "utf8");
|
|
13200
13398
|
let routeName = positional[2];
|
|
13201
13399
|
let addPath = ".";
|
|
13202
13400
|
let addName;
|
|
13203
13401
|
if (sub === "list" && !routeName) usage2();
|
|
13204
13402
|
if (sub === "add") {
|
|
13205
13403
|
try {
|
|
13206
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
13404
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync23(resolvePath(p.replace(/^~(?=\/|$)/, homedir7()))));
|
|
13207
13405
|
routeName = a.route;
|
|
13208
13406
|
addPath = a.path;
|
|
13209
13407
|
addName = a.name;
|
|
@@ -13218,9 +13416,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13218
13416
|
}
|
|
13219
13417
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
13220
13418
|
let resolvedRemotes;
|
|
13221
|
-
if (
|
|
13419
|
+
if (existsSync23(resolvedFile)) {
|
|
13222
13420
|
try {
|
|
13223
|
-
resolvedRemotes = JSON.parse(
|
|
13421
|
+
resolvedRemotes = JSON.parse(readFileSync16(resolvedFile, "utf8"));
|
|
13224
13422
|
} catch {
|
|
13225
13423
|
}
|
|
13226
13424
|
}
|
|
@@ -13235,14 +13433,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13235
13433
|
for (const r of repos) {
|
|
13236
13434
|
const origin2 = originOf(r.dir);
|
|
13237
13435
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
13238
|
-
const note = !
|
|
13436
|
+
const note = !existsSync23(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
13239
13437
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
13240
13438
|
`);
|
|
13241
13439
|
}
|
|
13242
13440
|
break;
|
|
13243
13441
|
}
|
|
13244
|
-
const target = resolvePath(addPath.replace(/^~(?=\/|$)/,
|
|
13245
|
-
const top =
|
|
13442
|
+
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir7()));
|
|
13443
|
+
const top = existsSync23(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
13246
13444
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
13247
13445
|
const origin = originOf(top);
|
|
13248
13446
|
const plan = planRepoAdd({
|
|
@@ -13291,15 +13489,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13291
13489
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
13292
13490
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
13293
13491
|
`);
|
|
13294
|
-
if (
|
|
13295
|
-
const raw = JSON.parse(
|
|
13492
|
+
if (existsSync23(resolvedFile)) {
|
|
13493
|
+
const raw = JSON.parse(readFileSync16(resolvedFile, "utf8"));
|
|
13296
13494
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
13297
13495
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
13298
|
-
|
|
13496
|
+
writeFileSync13(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
13299
13497
|
`);
|
|
13300
13498
|
}
|
|
13301
13499
|
}
|
|
13302
|
-
|
|
13500
|
+
writeFileSync13(cfg.configFile, plan.text);
|
|
13303
13501
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
13304
13502
|
`);
|
|
13305
13503
|
} catch (e) {
|
|
@@ -13538,10 +13736,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13538
13736
|
operator = await pickOperator(found.holds);
|
|
13539
13737
|
}
|
|
13540
13738
|
let previous;
|
|
13541
|
-
const hadPreviousFile =
|
|
13739
|
+
const hadPreviousFile = existsSync23(resolvedPath);
|
|
13542
13740
|
if (hadPreviousFile) {
|
|
13543
13741
|
try {
|
|
13544
|
-
previous = JSON.parse(
|
|
13742
|
+
previous = JSON.parse(readFileSync16(resolvedPath, "utf8"));
|
|
13545
13743
|
} catch {
|
|
13546
13744
|
}
|
|
13547
13745
|
}
|
|
@@ -13602,10 +13800,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13602
13800
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
13603
13801
|
`);
|
|
13604
13802
|
} else {
|
|
13605
|
-
|
|
13803
|
+
mkdirSync13(dirname12(resolvedPath), { recursive: true });
|
|
13606
13804
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
13607
13805
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
13608
|
-
|
|
13806
|
+
writeFileSync13(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
13609
13807
|
`);
|
|
13610
13808
|
renameSync2(tmpPath, resolvedPath);
|
|
13611
13809
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -13619,8 +13817,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13619
13817
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
13620
13818
|
`);
|
|
13621
13819
|
} else {
|
|
13622
|
-
|
|
13623
|
-
|
|
13820
|
+
mkdirSync13(dirname12(apiKeyFile), { recursive: true });
|
|
13821
|
+
writeFileSync13(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
13624
13822
|
`, { mode: 384 });
|
|
13625
13823
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
13626
13824
|
`);
|
|
@@ -13629,7 +13827,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13629
13827
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
13630
13828
|
const wizard = firstRun;
|
|
13631
13829
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
13632
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
13830
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join22(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
13633
13831
|
const routeBlock = renderConnection(
|
|
13634
13832
|
found,
|
|
13635
13833
|
route2,
|
|
@@ -14116,7 +14314,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
14116
14314
|
}
|
|
14117
14315
|
case "disconnect": {
|
|
14118
14316
|
const url = mcpUrlFor(value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL);
|
|
14119
|
-
|
|
14317
|
+
const disconnectHarnesses = /* @__PURE__ */ new Set([...shipPair(cfg.ship), ...shipToolchains(cfg.ship).map((t) => t.harness).filter(isPairHarness)]);
|
|
14318
|
+
for (const h of disconnectHarnesses) {
|
|
14120
14319
|
process.stderr.write(`${pairInstallerFor(h).removePair({ url, bin: cfg.ship.agent.bin, dryRun })}
|
|
14121
14320
|
`);
|
|
14122
14321
|
}
|
|
@@ -14126,6 +14325,38 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
14126
14325
|
if (!await installScheduler({ replace: flag("replace") })) process.exit(2);
|
|
14127
14326
|
break;
|
|
14128
14327
|
}
|
|
14328
|
+
case "reload": {
|
|
14329
|
+
const sentinel = reloadSentinelFor(cfg.ship.stateDir);
|
|
14330
|
+
if (dryRun) {
|
|
14331
|
+
process.stdout.write(`would write ${sentinel}
|
|
14332
|
+
`);
|
|
14333
|
+
break;
|
|
14334
|
+
}
|
|
14335
|
+
const host = hostPlatform();
|
|
14336
|
+
const control = host === "windows" ? null : planDaemonControl(CREW_HOME, host, detectSystemd());
|
|
14337
|
+
const unitStatus = control?.statusCommand ? spawnSync5(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
|
|
14338
|
+
const unitRunning = unitStatus !== null && !unitStatus.error && unitStatus.status === 0;
|
|
14339
|
+
if (!unitRunning) {
|
|
14340
|
+
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
14341
|
+
break;
|
|
14342
|
+
}
|
|
14343
|
+
mkdirSync13(cfg.ship.stateDir, { recursive: true });
|
|
14344
|
+
writeFileSync13(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
14345
|
+
`);
|
|
14346
|
+
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
14347
|
+
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
14348
|
+
for (const cmd of [control.stopCommand, control.startCommand]) {
|
|
14349
|
+
const res = spawnSync5(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
14350
|
+
if (cmd === control.startCommand && (res.error || res.status !== 0)) {
|
|
14351
|
+
process.stderr.write(`crew reload: ${cmd.join(" ")} exited ${res.status ?? "error"}
|
|
14352
|
+
`);
|
|
14353
|
+
process.exitCode = 1;
|
|
14354
|
+
}
|
|
14355
|
+
}
|
|
14356
|
+
process.stdout.write("crew reload: restarted the daemon unit now\n");
|
|
14357
|
+
}
|
|
14358
|
+
break;
|
|
14359
|
+
}
|
|
14129
14360
|
case "uninstall": {
|
|
14130
14361
|
const host = hostPlatform();
|
|
14131
14362
|
if (host === "windows") {
|
|
@@ -14161,7 +14392,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
14161
14392
|
}
|
|
14162
14393
|
case "log": {
|
|
14163
14394
|
try {
|
|
14164
|
-
const text =
|
|
14395
|
+
const text = readFileSync16(cfg.ship.logFile, "utf8").split("\n");
|
|
14165
14396
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
14166
14397
|
`);
|
|
14167
14398
|
} catch {
|
|
@@ -14371,13 +14602,23 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
14371
14602
|
);
|
|
14372
14603
|
try {
|
|
14373
14604
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
14374
|
-
const evSize =
|
|
14605
|
+
const evSize = existsSync23(evFile) ? statSync9(evFile).size : 0;
|
|
14375
14606
|
process.stdout.write(
|
|
14376
14607
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
14377
14608
|
` : "events log: ok\n"
|
|
14378
14609
|
);
|
|
14379
14610
|
} catch {
|
|
14380
14611
|
}
|
|
14612
|
+
const daemonStartedAt = readDaemonStarted(cfg.ship.stateDir);
|
|
14613
|
+
if (daemonStartedAt !== null) {
|
|
14614
|
+
const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
|
|
14615
|
+
if (changed2) {
|
|
14616
|
+
process.stdout.write(
|
|
14617
|
+
`daemon config: WARN ${changed2} changed at ${new Date(statSync9(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
|
|
14618
|
+
`
|
|
14619
|
+
);
|
|
14620
|
+
}
|
|
14621
|
+
}
|
|
14381
14622
|
const tracker = new Tracker(route, cfg.ship);
|
|
14382
14623
|
const contractProblems = validateContract(tracker.contract);
|
|
14383
14624
|
process.stdout.write(
|