@gftdcojp/gftd-cli
Version:
GFTD Schema Management CLI - Drizzle-like schema management for Kafka and ksqlDB
537 lines • 22.3 kB
JavaScript
;
/**
* 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