add custom content installation question to indicate location of custom content

This commit is contained in:
Brian Madison 2025-12-07 13:39:03 -06:00
parent 987f81ff64
commit b68e5c0225
4 changed files with 231 additions and 17 deletions

Binary file not shown.

View File

@ -798,6 +798,53 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
}
// Install custom content if provided AND selected
if (
config.customContent &&
config.customContent.hasCustomContent &&
config.customContent.customPath &&
config.customContent.selected &&
config.customContent.selectedFiles
) {
spinner.start('Installing custom content...');
const { CustomHandler } = require('../custom/handler');
const customHandler = new CustomHandler();
// Use the selected files instead of finding all files
const customFiles = config.customContent.selectedFiles;
if (customFiles.length > 0) {
console.log(chalk.cyan(`\n Found ${customFiles.length} custom content file(s):`));
for (const customFile of customFiles) {
const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
if (customInfo) {
console.log(chalk.dim(`${customInfo.name} (${customInfo.relativePath})`));
// Install the custom content
const result = await customHandler.install(
customInfo.path,
bmadDir,
{ ...config.coreConfig, ...customInfo.config },
(filePath) => {
// Track installed files
this.installedFiles.push(filePath);
},
);
if (result.errors.length > 0) {
console.log(chalk.yellow(` ⚠️ ${result.errors.length} error(s) occurred`));
for (const error of result.errors) {
console.log(chalk.dim(` - ${error}`));
}
} else {
console.log(chalk.green(` ✓ Installed ${result.agentsInstalled} agents, ${result.workflowsInstalled} workflows`));
}
}
}
}
spinner.succeed('Custom content installed');
}
// Generate clean config.yaml files for each installed module
spinner.start('Generating module configurations...');
await this.generateModuleConfigs(bmadDir, moduleConfigs);

View File

@ -68,9 +68,10 @@ class CustomHandler {
/**
* Get custom content info from a custom.yaml file
* @param {string} customYamlPath - Path to custom.yaml file
* @param {string} projectRoot - Project root directory for calculating relative paths
* @returns {Object|null} Custom content info
*/
async getCustomInfo(customYamlPath) {
async getCustomInfo(customYamlPath, projectRoot = null) {
try {
const configContent = await fs.readFile(customYamlPath, 'utf8');
@ -84,7 +85,9 @@ class CustomHandler {
}
const customDir = path.dirname(customYamlPath);
const relativePath = path.relative(process.cwd(), customDir);
// Use provided projectRoot or fall back to process.cwd()
const basePath = projectRoot || process.cwd();
const relativePath = path.relative(basePath, customDir);
return {
id: config.code || path.basename(customDir),
@ -236,13 +239,20 @@ class CustomHandler {
// Copy with placeholder replacement for text files
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json'];
if (textExtensions.some((ext) => entry.name.endsWith(ext))) {
await this.fileOps.copyFile(sourcePath, targetPath, {
bmadFolder: config.bmad_folder || 'bmad',
userName: config.user_name || 'User',
communicationLanguage: config.communication_language || 'English',
outputFolder: config.output_folder || 'docs',
});
// Read source content
let content = await fs.readFile(sourcePath, 'utf8');
// Replace placeholders
content = content.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
content = content.replaceAll('{user_name}', config.user_name || 'User');
content = content.replaceAll('{communication_language}', config.communication_language || 'English');
content = content.replaceAll('{output_folder}', config.output_folder || 'docs');
// Write to target
await fs.ensureDir(path.dirname(targetPath));
await fs.writeFile(targetPath, content, 'utf8');
} else {
// Copy binary files as-is
await fs.copy(sourcePath, targetPath);
}

View File

@ -52,6 +52,9 @@ class UI {
await installer.handleLegacyV4Migration(confirmedDirectory, legacyV4);
}
// Prompt for custom content location (separate from installation directory)
const customContentConfig = await this.promptCustomContentLocation();
// Check if there's an existing BMAD installation
const fs = require('fs-extra');
const path = require('node:path');
@ -85,9 +88,12 @@ class UI {
// Handle quick update separately
if (actionType === 'quick-update') {
// Even for quick update, ask about custom content
const customContentConfig = await this.promptCustomContentLocation();
return {
actionType: 'quick-update',
directory: confirmedDirectory,
customContent: customContentConfig,
};
}
@ -125,8 +131,21 @@ class UI {
console.log(chalk.cyan('\n📦 Keeping existing modules: ') + selectedModules.join(', '));
} else {
// Only show module selection for new installs
const moduleChoices = await this.getModuleChoices(installedModuleIds);
const moduleChoices = await this.getModuleChoices(installedModuleIds, customContentConfig);
selectedModules = await this.selectModules(moduleChoices);
// Check which custom content items were selected
const selectedCustomContent = selectedModules.filter((mod) => mod.startsWith('__CUSTOM_CONTENT__'));
if (selectedCustomContent.length > 0) {
customContentConfig.selected = true;
customContentConfig.selectedFiles = selectedCustomContent.map((mod) => mod.replace('__CUSTOM_CONTENT__', ''));
// Filter out custom content markers since they're not real modules
selectedModules = selectedModules.filter((mod) => !mod.startsWith('__CUSTOM_CONTENT__'));
} else if (customContentConfig.hasCustomContent) {
// User provided custom content but didn't select any
customContentConfig.selected = false;
customContentConfig.selectedFiles = [];
}
}
// Prompt for AgentVibes TTS integration
@ -147,7 +166,9 @@ class UI {
ides: toolSelection.ides,
skipIde: toolSelection.skipIde,
coreConfig: coreConfig, // Pass collected core config to installer
enableAgentVibes: agentVibesConfig.enabled, // AgentVibes TTS integration
// Custom content configuration
customContent: customContentConfig,
enableAgentVibes: agentVibesConfig.enabled,
agentVibesInstalled: agentVibesConfig.alreadyInstalled,
};
}
@ -483,19 +504,50 @@ class UI {
/**
* Get module choices for selection
* @param {Set} installedModuleIds - Currently installed module IDs
* @param {Object} customContentConfig - Custom content configuration
* @returns {Array} Module choices for inquirer
*/
async getModuleChoices(installedModuleIds) {
async getModuleChoices(installedModuleIds, customContentConfig = null) {
const moduleChoices = [];
const isNewInstallation = installedModuleIds.size === 0;
// Add custom content items first if found
if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) {
// Add separator before custom content
moduleChoices.push(new inquirer.Separator('── Custom Content ──'));
// Get the custom content info to display proper names
const { CustomHandler } = require('../installers/lib/custom/handler');
const customHandler = new CustomHandler();
const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
for (const customFile of customFiles) {
const customInfo = await customHandler.getCustomInfo(customFile);
if (customInfo) {
moduleChoices.push({
name: `${chalk.cyan('✓')} ${customInfo.name} ${chalk.gray(`(${customInfo.relativePath})`)}`,
value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
checked: true, // Default to selected since user chose to provide custom content
});
}
}
// Add separator for official content
moduleChoices.push(new inquirer.Separator('── Official Content ──'));
}
// Add official modules
const { ModuleManager } = require('../installers/lib/modules/manager');
const moduleManager = new ModuleManager();
const availableModules = await moduleManager.listAvailable();
const isNewInstallation = installedModuleIds.size === 0;
const moduleChoices = availableModules.map((mod) => ({
name: mod.isCustom ? `${mod.name} ${chalk.red('(Custom)')}` : mod.name,
value: mod.id,
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
}));
for (const mod of availableModules) {
moduleChoices.push({
name: mod.name,
value: mod.id,
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
});
}
return moduleChoices;
}
@ -574,6 +626,111 @@ class UI {
}
}
/**
* Prompt for custom content location
* @returns {Object} Custom content configuration
*/
async promptCustomContentLocation() {
try {
CLIUtils.displaySection('Custom Content', 'Optional: Add custom agents and workflows');
const { hasCustomContent } = await inquirer.prompt([
{
type: 'list',
name: 'hasCustomContent',
message: 'Do you have custom content to install?',
choices: [
{ name: 'No (skip custom content)', value: 'none' },
{ name: 'Enter a directory path', value: 'directory' },
{ name: 'Enter a URL', value: 'url' },
],
default: 'none',
},
]);
if (hasCustomContent === 'none') {
return { hasCustomContent: false };
}
if (hasCustomContent === 'url') {
console.log(chalk.yellow('\nURL-based custom content installation is coming soon!'));
console.log(chalk.cyan('For now, please download your custom content and choose "Enter a directory path".\n'));
return { hasCustomContent: false };
}
if (hasCustomContent === 'directory') {
let customPath;
while (!customPath) {
let expandedPath;
const { directory } = await inquirer.prompt([
{
type: 'input',
name: 'directory',
message: 'Enter the path to your custom content directory:',
default: process.cwd(), // Use actual current working directory
validate: async (input) => {
if (!input || input.trim() === '') {
return 'Please enter a directory path';
}
try {
expandedPath = this.expandUserPath(input.trim());
} catch (error) {
return error.message;
}
// Check if the path exists
const pathExists = await fs.pathExists(expandedPath);
if (!pathExists) {
return 'Directory does not exist';
}
return true;
},
},
]);
// Now expand the path for use after the prompt
expandedPath = this.expandUserPath(directory.trim());
// Check if directory has custom content
const { CustomHandler } = require('../installers/lib/custom/handler');
const customHandler = new CustomHandler();
const customFiles = await customHandler.findCustomContent(expandedPath);
if (customFiles.length === 0) {
console.log(chalk.yellow(`\nNo custom.yaml files found in ${expandedPath}`));
const { tryAgain } = await inquirer.prompt([
{
type: 'confirm',
name: 'tryAgain',
message: 'Try a different directory?',
default: true,
},
]);
if (tryAgain) {
continue;
} else {
return { hasCustomContent: false };
}
}
customPath = expandedPath;
console.log(chalk.green(`\n✓ Found ${customFiles.length} custom content file(s)`));
}
return { hasCustomContent: true, customPath };
}
return { hasCustomContent: false };
} catch (error) {
console.error(chalk.red('Error in custom content prompt:'), error);
return { hasCustomContent: false };
}
}
/**
* Confirm directory selection
* @param {string} directory - The directory path