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,
|
resolveSubagentFiles,
|
||||||
} = require('./shared/module-injections');
|
} = require('./shared/module-injections');
|
||||||
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
||||||
|
const { detectGitRepo, generateBootstrapPrompt } = require('../utils/git-repo-detector');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Claude Code IDE setup handler
|
* Claude Code IDE setup handler
|
||||||
|
|
@ -154,6 +155,9 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
// Install BMAD Guide skill to user's Claude skills directory
|
// Install BMAD Guide skill to user's Claude skills directory
|
||||||
await this.installBmadGuideSkill();
|
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)
|
// Generate workflow commands from manifest (if it exists)
|
||||||
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||||
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
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
|
* 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