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
This commit is contained in:
parent
11763ba343
commit
0efbea9136
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
Loading…
Reference in New Issue