feat(installer): add --update-manifest-only option for manifest regeneration

Add new CLI option to update installation manifests without full reinstallation:

• Add --update-manifest-only flag to installer CLI
• Implement comprehensive manifest update logic in installer.js
• Scan existing installations and regenerate accurate manifests
• Preserve configuration while updating file hashes and metadata
• Add npm run update:manifest convenience script

Features:
- Validates existing .bmad-core installations
- Scans all files recursively and generates current state manifests
- Supports both core and expansion pack installations
- Compatible with all usage patterns (CLI, npm, npx)
- Fast execution with zero file disruption

Resolves manifest sync issues when installations drift from recorded state.
Eliminates need for full reinstallation when only manifest needs updating.
This commit is contained in:
Mainak Chhari 2025-09-16 18:31:08 +05:30
parent f09e282d72
commit 205531fe1b
No known key found for this signature in database
GPG Key ID: DC96D831FEA91CF6
3 changed files with 140 additions and 3 deletions

View File

@ -43,6 +43,7 @@
"release:patch": "gh workflow run \"Manual Release\" -f version_bump=patch",
"release:watch": "gh run watch",
"setup:hooks": "chmod +x tools/setup-hooks.sh && ./tools/setup-hooks.sh",
"update:manifest": "node tools/installer/bin/bmad.js install --update-manifest-only",
"validate": "node tools/cli.js validate",
"version:all": "node tools/bump-all-versions.js",
"version:all:major": "node tools/bump-all-versions.js major",

View File

@ -47,6 +47,7 @@ program
.option('-f, --full', 'Install complete BMad Method')
.option('-x, --expansion-only', 'Install only expansion packs (no bmad-core)')
.option('-d, --directory <path>', 'Installation directory')
.option('--update-manifest-only', 'Only rebuild/update the install-manifest.yaml file')
.option(
'-i, --ide <ide...>',
'Configure for specific IDE(s) - can specify multiple (cursor, claude-code, windsurf, trae, roo, kilo, cline, gemini, qwen-code, github-copilot, codex, codex-web, auggie-cli, iflow-cli, opencode, other)',
@ -57,7 +58,15 @@ program
)
.action(async (options) => {
try {
if (!options.full && !options.expansionOnly) {
if (options.updateManifestOnly) {
// Manifest-only update mode
const config = {
directory: options.directory || '.',
updateManifestOnly: true,
};
await installer.updateManifestOnly(config);
process.exit(0);
} else if (!options.full && !options.expansionOnly) {
// Interactive mode
const answers = await promptInstallation();
if (!answers._alreadyInstalled) {
@ -196,12 +205,12 @@ async function promptInstallation() {
// Display ASCII logo
console.log(
chalk.bold.cyan(`
`),
);

View File

@ -2008,6 +2008,133 @@ class Installer {
process.exit(code);
});
}
async updateManifestOnly(config) {
const spinner = ora('Updating installation manifest...').start();
try {
// Store the original CWD where npx was executed
const originalCwd = process.env.INIT_CWD || process.env.PWD || process.cwd();
// Resolve installation directory relative to where the user ran the command
let installDir = path.isAbsolute(config.directory)
? config.directory
: path.resolve(originalCwd, config.directory);
if (path.basename(installDir) === '.bmad-core') {
// If user points directly to .bmad-core, treat its parent as the project root
installDir = path.dirname(installDir);
}
// Check if installation directory exists
if (!(await fileManager.pathExists(installDir))) {
spinner.fail(`Installation directory does not exist: ${installDir}`);
throw new Error(`Directory not found: ${installDir}`);
}
// Check if .bmad-core exists (required for manifest update)
const bmadCorePath = path.join(installDir, '.bmad-core');
if (!(await fileManager.pathExists(bmadCorePath))) {
spinner.fail('No BMad installation found (missing .bmad-core directory)');
throw new Error('No existing BMad installation found. Use regular install command first.');
}
spinner.text = 'Scanning installed files...';
// Detect current installation state
const state = await this.detectInstallationState(installDir);
if (state.type !== 'v4_existing') {
spinner.fail('No valid BMad v4 installation found');
throw new Error('Cannot update manifest - no valid v4 installation detected');
}
let updatedManifests = [];
// Update core manifest if bmad-core exists
if (state.hasBmadCore) {
spinner.text = 'Updating core manifest...';
// Scan all files in .bmad-core directory
const coreFiles = await resourceLocator.findFiles('**/*', {
cwd: bmadCorePath,
nodir: true,
});
// Convert to relative paths from install directory
const relativeFiles = coreFiles.map((file) => `.bmad-core/${file}`);
// Create manifest config based on existing manifest or defaults
const existingManifest = state.manifest || {};
const manifestConfig = {
installType: existingManifest.install_type || 'full',
ides: existingManifest.ide_config || [],
expansionPacks: existingManifest.expansion_packs || [],
prdSharded: existingManifest.prd_sharded,
architectureSharded: existingManifest.architecture_sharded,
};
await fileManager.createManifest(installDir, manifestConfig, relativeFiles);
updatedManifests.push('Core manifest (.bmad-core/install-manifest.yaml)');
}
// Update expansion pack manifests
if (state.expansionPacks && Object.keys(state.expansionPacks).length > 0) {
spinner.text = 'Updating expansion pack manifests...';
for (const [packId, packInfo] of Object.entries(state.expansionPacks)) {
const packPath = path.join(installDir, `.${packId}`);
if (await fileManager.pathExists(packPath)) {
// Scan all files in expansion pack directory
const packFiles = await resourceLocator.findFiles('**/*', {
cwd: packPath,
nodir: true,
});
// Convert to relative paths from install directory
const relativePackFiles = packFiles.map((file) => `.${packId}/${file}`);
// Create expansion pack manifest config
const packManifestConfig = {
installType: 'expansion',
expansionPackVersion:
packInfo.manifest?.version ||
require(path.resolve(__dirname, '../../../package.json')).version,
};
await fileManager.createExpansionPackManifest(
installDir,
packId,
packManifestConfig,
relativePackFiles,
);
updatedManifests.push(`${packId} manifest (.${packId}/install-manifest.yaml)`);
}
}
}
spinner.succeed('Installation manifest updated successfully!');
// Show summary
console.log(chalk.green('\n✓ Manifest Update Complete'));
console.log(chalk.cyan(`📍 Installation directory: ${installDir}`));
console.log(chalk.cyan('📄 Updated manifests:'));
for (const manifest of updatedManifests) {
console.log(chalk.green(`${manifest}`));
}
if (updatedManifests.length === 0) {
console.log(chalk.yellow(' ⚠️ No manifests found to update'));
}
console.log(chalk.dim('\nManifests now reflect the current state of installed files.'));
} catch (error) {
spinner.fail('Manifest update failed');
throw error;
}
}
}
module.exports = new Installer();