This commit is contained in:
Alex Verkhovsky 2026-02-24 18:47:47 -07:00 committed by GitHub
commit 536b845a09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 57 additions and 1 deletions

View File

@ -36,6 +36,25 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
async setup(projectDir, bmadDir, options = {}) { async setup(projectDir, bmadDir, options = {}) {
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
// Check for BMAD files in ancestor directories that would cause duplicates
if (this.installerConfig?.ancestor_conflict_check) {
const conflict = await this.findAncestorConflict(projectDir);
if (conflict) {
await prompts.log.error(
`Found existing BMAD commands in ancestor directory: ${conflict}\n` +
` ${this.name} inherits commands from parent directories, so this would cause duplicates.\n` +
` Please remove the BMAD files from that directory first:\n` +
` rm "${conflict}/"bmad*`,
);
return {
success: false,
reason: 'ancestor-conflict',
error: `Ancestor conflict: ${conflict}`,
conflictDir: conflict,
};
}
}
// Clean up any old BMAD installation first // Clean up any old BMAD installation first
await this.cleanup(projectDir, options); await this.cleanup(projectDir, options);
@ -531,6 +550,40 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
} }
} }
} }
/**
* Check ancestor directories for existing BMAD files in the same target_dir.
* IDEs like Claude Code inherit commands from parent directories, so an existing
* installation in an ancestor would cause duplicate commands.
* @param {string} projectDir - Project directory being installed to
* @returns {Promise<string|null>} Path to conflicting directory, or null if clean
*/
async findAncestorConflict(projectDir) {
const targetDir = this.installerConfig?.target_dir;
if (!targetDir) return null;
const resolvedProject = path.resolve(projectDir);
let current = path.dirname(resolvedProject);
const root = path.parse(current).root;
while (current !== root && current.length > root.length) {
const candidatePath = path.join(current, targetDir);
try {
if (await fs.pathExists(candidatePath)) {
const entries = await fs.readdir(candidatePath);
const hasBmad = entries.some((e) => typeof e === 'string' && e.startsWith('bmad'));
if (hasBmad) {
return candidatePath;
}
}
} catch {
// Can't read directory — skip
}
current = path.dirname(current);
}
return null;
}
/** /**
* Recursively remove empty directories walking up from dir toward projectDir * Recursively remove empty directories walking up from dir toward projectDir
* Stops at projectDir boundary never removes projectDir itself * Stops at projectDir boundary never removes projectDir itself

View File

@ -206,7 +206,9 @@ class IdeManager {
if (handlerResult.tools > 0) parts.push(`${handlerResult.tools} tools`); if (handlerResult.tools > 0) parts.push(`${handlerResult.tools} tools`);
detail = parts.join(', '); detail = parts.join(', ');
} }
return { success: true, ide: ideName, detail, handlerResult }; // Propagate handler's success status (default true for backward compat)
const success = handlerResult?.success !== false;
return { success, ide: ideName, detail, error: handlerResult?.error, handlerResult };
} catch (error) { } catch (error) {
await prompts.log.error(`Failed to setup ${ideName}: ${error.message}`); await prompts.log.error(`Failed to setup ${ideName}: ${error.message}`);
return { success: false, ide: ideName, error: error.message }; return { success: false, ide: ideName, error: error.message };

View File

@ -40,6 +40,7 @@ platforms:
installer: installer:
target_dir: .claude/commands target_dir: .claude/commands
template_type: default template_type: default
ancestor_conflict_check: true
cline: cline:
name: "Cline" name: "Cline"