You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

66 lines
1.8 KiB
TypeScript

import { spawn, type ChildProcess } from 'node:child_process';
export class CloudflareTunnel {
private process: ChildProcess | null = null;
private url: string | null = null;
async start(port: number): Promise<string> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Tunnel startup timed out after 30s'));
}, 30000);
this.process = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${port}`], {
stdio: ['ignore', 'pipe', 'pipe'],
});
const handleOutput = (data: Buffer) => {
const text = data.toString();
// cloudflared prints the URL to stderr
const urlMatch = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
if (urlMatch && !this.url) {
this.url = urlMatch[0];
clearTimeout(timeout);
resolve(this.url);
}
};
this.process.stdout?.on('data', handleOutput);
this.process.stderr?.on('data', handleOutput);
this.process.on('error', (err) => {
clearTimeout(timeout);
if (err.message.includes('ENOENT')) {
reject(
new Error(
'cloudflared not found. Install with: brew install cloudflared\n' +
'Or disable tunnel with: --no-tunnel'
)
);
} else {
reject(err);
}
});
this.process.on('exit', (code) => {
if (!this.url) {
clearTimeout(timeout);
reject(new Error(`cloudflared exited with code ${code}`));
}
});
});
}
getUrl(): string | null {
return this.url;
}
stop(): void {
if (this.process) {
this.process.kill('SIGTERM');
this.process = null;
this.url = null;
}
}
}