fix(installer): refuse install when ancestor dir has BMAD commands (#1735)
* 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. * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d94757a7d4
commit
2d2f4855b1
|
|
@ -34,6 +34,25 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
* @returns {Promise<Object>} Setup result
|
* @returns {Promise<Object>} Setup result
|
||||||
*/
|
*/
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
async setup(projectDir, bmadDir, options = {}) {
|
||||||
|
// 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 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 -rf "${conflict}"/bmad*`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: 'ancestor-conflict',
|
||||||
|
error: `Ancestor conflict: ${conflict}`,
|
||||||
|
conflictDir: conflict,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
|
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
|
||||||
|
|
||||||
// Clean up any old BMAD installation first
|
// Clean up any old BMAD installation first
|
||||||
|
|
@ -540,6 +559,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 = await fs.realpath(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.toLowerCase().startsWith('bmad'));
|
||||||
|
if (hasBmad) {
|
||||||
|
return candidatePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Can't read directory — skip
|
||||||
|
}
|
||||||
|
current = path.dirname(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Walk up ancestor directories from relativeDir toward projectDir, removing each if empty
|
* Walk up ancestor directories from relativeDir toward projectDir, removing each if empty
|
||||||
* Stops at projectDir boundary — never removes projectDir itself
|
* Stops at projectDir boundary — never removes projectDir itself
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -202,6 +203,9 @@ platforms:
|
||||||
# artifact_types: [agents, workflows, tasks, tools]
|
# artifact_types: [agents, workflows, tasks, tools]
|
||||||
# artifact_types: array (optional) # Filter which artifacts to install (default: all)
|
# artifact_types: array (optional) # Filter which artifacts to install (default: all)
|
||||||
# skip_existing: boolean (optional) # Skip files that already exist (default: false)
|
# 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
|
# Platform Categories
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue