UNPKG

@jjdenhertog/ai-driven-development

Version:

AI-driven development workflow with learning capabilities for Claude

351 lines (342 loc) 17.3 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.containerStartCommandAlternative = containerStartCommandAlternative; /* eslint-disable max-depth */ /* eslint-disable unicorn/no-array-push-push */ const fs_extra_1 = require("fs-extra"); const node_child_process_1 = require("node:child_process"); const node_path_1 = require("node:path"); const node_util_1 = require("node:util"); const checkDockerAvailable_1 = require("../utils/docker/checkDockerAvailable"); const getContainerName_1 = require("../utils/docker/getContainerName"); const getContainerStatus_1 = require("../utils/docker/getContainerStatus"); const isContainerRunning_1 = require("../utils/docker/isContainerRunning"); const logger_1 = require("../utils/logger"); const isRunningInDocker_1 = require("../utils/docker/isRunningInDocker"); const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec); // Default allowed domains for AI development containers const DEFAULT_ALLOWED_DOMAINS = [ // GitHub 'github.com', 'api.github.com', 'raw.githubusercontent.com', 'github.githubassets.com', 'collector.github.com', // NPM/Node 'registry.npmjs.org', 'nodejs.org', 'npmjs.com', // AI Services 'api.anthropic.com', 'sentry.io', 'statsig.anthropic.com', 'statsig.com', // Development tools 'typescriptlang.org', 'www.typescriptlang.org', 'eslint.org', 'prettier.io', 'typicode.github.io', 'commitlint.js.org', // Frontend frameworks 'nextjs.org', 'vercel.com', 'react.dev', 'reactjs.org', // UI libraries 'mui.com', 'material-ui.com', 'tailwindcss.com', 'styled-components.com', 'emotion.sh', 'chakra-ui.com', 'ant.design', 'getbootstrap.com', // CSS/Build tools 'sass-lang.com', 'postcss.org', // Documentation 'developer.mozilla.org' ]; function createSquidProxyContainer(containerName, allowedDomains) { return __awaiter(this, void 0, void 0, function* () { const proxyName = `${containerName}-proxy`; // Check if proxy already exists if (yield (0, isContainerRunning_1.isContainerRunning)(proxyName)) { (0, logger_1.log)(`Proxy container ${proxyName} already running`, 'info'); return proxyName; } // Create squid configuration const squidConf = ` # Squid configuration for AI container network filtering http_port 3128 # Access control acl localnet src 10.0.0.0/8 acl localnet src 172.16.0.0/12 acl localnet src 192.168.0.0/16 # Allowed domains ${allowedDomains.map(domain => `acl allowed_domains dstdomain .${domain}`).join('\n')} # Allow localhost acl localhost dst 127.0.0.1/32 ::1 # Safe ports acl SSL_ports port 443 acl Safe_ports port 80 acl Safe_ports port 443 acl Safe_ports port 22 # Deny unsafe ports http_access deny !Safe_ports # Allow localhost access http_access allow localhost # Allow only whitelisted domains http_access allow allowed_domains http_access deny all # Logging access_log none cache_store_log none # Performance cache deny all `; // Create config directory const configDir = `/tmp/squid-${containerName}`; (0, fs_extra_1.mkdirpSync)(configDir); (0, fs_extra_1.writeFileSync)((0, node_path_1.join)(configDir, 'squid.conf'), squidConf); // Build custom squid image with our config const dockerfile = ` FROM ubuntu/squid:latest COPY squid.conf /etc/squid/squid.conf RUN chmod 644 /etc/squid/squid.conf `; (0, fs_extra_1.writeFileSync)((0, node_path_1.join)(configDir, 'Dockerfile'), dockerfile); try { // Build the proxy image (0, logger_1.log)(`Building Squid proxy image for ${containerName}...`, 'info'); yield execAsync(`docker build -t ${proxyName}-image ${configDir}`); // Create isolated network const networkName = `${containerName}-net`; yield execAsync(`docker network create --internal ${networkName} 2>/dev/null || true`); // Run the proxy container (0, logger_1.log)(`Starting Squid proxy container ${proxyName}...`, 'info'); yield execAsync(`docker run -d --name ${proxyName} --network ${networkName} ${proxyName}-image`); // Connect proxy to bridge network for external access yield execAsync(`docker network connect bridge ${proxyName}`); (0, logger_1.log)(`Proxy container ${proxyName} started successfully`, 'success'); return proxyName; } catch (error) { (0, logger_1.log)(`Failed to create proxy container: ${error instanceof Error ? error.message : String(error)}`, 'error'); throw error; } }); } function containerStartCommandAlternative(options) { return __awaiter(this, void 0, void 0, function* () { const { name, type, path = process.cwd(), proxyPort = 8888, networkFilter = false } = options; try { // Check Docker availability const docker = yield (0, checkDockerAvailable_1.checkDockerAvailable)(); if (!docker.available) { (0, logger_1.log)(docker.error || 'Docker is not available', 'error'); throw new Error('Docker is required to run containers'); } // Check if .aidev-containers exists const devcontainerPath = (0, node_path_1.join)(path, '.aidev-containers'); if (!(0, fs_extra_1.existsSync)(devcontainerPath)) { (0, logger_1.log)('No .aidev-containers directory found. Run "aidev init" first.', 'error'); (0, logger_1.log)(path, 'info'); throw new Error('.aidev-containers directory not found'); } // Determine which container config to use const configType = type || name; (0, logger_1.log)(`Starting container '${name}' with type '${configType}'...`, 'info'); if (networkFilter) { (0, logger_1.log)('Network filtering enabled - container will only access whitelisted domains', 'info'); } // Check if specific container config exists const configPath = (0, node_path_1.join)(devcontainerPath, configType, 'devcontainer.json'); if (!(0, fs_extra_1.existsSync)(configPath)) { (0, logger_1.log)(`No container configuration found for type '${configType}' at ${configPath}`, 'error'); (0, logger_1.log)(`Available types: code, learn, plan, web`, 'info'); throw new Error(`Missing ${configType} container configuration`); } // Get the container name const containerName = (0, getContainerName_1.getContainerName)(name); if (yield (0, isContainerRunning_1.isContainerRunning)(containerName)) { (0, logger_1.log)(`Container ${containerName} is already running`, 'warn'); return; } // Check if container exists but is stopped const status = yield (0, getContainerStatus_1.getContainerStatus)(containerName); if (status && status.state !== 'running') { (0, logger_1.log)(`Starting existing container ${containerName}...`, 'info'); yield execAsync(`docker start ${containerName}`, { cwd: path }); (0, logger_1.log)(`Container ${containerName} started successfully`, 'success'); return; } // Set up network filtering if enabled let proxyContainerName; if (networkFilter) { proxyContainerName = yield createSquidProxyContainer(containerName, DEFAULT_ALLOWED_DOMAINS); } // Build the Docker image (0, logger_1.log)(`Building Docker image for ${name} using ${configType} configuration...`, 'info'); const dockerfilePath = (0, node_path_1.join)(devcontainerPath, configType, 'Dockerfile'); try { yield execAsync(`docker build -f "${dockerfilePath}" -t ${containerName} "${devcontainerPath}"`, { maxBuffer: 1024 * 1024 * 10, cwd: path } // 10MB buffer for build output ); (0, logger_1.log)(`Docker image built successfully`, 'success'); } catch (error) { (0, logger_1.log)(`Failed to build Docker image: ${error instanceof Error ? error.message : String(error)}`, 'error'); throw error; } // Prepare run command const runArgs = [ 'run', '-dit', // -d for detached, -i for interactive, -t for tty '--name', containerName ]; // Apply network filtering if enabled if (networkFilter && proxyContainerName) { const networkName = `${containerName}-net`; // Use isolated network runArgs.push('--network', networkName); // Configure proxy environment variables runArgs.push('-e', `http_proxy=http://${proxyContainerName}:3128`); runArgs.push('-e', `https_proxy=http://${proxyContainerName}:3128`); runArgs.push('-e', 'no_proxy=localhost,127.0.0.1,::1'); // Configure package managers to use proxy runArgs.push('-e', `npm_config_proxy=http://${proxyContainerName}:3128`); runArgs.push('-e', `npm_config_https_proxy=http://${proxyContainerName}:3128`); (0, logger_1.log)(`Container will use proxy ${proxyContainerName} for network filtering`, 'info'); } if (process.env.AIDEV_HOST_WORKSPACE) { // Running in a standardized workstation environment const currentPath = path; const workspaceBase = '/workspace'; if (currentPath.startsWith(workspaceBase)) { // Extract relative path from /workspace const relativePath = currentPath.slice(workspaceBase.length); const hostPath = process.env.AIDEV_HOST_WORKSPACE + relativePath; runArgs.push('-v', `${hostPath}:/workspace`); runArgs.push('-v', `${process.env.AIDEV_HOST_WORKSPACE}/.dev-credentials:/credentials`); (0, logger_1.log)(`Using workstation path mapping: ${hostPath}`, 'info'); } else { (0, logger_1.log)('Current directory is not under /workspace', 'error'); throw new Error('When using AIDEV_HOST_WORKSPACE, you must be in /workspace directory'); } } else { // Standard host execution runArgs.push('-v', `${path}:/workspace`); } runArgs.push('--workdir', '/workspace', '--cap-add=NET_ADMIN', '--cap-add=NET_RAW'); // Add environment variables runArgs.push('-e', 'NODE_OPTIONS=--max-old-space-size=4096'); const webPort = process.env.AIDEV_WEB_PORT ? parseInt(process.env.AIDEV_WEB_PORT) : 3001; if (configType === 'web') { const containerId = yield getContainerId(); // Use AIDEV_WEB_PORT if available, otherwise use the port parameter runArgs.push('-p', `${webPort}:${webPort}`); runArgs.push('-e', `AIDEV_WEB_PORT=${webPort}`); runArgs.push('-e', `CONTAINER_PREFIX=${(0, getContainerName_1.getContainerName)('')}`); (0, logger_1.log)(`Web container will run on port ${webPort}`, 'info'); const runningInDocker = (0, isRunningInDocker_1.isRunningInDocker)(); if (runningInDocker) { if (containerId) { const network = yield getCurrentContainerNetwork(containerId); const containerName = yield getCurrentContainerName(containerId); if (network && containerName) { if (!networkFilter) { // Only use parent network if not using network filtering runArgs.push('--network', network); } runArgs.push('-e', `AIDEV_HOST_PROXY=http://${containerName}:${proxyPort}`); (0, logger_1.log)(`Container will join network of ${containerName}: ${network}`, 'info'); } } } else { runArgs.push('-e', `AIDEV_HOST_PROXY=http://host.docker.internal:${proxyPort}`); } (0, logger_1.log)(`Container can use a proxy for container management`, 'info'); (0, logger_1.log)(`Make sure to start the proxy with: aidev proxy start`, 'info'); } // Add the container image name runArgs.push(containerName); // Run the container (0, logger_1.log)(`Starting container ${containerName}...`, 'info'); try { yield execAsync(`docker ${runArgs.join(' ')}`, { cwd: path }); (0, logger_1.log)(`Container ${containerName} started successfully`, 'success'); if (networkFilter) { (0, logger_1.log)('', 'info'); (0, logger_1.log)('Network filtering is active. The container can only access:', 'info'); (0, logger_1.log)('- GitHub repositories and APIs', 'info'); (0, logger_1.log)('- NPM registry', 'info'); (0, logger_1.log)('- AI services (Anthropic)', 'info'); (0, logger_1.log)('- Common development resources', 'info'); (0, logger_1.log)('', 'info'); (0, logger_1.log)('To test network filtering:', 'info'); (0, logger_1.log)(` aidev container exec ${name} curl https://example.com # Should fail`, 'info'); (0, logger_1.log)(` aidev container exec ${name} curl https://api.github.com # Should work`, 'info'); } if (configType === 'web') { (0, logger_1.log)(`Web interface will be available at http://localhost:${webPort} once startup is complete`, 'info'); (0, logger_1.log)(`Use "aidev container logs ${name} -f" to monitor startup progress`, 'info'); } else { (0, logger_1.log)(`Use "aidev container exec ${name} <command>" to run commands in the container`, 'info'); } } catch (error) { (0, logger_1.log)(`Failed to start container: ${error instanceof Error ? error.message : String(error)}`, 'error'); throw error; } } catch (error) { (0, logger_1.log)(`Failed to start ${name} container: ${error instanceof Error ? error.message : String(error)}`, 'error'); throw error; } }); } function getContainerId() { return __awaiter(this, void 0, void 0, function* () { const { stdout: containerId } = yield execAsync(`cat /proc/self/cgroup 2>/dev/null | grep -o -E '[0-9a-f]{64}' | head -n 1 || echo ""`); if (containerId.trim()) return containerId.trim(); const { stdout: fallbackId } = yield execAsync(`cat /proc/1/cgroup 2>/dev/null | grep 'docker' | sed 's/^.*\\///' | tail -n 1 || echo ""`); if (fallbackId.trim()) return fallbackId.trim(); }); } function getCurrentContainerName(containerId) { return __awaiter(this, void 0, void 0, function* () { const result = yield execAsync(`docker inspect ${containerId} --format='{{.Name}}'`); return result.stdout.trim().replace(/^\//, ''); }); } function getCurrentContainerNetwork(containerId) { return __awaiter(this, void 0, void 0, function* () { try { const result = yield execAsync(`docker inspect ${containerId} --format='{{range $net, $config := .NetworkSettings.Networks}}{{$net}}{{end}}'`); return result.stdout .trim() .split('\n') .filter(n => n && n !== 'bridge')[0]; } catch (_error) { (0, logger_1.log)('Could not determine current container network', 'warn'); return null; } }); } //# sourceMappingURL=containerStartCommandAlternative.js.map