From 0efbea913669bad23419c2f817f46f952da8eb16 Mon Sep 17 00:00:00 2001 From: Jonah Schulte Date: Thu, 8 Jan 2026 20:51:55 -0500 Subject: [PATCH] feat(installer): auto-detect git repo and pre-fill bootstrap prompts During installation, the Claude Code installer now: 1. Detects the git remote URL (supports GitHub.com and Enterprise) 2. Generates a pre-filled bootstrap prompt at _bmad/claude-desktop-bootstrap.md 3. Replaces YOUR-ORG/YOUR-REPO placeholders in all installed docs This enables seamless Claude Desktop usage - users just copy the generated bootstrap prompt which has their repo details already filled in. Supports all git URL formats: - SSH: git@github.com:owner/repo.git - SSH Enterprise: git@ghe.company.com:owner/repo.git - HTTPS: https://github.com/owner/repo.git - HTTP Enterprise: http://ghe.company.com/owner/repo --- tools/cli/installers/lib/ide/claude-code.js | 118 +++++++++++++ .../installers/lib/utils/git-repo-detector.js | 159 ++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 tools/cli/installers/lib/utils/git-repo-detector.js diff --git a/tools/cli/installers/lib/ide/claude-code.js b/tools/cli/installers/lib/ide/claude-code.js index 41d0886f..702bb810 100644 --- a/tools/cli/installers/lib/ide/claude-code.js +++ b/tools/cli/installers/lib/ide/claude-code.js @@ -13,6 +13,7 @@ const { resolveSubagentFiles, } = require('./shared/module-injections'); const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts'); +const { detectGitRepo, generateBootstrapPrompt } = require('../utils/git-repo-detector'); /** * Claude Code IDE setup handler @@ -154,6 +155,9 @@ class ClaudeCodeSetup extends BaseIdeSetup { // Install BMAD Guide skill to user's Claude skills directory await this.installBmadGuideSkill(); + // Detect git repo and generate bootstrap prompt + replace placeholders + await this.setupGitHubIntegration(projectDir, bmadDir); + // Generate workflow commands from manifest (if it exists) const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir); @@ -230,6 +234,120 @@ class ClaudeCodeSetup extends BaseIdeSetup { } } + /** + * Detect git repo and set up GitHub integration + * - Generates a pre-filled bootstrap prompt + * - Replaces YOUR-ORG/YOUR-REPO placeholders in documentation + * @param {string} projectDir - Project directory + * @param {string} bmadDir - BMAD installation directory + */ + async setupGitHubIntegration(projectDir, bmadDir) { + try { + const repoInfo = detectGitRepo(projectDir); + + if (!repoInfo) { + console.log(chalk.dim(' Skipping GitHub integration (not a git repo or no remote configured)')); + return; + } + + console.log(chalk.cyan(` Detected GitHub repo: ${repoInfo.owner}/${repoInfo.repo}`)); + if (repoInfo.isEnterprise) { + console.log(chalk.dim(` Enterprise host: ${repoInfo.host}`)); + } + + // Generate bootstrap prompt file + const bootstrapContent = generateBootstrapPrompt(repoInfo); + const bootstrapPath = path.join(bmadDir, 'claude-desktop-bootstrap.md'); + await fs.writeFile(bootstrapPath, bootstrapContent, 'utf8'); + console.log(chalk.green(' ✓ Generated Claude Desktop bootstrap prompt')); + console.log(chalk.dim(` Location: ${path.relative(projectDir, bootstrapPath)}`)); + + // Replace placeholders in documentation files + await this.replaceRepoPlaceholders(bmadDir, repoInfo); + } catch (error) { + console.log(chalk.yellow(` ⚠ Warning: GitHub integration setup failed: ${error.message}`)); + } + } + + /** + * Replace YOUR-ORG/YOUR-REPO placeholders in documentation files + * @param {string} bmadDir - BMAD installation directory + * @param {Object} repoInfo - Repository info from detectGitRepo + */ + async replaceRepoPlaceholders(bmadDir, repoInfo) { + // Patterns to replace + const replacements = [ + { pattern: /YOUR-ORG\/YOUR-REPO/g, replacement: `${repoInfo.owner}/${repoInfo.repo}` }, + { pattern: /github\.com\/YOUR-ORG\/YOUR-REPO/g, replacement: `${repoInfo.host}/${repoInfo.owner}/${repoInfo.repo}` }, + { pattern: /YOUR-ORG/g, replacement: repoInfo.owner }, + { pattern: /YOUR-REPO/g, replacement: repoInfo.repo }, + ]; + + // Find all markdown and yaml files in bmad directory + const files = await this.findFilesRecursive(bmadDir, ['.md', '.yaml', '.yml']); + let replacedCount = 0; + + for (const filePath of files) { + try { + let content = await fs.readFile(filePath, 'utf8'); + let modified = false; + + for (const { pattern, replacement } of replacements) { + if (pattern.test(content)) { + content = content.replace(pattern, replacement); + modified = true; + } + } + + if (modified) { + await fs.writeFile(filePath, content, 'utf8'); + replacedCount++; + } + } catch { + // Skip files that can't be read/written + } + } + + if (replacedCount > 0) { + console.log(chalk.dim(` Updated ${replacedCount} files with repo details`)); + } + } + + /** + * Recursively find files with specific extensions + * @param {string} dir - Directory to search + * @param {string[]} extensions - File extensions to match + * @returns {string[]} Array of file paths + */ + async findFilesRecursive(dir, extensions) { + const files = []; + + try { + const entries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + // Skip node_modules and hidden directories + if (!entry.name.startsWith('.') && entry.name !== 'node_modules') { + const subFiles = await this.findFilesRecursive(fullPath, extensions); + files.push(...subFiles); + } + } else if (entry.isFile()) { + const ext = path.extname(entry.name).toLowerCase(); + if (extensions.includes(ext)) { + files.push(fullPath); + } + } + } + } catch { + // Skip directories that can't be read + } + + return files; + } + /** * Read and process file content */ diff --git a/tools/cli/installers/lib/utils/git-repo-detector.js b/tools/cli/installers/lib/utils/git-repo-detector.js new file mode 100644 index 00000000..acc9182d --- /dev/null +++ b/tools/cli/installers/lib/utils/git-repo-detector.js @@ -0,0 +1,159 @@ +/** + * Git Repository Detector + * Detects and parses git remote URLs to extract repository information + */ + +const { execSync } = require('node:child_process'); +const path = require('node:path'); + +/** + * Parse a git remote URL into its components + * Handles SSH, HTTPS, and GitHub Enterprise formats + * + * @param {string} remoteUrl - The git remote URL + * @returns {Object|null} Parsed repo info or null if unparseable + */ +function parseGitRemoteUrl(remoteUrl) { + if (!remoteUrl) return null; + + // Clean up the URL + const url = remoteUrl.trim(); + + // SSH format: git@github.com:owner/repo.git + const sshMatch = url.match(/^git@([^:]+):([^/]+)\/(.+?)(?:\.git)?$/); + if (sshMatch) { + return { + host: sshMatch[1], + owner: sshMatch[2], + repo: sshMatch[3], + fullUrl: `https://${sshMatch[1]}/${sshMatch[2]}/${sshMatch[3]}`, + isEnterprise: sshMatch[1] !== 'github.com', + }; + } + + // HTTPS format: https://github.com/owner/repo.git + // Also handles: http://ghe.company.com/owner/repo + const httpsMatch = url.match(/^https?:\/\/([^/]+)\/([^/]+)\/(.+?)(?:\.git)?$/); + if (httpsMatch) { + return { + host: httpsMatch[1], + owner: httpsMatch[2], + repo: httpsMatch[3], + fullUrl: `https://${httpsMatch[1]}/${httpsMatch[2]}/${httpsMatch[3]}`, + isEnterprise: httpsMatch[1] !== 'github.com', + }; + } + + return null; +} + +/** + * Detect the git remote URL for a project directory + * + * @param {string} projectDir - The project directory path + * @returns {Object|null} Parsed repo info or null if not a git repo + */ +function detectGitRepo(projectDir) { + try { + const remoteUrl = execSync('git config --get remote.origin.url', { + cwd: projectDir, + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + }); + + return parseGitRemoteUrl(remoteUrl); + } catch { + // Not a git repo or no remote configured + return null; + } +} + +/** + * Generate the bootstrap prompt content with repo details filled in + * + * @param {Object} repoInfo - Repository info from detectGitRepo + * @param {string} agentPath - Path to the agent file in the repo + * @returns {string} The bootstrap prompt content + */ +function generateBootstrapPrompt(repoInfo, agentPath = 'src/modules/bmm/agents/po.agent.yaml') { + const repoRef = repoInfo.isEnterprise + ? `${repoInfo.host}/${repoInfo.owner}/${repoInfo.repo}` + : `${repoInfo.owner}/${repoInfo.repo}`; + + return `# BMAD Product Owner - Claude Desktop Bootstrap + +This prompt is pre-configured for your repository. + +--- + +## Quick Start + +Copy and paste this into Claude Desktop: + +\`\`\` +Load the Product Owner agent from ${repoInfo.fullUrl} +(path: ${agentPath}) and enter PO mode. +Show me what needs my attention. +\`\`\` + +--- + +## Full Version + +\`\`\` +Fetch and embody the BMAD Product Owner agent. + +1. Read the agent definition from GitHub: + - Host: ${repoInfo.host} + - Repository: ${repoInfo.owner}/${repoInfo.repo} + - Path: ${agentPath} + +2. After reading, fully embody this agent: + - Adopt the persona (name, role, communication style) + - Internalize all principles + - Make the menu commands available + +3. Introduce yourself and show the available commands. + +4. Then check: what PRDs or stories need my attention? + +Use GitHub MCP tools (mcp__github__*) for all GitHub operations. +\`\`\` + +--- + +## For Stakeholders + +\`\`\` +I'm a stakeholder who needs to review PRDs and give feedback. + +Load the Product Owner agent from ${repoInfo.fullUrl} +(path: ${agentPath}) + +Then show me: +1. What PRDs need my feedback +2. What PRDs need my sign-off + +I'll mainly use: MT (my tasks), SF (submit feedback), SO (sign off) +\`\`\` + +--- + +## Repository Details + +| Field | Value | +|-------|-------| +| Host | ${repoInfo.host} | +| Owner | ${repoInfo.owner} | +| Repo | ${repoInfo.repo} | +| Enterprise | ${repoInfo.isEnterprise ? 'Yes' : 'No'} | + +Generated during BMAD installation. +`; +} + +module.exports = { + parseGitRemoteUrl, + detectGitRepo, + generateBootstrapPrompt, +};