add custom content installation question to indicate location of custom content
This commit is contained in:
parent
987f81ff64
commit
b68e5c0225
Binary file not shown.
|
|
@ -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
|
// Generate clean config.yaml files for each installed module
|
||||||
spinner.start('Generating module configurations...');
|
spinner.start('Generating module configurations...');
|
||||||
await this.generateModuleConfigs(bmadDir, moduleConfigs);
|
await this.generateModuleConfigs(bmadDir, moduleConfigs);
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,10 @@ class CustomHandler {
|
||||||
/**
|
/**
|
||||||
* Get custom content info from a custom.yaml file
|
* Get custom content info from a custom.yaml file
|
||||||
* @param {string} customYamlPath - Path to 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
|
* @returns {Object|null} Custom content info
|
||||||
*/
|
*/
|
||||||
async getCustomInfo(customYamlPath) {
|
async getCustomInfo(customYamlPath, projectRoot = null) {
|
||||||
try {
|
try {
|
||||||
const configContent = await fs.readFile(customYamlPath, 'utf8');
|
const configContent = await fs.readFile(customYamlPath, 'utf8');
|
||||||
|
|
||||||
|
|
@ -84,7 +85,9 @@ class CustomHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
const customDir = path.dirname(customYamlPath);
|
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 {
|
return {
|
||||||
id: config.code || path.basename(customDir),
|
id: config.code || path.basename(customDir),
|
||||||
|
|
@ -236,13 +239,20 @@ class CustomHandler {
|
||||||
// Copy with placeholder replacement for text files
|
// Copy with placeholder replacement for text files
|
||||||
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json'];
|
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json'];
|
||||||
if (textExtensions.some((ext) => entry.name.endsWith(ext))) {
|
if (textExtensions.some((ext) => entry.name.endsWith(ext))) {
|
||||||
await this.fileOps.copyFile(sourcePath, targetPath, {
|
// Read source content
|
||||||
bmadFolder: config.bmad_folder || 'bmad',
|
let content = await fs.readFile(sourcePath, 'utf8');
|
||||||
userName: config.user_name || 'User',
|
|
||||||
communicationLanguage: config.communication_language || 'English',
|
// Replace placeholders
|
||||||
outputFolder: config.output_folder || 'docs',
|
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 {
|
} else {
|
||||||
|
// Copy binary files as-is
|
||||||
await fs.copy(sourcePath, targetPath);
|
await fs.copy(sourcePath, targetPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ class UI {
|
||||||
await installer.handleLegacyV4Migration(confirmedDirectory, legacyV4);
|
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
|
// Check if there's an existing BMAD installation
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
|
@ -85,9 +88,12 @@ class UI {
|
||||||
|
|
||||||
// Handle quick update separately
|
// Handle quick update separately
|
||||||
if (actionType === 'quick-update') {
|
if (actionType === 'quick-update') {
|
||||||
|
// Even for quick update, ask about custom content
|
||||||
|
const customContentConfig = await this.promptCustomContentLocation();
|
||||||
return {
|
return {
|
||||||
actionType: 'quick-update',
|
actionType: 'quick-update',
|
||||||
directory: confirmedDirectory,
|
directory: confirmedDirectory,
|
||||||
|
customContent: customContentConfig,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,8 +131,21 @@ class UI {
|
||||||
console.log(chalk.cyan('\n📦 Keeping existing modules: ') + selectedModules.join(', '));
|
console.log(chalk.cyan('\n📦 Keeping existing modules: ') + selectedModules.join(', '));
|
||||||
} else {
|
} else {
|
||||||
// Only show module selection for new installs
|
// 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);
|
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
|
// Prompt for AgentVibes TTS integration
|
||||||
|
|
@ -147,7 +166,9 @@ class UI {
|
||||||
ides: toolSelection.ides,
|
ides: toolSelection.ides,
|
||||||
skipIde: toolSelection.skipIde,
|
skipIde: toolSelection.skipIde,
|
||||||
coreConfig: coreConfig, // Pass collected core config to installer
|
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,
|
agentVibesInstalled: agentVibesConfig.alreadyInstalled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -483,19 +504,50 @@ class UI {
|
||||||
/**
|
/**
|
||||||
* Get module choices for selection
|
* Get module choices for selection
|
||||||
* @param {Set} installedModuleIds - Currently installed module IDs
|
* @param {Set} installedModuleIds - Currently installed module IDs
|
||||||
|
* @param {Object} customContentConfig - Custom content configuration
|
||||||
* @returns {Array} Module choices for inquirer
|
* @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 } = require('../installers/lib/modules/manager');
|
||||||
const moduleManager = new ModuleManager();
|
const moduleManager = new ModuleManager();
|
||||||
const availableModules = await moduleManager.listAvailable();
|
const availableModules = await moduleManager.listAvailable();
|
||||||
|
|
||||||
const isNewInstallation = installedModuleIds.size === 0;
|
for (const mod of availableModules) {
|
||||||
const moduleChoices = availableModules.map((mod) => ({
|
moduleChoices.push({
|
||||||
name: mod.isCustom ? `${mod.name} ${chalk.red('(Custom)')}` : mod.name,
|
name: mod.name,
|
||||||
value: mod.id,
|
value: mod.id,
|
||||||
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
|
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
|
||||||
}));
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return moduleChoices;
|
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
|
* Confirm directory selection
|
||||||
* @param {string} directory - The directory path
|
* @param {string} directory - The directory path
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue