130 lines
3.6 KiB
JavaScript
130 lines
3.6 KiB
JavaScript
const path = require('node:path');
|
|
const fs = require('fs-extra');
|
|
const { getProjectRoot } = require('../../../lib/project-root');
|
|
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
|
|
|
|
class InstallPaths {
|
|
static async create(config) {
|
|
const srcDir = getProjectRoot();
|
|
await assertReadableDir(srcDir, 'BMAD source root');
|
|
|
|
const pkgPath = path.join(srcDir, 'package.json');
|
|
await assertReadableFile(pkgPath, 'package.json');
|
|
const version = require(pkgPath).version;
|
|
|
|
const projectRoot = path.resolve(config.directory);
|
|
await ensureWritableDir(projectRoot, 'project root');
|
|
|
|
const bmadDir = path.join(projectRoot, BMAD_FOLDER_NAME);
|
|
const isUpdate = await fs.pathExists(bmadDir);
|
|
|
|
const configDir = path.join(bmadDir, '_config');
|
|
const agentsDir = path.join(configDir, 'agents');
|
|
const customCacheDir = path.join(configDir, 'custom');
|
|
const coreDir = path.join(bmadDir, 'core');
|
|
|
|
for (const [dir, label] of [
|
|
[bmadDir, 'bmad directory'],
|
|
[configDir, 'config directory'],
|
|
[agentsDir, 'agents config directory'],
|
|
[customCacheDir, 'custom modules cache'],
|
|
[coreDir, 'core module directory'],
|
|
]) {
|
|
await ensureWritableDir(dir, label);
|
|
}
|
|
|
|
return new InstallPaths({
|
|
srcDir,
|
|
version,
|
|
projectRoot,
|
|
bmadDir,
|
|
configDir,
|
|
agentsDir,
|
|
customCacheDir,
|
|
coreDir,
|
|
isUpdate,
|
|
});
|
|
}
|
|
|
|
constructor(props) {
|
|
Object.assign(this, props);
|
|
Object.freeze(this);
|
|
}
|
|
|
|
manifestFile() {
|
|
return path.join(this.configDir, 'manifest.yaml');
|
|
}
|
|
agentManifest() {
|
|
return path.join(this.configDir, 'agent-manifest.csv');
|
|
}
|
|
filesManifest() {
|
|
return path.join(this.configDir, 'files-manifest.csv');
|
|
}
|
|
helpCatalog() {
|
|
return path.join(this.configDir, 'bmad-help.csv');
|
|
}
|
|
moduleDir(name) {
|
|
return path.join(this.bmadDir, name);
|
|
}
|
|
moduleConfig(name) {
|
|
return path.join(this.bmadDir, name, 'config.yaml');
|
|
}
|
|
}
|
|
|
|
async function assertReadableDir(dirPath, label) {
|
|
const stat = await fs.stat(dirPath).catch(() => null);
|
|
if (!stat) {
|
|
throw new Error(`${label} does not exist: ${dirPath}`);
|
|
}
|
|
if (!stat.isDirectory()) {
|
|
throw new Error(`${label} is not a directory: ${dirPath}`);
|
|
}
|
|
try {
|
|
await fs.access(dirPath, fs.constants.R_OK);
|
|
} catch {
|
|
throw new Error(`${label} is not readable: ${dirPath}`);
|
|
}
|
|
}
|
|
|
|
async function assertReadableFile(filePath, label) {
|
|
const stat = await fs.stat(filePath).catch(() => null);
|
|
if (!stat) {
|
|
throw new Error(`${label} does not exist: ${filePath}`);
|
|
}
|
|
if (!stat.isFile()) {
|
|
throw new Error(`${label} is not a file: ${filePath}`);
|
|
}
|
|
try {
|
|
await fs.access(filePath, fs.constants.R_OK);
|
|
} catch {
|
|
throw new Error(`${label} is not readable: ${filePath}`);
|
|
}
|
|
}
|
|
|
|
async function ensureWritableDir(dirPath, label) {
|
|
const stat = await fs.stat(dirPath).catch(() => null);
|
|
if (stat && !stat.isDirectory()) {
|
|
throw new Error(`${label} exists but is not a directory: ${dirPath}`);
|
|
}
|
|
|
|
try {
|
|
await fs.ensureDir(dirPath);
|
|
} catch (error) {
|
|
if (error.code === 'EACCES') {
|
|
throw new Error(`${label}: permission denied creating directory: ${dirPath}`);
|
|
}
|
|
if (error.code === 'ENOSPC') {
|
|
throw new Error(`${label}: no space left on device: ${dirPath}`);
|
|
}
|
|
throw new Error(`${label}: cannot create directory: ${dirPath} (${error.message})`);
|
|
}
|
|
|
|
try {
|
|
await fs.access(dirPath, fs.constants.R_OK | fs.constants.W_OK);
|
|
} catch {
|
|
throw new Error(`${label} is not writable: ${dirPath}`);
|
|
}
|
|
}
|
|
|
|
module.exports = { InstallPaths };
|