From 48b3bb1709f6ba9cf4830928de420e8705988157 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Sun, 22 Feb 2026 14:16:07 -0700 Subject: [PATCH 1/3] fix(installer): refuse install when ancestor dir has BMAD commands Claude Code inherits slash commands from parent directories, so installing into a nested project when a parent already has .claude/commands with bmad-* files causes duplicate entries in the autocomplete. Add ancestor_conflict_check flag (enabled for claude-code) that walks up the directory tree before install. If BMAD files are found in an ancestor target_dir, the installer refuses with an actionable error. Also fix IdeManager.setup() to propagate handler success status instead of unconditionally returning success: true. --- .../cli/installers/lib/ide/_config-driven.js | 53 +++++++++++++++++++ tools/cli/installers/lib/ide/manager.js | 4 +- .../installers/lib/ide/platform-codes.yaml | 1 + 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 9541c75ed..b258492e0 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -36,6 +36,25 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { async setup(projectDir, bmadDir, options = {}) { 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 await this.cleanup(projectDir, options); @@ -532,6 +551,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} 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 * Stops at projectDir boundary — never removes projectDir itself diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index 9b8df1597..9e286fdd3 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -206,7 +206,9 @@ class IdeManager { if (handlerResult.tools > 0) parts.push(`${handlerResult.tools} tools`); 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) { await prompts.log.error(`Failed to setup ${ideName}: ${error.message}`); return { success: false, ide: ideName, error: error.message }; diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml index 16723f9c5..b97b82ae1 100644 --- a/tools/cli/installers/lib/ide/platform-codes.yaml +++ b/tools/cli/installers/lib/ide/platform-codes.yaml @@ -40,6 +40,7 @@ platforms: installer: target_dir: .claude/commands template_type: default + ancestor_conflict_check: true cline: name: "Cline" From 886a070d2beaffcfbfa0f21491e6402523c83569 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Tue, 24 Feb 2026 19:34:39 -0700 Subject: [PATCH 2/3] Address code review feedback from CodeRabbit and Augment - Move "Setting up..." log after conflict check so it only shows when install will proceed - Fix rm command: add -rf flags and correct quoting for glob outside quotes - Improve error wording: "ancestor installation" instead of misleading "ancestor directory" - Use case-insensitive startsWith for bmad file detection (macOS/Windows) - Document ancestor_conflict_check in the installer config schema Co-Authored-By: Claude Opus 4.6 --- tools/cli/installers/lib/ide/_config-driven.js | 10 +++++----- tools/cli/installers/lib/ide/platform-codes.yaml | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index c0fea9a62..221f46905 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -34,17 +34,15 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { * @returns {Promise} Setup result */ async setup(projectDir, bmadDir, options = {}) { - 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` + + `Found existing BMAD commands in ancestor installation: ${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*`, + ` rm -rf "${conflict}"/bmad*`, ); return { success: false, @@ -55,6 +53,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { } } + if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); + // Clean up any old BMAD installation first await this.cleanup(projectDir, options); @@ -570,7 +570,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} try { if (await fs.pathExists(candidatePath)) { const entries = await fs.readdir(candidatePath); - const hasBmad = entries.some((e) => typeof e === 'string' && e.startsWith('bmad')); + const hasBmad = entries.some((e) => typeof e === 'string' && e.toLowerCase().startsWith('bmad')); if (hasBmad) { return candidatePath; } diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml index b97b82ae1..69ffd6848 100644 --- a/tools/cli/installers/lib/ide/platform-codes.yaml +++ b/tools/cli/installers/lib/ide/platform-codes.yaml @@ -198,6 +198,9 @@ platforms: # artifact_types: [agents, workflows, tasks, tools] # artifact_types: array (optional) # Filter which artifacts to install (default: all) # skip_existing: boolean (optional) # Skip files that already exist (default: false) +# ancestor_conflict_check: boolean (optional) # Refuse install when ancestor dir has BMAD files +# # in the same target_dir (for IDEs that inherit +# # commands from parent directories) # ============================================================================ # Platform Categories From 1c789c05e24b5c8aa807b9ccd63955b42418d471 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Wed, 25 Feb 2026 12:39:59 -0700 Subject: [PATCH 3/3] fix(installer): resolve symlinks before ancestor conflict walk Use fs.realpath() instead of path.resolve() so the ancestor directory walk follows the physical filesystem path, not the logical symlink path. Co-Authored-By: Claude Opus 4.6 --- tools/cli/installers/lib/ide/_config-driven.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 221f46905..05b1a7389 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -561,7 +561,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} const targetDir = this.installerConfig?.target_dir; if (!targetDir) return null; - const resolvedProject = path.resolve(projectDir); + const resolvedProject = await fs.realpath(path.resolve(projectDir)); let current = path.dirname(resolvedProject); const root = path.parse(current).root;