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 { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
/**
* Gemini CLI setup handler
@@ -15,8 +14,6 @@ class GeminiSetup extends BaseIdeSetup {
super('gemini', 'Gemini CLI', false);
this.configDir = '.gemini';
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);
// Clean up any existing BMAD files before reinstalling
await this.cleanup(projectDir);
// Use UnifiedInstaller for agents and workflows
const installer = new UnifiedInstaller(this.bmadFolderName);
// Generate agent launchers
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
const config = {
targetDir: commandsDir,
namingStyle: NamingStyle.FLAT_DASH,
templateType: TemplateType.GEMINI,
fileExtension: '.toml',
};
// Get tasks and workflows (ALL workflows now generate commands)
const tasks = await this.getTasks(bmadDir);
const counts = await installer.install(projectDir, bmadDir, config, options.selectedModules || []);
// Get ALL workflows using the new workflow command generator
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
// 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}`));
}
}
// Generate activation names for display
const agentActivation = `/bmad_agents_{agent-name}`;
const workflowActivation = `/bmad_workflows_{workflow-name}`;
const taskActivation = `/bmad_tasks_{task-name}`;
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents configured`));
console.log(chalk.dim(` - ${taskCount} tasks configured`));
console.log(chalk.dim(` - ${workflowCount} workflows configured`));
console.log(chalk.dim(` - ${counts.agents} agents configured`));
console.log(chalk.dim(` - ${counts.workflows} 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(` - Agent activation: /bmad_agents_{agent-name}`));
console.log(chalk.dim(` - Task activation: /bmad_tasks_{task-name}`));
console.log(chalk.dim(` - Workflow activation: /bmad_workflows_{workflow-name}`));
console.log(chalk.dim(` - Agent activation: ${agentActivation}`));
console.log(chalk.dim(` - Workflow activation: ${workflowActivation}`));
console.log(chalk.dim(` - Task activation: ${taskActivation}`));
return {
success: true,
agents: agentCount,
tasks: taskCount,
workflows: workflowCount,
...counts,
};
}
/**
* 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
*/

View File

@@ -5,6 +5,9 @@
* - 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
const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools'];
const AGENT_SEGMENT = 'agents';
@@ -18,15 +21,16 @@ const AGENT_SEGMENT = 'agents';
* @param {string} module - Module name (e.g., 'bmm', 'core')
* @param {string} type - Artifact type ('agents', 'workflows', 'tasks', 'tools')
* @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'
*/
function toUnderscoreName(module, type, name) {
function toUnderscoreName(module, type, name, fileExtension = DEFAULT_FILE_EXTENSION) {
const isAgent = type === AGENT_SEGMENT;
// For core module, skip the module prefix: use 'bmad_name.md' instead of 'bmad_core_name.md'
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)
*
* @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'
*/
function toUnderscorePath(relativePath) {
const withoutExt = relativePath.replace('.md', '');
function toUnderscorePath(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];
@@ -47,7 +55,7 @@ function toUnderscorePath(relativePath) {
const name = parts.slice(2).join('_');
// 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'
*
* @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'
*/
function customAgentUnderscoreName(agentName) {
return `bmad_custom_${agentName}.md`;
function customAgentUnderscoreName(agentName, fileExtension = DEFAULT_FILE_EXTENSION) {
return `bmad_custom_${agentName}${fileExtension}`;
}
/**
@@ -134,9 +143,9 @@ function parseUnderscoreName(filename) {
}
// Backward compatibility aliases (deprecated)
// Note: These now use toDashPath and customAgentDashName which convert underscores to dashes
const toColonName = toUnderscoreName;
const toColonPath = toUnderscorePath;
const toDashPath = toUnderscorePath;
const toDashName = toUnderscoreName;
const customAgentColonName = customAgentUnderscoreName;
const customAgentDashName = customAgentUnderscoreName;
const isColonFormat = isUnderscoreFormat;
@@ -144,7 +153,46 @@ const isDashFormat = isUnderscoreFormat;
const parseColonName = 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 = {
DEFAULT_FILE_EXTENSION,
toUnderscoreName,
toUnderscorePath,
customAgentUnderscoreName,
@@ -153,6 +201,7 @@ module.exports = {
// Backward compatibility aliases
toColonName,
toColonPath,
toDashName,
toDashPath,
customAgentColonName,
customAgentDashName,

View File

@@ -16,8 +16,11 @@ class TaskToolCommandGenerator {
/**
* 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}`;
// Convert path to use {project-root} placeholder
@@ -26,16 +29,29 @@ class TaskToolCommandGenerator {
itemPath = `{project-root}/${itemPath}`;
}
return `---
description: '${description.replaceAll("'", "''")}'
---
# ${item.displayName || item.name}
const content = `# ${item.displayName || item.name}
LOAD and execute the ${type} at: ${itemPath}
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} bmadDir - BMAD installation directory
* @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
*/
async generateColonTaskToolCommands(projectDir, bmadDir, baseCommandsDir) {
async generateColonTaskToolCommands(projectDir, bmadDir, baseCommandsDir, fileExtension = '.md') {
const tasks = await this.loadTaskManifest(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 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;
// DEBUG: Log parameters
console.log(`[DEBUG generateColonTaskToolCommands] baseCommandsDir: ${baseCommandsDir}`);
console.log(`[DEBUG generateColonTaskToolCommands] baseCommandsDir: ${baseCommandsDir}, format=${format}`);
// Generate command files for tasks
for (const task of standaloneTasks) {
const commandContent = this.generateCommandContent(task, 'task');
// Use underscore format: bmad_bmm_name.md
const flatName = toColonName(task.module, 'tasks', task.name);
const commandContent = this.generateCommandContent(task, 'task', format);
// Use underscore format: bmad_bmm_name.<ext>
const flatName = toColonName(task.module, 'tasks', task.name, fileExtension);
const commandPath = path.join(baseCommandsDir, flatName);
console.log(`[DEBUG generateColonTaskToolCommands] Writing task ${task.name} to: ${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
for (const tool of standaloneTools) {
const commandContent = this.generateCommandContent(tool, 'tool');
// Use underscore format: bmad_bmm_name.md
const flatName = toColonName(tool.module, 'tools', tool.name);
const commandContent = this.generateCommandContent(tool, 'tool', format);
// Use underscore format: bmad_bmm_name.<ext>
const flatName = toColonName(tool.module, 'tools', tool.name, fileExtension);
const commandPath = path.join(baseCommandsDir, flatName);
await fs.ensureDir(path.dirname(commandPath));
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)
* Creates flat files like: bmad_bmm_bmad-help.md
* Generate task and tool commands using dash format
* Creates flat files like: bmad-bmm-bmad-help.md
*
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @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
*/
async generateDashTaskToolCommands(projectDir, bmadDir, baseCommandsDir) {
async generateDashTaskToolCommands(projectDir, bmadDir, baseCommandsDir, fileExtension = '.md') {
const tasks = await this.loadTaskManifest(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 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;
// Generate command files for tasks
for (const task of standaloneTasks) {
const commandContent = this.generateCommandContent(task, 'task');
// Use underscore format: bmad_bmm_name.md (toDashPath aliases toColonPath)
const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`);
const commandContent = this.generateCommandContent(task, 'task', format);
// Use dash format: bmad-bmm-task-name.<ext>
const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`, fileExtension);
const commandPath = path.join(baseCommandsDir, flatName);
await fs.ensureDir(path.dirname(commandPath));
await fs.writeFile(commandPath, commandContent);
@@ -168,9 +190,9 @@ Follow all instructions in the ${type} file exactly as written.
// Generate command files for tools
for (const tool of standaloneTools) {
const commandContent = this.generateCommandContent(tool, 'tool');
// Use underscore format: bmad_bmm_name.md (toDashPath aliases toColonPath)
const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`);
const commandContent = this.generateCommandContent(tool, 'tool', format);
// Use dash format: bmad-bmm-tool-name.<ext>
const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`, fileExtension);
const commandPath = path.join(baseCommandsDir, flatName);
await fs.ensureDir(path.dirname(commandPath));
await fs.writeFile(commandPath, commandContent);

View File

@@ -39,6 +39,7 @@ const TemplateType = {
CLINE: 'cline', // No frontmatter, direct content
WINDSURF: 'windsurf', // YAML with auto_execution_mode
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 {NamingStyle} namingStyle - How to name files
* @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 {Function} [customTemplateFn] - Optional custom template function
*/
@@ -73,12 +75,13 @@ class UnifiedInstaller {
targetDir,
namingStyle = NamingStyle.FLAT_COLON,
templateType = TemplateType.CLAUDE,
fileExtension = '.md',
includeNestedStructure = false,
customTemplateFn = null,
} = config;
// Clean up any existing BMAD files in target directory
await this.cleanupBmadFiles(targetDir);
await this.cleanupBmadFiles(targetDir, fileExtension);
// Ensure target directory exists
await fs.ensureDir(targetDir);
@@ -95,7 +98,7 @@ class UnifiedInstaller {
// 1. Install Agents
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
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)
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
@@ -109,6 +112,7 @@ class UnifiedInstaller {
targetDir,
namingStyle,
templateType,
fileExtension,
customTemplateFn,
'workflow',
);
@@ -121,8 +125,8 @@ class UnifiedInstaller {
// TODO: Remove nested branch entirely after verification
const taskToolResult =
namingStyle === NamingStyle.FLAT_DASH
? await ttGen.generateDashTaskToolCommands(projectDir, bmadDir, targetDir)
: await ttGen.generateColonTaskToolCommands(projectDir, bmadDir, targetDir);
? await ttGen.generateDashTaskToolCommands(projectDir, bmadDir, targetDir, fileExtension)
: await ttGen.generateColonTaskToolCommands(projectDir, bmadDir, targetDir, fileExtension);
counts.tasks = taskToolResult.tasks || 0;
counts.tools = taskToolResult.tools || 0;
@@ -134,8 +138,10 @@ class UnifiedInstaller {
/**
* 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))) {
return;
}
@@ -144,7 +150,8 @@ class UnifiedInstaller {
const entries = await fs.readdir(targetDir, { withFileTypes: true });
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);
await fs.remove(entryPath);
}
@@ -153,9 +160,17 @@ class UnifiedInstaller {
/**
* 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) {
console.log(`[DEBUG] writeArtifacts: artifactType=${artifactType}, count=${artifacts.length}, targetDir=${targetDir}`);
async writeArtifacts(artifacts, targetDir, namingStyle, templateType, fileExtension, customTemplateFn, artifactType) {
console.log(`[DEBUG] writeArtifacts: artifactType=${artifactType}, count=${artifacts.length}, targetDir=${targetDir}, fileExtension=${fileExtension}`);
let written = 0;
for (const artifact of artifacts) {
@@ -165,14 +180,14 @@ class UnifiedInstaller {
console.log(`[DEBUG] writeArtifacts processing: relativePath=${artifact.relativePath}, name=${artifact.name}`);
if (namingStyle === NamingStyle.FLAT_COLON) {
const flatName = toColonPath(artifact.relativePath);
const flatName = toColonPath(artifact.relativePath, fileExtension);
targetPath = path.join(targetDir, flatName);
} else if (namingStyle === NamingStyle.FLAT_DASH) {
const flatName = toDashPath(artifact.relativePath);
const flatName = toDashPath(artifact.relativePath, fileExtension);
targetPath = path.join(targetDir, flatName);
} else {
// Fallback: treat as flat even if NESTED specified
const flatName = toColonPath(artifact.relativePath);
const flatName = toColonPath(artifact.relativePath, fileExtension);
targetPath = path.join(targetDir, flatName);
}
@@ -218,6 +233,11 @@ class UnifiedInstaller {
return this.addAugmentFrontmatter(artifact, content);
}
case TemplateType.GEMINI: {
// Add Gemini TOML frontmatter
return this.addGeminiFrontmatter(artifact, content);
}
default: {
return content;
}
@@ -269,6 +289,31 @@ description: ${name}
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
*/