all modules custom or core use the same installer and have consistent behavior now.

This commit is contained in:
Brian Madison 2025-12-07 17:17:50 -06:00
parent baaa984a90
commit 6430173738
44 changed files with 393 additions and 253 deletions

View File

@ -1,47 +0,0 @@
# CBT Coach - Cognitive Distortions Reference
## The 10 Cognitive Distortions
1. **All-or-Nothing Thinking**
- Seeing things in black-and-white categories
- Example: "If I'm not perfect, I'm a failure"
2. **Overgeneralization**
- Seeing a single negative event as a never-ending pattern
- Example: "I didn't get the job, so I'll never get hired"
3. **Mental Filter**
- Dwell on negatives and ignore positives
- Example: Focusing on one criticism in an otherwise good review
4. **Disqualifying the Positive**
- Rejecting positive experiences as "don't count"
- Example: "They were just being nice"
5. **Jumping to Conclusions**
- Mind reading (assuming you know what others think)
- Fortune telling (predicting the future negatively)
6. **Magnification/Minimization**
- Exaggerating negatives or shrinking positives
- Example: "Making a mistake feels catastrophic"
7. **Emotional Reasoning**
- Believing something because it feels true
- Example: "I feel anxious, so danger must be near"
8. **"Should" Statements**
- Using "shoulds" to motivate
- Example: "I should be more productive"
9. **Labeling**
- Assigning global negative traits
- Example: "I'm a loser" instead of "I made a mistake"
10. **Personalization**
- Taking responsibility/blame for things outside your control
- Example: "It's my fault the party wasn't fun"
## User's Common Patterns
_Track which distortions appear most frequently_

View File

@ -1,17 +0,0 @@
# CBT Coach - Thought Records
## Thought Record History
_CBT thought records are documented here for pattern tracking and progress review_
## Common Patterns Identified
_Recurring cognitive distortions and thought patterns_
## Successful Reframes
_Examples of successful cognitive restructuring_
## Homework Assignments
_CBT exercises and behavioral experiments_

View File

@ -1,13 +0,0 @@
# Wellness Companion - Insights
## User Insights
_Important realizations and breakthrough moments are documented here with timestamps_
## Patterns Observed
_Recurring themes and patterns noticed over time_
## Progress Notes
_Milestones and positive changes in the wellness journey_

View File

@ -1,30 +0,0 @@
# Wellness Companion - Instructions
## Safety Protocols
1. Always validate user feelings before offering guidance
2. Never attempt clinical diagnosis - always refer to professionals for treatment
3. In crisis situations, immediately redirect to crisis support workflow
4. Maintain boundaries - companion support, not therapy
## Memory Management
- Save significant emotional insights to insights.md
- Track recurring patterns in patterns.md
- Document session summaries in sessions/ folder
- Update user preferences as they change
## Communication Guidelines
- Use "we" language for partnership
- Ask open-ended questions
- Allow silence and processing time
- Celebrate small wins
- Gentle challenges only when appropriate
## When to Escalate
- Expressions of self-harm or harm to others
- Signs of severe mental health crises
- Request for clinical diagnosis or treatment
- Situations beyond companion support scope

View File

@ -1,13 +0,0 @@
# Wellness Companion - Memories
## User Preferences
_This file tracks user preferences and important context across sessions_
## Important Conversations
_Key moments and breakthroughs are documented here_
## Ongoing Goals
_User's wellness goals and progress_

View File

@ -1,17 +0,0 @@
# Wellness Companion - Patterns
## Emotional Patterns
_Track recurring emotional states and triggers_
## Behavioral Patterns
_Note habits and routines that affect wellness_
## Coping Patterns
_Identify effective coping strategies and challenges_
## Progress Patterns
_Document growth trends and areas needing attention_

View File

@ -64,7 +64,7 @@ A custom module follows this structure:
my-module/ my-module/
├── _module-installer/ ├── _module-installer/
│ ├── installer.js # optional, when it exists it will run with module installation │ ├── installer.js # optional, when it exists it will run with module installation
│ └── install-config.yaml # Module installation configuration with custom question and answer capture ├── module.yaml # Module installation configuration with custom question and answer capture
├── docs/ # Module documentation ├── docs/ # Module documentation
├── agents/ # Module-specific agents ├── agents/ # Module-specific agents
├── workflows/ # Module-specific workflows ├── workflows/ # Module-specific workflows
@ -77,7 +77,7 @@ my-module/
#### Module Configuration #### Module Configuration
The `_module-installer/install-config.yaml` file defines how your module is installed: The `module.yaml` file defines how your module is installed:
```yaml ```yaml
# Module metadata # Module metadata
@ -99,12 +99,6 @@ my_setting:
See `/example-custom-module` for a complete example: See `/example-custom-module` for a complete example:
```bash
# The example is ready to use - just rename the _module-installer/install-config file:
mv example-custom-module/mwm/_module-installer/install-config.bak \
example-custom-module/mwm/_module-installer/install-config.yaml
```
## Installation Process ## Installation Process
### Step 1: Running the Installer ### Step 1: Running the Installer
@ -129,7 +123,7 @@ If you select "Enter a directory path", the installer will prompt for the locati
The installer will: The installer will:
- Scan the directory and all subdirectories for the presence of a `custom.yaml` file (standalone content such as agents and workflows) - Scan the directory and all subdirectories for the presence of a `custom.yaml` file (standalone content such as agents and workflows)
- Scan for `_module-installer/install-config.yaml` files (modules) - Scan for `module.yaml` files (modules)
- Display an indication of how many installable folders it has found. Note that a project with stand along agents and workflows all under a single folder like the example will just list the count as 1 for that directory. - Display an indication of how many installable folders it has found. Note that a project with stand along agents and workflows all under a single folder like the example will just list the count as 1 for that directory.
### Step 3: Selecting Content ### Step 3: Selecting Content
@ -230,7 +224,7 @@ Custom content can be distributed:
### No Custom Content Found ### No Custom Content Found
- Ensure your `custom.yaml` or `install-config.yaml` files are properly named - Ensure your `custom.yaml` or `module.yaml` files are properly named
- Check file permissions - Check file permissions
- Verify the directory path is correct - Verify the directory path is correct

View File

@ -59,6 +59,7 @@ project-root/
### Key Exclusions ### Key Exclusions
- `_module-installer/` directories are never copied to destination - `_module-installer/` directories are never copied to destination
- module.yaml
- `localskip="true"` agents are filtered out - `localskip="true"` agents are filtered out
- Source `config.yaml` templates are replaced with generated configs - Source `config.yaml` templates are replaced with generated configs
@ -92,8 +93,8 @@ Creative Innovation Studio for design workflows
``` ```
src/modules/{module}/ src/modules/{module}/
├── _module-installer/ # Not copied to destination ├── _module-installer/ # Not copied to destination
│ ├── installer.js # Post-install logic │ ├── installer.js # Post-install logic
│ └── install-config.yaml ├── module.yaml
├── agents/ ├── agents/
├── tasks/ ├── tasks/
├── templates/ ├── templates/
@ -107,7 +108,7 @@ src/modules/{module}/
### Collection Process ### Collection Process
Modules define prompts in `install-config.yaml`: Modules define prompts in `module.yaml`:
```yaml ```yaml
project_name: project_name:
@ -218,12 +219,12 @@ Platform-specific content without source modification:
src/modules/mymod/ src/modules/mymod/
├── _module-installer/ ├── _module-installer/
│ ├── installer.js │ ├── installer.js
│ └── install-config.yaml ├── module.yaml
├── agents/ ├── agents/
└── tasks/ └── tasks/
``` ```
2. **Configuration** (`install-config.yaml`) 2. **Configuration** (`module.yaml`)
```yaml ```yaml
code: mymod code: mymod

View File

@ -41,7 +41,7 @@ CLI uses Commander.js, commands auto-loaded from `tools/cli/commands/`:
### Core Architecture Patterns ### Core Architecture Patterns
1. **IDE Handlers**: Each IDE extends BaseIdeSetup class 1. **IDE Handlers**: Each IDE extends BaseIdeSetup class
2. **Module Installers**: Modules can have `_module-installer/installer.js` 2. **Module Installers**: Modules can have `module.yaml` and `_module-installer/installer.js`
3. **Sub-modules**: IDE-specific customizations in `sub-modules/{ide-name}/` 3. **Sub-modules**: IDE-specific customizations in `sub-modules/{ide-name}/`
4. **Shared Utilities**: `tools/cli/installers/lib/ide/shared/` contains generators 4. **Shared Utilities**: `tools/cli/installers/lib/ide/shared/` contains generators

View File

@ -117,7 +117,7 @@ Contains:
- Add new IDE handler: Create file in /tools/cli/installers/lib/ide/, extend BaseIdeSetup - Add new IDE handler: Create file in /tools/cli/installers/lib/ide/, extend BaseIdeSetup
- Fix installer bug: Check installer.js (94KB - main logic) - Fix installer bug: Check installer.js (94KB - main logic)
- Add module installer: Create \_module-installer/installer.js in module - Add module installer: Create \_module-installer/installer.js if custom installer logic needed
- Update shared generators: Modify files in /shared/ directory - Update shared generators: Modify files in /shared/ directory
## Relationships ## Relationships

View File

@ -27,7 +27,7 @@ src/modules/{module-name}/
│ ├── injections.yaml │ ├── injections.yaml
│ ├── config.yaml │ ├── config.yaml
│ └── sub-agents/ │ └── sub-agents/
├── install-config.yaml # Module install configuration ├── module.yaml # Module install configuration
└── README.md # Module documentation └── README.md # Module documentation
``` ```
@ -145,7 +145,7 @@ Defined in @/tools/cli/lib/platform-codes.js
- Create new module installer: Add \_module-installer/installer.js - Create new module installer: Add \_module-installer/installer.js
- Add IDE sub-module: Create sub-modules/{ide-name}/ with config - Add IDE sub-module: Create sub-modules/{ide-name}/ with config
- Add new IDE support: Create handler in installers/lib/ide/ - Add new IDE support: Create handler in installers/lib/ide/
- Customize module installation: Modify install-config.yaml - Customize module installation: Modify module.yaml
## Relationships ## Relationships

View File

@ -1,3 +1,4 @@
code: bmad-custom code: bmad-custom
name: "BMAD-Custom: Sample Stand Alone Custom Agents and Workflows" name: "BMAD-Custom: Sample Stand Alone Custom Agents and Workflows"
default_selected: true default_selected: true
type: custom

View File

@ -3,9 +3,6 @@
This module is an example and is not at all recommended for any usage, this module was not vetted by any medical professionals and should This module is an example and is not at all recommended for any usage, this module was not vetted by any medical professionals and should
be considered at best for entertainment purposes only. be considered at best for entertainment purposes only.
IF you want to see how a custom module installation works, copy this whole folder to where you will be installing from with npx, and rename
"\_module-installer/install-config.bak" to "\_module-installer/install-config.yaml".
You should see the option in the module selector when installing. You should see the option in the module selector when installing.
If you have received a module from someone else that is not in the official installation - you can install it similarly by running the If you have received a module from someone else that is not in the official installation - you can install it similarly by running the

View File

@ -4,6 +4,7 @@
code: mwm code: mwm
name: "MWM: Mental Wellness Module" name: "MWM: Mental Wellness Module"
default_selected: false default_selected: false
type: module
header: "MWM™: Custom Wellness Module" header: "MWM™: Custom Wellness Module"
subheader: "Demo of Potential Non Coding Custom Module Use case" subheader: "Demo of Potential Non Coding Custom Module Use case"

View File

@ -6,7 +6,7 @@ const chalk = require('chalk');
* *
* @param {Object} options - Installation options * @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project * @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from install-config.yaml * @param {Object} options.config - Module configuration from module.yaml
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed * @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
* @param {Object} options.logger - Logger instance for output * @param {Object} options.logger - Logger instance for output
* @returns {Promise<boolean>} - Success status * @returns {Promise<boolean>} - Success status

View File

@ -8,7 +8,7 @@ const chalk = require('chalk');
* *
* @param {Object} options - Installation options * @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project * @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from install-config.yaml * @param {Object} options.config - Module configuration from module.yaml
* @param {Object} options.coreConfig - Core configuration containing user_name * @param {Object} options.coreConfig - Core configuration containing user_name
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed * @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
* @param {Object} options.logger - Logger instance for output * @param {Object} options.logger - Logger instance for output

View File

@ -113,10 +113,10 @@ For a [module type] module, we'll create this structure:"
│ └── [template-files] │ └── [template-files]
├── data/ # Module data files ├── data/ # Module data files
│ └── [data-files] │ └── [data-files]
├── module.yaml # Required
├── _module-installer/ # Installation configuration ├── _module-installer/ # Installation configuration
│ ├── install-config.yaml # Required │ ├── installer.js # Optional
│ ├── installer.js # Optional │ └── assets/ # Optional install assets
│ └── assets/ # Optional install assets
└── README.md # Module documentation └── README.md # Module documentation
``` ```

View File

@ -184,7 +184,7 @@ Update module-plan.md with configuration section:
### Result Configuration Structure ### Result Configuration Structure
The install-config.yaml will generate: The module.yaml will generate:
- Module configuration at: {bmad_folder}/{module_code}/config.yaml - Module configuration at: {bmad_folder}/{module_code}/config.yaml
- User settings stored as: [describe structure] - User settings stored as: [describe structure]
```` ````

View File

@ -37,7 +37,7 @@ partyModeWorkflow: '{project-root}/{bmad_folder}/core/workflows/party-mode/workf
## EXECUTION PROTOCOLS: ## EXECUTION PROTOCOLS:
- 🎯 Use configuration plan from step 5 - 🎯 Use configuration plan from step 5
- 💾 Create install-config.yaml with all fields - 💾 Create module.yaml with all fields
- 📖 Add "step-08-installer" to stepsCompleted array` before loading next step - 📖 Add "step-08-installer" to stepsCompleted array` before loading next step
- 🚫 FORBIDDEN to load next step until user selects 'C' - 🚫 FORBIDDEN to load next step until user selects 'C'
@ -50,7 +50,7 @@ partyModeWorkflow: '{project-root}/{bmad_folder}/core/workflows/party-mode/workf
## STEP GOAL: ## STEP GOAL:
To create the module installer configuration (install-config.yaml) that defines how users will install and configure the module. To create the module installer configuration (module.yaml) that defines how users will install and configure the module.
## INSTALLER SETUP PROCESS: ## INSTALLER SETUP PROCESS:
@ -74,11 +74,11 @@ From step 5, we planned these configuration fields:
Ensure \_module-installer directory exists Ensure \_module-installer directory exists
Directory: {custom_module_location}/{module_name}/\_module-installer/ Directory: {custom_module_location}/{module_name}/\_module-installer/
### 3. Create install-config.yaml ### 3. Create module.yaml
"I'll create the install-config.yaml file based on your configuration plan. This is the core installer configuration file." "I'll create the module.yaml file based on your configuration plan. This is the core installer configuration file."
Create file: {custom_module_location}/{module_name}/\_module-installer/install-config.yaml from template {installConfigTemplate} Create file: {custom_module_location}/{module_name}/module.yaml from template {installConfigTemplate}
### 4. Handle Custom Installation Logic ### 4. Handle Custom Installation Logic
@ -117,7 +117,7 @@ Update module-plan.md with installer section:
### Install Configuration ### Install Configuration
- File: \_module-installer/install-config.yaml - File: module.yaml
- Module code: {module_name} - Module code: {module_name}
- Default selected: false - Default selected: false
- Configuration fields: [count] - Configuration fields: [count]
@ -166,7 +166,7 @@ Display: **Select an Option:** [A] Advanced Elicitation [P] Party Mode [C] Conti
### ✅ SUCCESS: ### ✅ SUCCESS:
- install-config.yaml created with all planned fields - module.yaml created with all planned fields
- YAML syntax valid - YAML syntax valid
- Custom installation logic prepared (if needed) - Custom installation logic prepared (if needed)
- Installer follows BMAD standards - Installer follows BMAD standards
@ -174,7 +174,7 @@ Display: **Select an Option:** [A] Advanced Elicitation [P] Party Mode [C] Conti
### ❌ SYSTEM FAILURE: ### ❌ SYSTEM FAILURE:
- Not creating install-config.yaml - Not creating module.yaml
- Invalid YAML syntax - Invalid YAML syntax
- Missing required fields - Missing required fields
- Not using proper path templates - Not using proper path templates

View File

@ -133,7 +133,8 @@ bmad install {module_name}
├── tasks/ # Task files ├── tasks/ # Task files
├── templates/ # Shared templates ├── templates/ # Shared templates
├── data/ # Module data ├── data/ # Module data
├── _module-installer/ # Installation config ├── _module-installer/ # Installation optional js file with custom install routine
├── module.yaml # yaml config and install questions
└── README.md # This file └── README.md # This file
``` ```

View File

@ -207,9 +207,10 @@ workflow {workflow_name}
├── workflows/ # ✅ Structure created, plans written ├── workflows/ # ✅ Structure created, plans written
├── tasks/ # ✅ Created ├── tasks/ # ✅ Created
├── templates/ # ✅ Created ├── templates/ # ✅ Created
├── data/ # ✅ Created ├── data/ # ✅ Created
├── _module-installer/ # ✅ Configured ├── _module-installer/ # ✅ Configured
└── README.md # ✅ Complete └── README.md # ✅ Complete
└── module.yaml # ✅ Complete
``` ```
## Completion Criteria ## Completion Criteria

View File

@ -73,8 +73,8 @@ Expected Structure:
├── templates/ [✅/❌] ├── templates/ [✅/❌]
├── data/ [✅/❌] ├── data/ [✅/❌]
├── _module-installer/ [✅/❌] ├── _module-installer/ [✅/❌]
├── install-config.yaml [✅/❌] └── installer.js [✅/N/A]
│ └── installer.js [✅/N/A] ├── module.yaml [✅/❌]
└── README.md [✅/❌] └── README.md [✅/❌]
``` ```
@ -87,7 +87,7 @@ Expected Structure:
"**2. Configuration Files Check**" "**2. Configuration Files Check**"
**Install Configuration:** **Install Configuration:**
Validate install-config.yaml Validate module.yaml
- [ ] YAML syntax valid - [ ] YAML syntax valid
- [ ] Module code matches folder name - [ ] Module code matches folder name

View File

@ -6,7 +6,7 @@
/** /**
* @param {Object} options - Installation options * @param {Object} options - Installation options
* @param {string} options.projectRoot - Project root directory * @param {string} options.projectRoot - Project root directory
* @param {Object} options.config - Module configuration from install-config.yaml * @param {Object} options.config - Module configuration from module.yaml
* @param {Array} options.installedIDEs - List of IDE codes being configured * @param {Array} options.installedIDEs - List of IDE codes being configured
* @param {Object} options.logger - Logger instance (log, warn, error methods) * @param {Object} options.logger - Logger instance (log, warn, error methods)
* @returns {boolean} - true if successful, false to abort installation * @returns {boolean} - true if successful, false to abort installation

View File

@ -13,15 +13,15 @@ This document provides the validation criteria used in step-11-validate.md to en
- [ ] data/ - Module data - [ ] data/ - Module data
- [ ] \_module-installer/ - Installation config - [ ] \_module-installer/ - Installation config
- [ ] README.md - Module documentation - [ ] README.md - Module documentation
- [ ] module.yaml - module config file
### Required Files in \_module-installer/ ### Optional File in \_module-installer/
- [ ] install-config.yaml - Installation configuration
- [ ] installer.js - Custom logic (if needed) - [ ] installer.js - Custom logic (if needed)
## Configuration Validation ## Configuration Validation
### install-config.yaml ### module.yaml
- [ ] Valid YAML syntax - [ ] Valid YAML syntax
- [ ] Module code matches folder name - [ ] Module code matches folder name

View File

@ -98,7 +98,7 @@ After getting the workflow name:
Based on the module selection, confirm the target location: Based on the module selection, confirm the target location:
- For bmb module: `{custom_workflow_location}` (defaults to `{bmad_folder}/custom/src/workflows`) - For bmb module: `{custom_workflow_location}` (defaults to `{bmad_folder}/custom/src/workflows`)
- For other modules: Check their install-config.yaml for custom workflow locations - For other modules: Check their module.yaml for custom workflow locations
- Confirm the exact folder path where the workflow will be created - Confirm the exact folder path where the workflow will be created
- Store the confirmed path as `{targetWorkflowPath}` - Store the confirmed path as `{targetWorkflowPath}`

View File

@ -109,7 +109,7 @@ Create the workflow folder structure in the target location:
``` ```
For bmb module, this will be: `{bmad_folder}/custom/src/workflows/{workflow_name}/` For bmb module, this will be: `{bmad_folder}/custom/src/workflows/{workflow_name}/`
For other modules, check their install-config.yaml for custom_workflow_location For other modules, check their module.yaml for custom_workflow_location
### 3. Generate workflow.md ### 3. Generate workflow.md

View File

@ -129,8 +129,9 @@ bmgd/
│ (Uses BMM workflows via cross-module references) │ (Uses BMM workflows via cross-module references)
├── templates/ ├── templates/
├── data/ ├── data/
├── module.yaml
└── _module-installer/ └── _module-installer/
└── install-config.yaml └── installer.js (optional)
``` ```
## Configuration ## Configuration

View File

@ -9,7 +9,7 @@ const platformCodes = require(path.join(__dirname, '../../../../tools/cli/lib/pl
* *
* @param {Object} options - Installation options * @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project * @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from install-config.yaml * @param {Object} options.config - Module configuration from module.yaml
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed * @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
* @param {Object} options.logger - Logger instance for output * @param {Object} options.logger - Logger instance for output
* @returns {Promise<boolean>} - Success status * @returns {Promise<boolean>} - Success status

View File

@ -5,7 +5,7 @@ const chalk = require('chalk');
* *
* @param {Object} options - Installation options * @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project * @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from install-config.yaml * @param {Object} options.config - Module configuration from module.yaml
* @param {Object} options.logger - Logger instance for output * @param {Object} options.logger - Logger instance for output
* @param {Object} options.platformInfo - Platform metadata from global config * @param {Object} options.platformInfo - Platform metadata from global config
* @returns {Promise<boolean>} - Success status * @returns {Promise<boolean>} - Success status

View File

@ -5,7 +5,7 @@ const chalk = require('chalk');
* *
* @param {Object} options - Installation options * @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project * @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from install-config.yaml * @param {Object} options.config - Module configuration from module.yaml
* @param {Object} options.logger - Logger instance for output * @param {Object} options.logger - Logger instance for output
* @returns {Promise<boolean>} - Success status * @returns {Promise<boolean>} - Success status
*/ */

View File

@ -8,7 +8,7 @@ const chalk = require('chalk');
* *
* @param {Object} options - Installation options * @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project * @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from install-config.yaml * @param {Object} options.config - Module configuration from module.yaml
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed * @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
* @param {Object} options.logger - Logger instance for output * @param {Object} options.logger - Logger instance for output
* @returns {Promise<boolean>} - Success status * @returns {Promise<boolean>} - Success status

View File

@ -98,7 +98,7 @@ The installer is a multi-stage system that handles agent compilation, IDE integr
``` ```
1. Collect User Input 1. Collect User Input
- Target directory, modules, IDEs - Target directory, modules, IDEs
- Custom module configuration (via install-config.yaml) - Custom module configuration (via module.yaml)
2. Pre-Installation 2. Pre-Installation
- Validate target, check conflicts, backup existing installations - Validate target, check conflicts, backup existing installations
@ -183,12 +183,12 @@ The installer supports **15 IDE environments** through a base-derived architectu
### Custom Module Configuration ### Custom Module Configuration
Modules define interactive configuration menus via `install-config.yaml` files in their `_module-installer/` directories. Modules define interactive configuration menus via `module.yaml` files in their `_module-installer/` directories.
**Config File Location**: **Config File Location**:
- Core: `src/core/_module-installer/install-config.yaml` - Core: `src/core/module.yaml`
- Modules: `src/modules/{module}/_module-installer/install-config.yaml` - Modules: `src/modules/{module}/module.yaml`
**Configuration Types**: **Configuration Types**:

View File

@ -183,24 +183,28 @@ class ConfigCollector {
// Load module's install config schema // Load module's install config schema
// First, try the standard src/modules location // First, try the standard src/modules location
let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'install-config.yaml'); let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
let moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
// If not found in src/modules, we need to find it by searching the project // If not found in src/modules, we need to find it by searching the project
if (!(await fs.pathExists(installerConfigPath))) { if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
// Use the module manager to find the module source // Use the module manager to find the module source
const { ModuleManager } = require('../modules/manager'); const { ModuleManager } = require('../modules/manager');
const moduleManager = new ModuleManager(); const moduleManager = new ModuleManager();
const moduleSourcePath = await moduleManager.findModuleSource(moduleName); const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
if (moduleSourcePath) { if (moduleSourcePath) {
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'install-config.yaml'); installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
} }
} }
let configPath = null; let configPath = null;
let isCustomModule = false; let isCustomModule = false;
if (await fs.pathExists(installerConfigPath)) { if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath; configPath = installerConfigPath;
} else { } else {
// Check if this is a custom module with custom.yaml // Check if this is a custom module with custom.yaml
@ -448,22 +452,26 @@ class ConfigCollector {
} }
// Load module's config // Load module's config
// First, try the standard src/modules location // First, try the standard src/modules location
let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'install-config.yaml'); let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
let moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
// If not found in src/modules, we need to find it by searching the project // If not found in src/modules, we need to find it by searching the project
if (!(await fs.pathExists(installerConfigPath))) { if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
// Use the module manager to find the module source // Use the module manager to find the module source
const { ModuleManager } = require('../modules/manager'); const { ModuleManager } = require('../modules/manager');
const moduleManager = new ModuleManager(); const moduleManager = new ModuleManager();
const moduleSourcePath = await moduleManager.findModuleSource(moduleName); const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
if (moduleSourcePath) { if (moduleSourcePath) {
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'install-config.yaml'); installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
} }
} }
let configPath = null; let configPath = null;
if (await fs.pathExists(installerConfigPath)) { if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath; configPath = installerConfigPath;
} else { } else {
// No config for this module // No config for this module

View File

@ -753,10 +753,39 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Resolve dependencies for selected modules // Resolve dependencies for selected modules
spinner.text = 'Resolving dependencies...'; spinner.text = 'Resolving dependencies...';
const projectRoot = getProjectRoot(); const projectRoot = getProjectRoot();
const modulesToInstall = config.installCore ? ['core', ...config.modules] : config.modules;
// Add custom content modules to the modules list for installation
let allModules = [...(config.modules || [])];
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
// Add custom modules to the installation list
for (const customFile of config.customContent.selectedFiles) {
const { CustomHandler } = require('../custom/handler');
const customHandler = new CustomHandler();
const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
if (customInfo && customInfo.id) {
allModules.push(customInfo.id);
}
}
}
const modulesToInstall = config.installCore ? ['core', ...allModules] : allModules;
// For dependency resolution, we need to pass the project root // For dependency resolution, we need to pass the project root
const resolution = await this.dependencyResolver.resolve(projectRoot, config.modules || [], { verbose: config.verbose }); // Create a temporary module manager that knows about custom content locations
const tempModuleManager = new ModuleManager({
scanProjectForModules: true,
});
// Make sure custom modules are discoverable
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
// The dependency resolver needs to know about these modules
// We'll handle custom modules separately in the installation loop
}
const resolution = await this.dependencyResolver.resolve(projectRoot, allModules, {
verbose: config.verbose,
moduleManager: tempModuleManager,
});
if (config.verbose) { if (config.verbose) {
spinner.succeed('Dependencies resolved'); spinner.succeed('Dependencies resolved');
@ -772,16 +801,90 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
} }
// Install modules with their dependencies // Install modules with their dependencies
if (config.modules && config.modules.length > 0) { if (allModules && allModules.length > 0) {
for (const moduleName of config.modules) { const installedModuleNames = new Set();
for (const moduleName of allModules) {
// Skip if already installed
if (installedModuleNames.has(moduleName)) {
continue;
}
installedModuleNames.add(moduleName);
spinner.start(`Installing module: ${moduleName}...`); spinner.start(`Installing module: ${moduleName}...`);
await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]);
// Check if this is a custom module
let isCustomModule = false;
let customInfo = null;
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
const { CustomHandler } = require('../custom/handler');
const customHandler = new CustomHandler();
for (const customFile of config.customContent.selectedFiles) {
const info = await customHandler.getCustomInfo(customFile, projectDir);
if (info && info.id === moduleName) {
isCustomModule = true;
customInfo = info;
break;
}
}
}
if (isCustomModule && customInfo) {
// Install custom module using CustomHandler but as a proper module
const { CustomHandler } = require('../custom/handler');
const customHandler = new CustomHandler();
// Install to module directory instead of custom directory
const moduleTargetPath = path.join(bmadDir, moduleName);
await fs.ensureDir(moduleTargetPath);
const result = await customHandler.install(
customInfo.path,
path.join(bmadDir, 'temp-custom'),
{ ...config.coreConfig, ...customInfo.config, _bmadDir: bmadDir },
(filePath) => {
// Track installed files with correct path
const relativePath = path.relative(path.join(bmadDir, 'temp-custom'), filePath);
const finalPath = path.join(moduleTargetPath, relativePath);
this.installedFiles.push(finalPath);
},
);
// Move from temp-custom to actual module directory
const tempCustomPath = path.join(bmadDir, 'temp-custom');
if (await fs.pathExists(tempCustomPath)) {
const customDir = path.join(tempCustomPath, 'custom');
if (await fs.pathExists(customDir)) {
// Move contents to module directory
const items = await fs.readdir(customDir);
for (const item of items) {
const srcPath = path.join(customDir, item);
const destPath = path.join(moduleTargetPath, item);
// If destination exists, remove it first (or we could merge)
if (await fs.pathExists(destPath)) {
await fs.remove(destPath);
}
await fs.move(srcPath, destPath);
}
}
await fs.remove(tempCustomPath);
}
// Create module config
await this.generateModuleConfigs(bmadDir, { [moduleName]: { ...config.coreConfig, ...customInfo.config } });
} else {
// Regular module installation
await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]);
}
spinner.succeed(`Module installed: ${moduleName}`); spinner.succeed(`Module installed: ${moduleName}`);
} }
// Install partial modules (only dependencies) // Install partial modules (only dependencies)
for (const [module, files] of Object.entries(resolution.byModule)) { for (const [module, files] of Object.entries(resolution.byModule)) {
if (!config.modules.includes(module) && module !== 'core') { if (!allModules.includes(module) && module !== 'core') {
const totalFiles = const totalFiles =
files.agents.length + files.agents.length +
files.tasks.length + files.tasks.length +
@ -799,6 +902,11 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
} }
// Install custom content if provided AND selected // Install custom content if provided AND selected
// Process custom content that wasn't installed as modules
// This is now handled in the module installation loop above
// This section is kept for backward compatibility with any custom content
// that doesn't have a module structure
const remainingCustomContent = [];
if ( if (
config.customContent && config.customContent &&
config.customContent.hasCustomContent && config.customContent.hasCustomContent &&
@ -806,12 +914,26 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
config.customContent.selected && config.customContent.selected &&
config.customContent.selectedFiles config.customContent.selectedFiles
) { ) {
spinner.start('Installing custom content...'); // Filter out custom modules that were already installed
for (const customFile of config.customContent.selectedFiles) {
const { CustomHandler } = require('../custom/handler');
const customHandler = new CustomHandler();
const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
// Skip if this was installed as a module
if (!customInfo || !customInfo.id || !allModules.includes(customInfo.id)) {
remainingCustomContent.push(customFile);
}
}
}
if (remainingCustomContent.length > 0) {
spinner.start('Installing remaining custom content...');
const { CustomHandler } = require('../custom/handler'); const { CustomHandler } = require('../custom/handler');
const customHandler = new CustomHandler(); const customHandler = new CustomHandler();
// Use the selected files instead of finding all files // Use the remaining files
const customFiles = config.customContent.selectedFiles; const customFiles = remainingCustomContent;
if (customFiles.length > 0) { if (customFiles.length > 0) {
console.log(chalk.cyan(`\n Found ${customFiles.length} custom content file(s):`)); console.log(chalk.cyan(`\n Found ${customFiles.length} custom content file(s):`));
@ -867,10 +989,10 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
spinner.start('Generating workflow and agent manifests...'); spinner.start('Generating workflow and agent manifests...');
const manifestGen = new ManifestGenerator(); const manifestGen = new ManifestGenerator();
// Include preserved modules (from quick update) in the manifest // Include preserved modules (from quick update) and custom modules in the manifest
const allModulesToList = config._preserveModules ? [...(config.modules || []), ...config._preserveModules] : config.modules || []; const allModulesToList = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules || [];
const manifestStats = await manifestGen.generateManifests(bmadDir, config.modules || [], this.installedFiles, { const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesToList, this.installedFiles, {
ides: config.ides || [], ides: config.ides || [],
preservedModules: config._preserveModules || [], // Scan these from installed bmad/ dir preservedModules: config._preserveModules || [], // Scan these from installed bmad/ dir
}); });

View File

@ -3,6 +3,7 @@ const fs = require('fs-extra');
const chalk = require('chalk'); const chalk = require('chalk');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const { FileOps } = require('../../../lib/file-ops'); const { FileOps } = require('../../../lib/file-ops');
const { XmlHandler } = require('../../../lib/xml-handler');
/** /**
* Handler for custom content (custom.yaml) * Handler for custom content (custom.yaml)
@ -11,6 +12,7 @@ const { FileOps } = require('../../../lib/file-ops');
class CustomHandler { class CustomHandler {
constructor() { constructor() {
this.fileOps = new FileOps(); this.fileOps = new FileOps();
this.xmlHandler = new XmlHandler();
} }
/** /**
@ -52,6 +54,12 @@ class CustomHandler {
} else if (entry.name === 'custom.yaml') { } else if (entry.name === 'custom.yaml') {
// Found a custom.yaml file // Found a custom.yaml file
customPaths.push(fullPath); customPaths.push(fullPath);
} else if (
entry.name === 'module.yaml' && // Check if this is a custom module (either in _module-installer or in root directory)
// Skip if it's in src/modules (those are standard modules)
!fullPath.includes(path.join('src', 'modules'))
) {
customPaths.push(fullPath);
} }
} }
} catch { } catch {
@ -66,40 +74,44 @@ class CustomHandler {
} }
/** /**
* Get custom content info from a custom.yaml file * Get custom content info from a custom.yaml or module.yaml file
* @param {string} customYamlPath - Path to custom.yaml file * @param {string} configPath - Path to config file
* @param {string} projectRoot - Project root directory for calculating relative paths * @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, projectRoot = null) { async getCustomInfo(configPath, projectRoot = null) {
try { try {
const configContent = await fs.readFile(customYamlPath, 'utf8'); const configContent = await fs.readFile(configPath, 'utf8');
// Try to parse YAML with error handling // Try to parse YAML with error handling
let config; let config;
try { try {
config = yaml.load(configContent); config = yaml.load(configContent);
} catch (parseError) { } catch (parseError) {
console.warn(chalk.yellow(`Warning: YAML parse error in ${customYamlPath}:`, parseError.message)); console.warn(chalk.yellow(`Warning: YAML parse error in ${configPath}:`, parseError.message));
return null; return null;
} }
const customDir = path.dirname(customYamlPath); // Check if this is an module.yaml (module) or custom.yaml (custom content)
const isInstallConfig = configPath.endsWith('module.yaml');
const configDir = path.dirname(configPath);
// Use provided projectRoot or fall back to process.cwd() // Use provided projectRoot or fall back to process.cwd()
const basePath = projectRoot || process.cwd(); const basePath = projectRoot || process.cwd();
const relativePath = path.relative(basePath, customDir); const relativePath = path.relative(basePath, configDir);
return { return {
id: config.code || path.basename(customDir), id: config.code || 'unknown-code',
name: config.name || `Custom: ${path.basename(customDir)}`, name: config.name,
description: config.description || 'Custom agents and workflows', description: config.description || '',
path: customDir, path: configDir,
relativePath: relativePath, relativePath: relativePath,
defaultSelected: config.default_selected === true, defaultSelected: config.default_selected === true,
config: config, config: config,
isInstallConfig: isInstallConfig, // Track which type this is
}; };
} catch (error) { } catch (error) {
console.warn(chalk.yellow(`Warning: Failed to read ${customYamlPath}:`, error.message)); console.warn(chalk.yellow(`Warning: Failed to read ${configPath}:`, error.message));
return null; return null;
} }
} }
@ -131,10 +143,10 @@ class CustomHandler {
await fs.ensureDir(bmadAgentsDir); await fs.ensureDir(bmadAgentsDir);
await fs.ensureDir(bmadWorkflowsDir); await fs.ensureDir(bmadWorkflowsDir);
// Process agents - copy entire agents directory structure // Process agents - compile and copy agents
const agentsDir = path.join(customPath, 'agents'); const agentsDir = path.join(customPath, 'agents');
if (await fs.pathExists(agentsDir)) { if (await fs.pathExists(agentsDir)) {
await this.copyDirectory(agentsDir, bmadAgentsDir, results, fileTrackingCallback, config); await this.compileAndCopyAgents(agentsDir, bmadAgentsDir, bmadDir, config, fileTrackingCallback, results);
// Count agent files // Count agent files
const agentFiles = await this.findFilesRecursively(agentsDir, ['.agent.yaml', '.md']); const agentFiles = await this.findFilesRecursively(agentsDir, ['.agent.yaml', '.md']);
@ -271,6 +283,114 @@ class CustomHandler {
} }
} }
} }
/**
* Compile .agent.yaml files to .md format and handle sidecars
* @param {string} sourceAgentsPath - Source agents directory
* @param {string} targetAgentsPath - Target agents directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} config - Configuration for placeholder replacement
* @param {Function} fileTrackingCallback - Optional callback to track installed files
* @param {Object} results - Results object to update
*/
async compileAndCopyAgents(sourceAgentsPath, targetAgentsPath, bmadDir, config, fileTrackingCallback, results) {
// Get all .agent.yaml files recursively
const agentFiles = await this.findFilesRecursively(sourceAgentsPath, ['.agent.yaml']);
for (const agentFile of agentFiles) {
const relativePath = path.relative(sourceAgentsPath, agentFile);
const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));
await fs.ensureDir(targetDir);
const agentName = path.basename(agentFile, '.agent.yaml');
const targetMdPath = path.join(targetDir, `${agentName}.md`);
// Use the actual bmadDir if available (for when installing to temp dir)
const actualBmadDir = config._bmadDir || bmadDir;
const customizePath = path.join(actualBmadDir, '_cfg', 'agents', `custom-${agentName}.customize.yaml`);
// Read and compile the YAML
try {
const yamlContent = await fs.readFile(agentFile, 'utf8');
const { compileAgent } = require('../../../lib/agent/compiler');
// Create customize template if it doesn't exist
if (!(await fs.pathExists(customizePath))) {
const { getSourcePath } = require('../../../lib/project-root');
const genericTemplatePath = getSourcePath('utility', 'templates', 'agent.customize.template.yaml');
if (await fs.pathExists(genericTemplatePath)) {
// Copy with placeholder replacement
let templateContent = await fs.readFile(genericTemplatePath, 'utf8');
templateContent = templateContent.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
await fs.writeFile(customizePath, templateContent, 'utf8');
console.log(chalk.dim(` Created customize: custom-${agentName}.customize.yaml`));
}
}
// Compile the agent
const { xml } = compileAgent(yamlContent, {}, agentName, relativePath, { config });
// Replace placeholders in the compiled content
let processedXml = xml;
processedXml = processedXml.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
processedXml = processedXml.replaceAll('{user_name}', config.user_name || 'User');
processedXml = processedXml.replaceAll('{communication_language}', config.communication_language || 'English');
processedXml = processedXml.replaceAll('{output_folder}', config.output_folder || 'docs');
// Write the compiled MD file
await fs.writeFile(targetMdPath, processedXml, 'utf8');
// Check if agent has sidecar
let hasSidecar = false;
try {
const yamlLib = require('yaml');
const agentYaml = yamlLib.parse(yamlContent);
hasSidecar = agentYaml?.agent?.metadata?.hasSidecar === true;
} catch {
// Continue without sidecar processing
}
// Copy sidecar files if agent has hasSidecar flag
if (hasSidecar && config.agent_sidecar_folder) {
const { copyAgentSidecarFiles } = require('../../../lib/agent/installer');
// Resolve agent sidecar folder path
const projectDir = path.dirname(bmadDir);
const resolvedSidecarFolder = config.agent_sidecar_folder
.replaceAll('{project-root}', projectDir)
.replaceAll('{bmad_folder}', path.basename(bmadDir));
// Create sidecar directory for this agent
const agentSidecarDir = path.join(resolvedSidecarFolder, agentName);
await fs.ensureDir(agentSidecarDir);
// Copy sidecar files
const sidecarResult = copyAgentSidecarFiles(path.dirname(agentFile), agentSidecarDir, agentFile);
if (sidecarResult.copied.length > 0) {
console.log(chalk.dim(` Copied ${sidecarResult.copied.length} sidecar file(s) to: ${agentSidecarDir}`));
}
if (sidecarResult.preserved.length > 0) {
console.log(chalk.dim(` Preserved ${sidecarResult.preserved.length} existing sidecar file(s)`));
}
}
// Track the file
if (fileTrackingCallback) {
fileTrackingCallback(targetMdPath);
}
console.log(
chalk.dim(
` Compiled agent: ${agentName} -> ${path.relative(targetAgentsPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
),
);
} catch (error) {
console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message));
results.errors.push(`Failed to compile agent ${agentName}: ${error.message}`);
}
}
}
} }
module.exports = { CustomHandler }; module.exports = { CustomHandler };

View File

@ -107,7 +107,7 @@ class ModuleManager {
} }
/** /**
* Find all modules in the project by searching for install-config.yaml files * Find all modules in the project by searching for module.yaml files
* @returns {Array} List of module paths * @returns {Array} List of module paths
*/ */
async findModulesInProject() { async findModulesInProject() {
@ -144,12 +144,14 @@ class ModuleManager {
continue; continue;
} }
// Check if this directory contains a module (install-config.yaml OR custom.yaml) // Check if this directory contains a module (module.yaml OR custom.yaml)
const installerConfigPath = path.join(fullPath, '_module-installer', 'install-config.yaml'); const moduleConfigPath = path.join(fullPath, 'module.yaml');
const installerConfigPath = path.join(fullPath, '_module-installer', 'module.yaml');
const customConfigPath = path.join(fullPath, '_module-installer', 'custom.yaml'); const customConfigPath = path.join(fullPath, '_module-installer', 'custom.yaml');
const rootCustomConfigPath = path.join(fullPath, 'custom.yaml'); const rootCustomConfigPath = path.join(fullPath, 'custom.yaml');
if ( if (
(await fs.pathExists(moduleConfigPath)) ||
(await fs.pathExists(installerConfigPath)) || (await fs.pathExists(installerConfigPath)) ||
(await fs.pathExists(customConfigPath)) || (await fs.pathExists(customConfigPath)) ||
(await fs.pathExists(rootCustomConfigPath)) (await fs.pathExists(rootCustomConfigPath))
@ -189,12 +191,17 @@ class ModuleManager {
for (const entry of entries) { for (const entry of entries) {
if (entry.isDirectory()) { if (entry.isDirectory()) {
const modulePath = path.join(this.modulesSourcePath, entry.name); const modulePath = path.join(this.modulesSourcePath, entry.name);
// Check for module structure (install-config.yaml OR custom.yaml) // Check for module structure (module.yaml OR custom.yaml)
const installerConfigPath = path.join(modulePath, '_module-installer', 'install-config.yaml'); const moduleConfigPath = path.join(modulePath, 'module.yaml');
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml'); const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
// Skip if this doesn't look like a module // Skip if this doesn't look like a module
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(customConfigPath))) { if (
!(await fs.pathExists(moduleConfigPath)) &&
!(await fs.pathExists(installerConfigPath)) &&
!(await fs.pathExists(customConfigPath))
) {
continue; continue;
} }
@ -246,13 +253,16 @@ class ModuleManager {
* @returns {Object|null} Module info or null if not a valid module * @returns {Object|null} Module info or null if not a valid module
*/ */
async getModuleInfo(modulePath, defaultName, sourceDescription) { async getModuleInfo(modulePath, defaultName, sourceDescription) {
// Check for module structure (install-config.yaml OR custom.yaml) // Check for module structure (module.yaml OR custom.yaml)
const installerConfigPath = path.join(modulePath, '_module-installer', 'install-config.yaml'); const moduleConfigPath = path.join(modulePath, 'module.yaml');
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml'); const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
const rootCustomConfigPath = path.join(modulePath, 'custom.yaml'); const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
let configPath = null; let configPath = null;
if (await fs.pathExists(installerConfigPath)) { if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath; configPath = installerConfigPath;
} else if (await fs.pathExists(customConfigPath)) { } else if (await fs.pathExists(customConfigPath)) {
configPath = customConfigPath; configPath = customConfigPath;
@ -313,10 +323,11 @@ class ModuleManager {
// First, check src/modules // First, check src/modules
const srcModulePath = path.join(this.modulesSourcePath, moduleName); const srcModulePath = path.join(this.modulesSourcePath, moduleName);
if (await fs.pathExists(srcModulePath)) { if (await fs.pathExists(srcModulePath)) {
// Check if this looks like a module (has install-config.yaml) // Check if this looks like a module (has module.yaml)
const installerConfigPath = path.join(srcModulePath, '_module-installer', 'install-config.yaml'); const moduleConfigPath = path.join(srcModulePath, 'module.yaml');
const installerConfigPath = path.join(srcModulePath, '_module-installer', 'module.yaml');
if (await fs.pathExists(installerConfigPath)) { if ((await fs.pathExists(moduleConfigPath)) || (await fs.pathExists(installerConfigPath))) {
return srcModulePath; return srcModulePath;
} }
@ -338,12 +349,15 @@ class ModuleManager {
// Also check by module ID (not just folder name) // Also check by module ID (not just folder name)
// Need to read configs to match by ID // Need to read configs to match by ID
for (const modulePath of allModulePaths) { for (const modulePath of allModulePaths) {
const installerConfigPath = path.join(modulePath, '_module-installer', 'install-config.yaml'); const moduleConfigPath = path.join(modulePath, 'module.yaml');
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml'); const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
const rootCustomConfigPath = path.join(modulePath, 'custom.yaml'); const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
let configPath = null; let configPath = null;
if (await fs.pathExists(installerConfigPath)) { if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath; configPath = installerConfigPath;
} else if (await fs.pathExists(customConfigPath)) { } else if (await fs.pathExists(customConfigPath)) {
configPath = customConfigPath; configPath = customConfigPath;
@ -584,7 +598,7 @@ class ModuleManager {
} }
// Skip _module-installer directory - it's only needed at install time // Skip _module-installer directory - it's only needed at install time
if (file.startsWith('_module-installer/')) { if (file.startsWith('_module-installer/') || file === 'module.yaml') {
continue; continue;
} }

View File

@ -84,8 +84,8 @@ const CLIUtils = {
/** /**
* Display module configuration header * Display module configuration header
* @param {string} moduleName - Module name (fallback if no custom header) * @param {string} moduleName - Module name (fallback if no custom header)
* @param {string} header - Custom header from install-config.yaml * @param {string} header - Custom header from module.yaml
* @param {string} subheader - Custom subheader from install-config.yaml * @param {string} subheader - Custom subheader from module.yaml
*/ */
displayModuleConfigHeader(moduleName, header = null, subheader = null) { displayModuleConfigHeader(moduleName, header = null, subheader = null) {
// Simple blue banner with custom header/subheader if provided // Simple blue banner with custom header/subheader if provided
@ -100,8 +100,8 @@ const CLIUtils = {
/** /**
* Display module with no custom configuration * Display module with no custom configuration
* @param {string} moduleName - Module name (fallback if no custom header) * @param {string} moduleName - Module name (fallback if no custom header)
* @param {string} header - Custom header from install-config.yaml * @param {string} header - Custom header from module.yaml
* @param {string} subheader - Custom subheader from install-config.yaml * @param {string} subheader - Custom subheader from module.yaml
*/ */
displayModuleNoConfig(moduleName, header = null, subheader = null) { displayModuleNoConfig(moduleName, header = null, subheader = null) {
// Show full banner with header/subheader, just like modules with config // Show full banner with header/subheader, just like modules with config

View File

@ -142,8 +142,19 @@ class UI {
if (selectedCustomContent.length > 0) { if (selectedCustomContent.length > 0) {
customContentConfig.selected = true; customContentConfig.selected = true;
customContentConfig.selectedFiles = selectedCustomContent.map((mod) => mod.replace('__CUSTOM_CONTENT__', '')); customContentConfig.selectedFiles = selectedCustomContent.map((mod) => mod.replace('__CUSTOM_CONTENT__', ''));
// Filter out custom content markers since they're not real modules // Convert custom content to module IDs for installation
selectedModules = selectedModules.filter((mod) => !mod.startsWith('__CUSTOM_CONTENT__')); const customContentModuleIds = [];
const { CustomHandler } = require('../installers/lib/custom/handler');
const customHandler = new CustomHandler();
for (const customFile of customContentConfig.selectedFiles) {
// Get the module info to extract the ID
const customInfo = await customHandler.getCustomInfo(customFile);
if (customInfo) {
customContentModuleIds.push(customInfo.id);
}
}
// Filter out custom content markers and add module IDs
selectedModules = [...selectedModules.filter((mod) => !mod.startsWith('__CUSTOM_CONTENT__')), ...customContentModuleIds];
} else if (customContentConfig.hasCustomContent) { } else if (customContentConfig.hasCustomContent) {
// User provided custom content but didn't select any // User provided custom content but didn't select any
customContentConfig.selected = false; customContentConfig.selected = false;
@ -669,7 +680,7 @@ class UI {
*/ */
async promptCustomContentLocation() { async promptCustomContentLocation() {
try { try {
CLIUtils.displaySection('Custom Content', 'Optional: Add custom agents and workflows'); CLIUtils.displaySection('Custom Content', 'Optional: Add custom agents, workflows, and modules');
const { hasCustomContent } = await inquirer.prompt([ const { hasCustomContent } = await inquirer.prompt([
{ {
@ -703,7 +714,7 @@ class UI {
{ {
type: 'input', type: 'input',
name: 'directory', name: 'directory',
message: 'Enter the path to your custom content directory:', message: 'Enter directory to search for custom content (will scan subfolders):',
default: process.cwd(), // Use actual current working directory default: process.cwd(), // Use actual current working directory
validate: async (input) => { validate: async (input) => {
if (!input || input.trim() === '') { if (!input || input.trim() === '') {
@ -736,7 +747,7 @@ class UI {
const customFiles = await customHandler.findCustomContent(expandedPath); const customFiles = await customHandler.findCustomContent(expandedPath);
if (customFiles.length === 0) { if (customFiles.length === 0) {
console.log(chalk.yellow(`\nNo custom.yaml files found in ${expandedPath}`)); console.log(chalk.yellow(`\nNo custom content found in ${expandedPath}`));
const { tryAgain } = await inquirer.prompt([ const { tryAgain } = await inquirer.prompt([
{ {
@ -755,7 +766,12 @@ class UI {
} }
customPath = expandedPath; customPath = expandedPath;
console.log(chalk.green(`\n✓ Found ${customFiles.length} custom content file(s)`)); console.log(chalk.green(`\n✓ Found ${customFiles.length} custom content item(s):`));
for (const file of customFiles) {
const relativePath = path.relative(expandedPath, path.dirname(file));
const folderName = path.dirname(file).split(path.sep).pop();
console.log(chalk.dim(`${folderName} ${chalk.gray(`(${relativePath})`)}`));
}
} }
return { hasCustomContent: true, customPath }; return { hasCustomContent: true, customPath };