fix(cli): address code review findings for uninstall command
- Add path traversal guard in uninstallOutputFolder (resolve + startsWith) - Thread silent flag through to cleanupCopilotInstructions - Trim text input before path.resolve in directory prompt - DRY uninstall() by delegating to extracted helper methods - Validate projectDir existence before probing for BMAD - Use fs.rmdir instead of fs.remove in removeEmptyParents (race safety) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fc3c3d873c
commit
54876a6af3
|
|
@ -43,12 +43,17 @@ module.exports = {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
projectDir = path.resolve(customDir);
|
projectDir = path.resolve(customDir.trim());
|
||||||
} else {
|
} else {
|
||||||
projectDir = process.cwd();
|
projectDir = process.cwd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(projectDir))) {
|
||||||
|
await prompts.log.error(`Directory does not exist: ${projectDir}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
const { bmadDir } = await installer.findBmadDir(projectDir);
|
const { bmadDir } = await installer.findBmadDir(projectDir);
|
||||||
|
|
||||||
if (!(await fs.pathExists(bmadDir))) {
|
if (!(await fs.pathExists(bmadDir))) {
|
||||||
|
|
|
||||||
|
|
@ -1552,30 +1552,18 @@ class Installer {
|
||||||
|
|
||||||
// 2. IDE CLEANUP (before _bmad/ deletion so configs are accessible)
|
// 2. IDE CLEANUP (before _bmad/ deletion so configs are accessible)
|
||||||
if (options.removeIdeConfigs !== false) {
|
if (options.removeIdeConfigs !== false) {
|
||||||
await this.ideManager.ensureInitialized();
|
await this.uninstallIdeConfigs(projectDir, existingInstall, { silent: options.silent });
|
||||||
const cleanupOptions = { isUninstall: true };
|
|
||||||
const ideList = existingInstall.ides || [];
|
|
||||||
if (ideList.length > 0) {
|
|
||||||
await this.ideManager.cleanupByList(projectDir, ideList, cleanupOptions);
|
|
||||||
} else {
|
|
||||||
await this.ideManager.cleanup(projectDir, cleanupOptions);
|
|
||||||
}
|
|
||||||
removed.ideConfigs = true;
|
removed.ideConfigs = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. OUTPUT FOLDER (only if explicitly requested)
|
// 3. OUTPUT FOLDER (only if explicitly requested)
|
||||||
if (options.removeOutputFolder === true && outputFolder) {
|
if (options.removeOutputFolder === true && outputFolder) {
|
||||||
const outputPath = path.join(projectDir, outputFolder);
|
removed.outputFolder = await this.uninstallOutputFolder(projectDir, outputFolder);
|
||||||
if (await fs.pathExists(outputPath)) {
|
|
||||||
await fs.remove(outputPath);
|
|
||||||
removed.outputFolder = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. BMAD DIRECTORY (last, after everything that needs it)
|
// 4. BMAD DIRECTORY (last, after everything that needs it)
|
||||||
if (options.removeModules !== false) {
|
if (options.removeModules !== false) {
|
||||||
await fs.remove(bmadDir);
|
removed.modules = await this.uninstallModules(projectDir);
|
||||||
removed.modules = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, removed, version: existingInstall.version };
|
return { success: true, removed, version: existingInstall.version };
|
||||||
|
|
@ -1606,7 +1594,11 @@ class Installer {
|
||||||
*/
|
*/
|
||||||
async uninstallOutputFolder(projectDir, outputFolder) {
|
async uninstallOutputFolder(projectDir, outputFolder) {
|
||||||
if (!outputFolder) return false;
|
if (!outputFolder) return false;
|
||||||
const outputPath = path.join(projectDir, outputFolder);
|
const resolvedProject = path.resolve(projectDir);
|
||||||
|
const outputPath = path.resolve(resolvedProject, outputFolder);
|
||||||
|
if (!outputPath.startsWith(resolvedProject + path.sep)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (await fs.pathExists(outputPath)) {
|
if (await fs.pathExists(outputPath)) {
|
||||||
await fs.remove(outputPath);
|
await fs.remove(outputPath);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -548,7 +548,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
if (!(await fs.pathExists(fullPath))) break;
|
if (!(await fs.pathExists(fullPath))) break;
|
||||||
const remaining = await fs.readdir(fullPath);
|
const remaining = await fs.readdir(fullPath);
|
||||||
if (remaining.length > 0) break;
|
if (remaining.length > 0) break;
|
||||||
await fs.remove(fullPath);
|
await fs.rmdir(fullPath);
|
||||||
} catch {
|
} catch {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -640,7 +640,7 @@ Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent ac
|
||||||
// handles marker-based replacement in a single read-modify-write pass,
|
// handles marker-based replacement in a single read-modify-write pass,
|
||||||
// which correctly preserves user content outside the markers.
|
// which correctly preserves user content outside the markers.
|
||||||
if (options.isUninstall) {
|
if (options.isUninstall) {
|
||||||
await this.cleanupCopilotInstructions(projectDir);
|
await this.cleanupCopilotInstructions(projectDir, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -649,8 +649,9 @@ Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent ac
|
||||||
* If file becomes empty after stripping, delete it.
|
* If file becomes empty after stripping, delete it.
|
||||||
* If a .bak backup exists and the main file was deleted, restore the backup.
|
* If a .bak backup exists and the main file was deleted, restore the backup.
|
||||||
* @param {string} projectDir - Project directory
|
* @param {string} projectDir - Project directory
|
||||||
|
* @param {Object} [options] - Options (e.g. { silent: true })
|
||||||
*/
|
*/
|
||||||
async cleanupCopilotInstructions(projectDir) {
|
async cleanupCopilotInstructions(projectDir, options = {}) {
|
||||||
const instructionsPath = path.join(projectDir, this.githubDir, 'copilot-instructions.md');
|
const instructionsPath = path.join(projectDir, this.githubDir, 'copilot-instructions.md');
|
||||||
const backupPath = `${instructionsPath}.bak`;
|
const backupPath = `${instructionsPath}.bak`;
|
||||||
|
|
||||||
|
|
@ -680,8 +681,10 @@ Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent ac
|
||||||
// If backup exists, restore it
|
// If backup exists, restore it
|
||||||
if (await fs.pathExists(backupPath)) {
|
if (await fs.pathExists(backupPath)) {
|
||||||
await fs.rename(backupPath, instructionsPath);
|
await fs.rename(backupPath, instructionsPath);
|
||||||
|
if (!options.silent) {
|
||||||
await prompts.log.message(' Restored copilot-instructions.md from backup');
|
await prompts.log.message(' Restored copilot-instructions.md from backup');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Write cleaned content back (preserve original whitespace)
|
// Write cleaned content back (preserve original whitespace)
|
||||||
await fs.writeFile(instructionsPath, cleaned, 'utf8');
|
await fs.writeFile(instructionsPath, cleaned, 'utf8');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue