refactor: extract shared flat-file install methods to base class

This commit is contained in:
Alex Verkhovsky 2025-12-26 02:07:54 -08:00
parent c15ad174ed
commit bdacdad3a8
3 changed files with 73 additions and 70 deletions

View File

@ -629,6 +629,58 @@ class BaseIdeSetup {
return `bmad-${sanitized}`; return `bmad-${sanitized}`;
} }
/**
* Clear old BMAD files from a directory (prefix-based cleanup)
* Removes files and directories starting with 'bmad-' or named 'bmad'
* Used by IDEs with flat slash command structure (Antigravity, Codex)
* @param {string} dir - Directory to clean
* @returns {number} Number of items removed
*/
async clearBmadPrefixedFiles(dir) {
if (!(await fs.pathExists(dir))) {
return 0;
}
const entries = await fs.readdir(dir);
let removedCount = 0;
for (const entry of entries) {
if (!entry.startsWith('bmad-') && entry !== 'bmad') {
continue;
}
const entryPath = path.join(dir, entry);
const stat = await fs.stat(entryPath);
if (stat.isFile() || stat.isDirectory()) {
await fs.remove(entryPath);
removedCount++;
}
}
return removedCount;
}
/**
* Write artifacts with flattened naming to a directory
* Used by IDEs with flat slash command structure (Antigravity, Codex)
* @param {Array} artifacts - Array of artifact objects with relativePath and content
* @param {string} destDir - Destination directory
* @returns {number} Number of files written
*/
async writeFlattenedArtifacts(artifacts, destDir) {
await this.ensureDir(destDir);
let written = 0;
for (const artifact of artifacts) {
const flattenedName = this.flattenFilename(artifact.relativePath);
const targetPath = path.join(destDir, flattenedName);
await this.writeFile(targetPath, artifact.content);
written++;
}
return written;
}
/** /**
* Create agent configuration file * Create agent configuration file
* @param {string} bmadDir - BMAD installation directory * @param {string} bmadDir - BMAD installation directory

View File

@ -85,14 +85,15 @@ class AntigravitySetup extends BaseIdeSetup {
/** /**
* Cleanup old BMAD installation before reinstalling * Cleanup old BMAD installation before reinstalling
* Removes files and directories starting with 'bmad-' from workflows dir
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory
*/ */
async cleanup(projectDir) { async cleanup(projectDir) {
const bmadWorkflowsDir = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad'); const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir);
const removedCount = await this.clearBmadPrefixedFiles(workflowsDir);
if (await fs.pathExists(bmadWorkflowsDir)) { if (removedCount > 0) {
await fs.remove(bmadWorkflowsDir); console.log(chalk.dim(` Removed ${removedCount} old BMAD items from ${this.name}`));
console.log(chalk.dim(` Removed old BMAD workflows from ${this.name}`));
} }
} }
@ -114,25 +115,16 @@ class AntigravitySetup extends BaseIdeSetup {
// Create .agent/workflows directory structure // Create .agent/workflows directory structure
const agentDir = path.join(projectDir, this.configDir); const agentDir = path.join(projectDir, this.configDir);
const workflowsDir = path.join(agentDir, this.workflowsDir); const workflowsDir = path.join(agentDir, this.workflowsDir);
const bmadWorkflowsDir = path.join(workflowsDir, 'bmad');
await this.ensureDir(bmadWorkflowsDir); await this.ensureDir(workflowsDir);
// Generate agent launchers using AgentCommandGenerator // Generate agent launchers using AgentCommandGenerator
// This creates small launcher files that reference the actual agents in _bmad/ // This creates small launcher files that reference the actual agents in _bmad/
const agentGen = new AgentCommandGenerator(this.bmadFolderName); const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
// Write agent launcher files with FLATTENED naming // Write agent launcher files with flattened naming directly to workflows dir
// Antigravity ignores directory structure, so we flatten to: bmad-module-agents-name.md const agentCount = await this.writeFlattenedArtifacts(agentArtifacts, workflowsDir);
// 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 // Process Antigravity specific injections for installed modules
// Use pre-collected configuration if available, or skip if already configured // Use pre-collected configuration if available, or skip if already configured
@ -150,16 +142,9 @@ class AntigravitySetup extends BaseIdeSetup {
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir); const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
// Write workflow-command artifacts with FLATTENED naming // Filter to workflow-command type and write with flattened naming
let workflowCommandCount = 0; const workflowCommands = workflowArtifacts.filter((a) => a.type === 'workflow-command');
for (const artifact of workflowArtifacts) { const workflowCommandCount = await this.writeFlattenedArtifacts(workflowCommands, workflowsDir);
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) // Generate task and tool commands from manifests (if they exist)
const taskToolGen = new TaskToolCommandGenerator(); const taskToolGen = new TaskToolCommandGenerator();
@ -177,7 +162,7 @@ class AntigravitySetup extends BaseIdeSetup {
), ),
); );
} }
console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, bmadWorkflowsDir)}`)); console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, workflowsDir)}`));
console.log(chalk.yellow(`\n Note: Antigravity uses flattened slash commands (e.g., /bmad-module-agents-name)`)); console.log(chalk.yellow(`\n Note: Antigravity uses flattened slash commands (e.g., /bmad-module-agents-name)`));
return { return {
@ -463,12 +448,11 @@ class AntigravitySetup extends BaseIdeSetup {
* @returns {Object} Installation result * @returns {Object} Installation result
*/ */
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
// Create .agent/workflows/bmad directory structure (same as regular agents) // Create .agent/workflows directory structure (flat, no bmad/ subdirectory)
const agentDir = path.join(projectDir, this.configDir); const agentDir = path.join(projectDir, this.configDir);
const workflowsDir = path.join(agentDir, this.workflowsDir); const workflowsDir = path.join(agentDir, this.workflowsDir);
const bmadWorkflowsDir = path.join(workflowsDir, 'bmad');
await fs.ensureDir(bmadWorkflowsDir); await fs.ensureDir(workflowsDir);
// Create custom agent launcher with same pattern as regular agents // Create custom agent launcher with same pattern as regular agents
const launcherContent = `name: '${agentName}' const launcherContent = `name: '${agentName}'
@ -490,7 +474,7 @@ usage: |
**IMPORTANT**: Run @${agentPath} to load the complete agent before using this launcher!`; **IMPORTANT**: Run @${agentPath} to load the complete agent before using this launcher!`;
const fileName = `bmad-custom-agents-${agentName}.md`; const fileName = `bmad-custom-agents-${agentName}.md`;
const launcherPath = path.join(bmadWorkflowsDir, fileName); const launcherPath = path.join(workflowsDir, fileName);
// Write the launcher file // Write the launcher file
await fs.writeFile(launcherPath, launcherContent, 'utf8'); await fs.writeFile(launcherPath, launcherContent, 'utf8');

View File

@ -95,8 +95,8 @@ class CodexSetup extends BaseIdeSetup {
const destDir = this.getCodexPromptDir(projectDir, installLocation); const destDir = this.getCodexPromptDir(projectDir, installLocation);
await fs.ensureDir(destDir); await fs.ensureDir(destDir);
await this.clearOldBmadFiles(destDir); await this.clearBmadPrefixedFiles(destDir);
const written = await this.flattenAndWriteArtifacts(artifacts, destDir); const written = await this.writeFlattenedArtifacts(artifacts, destDir);
console.log(chalk.green(`${this.name} configured:`)); console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - Mode: CLI`)); console.log(chalk.dim(` - Mode: CLI`));
@ -212,40 +212,7 @@ class CodexSetup extends BaseIdeSetup {
return path.join(os.homedir(), '.codex', 'prompts'); return path.join(os.homedir(), '.codex', 'prompts');
} }
async flattenAndWriteArtifacts(artifacts, destDir) { // Uses inherited writeFlattenedArtifacts() and clearBmadPrefixedFiles() from BaseIdeSetup
let written = 0;
for (const artifact of artifacts) {
const flattenedName = this.flattenFilename(artifact.relativePath);
const targetPath = path.join(destDir, flattenedName);
await fs.writeFile(targetPath, artifact.content);
written++;
}
return written;
}
async clearOldBmadFiles(destDir) {
if (!(await fs.pathExists(destDir))) {
return;
}
const entries = await fs.readdir(destDir);
for (const entry of entries) {
if (!entry.startsWith('bmad-')) {
continue;
}
const entryPath = path.join(destDir, entry);
const stat = await fs.stat(entryPath);
if (stat.isFile()) {
await fs.remove(entryPath);
} else if (stat.isDirectory()) {
await fs.remove(entryPath);
}
}
}
async readAndProcessWithProject(filePath, metadata, projectDir) { async readAndProcessWithProject(filePath, metadata, projectDir) {
const content = await fs.readFile(filePath, 'utf8'); const content = await fs.readFile(filePath, 'utf8');
@ -337,11 +304,11 @@ class CodexSetup extends BaseIdeSetup {
async cleanup(projectDir = null) { async cleanup(projectDir = null) {
// Clean both global and project-specific locations // Clean both global and project-specific locations
const globalDir = this.getCodexPromptDir(null, 'global'); const globalDir = this.getCodexPromptDir(null, 'global');
await this.clearOldBmadFiles(globalDir); await this.clearBmadPrefixedFiles(globalDir);
if (projectDir) { if (projectDir) {
const projectSpecificDir = this.getCodexPromptDir(projectDir, 'project'); const projectSpecificDir = this.getCodexPromptDir(projectDir, 'project');
await this.clearOldBmadFiles(projectSpecificDir); await this.clearBmadPrefixedFiles(projectSpecificDir);
} }
} }