@tablation/crew 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +154 -16
- package/package.json +1 -1
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
|
|
|
@@ -668,10 +669,144 @@ var geminiPair = {
|
|
|
668
669
|
return hadBlock || hadMcp ? `pair(gemini): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what}${kept}` : `pair(gemini): nothing to remove${kept}`;
|
|
669
670
|
}
|
|
670
671
|
};
|
|
672
|
+
var VSCODE_FLAVOURS = ["Code", "Code - Insiders"];
|
|
673
|
+
function vscodeUserDirs(o) {
|
|
674
|
+
const home = o.home ?? homedir2();
|
|
675
|
+
const platform2 = o.platform ?? process.platform;
|
|
676
|
+
const root = platform2 === "darwin" ? join3(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join3(home, "AppData", "Roaming") : join3(home, ".config");
|
|
677
|
+
const all = VSCODE_FLAVOURS.map((f) => join3(root, f, "User"));
|
|
678
|
+
const present = all.filter((d) => existsSync2(d));
|
|
679
|
+
return present.length > 0 ? present : [all[0]];
|
|
680
|
+
}
|
|
681
|
+
var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
|
|
682
|
+
function readVscodeMcp(path) {
|
|
683
|
+
const text = readIfExists(path);
|
|
684
|
+
if (text === void 0 || text.trim() === "") return {};
|
|
685
|
+
try {
|
|
686
|
+
const j = JSON.parse(text);
|
|
687
|
+
return j && typeof j === "object" && !Array.isArray(j) ? j : void 0;
|
|
688
|
+
} catch {
|
|
689
|
+
return void 0;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
var COPILOT_AUTH = `Bearer \${env:${PAIR_KEY_VAR}}`;
|
|
693
|
+
var copilotEntry = (url) => ({ type: "http", url, headers: { Authorization: COPILOT_AUTH } });
|
|
694
|
+
function copilotMcpState(s, url) {
|
|
695
|
+
const e = s.servers?.[SERVER];
|
|
696
|
+
if (!e) return { state: "missing" };
|
|
697
|
+
if (e.url !== url) return { state: "different", detail: `it points at ${e.url ?? "(no url)"}, not ${url}` };
|
|
698
|
+
if (e.headers?.Authorization !== COPILOT_AUTH) return { state: "different", detail: `it does not read its key from \${env:${PAIR_KEY_VAR}}` };
|
|
699
|
+
return { state: "registered" };
|
|
700
|
+
}
|
|
701
|
+
function installCopilotInstructions(path, dryRun) {
|
|
702
|
+
const before = readIfExists(path);
|
|
703
|
+
const base = before ?? COPILOT_FRONTMATTER;
|
|
704
|
+
if (pairBlockIsBroken(base)) return { changed: false, error: `${path} has an unmatched ${PAIR_BLOCK_START}/${PAIR_BLOCK_END} marker \u2014 fix it by hand` };
|
|
705
|
+
const after = upsertPairBlock(base, pairBlock("copilot"));
|
|
706
|
+
if (after === before) return { changed: false };
|
|
707
|
+
if (!dryRun) writeFileMkdir(path, after);
|
|
708
|
+
return { changed: true };
|
|
709
|
+
}
|
|
710
|
+
function removeCopilotInstructions(path, dryRun) {
|
|
711
|
+
const before = readIfExists(path);
|
|
712
|
+
if (before === void 0 || !hasPairBlock(before)) return false;
|
|
713
|
+
if (dryRun) return true;
|
|
714
|
+
const after = removePairBlock(before);
|
|
715
|
+
if (after.trim() === "" || after.trim() === COPILOT_FRONTMATTER.trim()) rmSync2(path);
|
|
716
|
+
else writeFileSync(path, after);
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
var copilotPair = {
|
|
720
|
+
harness: "copilot",
|
|
721
|
+
displayName: "GitHub Copilot (VS Code)",
|
|
722
|
+
installPair(o) {
|
|
723
|
+
const lines = [];
|
|
724
|
+
for (const dir of vscodeUserDirs(o)) {
|
|
725
|
+
const mcpPath = join3(dir, "mcp.json");
|
|
726
|
+
const instrPath = join3(dir, "prompts", "crew-pair.instructions.md");
|
|
727
|
+
const settings = readVscodeMcp(mcpPath);
|
|
728
|
+
if (!settings) {
|
|
729
|
+
lines.push(`pair(copilot): ${mcpPath} is not a plain JSON object (comments are not supported) \u2014 fix it by hand; nothing was written there`);
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
const st = copilotMcpState(settings, o.url);
|
|
733
|
+
if (st.state === "different" && !o.replace) {
|
|
734
|
+
lines.push(`pair(copilot): left ${mcpPath} alone \u2014 a "${SERVER}" server exists and ${st.detail}. Re-run with --mcp replace to overwrite it.`);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
const instr = installCopilotInstructions(instrPath, o.dryRun);
|
|
738
|
+
if (instr.error) {
|
|
739
|
+
lines.push(`pair(copilot): ${instr.error}`);
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const mcpChanged = st.state !== "registered";
|
|
743
|
+
if (mcpChanged && !o.dryRun) {
|
|
744
|
+
const next = { ...settings, servers: { ...settings.servers, [SERVER]: copilotEntry(o.url) } };
|
|
745
|
+
writeFileMkdir(mcpPath, `${JSON.stringify(next, null, 2)}
|
|
746
|
+
`);
|
|
747
|
+
}
|
|
748
|
+
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}})`);
|
|
749
|
+
}
|
|
750
|
+
return lines.join("\n");
|
|
751
|
+
},
|
|
752
|
+
verifyPair(o) {
|
|
753
|
+
const verdicts = vscodeUserDirs(o).map((dir) => {
|
|
754
|
+
const instrPath = join3(dir, "prompts", "crew-pair.instructions.md");
|
|
755
|
+
const mcpPath = join3(dir, "mcp.json");
|
|
756
|
+
const b = blockVerdict(instrPath, "copilot");
|
|
757
|
+
if (!b.ok) return b;
|
|
758
|
+
const settings = readVscodeMcp(mcpPath);
|
|
759
|
+
if (!settings) return { ok: false, detail: `${mcpPath} is not a plain JSON object` };
|
|
760
|
+
const st = copilotMcpState(settings, o.url);
|
|
761
|
+
if (st.state === "missing") return { ok: false, detail: `no ${SERVER} server in ${mcpPath}` };
|
|
762
|
+
if (st.state === "different") return { ok: false, detail: `${SERVER} in ${mcpPath} ${st.detail}` };
|
|
763
|
+
return { ok: true, detail: `${instrPath}, ${mcpPath}` };
|
|
764
|
+
});
|
|
765
|
+
return verdicts.find((v) => !v.ok) ?? { ok: true, detail: verdicts.map((v) => v.detail).join("; ") };
|
|
766
|
+
},
|
|
767
|
+
removePair(o) {
|
|
768
|
+
const lines = [];
|
|
769
|
+
for (const dir of vscodeUserDirs(o)) {
|
|
770
|
+
const mcpPath = join3(dir, "mcp.json");
|
|
771
|
+
const hadBlock = removeCopilotInstructions(join3(dir, "prompts", "crew-pair.instructions.md"), o.dryRun);
|
|
772
|
+
const settings = readVscodeMcp(mcpPath);
|
|
773
|
+
const st = settings ? copilotMcpState(settings, o.url) : { state: "missing" };
|
|
774
|
+
const hadMcp = st.state === "registered";
|
|
775
|
+
const kept = st.state === "different" ? `; left the existing ${SERVER} server alone \u2014 ${st.detail}` : "";
|
|
776
|
+
if (hadMcp && settings && !o.dryRun) {
|
|
777
|
+
const { [SERVER]: _gone, ...rest } = settings.servers ?? {};
|
|
778
|
+
const next = { ...settings };
|
|
779
|
+
if (Object.keys(rest).length > 0) next.servers = rest;
|
|
780
|
+
else delete next.servers;
|
|
781
|
+
if (Object.keys(next).length === 0) rmSync2(mcpPath);
|
|
782
|
+
else writeFileSync(mcpPath, `${JSON.stringify(next, null, 2)}
|
|
783
|
+
`);
|
|
784
|
+
}
|
|
785
|
+
const what = hadBlock && hadMcp ? `the Pair instructions and ${SERVER} server` : hadBlock ? "the Pair instructions" : `the ${SERVER} server`;
|
|
786
|
+
lines.push(hadBlock || hadMcp ? `pair(copilot): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what} (${dir})${kept}` : `pair(copilot): nothing to remove in ${dir}${kept}`);
|
|
787
|
+
}
|
|
788
|
+
return lines.join("\n");
|
|
789
|
+
},
|
|
790
|
+
/**
|
|
791
|
+
* VS Code launched from the Dock or Spotlight does not inherit a shell's
|
|
792
|
+
* exports, so `${env:CREW_API_KEY}` expands to nothing there. macOS only: the
|
|
793
|
+
* login environment is `launchctl`'s, and it is the one place we can ask.
|
|
794
|
+
*/
|
|
795
|
+
envWarning(o) {
|
|
796
|
+
if ((o.platform ?? process.platform) !== "darwin") return void 0;
|
|
797
|
+
const get2 = o.loginEnv ?? ((name) => {
|
|
798
|
+
const r = spawnSync2("launchctl", ["getenv", name], { encoding: "utf8" });
|
|
799
|
+
return r.status === 0 ? r.stdout.trim() : void 0;
|
|
800
|
+
});
|
|
801
|
+
if (get2(PAIR_KEY_VAR)) return void 0;
|
|
802
|
+
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.`;
|
|
803
|
+
}
|
|
804
|
+
};
|
|
671
805
|
var PAIR_INSTALLERS = {
|
|
672
806
|
[claudePair.harness]: claudePair,
|
|
673
807
|
[codexPair.harness]: codexPair,
|
|
674
|
-
[geminiPair.harness]: geminiPair
|
|
808
|
+
[geminiPair.harness]: geminiPair,
|
|
809
|
+
[copilotPair.harness]: copilotPair
|
|
675
810
|
};
|
|
676
811
|
var PAIR_HARNESS_NAMES = Object.keys(PAIR_INSTALLERS);
|
|
677
812
|
var DEFAULT_PAIR = ["claude"];
|
|
@@ -2908,7 +3043,7 @@ function describeRelease(o, route) {
|
|
|
2908
3043
|
import { dirname as dirname4, join as join7, extname } from "path";
|
|
2909
3044
|
import { homedir as homedir4 } from "os";
|
|
2910
3045
|
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
|
|
3046
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2912
3047
|
import { createHash } from "crypto";
|
|
2913
3048
|
|
|
2914
3049
|
// src/runtime-info.ts
|
|
@@ -3199,7 +3334,7 @@ function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
|
3199
3334
|
}
|
|
3200
3335
|
function runAll(commands, cwd, log) {
|
|
3201
3336
|
for (const [bin, ...args] of commands) {
|
|
3202
|
-
const res =
|
|
3337
|
+
const res = spawnSync3(bin, args, { cwd, encoding: "utf8" });
|
|
3203
3338
|
if (res.error || res.status !== 0) {
|
|
3204
3339
|
log.warn(`${bin} ${args.join(" ")} exited ${res.status ?? "error"}`, {
|
|
3205
3340
|
output: `${res.stdout ?? ""}${res.stderr ?? ""}${res.error ? String(res.error) : ""}`
|
|
@@ -3264,7 +3399,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
3264
3399
|
if (dryRun) {
|
|
3265
3400
|
log.emit(`would run: launchctl remove ${plan.removeLabel}`);
|
|
3266
3401
|
} else {
|
|
3267
|
-
if (
|
|
3402
|
+
if (spawnSync3("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
|
|
3268
3403
|
log.emit(`removed loaded job ${plan.removeLabel}`);
|
|
3269
3404
|
}
|
|
3270
3405
|
}
|
|
@@ -3301,7 +3436,7 @@ function programFromPlist(text) {
|
|
|
3301
3436
|
return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
|
|
3302
3437
|
}
|
|
3303
3438
|
function loadedCrewLabels() {
|
|
3304
|
-
const list =
|
|
3439
|
+
const list = spawnSync3("launchctl", ["list"], { encoding: "utf8" });
|
|
3305
3440
|
if (list.status !== 0) return [];
|
|
3306
3441
|
return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
|
|
3307
3442
|
}
|
|
@@ -3337,8 +3472,8 @@ function removeLaunchdUnit(unit, dryRun, log) {
|
|
|
3337
3472
|
log.emit(`would unload and remove ${unit.label}`);
|
|
3338
3473
|
return;
|
|
3339
3474
|
}
|
|
3340
|
-
if (existsSync6(unit.plistPath))
|
|
3341
|
-
|
|
3475
|
+
if (existsSync6(unit.plistPath)) spawnSync3("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
|
|
3476
|
+
spawnSync3("launchctl", ["remove", unit.label], { encoding: "utf8" });
|
|
3342
3477
|
if (existsSync6(unit.plistPath)) unlinkSync(unit.plistPath);
|
|
3343
3478
|
log.emit(`removed ${unit.label} (${unit.plistPath})`);
|
|
3344
3479
|
}
|
|
@@ -3371,7 +3506,7 @@ function parseLaunchctlList(text) {
|
|
|
3371
3506
|
return jobs;
|
|
3372
3507
|
}
|
|
3373
3508
|
function describeForeignLaunchdUnits(crewHome, dir) {
|
|
3374
|
-
const list =
|
|
3509
|
+
const list = spawnSync3("launchctl", ["list"], { encoding: "utf8" });
|
|
3375
3510
|
const jobs = parseLaunchctlList(list.status === 0 ? list.stdout ?? "" : "");
|
|
3376
3511
|
const foreign = foreignLaunchdUnits(crewHome, dir, jobs.keys());
|
|
3377
3512
|
return foreign.map((u) => {
|
|
@@ -3384,11 +3519,11 @@ function describeForeignLaunchdUnits(crewHome, dir) {
|
|
|
3384
3519
|
});
|
|
3385
3520
|
}
|
|
3386
3521
|
function readCrontab() {
|
|
3387
|
-
const res =
|
|
3522
|
+
const res = spawnSync3("crontab", ["-l"], { encoding: "utf8" });
|
|
3388
3523
|
return res.status === 0 ? res.stdout ?? "" : "";
|
|
3389
3524
|
}
|
|
3390
3525
|
function writeCrontab(text) {
|
|
3391
|
-
|
|
3526
|
+
spawnSync3("crontab", ["-"], { input: text, encoding: "utf8" });
|
|
3392
3527
|
}
|
|
3393
3528
|
function addCrontabLine(marker, line) {
|
|
3394
3529
|
const existing = readCrontab();
|
|
@@ -3412,7 +3547,7 @@ function removeCrontabLine(marker) {
|
|
|
3412
3547
|
}
|
|
3413
3548
|
function detectSystemd() {
|
|
3414
3549
|
if (process.platform !== "linux") return false;
|
|
3415
|
-
const res =
|
|
3550
|
+
const res = spawnSync3("systemctl", ["--user", "status"], { encoding: "utf8" });
|
|
3416
3551
|
return !res.error;
|
|
3417
3552
|
}
|
|
3418
3553
|
|
|
@@ -11734,7 +11869,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
11734
11869
|
`
|
|
11735
11870
|
);
|
|
11736
11871
|
if (!dryRun) {
|
|
11737
|
-
|
|
11872
|
+
spawnSync4("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
|
|
11738
11873
|
try {
|
|
11739
11874
|
unlinkSync5(staleTimer);
|
|
11740
11875
|
} catch {
|
|
@@ -11773,7 +11908,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
11773
11908
|
async function doctorFix(o) {
|
|
11774
11909
|
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
11910
|
const unitExists = (p) => existsSync21(p);
|
|
11776
|
-
const crontab = o.host === "linux" ?
|
|
11911
|
+
const crontab = o.host === "linux" ? spawnSync4("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
11777
11912
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
11778
11913
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
11779
11914
|
for (const n of plan.notes) process.stdout.write(`fix: ${n}
|
|
@@ -12645,7 +12780,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12645
12780
|
}
|
|
12646
12781
|
let failed = false;
|
|
12647
12782
|
for (const cmd2 of commands) {
|
|
12648
|
-
const res2 =
|
|
12783
|
+
const res2 = spawnSync4(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
12649
12784
|
if (res2.stdout) process.stdout.write(res2.stdout);
|
|
12650
12785
|
if (res2.stderr) process.stderr.write(res2.stderr);
|
|
12651
12786
|
if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
|
|
@@ -12663,7 +12798,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12663
12798
|
`);
|
|
12664
12799
|
break;
|
|
12665
12800
|
}
|
|
12666
|
-
const res =
|
|
12801
|
+
const res = spawnSync4(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
12667
12802
|
if (res.stdout) process.stdout.write(res.stdout);
|
|
12668
12803
|
if (res.stderr) process.stderr.write(res.stderr);
|
|
12669
12804
|
if (daemonSub !== "status" && (res.error || res.status !== 0)) {
|
|
@@ -14242,6 +14377,9 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
14242
14377
|
process.stdout.write(`pair ${h}:`.padEnd(19) + (v.ok ? ` installed (${v.detail})
|
|
14243
14378
|
` : ` NOT installed \u2014 ${v.detail}; run crew connect
|
|
14244
14379
|
`));
|
|
14380
|
+
const envWarn = inst.envWarning?.({ url: mcpUrlFor(route.baseUrl) });
|
|
14381
|
+
if (envWarn) process.stdout.write(`pair ${h} env:`.padEnd(19) + ` WARN \u2014 ${envWarn}
|
|
14382
|
+
`);
|
|
14245
14383
|
}
|
|
14246
14384
|
const shipVendors = new Set(shipToolchains(cfg.ship).map(toolchainVendor));
|
|
14247
14385
|
if (route.apiKey && route.resolved) {
|
package/package.json
CHANGED