Files
Dani 92e2d1184d feat: Add Docker Sandbox Provider for local Claude Code execution
🐳 New Docker Sandbox Provider - Execute Claude Code in isolated local containers

## Features
- ✅ Fully local execution with complete isolation
- ✅ Claude Agent SDK integration with async generator pattern
- ✅ Multi-directory file detection (/app and /tmp)
- ✅ Support for specialized agents
- ✅ Automated execution with --yes flag
- ✅ Custom Docker environments

## Files Added
- cli-tool/components/sandbox/docker/Dockerfile - Node.js 22 Alpine image
- cli-tool/components/sandbox/docker/docker-launcher.js - Container orchestration
- cli-tool/components/sandbox/docker/execute.js - Claude SDK execution script
- cli-tool/components/sandbox/docker/package.json - Dependencies
- cli-tool/components/sandbox/docker/README.md - Complete documentation

## Changes
- cli-tool/package.json: Bump version to 1.27.0, add docker files to npm package
- cli-tool/src/index.js: Add Docker sandbox execution logic and --yes flag support
- docs/component.html: Add Docker provider documentation and comparison table
- docs/css/component-page.css: Docker provider styling
- docs/js/component-page.js: Docker provider tab functionality

## Usage
npx claude-code-templates@latest --sandbox docker --agent example --prompt "task" --yes

## Requirements
- Docker Desktop installed and running
- Anthropic API key configured

🎉 Version 1.27.0 published to npm

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:34:49 -05:00

252 lines
7.5 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Docker Sandbox Executor
* Runs inside Docker container using Claude Agent SDK
*/
import { query } from '@anthropic-ai/claude-agent-sdk';
import fs from 'fs/promises';
import path from 'path';
import { spawn } from 'child_process';
// Parse command line arguments
const args = process.argv.slice(2);
const prompt = args[0] || 'Hello, Claude!';
const componentsToInstall = args[1] || '';
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
// Validate API key
if (!anthropicApiKey) {
console.error('❌ Error: ANTHROPIC_API_KEY environment variable is required');
process.exit(1);
}
console.log('🐳 Docker Sandbox Executor');
console.log('═══════════════════════════════════════\n');
/**
* Install Claude Code components if specified
*/
async function installComponents() {
if (!componentsToInstall || componentsToInstall.trim() === '') {
return true;
}
console.log('📦 Installing components...');
console.log(` Components: ${componentsToInstall}\n`);
return new Promise((resolve) => {
const installCmd = `npx claude-code-templates@latest ${componentsToInstall} --yes`;
const child = spawn('sh', ['-c', installCmd], {
stdio: 'inherit',
env: process.env
});
child.on('close', (code) => {
if (code === 0) {
console.log('\n✅ Components installed successfully\n');
resolve(true);
} else {
console.log('\n⚠️ Component installation had warnings (continuing...)\n');
resolve(true); // Continue even if installation has warnings
}
});
child.on('error', (error) => {
console.error(`❌ Installation error: ${error.message}`);
resolve(false);
});
});
}
/**
* Execute Claude Code query using Agent SDK
*/
async function executeQuery() {
try {
console.log('🤖 Executing Claude Code...');
console.log(` Prompt: "${prompt.substring(0, 80)}${prompt.length > 80 ? '...' : ''}"\n`);
console.log('─'.repeat(60));
console.log('📝 CLAUDE OUTPUT:');
console.log('─'.repeat(60) + '\n');
// Enhance prompt with working directory context
const enhancedPrompt = `${prompt}\n\nNote: Your current working directory is /app. When creating files, save them in the current directory (/app) so they can be captured in the output.`;
// query() returns an async generator - we need to iterate it
const generator = query({
prompt: enhancedPrompt,
options: {
apiKey: anthropicApiKey,
model: 'claude-sonnet-4-5',
permissionMode: 'bypassPermissions', // Auto-allow all tool uses
}
});
let assistantResponses = [];
let messageCount = 0;
// Iterate through the async generator
for await (const message of generator) {
messageCount++;
if (message.type === 'assistant') {
// Extract text from assistant message content
if (message.message && message.message.content) {
const content = Array.isArray(message.message.content)
? message.message.content
: [message.message.content];
content.forEach(block => {
if (block.type === 'text') {
console.log(block.text);
assistantResponses.push(block.text);
}
});
}
} else if (message.type === 'result') {
// Show final result metadata
console.log('\n' + '─'.repeat(60));
console.log(`✅ Execution completed (${message.num_turns} turn${message.num_turns > 1 ? 's' : ''})`);
console.log(` Duration: ${message.duration_ms}ms`);
console.log(` Cost: $${message.total_cost_usd.toFixed(5)}`);
console.log('─'.repeat(60) + '\n');
}
}
// Show response summary
const responseText = assistantResponses.join('\n');
if (responseText) {
console.log('📄 Response Summary:');
console.log(` ${messageCount} message(s) received`);
console.log(` ${assistantResponses.length} assistant response(s)`);
console.log(` ${responseText.length} characters generated`);
console.log('');
}
return true;
} catch (error) {
console.error('\n❌ Execution error:', error.message);
if (error.stack) {
console.error('\nStack trace:');
console.error(error.stack);
}
return false;
}
}
/**
* Find and copy generated files to output directory
*/
async function copyGeneratedFiles() {
try {
console.log('📁 Searching for generated files...\n');
// Common file extensions to look for
const extensions = [
'js', 'jsx', 'ts', 'tsx',
'py', 'html', 'css', 'scss',
'json', 'md', 'yaml', 'yml',
'txt', 'sh', 'bash'
];
// Search for files in multiple directories
const { execSync } = await import('child_process');
const findPattern = extensions.map(ext => `-name "*.${ext}"`).join(' -o ');
// Search in /app and /tmp for generated files
const searchPaths = ['/app', '/tmp'];
let allFiles = [];
for (const searchPath of searchPaths) {
const findCmd = `find ${searchPath} -type f \\( ${findPattern} \\) ! -path "*/node_modules/*" ! -path "*/.npm/*" ! -path "/app/execute.js" ! -path "/app/package*.json" -newer /app/execute.js 2>/dev/null | head -50`;
try {
const output = execSync(findCmd, { encoding: 'utf8' });
const files = output.trim().split('\n').filter(f => f.trim());
allFiles = allFiles.concat(files);
} catch (error) {
// Continue to next search path
}
}
if (allFiles.length === 0) {
console.log('ℹ️ No generated files found\n');
return;
}
console.log(`📦 Found ${allFiles.length} file(s):\n`);
// Copy files to output directory preserving structure
let copiedCount = 0;
for (const file of allFiles) {
try {
// Determine relative path based on source directory
let relativePath;
if (file.startsWith('/app/')) {
relativePath = file.replace('/app/', '');
} else if (file.startsWith('/tmp/')) {
relativePath = file.replace('/tmp/', '');
} else {
relativePath = path.basename(file);
}
const outputPath = path.join('/output', relativePath);
// Create directory structure
await fs.mkdir(path.dirname(outputPath), { recursive: true });
// Copy file
await fs.copyFile(file, outputPath);
console.log(` ✅ ${relativePath}`);
copiedCount++;
} catch (error) {
console.log(` ⚠️ Failed to copy: ${file}`);
}
}
if (copiedCount > 0) {
console.log(`\n✅ Copied ${copiedCount} file(s) to output directory\n`);
}
} catch (error) {
console.error('❌ Error copying files:', error.message);
}
}
/**
* Main execution flow
*/
async function main() {
try {
// Step 1: Install components
const installSuccess = await installComponents();
if (!installSuccess) {
console.error('❌ Component installation failed');
process.exit(1);
}
// Step 2: Execute Claude query
const executeSuccess = await executeQuery();
if (!executeSuccess) {
console.error('❌ Query execution failed');
process.exit(1);
}
// Step 3: Copy generated files
await copyGeneratedFiles();
console.log('🎉 Docker sandbox execution completed successfully!');
process.exit(0);
} catch (error) {
console.error('❌ Fatal error:', error.message);
process.exit(1);
}
}
// Run main function
main();