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
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;
|
|
}
|
|
}
|
|
}
|