UNPKG

claudes-office

Version:

CLI tool to initialize Claude's office in your project

964 lines โ€ข 65.5 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const commander_1 = require("commander"); const chalk_1 = __importDefault(require("chalk")); const inquirer_1 = __importDefault(require("inquirer")); const figlet_1 = __importDefault(require("figlet")); const ora_1 = __importDefault(require("ora")); // Import types const commands_1 = require("./types/commands"); // Get the package version // eslint-disable-next-line @typescript-eslint/no-var-requires 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_1.default.bold.cyan(figlet_1.default.textSync('CLAUDE\'S', { font: 'ANSI Shadow', horizontalLayout: 'default', verticalLayout: 'default', }))); console.log(chalk_1.default.bold.magenta(figlet_1.default.textSync('OFFICE', { font: 'ANSI Shadow', horizontalLayout: 'default', verticalLayout: 'default', }))); console.log('\n'); console.log(chalk_1.default.bold.yellowBright(' โœจ Your AI Assistant\'s Workspace โœจ')); console.log(chalk_1.default.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_1.default.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_1.default.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) { const err = error; console.error(`Error replacing name in ${filePath}:`, err.message); console.error(err.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) { const err = renameError; console.error(`Error renaming file ${entryPath} to ${newPath}:`, err.message); console.error(err.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) { const err = renameError; console.error(`Error renaming directory ${entryPath} to ${newPath}:`, err.message); console.error(err.stack); } } else { console.log(`Directory ${entryPath} doesn't need renaming or doesn't exist`); } } } console.log(`Completed processing directory: ${dirPath}`); } catch (error) { const err = error; console.error(`Error replacing name in directory ${dirPath}:`, err.message); console.error(err.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_1.default.bold.cyan('\n๐Ÿ“˜ COMMANDS:\n')); console.log(chalk_1.default.greenBright(' โ–ถ๏ธ init') + chalk_1.default.white(' Initialize office structure')); console.log(chalk_1.default.greenBright(' โ–ถ๏ธ add-roles') + chalk_1.default.white(' Add roles to existing installation')); console.log(chalk_1.default.greenBright(' โ–ถ๏ธ update') + chalk_1.default.white(' Update existing installation')); console.log(chalk_1.default.greenBright(' โ–ถ๏ธ help') + chalk_1.default.white(' Display this help information')); console.log(chalk_1.default.bold.cyan('\n๐Ÿ“˜ OPTIONS:\n')); console.log(chalk_1.default.yellow(' --force') + chalk_1.default.white(' Force initialization even if directory exists')); console.log(chalk_1.default.yellow(' --all-roles') + chalk_1.default.white(' Install all available roles')); console.log(chalk_1.default.yellow(' --roles-only') + chalk_1.default.white(' Only install roles (minimal installation)')); console.log(chalk_1.default.yellow(' --no-interactive') + chalk_1.default.white(' Non-interactive mode for CI environments')); console.log(chalk_1.default.yellow(' --debug') + chalk_1.default.white(' Enable debug mode for troubleshooting')); console.log(chalk_1.default.bold.cyan('\n๐Ÿ“˜ EXAMPLES:\n')); console.log(chalk_1.default.gray(' claudes-office init # Interactive wizard')); console.log(chalk_1.default.gray(' claudes-office init --all-roles # All roles installation')); console.log(chalk_1.default.gray(' claudes-office add-roles --all # Add all available roles')); console.log(chalk_1.default.bold.magenta('\n๐Ÿ“• For more information, visit:')); console.log(chalk_1.default.underline.blue(' https://github.com/yourusername/claudes-office\n')); }; // Configure the CLI program const program = new commander_1.Command(); 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_1.default.gray(`[DEBUG] ${message}`)); } }; debug('Starting initialization with options: ' + JSON.stringify(options)); // Display the logo and welcome message displayLogo(); // Intro spinner const introSpinner = (0, ora_1.default)({ text: chalk_1.default.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_1.default.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_1.default.yellow.bold('\nโš ๏ธ Warning: Office directory already exists in this location.')); console.log(chalk_1.default.yellow('Use --force to overwrite or run `claudes-office update` to update existing files.')); if (options.interactive) { const { proceed } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'proceed', message: 'Do you want to continue anyway and overwrite existing files?', default: false } ]); if (!proceed) { console.log(chalk_1.default.red.bold('\nโŒ Initialization cancelled.')); return; } } else { console.log(chalk_1.default.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_1.default.yellow.bold('\nโš ๏ธ Warning: No package.json found. This doesn\'t appear to be a Node.js project.')); console.log(chalk_1.default.yellow('You can still proceed, but please make sure you\'re in the correct directory.')); if (options.interactive) { const { proceed } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'proceed', message: 'Do you want to continue anyway?', default: true } ]); if (!proceed) { console.log(chalk_1.default.red.bold('\nโŒ Initialization cancelled.')); return; } } else if (!options.force) { // In non-interactive mode, exit unless force is specified console.log(chalk_1.default.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; let selectedProjectTypes; if (options.allRoles) { console.log(chalk_1.default.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_1.default.greenBright.bold('\n๐Ÿง™ Welcome to the Office Setup Wizard!')); console.log(chalk_1.default.white('This wizard will help you select the ideal roles for your AI assistant.')); console.log(chalk_1.default.gray('Tip: Use --all-roles flag to install all available roles\n')); // Ask for project types (multiple selection) const projectResponse = await inquirer_1.default.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; } } ]); 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 === commands_1.ProjectType.FRONTEND || projType.value === commands_1.ProjectType.FULLSTACK) { const { frameworks } = await inquirer_1.default.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) { const foundFramework = frontendFrameworks.find(f => f.value === framework); if (foundFramework) { selectedFrameworks.push(foundFramework); } } } } } if (projType.value === commands_1.ProjectType.BACKEND || projType.value === commands_1.ProjectType.FULLSTACK) { const { frameworks } = await inquirer_1.default.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) { const foundFramework = backendFrameworks.find(f => f.value === framework); if (foundFramework) { selectedFrameworks.push(foundFramework); } } } } } if (projType.value === commands_1.ProjectType.MOBILE) { const { frameworks } = await inquirer_1.default.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) { const foundFramework = mobileFrameworks.find(f => f.value === framework); if (foundFramework) { selectedFrameworks.push(foundFramework); } } } } } if (projType.value === commands_1.ProjectType.DATA) { const { frameworks } = await inquirer_1.default.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) { const foundFramework = dataFrameworks.find(f => f.value === framework); if (foundFramework) { selectedFrameworks.push(foundFramework); } } } } } } // 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_1.default.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_1.default.magenta.bold('\n๐Ÿ“‹ Role Selection Summary:')); console.log(chalk_1.default.white(`Project Type: ${projectType ? projectType.name.replace(/^[^ ]+ /, '') : 'Generic'}`)); if (selectedFrameworks.length > 0) { console.log(chalk_1.default.white('Technology-Specific Roles:')); selectedFrameworks.forEach(framework => { console.log(` ${chalk_1.default.cyan('โ€ข')} ${chalk_1.default.green(framework.name.replace(/^[^ ]+ /, ''))} ${chalk_1.default.yellow('โœจ')}`); }); } else { console.log(chalk_1.default.gray('No specific technology roles selected. Including generic roles only.')); } // Installation spinner let installSpinner = (0, ora_1.default)({ text: chalk_1.default.cyan('Installing office structure...'), color: 'cyan', spinner: 'dots' }).start(); // Debug - trace the execution flow debug('Starting installation process'); // Create output directories let 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; let roleStatus = { installed: [], missing: [] }; try { // 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); } } // 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) { // Pause the spinner while asking about DevOps roles installSpinner.stop(); const { addDevops } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'addDevops', message: 'Would you like to include DevOps roles?', default: true } ]); // Restart the spinner after getting the answer installSpinner.start(); 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); } // Apply custom name if provided if (customName) { installSpinner.text = chalk_1.default.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); let newOfficeDir = officeDir; // 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}'`); newOfficeDir = path.join(currentDir, newOfficeDirName); try { debug(`Renaming from ${officeDir} to ${newOfficeDir}`); await fs.rename(officeDir, newOfficeDir); // Update variables for later use debug(`Main directory renamed to '${newOfficeDirName}' successfully`); } catch (renameError) { const err = renameError; console.error(chalk_1.default.yellow(`Warning: Could not rename main directory: ${err.message}`)); debug(`Error renaming directory: ${err.stack}`); } } else { debug(`Directory name ${officeDirName} does not contain 'claude', no renaming needed`); } } catch (nameError) { const err = nameError; console.error(chalk_1.default.yellow(`Warning: Error during name personalization: ${err.message}`)); debug(`Error in name personalization: ${err.stack}`); } } else { debug('No custom name provided, skipping name replacement'); } // Installation complete installSpinner.succeed(chalk_1.default.green('Installation completed successfully!')); } catch (error) { installSpinner.fail(chalk_1.default.red('Installation failed')); console.error(chalk_1.default.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_1.default.green.bold('\nโœจ Your AI Assistant\'s Office has been successfully installed! โœจ')); console.log(chalk_1.default.bold.cyan('\n๐Ÿ“ Files created:')); console.log(` ${chalk_1.default.yellow('โ€ข')} ${chalk_1.default.white('CLAUDE.md')} (main configuration)`); console.log(` ${chalk_1.default.yellow('โ€ข')} ${chalk_1.default.white('Initialize_Project.md')} (initialization instructions)`); console.log(` ${chalk_1.default.yellow('โ€ข')} ${chalk_1.default.white(officeDirName + '/')} (organizational directory)`); console.log(chalk_1.default.bold.cyan('\n๐Ÿ“‹ Installation summary:')); // Show project types (can be multiple now) if (options.allRoles) { console.log(` ${chalk_1.default.magenta('โ–ถ')} Project type: ${chalk_1.default.green('All')}`); } else if (typeof selectedProjectTypes !== 'undefined' && selectedProjectTypes && selectedProjectTypes.length > 0) { const projectTypeNames = selectedProjectTypes.map(pt => pt.name.replace(/^[^ ]+ /, '')).join(', '); console.log(` ${chalk_1.default.magenta('โ–ถ')} Project types: ${chalk_1.default.green(projectTypeNames)}`); } else { console.log(` ${chalk_1.default.magenta('โ–ถ')} Project type: ${projectType ? chalk_1.default.green(projectType.name.replace(/^[^ ]+ /, '')) : chalk_1.default.gray('Generic')}`); } // Show technologies if (selectedFrameworks.length > 0) { console.log(` ${chalk_1.default.magenta('โ–ถ')} Technologies: ${chalk_1.default.green(selectedFrameworks.map(f => f.name.replace(/^[^ ]+ /, '')).join(', '))}`); } else { console.log(` ${chalk_1.default.magenta('โ–ถ')} Technologies: ${chalk_1.default.gray('None selected')}`); } // Show role files installed if (roleStatus) { console.log(` ${chalk_1.default.magenta('โ–ถ')} Role files: ${chalk_1.default.green(`Generic roles + ${roleStatus.installed.length} technology-specific roles`)}`); // Show missing roles if any if (roleStatus.missing.length > 0) { console.log(` ${chalk_1.default.yellow('โ–ถ')} Missing roles: ${chalk_1.default.yellow(`${roleStatus.missing.join(', ')} (directories created for future use)`)}`); } } else { console.log(` ${chalk_1.default.magenta('โ–ถ')} Role files: ${chalk_1.default.green(`Generic roles + technology-specific roles`)}`); } // Show custom name if applied if (customName) { console.log(` ${chalk_1.default.magenta('โ–ถ')} Custom name: ${chalk_1.default.green(`Changed "Claude" to "${customName}"`)}`); } console.log(chalk_1.default.bold.cyan('\n๐Ÿ“ Next steps:')); console.log(` ${chalk_1.default.white('1.')} Review ${chalk_1.default.yellow('CLAUDE.md')} to customize your AI experience`); console.log(` ${chalk_1.default.white('2.')} Have your assistant read ${chalk_1.default.yellow('Initialize_Project.md')} to set up the workspace`); console.log(` ${chalk_1.default.white('3.')} Use ${chalk_1.default.yellow(`claude read ${officeDirName}/roles/[specific-role].md`)} to activate specialized expertise`); console.log('\n'); } catch (error) { console.error(chalk_1.default.red.bold('\nโŒ Error initializing the office:'), error.message); if (options.debug) { console.error(chalk_1.default.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_1.default.gray(`[DEBUG] ${message}`)); }