UNPKG

@gftdcojp/gftd-cli

Version:

GFTD Schema Management CLI - Drizzle-like schema management for Kafka and ksqlDB

537 lines 22.3 kB
"use strict"; /** * Dev Proxy Manager - Local Development Proxy System * Manages multiple projects with subdomain routing and DNS management */ 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 }); exports.DevProxyManager = void 0; const express_1 = __importDefault(require("express")); const http_proxy_middleware_1 = require("http-proxy-middleware"); const uuid_1 = require("uuid"); const fs = __importStar(require("fs-extra")); const path = __importStar(require("node:path")); const node_child_process_1 = require("node:child_process"); const dns_manager_1 = require("./dns-manager"); const chalk_1 = __importDefault(require("chalk")); class DevProxyManager { constructor(configPath) { this.proxyPort = 8081; this.domain = 'gftd.ai.dev'; this.projects = new Map(); this.processes = new Map(); this.nextPort = 3000; this.dnsEnabled = true; this.dnsAutoManage = true; this.configPath = configPath || path.join(process.cwd(), 'gftd.json'); this.dnsManager = new dns_manager_1.DnsManager(this.domain); this.loadConfig(); } /** * 設定ファイルの読み込み */ loadConfig() { try { if (fs.existsSync(this.configPath)) { const config = fs.readJsonSync(this.configPath); this.proxyPort = config.port || 8080; this.domain = config.domain || 'gftd.ai.dev'; this.dnsEnabled = config.dnsEnabled !== false; this.dnsAutoManage = config.dnsAutoManage !== false; // DNS Managerのドメインも更新 this.dnsManager = new dns_manager_1.DnsManager(this.domain); // プロジェクト設定を復元 Object.entries(config.projects || {}).forEach(([id, project]) => { this.projects.set(id, { ...project, status: 'stopped', // 起動時は全て停止状態 lastAccessed: project.lastAccessed ? new Date(project.lastAccessed) : undefined, createdAt: new Date(project.createdAt) }); }); // 次のポート番号を設定 const usedPorts = Array.from(this.projects.values()).map(p => p.port); this.nextPort = Math.max(3000, ...usedPorts) + 1; } } catch (error) { console.warn(chalk_1.default.yellow('⚠️ Failed to load dev config, using defaults')); } } /** * 設定ファイルの保存 */ saveConfig() { try { const config = { port: this.proxyPort, domain: this.domain, dnsEnabled: this.dnsEnabled, dnsAutoManage: this.dnsAutoManage, projects: Object.fromEntries(Array.from(this.projects.entries()).map(([id, project]) => [ id, { ...project, lastAccessed: project.lastAccessed?.toISOString(), createdAt: project.createdAt.toISOString() } ])), autoStart: true, timeout: 30000 }; fs.ensureFileSync(this.configPath); fs.writeJsonSync(this.configPath, config, { spaces: 2 }); } catch (error) { console.error(chalk_1.default.red('❌ Failed to save dev config:'), error); } } /** * DNS管理の初期化 */ async initializeDns() { if (!this.dnsEnabled) { console.log(chalk_1.default.yellow('⚠️ DNS management is disabled')); return; } try { await this.dnsManager.initialize(); console.log(chalk_1.default.green('✅ DNS management initialized')); } catch (error) { console.error(chalk_1.default.red('❌ Failed to initialize DNS management:'), error); this.dnsEnabled = false; } } /** * DNS管理の状態を表示 */ async showDnsStatus() { if (!this.dnsEnabled) { console.log(chalk_1.default.yellow('⚠️ DNS management is disabled')); return; } await this.dnsManager.showStatus(); } /** * 新しいプロジェクトを追加 */ async addProject(options) { const projectPath = options.path || process.cwd(); const projectName = options.name || path.basename(projectPath); const subdomain = options.subdomain || this.generateSubdomain(projectName); const command = options.command || 'pnpm dev'; const port = options.port || this.getNextPort(); const manageDns = options.manageDns !== false && this.dnsEnabled && this.dnsAutoManage; // プロジェクトIDの生成 const projectId = (0, uuid_1.v4)(); const project = { id: projectId, name: projectName, path: projectPath, port, subdomain, command, status: 'stopped', createdAt: new Date() }; this.projects.set(projectId, project); this.saveConfig(); // DNS エントリを追加 if (manageDns) { try { await this.dnsManager.addProjectEntry(subdomain, projectName); console.log(chalk_1.default.green(`✅ DNS entry added for ${subdomain}.${this.domain}`)); } catch (error) { console.warn(chalk_1.default.yellow(`⚠️ Failed to add DNS entry: ${error}`)); } } console.log(chalk_1.default.green(`✅ Added project: ${projectName}`)); console.log(chalk_1.default.blue(` 📁 Path: ${projectPath}`)); console.log(chalk_1.default.blue(` 🌐 URL: http://${subdomain}.${this.domain}`)); console.log(chalk_1.default.blue(` 🚀 Port: ${port}`)); if (!manageDns) { console.log(chalk_1.default.yellow(` ⚠️ DNS not managed automatically - add manually: 127.0.0.1 ${subdomain}.${this.domain}`)); } return project; } /** * プロジェクトを削除 */ async removeProject(projectId) { const project = this.projects.get(projectId); if (!project) { throw new Error(`Project ${projectId} not found`); } // プロジェクトを停止 if (project.status === 'running') { await this.stopProject(projectId); } // DNS エントリを削除 if (this.dnsEnabled && this.dnsAutoManage) { try { await this.dnsManager.removeProjectEntry(project.subdomain); console.log(chalk_1.default.green(`✅ DNS entry removed for ${project.subdomain}.${this.domain}`)); } catch (error) { console.warn(chalk_1.default.yellow(`⚠️ Failed to remove DNS entry: ${error}`)); } } // プロジェクトを削除 this.projects.delete(projectId); this.saveConfig(); console.log(chalk_1.default.green(`✅ Removed project: ${project.name}`)); } /** * プロジェクトを開始 */ async startProject(projectId) { const project = this.projects.get(projectId); if (!project) { throw new Error(`Project ${projectId} not found`); } if (project.status === 'running') { console.log(chalk_1.default.yellow(`⚠️ Project ${project.name} is already running`)); return; } console.log(chalk_1.default.blue(`🚀 Starting project: ${project.name}`)); try { project.status = 'starting'; this.projects.set(projectId, project); // プロジェクトのコマンドを実行 const childProcess = (0, node_child_process_1.spawn)(project.command, { shell: true, cwd: project.path, env: { ...process.env, PORT: project.port.toString() }, stdio: ['pipe', 'pipe', 'pipe'] }); if (childProcess.pid) { project.pid = childProcess.pid; this.processes.set(projectId, childProcess); // プロセスの出力を監視 childProcess.stdout?.on('data', (data) => { console.log(chalk_1.default.gray(`[${project.name}] ${data.toString().trim()}`)); }); childProcess.stderr?.on('data', (data) => { console.error(chalk_1.default.red(`[${project.name}] ${data.toString().trim()}`)); }); childProcess.on('exit', (code) => { console.log(chalk_1.default.yellow(`[${project.name}] Process exited with code ${code}`)); project.status = 'stopped'; project.pid = undefined; this.projects.set(projectId, project); this.processes.delete(projectId); this.saveConfig(); }); // 起動完了を待機 await this.waitForPortOpen(project.port); project.status = 'running'; this.projects.set(projectId, project); this.saveConfig(); console.log(chalk_1.default.green(`✅ Project ${project.name} started successfully`)); console.log(chalk_1.default.blue(` 🌐 Access: http://${project.subdomain}.${this.domain}`)); } else { throw new Error('Failed to start process'); } } catch (error) { project.status = 'error'; this.projects.set(projectId, project); this.saveConfig(); throw error; } } /** * プロジェクトを停止 */ async stopProject(projectId) { const project = this.projects.get(projectId); if (!project) { throw new Error(`Project ${projectId} not found`); } const process = this.processes.get(projectId); if (process && project.pid) { console.log(chalk_1.default.yellow(`🛑 Stopping project: ${project.name}`)); try { process.kill('SIGTERM'); // 5秒待って強制終了 setTimeout(() => { if (process && !process.killed) { process.kill('SIGKILL'); } }, 5000); project.status = 'stopped'; project.pid = undefined; this.projects.set(projectId, project); this.processes.delete(projectId); this.saveConfig(); console.log(chalk_1.default.green(`✅ Project ${project.name} stopped`)); } catch (error) { console.error(chalk_1.default.red(`❌ Error stopping project: ${error}`)); } } } /** * プロキシサーバーを開始 */ async startProxy() { if (this.proxyServer) { console.log(chalk_1.default.yellow('⚠️ Proxy server is already running')); return; } console.log(chalk_1.default.blue('🚀 Starting proxy server...')); // DNS管理を初期化 await this.initializeDns(); this.proxyServer = (0, express_1.default)(); // CORS設定 this.proxyServer.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (req.method === 'OPTIONS') { res.sendStatus(200); } else { next(); } }); // サブドメイン別のプロキシ設定 this.proxyServer.use((req, res, next) => { const host = req.get('host'); if (!host) { return res.status(400).send('Host header is required'); } const subdomain = this.extractSubdomain(host); if (!subdomain) { return res.status(404).send('Invalid subdomain'); } const project = this.findProjectBySubdomain(subdomain); if (!project) { return res.status(404).send(`Project not found for subdomain: ${subdomain}`); } if (project.status !== 'running') { return res.status(503).send(`Project ${project.name} is not running`); } // プロキシ設定 const proxy = (0, http_proxy_middleware_1.createProxyMiddleware)({ target: `http://localhost:${project.port}`, changeOrigin: true, ws: true, onError: (err, req, res) => { console.error(chalk_1.default.red(`❌ Proxy error for ${project.name}:`, err.message)); res.status(502).send(`Proxy error: ${err.message}`); }, onProxyReq: (proxyReq, req, res) => { // アクセス時間を更新 project.lastAccessed = new Date(); this.projects.set(project.id, project); } }); proxy(req, res, next); }); // プロキシサーバーを開始 this.proxyServer.listen(this.proxyPort, () => { console.log(chalk_1.default.green(`✅ Proxy server started on port ${this.proxyPort}`)); console.log(chalk_1.default.blue(` 🌐 Access projects via: http://{subdomain}.${this.domain}`)); // DNS設定のヒント this.showDnsSetupInstructions(); }); } /** * 全てのプロジェクトを停止 */ async stopAll() { console.log(chalk_1.default.yellow('🛑 Stopping all projects...')); const stopPromises = Array.from(this.projects.keys()).map(async (projectId) => { try { await this.stopProject(projectId); } catch (error) { console.error(chalk_1.default.red(`❌ Error stopping project ${projectId}:`, error)); } }); await Promise.all(stopPromises); console.log(chalk_1.default.green('✅ All projects stopped')); } /** * 全てのDNSエントリを削除 */ async cleanupDns() { if (!this.dnsEnabled) { console.log(chalk_1.default.yellow('⚠️ DNS management is disabled')); return; } try { await this.dnsManager.removeAllEntries(); console.log(chalk_1.default.green('✅ All DNS entries removed')); } catch (error) { console.error(chalk_1.default.red('❌ Failed to cleanup DNS:'), error); } } /** * DNSバックアップを復元 */ async restoreDnsBackup() { if (!this.dnsEnabled) { console.log(chalk_1.default.yellow('⚠️ DNS management is disabled')); return; } try { await this.dnsManager.restoreBackup(); console.log(chalk_1.default.green('✅ DNS backup restored')); } catch (error) { console.error(chalk_1.default.red('❌ Failed to restore DNS backup:'), error); } } /** * プロジェクト一覧を表示 */ listProjects() { console.log(chalk_1.default.blue('📋 Development Projects:')); console.log(''); if (this.projects.size === 0) { console.log(chalk_1.default.gray(' No projects found. Use "gftd dev" to add a project.')); return; } this.projects.forEach((project) => { const statusIcon = this.getStatusIcon(project.status); const statusColor = this.getStatusColor(project.status); console.log(` ${statusIcon} ${chalk_1.default.bold(project.name)}`); console.log(` ${chalk_1.default.gray('ID:')} ${project.id}`); console.log(` ${chalk_1.default.gray('URL:')} http://${project.subdomain}.${this.domain}`); console.log(` ${chalk_1.default.gray('Port:')} ${project.port}`); console.log(` ${chalk_1.default.gray('Path:')} ${project.path}`); console.log(` ${chalk_1.default.gray('Status:')} ${statusColor(project.status)}`); console.log(` ${chalk_1.default.gray('Command:')} ${project.command}`); if (project.lastAccessed) { console.log(` ${chalk_1.default.gray('Last accessed:')} ${project.lastAccessed.toLocaleString()}`); } console.log(''); }); // DNS管理状況を表示 if (this.dnsEnabled) { console.log(chalk_1.default.blue('🌐 DNS Management:')); console.log(` ${chalk_1.default.gray('Status:')} ${this.dnsEnabled ? chalk_1.default.green('Enabled') : chalk_1.default.red('Disabled')}`); console.log(` ${chalk_1.default.gray('Auto-manage:')} ${this.dnsAutoManage ? chalk_1.default.green('Yes') : chalk_1.default.red('No')}`); console.log(` ${chalk_1.default.gray('Domain:')} ${this.domain}`); console.log(''); } } // Helper methods generateSubdomain(projectName) { return projectName.toLowerCase().replace(/[^a-z0-9]/g, '-'); } getNextPort() { return this.nextPort++; } extractSubdomain(host) { const parts = host.split('.'); if (parts.length >= 3 && host.endsWith(this.domain)) { return parts[0]; } return null; } findProjectBySubdomain(subdomain) { return Array.from(this.projects.values()).find(p => p.subdomain === subdomain); } getStatusIcon(status) { switch (status) { case 'running': return '🟢'; case 'starting': return '🟡'; case 'stopped': return '🔴'; case 'error': return '❌'; default: return '⚪'; } } getStatusColor(status) { switch (status) { case 'running': return chalk_1.default.green; case 'starting': return chalk_1.default.yellow; case 'stopped': return chalk_1.default.red; case 'error': return chalk_1.default.red; default: return chalk_1.default.gray; } } async waitForPortOpen(port, timeout = 30000) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { try { await new Promise((resolve, reject) => { const net = require('net'); const socket = net.createConnection(port, 'localhost'); socket.on('connect', () => { socket.destroy(); resolve(true); }); socket.on('error', reject); }); return; } catch (error) { await new Promise(resolve => setTimeout(resolve, 1000)); } } throw new Error(`Port ${port} did not open within ${timeout}ms`); } showDnsSetupInstructions() { console.log(''); console.log(chalk_1.default.blue('🔧 DNS Setup:')); console.log(''); if (this.dnsEnabled && this.dnsAutoManage) { console.log(chalk_1.default.green('✅ DNS is automatically managed by GFTD CLI')); console.log(chalk_1.default.gray(' Your hosts file is automatically updated when projects are added/removed')); } else { console.log(chalk_1.default.gray(' Manual DNS setup required. Add to your /etc/hosts:')); console.log(''); console.log(chalk_1.default.yellow(` 127.0.0.1 *.${this.domain}`)); console.log(''); console.log(chalk_1.default.gray(' Or use a local DNS server like dnsmasq:')); console.log(chalk_1.default.yellow(` echo "address=/.${this.domain}/127.0.0.1" | sudo tee /etc/dnsmasq.d/gftd-dev.conf`)); console.log(chalk_1.default.yellow(' sudo brew services restart dnsmasq')); } console.log(''); } } exports.DevProxyManager = DevProxyManager; //# sourceMappingURL=dev-proxy-manager.js.map