UNPKG

claudes-office

Version:

CLI tool to initialize Claude's office in your project

1,245 lines (1,059 loc) โ€ข 56.2 kB
#!/usr/bin/env node const fs = require('fs-extra'); const path = require('path'); const { program } = require('commander'); const chalk = require('chalk'); const inquirer = require('inquirer'); const figlet = require('figlet'); const ora = require('ora'); // Get the package version const packageJson = require('../package.json'); // Helper function to create an awesome ASCII art logo with enhanced colors and styling const displayLogo = () => { console.log('\n'); console.log(chalk.bold.cyan(figlet.textSync('CLAUDE\'S', { font: 'ANSI Shadow', horizontalLayout: 'default', verticalLayout: 'default', }))); console.log(chalk.bold.magenta(figlet.textSync('OFFICE', { font: 'ANSI Shadow', horizontalLayout: 'default', verticalLayout: 'default', }))); console.log('\n'); console.log(chalk.bold.yellowBright(' โœจ Your AI Assistant\'s Workspace โœจ')); console.log(chalk.bold.green(` ๐Ÿ“ Version ${packageJson.version} ๐Ÿ“`)); console.log('\n'); }; // Helper function to check if a directory exists and it's a valid role async function getAvailableRolesInDirectory(dirPath) { if (!await fs.pathExists(dirPath)) { return []; } try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); const validDirs = []; for (const entry of entries) { if (entry.isDirectory()) { // Check if there's at least one .md file inside the directory const subDirPath = path.join(dirPath, entry.name); const subEntries = await fs.readdir(subDirPath, { withFileTypes: true }); const hasMdFile = subEntries.some(subEntry => subEntry.isFile() && subEntry.name.endsWith('.md') ); if (hasMdFile || entry.name !== '.DS_Store') { validDirs.push({ name: entry.name.charAt(0).toUpperCase() + entry.name.slice(1).replace(/-/g, ' '), value: entry.name }); } } } return validDirs; } catch (error) { return []; } } // Get custom assistant name from the user async function promptForCustomName() { const { useCustomName } = await inquirer.prompt([ { type: 'confirm', name: 'useCustomName', message: 'Would you like to use a custom name instead of "Claude"?', default: false } ]); if (!useCustomName) { return null; } const { customName } = await inquirer.prompt([ { type: 'input', name: 'customName', message: 'Enter the custom name for your assistant:', validate: (input) => { if (!input.trim()) { return 'Please enter a valid name'; } return true; } } ]); return customName.trim(); } // Replace all instances of "Claude" with custom name in a file async function replaceNameInFile(filePath, originalName, customName) { console.log(`Replacing name in file: ${filePath}`); // Always visible if (!await fs.pathExists(filePath)) { console.log(`File not found: ${filePath}`); return false; } try { let content = await fs.readFile(filePath, 'utf8'); console.log(`Read file content for ${filePath}, length: ${content.length}`); // Handle different case variations const patterns = [ new RegExp(originalName, 'g'), // Claude new RegExp(originalName.toLowerCase(), 'g'), // claude new RegExp(originalName.toUpperCase(), 'g'), // CLAUDE new RegExp(originalName + '\'s', 'g'), // Claude's new RegExp(originalName.toLowerCase() + '\'s', 'g'), // claude's new RegExp(originalName.toUpperCase() + '\'S', 'g') // CLAUDE'S ]; let modified = false; let totalReplaced = 0; // Replace each pattern with the appropriate case for (const pattern of patterns) { let thisPatternReplaced = 0; content = content.replace(pattern, (match) => { thisPatternReplaced++; totalReplaced++; modified = true; let replacement; if (match === match.toUpperCase()) replacement = customName.toUpperCase(); else if (match === match.toLowerCase()) replacement = customName.toLowerCase(); else if (match.includes("'s")) replacement = customName + "'s"; else if (match.includes("'S")) replacement = customName.toUpperCase() + "'S"; else replacement = customName.charAt(0).toUpperCase() + customName.slice(1); console.log(`Replaced "${match}" with "${replacement}"`); return replacement; }); if (thisPatternReplaced > 0) { console.log(`Pattern ${pattern} replaced ${thisPatternReplaced} times`); } } if (modified) { console.log(`Writing modified file with ${totalReplaced} replacements to ${filePath}`); await fs.writeFile(filePath, content, 'utf8'); return true; } else { console.log(`No replacements made in ${filePath}`); return false; } } catch (error) { console.error(`Error replacing name in ${filePath}:`, error.message); console.error(error.stack); return false; } } // Replace all instances of "Claude" with custom name in directory and filenames async function replaceNameInDirectory(dirPath, originalName, customName) { console.log(`Processing directory for name replacement: ${dirPath}`); if (!await fs.pathExists(dirPath)) { console.log(`Directory not found: ${dirPath}`); return; } try { // Get all entries first console.log(`Reading directory entries from: ${dirPath}`); const entries = await fs.readdir(dirPath, { withFileTypes: true }); console.log(`Found ${entries.length} entries in directory`); const directories = []; // Process files first console.log(`Processing ${entries.length} files in ${dirPath}`); for (const entry of entries) { if (entry.isFile()) { const entryPath = path.join(dirPath, entry.name); console.log(`Processing file: ${entryPath}`); await replaceNameInFile(entryPath, originalName, customName); } else if (entry.isDirectory()) { console.log(`Found subdirectory: ${entry.name}`); directories.push(entry); } } // Process subdirectories recursively console.log(`Processing ${directories.length} subdirectories in ${dirPath}`); for (const dir of directories) { const entryPath = path.join(dirPath, dir.name); console.log(`Recursively processing subdirectory: ${entryPath}`); await replaceNameInDirectory(entryPath, originalName, customName); } // Now rename directories and files if needed (after content has been processed) // First rename files console.log(`Checking ${entries.length} files for name-based renaming`); for (const entry of entries) { if (entry.isFile() && entry.name.toLowerCase().includes(originalName.toLowerCase())) { const entryPath = path.join(dirPath, entry.name); console.log(`File ${entryPath} contains "${originalName}" and will be renamed`); // Create case-preserving replacement for filename let newName = entry.name.replace(new RegExp(originalName, 'gi'), (match) => { console.log(` - Matched "${match}" in filename ${entry.name}`); let replacement; if (match === match.toUpperCase()) replacement = customName.toUpperCase(); else if (match === match.toLowerCase()) replacement = customName.toLowerCase(); else if (match.includes("'s")) replacement = customName + "'s"; else if (match.includes("'S")) replacement = customName.toUpperCase() + "'S"; else replacement = customName.charAt(0).toUpperCase() + customName.slice(1); console.log(` - Replacing with "${replacement}"`); return replacement; }); console.log(`New filename will be: ${newName}`); const newPath = path.join(dirPath, newName); // Ensure we don't try to rename to the same name if (entry.name !== newName && await fs.pathExists(entryPath)) { try { console.log(`Renaming file from ${entryPath} to ${newPath}`); await fs.rename(entryPath, newPath); console.log(`File successfully renamed to: ${newPath}`); } catch (renameError) { console.error(`Error renaming file ${entryPath} to ${newPath}:`, renameError.message); console.error(renameError.stack); } } else { console.log(`File ${entryPath} doesn't need renaming or doesn't exist`); } } } // Then rename directories (from deepest to shallowest to avoid path issues) console.log(`Checking ${directories.length} directories for name-based renaming (in reverse order)`); for (const dir of directories.reverse()) { if (dir.name.toLowerCase().includes(originalName.toLowerCase())) { const entryPath = path.join(dirPath, dir.name); console.log(`Directory ${entryPath} contains "${originalName}" and will be renamed`); // Create case-preserving replacement let newName = dir.name.replace(new RegExp(originalName, 'gi'), (match) => { console.log(` - Matched "${match}" in directory name ${dir.name}`); let replacement; if (match === match.toUpperCase()) replacement = customName.toUpperCase(); else if (match === match.toLowerCase()) replacement = customName.toLowerCase(); else if (match.includes("'s")) replacement = customName + "'s"; else if (match.includes("'S")) replacement = customName.toUpperCase() + "'S"; else replacement = customName.charAt(0).toUpperCase() + customName.slice(1); console.log(` - Replacing with "${replacement}"`); return replacement; }); console.log(`New directory name will be: ${newName}`); const newPath = path.join(dirPath, newName); // Ensure we don't try to rename to the same name if (dir.name !== newName && await fs.pathExists(entryPath)) { try { console.log(`Renaming directory from ${entryPath} to ${newPath}`); await fs.rename(entryPath, newPath); console.log(`Directory successfully renamed to: ${newPath}`); } catch (renameError) { console.error(`Error renaming directory ${entryPath} to ${newPath}:`, renameError.message); console.error(renameError.stack); } } else { console.log(`Directory ${entryPath} doesn't need renaming or doesn't exist`); } } } console.log(`Completed processing directory: ${dirPath}`); } catch (error) { console.error(`Error replacing name in directory ${dirPath}:`, error.message); console.error(error.stack); } } // Project types with improved descriptions const projectTypes = [ { name: '๐Ÿ–ฅ๏ธ Frontend', description: 'Web UI applications and sites', value: 'frontend' }, { name: 'โš™๏ธ Backend', description: 'APIs, services and servers', value: 'backend' }, { name: '๐Ÿ”„ Full Stack', description: 'Combined frontend and backend', value: 'fullstack' }, { name: '๐Ÿ“ฑ Mobile', description: 'iOS/Android applications', value: 'mobile' }, { name: '๐Ÿ“Š Data Science', description: 'Data analysis and ML projects', value: 'data' } ]; // Frontend frameworks const frontendFrameworks = [ { name: 'โš›๏ธ React', description: 'Popular UI library by Facebook', value: 'react' }, { name: '๐ŸŸข Vue', description: 'Progressive JavaScript framework', value: 'vue' }, { name: '๐Ÿ”ด Angular', description: 'Platform for building web applications', value: 'angular' }, { name: '๐ŸŸ  Svelte', description: 'Compiler-based UI framework', value: 'svelte' } ]; // Backend frameworks const backendFrameworks = [ { name: '๐Ÿš‚ Express', description: 'Fast, unopinionated Node.js framework', value: 'express' }, { name: '๐Ÿฑ NestJS', description: 'Progressive Node.js framework', value: 'nestjs' }, { name: '๐Ÿฆ„ Django', description: 'High-level Python web framework', value: 'django' }, { name: '๐Ÿงช Flask', description: 'Lightweight Python web framework', value: 'flask' }, { name: 'โšก FastAPI', description: 'Modern, fast Python web framework', value: 'fastapi' }, { name: '๐Ÿ’Ž Ruby on Rails', description: 'Ruby web application framework', value: 'rails' } ]; // Mobile frameworks const mobileFrameworks = [ { name: '๐Ÿ“ฑ React Native', description: 'Cross-platform mobile framework', value: 'react-native' }, { name: '๐Ÿฆ Flutter', description: 'UI toolkit for cross-platform apps', value: 'flutter' } ]; // Data science frameworks const dataFrameworks = [ { name: '๐Ÿ Data Science', description: 'Python data science stack', value: 'data-science' }, { name: '๐Ÿง  Machine Learning', description: 'TensorFlow, PyTorch, etc.', value: 'machine-learning' } ]; // Enhanced help display const customHelp = () => { displayLogo(); console.log(chalk.bold.cyan('\n๐Ÿ“˜ COMMANDS:\n')); console.log(chalk.greenBright(' โ–ถ๏ธ init') + chalk.white(' Initialize office structure')); console.log(chalk.greenBright(' โ–ถ๏ธ add-roles') + chalk.white(' Add roles to existing installation')); console.log(chalk.greenBright(' โ–ถ๏ธ update') + chalk.white(' Update existing installation')); console.log(chalk.greenBright(' โ–ถ๏ธ help') + chalk.white(' Display this help information')); console.log(chalk.bold.cyan('\n๐Ÿ“˜ OPTIONS:\n')); console.log(chalk.yellow(' --force') + chalk.white(' Force initialization even if directory exists')); console.log(chalk.yellow(' --all-roles') + chalk.white(' Install all available roles')); console.log(chalk.yellow(' --roles-only') + chalk.white(' Only install roles (minimal installation)')); console.log(chalk.yellow(' --no-interactive') + chalk.white(' Non-interactive mode for CI environments')); console.log(chalk.yellow(' --debug') + chalk.white(' Enable debug mode for troubleshooting')); console.log(chalk.bold.cyan('\n๐Ÿ“˜ EXAMPLES:\n')); console.log(chalk.gray(' claudes-office init # Interactive wizard')); console.log(chalk.gray(' claudes-office init --all-roles # All roles installation')); console.log(chalk.gray(' claudes-office add-roles --all # Add all available roles')); console.log(chalk.bold.magenta('\n๐Ÿ“• For more information, visit:')); console.log(chalk.underline.blue(' https://github.com/yourusername/claudes-office\n')); }; // Configure the CLI program program .version(packageJson.version, '-v, --version') .description('Initialize your AI assistant\'s office in your project') .helpOption('-h, --help', 'Display help information') .option('--debug', 'Enable debug mode for detailed logging') .addHelpCommand(false); // Initialize command program .command('init') .description('Create the office structure in the current directory') .option('--force', 'Force initialization even if directory already exists') .option('--all-roles', 'Install all available roles (skips selection wizard)') .option('--roles-only', 'Only install roles (minimal installation)') .option('--no-interactive', 'Non-interactive mode for CI environments') .option('--debug', 'Enable debug mode for detailed logging') .action(async (options) => { try { // Set default for interactive mode options.interactive = options.interactive !== false; options.debug = !!options.debug; const currentDir = process.cwd(); const templateDir = path.join(__dirname, '..', 'template'); // Debug utility function const debug = (message) => { if (options.debug) { console.log(chalk.gray(`[DEBUG] ${message}`)); } }; debug('Starting initialization with options: ' + JSON.stringify(options)); // Display the logo and welcome message displayLogo(); // Intro spinner const introSpinner = ora({ text: chalk.cyan('Initializing AI assistant\'s office in your project...'), color: 'cyan', spinner: 'dots' }).start(); // Reduce delay to avoid hanging appearance await new Promise(resolve => setTimeout(resolve, 500)); introSpinner.succeed(chalk.green('Welcome to the AI Assistant\'s Office Setup!')); // Check if office already exists const officeDirName = 'claudes-office'; const officeExists = await fs.pathExists(path.join(currentDir, officeDirName)); if (officeExists && !options.force) { console.log(chalk.yellow.bold('\nโš ๏ธ Warning: Office directory already exists in this location.')); console.log(chalk.yellow('Use --force to overwrite or run `claudes-office update` to update existing files.')); if (options.interactive) { const { proceed } = await inquirer.prompt([ { type: 'confirm', name: 'proceed', message: 'Do you want to continue anyway and overwrite existing files?', default: false } ]); if (!proceed) { console.log(chalk.red.bold('\nโŒ Initialization cancelled.')); return; } } else { console.log(chalk.red.bold('\nโŒ Initialization cancelled.')); process.exit(1); // Ensure process terminates in non-interactive mode return; } } // Check if package.json exists (Node.js project) const isNodeProject = await fs.pathExists(path.join(currentDir, 'package.json')); if (!isNodeProject) { console.log(chalk.yellow.bold('\nโš ๏ธ Warning: No package.json found. This doesn\'t appear to be a Node.js project.')); console.log(chalk.yellow('You can still proceed, but please make sure you\'re in the correct directory.')); if (options.interactive) { const { proceed } = await inquirer.prompt([ { type: 'confirm', name: 'proceed', message: 'Do you want to continue anyway?', default: true } ]); if (!proceed) { console.log(chalk.red.bold('\nโŒ Initialization cancelled.')); return; } } else if (!options.force) { // In non-interactive mode, exit unless force is specified console.log(chalk.red.bold('\nโŒ Initialization cancelled. Use --force to proceed anyway.')); process.exit(1); return; } } // Ask for custom name let customName = null; if (options.interactive) { customName = await promptForCustomName(); } // Handle role selection let selectedFrameworks = []; let projectType = null; if (options.allRoles) { console.log(chalk.magenta.bold('\n๐ŸŽ Installing all available roles...')); // Add all frameworks to the selected list selectedFrameworks = [ ...frontendFrameworks, ...backendFrameworks, ...mobileFrameworks, ...dataFrameworks ]; projectType = { name: 'All', value: 'all' }; } else if (options.interactive) { // Welcome message for the wizard console.log(chalk.greenBright.bold('\n๐Ÿง™ Welcome to the Office Setup Wizard!')); console.log(chalk.white('This wizard will help you select the ideal roles for your AI assistant.')); console.log(chalk.gray('Tip: Use --all-roles flag to install all available roles\n')); // Ask for project types (multiple selection) const projectResponse = await inquirer.prompt([ { type: 'checkbox', name: 'types', message: 'What type of project are you working on? (select all that apply)', choices: projectTypes, pageSize: 10, validate: (answer) => { if (answer.length < 1) { return 'Please select at least one project type'; } return true; } } ]); const selectedProjectTypes = projectResponse.types.map(type => projectTypes.find(t => t.value === type) ); // Set the main project type for display purposes projectType = selectedProjectTypes.length > 0 ? selectedProjectTypes[0] : null; // For each selected project type, ask for frameworks for (const projType of selectedProjectTypes) { if (projType.value === 'frontend' || projType.value === 'fullstack') { const { frameworks } = await inquirer.prompt([ { type: 'checkbox', name: 'frameworks', message: 'Which frontend frameworks are you using? (select all that apply)', choices: [ ...frontendFrameworks, { name: 'โฉ Skip this selection', value: null } ], pageSize: 10 } ]); if (frameworks && frameworks.length > 0) { for (const framework of frameworks) { if (framework !== null) { selectedFrameworks.push(frontendFrameworks.find(f => f.value === framework)); } } } } if (projType.value === 'backend' || projType.value === 'fullstack') { const { frameworks } = await inquirer.prompt([ { type: 'checkbox', name: 'frameworks', message: 'Which backend frameworks are you using? (select all that apply)', choices: [ ...backendFrameworks, { name: 'โฉ Skip this selection', value: null } ], pageSize: 10 } ]); if (frameworks && frameworks.length > 0) { for (const framework of frameworks) { if (framework !== null) { selectedFrameworks.push(backendFrameworks.find(f => f.value === framework)); } } } } if (projType.value === 'mobile') { const { frameworks } = await inquirer.prompt([ { type: 'checkbox', name: 'frameworks', message: 'Which mobile frameworks are you using? (select all that apply)', choices: [ ...mobileFrameworks, { name: 'โฉ Skip this selection', value: null } ], pageSize: 10 } ]); if (frameworks && frameworks.length > 0) { for (const framework of frameworks) { if (framework !== null) { selectedFrameworks.push(mobileFrameworks.find(f => f.value === framework)); } } } } if (projType.value === 'data') { const { frameworks } = await inquirer.prompt([ { type: 'checkbox', name: 'frameworks', message: 'Which data science frameworks are you using? (select all that apply)', choices: [ ...dataFrameworks, { name: 'โฉ Skip this selection', value: null } ], pageSize: 10 } ]); if (frameworks && frameworks.length > 0) { for (const framework of frameworks) { if (framework !== null) { selectedFrameworks.push(dataFrameworks.find(f => f.value === framework)); } } } } } // Additional technologies (optional) // Create a list of all frameworks that weren't already selected const additionalOptions = [ ...frontendFrameworks, ...backendFrameworks, ...mobileFrameworks, ...dataFrameworks ].filter(framework => !selectedFrameworks.some(selected => selected.value === framework.value) ); if (additionalOptions.length > 0) { const { additionalFrameworks } = await inquirer.prompt([ { type: 'checkbox', name: 'additionalFrameworks', message: 'Select any additional technologies (optional):', choices: additionalOptions, pageSize: 15 } ]); if (additionalFrameworks && additionalFrameworks.length > 0) { for (const value of additionalFrameworks) { const framework = additionalOptions.find(opt => opt.value === value); if (framework) { selectedFrameworks.push(framework); } } } } } // Display summary of selections console.log(chalk.magenta.bold('\n๐Ÿ“‹ Role Selection Summary:')); console.log(chalk.white(`Project Type: ${projectType ? projectType.name.replace(/^[^ ]+ /, '') : 'Generic'}`)); if (selectedFrameworks.length > 0) { console.log(chalk.white('Technology-Specific Roles:')); selectedFrameworks.forEach(framework => { console.log(` ${chalk.cyan('โ€ข')} ${chalk.green(framework.name.replace(/^[^ ]+ /, ''))} ${chalk.yellow('โœจ')}`); }); } else { console.log(chalk.gray('No specific technology roles selected. Including generic roles only.')); } // Installation spinner const installSpinner = ora({ text: chalk.cyan('Installing office structure...'), color: 'cyan', spinner: 'dots' }).start(); // Remove the debug line as it's causing confusion with the spinner // process.stdout.write("- Initializing AI assistant's office in your project...\n"); try { // Add more debug output to trace the execution flow debug('Starting installation process'); // Create output directories const officeDir = path.join(currentDir, officeDirName); const rolesDir = path.join(officeDir, 'roles'); debug(`Creating directories: ${officeDir} and ${rolesDir}`); // Base directory name for template const baseNameTemplate = 'claudes-office'; const baseNameOutput = officeDirName; // Copy necessary files based on the installation type if (!options.rolesOnly) { debug('Copying root files and base office structure'); // Copy CLAUDE.md and Initialize_Project.md to the root debug('Copying CLAUDE.md and Initialize_Project.md to root'); await fs.copy(path.join(templateDir, 'CLAUDE.md'), path.join(currentDir, 'CLAUDE.md')); await fs.copy(path.join(templateDir, 'Initialize_Project.md'), path.join(currentDir, 'Initialize_Project.md')); // Copy base office structure (excluding roles directory and handling references/docs specially) debug('Reading template directory entries'); const entries = await fs.readdir(path.join(templateDir, baseNameTemplate), { withFileTypes: true }); debug(`Found ${entries.length} entries in template directory`); for (const entry of entries) { const sourcePath = path.join(templateDir, baseNameTemplate, entry.name); const destPath = path.join(officeDir, entry.name); // Skip roles directory only - we'll copy references normally if (entry.name !== 'roles') { debug(`Copying ${entry.name} from template to office directory`); await fs.copy(sourcePath, destPath); } } } // Create roles directory debug('Creating roles directory'); await fs.mkdirp(rolesDir); // Copy README.md for roles debug('Copying roles README.md'); await fs.copy( path.join(templateDir, baseNameTemplate, 'roles', 'README.md'), path.join(rolesDir, 'README.md') ); // Copy role_template.md debug('Copying role_template.md'); await fs.copy( path.join(templateDir, baseNameTemplate, 'roles', 'generic', 'role_template.md'), path.join(rolesDir, 'role_template.md') ); // Copy generic roles directly to rolesDir (not in a generic subdirectory) const genericSourceDir = path.join(templateDir, baseNameTemplate, 'roles', 'generic'); const genericEntries = await fs.readdir(genericSourceDir, { withFileTypes: true }); for (const entry of genericEntries) { const sourcePath = path.join(genericSourceDir, entry.name); const destPath = path.join(rolesDir, entry.name); if (entry.isFile() && entry.name.endsWith('.md')) { await fs.copy(sourcePath, destPath); } } // Create a map to track successful and missing roles const roleStatus = { installed: [], missing: [] }; // Copy selected project-specific roles if (selectedFrameworks.length > 0) { // Create directory structure for selected domains first const domainDirs = new Set(); for (const framework of selectedFrameworks) { let domain = ''; if (frontendFrameworks.some(f => f.value === framework.value)) { domain = 'frontend'; } else if (backendFrameworks.some(f => f.value === framework.value)) { domain = 'backend'; } else if (mobileFrameworks.some(f => f.value === framework.value)) { domain = 'mobile'; } else if (dataFrameworks.some(f => f.value === framework.value)) { domain = 'data'; } if (domain) { domainDirs.add(domain); } } // Create all needed domain directories directly in rolesDir for (const domain of domainDirs) { await fs.mkdirp(path.join(rolesDir, domain)); } // Now copy the selected frameworks for (const framework of selectedFrameworks) { let sourcePath; let destPath; let domain = ''; // Determine the source and destination paths based on framework type if (frontendFrameworks.some(f => f.value === framework.value)) { domain = 'frontend'; } else if (backendFrameworks.some(f => f.value === framework.value)) { domain = 'backend'; } else if (mobileFrameworks.some(f => f.value === framework.value)) { domain = 'mobile'; } else if (dataFrameworks.some(f => f.value === framework.value)) { domain = 'data'; } if (domain) { // Source path still includes project-specific in template sourcePath = path.join(templateDir, baseNameTemplate, 'roles', 'project-specific', domain, framework.value); // Destination path now directly under domain in rolesDir destPath = path.join(rolesDir, domain, framework.value); // Check if source directory exists and has at least one .md file if (sourcePath && await fs.pathExists(sourcePath)) { // Check if the directory has at least one .md file const entries = await fs.readdir(sourcePath, { withFileTypes: true }); const hasMdFile = entries.some(entry => entry.isFile() && entry.name.endsWith('.md') ); if (hasMdFile) { await fs.copy(sourcePath, destPath); roleStatus.installed.push(framework.name.replace(/^[^ ]+ /, '')); } else { // Create the directory anyway for future content await fs.mkdirp(destPath); roleStatus.missing.push(framework.name.replace(/^[^ ]+ /, '')); } } else { roleStatus.missing.push(framework.name.replace(/^[^ ]+ /, '')); } } } } // Add DevOps roles if appropriate const devopsDir = path.join(templateDir, baseNameTemplate, 'roles', 'project-specific', 'devops'); let includeDevops = false; if (await fs.pathExists(devopsDir) && options.interactive) { const { addDevops } = await inquirer.prompt([ { type: 'confirm', name: 'addDevops', message: 'Would you like to include DevOps roles?', default: true } ]); includeDevops = addDevops; } else if (options.allRoles) { includeDevops = true; } if (includeDevops) { // Place DevOps directly in the roles directory, not in project-specific const destDevopsDir = path.join(rolesDir, 'devops'); await fs.mkdirp(destDevopsDir); if (await fs.pathExists(devopsDir)) { await fs.copy(devopsDir, destDevopsDir); roleStatus.installed.push('DevOps'); } } // Copy combo roles directory if it exists const comboRolesDir = path.join(templateDir, baseNameTemplate, 'roles', 'combo-roles'); if (await fs.pathExists(comboRolesDir)) { const destComboRolesDir = path.join(rolesDir, 'combo-roles'); await fs.copy(comboRolesDir, destComboRolesDir); } // Create missing role directories (even if empty) directly in roles dir for (const domain of ['frontend', 'backend', 'mobile', 'data']) { const domainDir = path.join(rolesDir, domain); await fs.mkdirp(domainDir); } // We don't need to handle docs directory separately anymore since we're copying the references // directory entirely. This code is left intentionally empty as a placeholder to show // where the previous logic was. The references directory (including docs) is now copied // as part of the normal directory copying process above. // Apply custom name if provided if (customName) { installSpinner.text = chalk.cyan(`Personalizing with name: ${customName}...`); debug(`Applying custom name '${customName}' to replace 'Claude'`); try { // Replace in root files debug('Replacing name in root files'); const rootFile1Result = await replaceNameInFile(path.join(currentDir, 'CLAUDE.md'), 'Claude', customName); debug(`CLAUDE.md replacement result: ${rootFile1Result}`); const rootFile2Result = await replaceNameInFile(path.join(currentDir, 'Initialize_Project.md'), 'Claude', customName); debug(`Initialize_Project.md replacement result: ${rootFile2Result}`); // Replace in all files in the office directory debug('Replacing name in office directory files and folders'); await replaceNameInDirectory(officeDir, 'Claude', customName); // Rename the main directory if it contains "claude" if (officeDirName.toLowerCase().includes('claude')) { debug(`Renaming main office directory from '${officeDirName}'`); // Create case-preserving replacement let newOfficeDirName = officeDirName.replace(/claude/gi, (match) => { debug(`Matched "${match}" in directory name`); if (match === match.toUpperCase()) return customName.toUpperCase(); if (match === match.toLowerCase()) return customName.toLowerCase(); return customName.charAt(0).toUpperCase() + customName.slice(1); }); debug(`New directory name will be: '${newOfficeDirName}'`); const newOfficeDir = path.join(currentDir, newOfficeDirName); try { debug(`Renaming from ${officeDir} to ${newOfficeDir}`); await fs.rename(officeDir, newOfficeDir); // Update variables for later use officeDir = newOfficeDir; officeDirName = newOfficeDirName; debug(`Main directory renamed to '${newOfficeDirName}' successfully`); } catch (renameError) { console.error(chalk.yellow(`Warning: Could not rename main directory: ${renameError.message}`)); debug(`Error renaming directory: ${renameError.stack}`); } } else { debug(`Directory name ${officeDirName} does not contain 'claude', no renaming needed`); } } catch (nameError) { console.error(chalk.yellow(`Warning: Error during name personalization: ${nameError.message}`)); debug(`Error in name personalization: ${nameError.stack}`); } } else { debug('No custom name provided, skipping name replacement'); } // Installation complete installSpinner.succeed(chalk.green('Installation completed successfully!')); // Close the try block from installation } catch (error) { installSpinner.fail(chalk.red('Installation failed')); console.error(chalk.red(`Error during installation: ${error.message}`)); // Continue to show any error in the main try-catch block throw error; } // Success message with improved formatting console.log(chalk.green.bold('\nโœจ Your AI Assistant\'s Office has been successfully installed! โœจ')); console.log(chalk.bold.cyan('\n๐Ÿ“ Files created:')); console.log(` ${chalk.yellow('โ€ข')} ${chalk.white('CLAUDE.md')} (main configuration)`); console.log(` ${chalk.yellow('โ€ข')} ${chalk.white('Initialize_Project.md')} (initialization instructions)`); console.log(` ${chalk.yellow('โ€ข')} ${chalk.white(officeDirName + '/')} (organizational directory)`); console.log(chalk.bold.cyan('\n๐Ÿ“‹ Installation summary:')); // Show project types (can be multiple now) if (options.allRoles) { console.log(` ${chalk.magenta('โ–ถ')} Project type: ${chalk.green('All')}`); } else if (typeof selectedProjectTypes !== 'undefined' && selectedProjectTypes && selectedProjectTypes.length > 0) { const projectTypeNames = selectedProjectTypes.map(pt => pt.name.replace(/^[^ ]+ /, '')).join(', '); console.log(` ${chalk.magenta('โ–ถ')} Project types: ${chalk.green(projectTypeNames)}`); } else { console.log(` ${chalk.magenta('โ–ถ')} Project type: ${projectType ? chalk.green(projectType.name.replace(/^[^ ]+ /, '')) : chalk.gray('Generic')}`); } // Show technologies if (selectedFrameworks.length > 0) { console.log(` ${chalk.magenta('โ–ถ')} Technologies: ${chalk.green(selectedFrameworks.map(f => f.name.replace(/^[^ ]+ /, '')).join(', '))}`); } else { console.log(` ${chalk.magenta('โ–ถ')} Technologies: ${chalk.gray('None selected')}`); } // Show role files installed if (typeof roleStatus !== 'undefined' && roleStatus) { console.log(` ${chalk.magenta('โ–ถ')} Role files: ${chalk.green(`Generic roles + ${roleStatus.installed.length} technology-specific roles`)}`); // Show missing roles if any if (roleStatus.missing.length > 0) { console.log(` ${chalk.yellow('โ–ถ')} Missing roles: ${chalk.yellow(`${roleStatus.missing.join(', ')} (directories created for future use)`)}`); } } else { console.log(` ${chalk.magenta('โ–ถ')} Role files: ${chalk.green(`Generic roles + technology-specific roles`)}`); } // Show custom name if applied if (customName) { console.log(` ${chalk.magenta('โ–ถ')} Custom name: ${chalk.green(`Changed "Claude" to "${customName}"`)}`); } console.log(chalk.bold.cyan('\n๐Ÿ“ Next steps:')); console.log(` ${chalk.white('1.')} Review ${chalk.yellow('CLAUDE.md')} to customize your AI experience`); console.log(` ${chalk.white('2.')} Have your assistant read ${chalk.yellow('Initialize_Project.md')} to set up the workspace`); console.log(` ${chalk.white('3.')} Use ${chalk.yellow(`claude read ${officeDirName}/roles/[specific-role].md`)} to activate specialized expertise`); console.log('\n'); } catch (error) { // Make sure spinner is stopped if there's an error if (typeof installSpinner !== 'undefined' && installSpinner) { installSpinner.fail(chalk.red('Installation failed')); } console.error(chalk.red.bold('\nโŒ Error initializing the office:'), error.message); if (options.debug) { console.error(chalk.gray('Stack trace:'), error.stack); } process.exit(1); } }); // Add-roles command (enhanced) program .command('add-roles') .description('Add new roles to an existing office installation') .option('--all', 'Add all available roles') .option('--no-interactive', 'Non-interactive mode for CI environments') .option('--debug', 'Enable debug mode for detailed logging') .action(async (options) => { // Set default for interactive mode options.interactive = options.interactive !== false; options.debug = !!options.debug; // Debug utility function const debug = (message) => { if (options.debug) { console.log(chalk.gray(`[DEBUG] ${message}`)); } }; try { const currentDir = process.cwd(); const officeDirName = 'claudes-office'; const officeDir = path.join(currentDir, officeDirName); const rolesDir = path.join(officeDir, 'roles'); // Display the logo displayLogo(); // Check if office exists if (!await fs.pathExists(officeDir)) { console.log(chalk.red.bold('\nโŒ Error: Office not found in this directory.')); console.log(chalk.yellow('Please run `claudes-office init` first to create the office.')); return; } // Start spinner const spinner = ora({ text: chalk.cyan('Preparing to add new roles...'), color: 'cyan', spinner: 'dots' }).start(); // Path to the template directory const templateDir = path.join(__dirname, '..', 'template'); const baseNameTemplate = 'claudes-office'; // Check available roles in template const rolesTemplate = path.join(templateDir, baseNameTemplate, 'roles', 'project-specific'); // Initialize variables let selectedFrameworks = []; // Handle --all option if (options.all) { spinner.text = chalk.cyan('Installing all available roles...'); // Get available domains const domains = ['frontend', 'backend', 'mobile', 'data', 'devops']; // Track what we've added const added = []; // Process each domain for (const domain of domains) { const domainPath = path.join(rolesTemplate, domain); if (await fs.pathExists(domainPath)) { // Create domain directory if needed, directly in rolesDir const destDomainDir = path.join(rolesDir, domain); await fs.mkdirp(destDomainDir); // Get all framework directories in domain const entries = await fs.readdir(domainPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory() && entry.name !== '.DS_Store') { const frameworkPath = path.join(domainPath, entry.name); const destFrameworkPath = path.join(destDomainDir, entry.name); // Check if there are .md files (actual role content) const files = await fs.readdir(frameworkPath, { withFileTypes: true }); const hasMdFiles = files.some(file => file.isFile() && file.name.endsWith('.md')); if (hasMdFiles) { await fs.copy(frameworkPath, destFrameworkPath, { overwrite: false, errorOnExist: false }); added.push(`${domain}/${entry.name}`); } else { // Create the directory anyway await fs.mkdirp(destFrameworkPath); } } } } } // Add combo roles if they exist const comboRolesDir = path.join(templateDir, baseNameTemplate, 'roles', 'combo-roles'); if (await fs.pathExists(comboRolesDir)) { const destComboRolesDir = path.join(rolesDir, 'combo-roles'); await fs.mkdirp(destComboRolesDir); await fs.copy(comboRolesDir, destComboRolesDir, { overwrite: false, errorOnExist: false }); added.push('combo-roles'); } spinner.succeed(chalk.green('All available roles have been added!')); console.log(chalk.green.bold(`\nโœ… Added ${added.length} role directories to your installation!\n`)); return; } // Interactive role selection spinner.stop(); console.log(chalk.magenta.bold('\n๐Ÿง™ Role Selection Wizard')); console.log(chalk.white('Choose roles to add to your existing installation')); console.log(chalk.gray('Tip: Use --all flag to add all available roles\n')); // Get available domains and frameworks from template const availableDomains = []; // Frontend const frontendDir = path.join(rolesTemplate, 'frontend'); const availableFrontend = await getAvailableRolesInDirectory(frontendDir); if (availableFrontend.length > 0) { availableDomains.push({ name: '๐Ÿ–ฅ๏ธ Frontend Frameworks', value: 'frontend' }); } // Backend const backendDir = path.join(rolesTemplate, 'backend'); const availableBackend = await getAvailableRolesInDirectory(backendDir); if (availableBackend.length > 0) { availableDomains.push({ name: 'โš™๏ธ Backend Frameworks', value: 'backend' }); } // Mobile const mobileDir = path.join(rolesTemplate, 'mobile'); const availableMobile = await getAvailableRolesInDirectory(mobileDir); if (availableMobile.length > 0) { availableDomains.push({ name: '๐Ÿ“ฑ Mobile Frameworks', value: 'mobile' }); } // Data const dataDir = path.join(rolesTemplate, 'data'); const availableData = await getAvailableRolesInDirectory(dataDir); if (availableData.length > 0) { availableDomains.push({ name: '๐Ÿ“Š Data Science / ML', value: 'data' }); } // DevOps const devopsDir = path.join(rolesTemplate, 'devops'); const hasDevOps = await fs.pathExists(devopsDir); if (availableDomains.length === 0 && !hasDevOps) { console.log(chalk.yellow.bold('\nโš ๏ธ No role templates found in the package.')); console.log(chalk.yellow('You can create your own role files manually.')); return; } // Ask for domains const { selectedDomains } = await inquirer.prompt([ { type: 'checkbox', name: 'selectedDomains', message: 'Select technology domains to add:', choices: availableDomains, pageSize: 10, validate: (answer) => { if (answer.length < 1 && !hasDevOps) { return 'You must choose at least one domain or DevOps roles.'; } return true; } } ]); // Framework selection for each domain const roleSelections = []; for (const domain of selectedDomains) { let availableFrameworks = []; if (domain === 'frontend') { availableFrameworks = availableFrontend; } else if (domain === 'backend') { availableFrameworks = availableBackend; } else if (domain === 'mobile') { availableFrameworks = availableMobile; } else if (domain === 'data') { availableFrameworks = availableData; } if (availableFrameworks.length > 0) { const { frameworks } = await inquirer.prompt([ { type: 'checkbox', name: 'frameworks', message: `Select ${domain} frameworks to add:`, choices: availableFrameworks, pageSize: 15 } ]); for (const framework of frameworks) { roleSelections.push({ domain, framework }); } } } // Ask for DevOps roles let includeDevops = false; if (hasDevOps) { const { addDevops } = await inquirer.prompt([ { type: 'confirm', name: 'addDevops', message: 'Would you like to include DevOps roles?', default: true } ]); includeDevops = addDevops; } // Start installation spinner.text = chalk.cyan('Adding selected roles...'); spinner.start(); // We