BMAD-METHOD/tools/cli/lib/installer.js

272 lines
8.8 KiB
JavaScript

/**
* WDS Installer - Core orchestrator
* Copies WDS source files, compiles agents, creates folder structure, sets up IDE.
*/
const path = require('node:path');
const fs = require('fs-extra');
const chalk = require('chalk');
const ora = require('ora');
const yaml = require('js-yaml');
const inquirer = require('inquirer').default || require('inquirer');
const { compileAgentFile } = require('./compiler');
class Installer {
constructor() {
// Resolve directories relative to this file (tools/cli/lib/ -> up 3 levels)
const repoRoot = path.resolve(__dirname, '..', '..', '..');
this.srcDir = path.join(repoRoot, 'src');
this.docsDir = path.join(repoRoot, 'docs');
}
/**
* Main installation flow
* @param {Object} config - Configuration from UI prompts
*/
async install(config) {
const { projectDir, wdsFolder, root_folder } = config;
const wdsDir = path.join(projectDir, wdsFolder);
// Check if already installed
if (await fs.pathExists(wdsDir)) {
console.log(chalk.yellow(`\n ${wdsFolder}/ already exists.`));
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{ name: 'Update - Replace WDS files, keep config.yaml', value: 'update' },
{ name: 'Fresh install - Remove everything and start over', value: 'fresh' },
{ name: 'Cancel', value: 'cancel' },
],
},
]);
if (action === 'cancel') {
return { success: false };
}
if (action === 'fresh') {
const removeSpinner = ora('Removing existing WDS installation...').start();
await fs.remove(wdsDir);
removeSpinner.succeed('Old installation removed');
} else if (action === 'update') {
// Preserve config.yaml during update
const configPath = path.join(wdsDir, 'config.yaml');
let savedConfig = null;
if (await fs.pathExists(configPath)) {
savedConfig = await fs.readFile(configPath, 'utf8');
}
const removeSpinner = ora('Updating WDS files...').start();
await fs.remove(wdsDir);
removeSpinner.succeed('Old files cleared');
// Will be restored after copy
config._savedConfigYaml = savedConfig;
}
}
console.log('');
// Step 1: Copy source files
const spinner = ora('Copying WDS files...').start();
try {
await this.copySrcFiles(wdsDir);
spinner.succeed('WDS files copied');
} catch (error) {
spinner.fail('Failed to copy WDS files');
throw error;
}
// Step 2: Write config.yaml
const configSpinner = ora('Writing configuration...').start();
try {
await this.writeConfig(wdsDir, config);
configSpinner.succeed('Configuration saved');
} catch (error) {
configSpinner.fail('Failed to write configuration');
throw error;
}
// Step 3: Compile agents
const agentSpinner = ora('Compiling agents...').start();
try {
const agents = await this.compileAgents(wdsDir, wdsFolder);
agentSpinner.succeed(`Compiled ${agents.length} agents`);
} catch (error) {
agentSpinner.fail('Failed to compile agents');
throw error;
}
// Step 4: Create work products folder structure
const rootFolder = root_folder || 'design-process';
const docsSpinner = ora(`Creating project folders in ${rootFolder}/...`).start();
try {
await this.createDocsFolders(projectDir, rootFolder, config);
docsSpinner.succeed(`Project folders created in ${rootFolder}/`);
} catch (error) {
docsSpinner.fail('Failed to create project folders');
throw error;
}
// Step 5: Copy learning & reference material (always included)
const learnSpinner = ora('Copying learning & reference material...').start();
try {
await this.copyLearningMaterial(projectDir);
learnSpinner.succeed('Learning material added to _wds-learn/ (safe to remove when no longer needed)');
} catch (error) {
learnSpinner.fail('Failed to copy learning material');
throw error;
}
return { success: true, wdsDir, projectDir };
}
/**
* Copy src/ content into the target WDS directory
*/
async copySrcFiles(wdsDir) {
const contentDirs = ['agents', 'data', 'gems', 'skills', 'workflows'];
for (const dir of contentDirs) {
const src = path.join(this.srcDir, dir);
const dest = path.join(wdsDir, dir);
if (await fs.pathExists(src)) {
await fs.copy(src, dest);
}
}
// Copy module.yaml and module-help.csv
const moduleYaml = path.join(this.srcDir, 'module.yaml');
if (await fs.pathExists(moduleYaml)) {
await fs.copy(moduleYaml, path.join(wdsDir, 'module.yaml'));
}
const moduleHelp = path.join(this.srcDir, 'module-help.csv');
if (await fs.pathExists(moduleHelp)) {
await fs.copy(moduleHelp, path.join(wdsDir, 'module-help.csv'));
}
}
/**
* Write config.yaml from user answers (or restore saved config on update)
*/
async writeConfig(wdsDir, config) {
// On update, restore the user's existing config
if (config._savedConfigYaml) {
await fs.writeFile(path.join(wdsDir, 'config.yaml'), config._savedConfigYaml, 'utf8');
return;
}
// Get user name from git or system
const getUserName = () => {
try {
const { execSync } = require('child_process');
const gitName = execSync('git config user.name', { encoding: 'utf8' }).trim();
return gitName || 'Designer';
} catch {
return 'Designer';
}
};
const configData = {
user_name: getUserName(),
project_name: config.project_name || 'Untitled Project',
starting_point: config.starting_point || 'brief',
communication_language: 'en',
document_output_language: 'en',
output_folder: config.root_folder || 'design-process',
wds_folder: config.wdsFolder,
};
const yamlStr = yaml.dump(configData, { lineWidth: -1 });
await fs.writeFile(path.join(wdsDir, 'config.yaml'), `# WDS Configuration - Generated by installer\n${yamlStr}`, 'utf8');
}
/**
* Compile all .agent.yaml files in the agents directory
*/
async compileAgents(wdsDir, wdsFolder) {
const agentsDir = path.join(wdsDir, 'agents');
const files = await fs.readdir(agentsDir);
const agentFiles = files.filter((f) => f.endsWith('.agent.yaml'));
const results = [];
for (const file of agentFiles) {
const yamlPath = path.join(agentsDir, file);
const result = compileAgentFile(yamlPath, { wdsFolder });
results.push(result);
}
return results;
}
/**
* Copy learning & reference material into _wds-learn/ at project root.
* Users can safely delete this folder without affecting agents or workflows.
*/
async copyLearningMaterial(projectDir) {
const learnDir = path.join(projectDir, '_wds-learn');
const learningDirs = ['getting-started', 'learn-wds', 'method', 'models', 'tools'];
const excludeDirs = new Set(['course-explainers', 'Webinars']);
for (const dir of learningDirs) {
const src = path.join(this.docsDir, dir);
const dest = path.join(learnDir, dir);
if (await fs.pathExists(src)) {
await fs.copy(src, dest, {
filter: (srcPath) => {
const relative = path.relative(src, srcPath);
const topDir = relative.split(path.sep)[0];
return !excludeDirs.has(topDir);
},
});
}
}
}
/**
* Create the WDS work products folder structure
* @param {string} projectDir - Project root directory
* @param {string} rootFolder - Root folder name (design-process)
* @param {Object} config - Configuration object
*/
async createDocsFolders(projectDir, rootFolder, config) {
const docsPath = path.join(projectDir, rootFolder);
// Simplified 5-phase structure
const folders = [
'A-Product-Brief',
'B-Trigger-Map',
'C-UX-Scenarios',
'D-UX-Design',
'E-Design-System',
];
for (const folder of folders) {
const folderPath = path.join(docsPath, folder);
// Only create folder if it doesn't exist
if (!(await fs.pathExists(folderPath))) {
await fs.ensureDir(folderPath);
// Add .gitkeep to preserve empty directories (only if folder is empty)
const gitkeepPath = path.join(folderPath, '.gitkeep');
const existingFiles = await fs.readdir(folderPath);
if (existingFiles.length === 0) {
await fs.writeFile(gitkeepPath, '# This file ensures the directory is tracked by git\n');
}
}
}
// Create _progress folder for agent tracking
const progressPath = path.join(docsPath, '_progress');
await fs.ensureDir(progressPath);
await fs.ensureDir(path.join(progressPath, 'agent-dialogs'));
}
}
module.exports = { Installer };