128 lines
3.6 KiB
JavaScript
128 lines
3.6 KiB
JavaScript
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 };
|