versioned module downloads and manifest
This commit is contained in:
parent
aad132c9b1
commit
3f9ad4868c
|
|
@ -0,0 +1,65 @@
|
||||||
|
const chalk = require('chalk');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { Installer } = require('../installers/lib/core/installer');
|
||||||
|
const { Manifest } = require('../installers/lib/core/manifest');
|
||||||
|
const { UI } = require('../lib/ui');
|
||||||
|
|
||||||
|
const installer = new Installer();
|
||||||
|
const manifest = new Manifest();
|
||||||
|
const ui = new UI();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
command: 'status',
|
||||||
|
description: 'Display BMAD installation status and module versions',
|
||||||
|
options: [],
|
||||||
|
action: async (options) => {
|
||||||
|
try {
|
||||||
|
// Find the bmad directory
|
||||||
|
const projectDir = process.cwd();
|
||||||
|
const { bmadDir } = await installer.findBmadDir(projectDir);
|
||||||
|
|
||||||
|
// Check if bmad directory exists
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
if (!(await fs.pathExists(bmadDir))) {
|
||||||
|
console.log(chalk.yellow('No BMAD installation found in the current directory.'));
|
||||||
|
console.log(chalk.dim(`Expected location: ${bmadDir}`));
|
||||||
|
console.log(chalk.dim('\nRun "bmad install" to set up a new installation.'));
|
||||||
|
process.exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read manifest
|
||||||
|
const manifestData = await manifest._readRaw(bmadDir);
|
||||||
|
|
||||||
|
if (!manifestData) {
|
||||||
|
console.log(chalk.yellow('No BMAD installation manifest found.'));
|
||||||
|
console.log(chalk.dim('\nRun "bmad install" to set up a new installation.'));
|
||||||
|
process.exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get installation info
|
||||||
|
const installation = manifestData.installation || {};
|
||||||
|
const modules = manifestData.modules || [];
|
||||||
|
|
||||||
|
// Check for available updates (only for external modules)
|
||||||
|
const availableUpdates = await manifest.checkForUpdates(bmadDir);
|
||||||
|
|
||||||
|
// Display status
|
||||||
|
ui.displayStatus({
|
||||||
|
installation,
|
||||||
|
modules,
|
||||||
|
availableUpdates,
|
||||||
|
bmadDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(chalk.red('Status check failed:'), error.message);
|
||||||
|
if (process.env.BMAD_DEBUG) {
|
||||||
|
console.error(chalk.dim(error.stack));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -10,6 +10,7 @@ modules:
|
||||||
description: "Agent, Workflow and Module Builder"
|
description: "Agent, Workflow and Module Builder"
|
||||||
defaultSelected: false
|
defaultSelected: false
|
||||||
type: bmad-org
|
type: bmad-org
|
||||||
|
npmPackage: bmad-builder
|
||||||
|
|
||||||
bmad-creative-intelligence-suite:
|
bmad-creative-intelligence-suite:
|
||||||
url: https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite
|
url: https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite
|
||||||
|
|
@ -19,6 +20,7 @@ modules:
|
||||||
description: "Creative tools for writing, brainstorming, and more"
|
description: "Creative tools for writing, brainstorming, and more"
|
||||||
defaultSelected: false
|
defaultSelected: false
|
||||||
type: bmad-org
|
type: bmad-org
|
||||||
|
npmPackage: bmad-creative-intelligence-suite
|
||||||
|
|
||||||
bmad-game-dev-studio:
|
bmad-game-dev-studio:
|
||||||
url: https://github.com/bmad-code-org/bmad-module-game-dev-studio.git
|
url: https://github.com/bmad-code-org/bmad-module-game-dev-studio.git
|
||||||
|
|
@ -28,6 +30,7 @@ modules:
|
||||||
description: "Game development agents and workflows"
|
description: "Game development agents and workflows"
|
||||||
defaultSelected: false
|
defaultSelected: false
|
||||||
type: bmad-org
|
type: bmad-org
|
||||||
|
npmPackage: bmad-game-dev-studio
|
||||||
|
|
||||||
# TODO: Enable once fixes applied:
|
# TODO: Enable once fixes applied:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -534,18 +534,71 @@ class ManifestGenerator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write main manifest as YAML with installation info only
|
* Write main manifest as YAML with installation info only
|
||||||
|
* Fetches fresh version info for all modules
|
||||||
* @returns {string} Path to the manifest file
|
* @returns {string} Path to the manifest file
|
||||||
*/
|
*/
|
||||||
async writeMainManifest(cfgDir) {
|
async writeMainManifest(cfgDir) {
|
||||||
const manifestPath = path.join(cfgDir, 'manifest.yaml');
|
const manifestPath = path.join(cfgDir, 'manifest.yaml');
|
||||||
|
|
||||||
|
// Read existing manifest to preserve install date
|
||||||
|
let existingInstallDate = null;
|
||||||
|
const existingModulesMap = new Map();
|
||||||
|
|
||||||
|
if (await fs.pathExists(manifestPath)) {
|
||||||
|
try {
|
||||||
|
const existingContent = await fs.readFile(manifestPath, 'utf8');
|
||||||
|
const existingManifest = yaml.parse(existingContent);
|
||||||
|
|
||||||
|
// Preserve original install date
|
||||||
|
if (existingManifest.installation?.installDate) {
|
||||||
|
existingInstallDate = existingManifest.installation.installDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build map of existing modules for quick lookup
|
||||||
|
if (existingManifest.modules && Array.isArray(existingManifest.modules)) {
|
||||||
|
for (const m of existingManifest.modules) {
|
||||||
|
if (typeof m === 'object' && m.name) {
|
||||||
|
existingModulesMap.set(m.name, m);
|
||||||
|
} else if (typeof m === 'string') {
|
||||||
|
existingModulesMap.set(m, { installDate: existingInstallDate });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If we can't read existing manifest, continue with defaults
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch fresh version info for all modules
|
||||||
|
const { Manifest } = require('./manifest');
|
||||||
|
const manifestObj = new Manifest();
|
||||||
|
const updatedModules = [];
|
||||||
|
|
||||||
|
for (const moduleName of this.modules) {
|
||||||
|
// Get fresh version info from source
|
||||||
|
const versionInfo = await manifestObj.getModuleVersionInfo(moduleName, this.bmadDir);
|
||||||
|
|
||||||
|
// Get existing install date if available
|
||||||
|
const existing = existingModulesMap.get(moduleName);
|
||||||
|
|
||||||
|
updatedModules.push({
|
||||||
|
name: moduleName,
|
||||||
|
version: versionInfo.version,
|
||||||
|
installDate: existing?.installDate || new Date().toISOString(),
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
source: versionInfo.source,
|
||||||
|
npmPackage: versionInfo.npmPackage,
|
||||||
|
repoUrl: versionInfo.repoUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
installation: {
|
installation: {
|
||||||
version: packageJson.version,
|
version: packageJson.version,
|
||||||
installDate: new Date().toISOString(),
|
installDate: existingInstallDate || new Date().toISOString(),
|
||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
modules: this.modules, // Include ALL modules (standard and custom)
|
modules: updatedModules,
|
||||||
ides: this.selectedIdes,
|
ides: this.selectedIdes,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const crypto = require('node:crypto');
|
const crypto = require('node:crypto');
|
||||||
|
const { getProjectRoot } = require('../../../lib/project-root');
|
||||||
|
|
||||||
class Manifest {
|
class Manifest {
|
||||||
/**
|
/**
|
||||||
|
|
@ -16,14 +17,35 @@ class Manifest {
|
||||||
// Ensure _config directory exists
|
// Ensure _config directory exists
|
||||||
await fs.ensureDir(path.dirname(manifestPath));
|
await fs.ensureDir(path.dirname(manifestPath));
|
||||||
|
|
||||||
|
// Get the BMad version from package.json
|
||||||
|
const bmadVersion = data.version || require(path.join(process.cwd(), 'package.json')).version;
|
||||||
|
|
||||||
|
// Convert module list to new detailed format
|
||||||
|
const moduleDetails = [];
|
||||||
|
if (data.modules && Array.isArray(data.modules)) {
|
||||||
|
for (const moduleName of data.modules) {
|
||||||
|
// Core and BMM modules use the BMad version
|
||||||
|
const moduleVersion = moduleName === 'core' || moduleName === 'bmm' ? bmadVersion : null;
|
||||||
|
const now = data.installDate || new Date().toISOString();
|
||||||
|
|
||||||
|
moduleDetails.push({
|
||||||
|
name: moduleName,
|
||||||
|
version: moduleVersion,
|
||||||
|
installDate: now,
|
||||||
|
lastUpdated: now,
|
||||||
|
source: moduleName === 'core' || moduleName === 'bmm' ? 'built-in' : 'unknown',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Structure the manifest data
|
// Structure the manifest data
|
||||||
const manifestData = {
|
const manifestData = {
|
||||||
installation: {
|
installation: {
|
||||||
version: data.version || require(path.join(process.cwd(), 'package.json')).version,
|
version: bmadVersion,
|
||||||
installDate: data.installDate || new Date().toISOString(),
|
installDate: data.installDate || new Date().toISOString(),
|
||||||
lastUpdated: data.lastUpdated || new Date().toISOString(),
|
lastUpdated: data.lastUpdated || new Date().toISOString(),
|
||||||
},
|
},
|
||||||
modules: data.modules || [],
|
modules: moduleDetails,
|
||||||
ides: data.ides || [],
|
ides: data.ides || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -57,12 +79,23 @@ class Manifest {
|
||||||
const content = await fs.readFile(yamlPath, 'utf8');
|
const content = await fs.readFile(yamlPath, 'utf8');
|
||||||
const manifestData = yaml.parse(content);
|
const manifestData = yaml.parse(content);
|
||||||
|
|
||||||
|
// Handle new detailed module format
|
||||||
|
const modules = manifestData.modules || [];
|
||||||
|
|
||||||
|
// For backward compatibility: if modules is an array of strings (old format),
|
||||||
|
// the calling code may need the array of names
|
||||||
|
const moduleNames = modules.map((m) => (typeof m === 'string' ? m : m.name));
|
||||||
|
|
||||||
|
// Check if we have the new detailed format
|
||||||
|
const hasDetailedModules = modules.length > 0 && typeof modules[0] === 'object';
|
||||||
|
|
||||||
// Flatten the structure for compatibility with existing code
|
// Flatten the structure for compatibility with existing code
|
||||||
return {
|
return {
|
||||||
version: manifestData.installation?.version,
|
version: manifestData.installation?.version,
|
||||||
installDate: manifestData.installation?.installDate,
|
installDate: manifestData.installation?.installDate,
|
||||||
lastUpdated: manifestData.installation?.lastUpdated,
|
lastUpdated: manifestData.installation?.lastUpdated,
|
||||||
modules: manifestData.modules || [], // All modules (standard and custom)
|
modules: moduleNames, // Simple array of module names for backward compatibility
|
||||||
|
modulesDetailed: hasDetailedModules ? modules : null, // New detailed format
|
||||||
customModules: manifestData.customModules || [], // Keep for backward compatibility
|
customModules: manifestData.customModules || [], // Keep for backward compatibility
|
||||||
ides: manifestData.ides || [],
|
ides: manifestData.ides || [],
|
||||||
};
|
};
|
||||||
|
|
@ -82,28 +115,92 @@ class Manifest {
|
||||||
*/
|
*/
|
||||||
async update(bmadDir, updates, installedFiles = null) {
|
async update(bmadDir, updates, installedFiles = null) {
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
const manifest = (await this.read(bmadDir)) || {};
|
const manifest = (await this._readRaw(bmadDir)) || {
|
||||||
|
installation: {},
|
||||||
// Merge updates
|
modules: [],
|
||||||
Object.assign(manifest, updates);
|
ides: [],
|
||||||
manifest.lastUpdated = new Date().toISOString();
|
|
||||||
|
|
||||||
// Convert back to structured format for YAML
|
|
||||||
const manifestData = {
|
|
||||||
installation: {
|
|
||||||
version: manifest.version,
|
|
||||||
installDate: manifest.installDate,
|
|
||||||
lastUpdated: manifest.lastUpdated,
|
|
||||||
},
|
|
||||||
modules: manifest.modules || [], // All modules (standard and custom)
|
|
||||||
ides: manifest.ides || [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle module updates
|
||||||
|
if (updates.modules) {
|
||||||
|
// If modules is being updated, we need to preserve detailed module info
|
||||||
|
const existingDetailed = manifest.modules || [];
|
||||||
|
const incomingNames = updates.modules;
|
||||||
|
|
||||||
|
// Build updated modules array
|
||||||
|
const updatedModules = [];
|
||||||
|
for (const name of incomingNames) {
|
||||||
|
const existing = existingDetailed.find((m) => m.name === name);
|
||||||
|
if (existing) {
|
||||||
|
// Preserve existing details, update lastUpdated if this module is being updated
|
||||||
|
updatedModules.push({
|
||||||
|
...existing,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// New module - add with minimal details
|
||||||
|
updatedModules.push({
|
||||||
|
name,
|
||||||
|
version: null,
|
||||||
|
installDate: new Date().toISOString(),
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
source: 'unknown',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest.modules = updatedModules;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge other updates
|
||||||
|
if (updates.version) {
|
||||||
|
manifest.installation.version = updates.version;
|
||||||
|
}
|
||||||
|
if (updates.installDate) {
|
||||||
|
manifest.installation.installDate = updates.installDate;
|
||||||
|
}
|
||||||
|
manifest.installation.lastUpdated = new Date().toISOString();
|
||||||
|
|
||||||
|
if (updates.ides) {
|
||||||
|
manifest.ides = updates.ides;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle per-module version updates
|
||||||
|
if (updates.moduleVersions) {
|
||||||
|
for (const [moduleName, versionInfo] of Object.entries(updates.moduleVersions)) {
|
||||||
|
const moduleIndex = manifest.modules.findIndex((m) => m.name === moduleName);
|
||||||
|
if (moduleIndex !== -1) {
|
||||||
|
manifest.modules[moduleIndex] = {
|
||||||
|
...manifest.modules[moduleIndex],
|
||||||
|
...versionInfo,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle adding a new module with version info
|
||||||
|
if (updates.addModule) {
|
||||||
|
const { name, version, source, npmPackage, repoUrl } = updates.addModule;
|
||||||
|
const existing = manifest.modules.find((m) => m.name === name);
|
||||||
|
if (!existing) {
|
||||||
|
manifest.modules.push({
|
||||||
|
name,
|
||||||
|
version: version || null,
|
||||||
|
installDate: new Date().toISOString(),
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
source: source || 'external',
|
||||||
|
npmPackage: npmPackage || null,
|
||||||
|
repoUrl: repoUrl || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
|
const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
|
||||||
await fs.ensureDir(path.dirname(manifestPath));
|
await fs.ensureDir(path.dirname(manifestPath));
|
||||||
|
|
||||||
// Clean the manifest data to remove any non-serializable values
|
// Clean the manifest data to remove any non-serializable values
|
||||||
const cleanManifestData = structuredClone(manifestData);
|
const cleanManifestData = structuredClone(manifest);
|
||||||
|
|
||||||
const yamlContent = yaml.stringify(cleanManifestData, {
|
const yamlContent = yaml.stringify(cleanManifestData, {
|
||||||
indent: 2,
|
indent: 2,
|
||||||
|
|
@ -115,16 +212,61 @@ class Manifest {
|
||||||
const content = yamlContent.endsWith('\n') ? yamlContent : yamlContent + '\n';
|
const content = yamlContent.endsWith('\n') ? yamlContent : yamlContent + '\n';
|
||||||
await fs.writeFile(manifestPath, content, 'utf8');
|
await fs.writeFile(manifestPath, content, 'utf8');
|
||||||
|
|
||||||
return manifest;
|
// Return the flattened format for compatibility
|
||||||
|
return this._flattenManifest(manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a module to the manifest
|
* Read raw manifest data without flattening
|
||||||
|
* @param {string} bmadDir - Path to bmad directory
|
||||||
|
* @returns {Object|null} Raw manifest data or null if not found
|
||||||
|
*/
|
||||||
|
async _readRaw(bmadDir) {
|
||||||
|
const yamlPath = path.join(bmadDir, '_config', 'manifest.yaml');
|
||||||
|
const yaml = require('yaml');
|
||||||
|
|
||||||
|
if (await fs.pathExists(yamlPath)) {
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(yamlPath, 'utf8');
|
||||||
|
return yaml.parse(content);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to read YAML manifest:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flatten manifest for backward compatibility
|
||||||
|
* @param {Object} manifest - Raw manifest data
|
||||||
|
* @returns {Object} Flattened manifest
|
||||||
|
*/
|
||||||
|
_flattenManifest(manifest) {
|
||||||
|
const modules = manifest.modules || [];
|
||||||
|
const moduleNames = modules.map((m) => (typeof m === 'string' ? m : m.name));
|
||||||
|
const hasDetailedModules = modules.length > 0 && typeof modules[0] === 'object';
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: manifest.installation?.version,
|
||||||
|
installDate: manifest.installation?.installDate,
|
||||||
|
lastUpdated: manifest.installation?.lastUpdated,
|
||||||
|
modules: moduleNames,
|
||||||
|
modulesDetailed: hasDetailedModules ? modules : null,
|
||||||
|
customModules: manifest.customModules || [],
|
||||||
|
ides: manifest.ides || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a module to the manifest with optional version info
|
||||||
|
* If module already exists, update its version info
|
||||||
* @param {string} bmadDir - Path to bmad directory
|
* @param {string} bmadDir - Path to bmad directory
|
||||||
* @param {string} moduleName - Module name to add
|
* @param {string} moduleName - Module name to add
|
||||||
|
* @param {Object} options - Optional version info
|
||||||
*/
|
*/
|
||||||
async addModule(bmadDir, moduleName) {
|
async addModule(bmadDir, moduleName, options = {}) {
|
||||||
const manifest = await this.read(bmadDir);
|
const manifest = await this._readRaw(bmadDir);
|
||||||
if (!manifest) {
|
if (!manifest) {
|
||||||
throw new Error('No manifest found');
|
throw new Error('No manifest found');
|
||||||
}
|
}
|
||||||
|
|
@ -133,10 +275,33 @@ class Manifest {
|
||||||
manifest.modules = [];
|
manifest.modules = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!manifest.modules.includes(moduleName)) {
|
const existingIndex = manifest.modules.findIndex((m) => m.name === moduleName);
|
||||||
manifest.modules.push(moduleName);
|
|
||||||
await this.update(bmadDir, { modules: manifest.modules });
|
if (existingIndex === -1) {
|
||||||
|
// Module doesn't exist, add it
|
||||||
|
manifest.modules.push({
|
||||||
|
name: moduleName,
|
||||||
|
version: options.version || null,
|
||||||
|
installDate: new Date().toISOString(),
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
source: options.source || 'unknown',
|
||||||
|
npmPackage: options.npmPackage || null,
|
||||||
|
repoUrl: options.repoUrl || null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Module exists, update its version info
|
||||||
|
const existing = manifest.modules[existingIndex];
|
||||||
|
manifest.modules[existingIndex] = {
|
||||||
|
...existing,
|
||||||
|
version: options.version === undefined ? existing.version : options.version,
|
||||||
|
source: options.source || existing.source,
|
||||||
|
npmPackage: options.npmPackage === undefined ? existing.npmPackage : options.npmPackage,
|
||||||
|
repoUrl: options.repoUrl === undefined ? existing.repoUrl : options.repoUrl,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this._writeRaw(bmadDir, manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -145,18 +310,93 @@ class Manifest {
|
||||||
* @param {string} moduleName - Module name to remove
|
* @param {string} moduleName - Module name to remove
|
||||||
*/
|
*/
|
||||||
async removeModule(bmadDir, moduleName) {
|
async removeModule(bmadDir, moduleName) {
|
||||||
const manifest = await this.read(bmadDir);
|
const manifest = await this._readRaw(bmadDir);
|
||||||
if (!manifest || !manifest.modules) {
|
if (!manifest || !manifest.modules) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = manifest.modules.indexOf(moduleName);
|
const index = manifest.modules.findIndex((m) => m.name === moduleName);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
manifest.modules.splice(index, 1);
|
manifest.modules.splice(index, 1);
|
||||||
await this.update(bmadDir, { modules: manifest.modules });
|
await this._writeRaw(bmadDir, manifest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a single module's version info
|
||||||
|
* @param {string} bmadDir - Path to bmad directory
|
||||||
|
* @param {string} moduleName - Module name
|
||||||
|
* @param {Object} versionInfo - Version info to update
|
||||||
|
*/
|
||||||
|
async updateModuleVersion(bmadDir, moduleName, versionInfo) {
|
||||||
|
const manifest = await this._readRaw(bmadDir);
|
||||||
|
if (!manifest || !manifest.modules) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = manifest.modules.findIndex((m) => m.name === moduleName);
|
||||||
|
if (index !== -1) {
|
||||||
|
manifest.modules[index] = {
|
||||||
|
...manifest.modules[index],
|
||||||
|
...versionInfo,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
await this._writeRaw(bmadDir, manifest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get version info for a specific module
|
||||||
|
* @param {string} bmadDir - Path to bmad directory
|
||||||
|
* @param {string} moduleName - Module name
|
||||||
|
* @returns {Object|null} Module version info or null
|
||||||
|
*/
|
||||||
|
async getModuleVersion(bmadDir, moduleName) {
|
||||||
|
const manifest = await this._readRaw(bmadDir);
|
||||||
|
if (!manifest || !manifest.modules) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return manifest.modules.find((m) => m.name === moduleName) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all modules with their version info
|
||||||
|
* @param {string} bmadDir - Path to bmad directory
|
||||||
|
* @returns {Array} Array of module info objects
|
||||||
|
*/
|
||||||
|
async getAllModuleVersions(bmadDir) {
|
||||||
|
const manifest = await this._readRaw(bmadDir);
|
||||||
|
if (!manifest || !manifest.modules) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return manifest.modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write raw manifest data to file
|
||||||
|
* @param {string} bmadDir - Path to bmad directory
|
||||||
|
* @param {Object} manifestData - Raw manifest data to write
|
||||||
|
*/
|
||||||
|
async _writeRaw(bmadDir, manifestData) {
|
||||||
|
const yaml = require('yaml');
|
||||||
|
const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
|
||||||
|
|
||||||
|
await fs.ensureDir(path.dirname(manifestPath));
|
||||||
|
|
||||||
|
const cleanManifestData = structuredClone(manifestData);
|
||||||
|
|
||||||
|
const yamlContent = yaml.stringify(cleanManifestData, {
|
||||||
|
indent: 2,
|
||||||
|
lineWidth: 0,
|
||||||
|
sortKeys: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = yamlContent.endsWith('\n') ? yamlContent : yamlContent + '\n';
|
||||||
|
await fs.writeFile(manifestPath, content, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an IDE configuration to the manifest
|
* Add an IDE configuration to the manifest
|
||||||
* @param {string} bmadDir - Path to bmad directory
|
* @param {string} bmadDir - Path to bmad directory
|
||||||
|
|
@ -585,6 +825,212 @@ class Manifest {
|
||||||
await this.update(bmadDir, { customModules: manifest.customModules });
|
await this.update(bmadDir, { customModules: manifest.customModules });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get module version info from source
|
||||||
|
* @param {string} moduleName - Module name/code
|
||||||
|
* @param {string} bmadDir - Path to bmad directory
|
||||||
|
* @param {string} moduleSourcePath - Optional source path for custom modules
|
||||||
|
* @returns {Object} Version info object with version, source, npmPackage, repoUrl
|
||||||
|
*/
|
||||||
|
async getModuleVersionInfo(moduleName, bmadDir, moduleSourcePath = null) {
|
||||||
|
const os = require('node:os');
|
||||||
|
|
||||||
|
// Built-in modules use BMad version (only core and bmm are in BMAD-METHOD repo)
|
||||||
|
if (['core', 'bmm'].includes(moduleName)) {
|
||||||
|
const bmadVersion = require(path.join(getProjectRoot(), 'package.json')).version;
|
||||||
|
return {
|
||||||
|
version: bmadVersion,
|
||||||
|
source: 'built-in',
|
||||||
|
npmPackage: null,
|
||||||
|
repoUrl: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is an external official module
|
||||||
|
const { ExternalModuleManager } = require('../modules/external-manager');
|
||||||
|
const extMgr = new ExternalModuleManager();
|
||||||
|
const moduleInfo = await extMgr.getModuleByCode(moduleName);
|
||||||
|
|
||||||
|
if (moduleInfo) {
|
||||||
|
// External module - try to get version from npm registry first, then fall back to cache
|
||||||
|
let version = null;
|
||||||
|
|
||||||
|
if (moduleInfo.npmPackage) {
|
||||||
|
// Fetch version from npm registry
|
||||||
|
try {
|
||||||
|
version = await this.fetchNpmVersion(moduleInfo.npmPackage);
|
||||||
|
} catch {
|
||||||
|
// npm fetch failed, try cache as fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If npm didn't work, try reading from cached repo's package.json
|
||||||
|
if (!version) {
|
||||||
|
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleName);
|
||||||
|
const packageJsonPath = path.join(cacheDir, 'package.json');
|
||||||
|
|
||||||
|
if (await fs.pathExists(packageJsonPath)) {
|
||||||
|
try {
|
||||||
|
const pkg = require(packageJsonPath);
|
||||||
|
version = pkg.version;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to read package.json for ${moduleName}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: version,
|
||||||
|
source: 'external',
|
||||||
|
npmPackage: moduleInfo.npmPackage || null,
|
||||||
|
repoUrl: moduleInfo.url || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom module - check cache directory
|
||||||
|
const cacheDir = path.join(bmadDir, '_config', 'custom', moduleName);
|
||||||
|
const moduleYamlPath = path.join(cacheDir, 'module.yaml');
|
||||||
|
|
||||||
|
if (await fs.pathExists(moduleYamlPath)) {
|
||||||
|
try {
|
||||||
|
const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
|
||||||
|
const moduleConfig = yaml.parse(yamlContent);
|
||||||
|
return {
|
||||||
|
version: moduleConfig.version || null,
|
||||||
|
source: 'custom',
|
||||||
|
npmPackage: moduleConfig.npmPackage || null,
|
||||||
|
repoUrl: moduleConfig.repoUrl || null,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to read module.yaml for ${moduleName}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown module
|
||||||
|
return {
|
||||||
|
version: null,
|
||||||
|
source: 'unknown',
|
||||||
|
npmPackage: null,
|
||||||
|
repoUrl: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch latest version from npm for a package
|
||||||
|
* @param {string} packageName - npm package name
|
||||||
|
* @returns {string|null} Latest version or null
|
||||||
|
*/
|
||||||
|
async fetchNpmVersion(packageName) {
|
||||||
|
try {
|
||||||
|
const https = require('node:https');
|
||||||
|
const { execSync } = require('node:child_process');
|
||||||
|
|
||||||
|
// Try using npm view first (more reliable)
|
||||||
|
try {
|
||||||
|
const result = execSync(`npm view ${packageName} version`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
timeout: 10_000,
|
||||||
|
});
|
||||||
|
return result.trim();
|
||||||
|
} catch {
|
||||||
|
// Fallback to npm registry API
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
https
|
||||||
|
.get(`https://registry.npmjs.org/${packageName}`, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => (data += chunk));
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const pkg = JSON.parse(data);
|
||||||
|
resolve(pkg['dist-tags']?.latest || pkg.version || null);
|
||||||
|
} catch {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on('error', () => resolve(null));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for available updates for installed modules
|
||||||
|
* @param {string} bmadDir - Path to bmad directory
|
||||||
|
* @returns {Array} Array of update info objects
|
||||||
|
*/
|
||||||
|
async checkForUpdates(bmadDir) {
|
||||||
|
const modules = await this.getAllModuleVersions(bmadDir);
|
||||||
|
const updates = [];
|
||||||
|
|
||||||
|
for (const module of modules) {
|
||||||
|
if (!module.npmPackage) {
|
||||||
|
continue; // Skip modules without npm package (built-in)
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestVersion = await this.fetchNpmVersion(module.npmPackage);
|
||||||
|
if (!latestVersion) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module.version !== latestVersion) {
|
||||||
|
updates.push({
|
||||||
|
name: module.name,
|
||||||
|
installedVersion: module.version,
|
||||||
|
latestVersion: latestVersion,
|
||||||
|
npmPackage: module.npmPackage,
|
||||||
|
updateAvailable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two semantic versions
|
||||||
|
* @param {string} v1 - First version
|
||||||
|
* @param {string} v2 - Second version
|
||||||
|
* @returns {number} -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
|
||||||
|
*/
|
||||||
|
compareVersions(v1, v2) {
|
||||||
|
if (!v1 || !v2) return 0;
|
||||||
|
|
||||||
|
const normalize = (v) => {
|
||||||
|
// Remove leading 'v' if present
|
||||||
|
v = v.replace(/^v/, '');
|
||||||
|
// Handle prerelease tags
|
||||||
|
const parts = v.split('-');
|
||||||
|
const main = parts[0].split('.');
|
||||||
|
const prerelease = parts[1];
|
||||||
|
return { main, prerelease };
|
||||||
|
};
|
||||||
|
|
||||||
|
const n1 = normalize(v1);
|
||||||
|
const n2 = normalize(v2);
|
||||||
|
|
||||||
|
// Compare main version parts
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const num1 = parseInt(n1.main[i] || '0', 10);
|
||||||
|
const num2 = parseInt(n2.main[i] || '0', 10);
|
||||||
|
if (num1 !== num2) {
|
||||||
|
return num1 < num2 ? -1 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If main versions are equal, compare prerelease
|
||||||
|
if (n1.prerelease && n2.prerelease) {
|
||||||
|
return n1.prerelease < n2.prerelease ? -1 : n1.prerelease > n2.prerelease ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (n1.prerelease) return -1; // Prerelease is older than stable
|
||||||
|
if (n2.prerelease) return 1; // Stable is newer than prerelease
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { Manifest };
|
module.exports = { Manifest };
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ class ExternalModuleManager {
|
||||||
description: moduleConfig.description || '',
|
description: moduleConfig.description || '',
|
||||||
defaultSelected: moduleConfig.defaultSelected === true,
|
defaultSelected: moduleConfig.defaultSelected === true,
|
||||||
type: moduleConfig.type || 'community', // bmad-org or community
|
type: moduleConfig.type || 'community', // bmad-org or community
|
||||||
|
npmPackage: moduleConfig.npmPackage || null, // Include npm package name
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +96,7 @@ class ExternalModuleManager {
|
||||||
description: moduleConfig.description || '',
|
description: moduleConfig.description || '',
|
||||||
defaultSelected: moduleConfig.defaultSelected === true,
|
defaultSelected: moduleConfig.defaultSelected === true,
|
||||||
type: moduleConfig.type || 'community', // bmad-org or community
|
type: moduleConfig.type || 'community', // bmad-org or community
|
||||||
|
npmPackage: moduleConfig.npmPackage || null, // Include npm package name
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -371,9 +371,9 @@ class ModuleManager {
|
||||||
const fetchSpinner = ora(`Fetching ${moduleInfo.name}...`).start();
|
const fetchSpinner = ora(`Fetching ${moduleInfo.name}...`).start();
|
||||||
try {
|
try {
|
||||||
const currentRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
|
const currentRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
|
||||||
execSync('git fetch --depth 1', { cwd: moduleCacheDir, stdio: 'pipe' });
|
// Fetch and reset to remote - works better with shallow clones than pull
|
||||||
execSync('git checkout -f', { cwd: moduleCacheDir, stdio: 'pipe' });
|
execSync('git fetch origin --depth 1', { cwd: moduleCacheDir, stdio: 'pipe' });
|
||||||
execSync('git pull --ff-only', { cwd: moduleCacheDir, stdio: 'pipe' });
|
execSync('git reset --hard origin/HEAD', { cwd: moduleCacheDir, stdio: 'pipe' });
|
||||||
const newRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
|
const newRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
|
||||||
|
|
||||||
fetchSpinner.succeed(`Fetched ${moduleInfo.name}`);
|
fetchSpinner.succeed(`Fetched ${moduleInfo.name}`);
|
||||||
|
|
@ -555,10 +555,23 @@ class ModuleManager {
|
||||||
await this.runModuleInstaller(moduleName, bmadDir, options);
|
await this.runModuleInstaller(moduleName, bmadDir, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capture version info for manifest
|
||||||
|
const { Manifest } = require('../core/manifest');
|
||||||
|
const manifestObj = new Manifest();
|
||||||
|
const versionInfo = await manifestObj.getModuleVersionInfo(moduleName, bmadDir, sourcePath);
|
||||||
|
|
||||||
|
await manifestObj.addModule(bmadDir, moduleName, {
|
||||||
|
version: versionInfo.version,
|
||||||
|
source: versionInfo.source,
|
||||||
|
npmPackage: versionInfo.npmPackage,
|
||||||
|
repoUrl: versionInfo.repoUrl,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
path: targetPath,
|
path: targetPath,
|
||||||
|
versionInfo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1586,6 +1586,131 @@ class UI {
|
||||||
|
|
||||||
return proceed === 'proceed';
|
return proceed === 'proceed';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display module versions with update availability
|
||||||
|
* @param {Array} modules - Array of module info objects with version info
|
||||||
|
* @param {Array} availableUpdates - Array of available updates
|
||||||
|
*/
|
||||||
|
displayModuleVersions(modules, availableUpdates = []) {
|
||||||
|
console.log('');
|
||||||
|
console.log(chalk.cyan.bold('📦 Module Versions'));
|
||||||
|
console.log(chalk.gray('─'.repeat(80)));
|
||||||
|
|
||||||
|
// Group modules by source
|
||||||
|
const builtIn = modules.filter((m) => m.source === 'built-in');
|
||||||
|
const external = modules.filter((m) => m.source === 'external');
|
||||||
|
const custom = modules.filter((m) => m.source === 'custom');
|
||||||
|
const unknown = modules.filter((m) => m.source === 'unknown');
|
||||||
|
|
||||||
|
const displayGroup = (group, title) => {
|
||||||
|
if (group.length === 0) return;
|
||||||
|
|
||||||
|
console.log(chalk.yellow(`\n${title}`));
|
||||||
|
for (const module of group) {
|
||||||
|
const updateInfo = availableUpdates.find((u) => u.name === module.name);
|
||||||
|
const versionDisplay = module.version || chalk.gray('unknown');
|
||||||
|
|
||||||
|
if (updateInfo) {
|
||||||
|
console.log(
|
||||||
|
` ${chalk.cyan(module.name.padEnd(20))} ${versionDisplay} → ${chalk.green(updateInfo.latestVersion)} ${chalk.green('↑')}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(` ${chalk.cyan(module.name.padEnd(20))} ${versionDisplay} ${chalk.gray('✓')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
displayGroup(builtIn, 'Built-in Modules');
|
||||||
|
displayGroup(external, 'External Modules (Official)');
|
||||||
|
displayGroup(custom, 'Custom Modules');
|
||||||
|
displayGroup(unknown, 'Other Modules');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt user to select which modules to update
|
||||||
|
* @param {Array} availableUpdates - Array of available updates
|
||||||
|
* @returns {Array} Selected module names to update
|
||||||
|
*/
|
||||||
|
async promptUpdateSelection(availableUpdates) {
|
||||||
|
if (availableUpdates.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log(chalk.cyan.bold('🔄 Available Updates'));
|
||||||
|
console.log(chalk.gray('─'.repeat(80)));
|
||||||
|
|
||||||
|
const choices = availableUpdates.map((update) => ({
|
||||||
|
name: `${update.name} ${chalk.dim(`(v${update.installedVersion} → v${update.latestVersion})`)}`,
|
||||||
|
value: update.name,
|
||||||
|
checked: true, // Default to selecting all updates
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Add "Update All" and "Cancel" options
|
||||||
|
const action = await prompts.select({
|
||||||
|
message: 'How would you like to proceed?',
|
||||||
|
choices: [
|
||||||
|
{ name: 'Update all available modules', value: 'all' },
|
||||||
|
{ name: 'Select specific modules to update', value: 'select' },
|
||||||
|
{ name: 'Skip updates for now', value: 'skip' },
|
||||||
|
],
|
||||||
|
default: 'all',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (action === 'all') {
|
||||||
|
return availableUpdates.map((u) => u.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'skip') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow specific selection
|
||||||
|
const selected = await prompts.multiselect({
|
||||||
|
message: `Select modules to update ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
|
||||||
|
choices: choices,
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return selected || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display status of all installed modules
|
||||||
|
* @param {Object} statusData - Status data with modules, installation info, and available updates
|
||||||
|
*/
|
||||||
|
displayStatus(statusData) {
|
||||||
|
const { installation, modules, availableUpdates, bmadDir } = statusData;
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log(chalk.cyan.bold('📋 BMAD Status'));
|
||||||
|
console.log(chalk.gray('─'.repeat(80)));
|
||||||
|
|
||||||
|
// Installation info
|
||||||
|
console.log(chalk.yellow('\nInstallation'));
|
||||||
|
console.log(` ${chalk.gray('Version:'.padEnd(20))} ${installation.version || chalk.gray('unknown')}`);
|
||||||
|
console.log(` ${chalk.gray('Location:'.padEnd(20))} ${bmadDir}`);
|
||||||
|
console.log(` ${chalk.gray('Installed:'.padEnd(20))} ${new Date(installation.installDate).toLocaleDateString()}`);
|
||||||
|
console.log(
|
||||||
|
` ${chalk.gray('Last Updated:'.padEnd(20))} ${installation.lastUpdated ? new Date(installation.lastUpdated).toLocaleDateString() : chalk.gray('unknown')}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Module versions
|
||||||
|
this.displayModuleVersions(modules, availableUpdates);
|
||||||
|
|
||||||
|
// Update summary
|
||||||
|
if (availableUpdates.length > 0) {
|
||||||
|
console.log(chalk.yellow.bold(`\n⚠️ ${availableUpdates.length} update(s) available`));
|
||||||
|
console.log(chalk.dim(` Run 'bmad install' and select "Quick Update" to update`));
|
||||||
|
} else {
|
||||||
|
console.log(chalk.green.bold('\n✓ All modules are up to date'));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { UI };
|
module.exports = { UI };
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue