Fix gemini installer to use UnifiedInstaller with .toml support

- Add fileExtension parameter support to path-utils (toColonPath, toDashPath)
- Add TemplateType.GEMINI to unified-installer for TOML output format
- Update task-tool-command-generator to support TOML format
- Refactor gemini.js to use UnifiedInstaller with:
  - NamingStyle.FLAT_DASH for dash-separated filenames
  - TemplateType.GEMINI for TOML format (description + prompt fields)
  - fileExtension: '.toml' for Gemini CLI

TOML format:
description = "BMAD Agent: Title"
prompt = """
Content here
"""
This commit is contained in:
Brian Madison
2026-01-24 12:04:34 -06:00
parent c5d0fb55ba
commit 4cb5cc7dbc
4 changed files with 182 additions and 199 deletions

View File

@@ -3,8 +3,7 @@ const fs = require('fs-extra');
const yaml = require('yaml'); const yaml = require('yaml');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk'); const chalk = require('chalk');
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
/** /**
* Gemini CLI setup handler * Gemini CLI setup handler
@@ -15,8 +14,6 @@ class GeminiSetup extends BaseIdeSetup {
super('gemini', 'Gemini CLI', false); super('gemini', 'Gemini CLI', false);
this.configDir = '.gemini'; this.configDir = '.gemini';
this.commandsDir = 'commands'; this.commandsDir = 'commands';
this.agentTemplatePath = path.join(__dirname, 'templates', 'gemini-agent-command.toml');
this.taskTemplatePath = path.join(__dirname, 'templates', 'gemini-task-command.toml');
} }
/** /**
@@ -62,169 +59,39 @@ class GeminiSetup extends BaseIdeSetup {
await this.ensureDir(commandsDir); await this.ensureDir(commandsDir);
// Clean up any existing BMAD files before reinstalling // Use UnifiedInstaller for agents and workflows
await this.cleanup(projectDir); const installer = new UnifiedInstaller(this.bmadFolderName);
// Generate agent launchers const config = {
const agentGen = new AgentCommandGenerator(this.bmadFolderName); targetDir: commandsDir,
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); namingStyle: NamingStyle.FLAT_DASH,
templateType: TemplateType.GEMINI,
fileExtension: '.toml',
};
// Get tasks and workflows (ALL workflows now generate commands) const counts = await installer.install(projectDir, bmadDir, config, options.selectedModules || []);
const tasks = await this.getTasks(bmadDir);
// Get ALL workflows using the new workflow command generator // Generate activation names for display
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); const agentActivation = `/bmad_agents_{agent-name}`;
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); const workflowActivation = `/bmad_workflows_{workflow-name}`;
const taskActivation = `/bmad_tasks_{task-name}`;
// Install agents as TOML files with bmad- prefix (flat structure)
let agentCount = 0;
for (const artifact of agentArtifacts) {
const tomlContent = await this.createAgentLauncherToml(artifact);
// Flat structure: bmad-agent-{module}-{name}.toml
const tomlPath = path.join(commandsDir, `bmad-agent-${artifact.module}-${artifact.name}.toml`);
await this.writeFile(tomlPath, tomlContent);
agentCount++;
console.log(chalk.green(` ✓ Added agent: /bmad_agents_${artifact.module}_${artifact.name}`));
}
// Install tasks as TOML files with bmad- prefix (flat structure)
let taskCount = 0;
for (const task of tasks) {
const content = await this.readFile(task.path);
const tomlContent = await this.createTaskToml(task, content);
// Flat structure: bmad-task-{module}-{name}.toml
const tomlPath = path.join(commandsDir, `bmad-task-${task.module}-${task.name}.toml`);
await this.writeFile(tomlPath, tomlContent);
taskCount++;
console.log(chalk.green(` ✓ Added task: /bmad_tasks_${task.module}_${task.name}`));
}
// Install workflows as TOML files with bmad- prefix (flat structure)
let workflowCount = 0;
for (const artifact of workflowArtifacts) {
if (artifact.type === 'workflow-command') {
// Create TOML wrapper around workflow command content
const tomlContent = await this.createWorkflowToml(artifact);
// Flat structure: bmad-workflow-{module}-{name}.toml
const workflowName = path.basename(artifact.relativePath, '.md');
const tomlPath = path.join(commandsDir, `bmad-workflow-${artifact.module}-${workflowName}.toml`);
await this.writeFile(tomlPath, tomlContent);
workflowCount++;
console.log(chalk.green(` ✓ Added workflow: /bmad_workflows_${artifact.module}_${workflowName}`));
}
}
console.log(chalk.green(`${this.name} configured:`)); console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents configured`)); console.log(chalk.dim(` - ${counts.agents} agents configured`));
console.log(chalk.dim(` - ${taskCount} tasks configured`)); console.log(chalk.dim(` - ${counts.workflows} workflows configured`));
console.log(chalk.dim(` - ${workflowCount} workflows configured`)); console.log(chalk.dim(` - ${counts.tasks} tasks configured`));
console.log(chalk.dim(` - ${counts.tools} tools configured`));
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`)); console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
console.log(chalk.dim(` - Agent activation: /bmad_agents_{agent-name}`)); console.log(chalk.dim(` - Agent activation: ${agentActivation}`));
console.log(chalk.dim(` - Task activation: /bmad_tasks_{task-name}`)); console.log(chalk.dim(` - Workflow activation: ${workflowActivation}`));
console.log(chalk.dim(` - Workflow activation: /bmad_workflows_{workflow-name}`)); console.log(chalk.dim(` - Task activation: ${taskActivation}`));
return { return {
success: true, success: true,
agents: agentCount, ...counts,
tasks: taskCount,
workflows: workflowCount,
}; };
} }
/**
* Create agent launcher TOML content from artifact
*/
async createAgentLauncherToml(artifact) {
// Strip frontmatter from launcher content
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
const contentWithoutFrontmatter = artifact.content.replace(frontmatterRegex, '').trim();
// Extract title from launcher frontmatter
const titleMatch = artifact.content.match(/description:\s*"([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name);
// Create TOML wrapper around launcher content (without frontmatter)
const description = `BMAD ${artifact.module.toUpperCase()} Agent: ${title}`;
return `description = "${description}"
prompt = """
${contentWithoutFrontmatter}
"""
`;
}
/**
* Create agent TOML content using template
*/
async createAgentToml(agent, content) {
// Extract metadata
const titleMatch = content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
// Load template
const template = await fs.readFile(this.agentTemplatePath, 'utf8');
// Replace template variables
// Note: {user_name} and other {config_values} are left as-is for runtime substitution by Gemini
const tomlContent = template
.replaceAll('{{title}}', title)
.replaceAll('{_bmad}', '_bmad')
.replaceAll('{_bmad}', this.bmadFolderName)
.replaceAll('{{module}}', agent.module)
.replaceAll('{{name}}', agent.name);
return tomlContent;
}
/**
* Create task TOML content using template
*/
async createTaskToml(task, content) {
// Extract task name from XML if available
const nameMatch = content.match(/<name>([^<]+)<\/name>/);
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
// Load template
const template = await fs.readFile(this.taskTemplatePath, 'utf8');
// Replace template variables
const tomlContent = template
.replaceAll('{{taskName}}', taskName)
.replaceAll('{_bmad}', '_bmad')
.replaceAll('{_bmad}', this.bmadFolderName)
.replaceAll('{{module}}', task.module)
.replaceAll('{{filename}}', task.filename);
return tomlContent;
}
/**
* Create workflow TOML content from artifact
*/
async createWorkflowToml(artifact) {
// Extract description from artifact content
const descriptionMatch = artifact.content.match(/description:\s*"([^"]+)"/);
const description = descriptionMatch
? descriptionMatch[1]
: `BMAD ${artifact.module.toUpperCase()} Workflow: ${path.basename(artifact.relativePath, '.md')}`;
// Strip frontmatter from command content
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
const contentWithoutFrontmatter = artifact.content.replace(frontmatterRegex, '').trim();
return `description = "${description}"
prompt = """
${contentWithoutFrontmatter}
"""
`;
}
/** /**
* Cleanup Gemini configuration - surgically remove only BMAD files * Cleanup Gemini configuration - surgically remove only BMAD files
*/ */

View File

@@ -5,6 +5,9 @@
* - Underscore format (bmad_module_name.md) - Windows-compatible universal format * - Underscore format (bmad_module_name.md) - Windows-compatible universal format
*/ */
// Default file extension for backward compatibility
const DEFAULT_FILE_EXTENSION = '.md';
// Type segments - agents are included in naming, others are filtered out // Type segments - agents are included in naming, others are filtered out
const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools']; const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools'];
const AGENT_SEGMENT = 'agents'; const AGENT_SEGMENT = 'agents';
@@ -18,15 +21,16 @@ const AGENT_SEGMENT = 'agents';
* @param {string} module - Module name (e.g., 'bmm', 'core') * @param {string} module - Module name (e.g., 'bmm', 'core')
* @param {string} type - Artifact type ('agents', 'workflows', 'tasks', 'tools') * @param {string} type - Artifact type ('agents', 'workflows', 'tasks', 'tools')
* @param {string} name - Artifact name (e.g., 'pm', 'brainstorming') * @param {string} name - Artifact name (e.g., 'pm', 'brainstorming')
* @param {string} [fileExtension=DEFAULT_FILE_EXTENSION] - File extension including dot (e.g., '.md', '.toml')
* @returns {string} Flat filename like 'bmad_bmm_agent_pm.md' or 'bmad_bmm_correct-course.md' * @returns {string} Flat filename like 'bmad_bmm_agent_pm.md' or 'bmad_bmm_correct-course.md'
*/ */
function toUnderscoreName(module, type, name) { function toUnderscoreName(module, type, name, fileExtension = DEFAULT_FILE_EXTENSION) {
const isAgent = type === AGENT_SEGMENT; const isAgent = type === AGENT_SEGMENT;
// For core module, skip the module prefix: use 'bmad_name.md' instead of 'bmad_core_name.md' // For core module, skip the module prefix: use 'bmad_name.md' instead of 'bmad_core_name.md'
if (module === 'core') { if (module === 'core') {
return isAgent ? `bmad_agent_${name}.md` : `bmad_${name}.md`; return isAgent ? `bmad_agent_${name}${fileExtension}` : `bmad_${name}${fileExtension}`;
} }
return isAgent ? `bmad_${module}_agent_${name}.md` : `bmad_${module}_${name}.md`; return isAgent ? `bmad_${module}_agent_${name}${fileExtension}` : `bmad_${module}_${name}${fileExtension}`;
} }
/** /**
@@ -36,10 +40,14 @@ function toUnderscoreName(module, type, name) {
* Converts: 'core/agents/brainstorming.md' → 'bmad_agent_brainstorming.md' (core items skip module prefix) * Converts: 'core/agents/brainstorming.md' → 'bmad_agent_brainstorming.md' (core items skip module prefix)
* *
* @param {string} relativePath - Path like 'bmm/agents/pm.md' * @param {string} relativePath - Path like 'bmm/agents/pm.md'
* @param {string} [fileExtension=DEFAULT_FILE_EXTENSION] - File extension including dot (e.g., '.md', '.toml')
* @returns {string} Flat filename like 'bmad_bmm_agent_pm.md' or 'bmad_brainstorming.md' * @returns {string} Flat filename like 'bmad_bmm_agent_pm.md' or 'bmad_brainstorming.md'
*/ */
function toUnderscorePath(relativePath) { function toUnderscorePath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) {
const withoutExt = relativePath.replace('.md', ''); // Extract extension from relativePath to properly remove it
const extMatch = relativePath.match(/\.[^.]+$/);
const originalExt = extMatch ? extMatch[0] : '';
const withoutExt = relativePath.replace(originalExt, '');
const parts = withoutExt.split(/[/\\]/); const parts = withoutExt.split(/[/\\]/);
const module = parts[0]; const module = parts[0];
@@ -47,7 +55,7 @@ function toUnderscorePath(relativePath) {
const name = parts.slice(2).join('_'); const name = parts.slice(2).join('_');
// Use toUnderscoreName for consistency // Use toUnderscoreName for consistency
return toUnderscoreName(module, type, name); return toUnderscoreName(module, type, name, fileExtension);
} }
/** /**
@@ -55,10 +63,11 @@ function toUnderscorePath(relativePath) {
* Creates: 'bmad_custom_fred-commit-poet.md' * Creates: 'bmad_custom_fred-commit-poet.md'
* *
* @param {string} agentName - Custom agent name * @param {string} agentName - Custom agent name
* @param {string} [fileExtension=DEFAULT_FILE_EXTENSION] - File extension including dot (e.g., '.md', '.toml')
* @returns {string} Flat filename like 'bmad_custom_fred-commit-poet.md' * @returns {string} Flat filename like 'bmad_custom_fred-commit-poet.md'
*/ */
function customAgentUnderscoreName(agentName) { function customAgentUnderscoreName(agentName, fileExtension = DEFAULT_FILE_EXTENSION) {
return `bmad_custom_${agentName}.md`; return `bmad_custom_${agentName}${fileExtension}`;
} }
/** /**
@@ -134,9 +143,9 @@ function parseUnderscoreName(filename) {
} }
// Backward compatibility aliases (deprecated) // Backward compatibility aliases (deprecated)
// Note: These now use toDashPath and customAgentDashName which convert underscores to dashes
const toColonName = toUnderscoreName; const toColonName = toUnderscoreName;
const toColonPath = toUnderscorePath; const toDashName = toUnderscoreName;
const toDashPath = toUnderscorePath;
const customAgentColonName = customAgentUnderscoreName; const customAgentColonName = customAgentUnderscoreName;
const customAgentDashName = customAgentUnderscoreName; const customAgentDashName = customAgentUnderscoreName;
const isColonFormat = isUnderscoreFormat; const isColonFormat = isUnderscoreFormat;
@@ -144,7 +153,46 @@ const isDashFormat = isUnderscoreFormat;
const parseColonName = parseUnderscoreName; const parseColonName = parseUnderscoreName;
const parseDashName = parseUnderscoreName; const parseDashName = parseUnderscoreName;
/**
* Convert relative path to flat colon-separated name (for backward compatibility)
* This is actually the same as underscore format now (underscores in filenames)
* @param {string} relativePath - Path like 'bmm/agents/pm.md'
* @param {string} [fileExtension=DEFAULT_FILE_EXTENSION] - File extension including dot
* @returns {string} Flat filename like 'bmad_bmm_agent_pm.md'
*/
function toColonPath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) {
return toUnderscorePath(relativePath, fileExtension);
}
/**
* Convert relative path to flat dash-separated name
* Converts: 'bmm/agents/pm.md' → 'bmad-bmm-agent-pm.md'
* Converts: 'bmm/workflows/correct-course' → 'bmad-bmm-correct-course.md'
* @param {string} relativePath - Path like 'bmm/agents/pm.md'
* @param {string} [fileExtension=DEFAULT_FILE_EXTENSION] - File extension including dot
* @returns {string} Flat filename like 'bmad-bmm-agent-pm.md'
*/
function toDashPath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) {
// Extract extension from relativePath to properly remove it
const extMatch = relativePath.match(/\.[^.]+$/);
const originalExt = extMatch ? extMatch[0] : '';
const withoutExt = relativePath.replace(originalExt, '');
const parts = withoutExt.split(/[/\\]/);
const module = parts[0];
const type = parts[1];
const name = parts.slice(2).join('-');
// Use dash naming style
const isAgent = type === AGENT_SEGMENT;
if (module === 'core') {
return isAgent ? `bmad-agent-${name}${fileExtension}` : `bmad-${name}${fileExtension}`;
}
return isAgent ? `bmad-${module}-agent-${name}${fileExtension}` : `bmad-${module}-${name}${fileExtension}`;
}
module.exports = { module.exports = {
DEFAULT_FILE_EXTENSION,
toUnderscoreName, toUnderscoreName,
toUnderscorePath, toUnderscorePath,
customAgentUnderscoreName, customAgentUnderscoreName,
@@ -153,6 +201,7 @@ module.exports = {
// Backward compatibility aliases // Backward compatibility aliases
toColonName, toColonName,
toColonPath, toColonPath,
toDashName,
toDashPath, toDashPath,
customAgentColonName, customAgentColonName,
customAgentDashName, customAgentDashName,

View File

@@ -16,8 +16,11 @@ class TaskToolCommandGenerator {
/** /**
* Generate command content for a task or tool * Generate command content for a task or tool
* @param {Object} item - Task or tool item from manifest
* @param {string} type - 'task' or 'tool'
* @param {string} [format='yaml'] - Output format: 'yaml' or 'toml'
*/ */
generateCommandContent(item, type) { generateCommandContent(item, type, format = 'yaml') {
const description = item.description || `Execute ${item.displayName || item.name}`; const description = item.description || `Execute ${item.displayName || item.name}`;
// Convert path to use {project-root} placeholder // Convert path to use {project-root} placeholder
@@ -26,16 +29,29 @@ class TaskToolCommandGenerator {
itemPath = `{project-root}/${itemPath}`; itemPath = `{project-root}/${itemPath}`;
} }
return `--- const content = `# ${item.displayName || item.name}
description: '${description.replaceAll("'", "''")}'
---
# ${item.displayName || item.name}
LOAD and execute the ${type} at: ${itemPath} LOAD and execute the ${type} at: ${itemPath}
Follow all instructions in the ${type} file exactly as written. Follow all instructions in the ${type} file exactly as written.
`; `;
if (format === 'toml') {
// Escape any triple quotes in content
const escapedContent = content.replace(/"""/g, '\\"\\"\\"');
return `description = "${description}"
prompt = """
${escapedContent}
"""
`;
}
// Default YAML format
return `---
description: '${description.replaceAll("'", "''")}'
---
${content}`;
} }
/** /**
@@ -91,9 +107,10 @@ Follow all instructions in the ${type} file exactly as written.
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory * @param {string} bmadDir - BMAD installation directory
* @param {string} baseCommandsDir - Base commands directory for the IDE * @param {string} baseCommandsDir - Base commands directory for the IDE
* @param {string} [fileExtension='.md'] - File extension including dot (e.g., '.md', '.toml')
* @returns {Object} Generation results * @returns {Object} Generation results
*/ */
async generateColonTaskToolCommands(projectDir, bmadDir, baseCommandsDir) { async generateColonTaskToolCommands(projectDir, bmadDir, baseCommandsDir, fileExtension = '.md') {
const tasks = await this.loadTaskManifest(bmadDir); const tasks = await this.loadTaskManifest(bmadDir);
const tools = await this.loadToolManifest(bmadDir); const tools = await this.loadToolManifest(bmadDir);
@@ -101,16 +118,18 @@ Follow all instructions in the ${type} file exactly as written.
const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
// Determine format based on file extension
const format = fileExtension === '.toml' ? 'toml' : 'yaml';
let generatedCount = 0; let generatedCount = 0;
// DEBUG: Log parameters // DEBUG: Log parameters
console.log(`[DEBUG generateColonTaskToolCommands] baseCommandsDir: ${baseCommandsDir}`); console.log(`[DEBUG generateColonTaskToolCommands] baseCommandsDir: ${baseCommandsDir}, format=${format}`);
// Generate command files for tasks // Generate command files for tasks
for (const task of standaloneTasks) { for (const task of standaloneTasks) {
const commandContent = this.generateCommandContent(task, 'task'); const commandContent = this.generateCommandContent(task, 'task', format);
// Use underscore format: bmad_bmm_name.md // Use underscore format: bmad_bmm_name.<ext>
const flatName = toColonName(task.module, 'tasks', task.name); const flatName = toColonName(task.module, 'tasks', task.name, fileExtension);
const commandPath = path.join(baseCommandsDir, flatName); const commandPath = path.join(baseCommandsDir, flatName);
console.log(`[DEBUG generateColonTaskToolCommands] Writing task ${task.name} to: ${commandPath}`); console.log(`[DEBUG generateColonTaskToolCommands] Writing task ${task.name} to: ${commandPath}`);
await fs.ensureDir(path.dirname(commandPath)); await fs.ensureDir(path.dirname(commandPath));
@@ -120,9 +139,9 @@ Follow all instructions in the ${type} file exactly as written.
// Generate command files for tools // Generate command files for tools
for (const tool of standaloneTools) { for (const tool of standaloneTools) {
const commandContent = this.generateCommandContent(tool, 'tool'); const commandContent = this.generateCommandContent(tool, 'tool', format);
// Use underscore format: bmad_bmm_name.md // Use underscore format: bmad_bmm_name.<ext>
const flatName = toColonName(tool.module, 'tools', tool.name); const flatName = toColonName(tool.module, 'tools', tool.name, fileExtension);
const commandPath = path.join(baseCommandsDir, flatName); const commandPath = path.join(baseCommandsDir, flatName);
await fs.ensureDir(path.dirname(commandPath)); await fs.ensureDir(path.dirname(commandPath));
await fs.writeFile(commandPath, commandContent); await fs.writeFile(commandPath, commandContent);
@@ -137,15 +156,16 @@ Follow all instructions in the ${type} file exactly as written.
} }
/** /**
* Generate task and tool commands using underscore format (Windows-compatible) * Generate task and tool commands using dash format
* Creates flat files like: bmad_bmm_bmad-help.md * Creates flat files like: bmad-bmm-bmad-help.md
* *
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory * @param {string} bmadDir - BMAD installation directory
* @param {string} baseCommandsDir - Base commands directory for the IDE * @param {string} baseCommandsDir - Base commands directory for the IDE
* @param {string} [fileExtension='.md'] - File extension including dot (e.g., '.md', '.toml')
* @returns {Object} Generation results * @returns {Object} Generation results
*/ */
async generateDashTaskToolCommands(projectDir, bmadDir, baseCommandsDir) { async generateDashTaskToolCommands(projectDir, bmadDir, baseCommandsDir, fileExtension = '.md') {
const tasks = await this.loadTaskManifest(bmadDir); const tasks = await this.loadTaskManifest(bmadDir);
const tools = await this.loadToolManifest(bmadDir); const tools = await this.loadToolManifest(bmadDir);
@@ -153,13 +173,15 @@ Follow all instructions in the ${type} file exactly as written.
const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
// Determine format based on file extension
const format = fileExtension === '.toml' ? 'toml' : 'yaml';
let generatedCount = 0; let generatedCount = 0;
// Generate command files for tasks // Generate command files for tasks
for (const task of standaloneTasks) { for (const task of standaloneTasks) {
const commandContent = this.generateCommandContent(task, 'task'); const commandContent = this.generateCommandContent(task, 'task', format);
// Use underscore format: bmad_bmm_name.md (toDashPath aliases toColonPath) // Use dash format: bmad-bmm-task-name.<ext>
const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`); const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`, fileExtension);
const commandPath = path.join(baseCommandsDir, flatName); const commandPath = path.join(baseCommandsDir, flatName);
await fs.ensureDir(path.dirname(commandPath)); await fs.ensureDir(path.dirname(commandPath));
await fs.writeFile(commandPath, commandContent); await fs.writeFile(commandPath, commandContent);
@@ -168,9 +190,9 @@ Follow all instructions in the ${type} file exactly as written.
// Generate command files for tools // Generate command files for tools
for (const tool of standaloneTools) { for (const tool of standaloneTools) {
const commandContent = this.generateCommandContent(tool, 'tool'); const commandContent = this.generateCommandContent(tool, 'tool', format);
// Use underscore format: bmad_bmm_name.md (toDashPath aliases toColonPath) // Use dash format: bmad-bmm-tool-name.<ext>
const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`); const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`, fileExtension);
const commandPath = path.join(baseCommandsDir, flatName); const commandPath = path.join(baseCommandsDir, flatName);
await fs.ensureDir(path.dirname(commandPath)); await fs.ensureDir(path.dirname(commandPath));
await fs.writeFile(commandPath, commandContent); await fs.writeFile(commandPath, commandContent);

View File

@@ -39,6 +39,7 @@ const TemplateType = {
CLINE: 'cline', // No frontmatter, direct content CLINE: 'cline', // No frontmatter, direct content
WINDSURF: 'windsurf', // YAML with auto_execution_mode WINDSURF: 'windsurf', // YAML with auto_execution_mode
AUGMENT: 'augment', // YAML frontmatter AUGMENT: 'augment', // YAML frontmatter
GEMINI: 'gemini', // TOML frontmatter with description/prompt
}; };
/** /**
@@ -47,6 +48,7 @@ const TemplateType = {
* @property {string} targetDir - Full path to target directory * @property {string} targetDir - Full path to target directory
* @property {NamingStyle} namingStyle - How to name files * @property {NamingStyle} namingStyle - How to name files
* @property {TemplateType} templateType - What template format to use * @property {TemplateType} templateType - What template format to use
* @property {string} [fileExtension='.md'] - File extension including dot (e.g., '.md', '.toml')
* @property {boolean} includeNestedStructure - For NESTED style, create subdirectories * @property {boolean} includeNestedStructure - For NESTED style, create subdirectories
* @property {Function} [customTemplateFn] - Optional custom template function * @property {Function} [customTemplateFn] - Optional custom template function
*/ */
@@ -73,12 +75,13 @@ class UnifiedInstaller {
targetDir, targetDir,
namingStyle = NamingStyle.FLAT_COLON, namingStyle = NamingStyle.FLAT_COLON,
templateType = TemplateType.CLAUDE, templateType = TemplateType.CLAUDE,
fileExtension = '.md',
includeNestedStructure = false, includeNestedStructure = false,
customTemplateFn = null, customTemplateFn = null,
} = config; } = config;
// Clean up any existing BMAD files in target directory // Clean up any existing BMAD files in target directory
await this.cleanupBmadFiles(targetDir); await this.cleanupBmadFiles(targetDir, fileExtension);
// Ensure target directory exists // Ensure target directory exists
await fs.ensureDir(targetDir); await fs.ensureDir(targetDir);
@@ -95,7 +98,7 @@ class UnifiedInstaller {
// 1. Install Agents // 1. Install Agents
const agentGen = new AgentCommandGenerator(this.bmadFolderName); const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules); const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
counts.agents = await this.writeArtifacts(agentArtifacts, targetDir, namingStyle, templateType, customTemplateFn, 'agent'); counts.agents = await this.writeArtifacts(agentArtifacts, targetDir, namingStyle, templateType, fileExtension, customTemplateFn, 'agent');
// 2. Install Workflows (filter out README artifacts) // 2. Install Workflows (filter out README artifacts)
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
@@ -109,6 +112,7 @@ class UnifiedInstaller {
targetDir, targetDir,
namingStyle, namingStyle,
templateType, templateType,
fileExtension,
customTemplateFn, customTemplateFn,
'workflow', 'workflow',
); );
@@ -121,8 +125,8 @@ class UnifiedInstaller {
// TODO: Remove nested branch entirely after verification // TODO: Remove nested branch entirely after verification
const taskToolResult = const taskToolResult =
namingStyle === NamingStyle.FLAT_DASH namingStyle === NamingStyle.FLAT_DASH
? await ttGen.generateDashTaskToolCommands(projectDir, bmadDir, targetDir) ? await ttGen.generateDashTaskToolCommands(projectDir, bmadDir, targetDir, fileExtension)
: await ttGen.generateColonTaskToolCommands(projectDir, bmadDir, targetDir); : await ttGen.generateColonTaskToolCommands(projectDir, bmadDir, targetDir, fileExtension);
counts.tasks = taskToolResult.tasks || 0; counts.tasks = taskToolResult.tasks || 0;
counts.tools = taskToolResult.tools || 0; counts.tools = taskToolResult.tools || 0;
@@ -134,8 +138,10 @@ class UnifiedInstaller {
/** /**
* Clean up any existing BMAD files in target directory * Clean up any existing BMAD files in target directory
* @param {string} targetDir - Target directory to clean
* @param {string} [fileExtension='.md'] - File extension to match
*/ */
async cleanupBmadFiles(targetDir) { async cleanupBmadFiles(targetDir, fileExtension = '.md') {
if (!(await fs.pathExists(targetDir))) { if (!(await fs.pathExists(targetDir))) {
return; return;
} }
@@ -144,7 +150,8 @@ class UnifiedInstaller {
const entries = await fs.readdir(targetDir, { withFileTypes: true }); const entries = await fs.readdir(targetDir, { withFileTypes: true });
for (const entry of entries) { for (const entry of entries) {
if (entry.name.startsWith('bmad')) { // Only remove files with the matching extension
if (entry.name.startsWith('bmad') && entry.name.endsWith(fileExtension)) {
const entryPath = path.join(targetDir, entry.name); const entryPath = path.join(targetDir, entry.name);
await fs.remove(entryPath); await fs.remove(entryPath);
} }
@@ -153,9 +160,17 @@ class UnifiedInstaller {
/** /**
* Write artifacts with specified naming style and template * Write artifacts with specified naming style and template
* @param {Array} artifacts - Artifacts to write
* @param {string} targetDir - Target directory
* @param {NamingStyle} namingStyle - Naming style to use
* @param {TemplateType} templateType - Template type to use
* @param {string} fileExtension - File extension including dot
* @param {Function} customTemplateFn - Optional custom template function
* @param {string} artifactType - Type of artifact for logging
* @returns {Promise<number>} Number of artifacts written
*/ */
async writeArtifacts(artifacts, targetDir, namingStyle, templateType, customTemplateFn, artifactType) { async writeArtifacts(artifacts, targetDir, namingStyle, templateType, fileExtension, customTemplateFn, artifactType) {
console.log(`[DEBUG] writeArtifacts: artifactType=${artifactType}, count=${artifacts.length}, targetDir=${targetDir}`); console.log(`[DEBUG] writeArtifacts: artifactType=${artifactType}, count=${artifacts.length}, targetDir=${targetDir}, fileExtension=${fileExtension}`);
let written = 0; let written = 0;
for (const artifact of artifacts) { for (const artifact of artifacts) {
@@ -165,14 +180,14 @@ class UnifiedInstaller {
console.log(`[DEBUG] writeArtifacts processing: relativePath=${artifact.relativePath}, name=${artifact.name}`); console.log(`[DEBUG] writeArtifacts processing: relativePath=${artifact.relativePath}, name=${artifact.name}`);
if (namingStyle === NamingStyle.FLAT_COLON) { if (namingStyle === NamingStyle.FLAT_COLON) {
const flatName = toColonPath(artifact.relativePath); const flatName = toColonPath(artifact.relativePath, fileExtension);
targetPath = path.join(targetDir, flatName); targetPath = path.join(targetDir, flatName);
} else if (namingStyle === NamingStyle.FLAT_DASH) { } else if (namingStyle === NamingStyle.FLAT_DASH) {
const flatName = toDashPath(artifact.relativePath); const flatName = toDashPath(artifact.relativePath, fileExtension);
targetPath = path.join(targetDir, flatName); targetPath = path.join(targetDir, flatName);
} else { } else {
// Fallback: treat as flat even if NESTED specified // Fallback: treat as flat even if NESTED specified
const flatName = toColonPath(artifact.relativePath); const flatName = toColonPath(artifact.relativePath, fileExtension);
targetPath = path.join(targetDir, flatName); targetPath = path.join(targetDir, flatName);
} }
@@ -218,6 +233,11 @@ class UnifiedInstaller {
return this.addAugmentFrontmatter(artifact, content); return this.addAugmentFrontmatter(artifact, content);
} }
case TemplateType.GEMINI: {
// Add Gemini TOML frontmatter
return this.addGeminiFrontmatter(artifact, content);
}
default: { default: {
return content; return content;
} }
@@ -269,6 +289,31 @@ description: ${name}
return content; return content;
} }
/**
* Add Gemini TOML frontmatter
* Converts content to TOML format with description and prompt fields
*/
addGeminiFrontmatter(artifact, content) {
// Remove existing YAML frontmatter if present
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
const contentWithoutFrontmatter = content.replace(frontmatterRegex, '').trim();
// Extract description from artifact or content
let description = artifact.name || artifact.displayName || 'BMAD Command';
if (artifact.module) {
description = `BMAD ${artifact.module.toUpperCase()} ${artifact.type || 'Command'}: ${description}`;
}
// Escape any triple quotes in content
const escapedContent = contentWithoutFrontmatter.replace(/"""/g, '\\"\\"\\"');
return `description = "${description}"
prompt = """
${escapedContent}
"""
`;
}
/** /**
* Get tasks from manifest CSV * Get tasks from manifest CSV
*/ */