@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
JavaScript
;
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