#!/usr/bin/env node /** * ECC CodeBuddy Installer (Cross-platform Node.js version) * Installs Everything Claude Code workflows into a CodeBuddy project. * * Usage: * node install.js # Install to current directory * node install.js ~ # Install globally to ~/.codebuddy/ */ const fs = require('fs'); const path = require('path'); const os = require('os'); // Platform detection const isWindows = process.platform === 'win32'; /** * Get home directory cross-platform */ function getHomeDir() { return process.env.USERPROFILE || process.env.HOME || os.homedir(); } /** * Ensure directory exists */ function ensureDir(dirPath) { try { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } catch (err) { if (err.code !== 'EEXIST') { throw err; } } } /** * Read lines from a file */ function readLines(filePath) { try { if (!fs.existsSync(filePath)) { return []; } const content = fs.readFileSync(filePath, 'utf8'); return content.split('\n').filter(line => line.length > 0); } catch { return []; } } /** * Check if manifest contains an entry */ function manifestHasEntry(manifestPath, entry) { const lines = readLines(manifestPath); return lines.includes(entry); } /** * Add entry to manifest */ function ensureManifestEntry(manifestPath, entry) { try { const lines = readLines(manifestPath); if (!lines.includes(entry)) { const content = lines.join('\n') + (lines.length > 0 ? '\n' : '') + entry + '\n'; fs.writeFileSync(manifestPath, content, 'utf8'); } } catch (err) { console.error(`Error updating manifest: ${err.message}`); } } /** * Copy a file and manage in manifest */ function copyManagedFile(sourcePath, targetPath, manifestPath, manifestEntry, makeExecutable = false) { const alreadyManaged = manifestHasEntry(manifestPath, manifestEntry); // If target file already exists if (fs.existsSync(targetPath)) { if (alreadyManaged) { ensureManifestEntry(manifestPath, manifestEntry); } return false; } // Copy the file try { ensureDir(path.dirname(targetPath)); fs.copyFileSync(sourcePath, targetPath); // Make executable on Unix systems if (makeExecutable && !isWindows) { fs.chmodSync(targetPath, 0o755); } ensureManifestEntry(manifestPath, manifestEntry); return true; } catch (err) { console.error(`Error copying ${sourcePath}: ${err.message}`); return false; } } /** * Recursively find files in a directory */ function findFiles(dir, extension = '') { const results = []; try { if (!fs.existsSync(dir)) { return results; } function walk(currentPath) { try { const entries = fs.readdirSync(currentPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentPath, entry.name); if (entry.isDirectory()) { walk(fullPath); } else if (!extension || entry.name.endsWith(extension)) { results.push(fullPath); } } } catch { // Ignore permission errors } } walk(dir); } catch { // Ignore errors } return results.sort(); } /** * Main install function */ function doInstall() { // Resolve script directory (where this file lives) const scriptDir = path.dirname(path.resolve(__filename)); const repoRoot = path.dirname(scriptDir); 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 Installer'); console.log('======================='); console.log(''); console.log(`Source: ${repoRoot}`); console.log(`Target: ${codebuddyFullPath}/`); console.log(''); // Create subdirectories const subdirs = ['commands', 'agents', 'skills', 'rules']; for (const dir of subdirs) { ensureDir(path.join(codebuddyFullPath, dir)); } // Manifest file const manifest = path.join(codebuddyFullPath, '.ecc-manifest'); ensureDir(path.dirname(manifest)); // Counters let commands = 0; let agents = 0; let skills = 0; let rules = 0; // Copy commands const commandsDir = path.join(repoRoot, 'commands'); if (fs.existsSync(commandsDir)) { const files = findFiles(commandsDir, '.md'); for (const file of files) { if (path.basename(path.dirname(file)) === 'commands') { const localName = path.basename(file); const targetPath = path.join(codebuddyFullPath, 'commands', localName); if (copyManagedFile(file, targetPath, manifest, `commands/${localName}`)) { commands += 1; } } } } // Copy agents const agentsDir = path.join(repoRoot, 'agents'); if (fs.existsSync(agentsDir)) { const files = findFiles(agentsDir, '.md'); for (const file of files) { if (path.basename(path.dirname(file)) === 'agents') { const localName = path.basename(file); const targetPath = path.join(codebuddyFullPath, 'agents', localName); if (copyManagedFile(file, targetPath, manifest, `agents/${localName}`)) { agents += 1; } } } } // Copy skills (with subdirectories) const skillsDir = path.join(repoRoot, 'skills'); if (fs.existsSync(skillsDir)) { const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true }) .filter(entry => entry.isDirectory()) .map(entry => entry.name); for (const skillName of skillDirs) { const sourceSkillDir = path.join(skillsDir, skillName); const targetSkillDir = path.join(codebuddyFullPath, 'skills', skillName); let skillCopied = false; const skillFiles = findFiles(sourceSkillDir); for (const sourceFile of skillFiles) { const relativePath = path.relative(sourceSkillDir, sourceFile); const targetPath = path.join(targetSkillDir, relativePath); const manifestEntry = `skills/${skillName}/${relativePath.replace(/\\/g, '/')}`; if (copyManagedFile(sourceFile, targetPath, manifest, manifestEntry)) { skillCopied = true; } } if (skillCopied) { skills += 1; } } } // Copy rules (with subdirectories) const rulesDir = path.join(repoRoot, 'rules'); if (fs.existsSync(rulesDir)) { const ruleFiles = findFiles(rulesDir); for (const ruleFile of ruleFiles) { const relativePath = path.relative(rulesDir, ruleFile); const targetPath = path.join(codebuddyFullPath, 'rules', relativePath); const manifestEntry = `rules/${relativePath.replace(/\\/g, '/')}`; if (copyManagedFile(ruleFile, targetPath, manifest, manifestEntry)) { rules += 1; } } } // Copy README files (skip install/uninstall scripts to avoid broken // path references when the copied script runs from the target directory) const readmeFiles = ['README.md', 'README.zh-CN.md']; for (const readmeFile of readmeFiles) { const sourcePath = path.join(scriptDir, readmeFile); if (fs.existsSync(sourcePath)) { const targetPath = path.join(codebuddyFullPath, readmeFile); copyManagedFile(sourcePath, targetPath, manifest, readmeFile); } } // Add manifest itself ensureManifestEntry(manifest, '.ecc-manifest'); // Print summary console.log('Installation complete!'); console.log(''); console.log('Components installed:'); console.log(` Commands: ${commands}`); console.log(` Agents: ${agents}`); console.log(` Skills: ${skills}`); console.log(` Rules: ${rules}`); console.log(''); console.log(`Directory: ${path.basename(codebuddyFullPath)}`); console.log(''); console.log('Next steps:'); console.log(' 1. Open your project in CodeBuddy'); console.log(' 2. Type / to see available commands'); console.log(' 3. Enjoy the ECC workflows!'); console.log(''); console.log('To uninstall later:'); console.log(` cd ${codebuddyFullPath}`); console.log(' node uninstall.js'); console.log(''); } // Run installer try { doInstall(); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); }