claudes-office
Version:
CLI tool to initialize Claude's office in your project
1,245 lines (1,059 loc) โข 56.2 kB
JavaScript
#!/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