claude-code-templates
Version:
CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects
579 lines (500 loc) ⢠21.5 kB
JavaScript
const express = require('express');
const path = require('path');
const { spawn } = require('child_process');
const chalk = require('chalk');
const fs = require('fs');
const app = express();
const PORT = process.env.PORT || 3444;
// Load .env file from current working directory (where user runs the command)
function loadEnvFile() {
const envPath = path.join(process.cwd(), '.env');
if (fs.existsSync(envPath)) {
console.log(chalk.blue('š Loading .env file from:'), chalk.gray(envPath));
const envContent = fs.readFileSync(envPath, 'utf8');
const envVars = envContent.split('\n')
.filter(line => line.trim() && !line.startsWith('#'))
.reduce((acc, line) => {
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
const value = valueParts.join('=').trim().replace(/^["']|["']$/g, ''); // Remove quotes
acc[key.trim()] = value;
}
return acc;
}, {});
// Set environment variables
Object.assign(process.env, envVars);
const hasE2B = !!process.env.E2B_API_KEY;
const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;
console.log(chalk.green('ā
Environment variables loaded:'));
console.log(chalk.gray(` ⢠E2B_API_KEY: ${hasE2B ? 'Found' : 'Missing'}`));
console.log(chalk.gray(` ⢠ANTHROPIC_API_KEY: ${hasAnthropic ? 'Found' : 'Missing'}`));
return hasE2B && hasAnthropic;
} else {
console.log(chalk.yellow('ā ļø No .env file found in:'), chalk.gray(envPath));
return false;
}
}
// Load environment variables on startup
const hasApiKeys = loadEnvFile();
// Simple CORS middleware
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
// JSON parsing middleware
app.use(express.json());
// Store active tasks
const activeTasks = new Map();
// Serve the sandbox interface at root
app.get('/', (req, res) => {
// Try local file first (when running from npm package)
const localPath = path.join(__dirname, 'sandbox-interface.html');
// Fallback to docs folder (when running from source)
const docsPath = path.join(__dirname, '../../docs/sandbox-interface.html');
if (fs.existsSync(localPath)) {
res.sendFile(localPath);
} else if (fs.existsSync(docsPath)) {
res.sendFile(docsPath);
} else {
res.status(404).send(`
<html>
<body style="font-family: system-ui; padding: 40px; background: #0f0f0f; color: #e0e0e0;">
<h1>ā Sandbox Interface Not Found</h1>
<p>The sandbox-interface.html file could not be found.</p>
<p>Please reinstall the package or check your installation.</p>
<pre style="background: #1a1a1a; padding: 10px; border-radius: 5px;">
Tried paths:
- ${localPath}
- ${docsPath}
</pre>
</body>
</html>
`);
}
});
// Serve static files for CSS, JS, etc. (but not index.html at root)
app.use('/css', express.static(path.join(__dirname, '../../docs/css')));
app.use('/js', express.static(path.join(__dirname, '../../docs/js')));
app.use('/assets', express.static(path.join(__dirname, '../../docs/assets')));
// Serve components.json for agent autocomplete
app.get('/components.json', (req, res) => {
const componentsPath = path.join(__dirname, '../../docs/components.json');
if (fs.existsSync(componentsPath)) {
res.sendFile(componentsPath);
} else {
res.status(404).json({ error: 'Components file not found' });
}
});
// API endpoint to execute task (local or cloud)
app.post('/api/execute', async (req, res) => {
const { prompt, mode = 'local', agent = 'development-team/frontend-developer' } = req.body;
if (!prompt || prompt.trim().length < 10) {
return res.status(400).json({
success: false,
error: 'Please provide a detailed prompt (at least 10 characters)'
});
}
const taskId = `task-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
// Create task object
const task = {
id: taskId,
title: prompt.substring(0, 60) + (prompt.length > 60 ? '...' : ''),
prompt: prompt.trim(),
agent: agent,
mode: mode,
status: 'running',
startTime: new Date(),
progress: 0,
output: [],
sandboxId: null
};
activeTasks.set(taskId, task);
// Execute based on mode
if (mode === 'cloud') {
executeE2BTask(task);
} else {
executeLocalTask(task);
}
res.json({
success: true,
taskId: taskId,
message: 'Task started successfully'
});
});
// API endpoint to get task status
app.get('/api/task/:taskId', (req, res) => {
const task = activeTasks.get(req.params.taskId);
if (!task) {
return res.status(404).json({
success: false,
error: 'Task not found'
});
}
res.json({
success: true,
task: {
id: task.id,
title: task.title,
status: task.status,
progress: task.progress,
output: task.output.join('\\n'),
startTime: task.startTime,
endTime: task.endTime,
sandboxId: task.sandboxId
}
});
});
// API endpoint to get all tasks
app.get('/api/tasks', (req, res) => {
const tasks = Array.from(activeTasks.values()).map(task => ({
id: task.id,
title: task.title,
status: task.status,
progress: task.progress,
startTime: task.startTime,
endTime: task.endTime,
sandboxId: task.sandboxId,
output: task.output.slice(-3).join('\\n') // Last 3 lines for preview
}));
res.json({
success: true,
tasks: tasks.sort((a, b) => new Date(b.startTime) - new Date(a.startTime))
});
});
// API endpoint to install agent
app.post('/api/install-agent', async (req, res) => {
const { agentName } = req.body;
if (!agentName) {
return res.status(400).json({
success: false,
error: 'Agent name is required'
});
}
try {
console.log(chalk.blue('š§ Installing agent:'), chalk.cyan(agentName));
// Use the CLI tool to install the agent
const child = spawn('npx', ['claude-code-templates@latest', '--agent', agentName, '--yes'], {
cwd: process.cwd(),
stdio: ['pipe', 'pipe', 'pipe'],
shell: true
});
let output = [];
let error = [];
child.stdout.on('data', (data) => {
const lines = data.toString().split('\n').filter(line => line.trim());
output.push(...lines);
});
child.stderr.on('data', (data) => {
const lines = data.toString().split('\n').filter(line => line.trim());
error.push(...lines);
});
child.on('close', (code) => {
if (code === 0) {
console.log(chalk.green('ā
Agent installed successfully:'), chalk.cyan(agentName));
res.json({
success: true,
message: `Agent ${agentName} installed successfully`,
output: output.join('\n')
});
} else {
console.error(chalk.red('ā Agent installation failed:'), chalk.cyan(agentName));
res.status(500).json({
success: false,
error: `Failed to install agent ${agentName}`,
output: error.join('\n')
});
}
});
child.on('error', (error) => {
console.error(chalk.red('ā Agent installation error:'), error.message);
res.status(500).json({
success: false,
error: `Installation error: ${error.message}`
});
});
} catch (error) {
console.error(chalk.red('ā Failed to start agent installation:'), error.message);
res.status(500).json({
success: false,
error: `Failed to start installation: ${error.message}`
});
}
});
async function checkAndInstallAgent(agentName, task) {
// Check if agent exists in .claude directory
const claudeDir = path.join(process.cwd(), '.claude');
const agentPath = path.join(claudeDir, 'agents', `${agentName}.md`);
if (fs.existsSync(agentPath)) {
return true; // Agent already exists
}
task.output.push(`š§ Agent ${agentName} not found locally. Installing...`);
return new Promise((resolve, reject) => {
const child = spawn('npx', ['claude-code-templates@latest', '--agent', agentName, '--yes'], {
cwd: process.cwd(),
stdio: ['pipe', 'pipe', 'pipe'],
shell: true
});
child.stdout.on('data', (data) => {
const lines = data.toString().split('\n').filter(line => line.trim());
lines.forEach(line => {
task.output.push(`š¦ ${line}`);
});
});
child.stderr.on('data', (data) => {
const lines = data.toString().split('\n').filter(line => line.trim());
lines.forEach(line => {
task.output.push(`ā ļø ${line}`);
});
});
child.on('close', (code) => {
if (code === 0) {
task.output.push(`ā
Agent ${agentName} installed successfully`);
resolve(true);
} else {
task.output.push(`ā Failed to install agent ${agentName}`);
resolve(false);
}
});
child.on('error', (error) => {
task.output.push(`ā Installation error: ${error.message}`);
resolve(false);
});
});
}
async function executeE2BTask(task) {
try {
task.output.push('š Initializing E2B sandbox execution...');
task.progress = 10;
// Check and install agent if needed
if (task.agent !== 'development-team/frontend-developer') {
task.output.push(`š Checking agent: ${task.agent}`);
const agentInstalled = await checkAndInstallAgent(task.agent, task);
if (!agentInstalled) {
task.status = 'failed';
task.endTime = new Date();
task.output.push(`ā Could not install required agent: ${task.agent}`);
return;
}
task.progress = 15;
}
const e2bLauncherPath = path.join(__dirname, '../components/sandbox/e2b/e2b-launcher.py');
const agentParam = `--agent=${task.agent} --yes`;
// Build command arguments
const args = [
e2bLauncherPath,
task.prompt,
agentParam
];
// Add API keys from environment if available
if (process.env.E2B_API_KEY) {
args.push(process.env.E2B_API_KEY);
}
if (process.env.ANTHROPIC_API_KEY) {
args.push(process.env.ANTHROPIC_API_KEY);
}
task.output.push('š§ Starting Python E2B launcher...');
task.progress = 20;
// Execute the E2B launcher from the user's working directory
const child = spawn('python3', args, {
cwd: process.cwd(), // This ensures it runs from where the user executed the command
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env, // Pass all environment variables including loaded ones
PATH: process.env.PATH
}
});
// Handle stdout
child.stdout.on('data', (data) => {
const lines = data.toString().split('\\n').filter(line => line.trim());
lines.forEach(line => {
task.output.push(line);
// Update progress based on output
if (line.includes('Sandbox created:')) {
task.sandboxId = line.split('Sandbox created: ')[1] || 'unknown';
task.progress = 40;
} else if (line.includes('Installing')) {
task.progress = 60;
} else if (line.includes('Executing Claude Code')) {
task.progress = 80;
} else if (line.includes('Downloaded:')) {
task.progress = 95;
} else if (line.includes('Execution completed successfully')) {
task.progress = 100;
task.status = 'completed';
task.endTime = new Date();
}
});
});
// Handle stderr
child.stderr.on('data', (data) => {
const lines = data.toString().split('\\n').filter(line => line.trim());
lines.forEach(line => {
task.output.push(`ā ļø ${line}`);
});
});
// Handle process exit
child.on('close', (code) => {
if (code === 0) {
if (task.status !== 'completed') {
task.status = 'completed';
task.endTime = new Date();
task.progress = 100;
}
task.output.push('ā
Task completed successfully!');
} else {
task.status = 'failed';
task.endTime = new Date();
// Check if it's an API key error
const outputText = task.output.join(' ');
if (outputText.includes('E2B API key is required') || outputText.includes('Anthropic API key is required')) {
task.output.push('ā Missing API keys! Please add E2B_API_KEY and ANTHROPIC_API_KEY to your .env file');
task.output.push('š Get E2B key: https://e2b.dev/dashboard');
task.output.push('š Get Anthropic key: https://console.anthropic.com');
} else {
task.output.push(`ā Process exited with code: ${code}`);
}
}
});
// Handle process error
child.on('error', (error) => {
task.status = 'failed';
task.endTime = new Date();
task.output.push(`ā Execution error: ${error.message}`);
});
} catch (error) {
task.status = 'failed';
task.endTime = new Date();
task.output.push(`ā Failed to start execution: ${error.message}`);
}
}
async function executeLocalTask(task) {
try {
task.output.push('š„ļø Executing Claude Code locally...');
task.progress = 10;
// Check and install agent if needed
if (task.agent !== 'development-team/frontend-developer') {
task.output.push(`š Checking agent: ${task.agent}`);
const agentInstalled = await checkAndInstallAgent(task.agent, task);
if (!agentInstalled) {
task.status = 'failed';
task.endTime = new Date();
task.output.push(`ā Could not install required agent: ${task.agent}`);
return;
}
task.progress = 15;
}
task.output.push('š Checking if Claude Code CLI is available...');
// For local execution, we'll include the agent in the prompt if specified
let finalPrompt = task.prompt;
if (task.agent && task.agent !== 'development-team/frontend-developer') {
finalPrompt = `As a ${task.agent.replace('-', ' ')}, ${task.prompt}`;
}
// Execute Claude Code locally with just the prompt
const child = spawn('claude', [finalPrompt], {
cwd: process.cwd(),
stdio: ['ignore', 'pipe', 'pipe'], // Ignore stdin to prevent hanging
env: {
...process.env,
PATH: process.env.PATH
},
shell: true, // Use shell to find claude command
timeout: 300000 // 5 minute timeout
});
task.output.push('š Claude Code execution started');
task.progress = 30;
// Set up timeout to prevent hanging
const executionTimeout = setTimeout(() => {
task.output.push('ā° Execution timeout reached (5 minutes)');
task.output.push('š” This might indicate Claude Code is waiting for input or has hung');
task.status = 'failed';
task.endTime = new Date();
child.kill('SIGTERM');
}, 300000); // 5 minutes
// Handle stdout
child.stdout.on('data', (data) => {
const lines = data.toString().split('\\n').filter(line => line.trim());
lines.forEach(line => {
task.output.push(line);
// Update progress based on output patterns
if (line.includes('Reading') || line.includes('Analyzing')) {
task.progress = Math.min(task.progress + 5, 60);
} else if (line.includes('Writing') || line.includes('Creating')) {
task.progress = Math.min(task.progress + 10, 90);
} else if (line.includes('Done') || line.includes('Complete')) {
task.progress = 95;
}
});
});
// Handle stderr
child.stderr.on('data', (data) => {
const lines = data.toString().split('\\n').filter(line => line.trim());
lines.forEach(line => {
task.output.push(`ā ļø ${line}`);
});
});
// Handle process exit
child.on('close', (code) => {
clearTimeout(executionTimeout); // Clear timeout when process exits
if (code === 0) {
task.status = 'completed';
task.endTime = new Date();
task.progress = 100;
task.output.push('ā
Claude Code execution completed successfully!');
task.output.push('š Files were created/modified in your current directory');
} else {
task.status = 'failed';
task.endTime = new Date();
const outputText = task.output.join(' ');
if (outputText.includes('claude: command not found') || outputText.includes('not recognized')) {
task.output.push('ā Claude Code CLI not found!');
task.output.push('š” Please install Claude Code CLI first:');
task.output.push('š Visit: https://claude.ai/code');
} else {
task.output.push(`ā Process exited with code: ${code}`);
}
}
});
// Handle process error
child.on('error', (error) => {
clearTimeout(executionTimeout); // Clear timeout on error
task.status = 'failed';
task.endTime = new Date();
if (error.code === 'ENOENT') {
task.output.push('ā Claude Code CLI not found in PATH!');
task.output.push('š” Please install Claude Code CLI first:');
task.output.push('š Visit: https://claude.ai/code');
task.output.push('š Documentation: https://docs.anthropic.com/claude-code');
} else {
task.output.push(`ā Execution error: ${error.message}`);
}
});
} catch (error) {
task.status = 'failed';
task.endTime = new Date();
task.output.push(`ā Failed to start local execution: ${error.message}`);
}
}
// Start server
app.listen(PORT, () => {
console.log(chalk.blue('\\nšØ Claude Code Studio Server'));
console.log(chalk.cyan('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
console.log(chalk.green(`š Server running on http://localhost:${PORT}`));
console.log(chalk.gray('š” Local and cloud execution interface ready'));
if (hasApiKeys) {
console.log(chalk.green('\\nā
All API keys are configured and ready!'));
} else {
console.log(chalk.yellow('\\nā ļø API Keys Status:'));
console.log(chalk.gray(` ⢠E2B_API_KEY: ${process.env.E2B_API_KEY ? 'Found' : 'Missing'}`));
console.log(chalk.gray(` ⢠ANTHROPIC_API_KEY: ${process.env.ANTHROPIC_API_KEY ? 'Found' : 'Missing'}`));
console.log(chalk.yellow(' ⢠Please add these keys to your .env file'));
}
console.log('');
});
module.exports = app;