mirror of
https://github.com/bmad-code-org/BMAD-METHOD.git
synced 2026-01-30 04:32:02 +00:00
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:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user