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

313 lines 12.9 kB
"use strict"; 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.NgrokProvider = void 0; const child_process_1 = require("child_process"); const types_1 = require("./types"); const util_1 = require("util"); const fs = __importStar(require("fs")); const execPromise = (0, util_1.promisify)(child_process_1.exec); const fsPromises = fs.promises; class NgrokTunnelInstance { 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 NgrokProvider { constructor(_config) { this._config = _config; this.name = 'ngrok'; } async isAvailable() { try { const { stdout } = await execPromise('which ngrok'); return !!stdout.trim(); } catch { try { // Check if ngrok is available in common locations const { stdout } = await execPromise('ngrok --version 2>/dev/null'); return !!stdout; } catch { // Check if ngrok is in node_modules try { const ngrokPath = require.resolve('ngrok/bin/ngrok'); const stats = await fsPromises.stat(ngrokPath); return stats.isFile(); } catch { return false; } } } } async getNgrokExecutable() { // First try system ngrok try { const { stdout } = await execPromise('which ngrok'); if (stdout.trim()) { return 'ngrok'; } } catch { // Continue to other options } // Try to find ngrok in node_modules try { const ngrokPath = require.resolve('ngrok/bin/ngrok'); const stats = await fsPromises.stat(ngrokPath); if (stats.isFile()) { return ngrokPath; } } catch { // Continue to error } throw new types_1.TunnelProviderError(this.name, 'NGROK_NOT_FOUND', 'ngrok not found. Please install it from https://ngrok.com/download or run: npm install ngrok'); } async validateConfig() { const available = await this.isAvailable(); if (!available) { throw new types_1.TunnelProviderError(this.name, 'NGROK_NOT_FOUND', 'ngrok not found. Please install it from https://ngrok.com/download or run: npm install ngrok'); } // Check if auth token is configured if (!this._config?.authToken) { // Check if ngrok is already configured with an auth token try { const ngrokExe = await this.getNgrokExecutable(); const { stdout } = await execPromise(`${ngrokExe} config check`); if (!stdout.includes('authtoken')) { throw new types_1.TunnelProviderError(this.name, 'NGROK_AUTH_REQUIRED', 'ngrok requires an auth token. Set NGROK_AUTHTOKEN environment variable or pass authToken in config'); } } catch (error) { if (error instanceof types_1.TunnelProviderError) { throw error; } // If config check fails, we'll try to proceed anyway } } } async createTunnel(port, options) { await this.validateConfig(); const instance = new NgrokTunnelInstance(this.name, port); const ngrokExe = await this.getNgrokExecutable(); return new Promise((resolve, reject) => { const args = ['http', port.toString()]; // Add region if configured if (this._config?.region) { args.push('--region', this._config.region); } // Add auth token if provided if (this._config?.authToken) { args.push('--authtoken', this._config.authToken); } // Add metadata as headers if provided if (options.metadata) { for (const [key, value] of Object.entries(options.metadata)) { args.push('--request-header-add', `X-Tunnel-${key}: ${value}`); } } // Use JSON output for easier parsing args.push('--log', 'stdout', '--log-format', 'json'); const ngrok = (0, child_process_1.spawn)(ngrokExe, args, { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...(this._config?.authToken ? { NGROK_AUTHTOKEN: this._config.authToken } : {}) } }); instance.setProcess(ngrok); let urlFound = false; let errorOutput = ''; const handleOutput = (data) => { const output = data.toString(); try { // Try to parse each line as JSON const lines = output.trim().split('\n'); for (const line of lines) { if (!line.trim()) continue; try { const log = JSON.parse(line); // Look for the tunnel URL in the JSON output if (log.url && log.url.startsWith('https://') && !urlFound) { urlFound = true; instance.setUrl(log.url); resolve(instance); } // Capture errors if (log.err || log.error) { errorOutput += JSON.stringify(log) + '\n'; } } catch (_parseError) { // If not JSON, look for URL in plain text const urlMatch = output.match(/https:\/\/[a-zA-Z0-9-]+\.ngrok(?:-free)?\.app/); if (urlMatch && !urlFound) { urlFound = true; instance.setUrl(urlMatch[0]); resolve(instance); } } } } catch (_parseError) { // If parsing fails, try regex fallback const urlMatch = output.match(/https:\/\/[a-zA-Z0-9-]+\.ngrok(?:-free)?\.app/); if (urlMatch && !urlFound) { urlFound = true; instance.setUrl(urlMatch[0]); resolve(instance); } } // Capture any error messages if (output.toLowerCase().includes('error') || output.toLowerCase().includes('err')) { errorOutput += output; } }; ngrok.stdout.on('data', handleOutput); ngrok.stderr.on('data', handleOutput); ngrok.on('error', (error) => { instance.status = 'error'; const enhancedError = new types_1.TunnelProviderError(this.name, 'NGROK_START_ERROR', `Failed to start ngrok: ${error.message}`, 'Could not start ngrok tunnel. This might be a configuration or authentication issue.', [ 'Check that ngrok is properly installed', 'Verify your ngrok auth token is set: ngrok config add-authtoken YOUR_TOKEN', 'Try running ngrok manually: ngrok http ' + port, 'Check your ngrok account limits and quota' ]); reject(enhancedError); }); ngrok.on('exit', (code, signal) => { if (!urlFound) { instance.status = 'error'; const enhancedError = new types_1.TunnelProviderError(this.name, 'NGROK_EXIT_EARLY', `ngrok exited unexpectedly (code: ${code}, signal: ${signal}). ${errorOutput}`, 'ngrok tunnel exited before establishing connection.', [ 'Check that your ngrok auth token is valid', 'Verify the port is not already in use', 'Check if you have reached your ngrok account limits', 'Review ngrok logs for specific error details' ]); reject(enhancedError); } }); // Timeout if URL not found within 30 seconds setTimeout(() => { if (!urlFound) { ngrok.kill(); const enhancedError = new types_1.TunnelProviderError(this.name, 'NGROK_TIMEOUT', 'Timeout waiting for tunnel URL from ngrok', 'ngrok tunnel took too long to start. This usually indicates an authentication or network issue.', [ 'Check your internet connection', 'Verify your ngrok auth token is correct', 'Try running ngrok manually to test: ngrok http ' + port, 'Check if you have reached your ngrok account connection limits' ]); reject(enhancedError); } }, 30000); }); } } exports.NgrokProvider = NgrokProvider; //# sourceMappingURL=ngrok-provider.js.map