import { spawn, ChildProcess } from 'child_process';
import { ConfigLoader } from '../config/config-loader.js';
export class CloudflareTunnelManager {
private tunnelProcess: ChildProcess | null = null;
private tunnelUrl: string | null = null;
private config = ConfigLoader.getInstance();
async startTunnel(): Promise<string> {
const cloudflareConfig = this.config.getConfig().cloudflare;
if (!cloudflareConfig.enabled) {
throw new Error('Cloudflare tunnel is not enabled in configuration');
}
// Allow Cloudflare tunnel in production for debugging
// In a real production deployment, use a named tunnel instead
return new Promise((resolve, reject) => {
const port = this.config.getConfig().server.port;
const command = cloudflareConfig.tunnelCommand.replace('8787', port.toString());
console.log(`[TUNNEL] Starting Cloudflare tunnel: ${command}`);
// Parse command into command and args
const [cmd, ...args] = command.split(' ');
this.tunnelProcess = spawn(cmd, args, {
stdio: ['ignore', 'pipe', 'pipe']
});
let urlDetected = false;
// Listen to stdout for the tunnel URL
this.tunnelProcess.stdout?.on('data', (data) => {
const output = data.toString();
if (this.config.isDebugMode()) {
console.log(`[TUNNEL:STDOUT] ${output}`);
}
// Look for the tunnel URL pattern
const urlMatch = output.match(/https:\/\/[a-z-]+\.trycloudflare\.com/);
if (urlMatch && !urlDetected) {
urlDetected = true;
this.tunnelUrl = urlMatch[0];
console.log(`[TUNNEL] Detected tunnel URL: ${this.tunnelUrl}`);
// Update configuration with the new URL
this.config.updateTunnelUrl(this.tunnelUrl!);
resolve(this.tunnelUrl!);
}
});
// Listen to stderr for errors and additional info
this.tunnelProcess.stderr?.on('data', (data) => {
const output = data.toString();
if (this.config.isDebugMode()) {
console.log(`[TUNNEL:STDERR] ${output}`);
}
// Also check stderr for URL (cloudflared sometimes outputs there)
if (!urlDetected) {
const urlMatch = output.match(/https:\/\/[a-z-]+\.trycloudflare\.com/);
if (urlMatch) {
urlDetected = true;
this.tunnelUrl = urlMatch[0];
console.log(`[TUNNEL] Detected tunnel URL: ${this.tunnelUrl}`);
// Update configuration with the new URL
this.config.updateTunnelUrl(this.tunnelUrl!);
resolve(this.tunnelUrl!);
}
}
});
this.tunnelProcess.on('error', (error) => {
console.error(`[TUNNEL] Failed to start tunnel: ${error.message}`);
reject(error);
});
this.tunnelProcess.on('exit', (code) => {
console.log(`[TUNNEL] Process exited with code ${code}`);
this.tunnelProcess = null;
this.tunnelUrl = null;
});
// Timeout if URL not detected within 30 seconds
setTimeout(() => {
if (!urlDetected) {
reject(new Error('Timeout: Could not detect tunnel URL within 30 seconds'));
this.stopTunnel();
}
}, 30000);
});
}
stopTunnel(): void {
if (this.tunnelProcess) {
console.log('[TUNNEL] Stopping Cloudflare tunnel...');
this.tunnelProcess.kill();
this.tunnelProcess = null;
this.tunnelUrl = null;
}
}
getTunnelUrl(): string | null {
return this.tunnelUrl;
}
isRunning(): boolean {
return this.tunnelProcess !== null;
}
}