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}`;
}
/**
* 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
* @param {string} bmadDir - BMAD installation directory

View File

@ -85,14 +85,15 @@ class AntigravitySetup extends BaseIdeSetup {
/**
* Cleanup old BMAD installation before reinstalling
* Removes files and directories starting with 'bmad-' from workflows dir
* @param {string} projectDir - Project directory
*/
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)) {
await fs.remove(bmadWorkflowsDir);
console.log(chalk.dim(` Removed old BMAD workflows from ${this.name}`));
if (removedCount > 0) {
console.log(chalk.dim(` Removed ${removedCount} old BMAD items from ${this.name}`));
}
}
@ -114,25 +115,16 @@ class AntigravitySetup extends BaseIdeSetup {
// 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);
await this.ensureDir(workflowsDir);
// 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 || []);
const { artifacts: agentArtifacts } = 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++;
}
// Write agent launcher files with flattened naming directly to workflows dir
const agentCount = await this.writeFlattenedArtifacts(agentArtifacts, workflowsDir);
// Process Antigravity specific injections for installed modules
// 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 { 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++;
}
}
// Filter to workflow-command type and write with flattened naming
const workflowCommands = workflowArtifacts.filter((a) => a.type === 'workflow-command');
const workflowCommandCount = await this.writeFlattenedArtifacts(workflowCommands, workflowsDir);
// Generate task and tool commands from manifests (if they exist)
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)`));
return {
@ -463,12 +448,11 @@ class AntigravitySetup extends BaseIdeSetup {
* @returns {Object} Installation result
*/
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 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
const launcherContent = `name: '${agentName}'
@ -490,7 +474,7 @@ usage: |
**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);
const launcherPath = path.join(workflowsDir, fileName);
// Write the launcher file
await fs.writeFile(launcherPath, launcherContent, 'utf8');

View File

@ -95,8 +95,8 @@ class CodexSetup extends BaseIdeSetup {
const destDir = this.getCodexPromptDir(projectDir, installLocation);
await fs.ensureDir(destDir);
await this.clearOldBmadFiles(destDir);
const written = await this.flattenAndWriteArtifacts(artifacts, destDir);
await this.clearBmadPrefixedFiles(destDir);
const written = await this.writeFlattenedArtifacts(artifacts, destDir);
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - Mode: CLI`));
@ -212,40 +212,7 @@ class CodexSetup extends BaseIdeSetup {
return path.join(os.homedir(), '.codex', 'prompts');
}
async flattenAndWriteArtifacts(artifacts, destDir) {
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);
}
}
}
// Uses inherited writeFlattenedArtifacts() and clearBmadPrefixedFiles() from BaseIdeSetup
async readAndProcessWithProject(filePath, metadata, projectDir) {
const content = await fs.readFile(filePath, 'utf8');
@ -337,11 +304,11 @@ class CodexSetup extends BaseIdeSetup {
async cleanup(projectDir = null) {
// Clean both global and project-specific locations
const globalDir = this.getCodexPromptDir(null, 'global');
await this.clearOldBmadFiles(globalDir);
await this.clearBmadPrefixedFiles(globalDir);
if (projectDir) {
const projectSpecificDir = this.getCodexPromptDir(projectDir, 'project');
await this.clearOldBmadFiles(projectSpecificDir);
await this.clearBmadPrefixedFiles(projectSpecificDir);
}
}