@bridge_gpt/mcp-server 0.1.14 → 0.1.17

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.
@@ -0,0 +1,346 @@
1
+ /**
2
+ * start-tickets-prereqs — the single, shared source of per-OS prerequisite
3
+ * knowledge for the packaged `start-tickets` automation (BAPI-305).
4
+ *
5
+ * Both consumers read from the SAME descriptor set so they can never drift:
6
+ * - `runPreflight` (in start-tickets.ts) ENFORCES the preflight descriptors
7
+ * fail-fast before a real run (behaviour unchanged from BAPI-302/303).
8
+ * - the read-only `doctor` subcommand RENDERS the doctor descriptors
9
+ * (preflight set + `uv` + the selected agent) without failing fast.
10
+ *
11
+ * Dependency direction: this module imports only TYPES from `start-tickets.ts`
12
+ * (erased at compile time) and the `AgentSpec` type from `agent-registry.ts`, so
13
+ * the runtime graph stays acyclic — `start-tickets.ts` imports values FROM here,
14
+ * never the reverse.
15
+ */
16
+ // ---------------------------------------------------------------------------
17
+ // Constants (moved here from start-tickets.ts so both consumers share them)
18
+ // ---------------------------------------------------------------------------
19
+ /**
20
+ * Environment variable that overrides the Worktrunk binary name for nonstandard
21
+ * installs. Read from the injected `deps.env` (never the global `process.env`)
22
+ * so the override stays fully mockable in unit tests. Its value may be a bare
23
+ * command name or an absolute path. Uses the project-wide `BAPI_` prefix.
24
+ */
25
+ export const WORKTRUNK_BINARY_OVERRIDE_ENV = "BAPI_WORKTRUNK_BIN";
26
+ /** Windows Terminal launcher (distinct from the Worktrunk `git-wt` binary). */
27
+ export const WINDOWS_TERMINAL_COMMAND = "wt.exe";
28
+ /** PowerShell executables tried, in order, for the Windows fallback / launcher. */
29
+ export const WINDOWS_POWERSHELL_CANDIDATES = ["powershell.exe", "powershell"];
30
+ /** Default Worktrunk binary on Windows (winget alias avoids the wt.exe clash). */
31
+ export const DEFAULT_WINDOWS_WORKTRUNK_BINARY = "git-wt";
32
+ /** Default Worktrunk binary on macOS / Linux. */
33
+ export const DEFAULT_POSIX_WORKTRUNK_BINARY = "wt";
34
+ /** tmux binary used for Linux spawning. */
35
+ export const TMUX_COMMAND = "tmux";
36
+ /** Actionable hint emitted when Git Bash is missing on Windows. */
37
+ export const GIT_FOR_WINDOWS_BASH_HINT = "Install Git for Windows / Git Bash — Worktrunk runs its pre-start / post-start hooks via Git Bash.";
38
+ /** The read-only doctor invocation surfaced from preflight failures and elsewhere. */
39
+ export const START_TICKETS_DOCTOR_COMMAND = "npx -y @bridge_gpt/mcp-server doctor";
40
+ /** True only for the three first-class supported platforms. */
41
+ export function isSupportedStartTicketsPlatform(platform) {
42
+ return platform === "darwin" || platform === "win32" || platform === "linux";
43
+ }
44
+ /** Message shown when spawning is attempted on an unsupported platform. */
45
+ export function unsupportedPlatformMessage(platform) {
46
+ return (`start-tickets does not support this platform: '${platform}' is unsupported. ` +
47
+ "Supported platforms are darwin, win32, and linux. " +
48
+ "Use --dry-run to preview the intended commands on any OS.");
49
+ }
50
+ // ---------------------------------------------------------------------------
51
+ // Worktrunk binary resolution + command probing (moved from start-tickets.ts)
52
+ // ---------------------------------------------------------------------------
53
+ /**
54
+ * Resolve the Worktrunk binary name for a platform. A trimmed, non-empty
55
+ * `BAPI_WORKTRUNK_BIN` override wins; otherwise `git-wt` on Windows and `wt`
56
+ * elsewhere (including a non-throwing `wt` fallback for dry-run-only callers on
57
+ * unsupported platforms).
58
+ */
59
+ export function resolveWorktrunkBinary(platform, env) {
60
+ const override = env[WORKTRUNK_BINARY_OVERRIDE_ENV];
61
+ if (override !== undefined) {
62
+ const trimmed = override.trim();
63
+ if (trimmed.length > 0)
64
+ return trimmed;
65
+ }
66
+ if (platform === "win32")
67
+ return DEFAULT_WINDOWS_WORKTRUNK_BINARY;
68
+ return DEFAULT_POSIX_WORKTRUNK_BINARY;
69
+ }
70
+ /** True when a command result has exit code 0. */
71
+ export function commandSucceeded(result) {
72
+ return result.exitCode === 0;
73
+ }
74
+ /** The platform-correct PATH probe: `where` on Windows, `which` elsewhere. */
75
+ export function getCommandProbe(tool, platform) {
76
+ if (platform === "win32")
77
+ return { file: "where", args: [tool] };
78
+ return { file: "which", args: [tool] };
79
+ }
80
+ /** True when `tool` is resolvable on PATH using the platform-correct probe. */
81
+ export async function isCommandOnPath(deps, tool) {
82
+ const probe = getCommandProbe(tool, deps.platform);
83
+ const result = await deps.runCommand(probe.file, probe.args);
84
+ return commandSucceeded(result);
85
+ }
86
+ /** Return the first candidate command found on PATH, or null. Stops on first hit. */
87
+ export async function resolveFirstCommandOnPath(deps, candidates) {
88
+ for (const candidate of candidates) {
89
+ if (await isCommandOnPath(deps, candidate))
90
+ return candidate;
91
+ }
92
+ return null;
93
+ }
94
+ /**
95
+ * Windows-only usability probe for Git Bash. Uses a list-based `bash --version`
96
+ * rather than a bare `where bash` because Git for Windows commonly omits bash.exe
97
+ * from PATH while still shipping a usable bash.
98
+ */
99
+ export async function requireBashUsable(deps) {
100
+ const result = await deps.runCommand("bash", ["--version"]);
101
+ if (commandSucceeded(result))
102
+ return { ok: true };
103
+ return {
104
+ ok: false,
105
+ error: `bash is required on Windows but could not be run. ${GIT_FOR_WINDOWS_BASH_HINT}`,
106
+ };
107
+ }
108
+ /** Append the read-only doctor hint to a preflight failure message. */
109
+ export function appendDoctorHint(error) {
110
+ return `${error} Hint: Run ${START_TICKETS_DOCTOR_COMMAND} for a read-only start-tickets diagnostics report.`;
111
+ }
112
+ function hintForPlatform(hints, platform) {
113
+ if (platform === "win32")
114
+ return hints.win32;
115
+ if (platform === "linux")
116
+ return hints.linux;
117
+ return hints.darwin;
118
+ }
119
+ const WORKTRUNK_INSTALL_HINTS = {
120
+ darwin: "brew install worktrunk",
121
+ // Worktrunk installs as the git-wt winget alias on Windows.
122
+ win32: "Install Worktrunk via winget; it installs as the git-wt alias on Windows.",
123
+ // No universal one-line installer on Linux — point at the docs (link-only).
124
+ linux: "See the Worktrunk documentation for Linux install instructions: https://worktrunk.dev",
125
+ };
126
+ const GIT_INSTALL_HINTS = {
127
+ darwin: "xcode-select --install (or brew install git)",
128
+ linux: "Install git with your distro package manager, e.g. apt install git",
129
+ win32: "Install Git for Windows: https://git-scm.com/download/win",
130
+ };
131
+ const OSASCRIPT_INSTALL_HINTS = {
132
+ darwin: "osascript ships with macOS; if it is missing, repair your macOS command line tools.",
133
+ linux: "osascript is macOS-only.",
134
+ win32: "osascript is macOS-only.",
135
+ };
136
+ const TMUX_INSTALL_HINTS = {
137
+ darwin: "brew install tmux",
138
+ linux: "Install tmux with your distro package manager, e.g. apt install tmux",
139
+ win32: "tmux is used only on Linux.",
140
+ };
141
+ const GIT_BASH_INSTALL_HINTS = {
142
+ darwin: GIT_FOR_WINDOWS_BASH_HINT,
143
+ linux: GIT_FOR_WINDOWS_BASH_HINT,
144
+ win32: GIT_FOR_WINDOWS_BASH_HINT,
145
+ };
146
+ const WINDOWS_LAUNCHER_INSTALL_HINTS = {
147
+ darwin: "Windows Terminal / PowerShell are used only on Windows.",
148
+ linux: "Windows Terminal / PowerShell are used only on Windows.",
149
+ win32: "Install Windows Terminal (winget install Microsoft.WindowsTerminal) or ensure powershell.exe is on PATH.",
150
+ };
151
+ const GIT_WORK_TREE_INSTALL_HINTS = {
152
+ darwin: "Run start-tickets from inside a git repository work tree.",
153
+ linux: "Run start-tickets from inside a git repository work tree.",
154
+ win32: "Run start-tickets from inside a git repository work tree.",
155
+ };
156
+ const UV_INSTALL_HINTS = {
157
+ darwin: "brew install uv",
158
+ linux: "curl -LsSf https://astral.sh/uv/install.sh | sh",
159
+ win32: 'powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"',
160
+ };
161
+ function commandDescriptor(tool, label, installHint) {
162
+ return {
163
+ id: tool,
164
+ label,
165
+ installHint,
166
+ preflightError: `Required command not found on PATH: ${tool}.`,
167
+ probe: async (deps) => {
168
+ const found = await isCommandOnPath(deps, tool);
169
+ return found ? { found: true, detail: "found on PATH" } : { found: false };
170
+ },
171
+ };
172
+ }
173
+ function worktrunkDescriptor(binary) {
174
+ return {
175
+ id: "worktrunk",
176
+ label: `Worktrunk (${binary})`,
177
+ installHint: WORKTRUNK_INSTALL_HINTS,
178
+ preflightError: `Required command not found on PATH: ${binary}.`,
179
+ probe: async (deps) => {
180
+ const found = await isCommandOnPath(deps, binary);
181
+ // The resolved binary is shown via the label (`Worktrunk (<binary>)`); the
182
+ // detail stays generic so preflight enforcement uses `preflightError`.
183
+ return found ? { found: true, detail: "found on PATH" } : { found: false };
184
+ },
185
+ };
186
+ }
187
+ function gitBashDescriptor() {
188
+ return {
189
+ id: "git-bash",
190
+ label: "Git Bash (bash)",
191
+ installHint: GIT_BASH_INSTALL_HINTS,
192
+ probe: async (deps) => {
193
+ const result = await requireBashUsable(deps);
194
+ return result.ok ? { found: true, detail: "bash --version ok" } : { found: false, detail: result.error };
195
+ },
196
+ };
197
+ }
198
+ function windowsLauncherDescriptor() {
199
+ const candidates = [WINDOWS_TERMINAL_COMMAND, ...WINDOWS_POWERSHELL_CANDIDATES];
200
+ return {
201
+ id: "windows-launcher",
202
+ label: "Windows Terminal or PowerShell",
203
+ installHint: WINDOWS_LAUNCHER_INSTALL_HINTS,
204
+ preflightError: "Windows Terminal (wt.exe) or PowerShell is required to open a tab. " +
205
+ "Install Windows Terminal or ensure powershell.exe is on PATH.",
206
+ probe: async (deps) => {
207
+ const found = await resolveFirstCommandOnPath(deps, candidates);
208
+ return found ? { found: true, detail: found } : { found: false };
209
+ },
210
+ };
211
+ }
212
+ function gitWorkTreeDescriptor() {
213
+ return {
214
+ id: "git-work-tree",
215
+ label: "git work tree",
216
+ installHint: GIT_WORK_TREE_INSTALL_HINTS,
217
+ probe: async (deps) => {
218
+ const revParse = await deps.runCommand("git", ["rev-parse", "--is-inside-work-tree"], {
219
+ cwd: deps.cwd,
220
+ });
221
+ if (!commandSucceeded(revParse)) {
222
+ return {
223
+ found: false,
224
+ detail: "start-tickets must be run inside a git repository (git rev-parse --is-inside-work-tree failed).",
225
+ };
226
+ }
227
+ if (revParse.stdout.trim() !== "true") {
228
+ return {
229
+ found: false,
230
+ detail: "start-tickets must be run inside a git work tree (git rev-parse --is-inside-work-tree did not report 'true').",
231
+ };
232
+ }
233
+ return { found: true, detail: "inside a git work tree" };
234
+ },
235
+ };
236
+ }
237
+ function agentDescriptor(agent) {
238
+ return {
239
+ id: agent.command,
240
+ label: agent.name,
241
+ installHint: agent.installHint,
242
+ authNote: agent.authNote,
243
+ preflightError: `Required command not found on PATH: ${agent.command}.`,
244
+ probe: async (deps) => {
245
+ const found = await isCommandOnPath(deps, agent.command);
246
+ return found ? { found: true, detail: "found on PATH" } : { found: false };
247
+ },
248
+ };
249
+ }
250
+ function uvDescriptor() {
251
+ return commandDescriptor("uv", "uv", UV_INSTALL_HINTS);
252
+ }
253
+ /**
254
+ * The exact existing live-preflight requirement set per platform, plus the git
255
+ * work-tree custom check appended after the command checks. This is the ONLY
256
+ * place the per-OS preflight knowledge lives; `runPreflight` enforces it and
257
+ * `doctor` renders it (via `getDoctorPrereqDescriptors`). Returns a structured
258
+ * unsupported-platform error for non-darwin/win32/linux.
259
+ */
260
+ export function getPreflightPrereqDescriptors(platform, env) {
261
+ if (!isSupportedStartTicketsPlatform(platform)) {
262
+ return { ok: false, error: unsupportedPlatformMessage(platform) };
263
+ }
264
+ const worktrunkBinary = resolveWorktrunkBinary(platform, env);
265
+ const descriptors = [worktrunkDescriptor(worktrunkBinary)];
266
+ descriptors.push(commandDescriptor("git", "git", GIT_INSTALL_HINTS));
267
+ if (platform === "darwin") {
268
+ descriptors.push(commandDescriptor("osascript", "osascript", OSASCRIPT_INSTALL_HINTS));
269
+ }
270
+ else if (platform === "win32") {
271
+ descriptors.push(gitBashDescriptor());
272
+ descriptors.push(windowsLauncherDescriptor());
273
+ }
274
+ else {
275
+ descriptors.push(commandDescriptor(TMUX_COMMAND, TMUX_COMMAND, TMUX_INSTALL_HINTS));
276
+ }
277
+ // Git work-tree check shares the same descriptor source so runPreflight and
278
+ // doctor never drift on it.
279
+ descriptors.push(gitWorkTreeDescriptor());
280
+ return { ok: true, descriptors };
281
+ }
282
+ /**
283
+ * Doctor-only prerequisites: `uv` (a real gap — Worktrunk's pre-start hook runs
284
+ * `uv`, but live preflight does not check it) and the selected agent command.
285
+ * These are intentionally kept out of `runPreflight` so live behaviour stays
286
+ * additive (BAPI-302/303 regression-safe).
287
+ */
288
+ export function getDoctorOnlyPrereqDescriptors(_platform, _env, agent) {
289
+ return [uvDescriptor(), agentDescriptor(agent)];
290
+ }
291
+ /**
292
+ * The doctor's full descriptor set: the preflight descriptors (verbatim) plus
293
+ * the doctor-only `uv` and selected-agent descriptors. The preflight prefix is
294
+ * byte-for-byte the same as `getPreflightPrereqDescriptors`, which a drift-guard
295
+ * test asserts.
296
+ */
297
+ export function getDoctorPrereqDescriptors(platform, env, agent) {
298
+ const preflight = getPreflightPrereqDescriptors(platform, env);
299
+ if (!preflight.ok)
300
+ return preflight;
301
+ return {
302
+ ok: true,
303
+ descriptors: [...preflight.descriptors, ...getDoctorOnlyPrereqDescriptors(platform, env, agent)],
304
+ };
305
+ }
306
+ /** Probe one descriptor without throwing, resolving its platform-specific hint. */
307
+ export async function probePrerequisite(deps, descriptor) {
308
+ let outcome;
309
+ try {
310
+ outcome = await descriptor.probe(deps);
311
+ }
312
+ catch (err) {
313
+ outcome = { found: false, detail: err instanceof Error ? err.message : String(err) };
314
+ }
315
+ return {
316
+ id: descriptor.id,
317
+ label: descriptor.label,
318
+ found: outcome.found,
319
+ detail: outcome.detail,
320
+ installHint: hintForPlatform(descriptor.installHint, deps.platform),
321
+ authNote: descriptor.authNote,
322
+ };
323
+ }
324
+ /**
325
+ * Enforce the preflight descriptors fail-fast (matching the existing
326
+ * `runPreflight` behaviour). Iterates the shared preflight descriptors in order
327
+ * and returns on the first missing one. `uv` and the selected agent are NOT
328
+ * enforced here — they are doctor-only. Never throws.
329
+ */
330
+ export async function enforcePreflightPrerequisites(deps) {
331
+ const descriptorsResult = getPreflightPrereqDescriptors(deps.platform, deps.env);
332
+ if (!descriptorsResult.ok) {
333
+ return { ok: false, reason: "unsupported-platform", error: descriptorsResult.error };
334
+ }
335
+ for (const descriptor of descriptorsResult.descriptors) {
336
+ const probed = await probePrerequisite(deps, descriptor);
337
+ if (!probed.found) {
338
+ // Command-style descriptors carry their message in `preflightError`; the
339
+ // custom Git Bash / git-work-tree checks carry a specific message in
340
+ // `detail`. Prefer the descriptor's own error, then the probe detail.
341
+ const error = descriptor.preflightError ?? probed.detail ?? `Missing prerequisite: ${descriptor.label}.`;
342
+ return { ok: false, reason: "missing-prerequisite", error };
343
+ }
344
+ }
345
+ return { ok: true };
346
+ }