diff --git a/tools/cli/installers/lib/ide/gemini.js b/tools/cli/installers/lib/ide/gemini.js index a1673573..c26bb05a 100644 --- a/tools/cli/installers/lib/ide/gemini.js +++ b/tools/cli/installers/lib/ide/gemini.js @@ -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>/); - 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 */ diff --git a/tools/cli/installers/lib/ide/shared/path-utils.js b/tools/cli/installers/lib/ide/shared/path-utils.js index dc564774..6280f04d 100644 --- a/tools/cli/installers/lib/ide/shared/path-utils.js +++ b/tools/cli/installers/lib/ide/shared/path-utils.js @@ -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, diff --git a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js index 8483308b..5e35fdaf 100644 --- a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js @@ -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. + 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. + 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. + 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. + 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); diff --git a/tools/cli/installers/lib/ide/shared/unified-installer.js b/tools/cli/installers/lib/ide/shared/unified-installer.js index 98097320..75633460 100644 --- a/tools/cli/installers/lib/ide/shared/unified-installer.js +++ b/tools/cli/installers/lib/ide/shared/unified-installer.js @@ -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 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 */