Files
BMAD-METHOD/tools/cli/installers/lib/ide/antigravity.js
Davor Racic 5cb5606ba3 fix(cli): replace inquirer with @clack/prompts for Windows compatibility (#1316)
* fix(cli): replace inquirer with @clack/prompts for Windows compatibility

- Add new prompts.js wrapper around @clack/prompts to fix Windows arrow
  key navigation issues (libuv #852)
- Fix validation logic in github-copilot.js that always returned true
- Add support for primitive choice values (string/number) in select/multiselect
- Add 'when' property support for conditional questions in prompt()
- Update all IDE installers to use new prompts module

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): address code review feedback for prompts migration

- Move @clack/prompts from devDependencies to dependencies (critical)
- Remove unused inquirer dependency
- Fix potential crash in multiselect when initialValues is undefined
- Add async validator detection with explicit error message
- Extract validateCustomContentPathSync method in ui.js
- Extract promptInstallLocation methods in claude-code.js and antigravity.js
- Fix moduleId -> missing.id in installer.js remove flow
- Update multiselect to support native clack API (options/initialValues)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: update comments to reference @clack/prompts instead of inquirer

- Update bmad-cli.js comment about CLI prompts
- Update config-collector.js JSDoc comments
- Rename inquirer variable to choiceUtils in ui.js
- Update JSDoc returns and calls documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): add spacing between prompts and installation progress

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): add multiselect usage hints for inexperienced users

Add inline navigation hints to all multiselect prompts showing
(↑/↓ navigate, SPACE select, ENTER confirm) to help users
unfamiliar with terminal multiselect controls.

Also restore detailed warning when no tools are selected,
explaining that SPACE must be pressed to select items.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(cli): restore IDE grouping using groupMultiselect

Replace flat multiselect with native @clack/prompts groupMultiselect
component to restore visual grouping of IDE/tool options:
- "Previously Configured" - pre-selected IDEs from existing install
- "Recommended Tools" - starred preferred options
- "Additional Tools" - other available options

This restores the grouped UX that was lost during the Inquirer.js
to @clack/prompts migration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 16:25:35 -06:00

487 lines
18 KiB
JavaScript

const path = require('node:path');
const fs = require('fs-extra');
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');
const prompts = require('../../../lib/prompts');
/**
* Google Antigravity IDE setup handler
*
* Uses .agent/workflows/ directory for slash commands
*/
class AntigravitySetup extends BaseIdeSetup {
constructor() {
super('antigravity', 'Google Antigravity', true);
this.configDir = '.agent';
this.workflowsDir = 'workflows';
}
/**
* Prompt for subagent installation location
* @returns {Promise<string>} Selected location ('project' or 'user')
*/
async _promptInstallLocation() {
return prompts.select({
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',
});
}
/**
* 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 yaml = require('yaml');
try {
// Load injection configuration
const configContent = await fs.readFile(injectionConfigPath, 'utf8');
const injectionConfig = yaml.parse(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') {
config.installLocation = await this._promptInstallLocation();
}
}
} 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 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 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 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') {
location = await this._promptInstallLocation();
}
}
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) {
// First ask if they want to install subagents
const install = await prompts.select({
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 prompts.multiselect({
message: `Select subagents to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
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 targetPath = path.join(projectDir, injection.file);
if (await this.exists(targetPath)) {
let content = await fs.readFile(targetPath, 'utf8');
const marker = `<!-- IDE-INJECT-POINT: ${injection.point} -->`;
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 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}`));
}
}
/**
* Install a custom agent launcher for Antigravity
* @param {string} projectDir - Project directory
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
* @param {string} agentPath - Path to compiled agent (relative to project root)
* @param {Object} metadata - Agent metadata
* @returns {Object} Installation result
*/
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
// Create .agent/workflows/bmad directory structure (same as regular agents)
const agentDir = path.join(projectDir, this.configDir);
const workflowsDir = path.join(agentDir, this.workflowsDir);
const bmadWorkflowsDir = path.join(workflowsDir, 'bmad');
await fs.ensureDir(bmadWorkflowsDir);
// Create custom agent launcher with same pattern as regular agents
const launcherContent = `name: '${agentName}'
description: '${agentName} agent'
usage: |
Custom BMAD agent: ${agentName}
Launch with: /${agentName}
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
<agent-activation CRITICAL="TRUE">
1. LOAD the FULL agent file from @${agentPath}
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
3. EXECUTE as ${agentName} with full persona adoption
</agent-activation>
---
⚠️ **IMPORTANT**: Run @${agentPath} to load the complete agent before using this launcher!`;
const fileName = `bmad-custom-agents-${agentName}.md`;
const launcherPath = path.join(bmadWorkflowsDir, fileName);
// Write the launcher file
await fs.writeFile(launcherPath, launcherContent, 'utf8');
return {
ide: 'antigravity',
path: path.relative(projectDir, launcherPath),
command: `/${agentName}`,
type: 'custom-agent-launcher',
};
}
}
module.exports = { AntigravitySetup };