Compare commits

..

5 Commits

Author SHA1 Message Date
NEE 3b8d5de9da
Merge a955c8611d into 9debc165aa 2026-05-01 11:54:42 +00:00
nick a955c8611d docs(installer): add JSDoc docstrings to badge.js
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-05-01 19:54:34 +08:00
nick a8666294cb fix(installer): address CodeRabbit review feedback on badge integration
- Derive BADGE_PATTERN from BADGE_URL constant (configurable)
- Fix git remote regex to support repo names with dots
- Add try/catch around badge injection so IO errors don't abort install
- Add try/catch around badge cleanup in uninstall (best-effort)
- Pass noBadge through quickUpdate flow

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-05-01 15:54:05 +08:00
nick 4d0b0fdcd1 feat(installer): add badge prompt and auto-create README if missing
- Interactive prompt asks "Add BMAD badge to your README?" (default yes)
- --no-badge skips the prompt and disables badge
- --yes accepts badge by default
- If user wants badge but no README exists, creates README.md with
  project name and badge

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-05-01 15:36:50 +08:00
nick 707de90238 feat(installer): auto-inject BMAD version badge into project README
Lightweight installer integration that adds a dynamic BMAD version badge
to the project README during `bmad install`. The badge is powered by the
existing bmad-badge.vercel.app service (from terryso/bmad-badge, issue #2174).

- New badge.js module: resolves git remote, finds README, injects/removes badge
- Badge is added after install, shown in the install summary
- `--no-badge` flag to opt out
- Badge is removed during `bmad uninstall` when modules are removed
- Idempotent: re-running install skips if badge already present

Closes #2174

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-05-01 13:45:25 +08:00
4 changed files with 18 additions and 54 deletions

View File

@ -109,29 +109,6 @@ module.exports = {
})); }));
} }
// Resolve owner/repo for badge (git remote → prompt fallback)
if (!config.noBadge) {
const badge = require('../core/badge');
let remote = badge.resolveGitRemote(config.directory);
if (!remote) {
const input = await prompts.text({
message: 'Enter your GitHub owner/repo for the badge (e.g., nick/my-project):',
placeholder: 'owner/repo',
validate: (v) => (!v || !v.includes('/') ? 'Format: owner/repo' : undefined),
});
if (input) {
const [owner, repo] = input.split('/');
remote = { owner, repo };
}
}
if (remote) {
config.badgeOwner = remote.owner;
config.badgeRepo = remote.repo;
} else {
config.noBadge = true;
}
}
// Handle cancel // Handle cancel
if (config.actionType === 'cancel') { if (config.actionType === 'cancel') {
await prompts.log.warn('Installation cancelled.'); await prompts.log.warn('Installation cancelled.');

View File

@ -3,8 +3,10 @@ const { execSync } = require('node:child_process');
const fs = require('../fs-native'); const fs = require('../fs-native');
const BADGE_URL = 'https://bmad-badge.vercel.app'; const BADGE_URL = 'https://bmad-badge.vercel.app';
const escapedBadgeUrl = BADGE_URL.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); const escapedBadgeUrl = BADGE_URL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const BADGE_PATTERN = new RegExp(`\\[!\\[BMAD\\]\\(${escapedBadgeUrl}/[^)]+\\)\\]\\(https://github\\.com/bmad-code-org/BMAD-METHOD\\)`); const BADGE_PATTERN = new RegExp(
`\\[!\\[BMAD\\]\\(${escapedBadgeUrl}/[^)]+\\)\\]\\(https://github\\.com/bmad-code-org/BMAD-METHOD\\)`,
);
const README_NAMES = ['README.md', 'readme.md', 'README', 'readme']; const README_NAMES = ['README.md', 'readme.md', 'README', 'readme'];
/** /**
@ -81,9 +83,9 @@ function injectBadge(content, owner, repo) {
// Find the first heading (# title) // Find the first heading (# title)
let headingEnd = 0; let headingEnd = 0;
for (const [i, line] of lines.entries()) { for (let i = 0; i < lines.length; i++) {
headingEnd = i + 1; headingEnd = i + 1;
if (line.startsWith('#')) break; if (lines[i].startsWith('#')) break;
} }
// Check if there are existing badges right after the heading // Check if there are existing badges right after the heading

View File

@ -15,9 +15,6 @@ class Config {
quickUpdate, quickUpdate,
channelOptions, channelOptions,
setOverrides, setOverrides,
noBadge,
badgeOwner,
badgeRepo,
}) { }) {
this.directory = directory; this.directory = directory;
this.modules = Object.freeze([...modules]); this.modules = Object.freeze([...modules]);
@ -35,9 +32,6 @@ class Config {
// Intentionally NOT integrated with the prompt/template/schema flow; see // Intentionally NOT integrated with the prompt/template/schema flow; see
// `tools/installer/set-overrides.js` for the rationale and tradeoffs. // `tools/installer/set-overrides.js` for the rationale and tradeoffs.
this.setOverrides = setOverrides || {}; this.setOverrides = setOverrides || {};
this.noBadge = noBadge || false;
this.badgeOwner = badgeOwner || null;
this.badgeRepo = badgeRepo || null;
Object.freeze(this); Object.freeze(this);
} }
@ -64,9 +58,6 @@ class Config {
quickUpdate: userInput._quickUpdate || false, quickUpdate: userInput._quickUpdate || false,
channelOptions: userInput.channelOptions || null, channelOptions: userInput.channelOptions || null,
setOverrides: userInput.setOverrides || {}, setOverrides: userInput.setOverrides || {},
noBadge: userInput.noBadge || false,
badgeOwner: userInput.badgeOwner || null,
badgeRepo: userInput.badgeRepo || null,
}); });
} }

View File

@ -106,7 +106,7 @@ class Installer {
// Inject BMAD badge into README if applicable // Inject BMAD badge into README if applicable
if (!config.noBadge) { if (!config.noBadge) {
try { try {
await this._injectBadgeIfNeeded(paths.projectRoot, addResult, config); await this._injectBadgeIfNeeded(paths.projectRoot, addResult);
} catch (error) { } catch (error) {
addResult('Badge', 'warn', `skipped: ${error.message}`); addResult('Badge', 'warn', `skipped: ${error.message}`);
} }
@ -1048,26 +1048,26 @@ class Installer {
} }
/** /**
* Inject BMAD version badge into project README. * Inject BMAD version badge into project README if applicable.
* Uses owner/repo from config (resolved in UI layer). * Skipped when --no-badge is set, when no git remote is found,
* or when no README exists.
* @param {string} projectDir - Project root directory * @param {string} projectDir - Project root directory
* @param {Function} addResult - Callback to record results * @param {Function} addResult - Callback to record results
* @param {Object} config - Installation config with badgeOwner/badgeRepo
*/ */
async _injectBadgeIfNeeded(projectDir, addResult, config) { async _injectBadgeIfNeeded(projectDir, addResult) {
const badge = require('../core/badge'); const badge = require('../core/badge');
const owner = config.badgeOwner; const remote = badge.resolveGitRemote(projectDir);
const repo = config.badgeRepo; if (!remote) {
if (!owner || !repo) { addResult('Badge', 'warn', 'no git remote found');
addResult('Badge', 'warn', 'no owner/repo provided');
return; return;
} }
const readmePath = await badge.findReadme(projectDir); const readmePath = await badge.findReadme(projectDir);
if (!readmePath) { if (!readmePath) {
// No README — create one with the badge
const projectName = path.basename(projectDir); const projectName = path.basename(projectDir);
const content = badge.createReadmeWithBadge(owner, repo, projectName); const content = badge.createReadmeWithBadge(remote.owner, remote.repo, projectName);
const newReadmePath = path.join(projectDir, 'README.md'); const newReadmePath = path.join(projectDir, 'README.md');
await fs.writeFile(newReadmePath, content, 'utf8'); await fs.writeFile(newReadmePath, content, 'utf8');
addResult('Badge', 'ok', 'created README.md with badge'); addResult('Badge', 'ok', 'created README.md with badge');
@ -1076,15 +1076,11 @@ class Installer {
const content = await fs.readFile(readmePath, 'utf8'); const content = await fs.readFile(readmePath, 'utf8');
if (badge.hasBadge(content)) { if (badge.hasBadge(content)) {
// Update badge if owner/repo changed addResult('Badge', 'ok', 'already present');
const updated = badge.removeBadge(content);
const injected = badge.injectBadge(updated, owner, repo);
await fs.writeFile(readmePath, injected, 'utf8');
addResult('Badge', 'ok', `updated in ${path.basename(readmePath)}`);
return; return;
} }
const updated = badge.injectBadge(content, owner, repo); const updated = badge.injectBadge(content, remote.owner, remote.repo);
await fs.writeFile(readmePath, updated, 'utf8'); await fs.writeFile(readmePath, updated, 'utf8');
addResult('Badge', 'ok', `added to ${path.basename(readmePath)}`); addResult('Badge', 'ok', `added to ${path.basename(readmePath)}`);
} }
@ -1346,8 +1342,6 @@ class Installer {
modules: modulesToUpdate, modules: modulesToUpdate,
ides: configuredIdes, ides: configuredIdes,
noBadge: config.noBadge, noBadge: config.noBadge,
badgeOwner: config.badgeOwner,
badgeRepo: config.badgeRepo,
coreConfig: quickModules.collectedConfig.core, coreConfig: quickModules.collectedConfig.core,
moduleConfigs: quickModules.collectedConfig, moduleConfigs: quickModules.collectedConfig,
// Forward `--set` overrides so the post-install patch step // Forward `--set` overrides so the post-install patch step