refactor(installer): replace Detector class with ExistingInstall value object
Remove all legacy/v4 detection and migration code. Replace stateless Detector class with immutable ExistingInstall that exposes a static detect() factory and precomputed query properties (moduleIds, ides). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
232fba8cc8
commit
3e54815961
|
|
@ -63,8 +63,8 @@ module.exports = {
|
|||
|
||||
const existingInstall = await installer.getStatus(projectDir);
|
||||
const version = existingInstall.version || 'unknown';
|
||||
const modules = (existingInstall.modules || []).map((m) => m.id || m.name).join(', ');
|
||||
const ides = (existingInstall.ides || []).join(', ');
|
||||
const modules = existingInstall.moduleIds.join(', ');
|
||||
const ides = existingInstall.ides.join(', ');
|
||||
|
||||
const outputFolder = await installer.getOutputFolder(projectDir);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,223 +0,0 @@
|
|||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const yaml = require('yaml');
|
||||
const { Manifest } = require('./manifest');
|
||||
|
||||
class Detector {
|
||||
/**
|
||||
* Detect existing BMAD installation
|
||||
* @param {string} bmadDir - Path to bmad directory
|
||||
* @returns {Object} Installation status and details
|
||||
*/
|
||||
async detect(bmadDir) {
|
||||
const result = {
|
||||
installed: false,
|
||||
path: bmadDir,
|
||||
version: null,
|
||||
hasCore: false,
|
||||
modules: [],
|
||||
ides: [],
|
||||
customModules: [],
|
||||
manifest: null,
|
||||
};
|
||||
|
||||
// Check if bmad directory exists
|
||||
if (!(await fs.pathExists(bmadDir))) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check for manifest using the Manifest class
|
||||
const manifest = new Manifest();
|
||||
const manifestData = await manifest.read(bmadDir);
|
||||
if (manifestData) {
|
||||
result.manifest = manifestData;
|
||||
result.version = manifestData.version;
|
||||
result.installed = true;
|
||||
// Copy custom modules if they exist
|
||||
if (manifestData.customModules) {
|
||||
result.customModules = manifestData.customModules;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for core
|
||||
const corePath = path.join(bmadDir, 'core');
|
||||
if (await fs.pathExists(corePath)) {
|
||||
result.hasCore = true;
|
||||
|
||||
// Try to get core version from config
|
||||
const coreConfigPath = path.join(corePath, 'config.yaml');
|
||||
if (await fs.pathExists(coreConfigPath)) {
|
||||
try {
|
||||
const configContent = await fs.readFile(coreConfigPath, 'utf8');
|
||||
const config = yaml.parse(configContent);
|
||||
if (!result.version && config.version) {
|
||||
result.version = config.version;
|
||||
}
|
||||
} catch {
|
||||
// Ignore config read errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for modules
|
||||
// If manifest exists, use it as the source of truth for installed modules
|
||||
// Otherwise fall back to directory scanning (legacy installations)
|
||||
if (manifestData && manifestData.modules && manifestData.modules.length > 0) {
|
||||
// Use manifest module list - these are officially installed modules
|
||||
for (const moduleId of manifestData.modules) {
|
||||
const modulePath = path.join(bmadDir, moduleId);
|
||||
const moduleConfigPath = path.join(modulePath, 'config.yaml');
|
||||
|
||||
const moduleInfo = {
|
||||
id: moduleId,
|
||||
path: modulePath,
|
||||
version: 'unknown',
|
||||
};
|
||||
|
||||
if (await fs.pathExists(moduleConfigPath)) {
|
||||
try {
|
||||
const configContent = await fs.readFile(moduleConfigPath, 'utf8');
|
||||
const config = yaml.parse(configContent);
|
||||
moduleInfo.version = config.version || 'unknown';
|
||||
moduleInfo.name = config.name || moduleId;
|
||||
moduleInfo.description = config.description;
|
||||
} catch {
|
||||
// Ignore config read errors
|
||||
}
|
||||
}
|
||||
|
||||
result.modules.push(moduleInfo);
|
||||
}
|
||||
} else {
|
||||
// Fallback: scan directory for modules (legacy installations without manifest)
|
||||
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_config') {
|
||||
const modulePath = path.join(bmadDir, entry.name);
|
||||
const moduleConfigPath = path.join(modulePath, 'config.yaml');
|
||||
|
||||
// Only treat it as a module if it has a config.yaml
|
||||
if (await fs.pathExists(moduleConfigPath)) {
|
||||
const moduleInfo = {
|
||||
id: entry.name,
|
||||
path: modulePath,
|
||||
version: 'unknown',
|
||||
};
|
||||
|
||||
try {
|
||||
const configContent = await fs.readFile(moduleConfigPath, 'utf8');
|
||||
const config = yaml.parse(configContent);
|
||||
moduleInfo.version = config.version || 'unknown';
|
||||
moduleInfo.name = config.name || entry.name;
|
||||
moduleInfo.description = config.description;
|
||||
} catch {
|
||||
// Ignore config read errors
|
||||
}
|
||||
|
||||
result.modules.push(moduleInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for IDE configurations from manifest
|
||||
if (result.manifest && result.manifest.ides) {
|
||||
// Filter out any undefined/null values
|
||||
result.ides = result.manifest.ides.filter((ide) => ide && typeof ide === 'string');
|
||||
}
|
||||
|
||||
// Mark as installed if we found core or modules
|
||||
if (result.hasCore || result.modules.length > 0) {
|
||||
result.installed = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect legacy installation (_bmad-method, .bmm, .cis)
|
||||
* @param {string} projectDir - Project directory to check
|
||||
* @returns {Object} Legacy installation details
|
||||
*/
|
||||
async detectLegacy(projectDir) {
|
||||
const result = {
|
||||
hasLegacy: false,
|
||||
legacyCore: false,
|
||||
legacyModules: [],
|
||||
paths: [],
|
||||
};
|
||||
|
||||
// Check for legacy core (_bmad-method)
|
||||
const legacyCorePath = path.join(projectDir, '_bmad-method');
|
||||
if (await fs.pathExists(legacyCorePath)) {
|
||||
result.hasLegacy = true;
|
||||
result.legacyCore = true;
|
||||
result.paths.push(legacyCorePath);
|
||||
}
|
||||
|
||||
// Check for legacy modules (directories starting with .)
|
||||
const entries = await fs.readdir(projectDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (
|
||||
entry.isDirectory() &&
|
||||
entry.name.startsWith('.') &&
|
||||
entry.name !== '_bmad-method' &&
|
||||
!entry.name.startsWith('.git') &&
|
||||
!entry.name.startsWith('.vscode') &&
|
||||
!entry.name.startsWith('.idea')
|
||||
) {
|
||||
const modulePath = path.join(projectDir, entry.name);
|
||||
const moduleManifestPath = path.join(modulePath, 'install-manifest.yaml');
|
||||
|
||||
// Check if it's likely a BMAD module
|
||||
if ((await fs.pathExists(moduleManifestPath)) || (await fs.pathExists(path.join(modulePath, 'config.yaml')))) {
|
||||
result.hasLegacy = true;
|
||||
result.legacyModules.push({
|
||||
name: entry.name.slice(1), // Remove leading dot
|
||||
path: modulePath,
|
||||
});
|
||||
result.paths.push(modulePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if migration from legacy is needed
|
||||
* @param {string} projectDir - Project directory
|
||||
* @returns {Object} Migration requirements
|
||||
*/
|
||||
async checkMigrationNeeded(projectDir) {
|
||||
const bmadDir = path.join(projectDir, 'bmad');
|
||||
const current = await this.detect(bmadDir);
|
||||
const legacy = await this.detectLegacy(projectDir);
|
||||
|
||||
return {
|
||||
needed: legacy.hasLegacy && !current.installed,
|
||||
canMigrate: legacy.hasLegacy,
|
||||
legacy: legacy,
|
||||
current: current,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect legacy BMAD v4 .bmad-method folder
|
||||
* @param {string} projectDir - Project directory to check
|
||||
* @returns {{ hasLegacyV4: boolean, offenders: string[] }}
|
||||
*/
|
||||
async detectLegacyV4(projectDir) {
|
||||
const offenders = [];
|
||||
|
||||
// Check for .bmad-method folder
|
||||
const bmadMethodPath = path.join(projectDir, '.bmad-method');
|
||||
if (await fs.pathExists(bmadMethodPath)) {
|
||||
offenders.push(bmadMethodPath);
|
||||
}
|
||||
|
||||
return { hasLegacyV4: offenders.length > 0, offenders };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Detector };
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const yaml = require('yaml');
|
||||
const { Manifest } = require('./manifest');
|
||||
|
||||
/**
|
||||
* Immutable snapshot of an existing BMAD installation.
|
||||
* Pure query object — no filesystem operations after construction.
|
||||
*/
|
||||
class ExistingInstall {
|
||||
#version;
|
||||
|
||||
constructor({ installed, version, hasCore, modules, ides, customModules }) {
|
||||
this.installed = installed;
|
||||
this.#version = version;
|
||||
this.hasCore = hasCore;
|
||||
this.modules = Object.freeze(modules.map((m) => Object.freeze({ ...m })));
|
||||
this.moduleIds = Object.freeze(this.modules.map((m) => m.id));
|
||||
this.ides = Object.freeze([...ides]);
|
||||
this.customModules = Object.freeze([...customModules]);
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
get version() {
|
||||
if (!this.installed) {
|
||||
throw new Error('version is not available when nothing is installed');
|
||||
}
|
||||
return this.#version;
|
||||
}
|
||||
|
||||
static empty() {
|
||||
return new ExistingInstall({
|
||||
installed: false,
|
||||
version: null,
|
||||
hasCore: false,
|
||||
modules: [],
|
||||
ides: [],
|
||||
customModules: [],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a bmad directory and return an immutable snapshot of what's installed.
|
||||
* @param {string} bmadDir - Path to bmad directory
|
||||
* @returns {Promise<ExistingInstall>}
|
||||
*/
|
||||
static async detect(bmadDir) {
|
||||
if (!(await fs.pathExists(bmadDir))) {
|
||||
return ExistingInstall.empty();
|
||||
}
|
||||
|
||||
let version = null;
|
||||
let hasCore = false;
|
||||
const modules = [];
|
||||
let ides = [];
|
||||
let customModules = [];
|
||||
|
||||
const manifest = new Manifest();
|
||||
const manifestData = await manifest.read(bmadDir);
|
||||
if (manifestData) {
|
||||
version = manifestData.version;
|
||||
if (manifestData.customModules) {
|
||||
customModules = manifestData.customModules;
|
||||
}
|
||||
if (manifestData.ides) {
|
||||
ides = manifestData.ides.filter((ide) => ide && typeof ide === 'string');
|
||||
}
|
||||
}
|
||||
|
||||
const corePath = path.join(bmadDir, 'core');
|
||||
if (await fs.pathExists(corePath)) {
|
||||
hasCore = true;
|
||||
|
||||
if (!version) {
|
||||
const coreConfigPath = path.join(corePath, 'config.yaml');
|
||||
if (await fs.pathExists(coreConfigPath)) {
|
||||
try {
|
||||
const configContent = await fs.readFile(coreConfigPath, 'utf8');
|
||||
const config = yaml.parse(configContent);
|
||||
if (config.version) {
|
||||
version = config.version;
|
||||
}
|
||||
} catch {
|
||||
// Ignore config read errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (manifestData && manifestData.modules && manifestData.modules.length > 0) {
|
||||
for (const moduleId of manifestData.modules) {
|
||||
const modulePath = path.join(bmadDir, moduleId);
|
||||
const moduleConfigPath = path.join(modulePath, 'config.yaml');
|
||||
|
||||
const moduleInfo = {
|
||||
id: moduleId,
|
||||
path: modulePath,
|
||||
version: 'unknown',
|
||||
};
|
||||
|
||||
if (await fs.pathExists(moduleConfigPath)) {
|
||||
try {
|
||||
const configContent = await fs.readFile(moduleConfigPath, 'utf8');
|
||||
const config = yaml.parse(configContent);
|
||||
moduleInfo.version = config.version || 'unknown';
|
||||
moduleInfo.name = config.name || moduleId;
|
||||
moduleInfo.description = config.description;
|
||||
} catch {
|
||||
// Ignore config read errors
|
||||
}
|
||||
}
|
||||
|
||||
modules.push(moduleInfo);
|
||||
}
|
||||
}
|
||||
|
||||
const installed = hasCore || modules.length > 0 || !!manifestData;
|
||||
|
||||
if (!installed) {
|
||||
return ExistingInstall.empty();
|
||||
}
|
||||
|
||||
return new ExistingInstall({ installed, version, hasCore, modules, ides, customModules });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ExistingInstall };
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const { Detector } = require('./detector');
|
||||
const { Manifest } = require('./manifest');
|
||||
const { OfficialModules } = require('../modules/official-modules');
|
||||
const { CustomModules } = require('../modules/custom-modules');
|
||||
const { IdeManager } = require('../ide/manager');
|
||||
const { FileOps } = require('../../../lib/file-ops');
|
||||
const { Config } = require('../../../lib/config');
|
||||
const { Config } = require('./config');
|
||||
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||
const { ManifestGenerator } = require('./manifest-generator');
|
||||
const { IdeConfigManager } = require('./ide-config-manager');
|
||||
|
|
@ -16,16 +15,15 @@ const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
|
|||
const { InstallPaths } = require('./install-paths');
|
||||
const { ExternalModuleManager } = require('../modules/external-manager');
|
||||
|
||||
const { ExistingInstall } = require('./existing-install');
|
||||
|
||||
class Installer {
|
||||
constructor() {
|
||||
this.externalModuleManager = new ExternalModuleManager();
|
||||
this.detector = new Detector();
|
||||
this.manifest = new Manifest();
|
||||
this.officialModules = new OfficialModules();
|
||||
this.customModules = new CustomModules();
|
||||
this.ideManager = new IdeManager();
|
||||
this.fileOps = new FileOps();
|
||||
this.config = new Config();
|
||||
this.ideConfigManager = new IdeConfigManager();
|
||||
this.installedFiles = new Set(); // Track all installed files
|
||||
this.bmadFolderName = BMAD_FOLDER_NAME;
|
||||
|
|
@ -39,26 +37,22 @@ class Installer {
|
|||
* @param {string[]} config.ides - IDEs to configure
|
||||
*/
|
||||
async install(originalConfig) {
|
||||
const config = this._buildConfig(originalConfig);
|
||||
|
||||
// Everything else — custom modules, quick-update state, the whole mess
|
||||
const customConfig = { ...originalConfig };
|
||||
|
||||
const paths = await InstallPaths.create(config);
|
||||
|
||||
// Collect configurations for official modules
|
||||
await this.officialModules.collectConfigs(config, paths);
|
||||
|
||||
await this.customModules.discoverPaths(config, paths);
|
||||
|
||||
try {
|
||||
const existingInstall = await this.detector.detect(paths.bmadDir);
|
||||
const config = Config.build(originalConfig);
|
||||
const paths = await InstallPaths.create(config);
|
||||
const officialModules = await OfficialModules.build(config, paths);
|
||||
|
||||
const existingInstall = await ExistingInstall.detect(paths.bmadDir);
|
||||
|
||||
await this.customModules.discoverPaths(config, paths);
|
||||
|
||||
if (existingInstall.installed && !config.force) {
|
||||
if (!config.isQuickUpdate()) {
|
||||
await this._removeDeselectedModules(existingInstall, config, paths);
|
||||
}
|
||||
await this._prepareUpdateState(paths, config, customConfig, existingInstall);
|
||||
await this._prepareUpdateState(paths, config, customConfig, existingInstall, officialModules);
|
||||
}
|
||||
|
||||
const ideConfigurations = await this._loadIdeConfigurations(config, customConfig, paths);
|
||||
|
|
@ -74,8 +68,8 @@ class Installer {
|
|||
|
||||
await this._cacheCustomModules(paths, addResult);
|
||||
|
||||
const { officialModules, allModules } = await this._buildModuleLists(config, customConfig, paths);
|
||||
await this._installAndConfigure(config, customConfig, paths, officialModules, allModules, addResult);
|
||||
const { officialModuleIds, allModules } = await this._buildModuleLists(config, customConfig, paths);
|
||||
await this._installAndConfigure(config, customConfig, paths, officialModuleIds, allModules, addResult, officialModules);
|
||||
|
||||
await this._setupIdes(config, ideConfigurations, allModules, paths, addResult);
|
||||
|
||||
|
|
@ -123,7 +117,7 @@ class Installer {
|
|||
* No confirmation — the user's module selection is the decision.
|
||||
*/
|
||||
async _removeDeselectedModules(existingInstall, config, paths) {
|
||||
const previouslyInstalled = new Set(existingInstall.modules.map((m) => m.id));
|
||||
const previouslyInstalled = new Set(existingInstall.moduleIds);
|
||||
const newlySelected = new Set(config.modules || []);
|
||||
const toRemove = [...previouslyInstalled].filter((m) => !newlySelected.has(m) && m !== 'core');
|
||||
|
||||
|
|
@ -156,10 +150,8 @@ class Installer {
|
|||
const bmadDir = paths.bmadDir;
|
||||
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
|
||||
|
||||
const { Detector } = require('./detector');
|
||||
const detector = new Detector();
|
||||
const existingInstall = await detector.detect(bmadDir);
|
||||
const previouslyConfigured = existingInstall.ides || [];
|
||||
const existingInstall = await ExistingInstall.detect(bmadDir);
|
||||
const previouslyConfigured = existingInstall.ides;
|
||||
|
||||
for (const ide of config.ides || []) {
|
||||
if (previouslyConfigured.includes(ide) && savedIdeConfigs[ide]) {
|
||||
|
|
@ -201,7 +193,7 @@ class Installer {
|
|||
* No confirmation — the user's IDE selection is the decision.
|
||||
*/
|
||||
async _removeDeselectedIdes(existingInstall, config, ideConfigurations, paths) {
|
||||
const previouslyInstalled = new Set(existingInstall.ides || []);
|
||||
const previouslyInstalled = new Set(existingInstall.ides);
|
||||
const newlySelected = new Set(config.ides || []);
|
||||
const toRemove = [...previouslyInstalled].filter((ide) => !newlySelected.has(ide));
|
||||
|
||||
|
|
@ -243,7 +235,7 @@ class Installer {
|
|||
|
||||
/**
|
||||
* Build the official and combined module lists from config and custom sources.
|
||||
* @returns {{ officialModules: string[], allModules: string[] }}
|
||||
* @returns {{ officialModuleIds: string[], allModules: string[] }}
|
||||
*/
|
||||
async _buildModuleLists(config, customConfig, paths) {
|
||||
const finalCustomContent = customConfig.customContent;
|
||||
|
|
@ -274,25 +266,25 @@ class Installer {
|
|||
}
|
||||
}
|
||||
|
||||
const officialModules = (config.modules || []).filter((m) => !customModuleIds.has(m));
|
||||
const officialModuleIds = (config.modules || []).filter((m) => !customModuleIds.has(m));
|
||||
|
||||
const allModules = [...officialModules];
|
||||
const allModules = [...officialModuleIds];
|
||||
for (const id of customModuleIds) {
|
||||
if (!allModules.includes(id)) {
|
||||
allModules.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
return { officialModules, allModules };
|
||||
return { officialModuleIds, allModules };
|
||||
}
|
||||
|
||||
/**
|
||||
* Install modules, create directories, generate configs and manifests.
|
||||
*/
|
||||
async _installAndConfigure(config, customConfig, paths, officialModules, allModules, addResult) {
|
||||
async _installAndConfigure(config, customConfig, paths, officialModuleIds, allModules, addResult, officialModules) {
|
||||
const isQuickUpdate = config.isQuickUpdate();
|
||||
const finalCustomContent = customConfig.customContent;
|
||||
const moduleConfigs = this.officialModules.moduleConfigs;
|
||||
const moduleConfigs = officialModules.moduleConfigs;
|
||||
|
||||
const dirResults = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
|
||||
|
||||
|
|
@ -304,12 +296,12 @@ class Installer {
|
|||
task: async (message) => {
|
||||
const installedModuleNames = new Set();
|
||||
|
||||
await this._installOfficialModules(config, paths, officialModules, addResult, isQuickUpdate, {
|
||||
await this._installOfficialModules(config, paths, officialModuleIds, addResult, isQuickUpdate, officialModules, {
|
||||
message,
|
||||
installedModuleNames,
|
||||
});
|
||||
|
||||
await this._installCustomModules(customConfig, paths, finalCustomContent, addResult, isQuickUpdate, {
|
||||
await this._installCustomModules(customConfig, paths, finalCustomContent, addResult, isQuickUpdate, officialModules, {
|
||||
message,
|
||||
installedModuleNames,
|
||||
});
|
||||
|
|
@ -332,10 +324,10 @@ class Installer {
|
|||
if (config.modules && config.modules.length > 0) {
|
||||
for (const moduleName of config.modules) {
|
||||
message(`Setting up ${moduleName}...`);
|
||||
const result = await this.officialModules.createModuleDirectories(moduleName, paths.bmadDir, {
|
||||
const result = await officialModules.createModuleDirectories(moduleName, paths.bmadDir, {
|
||||
installedIDEs: config.ides || [],
|
||||
moduleConfig: moduleConfigs[moduleName] || {},
|
||||
existingModuleConfig: this.officialModules.existingConfig?.[moduleName] || {},
|
||||
existingModuleConfig: officialModules.existingConfig?.[moduleName] || {},
|
||||
coreConfig: moduleConfigs.core || {},
|
||||
logger: moduleLogger,
|
||||
silent: true,
|
||||
|
|
@ -526,31 +518,6 @@ class Installer {
|
|||
]);
|
||||
}
|
||||
|
||||
_buildConfig(originalConfig) {
|
||||
const modules = [...(originalConfig.modules || [])];
|
||||
if (originalConfig.installCore && !modules.includes('core')) {
|
||||
modules.unshift('core');
|
||||
}
|
||||
|
||||
return {
|
||||
directory: originalConfig.directory,
|
||||
modules,
|
||||
ides: originalConfig.skipIde ? [] : [...(originalConfig.ides || [])],
|
||||
skipPrompts: originalConfig.skipPrompts || false,
|
||||
verbose: originalConfig.verbose || false,
|
||||
force: originalConfig.force || false,
|
||||
actionType: originalConfig.actionType,
|
||||
coreConfig: originalConfig.coreConfig || {},
|
||||
moduleConfigs: originalConfig.moduleConfigs || null,
|
||||
hasCoreConfig() {
|
||||
return this.coreConfig && Object.keys(this.coreConfig).length > 0;
|
||||
},
|
||||
isQuickUpdate() {
|
||||
return originalConfig._quickUpdate || false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan the custom module cache directory and register any cached custom modules
|
||||
* that aren't already known from the manifest or external module list.
|
||||
|
|
@ -600,7 +567,7 @@ class Installer {
|
|||
* @param {Object} customConfig - Full config bag (mutated with update state)
|
||||
* @param {Object} existingInstall - Detection result from detector.detect()
|
||||
*/
|
||||
async _prepareUpdateState(paths, config, customConfig, existingInstall) {
|
||||
async _prepareUpdateState(paths, config, customConfig, existingInstall, officialModules) {
|
||||
customConfig._isUpdate = true;
|
||||
customConfig._existingInstall = existingInstall;
|
||||
|
||||
|
|
@ -622,7 +589,7 @@ class Installer {
|
|||
|
||||
config.coreConfig = existingCoreConfig;
|
||||
customConfig.coreConfig = existingCoreConfig;
|
||||
this.officialModules.moduleConfigs.core = existingCoreConfig;
|
||||
officialModules.moduleConfigs.core = existingCoreConfig;
|
||||
} catch (error) {
|
||||
await prompts.log.warn(`Warning: Could not read existing core config: ${error.message}`);
|
||||
}
|
||||
|
|
@ -678,22 +645,22 @@ class Installer {
|
|||
* Install official (non-custom) modules.
|
||||
* @param {Object} config - Installation configuration
|
||||
* @param {Object} paths - InstallPaths instance
|
||||
* @param {string[]} officialModules - Official module IDs to install
|
||||
* @param {string[]} officialModuleIds - Official module IDs to install
|
||||
* @param {Function} addResult - Callback to record installation results
|
||||
* @param {boolean} isQuickUpdate - Whether this is a quick update
|
||||
* @param {Object} ctx - Shared context: { message, installedModuleNames }
|
||||
*/
|
||||
async _installOfficialModules(config, paths, officialModules, addResult, isQuickUpdate, ctx) {
|
||||
async _installOfficialModules(config, paths, officialModuleIds, addResult, isQuickUpdate, officialModules, ctx) {
|
||||
const { message, installedModuleNames } = ctx;
|
||||
|
||||
for (const moduleName of officialModules) {
|
||||
for (const moduleName of officialModuleIds) {
|
||||
if (installedModuleNames.has(moduleName)) continue;
|
||||
installedModuleNames.add(moduleName);
|
||||
|
||||
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
|
||||
|
||||
const moduleConfig = this.officialModules.moduleConfigs[moduleName] || {};
|
||||
await this.officialModules.install(
|
||||
const moduleConfig = officialModules.moduleConfigs[moduleName] || {};
|
||||
await officialModules.install(
|
||||
moduleName,
|
||||
paths.bmadDir,
|
||||
(filePath) => {
|
||||
|
|
@ -720,7 +687,7 @@ class Installer {
|
|||
* @param {boolean} isQuickUpdate - Whether this is a quick update
|
||||
* @param {Object} ctx - Shared context: { message, installedModuleNames }
|
||||
*/
|
||||
async _installCustomModules(customConfig, paths, finalCustomContent, addResult, isQuickUpdate, ctx) {
|
||||
async _installCustomModules(customConfig, paths, finalCustomContent, addResult, isQuickUpdate, officialModules, ctx) {
|
||||
const { message, installedModuleNames } = ctx;
|
||||
|
||||
// Collect all custom module IDs with their info from all sources
|
||||
|
|
@ -776,8 +743,8 @@ class Installer {
|
|||
this.customModules.paths.set(moduleName, customInfo.path);
|
||||
}
|
||||
|
||||
const collectedModuleConfig = this.officialModules.moduleConfigs[moduleName] || {};
|
||||
await this.officialModules.install(
|
||||
const collectedModuleConfig = officialModules.moduleConfigs[moduleName] || {};
|
||||
await officialModules.install(
|
||||
moduleName,
|
||||
paths.bmadDir,
|
||||
(filePath) => {
|
||||
|
|
@ -834,19 +801,16 @@ class Installer {
|
|||
}
|
||||
|
||||
// Check for already configured IDEs
|
||||
const { Detector } = require('./detector');
|
||||
const detector = new Detector();
|
||||
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
|
||||
|
||||
// During full reinstall, use the saved previous IDEs since bmad dir was deleted
|
||||
// Otherwise detect from existing installation
|
||||
let previouslyConfiguredIdes;
|
||||
if (isFullReinstall) {
|
||||
// During reinstall, treat all IDEs as new (need configuration)
|
||||
previouslyConfiguredIdes = [];
|
||||
} else {
|
||||
const existingInstall = await detector.detect(bmadDir);
|
||||
previouslyConfiguredIdes = existingInstall.ides || [];
|
||||
const existingInstall = await ExistingInstall.detect(bmadDir);
|
||||
previouslyConfiguredIdes = existingInstall.ides;
|
||||
}
|
||||
|
||||
// Load saved IDE configurations for already-configured IDEs
|
||||
|
|
@ -1467,9 +1431,9 @@ class Installer {
|
|||
}
|
||||
|
||||
// Detect existing installation
|
||||
const existingInstall = await this.detector.detect(bmadDir);
|
||||
const installedModules = existingInstall.modules.map((m) => m.id);
|
||||
const configuredIdes = existingInstall.ides || [];
|
||||
const existingInstall = await ExistingInstall.detect(bmadDir);
|
||||
const installedModules = existingInstall.moduleIds;
|
||||
const configuredIdes = existingInstall.ides;
|
||||
const projectRoot = path.dirname(bmadDir);
|
||||
|
||||
// Get custom module sources: first from --custom-content (re-cache from source), then from cache
|
||||
|
|
@ -1532,7 +1496,7 @@ class Installer {
|
|||
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
|
||||
|
||||
// Get available modules (what we have source for)
|
||||
const availableModulesData = await this.officialModules.listAvailable();
|
||||
const availableModulesData = await new OfficialModules().listAvailable();
|
||||
const availableModules = [...availableModulesData.modules, ...availableModulesData.customModules];
|
||||
|
||||
// Add external official modules to available modules
|
||||
|
|
@ -1612,19 +1576,20 @@ class Installer {
|
|||
|
||||
// Load existing configs and collect new fields (if any)
|
||||
await prompts.log.info('Checking for new configuration options...');
|
||||
await this.officialModules.loadExistingConfig(projectDir);
|
||||
const quickModules = new OfficialModules();
|
||||
await quickModules.loadExistingConfig(projectDir);
|
||||
|
||||
let promptedForNewFields = false;
|
||||
|
||||
// Check core config for new fields
|
||||
const corePrompted = await this.officialModules.collectModuleConfigQuick('core', projectDir, true);
|
||||
const corePrompted = await quickModules.collectModuleConfigQuick('core', projectDir, true);
|
||||
if (corePrompted) {
|
||||
promptedForNewFields = true;
|
||||
}
|
||||
|
||||
// Check each module we're updating for new fields (NOT skipped modules)
|
||||
for (const moduleName of modulesToUpdate) {
|
||||
const modulePrompted = await this.officialModules.collectModuleConfigQuick(moduleName, projectDir, true);
|
||||
const modulePrompted = await quickModules.collectModuleConfigQuick(moduleName, projectDir, true);
|
||||
if (modulePrompted) {
|
||||
promptedForNewFields = true;
|
||||
}
|
||||
|
|
@ -1635,7 +1600,7 @@ class Installer {
|
|||
}
|
||||
|
||||
// Add metadata
|
||||
this.officialModules.collectedConfig._meta = {
|
||||
quickModules.collectedConfig._meta = {
|
||||
version: require(path.join(getProjectRoot(), 'package.json')).version,
|
||||
installDate: new Date().toISOString(),
|
||||
lastModified: new Date().toISOString(),
|
||||
|
|
@ -1646,7 +1611,8 @@ class Installer {
|
|||
directory: projectDir,
|
||||
modules: modulesToUpdate, // Only update modules we have source for (includes core)
|
||||
ides: configuredIdes,
|
||||
coreConfig: this.officialModules.collectedConfig.core,
|
||||
coreConfig: quickModules.collectedConfig.core,
|
||||
moduleConfigs: quickModules.collectedConfig, // Pass collected configs so build() picks them up
|
||||
actionType: 'install', // Use regular install flow
|
||||
_quickUpdate: true, // Flag to skip certain prompts
|
||||
_preserveModules: skippedModules, // Preserve these in manifest even though we didn't update them
|
||||
|
|
@ -1679,7 +1645,7 @@ class Installer {
|
|||
try {
|
||||
const projectDir = path.resolve(config.directory);
|
||||
const { bmadDir } = await this.findBmadDir(projectDir);
|
||||
const existingInstall = await this.detector.detect(bmadDir);
|
||||
const existingInstall = await ExistingInstall.detect(bmadDir);
|
||||
|
||||
if (!existingInstall.installed) {
|
||||
throw new Error(`No BMAD installation found at ${bmadDir}`);
|
||||
|
|
@ -1692,11 +1658,8 @@ class Installer {
|
|||
// Check for custom modules with missing sources before update
|
||||
const customModuleSources = new Map();
|
||||
|
||||
// Check manifest for backward compatibility
|
||||
if (existingInstall.customModules) {
|
||||
for (const customModule of existingInstall.customModules) {
|
||||
customModuleSources.set(customModule.id, customModule);
|
||||
}
|
||||
for (const customModule of existingInstall.customModules) {
|
||||
customModuleSources.set(customModule.id, customModule);
|
||||
}
|
||||
|
||||
// Also check cache directory
|
||||
|
|
@ -1745,7 +1708,7 @@ class Installer {
|
|||
bmadDir,
|
||||
projectRoot,
|
||||
'update',
|
||||
existingInstall.modules.map((m) => m.id),
|
||||
existingInstall.moduleIds,
|
||||
config.skipPrompts || false,
|
||||
);
|
||||
}
|
||||
|
|
@ -1770,8 +1733,9 @@ class Installer {
|
|||
await this.updateCore(bmadDir, config.force);
|
||||
}
|
||||
|
||||
const updateModules = new OfficialModules();
|
||||
for (const module of existingInstall.modules) {
|
||||
await this.officialModules.update(module.id, bmadDir, config.force, { installer: this });
|
||||
await updateModules.update(module.id, bmadDir, config.force, { installer: this });
|
||||
}
|
||||
|
||||
// Update manifest
|
||||
|
|
@ -1791,7 +1755,7 @@ class Installer {
|
|||
*/
|
||||
async updateCore(bmadDir, force = false) {
|
||||
if (force) {
|
||||
await this.officialModules.install('core', bmadDir, (filePath) => this.installedFiles.add(filePath), {
|
||||
await new OfficialModules().install('core', bmadDir, (filePath) => this.installedFiles.add(filePath), {
|
||||
skipModuleInstaller: true,
|
||||
silent: true,
|
||||
});
|
||||
|
|
@ -1821,7 +1785,7 @@ class Installer {
|
|||
}
|
||||
|
||||
// 1. DETECT: Read state BEFORE deleting anything
|
||||
const existingInstall = await this.detector.detect(bmadDir);
|
||||
const existingInstall = await ExistingInstall.detect(bmadDir);
|
||||
const outputFolder = await this._readOutputFolder(bmadDir);
|
||||
|
||||
const removed = { modules: false, ideConfigs: false, outputFolder: false };
|
||||
|
|
@ -1855,7 +1819,7 @@ class Installer {
|
|||
async uninstallIdeConfigs(projectDir, existingInstall, options = {}) {
|
||||
await this.ideManager.ensureInitialized();
|
||||
const cleanupOptions = { isUninstall: true, silent: options.silent };
|
||||
const ideList = existingInstall.ides || [];
|
||||
const ideList = existingInstall.ides;
|
||||
if (ideList.length > 0) {
|
||||
return this.ideManager.cleanupByList(projectDir, ideList, cleanupOptions);
|
||||
}
|
||||
|
|
@ -1902,14 +1866,14 @@ class Installer {
|
|||
async getStatus(directory) {
|
||||
const projectDir = path.resolve(directory);
|
||||
const { bmadDir } = await this.findBmadDir(projectDir);
|
||||
return await this.detector.detect(bmadDir);
|
||||
return await ExistingInstall.detect(bmadDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available modules
|
||||
*/
|
||||
async getAvailableModules() {
|
||||
return await this.officialModules.listAvailable();
|
||||
return await new OfficialModules().listAvailable();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1923,48 +1887,6 @@ class Installer {
|
|||
return this._readOutputFolder(bmadDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle legacy BMAD v4 detection with simple warning
|
||||
* @param {string} _projectDir - Project directory (unused in simplified version)
|
||||
* @param {Object} _legacyV4 - Legacy V4 detection result (unused in simplified version)
|
||||
*/
|
||||
async handleLegacyV4Migration(_projectDir, _legacyV4) {
|
||||
await prompts.note(
|
||||
'Found .bmad-method folder from BMAD v4 installation.\n\n' +
|
||||
'Before continuing with installation, we recommend:\n' +
|
||||
' 1. Remove the .bmad-method folder, OR\n' +
|
||||
' 2. Back it up by renaming it to another name (e.g., bmad-method-backup)\n\n' +
|
||||
'If your v4 installation set up rules or commands, you should remove those as well.',
|
||||
'Legacy BMAD v4 detected',
|
||||
);
|
||||
|
||||
const proceed = await prompts.select({
|
||||
message: 'What would you like to do?',
|
||||
choices: [
|
||||
{
|
||||
name: 'Exit and clean up manually (recommended)',
|
||||
value: 'exit',
|
||||
hint: 'Exit installation',
|
||||
},
|
||||
{
|
||||
name: 'Continue with installation anyway',
|
||||
value: 'continue',
|
||||
hint: 'Continue',
|
||||
},
|
||||
],
|
||||
default: 'exit',
|
||||
});
|
||||
|
||||
if (proceed === 'exit') {
|
||||
await prompts.log.info('Please remove the .bmad-method folder and any v4 rules/commands, then run the installer again.');
|
||||
// Allow event loop to flush pending I/O before exit
|
||||
setImmediate(() => process.exit(0));
|
||||
return;
|
||||
}
|
||||
|
||||
await prompts.log.warn('Proceeding with installation despite legacy v4 folder');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle missing custom module sources interactively
|
||||
* @param {Map} customModuleSources - Map of custom module ID to info
|
||||
|
|
@ -2201,29 +2123,12 @@ class Installer {
|
|||
/**
|
||||
* Find the bmad installation directory in a project
|
||||
* Always uses the standard _bmad folder name
|
||||
* Also checks for legacy _cfg folder for migration
|
||||
* @param {string} projectDir - Project directory
|
||||
* @returns {Promise<Object>} { bmadDir: string, hasLegacyCfg: boolean }
|
||||
* @returns {Promise<Object>} { bmadDir: string }
|
||||
*/
|
||||
async findBmadDir(projectDir) {
|
||||
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
|
||||
|
||||
// Check if project directory exists
|
||||
if (!(await fs.pathExists(projectDir))) {
|
||||
// Project doesn't exist yet, return default
|
||||
return { bmadDir, hasLegacyCfg: false };
|
||||
}
|
||||
|
||||
// Check for legacy _cfg folder if bmad directory exists
|
||||
let hasLegacyCfg = false;
|
||||
if (await fs.pathExists(bmadDir)) {
|
||||
const legacyCfgPath = path.join(bmadDir, '_cfg');
|
||||
if (await fs.pathExists(legacyCfgPath)) {
|
||||
hasLegacyCfg = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { bmadDir, hasLegacyCfg };
|
||||
return { bmadDir };
|
||||
}
|
||||
|
||||
async createDirectoryStructure(bmadDir) {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class CustomModules {
|
|||
}
|
||||
|
||||
// From manifest (regular updates)
|
||||
if (config._isUpdate && config._existingInstall && config._existingInstall.customModules) {
|
||||
if (config._isUpdate && config._existingInstall) {
|
||||
for (const customModule of config._existingInstall.customModules) {
|
||||
let absoluteSourcePath = customModule.sourcePath;
|
||||
|
||||
|
|
|
|||
|
|
@ -51,125 +51,11 @@ class UI {
|
|||
confirmedDirectory = await this.getConfirmedDirectory();
|
||||
}
|
||||
|
||||
// Preflight: Check for legacy BMAD v4 footprints immediately after getting directory
|
||||
const { Detector } = require('../installers/lib/core/detector');
|
||||
const { Installer } = require('../installers/lib/core/installer');
|
||||
const detector = new Detector();
|
||||
const installer = new Installer();
|
||||
const legacyV4 = await detector.detectLegacyV4(confirmedDirectory);
|
||||
if (legacyV4.hasLegacyV4) {
|
||||
await installer.handleLegacyV4Migration(confirmedDirectory, legacyV4);
|
||||
}
|
||||
const { bmadDir } = await installer.findBmadDir(confirmedDirectory);
|
||||
|
||||
// Check for legacy folders and prompt for rename before showing any menus
|
||||
let hasLegacyCfg = false;
|
||||
let hasLegacyBmadFolder = false;
|
||||
let bmadDir = null;
|
||||
let legacyBmadPath = null;
|
||||
|
||||
// First check for legacy .bmad folder (instead of _bmad)
|
||||
// Only check if directory exists
|
||||
if (await fs.pathExists(confirmedDirectory)) {
|
||||
const entries = await fs.readdir(confirmedDirectory, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && (entry.name === '.bmad' || entry.name === 'bmad')) {
|
||||
hasLegacyBmadFolder = true;
|
||||
legacyBmadPath = path.join(confirmedDirectory, entry.name);
|
||||
bmadDir = legacyBmadPath;
|
||||
|
||||
// Check if it has _cfg folder
|
||||
const cfgPath = path.join(legacyBmadPath, '_cfg');
|
||||
if (await fs.pathExists(cfgPath)) {
|
||||
hasLegacyCfg = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no .bmad or bmad found, check for current installations _bmad
|
||||
if (!hasLegacyBmadFolder) {
|
||||
const bmadResult = await installer.findBmadDir(confirmedDirectory);
|
||||
bmadDir = bmadResult.bmadDir;
|
||||
hasLegacyCfg = bmadResult.hasLegacyCfg;
|
||||
}
|
||||
|
||||
// Handle legacy .bmad or _cfg folder - these are very old (v4 or alpha)
|
||||
// Show version warning instead of offering conversion
|
||||
if (hasLegacyBmadFolder || hasLegacyCfg) {
|
||||
await prompts.log.warn('LEGACY INSTALLATION DETECTED');
|
||||
await prompts.note(
|
||||
'Found a ".bmad"/"bmad" folder, or a legacy "_cfg" folder under the bmad folder -\n' +
|
||||
'this is from an old BMAD version that is out of date for automatic upgrade,\n' +
|
||||
'manual intervention required.\n\n' +
|
||||
'You have a legacy version installed (v4 or alpha).\n' +
|
||||
'Legacy installations may have compatibility issues.\n\n' +
|
||||
'For the best experience, we strongly recommend:\n' +
|
||||
' 1. Delete your current BMAD installation folder (.bmad or bmad)\n' +
|
||||
' 2. Run a fresh installation\n\n' +
|
||||
'If you do not want to start fresh, you can attempt to proceed beyond this\n' +
|
||||
'point IF you have ensured the bmad folder is named _bmad, and under it there\n' +
|
||||
'is a _config folder. If you have a folder under your bmad folder named _cfg,\n' +
|
||||
'you would need to rename it _config, and then restart the installer.\n\n' +
|
||||
'Benefits of a fresh install:\n' +
|
||||
' \u2022 Cleaner configuration without legacy artifacts\n' +
|
||||
' \u2022 All new features properly configured\n' +
|
||||
' \u2022 Fewer potential conflicts\n\n' +
|
||||
'If you have already produced output from an earlier alpha version, you can\n' +
|
||||
'still retain those artifacts. After installation, ensure you configured during\n' +
|
||||
'install the proper file locations for artifacts depending on the module you\n' +
|
||||
'are using, or move the files to the proper locations.',
|
||||
'Legacy Installation Detected',
|
||||
);
|
||||
|
||||
const proceed = await prompts.select({
|
||||
message: 'How would you like to proceed?',
|
||||
choices: [
|
||||
{
|
||||
name: 'Cancel and do a fresh install (recommended)',
|
||||
value: 'cancel',
|
||||
},
|
||||
{
|
||||
name: 'Proceed anyway (will attempt update, potentially may fail or have unstable behavior)',
|
||||
value: 'proceed',
|
||||
},
|
||||
],
|
||||
default: 'cancel',
|
||||
});
|
||||
|
||||
if (proceed === 'cancel') {
|
||||
await prompts.note('1. Delete the existing bmad folder in your project\n' + "2. Run 'bmad install' again", 'To do a fresh install');
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const s = await prompts.spinner();
|
||||
s.start('Updating folder structure...');
|
||||
try {
|
||||
// Handle .bmad folder
|
||||
if (hasLegacyBmadFolder) {
|
||||
const newBmadPath = path.join(confirmedDirectory, '_bmad');
|
||||
await fs.move(legacyBmadPath, newBmadPath);
|
||||
bmadDir = newBmadPath;
|
||||
s.stop(`Renamed "${path.basename(legacyBmadPath)}" to "_bmad"`);
|
||||
}
|
||||
|
||||
// Handle _cfg folder (either from .bmad or standalone)
|
||||
const cfgPath = path.join(bmadDir, '_cfg');
|
||||
if (await fs.pathExists(cfgPath)) {
|
||||
s.start('Renaming configuration folder...');
|
||||
const newCfgPath = path.join(bmadDir, '_config');
|
||||
await fs.move(cfgPath, newCfgPath);
|
||||
s.stop('Renamed "_cfg" to "_config"');
|
||||
}
|
||||
} catch (error) {
|
||||
s.stop('Failed to update folder structure');
|
||||
await prompts.log.error(`Error: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there's an existing BMAD installation (after any folder renames)
|
||||
// Check if there's an existing BMAD installation
|
||||
const hasExistingInstall = await fs.pathExists(bmadDir);
|
||||
|
||||
let customContentConfig = { hasCustomContent: false };
|
||||
|
|
@ -188,15 +74,6 @@ class UI {
|
|||
const currentVersion = require(packageJsonPath).version;
|
||||
const installedVersion = existingInstall.version || 'unknown';
|
||||
|
||||
// Check if version is pre beta
|
||||
const shouldProceed = await this.showLegacyVersionWarning(installedVersion, currentVersion, path.basename(bmadDir), options);
|
||||
|
||||
// If user chose to cancel, exit the installer
|
||||
if (!shouldProceed) {
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build menu choices dynamically
|
||||
const choices = [];
|
||||
|
||||
|
|
@ -575,15 +452,12 @@ class UI {
|
|||
* @returns {Object} Tool configuration
|
||||
*/
|
||||
async promptToolSelection(projectDir, options = {}) {
|
||||
// Check for existing configured IDEs - use findBmadDir to detect custom folder names
|
||||
const { Detector } = require('../installers/lib/core/detector');
|
||||
const { ExistingInstall } = require('../installers/lib/core/existing-install');
|
||||
const { Installer } = require('../installers/lib/core/installer');
|
||||
const detector = new Detector();
|
||||
const installer = new Installer();
|
||||
const bmadResult = await installer.findBmadDir(projectDir || process.cwd());
|
||||
const bmadDir = bmadResult.bmadDir;
|
||||
const existingInstall = await detector.detect(bmadDir);
|
||||
const configuredIdes = existingInstall.ides || [];
|
||||
const { bmadDir } = await installer.findBmadDir(projectDir || process.cwd());
|
||||
const existingInstall = await ExistingInstall.detect(bmadDir);
|
||||
const configuredIdes = existingInstall.ides;
|
||||
|
||||
// Get IDE manager to fetch available IDEs dynamically
|
||||
const { IdeManager } = require('../installers/lib/ide/manager');
|
||||
|
|
@ -816,14 +690,12 @@ class UI {
|
|||
* @returns {Object} Object with existingInstall, installedModuleIds, and bmadDir
|
||||
*/
|
||||
async getExistingInstallation(directory) {
|
||||
const { Detector } = require('../installers/lib/core/detector');
|
||||
const { ExistingInstall } = require('../installers/lib/core/existing-install');
|
||||
const { Installer } = require('../installers/lib/core/installer');
|
||||
const detector = new Detector();
|
||||
const installer = new Installer();
|
||||
const bmadDirResult = await installer.findBmadDir(directory);
|
||||
const bmadDir = bmadDirResult.bmadDir;
|
||||
const existingInstall = await detector.detect(bmadDir);
|
||||
const installedModuleIds = new Set(existingInstall.modules.map((mod) => mod.id));
|
||||
const { bmadDir } = await installer.findBmadDir(directory);
|
||||
const existingInstall = await ExistingInstall.detect(bmadDir);
|
||||
const installedModuleIds = new Set(existingInstall.moduleIds);
|
||||
|
||||
return { existingInstall, installedModuleIds, bmadDir };
|
||||
}
|
||||
|
|
@ -1393,13 +1265,12 @@ class UI {
|
|||
* @returns {Array} List of configured IDEs
|
||||
*/
|
||||
async getConfiguredIdes(directory) {
|
||||
const { Detector } = require('../installers/lib/core/detector');
|
||||
const { ExistingInstall } = require('../installers/lib/core/existing-install');
|
||||
const { Installer } = require('../installers/lib/core/installer');
|
||||
const detector = new Detector();
|
||||
const installer = new Installer();
|
||||
const bmadResult = await installer.findBmadDir(directory);
|
||||
const existingInstall = await detector.detect(bmadResult.bmadDir);
|
||||
return existingInstall.ides || [];
|
||||
const { bmadDir } = await installer.findBmadDir(directory);
|
||||
const existingInstall = await ExistingInstall.detect(bmadDir);
|
||||
return existingInstall.ides;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1678,82 +1549,6 @@ class UI {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if installed version is a legacy version that needs fresh install
|
||||
* @param {string} installedVersion - The installed version
|
||||
* @returns {boolean} True if legacy (v4 or any alpha)
|
||||
*/
|
||||
isLegacyVersion(installedVersion) {
|
||||
if (!installedVersion || installedVersion === 'unknown') {
|
||||
return true; // Treat unknown as legacy for safety
|
||||
}
|
||||
// Check if version string contains -alpha or -Alpha (any v6 alpha)
|
||||
return /-alpha\./i.test(installedVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show warning for legacy version (v4 or alpha) and ask if user wants to proceed
|
||||
* @param {string} installedVersion - The installed version
|
||||
* @param {string} currentVersion - The current version
|
||||
* @param {string} bmadFolderName - Name of the BMAD folder
|
||||
* @returns {Promise<boolean>} True if user wants to proceed, false if they cancel
|
||||
*/
|
||||
async showLegacyVersionWarning(installedVersion, currentVersion, bmadFolderName, options = {}) {
|
||||
if (!this.isLegacyVersion(installedVersion)) {
|
||||
return true; // Not legacy, proceed
|
||||
}
|
||||
|
||||
let warningContent;
|
||||
if (installedVersion === 'unknown') {
|
||||
warningContent = 'Unable to detect your installed BMAD version.\n' + 'This appears to be a legacy or unsupported installation.';
|
||||
} else {
|
||||
warningContent =
|
||||
`You are updating from ${installedVersion} to ${currentVersion}.\n` + 'You have a legacy version installed (v4 or alpha).';
|
||||
}
|
||||
|
||||
warningContent +=
|
||||
'\n\nFor the best experience, we recommend:\n' +
|
||||
' 1. Delete your current BMAD installation folder\n' +
|
||||
` (the "${bmadFolderName}/" folder in your project)\n` +
|
||||
' 2. Run a fresh installation\n\n' +
|
||||
'Benefits of a fresh install:\n' +
|
||||
' \u2022 Cleaner configuration without legacy artifacts\n' +
|
||||
' \u2022 All new features properly configured\n' +
|
||||
' \u2022 Fewer potential conflicts';
|
||||
|
||||
await prompts.log.warn('VERSION WARNING');
|
||||
await prompts.note(warningContent, 'Version Warning');
|
||||
|
||||
if (options.yes) {
|
||||
await prompts.log.warn('Non-interactive mode (--yes): auto-proceeding with legacy update');
|
||||
return true;
|
||||
}
|
||||
|
||||
const proceed = await prompts.select({
|
||||
message: 'How would you like to proceed?',
|
||||
choices: [
|
||||
{
|
||||
name: 'Proceed with update anyway (may have issues)',
|
||||
value: 'proceed',
|
||||
},
|
||||
{
|
||||
name: 'Cancel (recommended - do a fresh install instead)',
|
||||
value: 'cancel',
|
||||
},
|
||||
],
|
||||
default: 'cancel',
|
||||
});
|
||||
|
||||
if (proceed === 'cancel') {
|
||||
await prompts.note(
|
||||
`1. Delete the "${bmadFolderName}/" folder in your project\n` + "2. Run 'bmad install' again",
|
||||
'To do a fresh install',
|
||||
);
|
||||
}
|
||||
|
||||
return proceed === 'proceed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Display module versions with update availability
|
||||
* @param {Array} modules - Array of module info objects with version info
|
||||
|
|
|
|||
Loading…
Reference in New Issue