UNPKG

@pimzino/claude-code-spec-workflow

Version:

Automated workflows for Claude Code. Includes spec-driven development (Requirements → Design → Tasks → Implementation) with intelligent task execution, optional steering documents and streamlined bug fix workflow (Report → Analyze → Fix → Verify). We have

183 lines 7.51 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CloudflareProvider = void 0; const child_process_1 = require("child_process"); const types_1 = require("./types"); const util_1 = require("util"); const execPromise = (0, util_1.promisify)(child_process_1.exec); class CloudflareTunnelInstance { constructor(provider, _port) { this.provider = provider; this._port = _port; this.status = 'active'; this.createdAt = new Date(); this.process = null; this._url = null; this.closing = false; } get url() { if (!this._url) { throw new Error('Tunnel URL not yet available'); } return this._url; } setUrl(url) { this._url = url; } setProcess(process) { this.process = process; } async close() { if (this.closing || this.status === 'closing') { return; } this.closing = true; this.status = 'closing'; if (this.process) { const process = this.process; return new Promise((resolve) => { const timeout = setTimeout(() => { if (process) { process.kill('SIGKILL'); } resolve(); }, 5000); process.once('exit', () => { clearTimeout(timeout); this.status = 'closing'; resolve(); }); process.kill('SIGTERM'); }); } } async getHealth() { if (this.status !== 'active' || !this._url) { return { healthy: false, error: `Tunnel is ${this.status}` }; } try { const start = Date.now(); // Perform actual HTTP health check const response = await fetch(this._url, { method: 'HEAD', signal: AbortSignal.timeout(5000) }); const latency = Date.now() - start; return { healthy: response.ok, latency, error: !response.ok ? `HTTP ${response.status}` : undefined }; } catch (error) { return { healthy: false, error: error instanceof Error ? error.message : 'Health check failed' }; } } } class CloudflareProvider { constructor(_config) { this._config = _config; this.name = 'cloudflare'; } async isAvailable() { try { const { stdout } = await execPromise('which cloudflared'); return !!stdout.trim(); } catch { try { // Check if cloudflared is available in common locations const { stdout } = await execPromise('cloudflared --version 2>/dev/null'); return !!stdout; } catch { return false; } } } async validateConfig() { const available = await this.isAvailable(); if (!available) { throw new types_1.TunnelProviderError(this.name, 'CLOUDFLARED_NOT_FOUND', 'cloudflared CLI not found. Please install it from https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/'); } } async createTunnel(port, options) { await this.validateConfig(); const instance = new CloudflareTunnelInstance(this.name, port); return new Promise((resolve, reject) => { const args = ['tunnel', '--url', `http://localhost:${port}`]; // Add metadata if provided if (options.metadata) { for (const [key, value] of Object.entries(options.metadata)) { args.push('--header', `X-Tunnel-${key}: ${value}`); } } const cloudflared = (0, child_process_1.spawn)('cloudflared', args, { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env } }); instance.setProcess(cloudflared); let urlFound = false; let errorOutput = ''; const handleOutput = (data) => { const output = data.toString(); // Look for the tunnel URL in the output const urlMatch = output.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/); if (urlMatch && !urlFound) { urlFound = true; instance.setUrl(urlMatch[0]); resolve(instance); } // Capture any error messages if (output.toLowerCase().includes('error')) { errorOutput += output; } }; cloudflared.stdout.on('data', handleOutput); cloudflared.stderr.on('data', handleOutput); cloudflared.on('error', (error) => { instance.status = 'error'; const enhancedError = new types_1.TunnelProviderError(this.name, 'CLOUDFLARED_START_ERROR', `Failed to start cloudflared: ${error.message}`, 'Could not start Cloudflare tunnel. This might be a configuration or network issue.', [ 'Check that cloudflared is properly installed', 'Verify your internet connection', 'Try running cloudflared manually: cloudflared tunnel --url http://localhost:' + port, 'Check firewall settings for outbound HTTPS access' ]); reject(enhancedError); }); cloudflared.on('exit', (code, signal) => { if (!urlFound) { instance.status = 'error'; const enhancedError = new types_1.TunnelProviderError(this.name, 'CLOUDFLARED_EXIT_EARLY', `cloudflared exited unexpectedly (code: ${code}, signal: ${signal}). ${errorOutput}`, 'Cloudflare tunnel exited before establishing connection.', [ 'Check that the port is not already in use', 'Verify cloudflared has proper permissions', 'Try a different port or check port conflicts', 'Review cloudflared logs for specific error details' ]); reject(enhancedError); } }); // Timeout if URL not found within 30 seconds setTimeout(() => { if (!urlFound) { cloudflared.kill(); const enhancedError = new types_1.TunnelProviderError(this.name, 'CLOUDFLARED_TIMEOUT', 'Timeout waiting for tunnel URL from cloudflared', 'Cloudflare tunnel took too long to start. This usually indicates a network or configuration issue.', [ 'Check your internet connection', 'Verify firewall allows outbound HTTPS connections', 'Try running cloudflared manually to test: cloudflared tunnel --url http://localhost:' + port, 'Check if you have any proxy or VPN that might interfere' ]); reject(enhancedError); } }, 30000); }); } } exports.CloudflareProvider = CloudflareProvider; //# sourceMappingURL=cloudflare-provider.js.map