@tablation/crew 0.32.0 → 0.34.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 +2 -2
- package/dist/cli.js +303 -21
- package/package.json +1 -1
package/crew.example.yaml
CHANGED
|
@@ -29,8 +29,7 @@ ship:
|
|
|
29
29
|
|
|
30
30
|
# Optional (CREW-1511): named toolchains — a harness adapter plus the binary
|
|
31
31
|
# and model to run it with. The `agent:` block above keeps working and
|
|
32
|
-
# defines an implicit toolchain named `claude`.
|
|
33
|
-
# exists today. See docs/toolchains.md.
|
|
32
|
+
# defines an implicit toolchain named `claude`. Harnesses: claude, gemini. See docs/toolchains.md.
|
|
34
33
|
# toolchain: claude # the ship's default; a route may override it
|
|
35
34
|
# toolchains:
|
|
36
35
|
# claude-wrapped:
|
|
@@ -42,6 +41,7 @@ ship:
|
|
|
42
41
|
# deep: { model: claude-opus-5-5 }
|
|
43
42
|
# args: [] # appended to the harness argv
|
|
44
43
|
# env: {} # added to the session environment
|
|
44
|
+
# gemini: { harness: gemini, bin: gemini, model: gemini-2.5-pro }
|
|
45
45
|
|
|
46
46
|
# shell: bash # defaults per platform
|
|
47
47
|
extraPath: "/opt/homebrew/bin" # prepended for hooks
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { resolve as resolve4, dirname as dirname12, join as join21, basename as basename9 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { hostname as hostname2, homedir as homedir6 } from "os";
|
|
7
|
-
import { spawnSync as
|
|
7
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
10
10
|
import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
|
|
@@ -159,6 +159,7 @@ var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
|
|
|
159
159
|
|
|
160
160
|
// src/pair-install.ts
|
|
161
161
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync } from "fs";
|
|
162
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
162
163
|
import { homedir as homedir2 } from "os";
|
|
163
164
|
import { dirname as dirname2, join as join3 } from "path";
|
|
164
165
|
|
|
@@ -615,6 +616,28 @@ function geminiMcpState(s, url) {
|
|
|
615
616
|
if (e.headers?.Authorization !== `Bearer $${PAIR_KEY_VAR}`) return { state: "different", detail: `it does not read its key from $${PAIR_KEY_VAR}` };
|
|
616
617
|
return { state: "registered" };
|
|
617
618
|
}
|
|
619
|
+
function inspectGeminiMcp(o) {
|
|
620
|
+
const p = geminiPaths(o.home ?? homedir2());
|
|
621
|
+
const settings = readGeminiSettings(p.config);
|
|
622
|
+
if (!settings) return { state: "different", detail: `${p.config} is not a JSON object` };
|
|
623
|
+
const st = geminiMcpState(settings, o.url);
|
|
624
|
+
return st.state === "registered" ? { state: "registered", url: o.url } : st;
|
|
625
|
+
}
|
|
626
|
+
function ensureGeminiMcp(o) {
|
|
627
|
+
const p = geminiPaths(o.home ?? homedir2());
|
|
628
|
+
const settings = readGeminiSettings(p.config);
|
|
629
|
+
if (!settings) return `gemini: ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
|
|
630
|
+
const st = geminiMcpState(settings, o.url);
|
|
631
|
+
if (st.state === "registered") return `gemini: ${SERVER} MCP server already registered in ${p.config}`;
|
|
632
|
+
if (st.state === "different" && !o.replace) {
|
|
633
|
+
return `gemini: left ${p.config} alone \u2014 a "${SERVER}" server exists and ${st.detail}. Re-run with --mcp replace to overwrite it.`;
|
|
634
|
+
}
|
|
635
|
+
if (!o.dryRun) {
|
|
636
|
+
writeFileMkdir(p.config, `${JSON.stringify({ ...settings, mcpServers: { ...settings.mcpServers, [SERVER]: geminiEntry(o.url) } }, null, 2)}
|
|
637
|
+
`);
|
|
638
|
+
}
|
|
639
|
+
return `gemini: ${o.dryRun ? "(dry run) would register" : "registered"} ${SERVER} in ${p.config} (key read from $${PAIR_KEY_VAR})`;
|
|
640
|
+
}
|
|
618
641
|
var geminiPair = {
|
|
619
642
|
harness: "gemini",
|
|
620
643
|
displayName: "Gemini CLI",
|
|
@@ -668,10 +691,144 @@ var geminiPair = {
|
|
|
668
691
|
return hadBlock || hadMcp ? `pair(gemini): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what}${kept}` : `pair(gemini): nothing to remove${kept}`;
|
|
669
692
|
}
|
|
670
693
|
};
|
|
694
|
+
var VSCODE_FLAVOURS = ["Code", "Code - Insiders"];
|
|
695
|
+
function vscodeUserDirs(o) {
|
|
696
|
+
const home = o.home ?? homedir2();
|
|
697
|
+
const platform2 = o.platform ?? process.platform;
|
|
698
|
+
const root = platform2 === "darwin" ? join3(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join3(home, "AppData", "Roaming") : join3(home, ".config");
|
|
699
|
+
const all = VSCODE_FLAVOURS.map((f) => join3(root, f, "User"));
|
|
700
|
+
const present = all.filter((d) => existsSync2(d));
|
|
701
|
+
return present.length > 0 ? present : [all[0]];
|
|
702
|
+
}
|
|
703
|
+
var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
|
|
704
|
+
function readVscodeMcp(path) {
|
|
705
|
+
const text = readIfExists(path);
|
|
706
|
+
if (text === void 0 || text.trim() === "") return {};
|
|
707
|
+
try {
|
|
708
|
+
const j = JSON.parse(text);
|
|
709
|
+
return j && typeof j === "object" && !Array.isArray(j) ? j : void 0;
|
|
710
|
+
} catch {
|
|
711
|
+
return void 0;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
var COPILOT_AUTH = `Bearer \${env:${PAIR_KEY_VAR}}`;
|
|
715
|
+
var copilotEntry = (url) => ({ type: "http", url, headers: { Authorization: COPILOT_AUTH } });
|
|
716
|
+
function copilotMcpState(s, url) {
|
|
717
|
+
const e = s.servers?.[SERVER];
|
|
718
|
+
if (!e) return { state: "missing" };
|
|
719
|
+
if (e.url !== url) return { state: "different", detail: `it points at ${e.url ?? "(no url)"}, not ${url}` };
|
|
720
|
+
if (e.headers?.Authorization !== COPILOT_AUTH) return { state: "different", detail: `it does not read its key from \${env:${PAIR_KEY_VAR}}` };
|
|
721
|
+
return { state: "registered" };
|
|
722
|
+
}
|
|
723
|
+
function installCopilotInstructions(path, dryRun) {
|
|
724
|
+
const before = readIfExists(path);
|
|
725
|
+
const base = before ?? COPILOT_FRONTMATTER;
|
|
726
|
+
if (pairBlockIsBroken(base)) return { changed: false, error: `${path} has an unmatched ${PAIR_BLOCK_START}/${PAIR_BLOCK_END} marker \u2014 fix it by hand` };
|
|
727
|
+
const after = upsertPairBlock(base, pairBlock("copilot"));
|
|
728
|
+
if (after === before) return { changed: false };
|
|
729
|
+
if (!dryRun) writeFileMkdir(path, after);
|
|
730
|
+
return { changed: true };
|
|
731
|
+
}
|
|
732
|
+
function removeCopilotInstructions(path, dryRun) {
|
|
733
|
+
const before = readIfExists(path);
|
|
734
|
+
if (before === void 0 || !hasPairBlock(before)) return false;
|
|
735
|
+
if (dryRun) return true;
|
|
736
|
+
const after = removePairBlock(before);
|
|
737
|
+
if (after.trim() === "" || after.trim() === COPILOT_FRONTMATTER.trim()) rmSync2(path);
|
|
738
|
+
else writeFileSync(path, after);
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
var copilotPair = {
|
|
742
|
+
harness: "copilot",
|
|
743
|
+
displayName: "GitHub Copilot (VS Code)",
|
|
744
|
+
installPair(o) {
|
|
745
|
+
const lines = [];
|
|
746
|
+
for (const dir of vscodeUserDirs(o)) {
|
|
747
|
+
const mcpPath = join3(dir, "mcp.json");
|
|
748
|
+
const instrPath = join3(dir, "prompts", "crew-pair.instructions.md");
|
|
749
|
+
const settings = readVscodeMcp(mcpPath);
|
|
750
|
+
if (!settings) {
|
|
751
|
+
lines.push(`pair(copilot): ${mcpPath} is not a plain JSON object (comments are not supported) \u2014 fix it by hand; nothing was written there`);
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const st = copilotMcpState(settings, o.url);
|
|
755
|
+
if (st.state === "different" && !o.replace) {
|
|
756
|
+
lines.push(`pair(copilot): left ${mcpPath} alone \u2014 a "${SERVER}" server exists and ${st.detail}. Re-run with --mcp replace to overwrite it.`);
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
const instr = installCopilotInstructions(instrPath, o.dryRun);
|
|
760
|
+
if (instr.error) {
|
|
761
|
+
lines.push(`pair(copilot): ${instr.error}`);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const mcpChanged = st.state !== "registered";
|
|
765
|
+
if (mcpChanged && !o.dryRun) {
|
|
766
|
+
const next = { ...settings, servers: { ...settings.servers, [SERVER]: copilotEntry(o.url) } };
|
|
767
|
+
writeFileMkdir(mcpPath, `${JSON.stringify(next, null, 2)}
|
|
768
|
+
`);
|
|
769
|
+
}
|
|
770
|
+
lines.push(!instr.changed && !mcpChanged ? `pair(copilot): already installed (${instrPath}, ${mcpPath})` : `pair(copilot): ${o.dryRun ? "(dry run) would install" : "installed"} the Pair instructions in ${instrPath} and ${SERVER} in ${mcpPath} (key read from \${env:${PAIR_KEY_VAR}})`);
|
|
771
|
+
}
|
|
772
|
+
return lines.join("\n");
|
|
773
|
+
},
|
|
774
|
+
verifyPair(o) {
|
|
775
|
+
const verdicts = vscodeUserDirs(o).map((dir) => {
|
|
776
|
+
const instrPath = join3(dir, "prompts", "crew-pair.instructions.md");
|
|
777
|
+
const mcpPath = join3(dir, "mcp.json");
|
|
778
|
+
const b = blockVerdict(instrPath, "copilot");
|
|
779
|
+
if (!b.ok) return b;
|
|
780
|
+
const settings = readVscodeMcp(mcpPath);
|
|
781
|
+
if (!settings) return { ok: false, detail: `${mcpPath} is not a plain JSON object` };
|
|
782
|
+
const st = copilotMcpState(settings, o.url);
|
|
783
|
+
if (st.state === "missing") return { ok: false, detail: `no ${SERVER} server in ${mcpPath}` };
|
|
784
|
+
if (st.state === "different") return { ok: false, detail: `${SERVER} in ${mcpPath} ${st.detail}` };
|
|
785
|
+
return { ok: true, detail: `${instrPath}, ${mcpPath}` };
|
|
786
|
+
});
|
|
787
|
+
return verdicts.find((v) => !v.ok) ?? { ok: true, detail: verdicts.map((v) => v.detail).join("; ") };
|
|
788
|
+
},
|
|
789
|
+
removePair(o) {
|
|
790
|
+
const lines = [];
|
|
791
|
+
for (const dir of vscodeUserDirs(o)) {
|
|
792
|
+
const mcpPath = join3(dir, "mcp.json");
|
|
793
|
+
const hadBlock = removeCopilotInstructions(join3(dir, "prompts", "crew-pair.instructions.md"), o.dryRun);
|
|
794
|
+
const settings = readVscodeMcp(mcpPath);
|
|
795
|
+
const st = settings ? copilotMcpState(settings, o.url) : { state: "missing" };
|
|
796
|
+
const hadMcp = st.state === "registered";
|
|
797
|
+
const kept = st.state === "different" ? `; left the existing ${SERVER} server alone \u2014 ${st.detail}` : "";
|
|
798
|
+
if (hadMcp && settings && !o.dryRun) {
|
|
799
|
+
const { [SERVER]: _gone, ...rest } = settings.servers ?? {};
|
|
800
|
+
const next = { ...settings };
|
|
801
|
+
if (Object.keys(rest).length > 0) next.servers = rest;
|
|
802
|
+
else delete next.servers;
|
|
803
|
+
if (Object.keys(next).length === 0) rmSync2(mcpPath);
|
|
804
|
+
else writeFileSync(mcpPath, `${JSON.stringify(next, null, 2)}
|
|
805
|
+
`);
|
|
806
|
+
}
|
|
807
|
+
const what = hadBlock && hadMcp ? `the Pair instructions and ${SERVER} server` : hadBlock ? "the Pair instructions" : `the ${SERVER} server`;
|
|
808
|
+
lines.push(hadBlock || hadMcp ? `pair(copilot): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what} (${dir})${kept}` : `pair(copilot): nothing to remove in ${dir}${kept}`);
|
|
809
|
+
}
|
|
810
|
+
return lines.join("\n");
|
|
811
|
+
},
|
|
812
|
+
/**
|
|
813
|
+
* VS Code launched from the Dock or Spotlight does not inherit a shell's
|
|
814
|
+
* exports, so `${env:CREW_API_KEY}` expands to nothing there. macOS only: the
|
|
815
|
+
* login environment is `launchctl`'s, and it is the one place we can ask.
|
|
816
|
+
*/
|
|
817
|
+
envWarning(o) {
|
|
818
|
+
if ((o.platform ?? process.platform) !== "darwin") return void 0;
|
|
819
|
+
const get2 = o.loginEnv ?? ((name) => {
|
|
820
|
+
const r = spawnSync2("launchctl", ["getenv", name], { encoding: "utf8" });
|
|
821
|
+
return r.status === 0 ? r.stdout.trim() : void 0;
|
|
822
|
+
});
|
|
823
|
+
if (get2(PAIR_KEY_VAR)) return void 0;
|
|
824
|
+
return `$${PAIR_KEY_VAR} is not in the login environment, so VS Code started from the Dock will not send the tracker key. Launch VS Code from a terminal (\`code .\`), or run \`launchctl setenv ${PAIR_KEY_VAR} <key>\` once per login.`;
|
|
825
|
+
}
|
|
826
|
+
};
|
|
671
827
|
var PAIR_INSTALLERS = {
|
|
672
828
|
[claudePair.harness]: claudePair,
|
|
673
829
|
[codexPair.harness]: codexPair,
|
|
674
|
-
[geminiPair.harness]: geminiPair
|
|
830
|
+
[geminiPair.harness]: geminiPair,
|
|
831
|
+
[copilotPair.harness]: copilotPair
|
|
675
832
|
};
|
|
676
833
|
var PAIR_HARNESS_NAMES = Object.keys(PAIR_INSTALLERS);
|
|
677
834
|
var DEFAULT_PAIR = ["claude"];
|
|
@@ -693,9 +850,120 @@ function harnessPromptNote(harness) {
|
|
|
693
850
|
|
|
694
851
|
// src/toolchain.ts
|
|
695
852
|
import { basename } from "path";
|
|
853
|
+
|
|
854
|
+
// src/adapters/gemini.ts
|
|
855
|
+
var BILLING_VARS_TO_UNSET2 = [
|
|
856
|
+
"GEMINI_API_KEY",
|
|
857
|
+
"GOOGLE_API_KEY",
|
|
858
|
+
"GOOGLE_GENAI_USE_VERTEXAI",
|
|
859
|
+
"GOOGLE_GENAI_USE_GCA",
|
|
860
|
+
"GOOGLE_CLOUD_PROJECT",
|
|
861
|
+
"GOOGLE_CLOUD_PROJECT_ID",
|
|
862
|
+
"GOOGLE_CLOUD_LOCATION",
|
|
863
|
+
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
864
|
+
"GOOGLE_GEMINI_BASE_URL",
|
|
865
|
+
"GEMINI_MODEL"
|
|
866
|
+
];
|
|
867
|
+
var READ_TOOLS = ["read_file", "read_many_files", "list_directory", "glob", "search_file_content"];
|
|
868
|
+
function geminiGrantFor(p) {
|
|
869
|
+
if (p.level === "read") return { approvalMode: "default", allowed: [...READ_TOOLS, "run_shell_command"] };
|
|
870
|
+
if (p.level === "write") return { approvalMode: "default", allowed: [...READ_TOOLS, "run_shell_command", "write_file"] };
|
|
871
|
+
return { approvalMode: "yolo", allowed: [] };
|
|
872
|
+
}
|
|
873
|
+
function geminiGrantArgs(role) {
|
|
874
|
+
const g = geminiGrantFor(profileFor(role));
|
|
875
|
+
return ["--approval-mode", g.approvalMode, ...g.allowed.length ? ["--allowed-tools", g.allowed.join(",")] : []];
|
|
876
|
+
}
|
|
877
|
+
function mapGeminiLine(line) {
|
|
878
|
+
const obj = line;
|
|
879
|
+
if (!obj || typeof obj !== "object") return [];
|
|
880
|
+
if (obj.type === "message" && obj.role === "assistant" && obj.content) return [{ kind: "text", text: obj.content }];
|
|
881
|
+
if (obj.type === "tool_use") return [{ kind: "tool", tool: obj.tool_name, target: toolTarget(obj.tool_name === "run_shell_command" ? "Bash" : obj.tool_name, obj.parameters) }];
|
|
882
|
+
return [];
|
|
883
|
+
}
|
|
884
|
+
function createGeminiStreamMapper() {
|
|
885
|
+
let pending = "";
|
|
886
|
+
const flush = () => {
|
|
887
|
+
const text = pending;
|
|
888
|
+
pending = "";
|
|
889
|
+
return text ? [{ kind: "text", text }] : [];
|
|
890
|
+
};
|
|
891
|
+
return {
|
|
892
|
+
flush,
|
|
893
|
+
map(line) {
|
|
894
|
+
const obj = line;
|
|
895
|
+
if (obj && typeof obj === "object" && obj.type === "message" && obj.role === "assistant") {
|
|
896
|
+
if (obj.content) pending += obj.content;
|
|
897
|
+
return [];
|
|
898
|
+
}
|
|
899
|
+
return [...flush(), ...mapGeminiLine(line)];
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
function extractGeminiResult(line) {
|
|
904
|
+
const obj = line;
|
|
905
|
+
if (obj?.type === "init") return obj.session_id ? { sessionId: obj.session_id } : void 0;
|
|
906
|
+
if (!obj || obj.type !== "result") return void 0;
|
|
907
|
+
const s = obj.stats;
|
|
908
|
+
return {
|
|
909
|
+
inputTokens: s?.input_tokens,
|
|
910
|
+
outputTokens: s?.output_tokens,
|
|
911
|
+
cacheReadTokens: s?.cached
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
var geminiAdapter = {
|
|
915
|
+
harness: "gemini",
|
|
916
|
+
displayName: "Gemini CLI",
|
|
917
|
+
vendor: "google",
|
|
918
|
+
ownsModel: (model) => model.startsWith("gemini-"),
|
|
919
|
+
// Gemini CLI has no per-run reasoning knob, so a tier's `reasoning` is ignored.
|
|
920
|
+
defaultBin: "gemini",
|
|
921
|
+
defaultModel: "gemini-2.5-pro",
|
|
922
|
+
defaultTiers: {
|
|
923
|
+
light: { model: "gemini-2.5-flash" },
|
|
924
|
+
standard: { model: "gemini-2.5-pro" },
|
|
925
|
+
deep: { model: "gemini-2.5-pro" }
|
|
926
|
+
},
|
|
927
|
+
limitations: [
|
|
928
|
+
"the tool grant is an allow list of tools that skip confirmation, not a sandbox: run_shell_command is allowed in every profile and sub-commands cannot be restricted",
|
|
929
|
+
"there is no deny list: destructive shell patterns (rm -rf, forced pushes, curl | sh) are not refused at the tool layer, unlike the Claude adapter",
|
|
930
|
+
"the edit profile runs in yolo mode (auto-approves every tool); the write profile (QA) withholds replace, not shell writes",
|
|
931
|
+
"design tools (skills, design canvas) are unavailable on this harness",
|
|
932
|
+
"the tracker MCP server is read from ~/.gemini/settings.json (there is no per-run --mcp-config), and MCP tool calls in the read and write profiles are not on the allow list",
|
|
933
|
+
"the stream carries no thinking blocks and streams assistant text in fragments, so the runner coalesces fragments into one Agent Log cycle per assistant turn; no turn count or cost is reported"
|
|
934
|
+
],
|
|
935
|
+
invocation(spec, o) {
|
|
936
|
+
const args = [
|
|
937
|
+
// The prompt arrives on stdin; `-p` puts the CLI in non-interactive mode.
|
|
938
|
+
"-p",
|
|
939
|
+
"",
|
|
940
|
+
"--model",
|
|
941
|
+
o.model,
|
|
942
|
+
"--output-format",
|
|
943
|
+
"stream-json",
|
|
944
|
+
...geminiGrantArgs(o.role),
|
|
945
|
+
...spec.args
|
|
946
|
+
];
|
|
947
|
+
return spec.launcher ? { bin: spec.launcherBin ?? spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
|
|
948
|
+
},
|
|
949
|
+
runEnv: (spec) => ({ ...spec.env }),
|
|
950
|
+
provider: () => "google",
|
|
951
|
+
unsetEnv: BILLING_VARS_TO_UNSET2,
|
|
952
|
+
mapStreamLine: mapGeminiLine,
|
|
953
|
+
createStreamMapper: createGeminiStreamMapper,
|
|
954
|
+
extractResult: extractGeminiResult,
|
|
955
|
+
extractTurnTokens: () => void 0,
|
|
956
|
+
inspectMcp(o) {
|
|
957
|
+
return inspectGeminiMcp(o);
|
|
958
|
+
},
|
|
959
|
+
ensureMcp: (o) => ensureGeminiMcp(o)
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
// src/toolchain.ts
|
|
696
963
|
var TIERS = ["light", "standard", "deep"];
|
|
697
964
|
var ADAPTERS = {
|
|
698
|
-
[claudeAdapter.harness]: claudeAdapter
|
|
965
|
+
[claudeAdapter.harness]: claudeAdapter,
|
|
966
|
+
[geminiAdapter.harness]: geminiAdapter
|
|
699
967
|
};
|
|
700
968
|
var HARNESS_NAMES = Object.keys(ADAPTERS);
|
|
701
969
|
function isHarness(name) {
|
|
@@ -2908,7 +3176,7 @@ function describeRelease(o, route) {
|
|
|
2908
3176
|
import { dirname as dirname4, join as join7, extname } from "path";
|
|
2909
3177
|
import { homedir as homedir4 } from "os";
|
|
2910
3178
|
import { accessSync, constants, existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync3, unlinkSync, realpathSync } from "fs";
|
|
2911
|
-
import { spawnSync as
|
|
3179
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2912
3180
|
import { createHash } from "crypto";
|
|
2913
3181
|
|
|
2914
3182
|
// src/runtime-info.ts
|
|
@@ -3199,7 +3467,7 @@ function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
|
3199
3467
|
}
|
|
3200
3468
|
function runAll(commands, cwd, log) {
|
|
3201
3469
|
for (const [bin, ...args] of commands) {
|
|
3202
|
-
const res =
|
|
3470
|
+
const res = spawnSync3(bin, args, { cwd, encoding: "utf8" });
|
|
3203
3471
|
if (res.error || res.status !== 0) {
|
|
3204
3472
|
log.warn(`${bin} ${args.join(" ")} exited ${res.status ?? "error"}`, {
|
|
3205
3473
|
output: `${res.stdout ?? ""}${res.stderr ?? ""}${res.error ? String(res.error) : ""}`
|
|
@@ -3264,7 +3532,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
3264
3532
|
if (dryRun) {
|
|
3265
3533
|
log.emit(`would run: launchctl remove ${plan.removeLabel}`);
|
|
3266
3534
|
} else {
|
|
3267
|
-
if (
|
|
3535
|
+
if (spawnSync3("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
|
|
3268
3536
|
log.emit(`removed loaded job ${plan.removeLabel}`);
|
|
3269
3537
|
}
|
|
3270
3538
|
}
|
|
@@ -3301,7 +3569,7 @@ function programFromPlist(text) {
|
|
|
3301
3569
|
return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
|
|
3302
3570
|
}
|
|
3303
3571
|
function loadedCrewLabels() {
|
|
3304
|
-
const list =
|
|
3572
|
+
const list = spawnSync3("launchctl", ["list"], { encoding: "utf8" });
|
|
3305
3573
|
if (list.status !== 0) return [];
|
|
3306
3574
|
return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
|
|
3307
3575
|
}
|
|
@@ -3337,8 +3605,8 @@ function removeLaunchdUnit(unit, dryRun, log) {
|
|
|
3337
3605
|
log.emit(`would unload and remove ${unit.label}`);
|
|
3338
3606
|
return;
|
|
3339
3607
|
}
|
|
3340
|
-
if (existsSync6(unit.plistPath))
|
|
3341
|
-
|
|
3608
|
+
if (existsSync6(unit.plistPath)) spawnSync3("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
|
|
3609
|
+
spawnSync3("launchctl", ["remove", unit.label], { encoding: "utf8" });
|
|
3342
3610
|
if (existsSync6(unit.plistPath)) unlinkSync(unit.plistPath);
|
|
3343
3611
|
log.emit(`removed ${unit.label} (${unit.plistPath})`);
|
|
3344
3612
|
}
|
|
@@ -3371,7 +3639,7 @@ function parseLaunchctlList(text) {
|
|
|
3371
3639
|
return jobs;
|
|
3372
3640
|
}
|
|
3373
3641
|
function describeForeignLaunchdUnits(crewHome, dir) {
|
|
3374
|
-
const list =
|
|
3642
|
+
const list = spawnSync3("launchctl", ["list"], { encoding: "utf8" });
|
|
3375
3643
|
const jobs = parseLaunchctlList(list.status === 0 ? list.stdout ?? "" : "");
|
|
3376
3644
|
const foreign = foreignLaunchdUnits(crewHome, dir, jobs.keys());
|
|
3377
3645
|
return foreign.map((u) => {
|
|
@@ -3384,11 +3652,11 @@ function describeForeignLaunchdUnits(crewHome, dir) {
|
|
|
3384
3652
|
});
|
|
3385
3653
|
}
|
|
3386
3654
|
function readCrontab() {
|
|
3387
|
-
const res =
|
|
3655
|
+
const res = spawnSync3("crontab", ["-l"], { encoding: "utf8" });
|
|
3388
3656
|
return res.status === 0 ? res.stdout ?? "" : "";
|
|
3389
3657
|
}
|
|
3390
3658
|
function writeCrontab(text) {
|
|
3391
|
-
|
|
3659
|
+
spawnSync3("crontab", ["-"], { input: text, encoding: "utf8" });
|
|
3392
3660
|
}
|
|
3393
3661
|
function addCrontabLine(marker, line) {
|
|
3394
3662
|
const existing = readCrontab();
|
|
@@ -3412,7 +3680,7 @@ function removeCrontabLine(marker) {
|
|
|
3412
3680
|
}
|
|
3413
3681
|
function detectSystemd() {
|
|
3414
3682
|
if (process.platform !== "linux") return false;
|
|
3415
|
-
const res =
|
|
3683
|
+
const res = spawnSync3("systemctl", ["--user", "status"], { encoding: "utf8" });
|
|
3416
3684
|
return !res.error;
|
|
3417
3685
|
}
|
|
3418
3686
|
|
|
@@ -6712,6 +6980,7 @@ function spawnAgent(plan, emit) {
|
|
|
6712
6980
|
let result;
|
|
6713
6981
|
const cycles = [];
|
|
6714
6982
|
const attribution = new TicketAttributionTracker();
|
|
6983
|
+
const mapper = adapter.createStreamMapper?.() ?? { map: (p) => adapter.mapStreamLine(p), flush: () => [] };
|
|
6715
6984
|
const handleLine = (line) => {
|
|
6716
6985
|
if (!line) return;
|
|
6717
6986
|
try {
|
|
@@ -6726,9 +6995,16 @@ function spawnAgent(plan, emit) {
|
|
|
6726
6995
|
return;
|
|
6727
6996
|
}
|
|
6728
6997
|
try {
|
|
6729
|
-
|
|
6998
|
+
const extracted = adapter.extractResult(parsed);
|
|
6999
|
+
if (extracted) result = { ...result, ...extracted };
|
|
7000
|
+
consume(parsed, mapper.map(parsed));
|
|
7001
|
+
} catch {
|
|
7002
|
+
}
|
|
7003
|
+
};
|
|
7004
|
+
const consume = (parsed, events) => {
|
|
7005
|
+
{
|
|
6730
7006
|
let turnText = "";
|
|
6731
|
-
for (const ev of
|
|
7007
|
+
for (const ev of events) {
|
|
6732
7008
|
const e = {
|
|
6733
7009
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6734
7010
|
cycle: emit.cycle,
|
|
@@ -6766,9 +7042,8 @@ function spawnAgent(plan, emit) {
|
|
|
6766
7042
|
`);
|
|
6767
7043
|
}
|
|
6768
7044
|
}
|
|
6769
|
-
const turnTokens = adapter.extractTurnTokens(parsed);
|
|
7045
|
+
const turnTokens = parsed === void 0 ? void 0 : adapter.extractTurnTokens(parsed);
|
|
6770
7046
|
if (turnTokens !== void 0) attribution.add(turnText, turnTokens);
|
|
6771
|
-
} catch {
|
|
6772
7047
|
}
|
|
6773
7048
|
};
|
|
6774
7049
|
child.stdout.on("data", (chunk) => {
|
|
@@ -6784,6 +7059,10 @@ function spawnAgent(plan, emit) {
|
|
|
6784
7059
|
});
|
|
6785
7060
|
child.on("close", async (code) => {
|
|
6786
7061
|
if (buf) handleLine(buf);
|
|
7062
|
+
try {
|
|
7063
|
+
consume(void 0, mapper.flush());
|
|
7064
|
+
} catch {
|
|
7065
|
+
}
|
|
6787
7066
|
await Promise.all([endSink(rawSink), endSink(eventsSink)]);
|
|
6788
7067
|
if (spawnFailed) return;
|
|
6789
7068
|
const attributedTicket = attribution.winner(plan.ticket);
|
|
@@ -11734,7 +12013,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
11734
12013
|
`
|
|
11735
12014
|
);
|
|
11736
12015
|
if (!dryRun) {
|
|
11737
|
-
|
|
12016
|
+
spawnSync4("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
|
|
11738
12017
|
try {
|
|
11739
12018
|
unlinkSync5(staleTimer);
|
|
11740
12019
|
} catch {
|
|
@@ -11773,7 +12052,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
11773
12052
|
async function doctorFix(o) {
|
|
11774
12053
|
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))));
|
|
11775
12054
|
const unitExists = (p) => existsSync21(p);
|
|
11776
|
-
const crontab = o.host === "linux" ?
|
|
12055
|
+
const crontab = o.host === "linux" ? spawnSync4("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
11777
12056
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
11778
12057
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
11779
12058
|
for (const n of plan.notes) process.stdout.write(`fix: ${n}
|
|
@@ -12645,7 +12924,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12645
12924
|
}
|
|
12646
12925
|
let failed = false;
|
|
12647
12926
|
for (const cmd2 of commands) {
|
|
12648
|
-
const res2 =
|
|
12927
|
+
const res2 = spawnSync4(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
12649
12928
|
if (res2.stdout) process.stdout.write(res2.stdout);
|
|
12650
12929
|
if (res2.stderr) process.stderr.write(res2.stderr);
|
|
12651
12930
|
if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
|
|
@@ -12663,7 +12942,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12663
12942
|
`);
|
|
12664
12943
|
break;
|
|
12665
12944
|
}
|
|
12666
|
-
const res =
|
|
12945
|
+
const res = spawnSync4(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
12667
12946
|
if (res.stdout) process.stdout.write(res.stdout);
|
|
12668
12947
|
if (res.stderr) process.stderr.write(res.stderr);
|
|
12669
12948
|
if (daemonSub !== "status" && (res.error || res.status !== 0)) {
|
|
@@ -14242,6 +14521,9 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
14242
14521
|
process.stdout.write(`pair ${h}:`.padEnd(19) + (v.ok ? ` installed (${v.detail})
|
|
14243
14522
|
` : ` NOT installed \u2014 ${v.detail}; run crew connect
|
|
14244
14523
|
`));
|
|
14524
|
+
const envWarn = inst.envWarning?.({ url: mcpUrlFor(route.baseUrl) });
|
|
14525
|
+
if (envWarn) process.stdout.write(`pair ${h} env:`.padEnd(19) + ` WARN \u2014 ${envWarn}
|
|
14526
|
+
`);
|
|
14245
14527
|
}
|
|
14246
14528
|
const shipVendors = new Set(shipToolchains(cfg.ship).map(toolchainVendor));
|
|
14247
14529
|
if (route.apiKey && route.resolved) {
|
package/package.json
CHANGED