diff --git a/tools/cli/installers/lib/ide/_base-ide.js b/tools/cli/installers/lib/ide/_base-ide.js index 25122ade..5ee39cb6 100644 --- a/tools/cli/installers/lib/ide/_base-ide.js +++ b/tools/cli/installers/lib/ide/_base-ide.js @@ -601,6 +601,18 @@ class BaseIdeSetup { .join(' '); } + /** + * Flatten a relative path to a single filename for flat slash command naming + * Example: 'module/agents/name.md' -> 'bmad-module-agents-name.md' + * Used by IDEs that ignore directory structure for slash commands (e.g., Antigravity, Codex) + * @param {string} relativePath - Relative path to flatten + * @returns {string} Flattened filename with 'bmad-' prefix + */ + flattenFilename(relativePath) { + const sanitized = relativePath.replaceAll(/[/\\]/g, '-'); + return `bmad-${sanitized}`; + } + /** * Create agent configuration file * @param {string} bmadDir - BMAD installation directory diff --git a/tools/cli/installers/lib/ide/antigravity.js b/tools/cli/installers/lib/ide/antigravity.js new file mode 100644 index 00000000..3bccd911 --- /dev/null +++ b/tools/cli/installers/lib/ide/antigravity.js @@ -0,0 +1,463 @@ +const path = require('node:path'); +const { BaseIdeSetup } = require('./_base-ide'); +const chalk = require('chalk'); +const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); +const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); +const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); +const { AgentCommandGenerator } = require('./shared/agent-command-generator'); +const { + loadModuleInjectionConfig, + shouldApplyInjection, + filterAgentInstructions, + resolveSubagentFiles, +} = require('./shared/module-injections'); +const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts'); + +/** + * Google Antigravity IDE setup handler + * + * Uses .agent/workflows/ directory for slash commands + */ +class AntigravitySetup extends BaseIdeSetup { + constructor() { + super('antigravity', 'Google Antigravity', false); + this.configDir = '.agent'; + this.workflowsDir = 'workflows'; + } + + /** + * Collect configuration choices before installation + * @param {Object} options - Configuration options + * @returns {Object} Collected configuration + */ + async collectConfiguration(options = {}) { + const config = { + subagentChoices: null, + installLocation: null, + }; + + const sourceModulesPath = getSourcePath('modules'); + const modules = options.selectedModules || []; + + for (const moduleName of modules) { + // Check for Antigravity sub-module injection config in SOURCE directory + const injectionConfigPath = path.join(sourceModulesPath, moduleName, 'sub-modules', 'antigravity', 'injections.yaml'); + + if (await this.exists(injectionConfigPath)) { + const fs = require('fs-extra'); + const yaml = require('js-yaml'); + + try { + // Load injection configuration + const configContent = await fs.readFile(injectionConfigPath, 'utf8'); + const injectionConfig = yaml.load(configContent); + + // Ask about subagents if they exist and we haven't asked yet + if (injectionConfig.subagents && !config.subagentChoices) { + config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents); + + if (config.subagentChoices.install !== 'none') { + // Ask for installation location + const inquirer = require('inquirer'); + const locationAnswer = await inquirer.prompt([ + { + type: 'list', + name: 'location', + message: 'Where would you like to install Antigravity subagents?', + choices: [ + { name: 'Project level (.agent/agents/)', value: 'project' }, + { name: 'User level (~/.agent/agents/)', value: 'user' }, + ], + default: 'project', + }, + ]); + config.installLocation = locationAnswer.location; + } + } + } catch (error) { + console.log(chalk.yellow(` Warning: Failed to process ${moduleName} features: ${error.message}`)); + } + } + } + + return config; + } + + /** + * Cleanup old BMAD installation before reinstalling + * @param {string} projectDir - Project directory + */ + async cleanup(projectDir) { + const fs = require('fs-extra'); + const bmadWorkflowsDir = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad'); + + if (await fs.pathExists(bmadWorkflowsDir)) { + await fs.remove(bmadWorkflowsDir); + console.log(chalk.dim(` Removed old BMAD workflows from ${this.name}`)); + } + } + + /** + * Setup Antigravity IDE configuration + * @param {string} projectDir - Project directory + * @param {string} bmadDir - BMAD installation directory + * @param {Object} options - Setup options + */ + async setup(projectDir, bmadDir, options = {}) { + // Store project directory for use in processContent + this.projectDir = projectDir; + + console.log(chalk.cyan(`Setting up ${this.name}...`)); + + // Clean up old BMAD installation first + await this.cleanup(projectDir); + + // Create .agent/workflows directory structure + const agentDir = path.join(projectDir, this.configDir); + const workflowsDir = path.join(agentDir, this.workflowsDir); + const bmadWorkflowsDir = path.join(workflowsDir, 'bmad'); + + await this.ensureDir(bmadWorkflowsDir); + + // Generate agent launchers using AgentCommandGenerator + // This creates small launcher files that reference the actual agents in .bmad/ + const agentGen = new AgentCommandGenerator(this.bmadFolderName); + const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); + + // Write agent launcher files with FLATTENED naming + // Antigravity ignores directory structure, so we flatten to: bmad-module-agents-name.md + // This creates slash commands like /bmad-bmm-agents-dev instead of /dev + let agentCount = 0; + for (const artifact of agentArtifacts) { + const flattenedName = this.flattenFilename(artifact.relativePath); + const targetPath = path.join(bmadWorkflowsDir, flattenedName); + await this.writeFile(targetPath, artifact.content); + agentCount++; + } + + // Process Antigravity specific injections for installed modules + // Use pre-collected configuration if available, or skip if already configured + if (options.preCollectedConfig && options.preCollectedConfig._alreadyConfigured) { + // IDE is already configured from previous installation, skip prompting + // Just process with default/existing configuration + await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, {}); + } else if (options.preCollectedConfig) { + await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, options.preCollectedConfig); + } else { + await this.processModuleInjections(projectDir, bmadDir, options); + } + + // Generate workflow commands from manifest (if it exists) + const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); + const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir); + + // Write workflow-command artifacts with FLATTENED naming + let workflowCommandCount = 0; + for (const artifact of workflowArtifacts) { + if (artifact.type === 'workflow-command') { + const flattenedName = this.flattenFilename(artifact.relativePath); + const targetPath = path.join(bmadWorkflowsDir, flattenedName); + await this.writeFile(targetPath, artifact.content); + workflowCommandCount++; + } + } + + // Generate task and tool commands from manifests (if they exist) + const taskToolGen = new TaskToolCommandGenerator(); + const taskToolResult = await taskToolGen.generateTaskToolCommands(projectDir, bmadDir); + + console.log(chalk.green(`✓ ${this.name} configured:`)); + console.log(chalk.dim(` - ${agentCount} agents installed`)); + if (workflowCommandCount > 0) { + console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated`)); + } + if (taskToolResult.generated > 0) { + console.log( + chalk.dim( + ` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`, + ), + ); + } + console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, bmadWorkflowsDir)}`)); + console.log(chalk.yellow(`\n Note: Antigravity uses flattened slash commands (e.g., /bmad-module-agents-name)`)); + + return { + success: true, + agents: agentCount, + }; + } + + /** + * Read and process file content + */ + async readAndProcess(filePath, metadata) { + const fs = require('fs-extra'); + const content = await fs.readFile(filePath, 'utf8'); + return this.processContent(content, metadata); + } + + /** + * Override processContent to keep {project-root} placeholder + */ + processContent(content, metadata = {}) { + // Use the base class method WITHOUT projectDir to preserve {project-root} placeholder + return super.processContent(content, metadata); + } + + /** + * Get agents from source modules (not installed location) + */ + async getAgentsFromSource(sourceDir, selectedModules) { + const fs = require('fs-extra'); + const agents = []; + + // Add core agents + const corePath = getModulePath('core'); + if (await fs.pathExists(path.join(corePath, 'agents'))) { + const coreAgents = await getAgentsFromDir(path.join(corePath, 'agents'), 'core'); + agents.push(...coreAgents); + } + + // Add module agents + for (const moduleName of selectedModules) { + const modulePath = path.join(sourceDir, moduleName); + const agentsPath = path.join(modulePath, 'agents'); + + if (await fs.pathExists(agentsPath)) { + const moduleAgents = await getAgentsFromDir(agentsPath, moduleName); + agents.push(...moduleAgents); + } + } + + return agents; + } + + /** + * Process module injections with pre-collected configuration + */ + async processModuleInjectionsWithConfig(projectDir, bmadDir, options, preCollectedConfig) { + // Get list of installed modules + const modules = options.selectedModules || []; + const { subagentChoices, installLocation } = preCollectedConfig; + + // Get the actual source directory (not the installation directory) + await this.processModuleInjectionsInternal({ + projectDir, + modules, + handler: 'antigravity', + subagentChoices, + installLocation, + interactive: false, + }); + } + + /** + * Process Antigravity specific injections for installed modules + * Looks for injections.yaml in each module's antigravity sub-module + */ + async processModuleInjections(projectDir, bmadDir, options) { + // Get list of installed modules + const modules = options.selectedModules || []; + let subagentChoices = null; + let installLocation = null; + + // Get the actual source directory (not the installation directory) + const { subagentChoices: updatedChoices, installLocation: updatedLocation } = await this.processModuleInjectionsInternal({ + projectDir, + modules, + handler: 'antigravity', + subagentChoices, + installLocation, + interactive: true, + }); + + if (updatedChoices) { + subagentChoices = updatedChoices; + } + if (updatedLocation) { + installLocation = updatedLocation; + } + } + + async processModuleInjectionsInternal({ projectDir, modules, handler, subagentChoices, installLocation, interactive = false }) { + let choices = subagentChoices; + let location = installLocation; + + for (const moduleName of modules) { + const configData = await loadModuleInjectionConfig(handler, moduleName); + + if (!configData) { + continue; + } + + const { config, handlerBaseDir } = configData; + + if (interactive) { + console.log(chalk.cyan(`\nConfiguring ${moduleName} ${handler} features...`)); + } + + if (interactive && config.subagents && !choices) { + choices = await this.promptSubagentInstallation(config.subagents); + + if (choices.install !== 'none') { + const inquirer = require('inquirer'); + const locationAnswer = await inquirer.prompt([ + { + type: 'list', + name: 'location', + message: 'Where would you like to install Antigravity subagents?', + choices: [ + { name: 'Project level (.agent/agents/)', value: 'project' }, + { name: 'User level (~/.agent/agents/)', value: 'user' }, + ], + default: 'project', + }, + ]); + location = locationAnswer.location; + } + } + + if (config.injections && choices && choices.install !== 'none') { + for (const injection of config.injections) { + if (shouldApplyInjection(injection, choices)) { + await this.injectContent(projectDir, injection, choices); + } + } + } + + if (config.subagents && choices && choices.install !== 'none') { + await this.copySelectedSubagents(projectDir, handlerBaseDir, config.subagents, choices, location || 'project'); + } + } + + return { subagentChoices: choices, installLocation: location }; + } + + /** + * Prompt user for subagent installation preferences + */ + async promptSubagentInstallation(subagentConfig) { + const inquirer = require('inquirer'); + + // First ask if they want to install subagents + const { install } = await inquirer.prompt([ + { + type: 'list', + name: 'install', + message: 'Would you like to install Antigravity subagents for enhanced functionality?', + choices: [ + { name: 'Yes, install all subagents', value: 'all' }, + { name: 'Yes, let me choose specific subagents', value: 'selective' }, + { name: 'No, skip subagent installation', value: 'none' }, + ], + default: 'all', + }, + ]); + + if (install === 'selective') { + // Show list of available subagents with descriptions + const subagentInfo = { + 'market-researcher.md': 'Market research and competitive analysis', + 'requirements-analyst.md': 'Requirements extraction and validation', + 'technical-evaluator.md': 'Technology stack evaluation', + 'epic-optimizer.md': 'Epic and story breakdown optimization', + 'document-reviewer.md': 'Document quality review', + }; + + const { selected } = await inquirer.prompt([ + { + type: 'checkbox', + name: 'selected', + message: 'Select subagents to install:', + choices: subagentConfig.files.map((file) => ({ + name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`, + value: file, + checked: true, + })), + }, + ]); + + return { install: 'selective', selected }; + } + + return { install }; + } + + /** + * Inject content at specified point in file + */ + async injectContent(projectDir, injection, subagentChoices = null) { + const fs = require('fs-extra'); + const targetPath = path.join(projectDir, injection.file); + + if (await this.exists(targetPath)) { + let content = await fs.readFile(targetPath, 'utf8'); + const marker = ``; + + if (content.includes(marker)) { + let injectionContent = injection.content; + + // Filter content if selective subagents chosen + if (subagentChoices && subagentChoices.install === 'selective' && injection.point === 'pm-agent-instructions') { + injectionContent = filterAgentInstructions(injection.content, subagentChoices.selected); + } + + content = content.replace(marker, injectionContent); + await fs.writeFile(targetPath, content); + console.log(chalk.dim(` Injected: ${injection.point} → ${injection.file}`)); + } + } + } + + /** + * Copy selected subagents to appropriate Antigravity agents directory + */ + async copySelectedSubagents(projectDir, handlerBaseDir, subagentConfig, choices, location) { + const fs = require('fs-extra'); + const os = require('node:os'); + + // Determine target directory based on user choice + let targetDir; + if (location === 'user') { + targetDir = path.join(os.homedir(), '.agent', 'agents'); + console.log(chalk.dim(` Installing subagents globally to: ~/.agent/agents/`)); + } else { + targetDir = path.join(projectDir, '.agent', 'agents'); + console.log(chalk.dim(` Installing subagents to project: .agent/agents/`)); + } + + // Ensure target directory exists + await this.ensureDir(targetDir); + + const resolvedFiles = await resolveSubagentFiles(handlerBaseDir, subagentConfig, choices); + + let copiedCount = 0; + for (const resolved of resolvedFiles) { + try { + const sourcePath = resolved.absolutePath; + + const subFolder = path.dirname(resolved.relativePath); + let targetPath; + if (subFolder && subFolder !== '.') { + const targetSubDir = path.join(targetDir, subFolder); + await this.ensureDir(targetSubDir); + targetPath = path.join(targetSubDir, path.basename(resolved.file)); + } else { + targetPath = path.join(targetDir, path.basename(resolved.file)); + } + + await fs.copyFile(sourcePath, targetPath); + console.log(chalk.green(` ✓ Installed: ${subFolder === '.' ? '' : `${subFolder}/`}${path.basename(resolved.file, '.md')}`)); + copiedCount++; + } catch (error) { + console.log(chalk.yellow(` ⚠ Error copying ${resolved.file}: ${error.message}`)); + } + } + + if (copiedCount > 0) { + console.log(chalk.dim(` Total subagents installed: ${copiedCount}`)); + } + } +} + +module.exports = { AntigravitySetup }; diff --git a/tools/cli/installers/lib/ide/codex.js b/tools/cli/installers/lib/ide/codex.js index 66a00327..480d805c 100644 --- a/tools/cli/installers/lib/ide/codex.js +++ b/tools/cli/installers/lib/ide/codex.js @@ -212,11 +212,6 @@ class CodexSetup extends BaseIdeSetup { return path.join(os.homedir(), '.codex', 'prompts'); } - flattenFilename(relativePath) { - const sanitized = relativePath.replaceAll(/[\\/]/g, '-'); - return `bmad-${sanitized}`; - } - async flattenAndWriteArtifacts(artifacts, destDir) { let written = 0;