feat: massive installer improvement update
This commit is contained in:
107
tools/bump-all-versions.js
Executable file
107
tools/bump-all-versions.js
Executable file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const bumpType = args[0] || 'minor'; // default to minor
|
||||
|
||||
if (!['major', 'minor', 'patch'].includes(bumpType)) {
|
||||
console.log('Usage: node bump-all-versions.js [major|minor|patch]');
|
||||
console.log('Default: minor');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function bumpVersion(currentVersion, type) {
|
||||
const [major, minor, patch] = currentVersion.split('.').map(Number);
|
||||
|
||||
switch (type) {
|
||||
case 'major':
|
||||
return `${major + 1}.0.0`;
|
||||
case 'minor':
|
||||
return `${major}.${minor + 1}.0`;
|
||||
case 'patch':
|
||||
return `${major}.${minor}.${patch + 1}`;
|
||||
default:
|
||||
return currentVersion;
|
||||
}
|
||||
}
|
||||
|
||||
async function bumpAllVersions() {
|
||||
const updatedItems = [];
|
||||
|
||||
// First, bump the core version
|
||||
const coreConfigPath = path.join(__dirname, '..', 'bmad-core', 'core-config.yml');
|
||||
try {
|
||||
const coreConfigContent = fs.readFileSync(coreConfigPath, 'utf8');
|
||||
const coreConfig = yaml.load(coreConfigContent);
|
||||
const oldCoreVersion = coreConfig.version || '1.0.0';
|
||||
const newCoreVersion = bumpVersion(oldCoreVersion, bumpType);
|
||||
|
||||
coreConfig.version = newCoreVersion;
|
||||
|
||||
const updatedCoreYaml = yaml.dump(coreConfig, { indent: 2 });
|
||||
fs.writeFileSync(coreConfigPath, updatedCoreYaml);
|
||||
|
||||
updatedItems.push({ type: 'core', name: 'BMad Core', oldVersion: oldCoreVersion, newVersion: newCoreVersion });
|
||||
console.log(`✓ BMad Core: ${oldCoreVersion} → ${newCoreVersion}`);
|
||||
} catch (error) {
|
||||
console.error(`✗ Failed to update BMad Core: ${error.message}`);
|
||||
}
|
||||
|
||||
// Then, bump all expansion packs
|
||||
const expansionPacksDir = path.join(__dirname, '..', 'expansion-packs');
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(expansionPacksDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'README.md') {
|
||||
const packId = entry.name;
|
||||
const configPath = path.join(expansionPacksDir, packId, 'config.yml');
|
||||
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||
const config = yaml.load(configContent);
|
||||
const oldVersion = config.version || '1.0.0';
|
||||
const newVersion = bumpVersion(oldVersion, bumpType);
|
||||
|
||||
config.version = newVersion;
|
||||
|
||||
const updatedYaml = yaml.dump(config, { indent: 2 });
|
||||
fs.writeFileSync(configPath, updatedYaml);
|
||||
|
||||
updatedItems.push({ type: 'expansion', name: packId, oldVersion, newVersion });
|
||||
console.log(`✓ ${packId}: ${oldVersion} → ${newVersion}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`✗ Failed to update ${packId}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedItems.length > 0) {
|
||||
const coreCount = updatedItems.filter(i => i.type === 'core').length;
|
||||
const expansionCount = updatedItems.filter(i => i.type === 'expansion').length;
|
||||
|
||||
console.log(`\n✓ Successfully bumped ${updatedItems.length} item(s) with ${bumpType} version bump`);
|
||||
if (coreCount > 0) console.log(` - ${coreCount} core`);
|
||||
if (expansionCount > 0) console.log(` - ${expansionCount} expansion pack(s)`);
|
||||
|
||||
console.log('\nNext steps:');
|
||||
console.log('1. Test the changes');
|
||||
console.log('2. Commit: git add -A && git commit -m "chore: bump all versions (' + bumpType + ')"');
|
||||
} else {
|
||||
console.log('No items found to update');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error reading expansion packs directory:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
bumpAllVersions();
|
||||
57
tools/bump-core-version.js
Normal file
57
tools/bump-core-version.js
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const bumpType = args[0] || 'minor'; // default to minor
|
||||
|
||||
if (!['major', 'minor', 'patch'].includes(bumpType)) {
|
||||
console.log('Usage: node bump-core-version.js [major|minor|patch]');
|
||||
console.log('Default: minor');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function bumpVersion(currentVersion, type) {
|
||||
const [major, minor, patch] = currentVersion.split('.').map(Number);
|
||||
|
||||
switch (type) {
|
||||
case 'major':
|
||||
return `${major + 1}.0.0`;
|
||||
case 'minor':
|
||||
return `${major}.${minor + 1}.0`;
|
||||
case 'patch':
|
||||
return `${major}.${minor}.${patch + 1}`;
|
||||
default:
|
||||
return currentVersion;
|
||||
}
|
||||
}
|
||||
|
||||
async function bumpCoreVersion() {
|
||||
try {
|
||||
const coreConfigPath = path.join(__dirname, '..', 'bmad-core', 'core-config.yml');
|
||||
|
||||
const coreConfigContent = fs.readFileSync(coreConfigPath, 'utf8');
|
||||
const coreConfig = yaml.load(coreConfigContent);
|
||||
const oldVersion = coreConfig.version || '1.0.0';
|
||||
const newVersion = bumpVersion(oldVersion, bumpType);
|
||||
|
||||
coreConfig.version = newVersion;
|
||||
|
||||
const updatedYaml = yaml.dump(coreConfig, { indent: 2 });
|
||||
fs.writeFileSync(coreConfigPath, updatedYaml);
|
||||
|
||||
console.log(`✓ BMad Core: ${oldVersion} → ${newVersion}`);
|
||||
console.log(`\n✓ Successfully bumped BMad Core with ${bumpType} version bump`);
|
||||
console.log('\nNext steps:');
|
||||
console.log('1. Test the changes');
|
||||
console.log('2. Commit: git add -A && git commit -m "chore: bump core version (' + bumpType + ')"');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating core version:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
bumpCoreVersion();
|
||||
78
tools/bump-expansion-version.js
Normal file
78
tools/bump-expansion-version.js
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 1 || args.length > 2) {
|
||||
console.log('Usage: node bump-expansion-version.js <expansion-pack-id> [major|minor|patch]');
|
||||
console.log('Default: minor');
|
||||
console.log('Example: node bump-expansion-version.js bmad-creator-tools patch');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const packId = args[0];
|
||||
const bumpType = args[1] || 'minor'; // default to minor
|
||||
|
||||
if (!['major', 'minor', 'patch'].includes(bumpType)) {
|
||||
console.error('Error: Bump type must be major, minor, or patch');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function bumpVersion(currentVersion, type) {
|
||||
const [major, minor, patch] = currentVersion.split('.').map(Number);
|
||||
|
||||
switch (type) {
|
||||
case 'major':
|
||||
return `${major + 1}.0.0`;
|
||||
case 'minor':
|
||||
return `${major}.${minor + 1}.0`;
|
||||
case 'patch':
|
||||
return `${major}.${minor}.${patch + 1}`;
|
||||
default:
|
||||
return currentVersion;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateVersion() {
|
||||
try {
|
||||
const configPath = path.join(__dirname, '..', 'expansion-packs', packId, 'config.yml');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
console.error(`Error: Expansion pack '${packId}' not found`);
|
||||
console.log('\nAvailable expansion packs:');
|
||||
const expansionPacksDir = path.join(__dirname, '..', 'expansion-packs');
|
||||
const entries = fs.readdirSync(expansionPacksDir, { withFileTypes: true });
|
||||
entries.forEach(entry => {
|
||||
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
||||
console.log(` - ${entry.name}`);
|
||||
}
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||
const config = yaml.load(configContent);
|
||||
const oldVersion = config.version || '1.0.0';
|
||||
const newVersion = bumpVersion(oldVersion, bumpType);
|
||||
|
||||
config.version = newVersion;
|
||||
|
||||
const updatedYaml = yaml.dump(config, { indent: 2 });
|
||||
fs.writeFileSync(configPath, updatedYaml);
|
||||
|
||||
console.log(`✓ ${packId}: ${oldVersion} → ${newVersion}`);
|
||||
console.log(`\n✓ Successfully bumped ${packId} with ${bumpType} version bump`);
|
||||
console.log('\nNext steps:');
|
||||
console.log('1. Test the changes');
|
||||
console.log('2. Commit: git add -A && git commit -m "chore: bump ' + packId + ' version (' + bumpType + ')"');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating version:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
updateVersion();
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
const { program } = require('commander');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
// Dynamic imports for ES modules
|
||||
let chalk, inquirer;
|
||||
@@ -45,17 +47,15 @@ program
|
||||
program
|
||||
.command('install')
|
||||
.description('Install BMAD Method agents and tools')
|
||||
.option('-f, --full', 'Install complete .bmad-core folder')
|
||||
.option('-a, --agent <agent>', 'Install specific agent with dependencies')
|
||||
.option('-t, --team <team>', 'Install specific team with required agents and dependencies')
|
||||
.option('-f, --full', 'Install complete BMAD Method')
|
||||
.option('-x, --expansion-only', 'Install only expansion packs (no bmad-core)')
|
||||
.option('-d, --directory <path>', 'Installation directory (default: .bmad-core)')
|
||||
.option('-d, --directory <path>', 'Installation directory')
|
||||
.option('-i, --ide <ide...>', 'Configure for specific IDE(s) - can specify multiple (cursor, claude-code, windsurf, roo, cline, gemini, other)')
|
||||
.option('-e, --expansion-packs <packs...>', 'Install specific expansion packs (can specify multiple)')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
await initializeModules();
|
||||
if (!options.full && !options.agent && !options.team && !options.expansionOnly) {
|
||||
if (!options.full && !options.expansionOnly) {
|
||||
// Interactive mode
|
||||
const answers = await promptInstallation();
|
||||
if (!answers._alreadyInstalled) {
|
||||
@@ -64,15 +64,11 @@ program
|
||||
} else {
|
||||
// Direct mode
|
||||
let installType = 'full';
|
||||
if (options.agent) installType = 'single-agent';
|
||||
else if (options.team) installType = 'team';
|
||||
else if (options.expansionOnly) installType = 'expansion-only';
|
||||
if (options.expansionOnly) installType = 'expansion-only';
|
||||
|
||||
const config = {
|
||||
installType,
|
||||
agent: options.agent,
|
||||
team: options.team,
|
||||
directory: options.directory || '.bmad-core',
|
||||
directory: options.directory || '.',
|
||||
ides: (options.ide || []).filter(ide => ide !== 'other'),
|
||||
expansionPacks: options.expansionPacks || []
|
||||
};
|
||||
@@ -100,19 +96,6 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.description('List available agents')
|
||||
.action(async () => {
|
||||
try {
|
||||
await installer.listAgents();
|
||||
} catch (error) {
|
||||
if (!chalk) await initializeModules();
|
||||
console.error(chalk.red('Error:'), error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('list:expansions')
|
||||
.description('List available expansion packs')
|
||||
@@ -145,7 +128,7 @@ async function promptInstallation() {
|
||||
|
||||
const answers = {};
|
||||
|
||||
// Ask for installation directory
|
||||
// Ask for installation directory first
|
||||
const { directory } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
@@ -161,147 +144,85 @@ async function promptInstallation() {
|
||||
]);
|
||||
answers.directory = directory;
|
||||
|
||||
// Check if this is an existing v4 installation
|
||||
const installDir = path.resolve(answers.directory);
|
||||
// Detect existing installations
|
||||
const installDir = path.resolve(directory);
|
||||
const state = await installer.detectInstallationState(installDir);
|
||||
|
||||
|
||||
// Check for existing expansion packs
|
||||
const existingExpansionPacks = state.expansionPacks || {};
|
||||
|
||||
// Get available expansion packs
|
||||
const availableExpansionPacks = await installer.getAvailableExpansionPacks();
|
||||
|
||||
// Build choices list
|
||||
const choices = [];
|
||||
|
||||
// Load core config to get short-title
|
||||
const coreConfigPath = path.join(__dirname, '..', '..', '..', 'bmad-core', 'core-config.yml');
|
||||
const coreConfig = yaml.load(await fs.readFile(coreConfigPath, 'utf8'));
|
||||
const coreShortTitle = coreConfig['short-title'] || 'BMad Agile Core System';
|
||||
|
||||
// Add BMAD core option
|
||||
let bmadOptionText;
|
||||
if (state.type === 'v4_existing') {
|
||||
console.log(chalk.yellow('\n🔍 Found existing BMAD v4 installation'));
|
||||
console.log(` Directory: ${installDir}`);
|
||||
console.log(` Version: ${state.manifest?.version || 'Unknown'}`);
|
||||
console.log(` Installed: ${state.manifest?.installed_at ? new Date(state.manifest.installed_at).toLocaleDateString() : 'Unknown'}`);
|
||||
|
||||
const { shouldUpdate } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'shouldUpdate',
|
||||
message: 'Would you like to update your existing BMAD v4 installation?',
|
||||
default: true
|
||||
}
|
||||
]);
|
||||
|
||||
if (shouldUpdate) {
|
||||
// Skip other prompts and go directly to update
|
||||
answers.installType = 'update';
|
||||
answers._alreadyInstalled = true; // Flag to prevent double installation
|
||||
await installer.install(answers);
|
||||
return answers; // Return the answers object
|
||||
}
|
||||
// If user doesn't want to update, continue with normal flow
|
||||
const currentVersion = state.manifest?.version || 'unknown';
|
||||
const newVersion = coreConfig.version || 'unknown'; // Use version from core-config.yml
|
||||
const versionInfo = currentVersion === newVersion
|
||||
? `(v${currentVersion} - reinstall)`
|
||||
: `(v${currentVersion} → v${newVersion})`;
|
||||
bmadOptionText = `Update ${coreShortTitle} ${versionInfo} .bmad-core`;
|
||||
} else {
|
||||
bmadOptionText = `Install ${coreShortTitle} (v${coreConfig.version || version}) .bmad-core`;
|
||||
}
|
||||
|
||||
// Ask for installation type
|
||||
const { installType } = await inquirer.prompt([
|
||||
|
||||
choices.push({
|
||||
name: bmadOptionText,
|
||||
value: 'bmad-core',
|
||||
checked: true
|
||||
});
|
||||
|
||||
// Add expansion pack options
|
||||
for (const pack of availableExpansionPacks) {
|
||||
const existing = existingExpansionPacks[pack.id];
|
||||
let packOptionText;
|
||||
|
||||
if (existing) {
|
||||
const currentVersion = existing.manifest?.version || 'unknown';
|
||||
const newVersion = pack.version;
|
||||
const versionInfo = currentVersion === newVersion
|
||||
? `(v${currentVersion} - reinstall)`
|
||||
: `(v${currentVersion} → v${newVersion})`;
|
||||
packOptionText = `Update ${pack.description} ${versionInfo} .${pack.id}`;
|
||||
} else {
|
||||
packOptionText = `Install ${pack.description} (v${pack.version}) .${pack.id}`;
|
||||
}
|
||||
|
||||
choices.push({
|
||||
name: packOptionText,
|
||||
value: pack.id,
|
||||
checked: false
|
||||
});
|
||||
}
|
||||
|
||||
// Ask what to install
|
||||
const { selectedItems } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'installType',
|
||||
message: 'How would you like to install BMAD?',
|
||||
choices: [
|
||||
{
|
||||
name: 'Complete installation (recommended) - All agents and tools',
|
||||
value: 'full'
|
||||
},
|
||||
{
|
||||
name: 'Team installation - Install a specific team with required agents',
|
||||
value: 'team'
|
||||
},
|
||||
{
|
||||
name: 'Single agent - Choose one agent to install',
|
||||
value: 'single-agent'
|
||||
},
|
||||
{
|
||||
name: 'Expansion packs only - Install expansion packs without bmad-core',
|
||||
value: 'expansion-only'
|
||||
type: 'checkbox',
|
||||
name: 'selectedItems',
|
||||
message: 'Select what to install/update (use space to select, enter to continue):',
|
||||
choices: choices,
|
||||
validate: (selected) => {
|
||||
if (selected.length === 0) {
|
||||
return 'Please select at least one item to install';
|
||||
}
|
||||
]
|
||||
return true;
|
||||
}
|
||||
}
|
||||
]);
|
||||
answers.installType = installType;
|
||||
|
||||
// If single agent, ask which one
|
||||
if (installType === 'single-agent') {
|
||||
const agents = await installer.getAvailableAgents();
|
||||
const { agent } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'agent',
|
||||
message: 'Select an agent to install:',
|
||||
choices: agents.map(a => ({
|
||||
name: `${a.id} - ${a.name} (${a.description})`,
|
||||
value: a.id
|
||||
}))
|
||||
}
|
||||
]);
|
||||
answers.agent = agent;
|
||||
}
|
||||
|
||||
// If team installation, ask which team
|
||||
if (installType === 'team') {
|
||||
const teams = await installer.getAvailableTeams();
|
||||
const { team } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'team',
|
||||
message: 'Select a team to install in your IDE project folder:',
|
||||
choices: teams.map(t => ({
|
||||
name: `${t.icon || '📋'} ${t.name}: ${t.description}`,
|
||||
value: t.id
|
||||
}))
|
||||
}
|
||||
]);
|
||||
answers.team = team;
|
||||
}
|
||||
|
||||
// Ask for expansion pack selection
|
||||
if (installType === 'full' || installType === 'team' || installType === 'expansion-only') {
|
||||
try {
|
||||
const availableExpansionPacks = await installer.getAvailableExpansionPacks();
|
||||
|
||||
if (availableExpansionPacks.length > 0) {
|
||||
let choices;
|
||||
let message;
|
||||
|
||||
if (installType === 'expansion-only') {
|
||||
message = 'Select expansion packs to install (required):'
|
||||
choices = availableExpansionPacks.map(pack => ({
|
||||
name: `${pack.name} - ${pack.description}`,
|
||||
value: pack.id
|
||||
}));
|
||||
} else {
|
||||
message = 'Select expansion packs to install (press Enter to skip, or check any to install):';
|
||||
choices = availableExpansionPacks.map(pack => ({
|
||||
name: `${pack.name} - ${pack.description}`,
|
||||
value: pack.id
|
||||
}));
|
||||
}
|
||||
|
||||
const { expansionPacks } = await inquirer.prompt([
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'expansionPacks',
|
||||
message,
|
||||
choices,
|
||||
validate: installType === 'expansion-only' ? (answer) => {
|
||||
if (answer.length < 1) {
|
||||
return 'You must select at least one expansion pack for expansion-only installation.';
|
||||
}
|
||||
return true;
|
||||
} : undefined
|
||||
}
|
||||
]);
|
||||
|
||||
// Use selected expansion packs directly
|
||||
answers.expansionPacks = expansionPacks;
|
||||
} else {
|
||||
answers.expansionPacks = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(chalk.yellow('Warning: Could not load expansion packs. Continuing without them.'));
|
||||
answers.expansionPacks = [];
|
||||
}
|
||||
} else {
|
||||
answers.expansionPacks = [];
|
||||
}
|
||||
|
||||
// Process selections
|
||||
answers.installType = selectedItems.includes('bmad-core') ? 'full' : 'expansion-only';
|
||||
answers.expansionPacks = selectedItems.filter(item => item !== 'bmad-core');
|
||||
|
||||
// Ask for IDE configuration
|
||||
const { ides } = await inquirer.prompt([
|
||||
|
||||
@@ -83,12 +83,22 @@ class FileManager {
|
||||
this.manifestFile
|
||||
);
|
||||
|
||||
// Read version from core-config.yml
|
||||
const coreConfigPath = path.join(__dirname, "../../../bmad-core/core-config.yml");
|
||||
let coreVersion = "unknown";
|
||||
try {
|
||||
const coreConfigContent = await fs.readFile(coreConfigPath, "utf8");
|
||||
const coreConfig = yaml.load(coreConfigContent);
|
||||
coreVersion = coreConfig.version || "unknown";
|
||||
} catch (error) {
|
||||
console.warn("Could not read version from core-config.yml, using 'unknown'");
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
version: require("../../../package.json").version,
|
||||
version: coreVersion,
|
||||
installed_at: new Date().toISOString(),
|
||||
install_type: config.installType,
|
||||
agent: config.agent || null,
|
||||
ide_setup: config.ide || null,
|
||||
ides_setup: config.ides || [],
|
||||
expansion_packs: config.expansionPacks || [],
|
||||
files: [],
|
||||
@@ -128,6 +138,21 @@ class FileManager {
|
||||
}
|
||||
}
|
||||
|
||||
async readExpansionPackManifest(installDir, packId) {
|
||||
const manifestPath = path.join(
|
||||
installDir,
|
||||
`.${packId}`,
|
||||
this.manifestFile
|
||||
);
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(manifestPath, "utf8");
|
||||
return yaml.load(content);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async checkModifiedFiles(installDir, manifest) {
|
||||
const modified = [];
|
||||
|
||||
@@ -143,6 +168,33 @@ class FileManager {
|
||||
return modified;
|
||||
}
|
||||
|
||||
async checkFileIntegrity(installDir, manifest) {
|
||||
const result = {
|
||||
missing: [],
|
||||
modified: []
|
||||
};
|
||||
|
||||
for (const file of manifest.files) {
|
||||
const filePath = path.join(installDir, file.path);
|
||||
|
||||
// Skip checking the manifest file itself - it will always be different due to timestamps
|
||||
if (file.path.endsWith('install-manifest.yml')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(await this.pathExists(filePath))) {
|
||||
result.missing.push(file.path);
|
||||
} else {
|
||||
const currentHash = await this.calculateFileHash(filePath);
|
||||
if (currentHash && currentHash !== file.hash) {
|
||||
result.modified.push(file.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async backupFile(filePath) {
|
||||
const backupPath = filePath + ".bak";
|
||||
let counter = 1;
|
||||
@@ -183,6 +235,42 @@ class FileManager {
|
||||
async removeDirectory(dirPath) {
|
||||
await fs.remove(dirPath);
|
||||
}
|
||||
|
||||
async createExpansionPackManifest(installDir, packId, config, files) {
|
||||
const manifestPath = path.join(
|
||||
installDir,
|
||||
`.${packId}`,
|
||||
this.manifestFile
|
||||
);
|
||||
|
||||
const manifest = {
|
||||
version: config.expansionPackVersion || require("../../../package.json").version,
|
||||
installed_at: new Date().toISOString(),
|
||||
install_type: config.installType,
|
||||
expansion_pack_id: config.expansionPackId,
|
||||
expansion_pack_name: config.expansionPackName,
|
||||
ides_setup: config.ides || [],
|
||||
files: [],
|
||||
};
|
||||
|
||||
// Add file information
|
||||
for (const file of files) {
|
||||
const filePath = path.join(installDir, file);
|
||||
const hash = await this.calculateFileHash(filePath);
|
||||
|
||||
manifest.files.push({
|
||||
path: file,
|
||||
hash: hash,
|
||||
modified: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Write manifest
|
||||
await fs.ensureDir(path.dirname(manifestPath));
|
||||
await fs.writeFile(manifestPath, yaml.dump(manifest, { indent: 2 }));
|
||||
|
||||
return manifest;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new FileManager();
|
||||
|
||||
@@ -16,6 +16,20 @@ async function initializeModules() {
|
||||
}
|
||||
|
||||
class Installer {
|
||||
async getCoreVersion() {
|
||||
const yaml = require("js-yaml");
|
||||
const fs = require("fs-extra");
|
||||
const coreConfigPath = path.join(__dirname, "../../../bmad-core/core-config.yml");
|
||||
try {
|
||||
const coreConfigContent = await fs.readFile(coreConfigPath, "utf8");
|
||||
const coreConfig = yaml.load(coreConfigContent);
|
||||
return coreConfig.version || "unknown";
|
||||
} catch (error) {
|
||||
console.warn("Could not read version from core-config.yml, using 'unknown'");
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
async install(config) {
|
||||
// Initialize ES modules
|
||||
await initializeModules();
|
||||
@@ -161,6 +175,7 @@ class Installer {
|
||||
hasBmadCore: false,
|
||||
hasOtherFiles: false,
|
||||
manifest: null,
|
||||
expansionPacks: {},
|
||||
};
|
||||
|
||||
// Check if directory exists
|
||||
@@ -209,10 +224,14 @@ class Installer {
|
||||
state.hasOtherFiles = true;
|
||||
}
|
||||
|
||||
// Check for expansion packs (folders starting with .)
|
||||
const expansionPacks = await this.detectExpansionPacks(installDir);
|
||||
state.expansionPacks = expansionPacks;
|
||||
|
||||
return state; // clean install
|
||||
}
|
||||
|
||||
async performFreshInstall(config, installDir, spinner) {
|
||||
async performFreshInstall(config, installDir, spinner, options = {}) {
|
||||
// Ensure modules are initialized
|
||||
await initializeModules();
|
||||
spinner.text = "Installing BMAD Method...";
|
||||
@@ -328,41 +347,13 @@ class Installer {
|
||||
const commonFiles = await this.copyCommonItems(installDir, ".bmad-core", spinner);
|
||||
files.push(...commonFiles);
|
||||
} else if (config.installType === "expansion-only") {
|
||||
// Expansion-only installation - create minimal .bmad-core structure
|
||||
spinner.text = "Creating minimal .bmad-core structure for expansion packs...";
|
||||
|
||||
const bmadCoreDestDir = path.join(installDir, ".bmad-core");
|
||||
await fileManager.ensureDirectory(bmadCoreDestDir);
|
||||
|
||||
// Create basic directory structure
|
||||
const dirs = ['agents', 'agent-teams', 'templates', 'tasks', 'checklists', 'workflows', 'data', 'utils', 'schemas'];
|
||||
for (const dir of dirs) {
|
||||
await fileManager.ensureDirectory(path.join(bmadCoreDestDir, dir));
|
||||
}
|
||||
|
||||
// Copy minimal required files (schemas, utils, etc.)
|
||||
const sourceBase = configLoader.getBmadCorePath();
|
||||
const essentialFiles = [
|
||||
'schemas/**/*',
|
||||
'utils/**/*'
|
||||
];
|
||||
|
||||
for (const pattern of essentialFiles) {
|
||||
const copiedFiles = await fileManager.copyGlobPattern(
|
||||
pattern,
|
||||
sourceBase,
|
||||
bmadCoreDestDir
|
||||
);
|
||||
files.push(...copiedFiles.map(f => `.bmad-core/${f}`));
|
||||
}
|
||||
|
||||
// Copy common/ items to .bmad-core
|
||||
spinner.text = "Copying common utilities...";
|
||||
await this.copyCommonItems(installDir, ".bmad-core", spinner);
|
||||
// Expansion-only installation - DO NOT create .bmad-core
|
||||
// Only install expansion packs
|
||||
spinner.text = "Installing expansion packs only...";
|
||||
}
|
||||
|
||||
// Install expansion packs if requested
|
||||
const expansionFiles = await this.installExpansionPacks(installDir, config.expansionPacks, spinner);
|
||||
const expansionFiles = await this.installExpansionPacks(installDir, config.expansionPacks, spinner, config);
|
||||
files.push(...expansionFiles);
|
||||
|
||||
// Install web bundles if requested
|
||||
@@ -385,12 +376,14 @@ class Installer {
|
||||
}
|
||||
}
|
||||
|
||||
// Create manifest
|
||||
spinner.text = "Creating installation manifest...";
|
||||
await fileManager.createManifest(installDir, config, files);
|
||||
// Create manifest (skip for expansion-only installations)
|
||||
if (config.installType !== "expansion-only") {
|
||||
spinner.text = "Creating installation manifest...";
|
||||
await fileManager.createManifest(installDir, config, files);
|
||||
}
|
||||
|
||||
spinner.succeed("Installation complete!");
|
||||
this.showSuccessMessage(config, installDir);
|
||||
this.showSuccessMessage(config, installDir, options);
|
||||
}
|
||||
|
||||
async handleExistingV4Installation(config, installDir, state, spinner) {
|
||||
@@ -398,33 +391,137 @@ class Installer {
|
||||
await initializeModules();
|
||||
spinner.stop();
|
||||
|
||||
const currentVersion = state.manifest.version;
|
||||
const newVersion = await this.getCoreVersion();
|
||||
const versionCompare = this.compareVersions(currentVersion, newVersion);
|
||||
|
||||
console.log(chalk.yellow("\n🔍 Found existing BMAD v4 installation"));
|
||||
console.log(` Directory: ${installDir}`);
|
||||
console.log(` Version: ${state.manifest.version}`);
|
||||
console.log(` Current version: ${currentVersion}`);
|
||||
console.log(` Available version: ${newVersion}`);
|
||||
console.log(
|
||||
` Installed: ${new Date(
|
||||
state.manifest.installed_at
|
||||
).toLocaleDateString()}`
|
||||
);
|
||||
|
||||
// Check file integrity
|
||||
spinner.start("Checking installation integrity...");
|
||||
const integrity = await fileManager.checkFileIntegrity(installDir, state.manifest);
|
||||
spinner.stop();
|
||||
|
||||
const hasMissingFiles = integrity.missing.length > 0;
|
||||
const hasModifiedFiles = integrity.modified.length > 0;
|
||||
const hasIntegrityIssues = hasMissingFiles || hasModifiedFiles;
|
||||
|
||||
if (hasIntegrityIssues) {
|
||||
console.log(chalk.red("\n⚠️ Installation issues detected:"));
|
||||
if (hasMissingFiles) {
|
||||
console.log(chalk.red(` Missing files: ${integrity.missing.length}`));
|
||||
if (integrity.missing.length <= 5) {
|
||||
integrity.missing.forEach(file => console.log(chalk.dim(` - ${file}`)));
|
||||
}
|
||||
}
|
||||
if (hasModifiedFiles) {
|
||||
console.log(chalk.yellow(` Modified files: ${integrity.modified.length}`));
|
||||
if (integrity.modified.length <= 5) {
|
||||
integrity.modified.forEach(file => console.log(chalk.dim(` - ${file}`)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show existing expansion packs
|
||||
if (Object.keys(state.expansionPacks).length > 0) {
|
||||
console.log(chalk.cyan("\n📦 Installed expansion packs:"));
|
||||
for (const [packId, packInfo] of Object.entries(state.expansionPacks)) {
|
||||
if (packInfo.hasManifest && packInfo.manifest) {
|
||||
console.log(` - ${packId} (v${packInfo.manifest.version || 'unknown'})`);
|
||||
} else {
|
||||
console.log(` - ${packId} (no manifest)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let choices = [];
|
||||
|
||||
if (versionCompare < 0) {
|
||||
console.log(chalk.cyan("\n⬆️ Upgrade available for BMAD core"));
|
||||
choices.push({ name: `Upgrade BMAD core (v${currentVersion} → v${newVersion})`, value: "upgrade" });
|
||||
} else if (versionCompare === 0) {
|
||||
if (hasIntegrityIssues) {
|
||||
// Offer repair option when files are missing or modified
|
||||
choices.push({
|
||||
name: "Repair installation (restore missing/modified files)",
|
||||
value: "repair"
|
||||
});
|
||||
}
|
||||
console.log(chalk.yellow("\n⚠️ Same version already installed"));
|
||||
choices.push({ name: `Force reinstall BMAD core (v${currentVersion} - reinstall)`, value: "reinstall" });
|
||||
} else {
|
||||
console.log(chalk.yellow("\n⬇️ Installed version is newer than available"));
|
||||
choices.push({ name: `Downgrade BMAD core (v${currentVersion} → v${newVersion})`, value: "reinstall" });
|
||||
}
|
||||
|
||||
choices.push(
|
||||
{ name: "Add/update expansion packs only", value: "expansions" },
|
||||
{ name: "Cancel", value: "cancel" }
|
||||
);
|
||||
|
||||
const { action } = await inquirer.prompt([
|
||||
{
|
||||
type: "list",
|
||||
name: "action",
|
||||
message: "What would you like to do?",
|
||||
choices: [
|
||||
{ name: "Update existing installation", value: "update" },
|
||||
{ name: "Reinstall (overwrite)", value: "reinstall" },
|
||||
{ name: "Cancel", value: "cancel" },
|
||||
],
|
||||
choices: choices,
|
||||
},
|
||||
]);
|
||||
|
||||
switch (action) {
|
||||
case "update":
|
||||
case "upgrade":
|
||||
return await this.performUpdate(config, installDir, state.manifest, spinner);
|
||||
case "repair":
|
||||
// For repair, restore missing/modified files while backing up modified ones
|
||||
return await this.performRepair(config, installDir, state.manifest, integrity, spinner);
|
||||
case "reinstall":
|
||||
// For reinstall, don't check for modifications - just overwrite
|
||||
return await this.performReinstall(config, installDir, spinner);
|
||||
case "expansions":
|
||||
// Ask which expansion packs to install
|
||||
const availableExpansionPacks = await this.getAvailableExpansionPacks();
|
||||
|
||||
if (availableExpansionPacks.length === 0) {
|
||||
console.log(chalk.yellow("No expansion packs available."));
|
||||
return;
|
||||
}
|
||||
|
||||
const { selectedPacks } = await inquirer.prompt([
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'selectedPacks',
|
||||
message: 'Select expansion packs to install/update:',
|
||||
choices: availableExpansionPacks.map(pack => ({
|
||||
name: `${pack.name} v${pack.version} - ${pack.description}`,
|
||||
value: pack.id,
|
||||
checked: state.expansionPacks[pack.id] !== undefined
|
||||
}))
|
||||
}
|
||||
]);
|
||||
|
||||
if (selectedPacks.length === 0) {
|
||||
console.log(chalk.yellow("No expansion packs selected."));
|
||||
return;
|
||||
}
|
||||
|
||||
spinner.start("Installing expansion packs...");
|
||||
const expansionFiles = await this.installExpansionPacks(installDir, selectedPacks, spinner, { ides: config.ides || [] });
|
||||
spinner.succeed("Expansion packs installed successfully!");
|
||||
|
||||
console.log(chalk.green("\n✓ Installation complete!"));
|
||||
console.log(chalk.green(`✓ Expansion packs installed/updated:`));
|
||||
for (const packId of selectedPacks) {
|
||||
console.log(chalk.green(` - ${packId} → .${packId}/`));
|
||||
}
|
||||
return;
|
||||
case "cancel":
|
||||
console.log("Installation cancelled.");
|
||||
return;
|
||||
@@ -525,12 +622,20 @@ class Installer {
|
||||
spinner.start("Checking for updates...");
|
||||
|
||||
try {
|
||||
// Check for modified files
|
||||
spinner.text = "Checking for modified files...";
|
||||
const modifiedFiles = await fileManager.checkModifiedFiles(
|
||||
installDir,
|
||||
manifest
|
||||
);
|
||||
// Get current and new versions
|
||||
const currentVersion = manifest.version;
|
||||
const newVersion = await this.getCoreVersion();
|
||||
const versionCompare = this.compareVersions(currentVersion, newVersion);
|
||||
|
||||
// Only check for modified files if it's an actual version upgrade
|
||||
let modifiedFiles = [];
|
||||
if (versionCompare !== 0) {
|
||||
spinner.text = "Checking for modified files...";
|
||||
modifiedFiles = await fileManager.checkModifiedFiles(
|
||||
installDir,
|
||||
manifest
|
||||
);
|
||||
}
|
||||
|
||||
if (modifiedFiles.length > 0) {
|
||||
spinner.warn("Found modified files");
|
||||
@@ -570,35 +675,117 @@ class Installer {
|
||||
}
|
||||
|
||||
// Perform update by re-running installation
|
||||
spinner.text = "Updating files...";
|
||||
spinner.text = versionCompare === 0 ? "Reinstalling files..." : "Updating files...";
|
||||
const config = {
|
||||
installType: manifest.install_type,
|
||||
agent: manifest.agent,
|
||||
directory: installDir,
|
||||
ide: newConfig?.ide || manifest.ide_setup, // Use new IDE choice if provided
|
||||
ides: newConfig?.ides || manifest.ides_setup || [],
|
||||
};
|
||||
|
||||
await this.performFreshInstall(config, installDir, spinner);
|
||||
await this.performFreshInstall(config, installDir, spinner, { isUpdate: true });
|
||||
} catch (error) {
|
||||
spinner.fail("Update failed");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async performRepair(config, installDir, manifest, integrity, spinner) {
|
||||
spinner.start("Preparing to repair installation...");
|
||||
|
||||
try {
|
||||
// Back up modified files
|
||||
if (integrity.modified.length > 0) {
|
||||
spinner.text = "Backing up modified files...";
|
||||
for (const file of integrity.modified) {
|
||||
const filePath = path.join(installDir, file);
|
||||
if (await fileManager.pathExists(filePath)) {
|
||||
const backupPath = await fileManager.backupFile(filePath);
|
||||
console.log(chalk.dim(` Backed up: ${file} → ${path.basename(backupPath)}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore missing and modified files
|
||||
spinner.text = "Restoring files...";
|
||||
const sourceBase = configLoader.getBmadCorePath();
|
||||
const filesToRestore = [...integrity.missing, ...integrity.modified];
|
||||
|
||||
for (const file of filesToRestore) {
|
||||
// Skip the manifest file itself
|
||||
if (file.endsWith('install-manifest.yml')) continue;
|
||||
|
||||
const relativePath = file.replace('.bmad-core/', '');
|
||||
const destPath = path.join(installDir, file);
|
||||
|
||||
// Check if this is a common/ file that needs special processing
|
||||
const commonBase = path.dirname(path.dirname(path.dirname(path.dirname(__filename))));
|
||||
const commonSourcePath = path.join(commonBase, 'common', relativePath);
|
||||
|
||||
if (await fileManager.pathExists(commonSourcePath)) {
|
||||
// This is a common/ file - needs template processing
|
||||
const fs = require('fs').promises;
|
||||
const content = await fs.readFile(commonSourcePath, 'utf8');
|
||||
const updatedContent = content.replace(/\{root\}/g, '.bmad-core');
|
||||
await fileManager.ensureDirectory(path.dirname(destPath));
|
||||
await fs.writeFile(destPath, updatedContent, 'utf8');
|
||||
spinner.text = `Restored: ${file}`;
|
||||
} else {
|
||||
// Regular file from bmad-core
|
||||
const sourcePath = path.join(sourceBase, relativePath);
|
||||
if (await fileManager.pathExists(sourcePath)) {
|
||||
await fileManager.copyFile(sourcePath, destPath);
|
||||
spinner.text = `Restored: ${file}`;
|
||||
} else {
|
||||
console.warn(chalk.yellow(` Warning: Source file not found: ${file}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spinner.succeed("Repair completed successfully!");
|
||||
|
||||
// Show summary
|
||||
console.log(chalk.green("\n✓ Installation repaired!"));
|
||||
if (integrity.missing.length > 0) {
|
||||
console.log(chalk.green(` Restored ${integrity.missing.length} missing files`));
|
||||
}
|
||||
if (integrity.modified.length > 0) {
|
||||
console.log(chalk.green(` Restored ${integrity.modified.length} modified files (backups created)`));
|
||||
}
|
||||
|
||||
// Warning for Cursor custom modes if agents were repaired
|
||||
const ides = manifest.ides_setup || [];
|
||||
if (ides.includes('cursor')) {
|
||||
console.log(chalk.yellow.bold("\n⚠️ IMPORTANT: Cursor Custom Modes Update Required"));
|
||||
console.log(chalk.yellow("Since agent files have been repaired, you need to manually update your Cursor custom modes:"));
|
||||
console.log(chalk.yellow("1. Open Cursor Settings (Cmd/Ctrl + ,)"));
|
||||
console.log(chalk.yellow("2. Go to: Features > Cursor Tab > Custom Modes"));
|
||||
console.log(chalk.yellow("3. Update each custom mode with the latest agent templates from:"));
|
||||
console.log(chalk.yellow(` ${path.join(installDir, '.bmad-core', 'agents')}`));
|
||||
console.log(chalk.yellow("4. Copy the full content of each agent file into the corresponding custom mode"));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
spinner.fail("Repair failed");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async performReinstall(config, installDir, spinner) {
|
||||
spinner.start("Reinstalling BMAD Method...");
|
||||
spinner.start("Preparing to reinstall BMAD Method...");
|
||||
|
||||
// Remove existing .bmad-core
|
||||
const bmadCorePath = path.join(installDir, ".bmad-core");
|
||||
if (await fileManager.pathExists(bmadCorePath)) {
|
||||
spinner.text = "Removing existing installation...";
|
||||
await fileManager.removeDirectory(bmadCorePath);
|
||||
}
|
||||
|
||||
return await this.performFreshInstall(config, installDir, spinner);
|
||||
|
||||
spinner.text = "Installing fresh copy...";
|
||||
return await this.performFreshInstall(config, installDir, spinner, { isUpdate: true });
|
||||
}
|
||||
|
||||
showSuccessMessage(config, installDir) {
|
||||
showSuccessMessage(config, installDir, options = {}) {
|
||||
console.log(chalk.green("\n✓ BMAD Method installed successfully!\n"));
|
||||
|
||||
const ides = config.ides || (config.ide ? [config.ide] : []);
|
||||
@@ -622,7 +809,9 @@ class Installer {
|
||||
|
||||
// Information about installation components
|
||||
console.log(chalk.bold("\n🎯 Installation Summary:"));
|
||||
console.log(chalk.green("✓ .bmad-core framework installed with all agents and workflows"));
|
||||
if (config.installType !== "expansion-only") {
|
||||
console.log(chalk.green("✓ .bmad-core framework installed with all agents and workflows"));
|
||||
}
|
||||
|
||||
if (config.expansionPacks && config.expansionPacks.length > 0) {
|
||||
console.log(chalk.green(`✓ Expansion packs installed:`));
|
||||
@@ -668,6 +857,17 @@ class Installer {
|
||||
chalk.dim("Need everything? Run: npx bmad-method install --full")
|
||||
);
|
||||
}
|
||||
|
||||
// Warning for Cursor custom modes if agents were updated
|
||||
if (options.isUpdate && ides.includes('cursor')) {
|
||||
console.log(chalk.yellow.bold("\n⚠️ IMPORTANT: Cursor Custom Modes Update Required"));
|
||||
console.log(chalk.yellow("Since agents have been updated, you need to manually update your Cursor custom modes:"));
|
||||
console.log(chalk.yellow("1. Open Cursor Settings (Cmd/Ctrl + ,)"));
|
||||
console.log(chalk.yellow("2. Go to: Features > Cursor Tab > Custom Modes"));
|
||||
console.log(chalk.yellow("3. Update each custom mode with the latest agent templates from:"));
|
||||
console.log(chalk.yellow(` ${path.join(installDir, '.bmad-core', 'agents')}`));
|
||||
console.log(chalk.yellow("4. Copy the full content of each agent file into the corresponding custom mode"));
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy method for backward compatibility
|
||||
@@ -767,8 +967,8 @@ class Installer {
|
||||
console.log(` Agent: ${manifest.agent}`);
|
||||
}
|
||||
|
||||
if (manifest.ide_setup) {
|
||||
console.log(` IDE Setup: ${manifest.ide_setup}`);
|
||||
if (manifest.ides_setup && manifest.ides_setup.length > 0) {
|
||||
console.log(` IDE Setup: ${manifest.ides_setup.join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(` Total Files: ${manifest.files.length}`);
|
||||
@@ -797,7 +997,7 @@ class Installer {
|
||||
return configLoader.getAvailableTeams();
|
||||
}
|
||||
|
||||
async installExpansionPacks(installDir, selectedPacks, spinner) {
|
||||
async installExpansionPacks(installDir, selectedPacks, spinner, config = {}) {
|
||||
if (!selectedPacks || selectedPacks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -816,11 +1016,112 @@ class Installer {
|
||||
console.warn(`Expansion pack ${packId} not found, skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if expansion pack already exists
|
||||
let expansionDotFolder = path.join(installDir, `.${packId}`);
|
||||
const existingManifestPath = path.join(expansionDotFolder, 'install-manifest.yml');
|
||||
|
||||
if (await fileManager.pathExists(existingManifestPath)) {
|
||||
spinner.stop();
|
||||
const existingManifest = await fileManager.readExpansionPackManifest(installDir, packId);
|
||||
|
||||
console.log(chalk.yellow(`\n🔍 Found existing ${pack.name} installation`));
|
||||
console.log(` Current version: ${existingManifest.version || 'unknown'}`);
|
||||
console.log(` New version: ${pack.version}`);
|
||||
|
||||
// Check integrity of existing expansion pack
|
||||
const packIntegrity = await fileManager.checkFileIntegrity(installDir, existingManifest);
|
||||
const hasPackIntegrityIssues = packIntegrity.missing.length > 0 || packIntegrity.modified.length > 0;
|
||||
|
||||
if (hasPackIntegrityIssues) {
|
||||
console.log(chalk.red(" ⚠️ Installation issues detected:"));
|
||||
if (packIntegrity.missing.length > 0) {
|
||||
console.log(chalk.red(` Missing files: ${packIntegrity.missing.length}`));
|
||||
}
|
||||
if (packIntegrity.modified.length > 0) {
|
||||
console.log(chalk.yellow(` Modified files: ${packIntegrity.modified.length}`));
|
||||
}
|
||||
}
|
||||
|
||||
const versionCompare = this.compareVersions(existingManifest.version || '0.0.0', pack.version);
|
||||
|
||||
if (versionCompare === 0) {
|
||||
console.log(chalk.yellow(' ⚠️ Same version already installed'));
|
||||
|
||||
const choices = [];
|
||||
if (hasPackIntegrityIssues) {
|
||||
choices.push({ name: 'Repair (restore missing/modified files)', value: 'repair' });
|
||||
}
|
||||
choices.push(
|
||||
{ name: 'Force reinstall (overwrite)', value: 'overwrite' },
|
||||
{ name: 'Skip this expansion pack', value: 'skip' },
|
||||
{ name: 'Cancel installation', value: 'cancel' }
|
||||
);
|
||||
|
||||
const { action } = await inquirer.prompt([{
|
||||
type: 'list',
|
||||
name: 'action',
|
||||
message: `${pack.name} v${pack.version} is already installed. What would you like to do?`,
|
||||
choices: choices
|
||||
}]);
|
||||
|
||||
if (action === 'skip') {
|
||||
spinner.start();
|
||||
continue;
|
||||
} else if (action === 'cancel') {
|
||||
console.log(chalk.red('Installation cancelled.'));
|
||||
process.exit(0);
|
||||
} else if (action === 'repair') {
|
||||
// Repair the expansion pack
|
||||
await this.repairExpansionPack(installDir, packId, pack, packIntegrity, spinner);
|
||||
continue;
|
||||
}
|
||||
} else if (versionCompare < 0) {
|
||||
console.log(chalk.cyan(' ⬆️ Upgrade available'));
|
||||
|
||||
const { proceed } = await inquirer.prompt([{
|
||||
type: 'confirm',
|
||||
name: 'proceed',
|
||||
message: `Upgrade ${pack.name} from v${existingManifest.version} to v${pack.version}?`,
|
||||
default: true
|
||||
}]);
|
||||
|
||||
if (!proceed) {
|
||||
spinner.start();
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.yellow(' ⬇️ Installed version is newer than available version'));
|
||||
|
||||
const { action } = await inquirer.prompt([{
|
||||
type: 'list',
|
||||
name: 'action',
|
||||
message: 'What would you like to do?',
|
||||
choices: [
|
||||
{ name: 'Keep current version', value: 'skip' },
|
||||
{ name: 'Downgrade to available version', value: 'downgrade' },
|
||||
{ name: 'Cancel installation', value: 'cancel' }
|
||||
]
|
||||
}]);
|
||||
|
||||
if (action === 'skip') {
|
||||
spinner.start();
|
||||
continue;
|
||||
} else if (action === 'cancel') {
|
||||
console.log(chalk.red('Installation cancelled.'));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, we're proceeding with installation
|
||||
spinner.start(`Removing old ${pack.name} installation...`);
|
||||
await fileManager.removeDirectory(expansionDotFolder);
|
||||
}
|
||||
|
||||
const expansionPackDir = pack.packPath;
|
||||
|
||||
// Create dedicated dot folder for this expansion pack
|
||||
const expansionDotFolder = path.join(installDir, `.${packId}`);
|
||||
// Ensure dedicated dot folder exists for this expansion pack
|
||||
expansionDotFolder = path.join(installDir, `.${packId}`);
|
||||
await fileManager.ensureDirectory(expansionDotFolder);
|
||||
|
||||
// Define the folders to copy from expansion packs
|
||||
@@ -888,9 +1189,28 @@ class Installer {
|
||||
// Check and resolve core agents referenced by teams
|
||||
await this.resolveExpansionPackCoreAgents(installDir, expansionDotFolder, packId, spinner);
|
||||
|
||||
// Create manifest for this expansion pack
|
||||
spinner.text = `Creating manifest for ${packId}...`;
|
||||
const expansionConfig = {
|
||||
installType: 'expansion-pack',
|
||||
expansionPackId: packId,
|
||||
expansionPackName: pack.name,
|
||||
expansionPackVersion: pack.version,
|
||||
ides: config.ides || [] // Use ides_setup instead of ide_setup
|
||||
};
|
||||
|
||||
// Get all files installed in this expansion pack
|
||||
const expansionPackFiles = glob.sync('**/*', {
|
||||
cwd: expansionDotFolder,
|
||||
nodir: true
|
||||
}).map(f => path.join(`.${packId}`, f));
|
||||
|
||||
await fileManager.createExpansionPackManifest(installDir, packId, expansionConfig, expansionPackFiles);
|
||||
|
||||
console.log(chalk.green(`✓ Installed expansion pack: ${pack.name} to ${`.${packId}`}`));
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Failed to install expansion pack ${packId}: ${error.message}`));
|
||||
console.error(chalk.red(`Stack trace: ${error.stack}`));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1034,7 +1354,8 @@ class Installer {
|
||||
console.log(chalk.dim(` Added agent dependency: ${depType}/${depFileName}`));
|
||||
} else {
|
||||
// Try common folder
|
||||
const commonDepPath = path.join(this.rootDir, 'common', depType, depFileName);
|
||||
const sourceBase = path.dirname(path.dirname(path.dirname(path.dirname(__filename)))); // Go up to project root
|
||||
const commonDepPath = path.join(sourceBase, 'common', depType, depFileName);
|
||||
if (await fileManager.pathExists(commonDepPath)) {
|
||||
const destDepPath = path.join(expansionDotFolder, depType, depFileName);
|
||||
await fileManager.copyFile(commonDepPath, destDepPath);
|
||||
@@ -1157,6 +1478,9 @@ class Installer {
|
||||
}
|
||||
|
||||
async copyCommonItems(installDir, targetSubdir, spinner) {
|
||||
// Ensure modules are initialized
|
||||
await initializeModules();
|
||||
|
||||
const glob = require('glob');
|
||||
const fs = require('fs').promises;
|
||||
const sourceBase = path.dirname(path.dirname(path.dirname(path.dirname(__filename)))); // Go up to project root
|
||||
@@ -1198,6 +1522,133 @@ class Installer {
|
||||
return copiedFiles;
|
||||
}
|
||||
|
||||
async detectExpansionPacks(installDir) {
|
||||
const expansionPacks = {};
|
||||
const glob = require("glob");
|
||||
|
||||
// Find all dot folders that might be expansion packs
|
||||
const dotFolders = glob.sync(".*", {
|
||||
cwd: installDir,
|
||||
ignore: [".git", ".git/**", ".bmad-core", ".bmad-core/**"],
|
||||
});
|
||||
|
||||
for (const folder of dotFolders) {
|
||||
const folderPath = path.join(installDir, folder);
|
||||
const stats = await fileManager.pathExists(folderPath);
|
||||
|
||||
if (stats) {
|
||||
// Check if it has a manifest
|
||||
const manifestPath = path.join(folderPath, "install-manifest.yml");
|
||||
if (await fileManager.pathExists(manifestPath)) {
|
||||
const manifest = await fileManager.readExpansionPackManifest(installDir, folder.substring(1));
|
||||
if (manifest) {
|
||||
expansionPacks[folder.substring(1)] = {
|
||||
path: folderPath,
|
||||
manifest: manifest,
|
||||
hasManifest: true
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Check if it has a config.yml (expansion pack without manifest)
|
||||
const configPath = path.join(folderPath, "config.yml");
|
||||
if (await fileManager.pathExists(configPath)) {
|
||||
expansionPacks[folder.substring(1)] = {
|
||||
path: folderPath,
|
||||
manifest: null,
|
||||
hasManifest: false
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return expansionPacks;
|
||||
}
|
||||
|
||||
async repairExpansionPack(installDir, packId, pack, integrity, spinner) {
|
||||
spinner.start(`Repairing ${pack.name}...`);
|
||||
|
||||
try {
|
||||
const expansionDotFolder = path.join(installDir, `.${packId}`);
|
||||
|
||||
// Back up modified files
|
||||
if (integrity.modified.length > 0) {
|
||||
spinner.text = "Backing up modified files...";
|
||||
for (const file of integrity.modified) {
|
||||
const filePath = path.join(installDir, file);
|
||||
if (await fileManager.pathExists(filePath)) {
|
||||
const backupPath = await fileManager.backupFile(filePath);
|
||||
console.log(chalk.dim(` Backed up: ${file} → ${path.basename(backupPath)}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore missing and modified files
|
||||
spinner.text = "Restoring files...";
|
||||
const filesToRestore = [...integrity.missing, ...integrity.modified];
|
||||
|
||||
for (const file of filesToRestore) {
|
||||
// Skip the manifest file itself
|
||||
if (file.endsWith('install-manifest.yml')) continue;
|
||||
|
||||
const relativePath = file.replace(`.${packId}/`, '');
|
||||
const sourcePath = path.join(pack.packPath, relativePath);
|
||||
const destPath = path.join(installDir, file);
|
||||
|
||||
// Check if this is a common/ file that needs special processing
|
||||
const commonBase = path.dirname(path.dirname(path.dirname(path.dirname(__filename))));
|
||||
const commonSourcePath = path.join(commonBase, 'common', relativePath);
|
||||
|
||||
if (await fileManager.pathExists(commonSourcePath)) {
|
||||
// This is a common/ file - needs template processing
|
||||
const fs = require('fs').promises;
|
||||
const content = await fs.readFile(commonSourcePath, 'utf8');
|
||||
const updatedContent = content.replace(/\{root\}/g, `.${packId}`);
|
||||
await fileManager.ensureDirectory(path.dirname(destPath));
|
||||
await fs.writeFile(destPath, updatedContent, 'utf8');
|
||||
spinner.text = `Restored: ${file}`;
|
||||
} else if (await fileManager.pathExists(sourcePath)) {
|
||||
// Regular file from expansion pack
|
||||
await fileManager.copyFile(sourcePath, destPath);
|
||||
spinner.text = `Restored: ${file}`;
|
||||
} else {
|
||||
console.warn(chalk.yellow(` Warning: Source file not found: ${file}`));
|
||||
}
|
||||
}
|
||||
|
||||
spinner.succeed(`${pack.name} repaired successfully!`);
|
||||
|
||||
// Show summary
|
||||
console.log(chalk.green(`\n✓ ${pack.name} repaired!`));
|
||||
if (integrity.missing.length > 0) {
|
||||
console.log(chalk.green(` Restored ${integrity.missing.length} missing files`));
|
||||
}
|
||||
if (integrity.modified.length > 0) {
|
||||
console.log(chalk.green(` Restored ${integrity.modified.length} modified files (backups created)`));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
spinner.fail(`Failed to repair ${pack.name}`);
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
compareVersions(v1, v2) {
|
||||
// Simple semver comparison
|
||||
const parts1 = v1.split('.').map(Number);
|
||||
const parts2 = v2.split('.').map(Number);
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const part1 = parts1[i] || 0;
|
||||
const part2 = parts2[i] || 0;
|
||||
|
||||
if (part1 > part2) return 1;
|
||||
if (part1 < part2) return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
async findInstallation() {
|
||||
// Look for .bmad-core in current directory or parent directories
|
||||
let currentDir = process.cwd();
|
||||
|
||||
54
tools/update-expansion-version.js
Executable file
54
tools/update-expansion-version.js
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 2) {
|
||||
console.log('Usage: node update-expansion-version.js <expansion-pack-id> <new-version>');
|
||||
console.log('Example: node update-expansion-version.js bmad-creator-tools 1.1.0');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [packId, newVersion] = args;
|
||||
|
||||
// Validate version format
|
||||
if (!/^\d+\.\d+\.\d+$/.test(newVersion)) {
|
||||
console.error('Error: Version must be in format X.Y.Z (e.g., 1.2.3)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function updateVersion() {
|
||||
try {
|
||||
// Update in config.yml
|
||||
const configPath = path.join(__dirname, '..', 'expansion-packs', packId, 'config.yml');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
console.error(`Error: Expansion pack '${packId}' not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||
const config = yaml.load(configContent);
|
||||
const oldVersion = config.version || 'unknown';
|
||||
|
||||
config.version = newVersion;
|
||||
|
||||
const updatedYaml = yaml.dump(config, { indent: 2 });
|
||||
fs.writeFileSync(configPath, updatedYaml);
|
||||
|
||||
console.log(`✓ Updated ${packId}/config.yml: ${oldVersion} → ${newVersion}`);
|
||||
console.log(`\n✓ Successfully updated ${packId} to version ${newVersion}`);
|
||||
console.log('\nNext steps:');
|
||||
console.log('1. Test the changes');
|
||||
console.log('2. Commit: git add -A && git commit -m "chore: bump ' + packId + ' to v' + newVersion + '"');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating version:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
updateVersion();
|
||||
Reference in New Issue
Block a user