bmad-stella 2.0.0 → 2.0.1

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.
@@ -1,13 +1,8 @@
1
1
  const { execSync } = require('node:child_process');
2
- const fs = require('fs-extra');
3
2
  const path = require('node:path');
4
3
  const chalk = require('chalk');
5
4
  const inquirer = require('inquirer');
6
5
 
7
- const GITHUB_API_USER_URL = 'https://api.github.com/user';
8
- const GITHUB_VERIFY_TIMEOUT_MS = 10_000;
9
- const MAX_GITHUB_VERIFY_ATTEMPTS = 3;
10
-
11
6
  class DependencyManager {
12
7
  constructor() {
13
8
  this.requiredMcpServers = {
@@ -28,28 +23,6 @@ class DependencyManager {
28
23
  },
29
24
  },
30
25
  },
31
- github: {
32
- name: 'GitHub MCP Server',
33
- description:
34
- 'GitHub integration for repository, issue, and pull request operations (authenticates with a GitHub personal access token)',
35
- transport: 'http',
36
- url: 'https://api.githubcopilot.com/mcp/',
37
- envVars: {},
38
- // GitHub's remote MCP server authenticates with a Personal Access Token sent as
39
- // 'Authorization: Bearer <token>' — not OAuth. To keep the token out of Claude's
40
- // config, the installer stores it in the project's git-ignored .env and registers
41
- // the server with a `headersHelper` that reads it at connection time. See:
42
- // https://code.claude.com/docs/en/mcp and
43
- // https://github.com/github/github-mcp-server/blob/main/docs/installation-guides/install-claude.md
44
- tokenAuth: {
45
- prompt:
46
- 'GitHub Personal Access Token (fine-grained, create at https://github.com/settings/personal-access-tokens):',
47
- envVar: 'GITHUB_PERSONAL_ACCESS_TOKEN',
48
- helpUrl: 'https://github.com/settings/personal-access-tokens',
49
- // Relative to the installed project root; the helper is copied here from bmad-core/utils/.
50
- helperRelPath: '.bmad-core/utils/github-mcp-auth.js',
51
- },
52
- },
53
26
  };
54
27
  }
55
28
 
@@ -81,9 +54,9 @@ class DependencyManager {
81
54
  });
82
55
  } catch (error) {
83
56
  // `claude mcp list` exits non-zero when ANY configured server is unhealthy — e.g. a
84
- // GitHub server whose headersHelper hasn't been trusted yet at install time. It still
85
- // prints per-server status to stdout, so parse that rather than discarding every
86
- // server's status (otherwise one pending server would mask a healthy one like Atlassian).
57
+ // server that is registered but not yet authenticated. It still prints per-server
58
+ // status to stdout, so parse that rather than discarding every server's status
59
+ // (otherwise one pending server would mask a healthy one like Atlassian).
87
60
  output = error.stdout ? String(error.stdout) : '';
88
61
  if (!output) {
89
62
  console.warn(chalk.yellow('Warning: Could not list MCP servers'), error.message);
@@ -147,6 +120,143 @@ class DependencyManager {
147
120
  }
148
121
  }
149
122
 
123
+ /**
124
+ * Silently remove artifacts left behind by the retired GitHub MCP integration
125
+ * (older installer versions registered a `github` MCP server, stored a PAT in the
126
+ * project's .env, and shipped a headers-helper script). The server is removed ONLY
127
+ * when its definition carries the BMad fingerprint — a headersHelper pointing at
128
+ * .bmad-core/utils/github-mcp-auth.js — which no manually-configured GitHub MCP
129
+ * would have, so a server the user added themselves is never touched. Best-effort
130
+ * and intentionally silent: any failure leaves things exactly as they are.
131
+ * @param {string} installDir
132
+ */
133
+ async cleanupLegacyGithubMcp(installDir) {
134
+ try {
135
+ await this._removeLegacyGithubServer(installDir);
136
+ } catch {
137
+ // silent — leave the server as-is
138
+ }
139
+ try {
140
+ await this._removeLegacyGithubEnvBlock(installDir);
141
+ } catch {
142
+ // silent — leave the .env as-is
143
+ }
144
+ try {
145
+ await this._removeLegacyGithubAuthHelper(installDir);
146
+ } catch {
147
+ // silent — leave the helper file as-is
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Remove the BMad-registered `github` MCP server for this project, identified by
153
+ * its headersHelper fingerprint in ~/.claude.json. No fingerprint → no removal.
154
+ * @param {string} installDir
155
+ */
156
+ async _removeLegacyGithubServer(installDir) {
157
+ if (!this.isClaudeCLIInstalled()) return;
158
+
159
+ const fsp = require('node:fs/promises');
160
+ const os = require('node:os');
161
+
162
+ let config;
163
+ try {
164
+ config = JSON.parse(await fsp.readFile(path.join(os.homedir(), '.claude.json'), 'utf8'));
165
+ } catch {
166
+ return; // no config or unparseable → nothing provably ours to remove
167
+ }
168
+
169
+ const normalize = (p) => {
170
+ const resolved = path.resolve(String(p));
171
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
172
+ };
173
+ const target = normalize(installDir);
174
+
175
+ let serverDef = null;
176
+ for (const [projectPath, project] of Object.entries(config.projects || {})) {
177
+ if (normalize(projectPath) === target) {
178
+ serverDef = project && project.mcpServers ? project.mcpServers.github : null;
179
+ break;
180
+ }
181
+ }
182
+ if (!serverDef) return;
183
+
184
+ // Only the BMad installer ever registered the server with a headersHelper
185
+ // pointing into .bmad-core — a user-added GitHub MCP fails this check and stays.
186
+ const helper = String(serverDef.headersHelper || '').replaceAll('\\', '/');
187
+ if (!helper.includes('.bmad-core/utils/github-mcp-auth.js')) return;
188
+
189
+ execSync('claude mcp remove github', { cwd: installDir, stdio: 'pipe' });
190
+ }
191
+
192
+ /**
193
+ * Strip the BMad-written GitHub managed block (and the PAT inside it) from the
194
+ * project's .env. Lines outside the markers — including a GITHUB_PERSONAL_ACCESS_TOKEN
195
+ * the user added themselves — are preserved byte-for-byte.
196
+ * @param {string} installDir
197
+ */
198
+ async _removeLegacyGithubEnvBlock(installDir) {
199
+ const fsp = require('node:fs/promises');
200
+ const envPath = path.join(installDir, 'bmad-docs', '.bmad-tokens', '.env');
201
+
202
+ let contents;
203
+ try {
204
+ contents = await fsp.readFile(envPath, 'utf8');
205
+ } catch {
206
+ return;
207
+ }
208
+
209
+ const START_MARKER = '# --- BMad-Stella GitHub managed (do not edit) ---';
210
+ const END_MARKER = '# --- end BMad-Stella GitHub managed ---';
211
+ if (!contents.includes(START_MARKER)) return;
212
+
213
+ const kept = [];
214
+ let insideBlock = false;
215
+ for (const line of contents.split(/\r?\n/)) {
216
+ const trimmed = line.trim();
217
+ if (trimmed === START_MARKER) {
218
+ insideBlock = true;
219
+ continue;
220
+ }
221
+ if (trimmed === END_MARKER) {
222
+ insideBlock = false;
223
+ continue;
224
+ }
225
+ if (insideBlock) continue;
226
+ kept.push(line);
227
+ }
228
+ while (kept.length > 0 && kept[0].trim() === '') kept.shift();
229
+ while (kept.length > 0 && kept.at(-1).trim() === '') kept.pop();
230
+
231
+ const output = kept.length > 0 ? `${kept.join('\n')}\n` : '';
232
+ // temp + rename so mode is enforced before the data lands at envPath
233
+ const tmpPath = `${envPath}.${process.pid}.${Date.now()}.tmp`;
234
+ await fsp.writeFile(tmpPath, output, { encoding: 'utf8', mode: 0o600 });
235
+ try {
236
+ await fsp.chmod(tmpPath, 0o600);
237
+ } catch {
238
+ // best-effort on Windows (ACLs apply)
239
+ }
240
+ await fsp.rename(tmpPath, envPath);
241
+ try {
242
+ await fsp.chmod(envPath, 0o600);
243
+ } catch {
244
+ // best-effort on Windows
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Delete the orphaned headers-helper script from an upgraded install
250
+ * (upgrades overwrite copied files but never remove ones dropped from bmad-core).
251
+ * @param {string} installDir
252
+ */
253
+ async _removeLegacyGithubAuthHelper(installDir) {
254
+ const fsp = require('node:fs/promises');
255
+ await fsp.rm(path.join(installDir, '.bmad-core', 'utils', 'github-mcp-auth.js'), {
256
+ force: true,
257
+ });
258
+ }
259
+
150
260
  /**
151
261
  * Check if a specific MCP server is installed
152
262
  * @param {string} installDir - Target installation directory
@@ -218,348 +328,6 @@ class DependencyManager {
218
328
  }
219
329
  }
220
330
 
221
- /**
222
- * Register an MCP server from a full JSON definition via `claude mcp add-json`.
223
- * Used for servers that need fields the `--transport/--env/--header` flags can't
224
- * express — e.g. GitHub's `headersHelper`. The JSON is passed as a single shell
225
- * argument, platform-quoted so embedded quotes/backslashes survive intact.
226
- * @param {string} installDir
227
- * @param {string} serverName
228
- * @param {object} serverDef - the server entry object (type, url, headersHelper, …)
229
- * @returns {Promise<boolean>}
230
- */
231
- async addMcpServerJson(installDir, serverName, serverDef) {
232
- try {
233
- const json = JSON.stringify(serverDef);
234
- const command = `claude mcp add-json ${serverName} ${this._shellQuoteArg(json)}`;
235
-
236
- console.log(chalk.cyan(`\n📦 Adding ${serverName} (JSON config)...`));
237
- console.log(chalk.dim(` Command: claude mcp add-json ${serverName} '${json}'`));
238
-
239
- execSync(command, { cwd: installDir, stdio: 'inherit' });
240
-
241
- console.log(chalk.green(`✓ Successfully added ${serverName}`));
242
- return true;
243
- } catch (error) {
244
- console.error(chalk.red(`\n✗ Failed to add ${serverName}:`), error.message);
245
- return false;
246
- }
247
- }
248
-
249
- /**
250
- * Quote a single argument for the platform shell that execSync uses
251
- * (cmd.exe on Windows, /bin/sh elsewhere) so JSON survives unmangled.
252
- *
253
- * On Windows the naive `"` → `\"` substitution breaks when the value contains
254
- * backslashes (e.g. file paths), because the MSVCRT argv parser counts the
255
- * backslash run that precedes a quote. We follow the documented rules: double a
256
- * run of N backslashes to 2N (and 2N+1) only when it precedes a `"` or the
257
- * closing quote. JSON has no cmd metacharacters (& | < > ^) so cmd-level escaping
258
- * isn't needed here.
259
- * @param {string} arg
260
- * @returns {string}
261
- */
262
- _shellQuoteArg(arg) {
263
- const str = String(arg);
264
- if (process.platform !== 'win32') {
265
- // POSIX sh: single-quote (JSON contains no single quotes); handle any defensively.
266
- return `'${str.replaceAll("'", `'\\''`)}'`;
267
- }
268
- let out = '"';
269
- let backslashes = 0;
270
- for (const ch of str) {
271
- if (ch === '\\') {
272
- backslashes += 1;
273
- } else if (ch === '"') {
274
- out += '\\'.repeat(backslashes * 2 + 1) + '"';
275
- backslashes = 0;
276
- } else {
277
- out += '\\'.repeat(backslashes) + ch;
278
- backslashes = 0;
279
- }
280
- }
281
- // Trailing backslashes precede the closing quote, so they must be doubled.
282
- out += '\\'.repeat(backslashes * 2) + '"';
283
- return out;
284
- }
285
-
286
- /**
287
- * Prompt (masked) for a token, returning the trimmed token or null if the user
288
- * leaves it blank (caller should skip that server).
289
- * @param {object} tokenAuth - serverConfig.tokenAuth ({prompt})
290
- * @returns {Promise<string|null>}
291
- */
292
- async promptForToken(tokenAuth) {
293
- const { token } = await inquirer.prompt([
294
- {
295
- type: 'password',
296
- name: 'token',
297
- mask: '*',
298
- message: tokenAuth.prompt || 'Enter access token:',
299
- validate: (input) => {
300
- if (!input || !input.trim()) return true; // allow blank → caller skips
301
- if (input.trim().length < 8)
302
- return 'That token looks too short — paste the full token, or leave blank to skip';
303
- return true;
304
- },
305
- // Strip a pasted 'Bearer ' prefix and surrounding whitespace; we add the scheme ourselves.
306
- filter: (input) => (input ? input.trim().replace(/^Bearer\s+/i, '') : input),
307
- },
308
- ]);
309
-
310
- return token && token.trim() ? token.trim() : null;
311
- }
312
-
313
- /** Escape hatch to skip live GitHub verification, mirroring BMAD_SKIP_JIRA_VERIFY. */
314
- _shouldSkipGithubVerify() {
315
- const flag = String(process.env.BMAD_SKIP_GITHUB_VERIFY || '').toLowerCase();
316
- return flag === '1' || flag === 'true' || flag === 'yes';
317
- }
318
-
319
- /**
320
- * Live-validate a GitHub PAT against `GET /user`. Never throws — returns a
321
- * classification the caller acts on. 401 → 'auth' (expired/revoked/invalid → re-enter);
322
- * 403 (rate-limit or policy) → 'network' (proceed, don't trap over a transient limit);
323
- * 2xx → 'ok' with the resolved login.
324
- * @param {string} token
325
- * @returns {Promise<{classification:'ok'|'auth'|'network'|'skipped', status:number|null, login:string|null, error:string|null}>}
326
- */
327
- async _verifyGithubToken(token) {
328
- const out = { classification: 'network', status: null, login: null, error: null };
329
- if (this._shouldSkipGithubVerify()) {
330
- out.classification = 'skipped';
331
- return out;
332
- }
333
- const tok = String(token || '').trim();
334
- if (!tok) {
335
- out.error = 'empty token';
336
- return out;
337
- }
338
-
339
- const signal =
340
- typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
341
- ? AbortSignal.timeout(GITHUB_VERIFY_TIMEOUT_MS)
342
- : undefined;
343
-
344
- try {
345
- // global fetch is available on the project's supported runtime (Node >=20.10); the
346
- // lint rule is conservative about the >=20.0.0 engines floor.
347
- // eslint-disable-next-line n/no-unsupported-features/node-builtins
348
- const response = await fetch(GITHUB_API_USER_URL, {
349
- method: 'GET',
350
- headers: {
351
- Authorization: `Bearer ${tok}`,
352
- Accept: 'application/vnd.github+json',
353
- 'X-GitHub-Api-Version': '2022-11-28',
354
- 'User-Agent': 'bmad-stella-installer/1.0',
355
- },
356
- redirect: 'follow',
357
- signal,
358
- });
359
- out.status = response.status;
360
- if (response.ok) {
361
- out.classification = 'ok';
362
- try {
363
- const body = await response.json();
364
- out.login = body.login || null;
365
- } catch {
366
- // a 2xx already proves the token works; body parse is best-effort
367
- }
368
- return out;
369
- }
370
- if (response.status === 401) {
371
- out.classification = 'auth'; // expired / revoked / invalid → re-enter
372
- return out;
373
- }
374
- // 403 on GitHub is usually rate-limiting (or org/SSO policy) — not proof the token is
375
- // bad — so treat it as unverifiable-but-proceed rather than forcing a re-enter.
376
- if (response.status === 403) {
377
- const remaining = response.headers.get('x-ratelimit-remaining');
378
- out.error = remaining === '0' ? 'GitHub API rate limit reached' : 'HTTP 403';
379
- return out; // classification stays 'network'
380
- }
381
- out.error = `HTTP ${response.status}`;
382
- return out; // other non-2xx → unverifiable, proceed
383
- } catch (error) {
384
- out.error = error.message || String(error); // timeout / DNS / offline
385
- return out;
386
- }
387
- }
388
-
389
- /**
390
- * Read an existing GitHub token from the environment or the project's .env (any line,
391
- * inside the managed block or not). Returns the trimmed token or null.
392
- * @param {string} installDir
393
- * @returns {Promise<string|null>}
394
- */
395
- async _readGithubTokenFromEnv(installDir) {
396
- const fromEnv = process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
397
- if (fromEnv && fromEnv.trim()) return fromEnv.trim();
398
-
399
- const fsp = require('node:fs/promises');
400
- let contents;
401
- try {
402
- contents = await fsp.readFile(
403
- path.join(installDir, 'bmad-docs', '.bmad-tokens', '.env'),
404
- 'utf8',
405
- );
406
- } catch {
407
- return null;
408
- }
409
- for (const rawLine of contents.split(/\r?\n/)) {
410
- const line = rawLine.trim();
411
- if (!line || line.startsWith('#')) continue;
412
- const eq = line.indexOf('=');
413
- if (eq === -1 || line.slice(0, eq).trim() !== 'GITHUB_PERSONAL_ACCESS_TOKEN') continue;
414
- let value = line.slice(eq + 1).trim();
415
- if (
416
- (value.startsWith('"') && value.endsWith('"')) ||
417
- (value.startsWith("'") && value.endsWith("'"))
418
- ) {
419
- value = value.slice(1, -1);
420
- }
421
- return value.trim() || null;
422
- }
423
- return null;
424
- }
425
-
426
- /** Print a friendly line for a GitHub verification result. */
427
- _reportGithubVerification(verification, successLabel) {
428
- if (verification.classification === 'ok') {
429
- const who = verification.login ? ` (authenticated as ${verification.login})` : '';
430
- console.log(chalk.green(`✓ ${successLabel}${who}`));
431
- } else if (verification.classification === 'skipped') {
432
- console.log(chalk.dim(' Skipping live verification (BMAD_SKIP_GITHUB_VERIFY set).'));
433
- } else {
434
- const why = verification.error ? ` (${verification.error})` : '';
435
- console.log(
436
- chalk.yellow(
437
- `⚠️ Could not verify the GitHub token${why} — proceeding. It will be checked when you run /mcp.`,
438
- ),
439
- );
440
- }
441
- }
442
-
443
- /**
444
- * Prompt for a fresh GitHub token and verify it, retrying on a 401 rejection up to the
445
- * attempt budget. Returns the accepted token, or null if the user leaves it blank (skip).
446
- * @param {object} tokenAuth
447
- * @returns {Promise<string|null>}
448
- */
449
- async _collectFreshGithubToken(tokenAuth) {
450
- let last = null;
451
- for (let attempt = 1; attempt <= MAX_GITHUB_VERIFY_ATTEMPTS; attempt += 1) {
452
- const token = await this.promptForToken(tokenAuth);
453
- if (!token) return null; // blank → skip
454
- last = token;
455
- const verification = await this._verifyGithubToken(token);
456
- if (verification.classification !== 'auth') {
457
- // ok / network / skipped → accept (don't trap the user on a transient/unreachable check)
458
- this._reportGithubVerification(verification, 'Verified GitHub token');
459
- return token;
460
- }
461
- console.log(chalk.red('✗ That token was rejected by GitHub (expired, revoked, or invalid).'));
462
- if (attempt < MAX_GITHUB_VERIFY_ATTEMPTS) {
463
- console.log(
464
- chalk.dim(` Attempt ${attempt}/${MAX_GITHUB_VERIFY_ATTEMPTS} failed — let's try again.`),
465
- );
466
- }
467
- }
468
- const { saveAnyway } = await inquirer.prompt([
469
- {
470
- type: 'confirm',
471
- name: 'saveAnyway',
472
- message: `Could not verify after ${MAX_GITHUB_VERIFY_ATTEMPTS} attempts. Save the token anyway?`,
473
- default: false,
474
- },
475
- ]);
476
- return saveAnyway ? last : null;
477
- }
478
-
479
- /**
480
- * Persist a single KEY=value into the project's git-ignored .env (mode 0600) inside
481
- * its own clearly-marked managed block, preserving every other line (including the
482
- * separate JIRA managed block written by jira-credentials-manager). Idempotent: an
483
- * existing block with the same label — or a legacy bare `KEY=` line from an earlier
484
- * format — is removed before the fresh block is appended, so re-runs don't duplicate.
485
- * Used to store the GitHub PAT that the github-mcp-auth headersHelper reads at connect time.
486
- * @param {string} installDir
487
- * @param {string} key
488
- * @param {string} value
489
- * @param {string} blockName - human label for the block header (e.g. 'GitHub')
490
- * @returns {Promise<{ok: boolean, envPath: string, error: string|null}>}
491
- */
492
- async persistEnvVar(installDir, key, value, blockName) {
493
- const fsp = require('node:fs/promises');
494
- // Tokens live in <project>/bmad-docs/.bmad-tokens/.env (git-ignored via bmad-docs/).
495
- const envPath = path.join(installDir, 'bmad-docs', '.bmad-tokens', '.env');
496
- const out = { ok: false, envPath, error: null };
497
-
498
- const startMarker = `# --- BMad-Stella ${blockName} managed (do not edit) ---`;
499
- const endMarker = `# --- end BMad-Stella ${blockName} managed ---`;
500
-
501
- try {
502
- let existing = '';
503
- try {
504
- existing = await fsp.readFile(envPath, 'utf8');
505
- } catch (error) {
506
- if (error.code !== 'ENOENT') throw error;
507
- }
508
-
509
- // Preserve all lines except (a) our own previous block and (b) any legacy bare
510
- // KEY= line. The JIRA block uses different markers/keys, so it is left untouched.
511
- const preserved = [];
512
- let insideOurBlock = false;
513
- for (const line of existing.split(/\r?\n/)) {
514
- const trimmed = line.trim();
515
- if (trimmed === startMarker) {
516
- insideOurBlock = true;
517
- continue;
518
- }
519
- if (trimmed === endMarker) {
520
- insideOurBlock = false;
521
- continue;
522
- }
523
- if (insideOurBlock) continue;
524
- const eq = trimmed.indexOf('=');
525
- if (eq !== -1 && !trimmed.startsWith('#') && trimmed.slice(0, eq).trim() === key) {
526
- continue; // drop legacy bare line for this key
527
- }
528
- preserved.push(line);
529
- }
530
- while (preserved.length > 0 && preserved[0].trim() === '') preserved.shift();
531
- while (preserved.length > 0 && preserved.at(-1).trim() === '') preserved.pop();
532
-
533
- const quoted = /[\s#"'=]/.test(value) ? `"${value.replaceAll('"', '\\"')}"` : value;
534
- const block = [startMarker, `${key}=${quoted}`, endMarker];
535
- // Prepend our block above any preserved content. This keeps the managed block at the
536
- // top and, when the JIRA helper later rewrites its own block at the end of the file,
537
- // leaves no stray leading blank line (the JIRA writer only trims trailing blanks).
538
- const body = preserved.length > 0 ? [...block, '', ...preserved] : block;
539
- const output = `${body.join('\n')}\n`;
540
-
541
- // temp + rename so mode is enforced before the data lands at envPath
542
- const tmpPath = `${envPath}.${process.pid}.${Date.now()}.tmp`;
543
- await fsp.mkdir(path.dirname(envPath), { recursive: true });
544
- await fsp.writeFile(tmpPath, output, { encoding: 'utf8', mode: 0o600 });
545
- try {
546
- await fsp.chmod(tmpPath, 0o600);
547
- } catch {
548
- // best-effort on Windows (ACLs apply)
549
- }
550
- await fsp.rename(tmpPath, envPath);
551
- try {
552
- await fsp.chmod(envPath, 0o600);
553
- } catch {
554
- // best-effort on Windows
555
- }
556
- out.ok = true;
557
- } catch (error) {
558
- out.error = error.message;
559
- }
560
- return out;
561
- }
562
-
563
331
  /**
564
332
  * Prompt user for environment variables
565
333
  * @param {object} envVarsConfig - Environment variables configuration
@@ -614,6 +382,10 @@ class DependencyManager {
614
382
  alreadyConfigured: [],
615
383
  };
616
384
 
385
+ // Silently clean up leftovers from the retired GitHub MCP integration on
386
+ // upgraded installs (fingerprinted server registration, stored PAT, helper script).
387
+ await this.cleanupLegacyGithubMcp(installDir);
388
+
617
389
  // Check if Claude CLI is installed
618
390
  if (!this.isClaudeCLIInstalled()) {
619
391
  console.log(
@@ -657,11 +429,6 @@ class DependencyManager {
657
429
  value: 'atlassian',
658
430
  checked: true,
659
431
  },
660
- {
661
- name: 'GitHub (for repository, issue, and PR integration)',
662
- value: 'github',
663
- checked: true,
664
- },
665
432
  {
666
433
  name: 'Other (custom MCP server)',
667
434
  value: 'other',
@@ -730,131 +497,6 @@ class DependencyManager {
730
497
  // server-by-server setup. atlassianBaseUrl captured above is reused there.
731
498
  }
732
499
 
733
- // Process GitHub if selected. GitHub's remote MCP server authenticates with a
734
- // Personal Access Token. We keep the token out of Claude's config: it's written to
735
- // the project's git-ignored .env, and the server is registered with a `headersHelper`
736
- // that reads it at connection time (see .bmad-core/utils/github-mcp-auth.js).
737
- if (selectedMcpServers.includes('github')) {
738
- const serverName = 'github';
739
- const serverConfig = this.requiredMcpServers[serverName];
740
- const { tokenAuth } = serverConfig;
741
- results.checked.push(serverName);
742
-
743
- console.log(chalk.cyan(`\n📦 Configuring ${serverConfig.name}...`));
744
- console.log(chalk.dim(` ${serverConfig.description}\n`));
745
-
746
- const isInstalled = await this.isMcpServerInstalled(installDir, serverName);
747
- const existingToken = await this._readGithubTokenFromEnv(installDir);
748
- let token = null;
749
-
750
- // 1. If a token already exists, offer to reuse it — verifying it's still live first.
751
- if (existingToken) {
752
- console.log(chalk.green('✓ Detected an existing GitHub token.'));
753
- const { reuse } = await inquirer.prompt([
754
- {
755
- type: 'confirm',
756
- name: 'reuse',
757
- message: 'Use the detected GitHub token as-is?',
758
- default: true,
759
- },
760
- ]);
761
- if (reuse) {
762
- const verification = await this._verifyGithubToken(existingToken);
763
- if (verification.classification === 'auth') {
764
- console.log(
765
- chalk.yellow(
766
- "⚠️ The detected token no longer works (expired, revoked, or invalid). Let's re-enter it.",
767
- ),
768
- );
769
- } else {
770
- // ok → verified; network/skipped → couldn't check but proceed (don't trap the user)
771
- this._reportGithubVerification(verification, 'Verified existing GitHub token');
772
- token = existingToken;
773
- }
774
- }
775
- }
776
-
777
- // 2. Otherwise (no token, declined reuse, or a dead token) prompt for a fresh one,
778
- // verifying it and retrying on a 401 rejection.
779
- if (!token) {
780
- console.log(
781
- chalk.dim(
782
- ` Create a fine-grained token at ${tokenAuth.helpUrl} with access to the repositories you want Claude to work with.`,
783
- ),
784
- );
785
- console.log(
786
- chalk.dim(
787
- ' Grant Repository permissions (read-only): Contents: Read AND Pull requests: Read (Metadata: Read is required automatically).',
788
- ),
789
- );
790
- console.log(
791
- chalk.dim(
792
- ' Contents: Read is required to fetch PR diffs and file contents — a Pull-requests-only token returns 403 on the diff.',
793
- ),
794
- );
795
- console.log(
796
- chalk.dim(
797
- ' Step-by-step guide: docs/github-pat-guide.md (also in .bmad-core after install).',
798
- ),
799
- );
800
- token = await this._collectFreshGithubToken(tokenAuth);
801
- }
802
-
803
- // 3. Persist the token to .env, then register the server (or just refresh .env if the
804
- // server is already registered — its headersHelper re-reads .env, so no re-add).
805
- if (token) {
806
- const envResult = await this.persistEnvVar(installDir, tokenAuth.envVar, token, 'GitHub');
807
- if (envResult.ok) {
808
- console.log(
809
- chalk.green(
810
- `✓ Stored ${tokenAuth.envVar} in ${path.relative(installDir, envResult.envPath) || '.env'} (git-ignored)`,
811
- ),
812
- );
813
-
814
- if (isInstalled) {
815
- console.log(
816
- chalk.green('✓ GitHub MCP server already registered; token updated in .env'),
817
- );
818
- results.alreadyConfigured.push(serverName);
819
- } else {
820
- // Register with a headersHelper that reads the token at connect time.
821
- // Absolute path → local scope in ~/.claude.json; no token is stored in config.
822
- const helperPath = path.join(installDir, tokenAuth.helperRelPath);
823
- const serverDef = {
824
- type: 'http',
825
- url: serverConfig.url,
826
- headersHelper: `node "${helperPath}"`,
827
- };
828
- const installSuccess = await this.addMcpServerJson(installDir, serverName, serverDef);
829
-
830
- if (installSuccess) {
831
- results.installed.push(serverName);
832
- console.log(
833
- chalk.dim(
834
- ' Note: on first connect Claude Code will ask you to trust this workspace (the helper runs a local command). Accept it, then run /mcp.',
835
- ),
836
- );
837
- } else {
838
- results.failed.push(serverName);
839
- }
840
- }
841
- } else {
842
- console.log(
843
- chalk.red(`✗ Could not write ${tokenAuth.envVar} to .env: ${envResult.error}`),
844
- );
845
- results.failed.push(serverName);
846
- }
847
- } else {
848
- console.log(chalk.yellow('⚠️ No token provided — skipping GitHub MCP setup.'));
849
- console.log(
850
- chalk.cyan(
851
- ` Add ${tokenAuth.envVar} to .env later, then re-run the installer to register the GitHub MCP server.`,
852
- ),
853
- );
854
- results.skipped.push(serverName);
855
- }
856
- }
857
-
858
500
  // Process Other (custom) MCP servers if selected
859
501
  if (selectedMcpServers.includes('other')) {
860
502
  let addAnother = true;
@@ -1039,15 +681,7 @@ class DependencyManager {
1039
681
  console.log(chalk.yellow(' You can configure them manually later using:'));
1040
682
  for (const server of results.failed) {
1041
683
  const serverConfig = this.requiredMcpServers[server];
1042
- if (serverConfig && serverConfig.tokenAuth) {
1043
- // Token-auth servers (e.g. GitHub) are registered via .env + headersHelper,
1044
- // which is awkward to type by hand — point the user back at the installer.
1045
- console.log(
1046
- chalk.cyan(
1047
- ` Set ${serverConfig.tokenAuth.envVar} in .env, then re-run the installer to register ${server}.`,
1048
- ),
1049
- );
1050
- } else if (serverConfig) {
684
+ if (serverConfig) {
1051
685
  let command = `claude mcp add --transport ${serverConfig.transport} ${server} ${serverConfig.url}`;
1052
686
  if (serverConfig.envVars && Object.keys(serverConfig.envVars).length > 0) {
1053
687
  for (const [envVar] of Object.entries(serverConfig.envVars)) {