mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-01 06:33:27 +08:00
* feat(install): add CodeBuddy(Tencent) adaptation with installation scripts * fix: add codebuddy to SUPPORTED_INSTALL_TARGETS * fix(codebuddy): resolve installer path issues, unused vars, and uninstall safety
292 lines
7.9 KiB
JavaScript
Executable File
292 lines
7.9 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* ECC CodeBuddy Uninstaller (Cross-platform Node.js version)
|
|
* Uninstalls Everything Claude Code workflows from a CodeBuddy project.
|
|
*
|
|
* Usage:
|
|
* node uninstall.js # Uninstall from current directory
|
|
* node uninstall.js ~ # Uninstall globally from ~/.codebuddy/
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const readline = require('readline');
|
|
|
|
/**
|
|
* Get home directory cross-platform
|
|
*/
|
|
function getHomeDir() {
|
|
return process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
}
|
|
|
|
/**
|
|
* Resolve a path to its canonical form
|
|
*/
|
|
function resolvePath(filePath) {
|
|
try {
|
|
return fs.realpathSync(filePath);
|
|
} catch {
|
|
// If realpath fails, return the path as-is
|
|
return path.resolve(filePath);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a manifest entry is valid (security check)
|
|
*/
|
|
function isValidManifestEntry(entry) {
|
|
// Reject empty, absolute paths, parent directory references
|
|
if (!entry || entry.length === 0) return false;
|
|
if (entry.startsWith('/')) return false;
|
|
if (entry.startsWith('~')) return false;
|
|
if (entry.includes('/../') || entry.includes('/..')) return false;
|
|
if (entry.startsWith('../') || entry.startsWith('..\\')) return false;
|
|
if (entry === '..' || entry === '...' || entry.includes('\\..\\')||entry.includes('/..')) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Read lines from manifest file
|
|
*/
|
|
function readManifest(manifestPath) {
|
|
try {
|
|
if (!fs.existsSync(manifestPath)) {
|
|
return [];
|
|
}
|
|
const content = fs.readFileSync(manifestPath, 'utf8');
|
|
return content.split('\n').filter(line => line.length > 0);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively find empty directories
|
|
*/
|
|
function findEmptyDirs(dirPath) {
|
|
const emptyDirs = [];
|
|
|
|
function walkDirs(currentPath) {
|
|
try {
|
|
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
const subdirs = entries.filter(e => e.isDirectory());
|
|
|
|
for (const subdir of subdirs) {
|
|
const subdirPath = path.join(currentPath, subdir.name);
|
|
walkDirs(subdirPath);
|
|
}
|
|
|
|
// Check if directory is now empty
|
|
try {
|
|
const remaining = fs.readdirSync(currentPath);
|
|
if (remaining.length === 0 && currentPath !== dirPath) {
|
|
emptyDirs.push(currentPath);
|
|
}
|
|
} catch {
|
|
// Directory might have been deleted
|
|
}
|
|
} catch {
|
|
// Ignore errors
|
|
}
|
|
}
|
|
|
|
walkDirs(dirPath);
|
|
return emptyDirs.sort().reverse(); // Sort in reverse for removal
|
|
}
|
|
|
|
/**
|
|
* Prompt user for confirmation
|
|
*/
|
|
async function promptConfirm(question) {
|
|
return new Promise((resolve) => {
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
});
|
|
|
|
rl.question(question, (answer) => {
|
|
rl.close();
|
|
resolve(/^[yY]$/.test(answer));
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Main uninstall function
|
|
*/
|
|
async function doUninstall() {
|
|
const codebuddyDirName = '.codebuddy';
|
|
|
|
// Parse arguments
|
|
let targetDir = process.cwd();
|
|
if (process.argv.length > 2) {
|
|
const arg = process.argv[2];
|
|
if (arg === '~' || arg === getHomeDir()) {
|
|
targetDir = getHomeDir();
|
|
} else {
|
|
targetDir = path.resolve(arg);
|
|
}
|
|
}
|
|
|
|
// Determine codebuddy full path
|
|
let codebuddyFullPath;
|
|
const baseName = path.basename(targetDir);
|
|
|
|
if (baseName === codebuddyDirName) {
|
|
codebuddyFullPath = targetDir;
|
|
} else {
|
|
codebuddyFullPath = path.join(targetDir, codebuddyDirName);
|
|
}
|
|
|
|
console.log('ECC CodeBuddy Uninstaller');
|
|
console.log('==========================');
|
|
console.log('');
|
|
console.log(`Target: ${codebuddyFullPath}/`);
|
|
console.log('');
|
|
|
|
// Check if codebuddy directory exists
|
|
if (!fs.existsSync(codebuddyFullPath)) {
|
|
console.error(`Error: ${codebuddyDirName} directory not found at ${targetDir}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const codebuddyRootResolved = resolvePath(codebuddyFullPath);
|
|
const manifest = path.join(codebuddyFullPath, '.ecc-manifest');
|
|
|
|
// Handle missing manifest
|
|
if (!fs.existsSync(manifest)) {
|
|
console.log('Warning: No manifest file found (.ecc-manifest)');
|
|
console.log('');
|
|
console.log('This could mean:');
|
|
console.log(' 1. ECC was installed with an older version without manifest support');
|
|
console.log(' 2. The manifest file was manually deleted');
|
|
console.log('');
|
|
|
|
const confirmed = await promptConfirm(`Do you want to remove the entire ${codebuddyDirName} directory? (y/N) `);
|
|
if (!confirmed) {
|
|
console.log('Uninstall cancelled.');
|
|
process.exit(0);
|
|
}
|
|
|
|
try {
|
|
fs.rmSync(codebuddyFullPath, { recursive: true, force: true });
|
|
console.log('Uninstall complete!');
|
|
console.log('');
|
|
console.log(`Removed: ${codebuddyFullPath}/`);
|
|
} catch (err) {
|
|
console.error(`Error removing directory: ${err.message}`);
|
|
process.exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
console.log('Found manifest file - will only remove files installed by ECC');
|
|
console.log('');
|
|
|
|
const confirmed = await promptConfirm(`Are you sure you want to uninstall ECC from ${codebuddyDirName}? (y/N) `);
|
|
if (!confirmed) {
|
|
console.log('Uninstall cancelled.');
|
|
process.exit(0);
|
|
}
|
|
|
|
// Read manifest and remove files
|
|
const manifestLines = readManifest(manifest);
|
|
let removed = 0;
|
|
let skipped = 0;
|
|
|
|
for (const filePath of manifestLines) {
|
|
if (!filePath || filePath.length === 0) continue;
|
|
|
|
if (!isValidManifestEntry(filePath)) {
|
|
console.log(`Skipped: ${filePath} (invalid manifest entry)`);
|
|
skipped += 1;
|
|
continue;
|
|
}
|
|
|
|
const fullPath = path.join(codebuddyFullPath, filePath);
|
|
|
|
// Security check: use path.relative() to ensure the manifest entry
|
|
// resolves inside the codebuddy directory. This is stricter than
|
|
// startsWith and correctly handles edge-cases with symlinks.
|
|
const relative = path.relative(codebuddyRootResolved, path.resolve(fullPath));
|
|
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
console.log(`Skipped: ${filePath} (outside target directory)`);
|
|
skipped += 1;
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const stats = fs.lstatSync(fullPath);
|
|
|
|
if (stats.isFile() || stats.isSymbolicLink()) {
|
|
fs.unlinkSync(fullPath);
|
|
console.log(`Removed: ${filePath}`);
|
|
removed += 1;
|
|
} else if (stats.isDirectory()) {
|
|
try {
|
|
const files = fs.readdirSync(fullPath);
|
|
if (files.length === 0) {
|
|
fs.rmdirSync(fullPath);
|
|
console.log(`Removed: ${filePath}/`);
|
|
removed += 1;
|
|
} else {
|
|
console.log(`Skipped: ${filePath}/ (not empty - contains user files)`);
|
|
skipped += 1;
|
|
}
|
|
} catch {
|
|
console.log(`Skipped: ${filePath}/ (not empty - contains user files)`);
|
|
skipped += 1;
|
|
}
|
|
}
|
|
} catch {
|
|
skipped += 1;
|
|
}
|
|
}
|
|
|
|
// Remove empty directories
|
|
const emptyDirs = findEmptyDirs(codebuddyFullPath);
|
|
for (const emptyDir of emptyDirs) {
|
|
try {
|
|
fs.rmdirSync(emptyDir);
|
|
const relativePath = path.relative(codebuddyFullPath, emptyDir);
|
|
console.log(`Removed: ${relativePath}/`);
|
|
removed += 1;
|
|
} catch {
|
|
// Directory might not be empty anymore
|
|
}
|
|
}
|
|
|
|
// Try to remove main codebuddy directory if empty
|
|
try {
|
|
const files = fs.readdirSync(codebuddyFullPath);
|
|
if (files.length === 0) {
|
|
fs.rmdirSync(codebuddyFullPath);
|
|
console.log(`Removed: ${codebuddyDirName}/`);
|
|
removed += 1;
|
|
}
|
|
} catch {
|
|
// Directory not empty
|
|
}
|
|
|
|
// Print summary
|
|
console.log('');
|
|
console.log('Uninstall complete!');
|
|
console.log('');
|
|
console.log('Summary:');
|
|
console.log(` Removed: ${removed} items`);
|
|
console.log(` Skipped: ${skipped} items (not found or user-modified)`);
|
|
console.log('');
|
|
|
|
if (fs.existsSync(codebuddyFullPath)) {
|
|
console.log(`Note: ${codebuddyDirName} directory still exists (contains user-added files)`);
|
|
}
|
|
}
|
|
|
|
// Run uninstaller
|
|
doUninstall().catch((error) => {
|
|
console.error(`Error: ${error.message}`);
|
|
process.exit(1);
|
|
});
|