claudes-office
Version:
CLI tool to initialize Claude's office in your project
964 lines โข 65.5 kB
JavaScript
#!/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}`));
}