UNPKG

@nlabs/lex

Version:
810 lines (809 loc) 117 kB
/** * Copyright (c) 2018-Present, Nitrogen Labs, Inc. * Copyrights licensed under the MIT License. See the accompanying LICENSE file for terms. */ import boxen from 'boxen'; import chalk from 'chalk'; import express from 'express'; import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs'; import { homedir } from 'os'; import { resolve as pathResolve, join, isAbsolute } from 'path'; import { pathToFileURL } from 'url'; import { WebSocketServer } from 'ws'; import { LexConfig, getPackageDir } from '../../LexConfig.js'; import { createSpinner, removeFiles } from '../../utils/app.js'; import { log } from '../../utils/log.js'; const getCacheDir = ()=>{ const cacheDir = join(homedir(), '.lex-cache'); if (!existsSync(cacheDir)) { mkdirSync(cacheDir, { recursive: true }); } return cacheDir; }; const getCachePath = ()=>join(getCacheDir(), 'public-ip.json'); const readPublicIpCache = ()=>{ const cachePath = getCachePath(); if (!existsSync(cachePath)) { return null; } try { const cacheData = readFileSync(cachePath, 'utf8'); const cache = JSON.parse(cacheData); // Check if cache is older than 1 week const oneWeekMs = 7 * 24 * 60 * 60 * 1000; if (Date.now() - cache.timestamp > oneWeekMs) { return null; } return cache; } catch { return null; } }; const writePublicIpCache = (ip)=>{ const cachePath = getCachePath(); const cache = { ip, timestamp: Date.now() }; writeFileSync(cachePath, JSON.stringify(cache, null, 2)); }; const fetchPublicIp = (forceRefresh = false)=>new Promise((resolve)=>{ if (!forceRefresh) { const cached = readPublicIpCache(); if (cached) { resolve(cached.ip); return; } } // Use fetch instead of https fetch('https://api.ipify.org').then((res)=>res.text()).then((data)=>{ const ip = data.trim(); if (ip) { writePublicIpCache(ip); } resolve(ip); }).catch(()=>resolve(undefined)); }); const displayServerStatus = (httpPort, httpsPort, wsPort, host, quiet, publicIp)=>{ if (quiet) { return; } const httpUrl = `http://${host}:${httpPort}`; const httpsUrl = `https://${host}:${httpsPort}`; const wsUrl = `ws://${host}:${wsPort}`; const wssUrl = `wss://${host}:${wsPort}`; let urlLines = `${chalk.green('HTTP:')} ${chalk.underline(httpUrl)}\n`; urlLines += `${chalk.green('HTTPS:')} ${chalk.underline(httpsUrl)}\n`; urlLines += `${chalk.green('WebSocket:')} ${chalk.underline(wsUrl)}\n`; urlLines += `${chalk.green('WSS:')} ${chalk.underline(wssUrl)}\n`; if (publicIp) { urlLines += `\n${chalk.green('Public:')} ${chalk.underline(`http://${publicIp}:${httpPort}`)}\n`; } const statusBox = boxen(`${chalk.cyan.bold('🚀 Serverless Development Server Running')}\n\n${urlLines}\n` + `${chalk.yellow('Press Ctrl+C to stop the server')}`, { backgroundColor: '#1a1a1a', borderColor: 'cyan', borderStyle: 'round', margin: 1, padding: 1 }); console.log(`\n${statusBox}\n`); }; const loadHandler = async (handlerPath, outputDir)=>{ try { console.log(`[Serverless] Parsing handler path: ${handlerPath}`); // Parse AWS Lambda handler format: "path/to/file.exportName" or "file.exportName" // Examples: "index.handler", "handlers/api.handler", "src/index.default" const handlerParts = handlerPath.split('.'); console.log('[Serverless] Handler parts after split:', handlerParts); let filePath; let exportName = null; if (handlerParts.length > 1) { // AWS Lambda format: "file.exportName" // Take the last part as export name, rest as file path // Handle cases like "index.handler" -> file: "index", export: "handler" // Or "handlers/api.handler" -> file: "handlers/api", export: "handler" exportName = handlerParts[handlerParts.length - 1] || null; filePath = handlerParts.slice(0, -1).join('.'); console.log(`[Serverless] Parsed AWS Lambda format - filePath: "${filePath}", exportName: "${exportName}"`); } else { // Simple format: just the file path filePath = handlerPath; console.log(`[Serverless] Simple format - filePath: "${filePath}"`); } // Ensure filePath doesn't have the export name in it if (filePath.includes('.handler') || filePath.includes('.default')) { console.error(`[Serverless] WARNING: filePath still contains export name! filePath: "${filePath}"`); // Try to fix it - remove the last part if it looks like an export name const pathParts = filePath.split('.'); if (pathParts.length > 1) { const lastPart = pathParts[pathParts.length - 1]; if ([ 'handler', 'default', 'index' ].includes(lastPart) && !exportName) { exportName = lastPart; filePath = pathParts.slice(0, -1).join('.'); console.log(`[Serverless] Fixed - filePath: "${filePath}", exportName: "${exportName}"`); } } } // Handle both relative paths and absolute paths let fullPath; if (isAbsolute(filePath)) { fullPath = filePath; } else { fullPath = pathResolve(outputDir, filePath); } console.log(`[Serverless] Resolved fullPath (before extensions): ${fullPath}`); // Try different extensions if file doesn't exist if (!existsSync(fullPath)) { const extensions = [ '.js', '.mjs', '.cjs' ]; const pathWithoutExt = fullPath.replace(/\.(js|mjs|cjs)$/, ''); console.log(`[Serverless] Trying extensions. Base path: ${pathWithoutExt}`); for (const ext of extensions){ const candidatePath = pathWithoutExt + ext; console.log(`[Serverless] Checking: ${candidatePath} (exists: ${existsSync(candidatePath)})`); if (existsSync(candidatePath)) { fullPath = candidatePath; console.log(`[Serverless] Found file with extension: ${fullPath}`); break; } } } else { console.log(`[Serverless] File exists without trying extensions: ${fullPath}`); } console.log(`[Serverless] Final fullPath: ${fullPath}`); console.log(`[Serverless] Export name: ${exportName || 'default/handler'}`); console.log(`[Serverless] File exists: ${existsSync(fullPath)}`); if (!existsSync(fullPath)) { console.error(`[Serverless] Handler file not found: ${fullPath}`); console.error(`[Serverless] Output directory: ${outputDir}`); console.error(`[Serverless] Handler path from config: ${handlerPath}`); throw new Error(`Handler file not found: ${fullPath}`); } // Dynamic import of the handler with better error handling // Add .js extension if importing TypeScript compiled output const importPath = fullPath.endsWith('.ts') ? fullPath.replace(/\.ts$/, '.js') : fullPath; try { // Convert to file:// URL for ES module imports (required for absolute paths) // Use pathToFileURL to ensure proper file:// URL format const importUrl = pathToFileURL(importPath).href; console.log(`[Serverless] Importing handler from: ${importUrl}`); console.log(`[Serverless] File path: ${importPath}`); // Use import() with the file URL // Note: If the handler file has import errors (like missing dependencies), // those will surface here, but that's a handler code issue, not a loader issue const handlerModule = await import(importUrl); console.log(`[Serverless] Handler module loaded successfully. Exports: ${Object.keys(handlerModule).join(', ')}`); // Get the handler based on export name or try defaults let handler; if (exportName) { handler = handlerModule[exportName]; if (!handler) { console.error(`[Serverless] Export "${exportName}" not found in module. Available exports: ${Object.keys(handlerModule).join(', ')}`); return null; } } else { // Try default, handler, or the module itself handler = handlerModule.default || handlerModule.handler || handlerModule; } console.log(`[Serverless] Handler found: ${typeof handler}, isFunction: ${typeof handler === 'function'}`); if (typeof handler !== 'function') { console.error(`[Serverless] Handler is not a function. Type: ${typeof handler}, Value:`, handler); return null; } return handler; } catch (importError) { console.error(`[Serverless] Import error for handler ${handlerPath}:`, importError.message); console.error('[Serverless] Import error stack:', importError.stack); // Check if this is a dependency resolution error (common with ES modules) if (importError.message && importError.message.includes('Cannot find module')) { console.error('[Serverless] This appears to be a dependency resolution error.'); console.error('[Serverless] The handler file exists, but one of its imports is failing.'); console.error('[Serverless] Check that all dependencies in the handler file are properly installed.'); console.error(`[Serverless] Handler file: ${importPath}`); console.error('[Serverless] Make sure the handler and its dependencies are compiled correctly.'); } return null; } } catch (error) { console.error(`[Serverless] Error loading handler ${handlerPath}:`, error.message); console.error('[Serverless] Error stack:', error.stack); return null; } }; const captureConsoleLogs = (handler, quiet)=>{ if (quiet) { return handler; } return async (event, context)=>{ // Capture console.log, console.error, etc. const originalConsoleLog = console.log; const originalConsoleError = console.error; const originalConsoleWarn = console.warn; const originalConsoleInfo = console.info; const logs = []; console.log = (...args)=>{ logs.push(`[LOG] ${args.join(' ')}`); originalConsoleLog(...args); }; console.error = (...args)=>{ logs.push(`[ERROR] ${args.join(' ')}`); originalConsoleError(...args); }; console.warn = (...args)=>{ logs.push(`[WARN] ${args.join(' ')}`); originalConsoleWarn(...args); }; console.info = (...args)=>{ logs.push(`[INFO] ${args.join(' ')}`); originalConsoleInfo(...args); }; try { const result = await handler(event, context); // Output captured logs if (logs.length > 0) { console.log(chalk.gray('--- Handler Console Output ---')); logs.forEach((log)=>console.log(chalk.gray(log))); console.log(chalk.gray('--- End Handler Console Output ---')); } return result; } finally{ // Restore original console methods console.log = originalConsoleLog; console.error = originalConsoleError; console.warn = originalConsoleWarn; console.info = originalConsoleInfo; } }; }; const createExpressServer = async (config, outputDir, httpPort, host, quiet, debug)=>{ const app = express(); // Enable CORS app.use((req, res, next)=>{ res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS'); res.header('Access-Control-Allow-Headers', '*'); res.header('Access-Control-Allow-Credentials', 'true'); if (req.method === 'OPTIONS') { res.sendStatus(200); } else { next(); } }); // Parse JSON bodies app.use(express.json()); // Load GraphQL handler const loadGraphQLSchema = async ()=>{ try { // Try to find a GraphQL handler let graphqlHandler = null; if (config.functions) { for (const [functionName, functionConfig] of Object.entries(config.functions)){ if (functionConfig.events) { for (const event of functionConfig.events){ if (event.http && event.http.path) { // Look for GraphQL endpoints if (event.http.path === '/public' || event.http.path === '/graphql') { graphqlHandler = await loadHandler(functionConfig.handler, outputDir); break; } } } } if (graphqlHandler) { break; } } } if (graphqlHandler) { log('Found GraphQL handler', 'info', quiet); return graphqlHandler; } return null; } catch (error) { log(`Error loading GraphQL handler: ${error.message}`, 'error', quiet); return null; } }; // Set up GraphQL handler for GraphQL requests try { const graphqlHandler = await loadGraphQLSchema(); if (graphqlHandler) { // Find the GraphQL path from the serverless config let graphqlPath = '/graphql'; // default fallback if (config.functions) { for (const [_functionName, functionConfig] of Object.entries(config.functions)){ if (functionConfig.events) { for (const event of functionConfig.events){ if (event?.http?.path) { graphqlPath = event.http.path; break; } } } if (graphqlPath !== '/graphql') { break; } } } // Set up GraphQL endpoint with enhanced console.log capture app.use(graphqlPath, async (req, res)=>{ // GraphQL Debug Logging if (debug && req.body && req.body.query) { log('🔍 GraphQL Debug Mode: Analyzing request...', 'info', false); log(`📝 GraphQL Query: ${req.body.query}`, 'info', false); if (req.body.variables) { log(`📊 GraphQL Variables: ${JSON.stringify(req.body.variables, null, 2)}`, 'info', false); } if (req.body.operationName) { log(`🏷️ GraphQL Operation: ${req.body.operationName}`, 'info', false); } } // Enhanced console.log capture const originalConsoleLog = console.log; const logs = []; console.log = (...args)=>{ const logMessage = args.map((arg)=>typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)).join(' '); logs.push(logMessage); originalConsoleLog(`[GraphQL] ${logMessage}`); }; // Create context for the handler const context = { awsRequestId: 'test-request-id', functionName: 'graphql', functionVersion: '$LATEST', getRemainingTimeInMillis: ()=>30000, invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:graphql', logGroupName: '/aws/lambda/graphql', logStreamName: 'test-log-stream', req, res }; // Wrap handler with console log capture const wrappedHandler = captureConsoleLogs(graphqlHandler, quiet); try { // Call the handler with GraphQL parameters const result = await wrappedHandler({ body: JSON.stringify(req.body), headers: req.headers, httpMethod: 'POST', path: graphqlPath, queryStringParameters: {} }, context); // Restore console.log console.log = originalConsoleLog; // Handle the result if (result && typeof result === 'object' && result.statusCode) { res.status(result.statusCode); if (result.headers) { Object.entries(result.headers).forEach(([key, value])=>{ res.setHeader(key, String(value)); }); } res.send(result.body); } else { res.json(result); } } catch (error) { // Restore console.log console.log = originalConsoleLog; log(`GraphQL handler error: ${error.message}`, 'error', false); res.status(500).json({ error: error.message }); } }); log(`GraphQL endpoint available at http://${host}:${httpPort}${graphqlPath}`, 'info', quiet); } } catch (error) { log(`Error setting up GraphQL: ${error.message}`, 'error', quiet); } // Fallback for non-GraphQL routes - handle all remaining routes app.use('/', async (req, res)=>{ try { const url = req.url || '/'; const method = req.method || 'GET'; const pathname = req.path || url.split('?')[0]; // Extract pathname without query string // Always log requests (not affected by quiet flag for debugging) console.log(`[Serverless] ${method} ${url} (pathname: ${pathname})`); // Find matching function let matchedFunction = null; if (config.functions) { const functionNames = Object.keys(config.functions); console.log(`[Serverless] Available functions: ${functionNames.join(', ')}`); console.log('[Serverless] Config functions:', JSON.stringify(config.functions, null, 2)); for (const [functionName, functionConfig] of Object.entries(config.functions)){ if (functionConfig.events) { for (const event of functionConfig.events){ if (event.http) { const eventPath = event.http.path || '/'; const eventMethod = (event.http.method || 'GET').toUpperCase(); const requestMethod = method.toUpperCase(); console.log(`[Serverless] Checking function ${functionName}: path="${eventPath}", method="${eventMethod}" against pathname="${pathname}", method="${requestMethod}"`); // Improved path matching - compare pathname without query string // Normalize paths (remove trailing slashes for comparison) const normalizedEventPath = eventPath.replace(/\/$/, '') || '/'; const normalizedPathname = pathname.replace(/\/$/, '') || '/'; if (normalizedEventPath === normalizedPathname && eventMethod === requestMethod) { matchedFunction = functionName; console.log(`[Serverless] ✓ Matched function: ${matchedFunction}`); break; } } } } if (matchedFunction) { break; } } } else { console.log('[Serverless] No functions found in config'); } if (matchedFunction && config.functions[matchedFunction]) { // Resolve handler path relative to output directory const handlerPath = config.functions[matchedFunction].handler; console.log(`[Serverless] Loading handler: ${handlerPath} from outputDir: ${outputDir}`); const handler = await loadHandler(handlerPath, outputDir); if (handler) { console.log(`[Serverless] Handler loaded successfully, type: ${typeof handler}`); const wrappedHandler = captureConsoleLogs(handler, quiet); const event = { body: req.body, headers: req.headers, httpMethod: method, path: url, queryStringParameters: req.query }; const context = { awsRequestId: 'test-request-id', functionName: matchedFunction, functionVersion: '$LATEST', getRemainingTimeInMillis: ()=>30000, invokedFunctionArn: `arn:aws:lambda:us-east-1:123456789012:function:${matchedFunction}`, logGroupName: `/aws/lambda/${matchedFunction}`, logStreamName: 'test-log-stream', memoryLimitInMB: '128' }; try { console.log('[Serverless] Calling handler with event:', JSON.stringify(event, null, 2)); const result = await wrappedHandler(event, context); console.log('[Serverless] Handler returned:', JSON.stringify(result, null, 2)); if (result && typeof result === 'object' && result.statusCode) { res.status(result.statusCode); if (result.headers) { Object.entries(result.headers).forEach(([key, value])=>{ res.setHeader(key, String(value)); }); } res.send(result.body); } else { res.json(result); } } catch (error) { console.error('[Serverless] Handler error:', error.message); console.error('[Serverless] Handler error stack:', error.stack); log(`Handler error: ${error.message}`, 'error', false); res.status(500).json({ error: error.message }); } } else { console.error(`[Serverless] Handler not found for function: ${matchedFunction}`); console.error(`[Serverless] Handler path: ${handlerPath}, Output dir: ${outputDir}`); res.status(404).json({ error: 'Handler not found' }); } } else { console.error(`[Serverless] Function not found for pathname: ${pathname}, method: ${method}`); console.error(`[Serverless] Available functions: ${config.functions ? Object.keys(config.functions).join(', ') : 'none'}`); res.status(404).json({ error: 'Function not found' }); } } catch (error) { log(`Route handling error: ${error.message}`, 'error', false); res.status(500).json({ error: error.message }); } }); return app; }; const createWebSocketServer = (config, outputDir, wsPort, quiet, debug)=>{ const wss = new WebSocketServer({ port: wsPort }); wss.on('connection', async (ws, req)=>{ log(`WebSocket connection established: ${req.url}`, 'info', false); ws.on('message', async (message)=>{ try { const data = JSON.parse(message.toString()); // Find matching WebSocket function let matchedFunction = null; if (config.functions) { for (const [functionName, functionConfig] of Object.entries(config.functions)){ if (functionConfig.events) { for (const event of functionConfig.events){ if (event.websocket) { const route = event.websocket.route || '$connect'; if (route === '$default' || route === data.action) { matchedFunction = functionName; break; } } } } if (matchedFunction) { break; } } } if (matchedFunction && config.functions[matchedFunction]) { const handler = await loadHandler(config.functions[matchedFunction].handler, outputDir); if (handler) { // Wrap handler with console log capture const wrappedHandler = captureConsoleLogs(handler, quiet); const event = { body: data.body || null, requestContext: { apiGateway: { endpoint: `ws://localhost:${wsPort}` }, connectionId: 'test-connection-id', routeKey: data.action || '$default' } }; const context = { awsRequestId: 'test-request-id', functionName: matchedFunction, functionVersion: '$LATEST', getRemainingTimeInMillis: ()=>30000, invokedFunctionArn: `arn:aws:lambda:us-east-1:123456789012:function:${matchedFunction}`, logGroupName: `/aws/lambda/${matchedFunction}`, logStreamName: 'test-log-stream', memoryLimitInMB: '128' }; const result = await wrappedHandler(event, context); // Handle Lambda response format for WebSocket if (result && typeof result === 'object' && result.statusCode) { // This is a Lambda response object, extract the body const body = result.body || ''; ws.send(body); } else { // This is a direct response, stringify it ws.send(JSON.stringify(result)); } } else { ws.send(JSON.stringify({ error: 'Handler not found' })); } } else { ws.send(JSON.stringify({ error: 'WebSocket function not found' })); } } catch (error) { log(`WebSocket error: ${error.message}`, 'error', false); ws.send(JSON.stringify({ error: error.message })); } }); ws.on('close', ()=>{ log('WebSocket connection closed', 'info', false); }); }); return wss; }; const loadEnvFile = (envPath)=>{ const envVars = {}; if (!existsSync(envPath)) { return envVars; } try { const envContent = readFileSync(envPath, 'utf8'); const lines = envContent.split('\n'); for (const line of lines){ const trimmedLine = line.trim(); // Skip empty lines and comments if (!trimmedLine || trimmedLine.startsWith('#')) { continue; } // Parse KEY=value format const equalIndex = trimmedLine.indexOf('='); if (equalIndex > 0) { const key = trimmedLine.substring(0, equalIndex).trim(); const value = trimmedLine.substring(equalIndex + 1).trim(); // Remove quotes if present const cleanValue = value.replace(/^["']|["']$/g, ''); if (key) { envVars[key] = cleanValue; } } } } catch (error) { log(`Warning: Could not load .env file at ${envPath}: ${error.message}`, 'warn', false); } return envVars; }; export const serverless = async (cmd, callback = ()=>({}))=>{ const { cliName = 'Lex', config, debug = false, host = 'localhost', httpPort = 5000, httpsPort = 5001, quiet = false, remove = false, test = false, usePublicIp, variables, wsPort = 5002 } = cmd; const spinner = createSpinner(quiet); log(`${cliName} starting serverless development server...`, 'info', quiet); await LexConfig.parseConfig(cmd); const { outputFullPath } = LexConfig.config; // Load environment variables from .env files const envPaths = [ pathResolve(process.cwd(), '.env'), pathResolve(process.cwd(), '.env.local'), pathResolve(process.cwd(), '.env.development') ]; let envVars = {}; // Load from .env files in order (later files override earlier ones) for (const envPath of envPaths){ const fileEnvVars = loadEnvFile(envPath); if (Object.keys(fileEnvVars).length > 0) { log(`Loaded environment variables from: ${envPath}`, 'info', quiet); } envVars = { ...envVars, ...fileEnvVars }; } // Start with default NODE_ENV and loaded .env variables let variablesObj = { NODE_ENV: 'development', ...envVars }; // Override with command line variables if provided if (variables) { try { const cliVars = JSON.parse(variables); variablesObj = { ...variablesObj, ...cliVars }; } catch (_error) { log(`\n${cliName} Error: Environment variables option is not a valid JSON object.`, 'error', quiet); callback(1); return 1; } } process.env = { ...process.env, ...variablesObj }; // If in test mode, exit early after loading environment variables if (test) { log('Test mode: Environment variables loaded, exiting', 'info', quiet); callback(0); return 0; } if (remove) { spinner.start('Cleaning output directory...'); await removeFiles(outputFullPath || ''); spinner.succeed('Successfully cleaned output directory!'); } // Load serverless configuration let serverlessConfig = {}; try { // Use getPackageDir to handle npm workspaces correctly const packageDir = getPackageDir(); // Try multiple config file formats const configFormats = config ? [ config ] : [ pathResolve(packageDir, 'lex.config.mjs'), pathResolve(packageDir, 'lex.config.js'), pathResolve(packageDir, 'lex.config.cjs'), pathResolve(packageDir, 'lex.config.ts') ]; let configPath = null; for (const candidatePath of configFormats){ if (existsSync(candidatePath)) { configPath = candidatePath; break; } } if (configPath) { log(`Loading serverless config from: ${configPath}`, 'info', quiet); const configModule = await import(configPath); serverlessConfig = configModule.default?.serverless || configModule.serverless || {}; log('Serverless config loaded successfully', 'info', quiet); const functionNames = Object.keys(serverlessConfig.functions || {}); log(`Loaded functions: ${functionNames.length > 0 ? functionNames.join(', ') : 'none'}`, 'info', quiet); // Debug: Print full config if debug mode if (debug) { log(`Full serverless config: ${JSON.stringify(serverlessConfig, null, 2)}`, 'info', false); } } else { log(`No serverless config found. Tried: ${configFormats.join(', ')}`, 'warn', quiet); } } catch (error) { log(`Error loading serverless config: ${error.message}`, 'error', quiet); if (debug) { log(`Config error stack: ${error.stack}`, 'error', false); } // Don't exit, continue with empty config } // Merge config with command line options const finalConfig = { ...serverlessConfig, custom: { 'serverless-offline': { cors: serverlessConfig.custom?.['serverless-offline']?.cors !== false, host: serverlessConfig.custom?.['serverless-offline']?.host || host, httpPort: serverlessConfig.custom?.['serverless-offline']?.httpPort || httpPort, httpsPort: serverlessConfig.custom?.['serverless-offline']?.httpsPort || httpsPort, wsPort: serverlessConfig.custom?.['serverless-offline']?.wsPort || wsPort } } }; const outputDir = outputFullPath || 'lib'; log(`Using output directory: ${outputDir}`, 'info', quiet); try { spinner.start('Starting serverless development server...'); const httpPort = finalConfig.custom['serverless-offline'].httpPort; const wsPort = finalConfig.custom['serverless-offline'].wsPort; const host = finalConfig.custom['serverless-offline'].host; log(`Creating HTTP server on ${host}:${httpPort}`, 'info', quiet); log(`Creating WebSocket server on port ${wsPort}`, 'info', quiet); // Create Express server const expressApp = await createExpressServer(finalConfig, outputDir, httpPort, host, quiet, debug); // Create WebSocket server const wsServer = createWebSocketServer(finalConfig, outputDir, wsPort, quiet, debug); // Handle server errors wsServer.on('error', (error)=>{ log(`WebSocket server error: ${error.message}`, 'error', quiet); spinner.fail('Failed to start WebSocket server.'); callback(1); return; }); // Start Express server const server = expressApp.listen(httpPort, host, ()=>{ spinner.succeed('Serverless development server started.'); displayServerStatus(httpPort, finalConfig.custom['serverless-offline'].httpsPort, wsPort, host, quiet); fetchPublicIp(usePublicIp).then((publicIp)=>{ if (publicIp) { displayServerStatus(httpPort, finalConfig.custom['serverless-offline'].httpsPort, wsPort, host, quiet, publicIp); } }); }); // Handle Express server errors server.on('error', (error)=>{ log(`Express server error: ${error.message}`, 'error', quiet); spinner.fail('Failed to start Express server.'); callback(1); return; }); // Handle graceful shutdown const shutdown = ()=>{ log('\nShutting down serverless development server...', 'info', quiet); server.close(); wsServer.close(); callback(0); }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); // Keep the process alive process.stdin.resume(); log('Serverless development server is running. Press Ctrl+C to stop.', 'info', quiet); // Don't call callback here, let the process stay alive return 0; } catch (error) { log(`\n${cliName} Error: ${error.message}`, 'error', quiet); spinner.fail('Failed to start serverless development server.'); callback(1); return 1; } }; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9jb21tYW5kcy9zZXJ2ZXJsZXNzL3NlcnZlcmxlc3MudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBDb3B5cmlnaHQgKGMpIDIwMTgtUHJlc2VudCwgTml0cm9nZW4gTGFicywgSW5jLlxuICogQ29weXJpZ2h0cyBsaWNlbnNlZCB1bmRlciB0aGUgTUlUIExpY2Vuc2UuIFNlZSB0aGUgYWNjb21wYW55aW5nIExJQ0VOU0UgZmlsZSBmb3IgdGVybXMuXG4gKi9cbmltcG9ydCBib3hlbiBmcm9tICdib3hlbic7XG5pbXBvcnQgY2hhbGsgZnJvbSAnY2hhbGsnO1xuaW1wb3J0IGV4cHJlc3MgZnJvbSAnZXhwcmVzcyc7XG5pbXBvcnQge3JlYWRGaWxlU3luYywgZXhpc3RzU3luYywgbWtkaXJTeW5jLCB3cml0ZUZpbGVTeW5jfSBmcm9tICdmcyc7XG5pbXBvcnQge2hvbWVkaXJ9IGZyb20gJ29zJztcbmltcG9ydCB7cmVzb2x2ZSBhcyBwYXRoUmVzb2x2ZSwgam9pbiwgaXNBYnNvbHV0ZX0gZnJvbSAncGF0aCc7XG5pbXBvcnQge3BhdGhUb0ZpbGVVUkx9IGZyb20gJ3VybCc7XG5pbXBvcnQge1dlYlNvY2tldFNlcnZlcn0gZnJvbSAnd3MnO1xuXG5pbXBvcnQge0xleENvbmZpZywgZ2V0UGFja2FnZURpcn0gZnJvbSAnLi4vLi4vTGV4Q29uZmlnLmpzJztcbmltcG9ydCB7Y3JlYXRlU3Bpbm5lciwgcmVtb3ZlRmlsZXN9IGZyb20gJy4uLy4uL3V0aWxzL2FwcC5qcyc7XG5pbXBvcnQge2xvZ30gZnJvbSAnLi4vLi4vdXRpbHMvbG9nLmpzJztcblxuZXhwb3J0IGludGVyZmFjZSBTZXJ2ZXJsZXNzT3B0aW9ucyB7XG4gIHJlYWRvbmx5IGNsaU5hbWU/OiBzdHJpbmc7XG4gIHJlYWRvbmx5IGNvbmZpZz86IHN0cmluZztcbiAgcmVhZG9ubHkgZGVidWc/OiBib29sZWFuO1xuICByZWFkb25seSBob3N0Pzogc3RyaW5nO1xuICByZWFkb25seSBodHRwUG9ydD86IG51bWJlcjtcbiAgcmVhZG9ubHkgaHR0cHNQb3J0PzogbnVtYmVyO1xuICByZWFkb25seSBxdWlldD86IGJvb2xlYW47XG4gIHJlYWRvbmx5IHJlbW92ZT86IGJvb2xlYW47XG4gIHJlYWRvbmx5IHRlc3Q/OiBib29sZWFuO1xuICByZWFkb25seSB1c2VQdWJsaWNJcD86IGJvb2xlYW47XG4gIHJlYWRvbmx5IHZhcmlhYmxlcz86IHN0cmluZztcbiAgcmVhZG9ubHkgd3NQb3J0PzogbnVtYmVyO1xufVxuXG5leHBvcnQgdHlwZSBTZXJ2ZXJsZXNzQ2FsbGJhY2sgPSAoc3RhdHVzOiBudW1iZXIpPT4gdm9pZDtcblxuaW50ZXJmYWNlIFB1YmxpY0lwQ2FjaGUge1xuICBpcDogc3RyaW5nO1xuICB0aW1lc3RhbXA6IG51bWJlcjtcbn1cblxuaW50ZXJmYWNlIFNlcnZlcmxlc3NIYW5kbGVyIHtcbiAgcmVhZG9ubHkgaGFuZGxlcjogc3RyaW5nO1xuICByZWFkb25seSBldmVudHM/OiBBcnJheTx7XG4gICAgcmVhZG9ubHkgaHR0cD86IHtcbiAgICAgIHJlYWRvbmx5IGNvcnM/OiBib29sZWFuO1xuICAgICAgcmVhZG9ubHkgbWV0aG9kPzogc3RyaW5nO1xuICAgICAgcmVhZG9ubHkgcGF0aD86IHN0cmluZztcbiAgICB9O1xuICAgIHJlYWRvbmx5IHdlYnNvY2tldD86IHtcbiAgICAgIHJlYWRvbmx5IHJvdXRlPzogc3RyaW5nO1xuICAgIH07XG4gIH0+O1xufVxuXG5pbnRlcmZhY2UgU2VydmVybGVzc0NvbmZpZyB7XG4gIHJlYWRvbmx5IGN1c3RvbT86IHtcbiAgICByZWFkb25seSAnc2VydmVybGVzcy1vZmZsaW5lJz86IHtcbiAgICAgIHJlYWRvbmx5IGNvcnM/OiBib29sZWFuO1xuICAgICAgcmVhZG9ubHkgaG9zdD86IHN0cmluZztcbiAgICAgIHJlYWRvbmx5IGh0dHBQb3J0PzogbnVtYmVyO1xuICAgICAgcmVhZG9ubHkgaHR0cHNQb3J0PzogbnVtYmVyO1xuICAgICAgcmVhZG9ubHkgd3NQb3J0PzogbnVtYmVyO1xuICAgIH07XG4gIH07XG4gIHJlYWRvbmx5IGZ1bmN0aW9ucz86IFJlY29yZDxzdHJpbmcsIFNlcnZlcmxlc3NIYW5kbGVyPjtcbn1cblxuY29uc3QgZ2V0Q2FjaGVEaXIgPSAoKTogc3RyaW5nID0+IHtcbiAgY29uc3QgY2FjaGVEaXIgPSBqb2luKGhvbWVkaXIoKSwgJy5sZXgtY2FjaGUnKTtcbiAgaWYoIWV4aXN0c1N5bmMoY2FjaGVEaXIpKSB7XG4gICAgbWtkaXJTeW5jKGNhY2hlRGlyLCB7cmVjdXJzaXZlOiB0cnVlfSk7XG4gIH1cbiAgcmV0dXJuIGNhY2hlRGlyO1xufTtcblxuY29uc3QgZ2V0Q2FjaGVQYXRoID0gKCk6IHN0cmluZyA9PiBqb2luKGdldENhY2hlRGlyKCksICdwdWJsaWMtaXAuanNvbicpO1xuXG5jb25zdCByZWFkUHVibGljSXBDYWNoZSA9ICgpOiBQdWJsaWNJcENhY2hlIHwgbnVsbCA9PiB7XG4gIGNvbnN0IGNhY2hlUGF0aCA9IGdldENhY2hlUGF0aCgpO1xuICBpZighZXhpc3RzU3luYyhjYWNoZVBhdGgpKSB7XG4gICAgcmV0dXJuIG51bGw7XG4gIH1cblxuICB0cnkge1xuICAgIGNvbnN0IGNhY2hlRGF0YSA9IHJlYWRGaWxlU3luYyhjYWNoZVBhdGgsICd1dGY4Jyk7XG4gICAgY29uc3QgY2FjaGU6IFB1YmxpY0lwQ2FjaGUgPSBKU09OLnBhcnNlKGNhY2hlRGF0YSk7XG5cbiAgICAvLyBDaGVjayBpZiBjYWNoZSBpcyBvbGRlciB0aGFuIDEgd2Vla1xuICAgIGNvbnN0IG9uZVdlZWtNcyA9IDcgKiAyNCAqIDYwICogNjAgKiAxMDAwO1xuICAgIGlmKERhdGUubm93KCkgLSBjYWNoZS50aW1lc3RhbXAgPiBvbmVXZWVrTXMpIHtcbiAgICAgIHJldHVybiBudWxsO1xuICAgIH1cblxuICAgIHJldHVybiBjYWNoZTtcbiAgfSBjYXRjaHtcbiAgICByZXR1cm4gbnVsbDtcbiAgfVxufTtcblxuY29uc3Qgd3JpdGVQdWJsaWNJcENhY2hlID0gKGlwOiBzdHJpbmcpOiB2b2lkID0+IHtcbiAgY29uc3QgY2FjaGVQYXRoID0gZ2V0Q2FjaGVQYXRoKCk7XG4gIGNvbnN0IGNhY2hlOiBQdWJsaWNJcENhY2hlID0ge1xuICAgIGlwLFxuICAgIHRpbWVzdGFtcDogRGF0ZS5ub3coKVxuICB9O1xuICB3cml0ZUZpbGVTeW5jKGNhY2hlUGF0aCwgSlNPTi5zdHJpbmdpZnkoY2FjaGUsIG51bGwsIDIpKTtcbn07XG5cbmNvbnN0IGZldGNoUHVibGljSXAgPSAoZm9yY2VSZWZyZXNoOiBib29sZWFuID0gZmFsc2UpOiBQcm9taXNlPHN0cmluZyB8IHVuZGVmaW5lZD4gPT4gbmV3IFByb21pc2UoKHJlc29sdmUpID0+IHtcbiAgaWYoIWZvcmNlUmVmcmVzaCkge1xuICAgIGNvbnN0IGNhY2hlZCA9IHJlYWRQdWJsaWNJcENhY2hlKCk7XG4gICAgaWYoY2FjaGVkKSB7XG4gICAgICByZXNvbHZlKGNhY2hlZC5pcCk7XG4gICAgICByZXR1cm47XG4gICAgfVxuICB9XG5cbiAgLy8gVXNlIGZldGNoIGluc3RlYWQgb2YgaHR0cHNcbiAgZmV0Y2goJ2h0dHBzOi8vYXBpLmlwaWZ5Lm9yZycpXG4gICAgLnRoZW4oKHJlcykgPT4gcmVzLnRleHQoKSlcbiAgICAudGhlbigoZGF0YSkgPT4ge1xuICAgICAgY29uc3QgaXAgPSBkYXRhLnRyaW0oKTtcbiAgICAgIGlmKGlwKSB7XG4gICAgICAgIHdyaXRlUHVibGljSXBDYWNoZShpcCk7XG4gICAgICB9XG4gICAgICByZXNvbHZlKGlwKTtcbiAgICB9KVxuICAgIC5jYXRjaCgoKSA9PiByZXNvbHZlKHVuZGVmaW5lZCkpO1xufSk7XG5cbmNvbnN0IGRpc3BsYXlTZXJ2ZXJTdGF0dXMgPSAoXG4gIGh0dHBQb3J0OiBudW1iZXIsXG4gIGh0dHBzUG9ydDogbnVtYmVyLFxuICB3c1BvcnQ6IG51bWJlcixcbiAgaG9zdDogc3RyaW5nLFxuICBxdWlldDogYm9vbGVhbixcbiAgcHVibGljSXA/OiBzdHJpbmdcbikgPT4ge1xuICBpZihxdWlldCkge1xuICAgIHJldHVybjtcbiAgfVxuXG4gIGNvbnN0IGh0dHBVcmwgPSBgaHR0cDovLyR7aG9zdH06JHtodHRwUG9ydH1gO1xuICBjb25zdCBodHRwc1VybCA9IGBodHRwczovLyR7aG9zdH06JHtodHRwc1BvcnR9YDtcbiAgY29uc3Qgd3NVcmwgPSBgd3M6Ly8ke2hvc3R9OiR7d3NQb3J0fWA7XG4gIGNvbnN0IHdzc1VybCA9IGB3c3M6Ly8ke2hvc3R9OiR7d3NQb3J0fWA7XG5cbiAgbGV0IHVybExpbmVzID0gYCR7Y2hhbGsuZ3JlZW4oJ0hUVFA6Jyl9ICAgICAgJHtjaGFsay51bmRlcmxpbmUoaHR0cFVybCl9XFxuYDtcbiAgdXJsTGluZXMgKz0gYCR7Y2hhbGsuZ3JlZW4oJ0hUVFBTOicpfSAgICAgJHtjaGFsay51bmRlcmxpbmUoaHR0cHNVcmwpfVxcbmA7XG4gIHVybExpbmVzICs9IGAke2NoYWxrLmdyZWVuKCdXZWJTb2NrZXQ6Jyl9ICR7Y2hhbGsudW5kZXJsaW5lKHdzVXJsKX1cXG5gO1xuICB1cmxMaW5lcyArPSBgJHtjaGFsay5ncmVlbignV1NTOicpfSAgICAgICAke2NoYWxrLnVuZGVybGluZSh3c3NVcmwpfVxcbmA7XG5cbiAgaWYocHVibGljSXApIHtcbiAgICB1cmxMaW5lcyArPSBgXFxuJHtjaGFsay5ncmVlbignUHVibGljOicpfSAgICAke2NoYWxrLnVuZGVybGluZShgaHR0cDovLyR7cHVibGljSXB9OiR7aHR0cFBvcnR9YCl9XFxuYDtcbiAgfVxuXG4gIGNvbnN0IHN0YXR1c0JveCA9IGJveGVuKFxuICAgIGAke2NoYWxrLmN5YW4uYm9sZCgn8J+agCBTZXJ2ZXJsZXNzIERldmVsb3BtZW50IFNlcnZlciBSdW5uaW5nJyl9XFxuXFxuJHt1cmxMaW5lc31cXG5gICtcbiAgICBgJHtjaGFsay55ZWxsb3coJ1ByZXNzIEN0cmwrQyB0byBzdG9wIHRoZSBzZXJ2ZXInKX1gLFxuICAgIHtcbiAgICAgIGJhY2tncm91bmRDb2xvcjogJyMxYTFhMWEnLFxuICAgICAgYm9yZGVyQ29sb3I6ICdjeWFuJyxcbiAgICAgIGJvcmRlclN0eWxlOiAncm91bmQnLFxuICAgICAgbWFyZ2luOiAxLFxuICAgICAgcGFkZGluZzogMVxuICAgIH1cbiAgKTtcblxuICBjb25zb2xlLmxvZyhgXFxuJHtzdGF0dXNCb3h9XFxuYCk7XG59O1xuXG5jb25zdCBsb2FkSGFuZGxlciA9IGFzeW5jIChoYW5kbGVyUGF0aDogc3RyaW5nLCBvdXRwdXREaXI6IHN0cmluZykgPT4ge1xuICB0cnkge1xuICAgIGNvbnNvbGUubG9nKGBbU2VydmVybGVzc10gUGFyc2luZyBoYW5kbGVyIHBhdGg6ICR7aGFuZGxlclBhdGh9YCk7XG5cbiAgICAvLyBQYXJzZSBBV1MgTGFtYmRhIGhhbmRsZXIgZm9ybWF0OiBcInBhdGgvdG8vZmlsZS5leHBvcnROYW1lXCIgb3IgXCJmaWxlLmV4cG9ydE5hbWVcIlxuICAgIC8vIEV4YW1wbGVzOiBcImluZGV4LmhhbmRsZXJcIiwgXCJoYW5kbGVycy9hcGkuaGFuZGxlclwiLCBcInNyYy9pbmRleC5kZWZhdWx0XCJcbiAgICBjb25zdCBoYW5kbGVyUGFydHMgPSBoYW5kbGVyUGF0aC5zcGxpdCgnLicpO1xuICAgIGNvbnNvbGUubG9nKCdbU2VydmVybGVzc10gSGFuZGxlciBwYXJ0cyBhZnRlciBzcGxpdDonLCBoYW5kbGVyUGFydHMpO1xuXG4gICAgbGV0IGZpbGVQYXRoOiBzdHJpbmc7XG4gICAgbGV0IGV4cG9ydE5hbWU6IHN0cmluZyB8IG51bGwgPSBudWxsO1xuXG4gICAgaWYoaGFuZGxlclBhcnRzLmxlbmd0aCA+IDEpIHtcbiAgICAgIC8vIEFXUyBMYW1iZGEgZm9ybWF0OiBcImZpbGUuZXhwb3J0TmFtZVwiXG4gICAgICAvLyBUYWtlIHRoZSBsYXN0IHBhcnQgYXMgZXhwb3J0IG5hbWUsIHJlc3QgYXMgZmlsZSBwYXRoXG4gICAgICAvLyBIYW5kbGUgY2FzZXMgbGlrZSBcImluZGV4LmhhbmRsZXJcIiAtPiBmaWxlOiBcImluZGV4XCIsIGV4cG9ydDogXCJoYW5kbGVyXCJcbiAgICAgIC8vIE9yIFwiaGFuZGxlcnMvYXBpLmhhbmRsZXJcIiAtPiBmaWxlOiBcImhhbmRsZXJzL2FwaVwiLCBleHBvcnQ6IFwiaGFuZGxlclwiXG4gICAgICBleHBvcnROYW1lID0gaGFuZGxlclBhcnRzW2hhbmRsZXJQYXJ0cy5sZW5ndGggLSAxXSB8fCBudWxsO1xuICAgICAgZmlsZVBhdGggPSBoYW5kbGVyUGFydHMuc2xpY2UoMCwgLTEpLmpvaW4oJy4nKTtcbiAgICAgIGNvbnNvbGUubG9nKGBbU2VydmVybGVzc10gUGFyc2VkIEFXUyBMYW1iZGEgZm9ybWF0IC0gZmlsZVBhdGg6IFwiJHtmaWxlUGF0aH1cIiwgZXhwb3J0TmFtZTogXCIke2V4cG9ydE5hbWV9XCJgKTtcbiAgICB9IGVsc2Uge1xuICAgICAgLy8gU2ltcGxlIGZvcm1hdDoganVzdCB0aGUgZmlsZSBwYXRoXG4gICAgICBmaWxlUGF0aCA9IGhhbmRsZXJQYXRoO1xuICAgICAgY29uc29sZS5sb2coYFtTZXJ2ZXJsZXNzXSBTaW1wbGUgZm9ybWF0IC0gZmlsZVBhdGg6IFwiJHtmaWxlUGF0aH1cImApO1xuICAgIH1cblxuICAgIC8vIEVuc3VyZSBmaWxlUGF0aCBkb2Vzbid0IGhhdmUgdGhlIGV4cG9ydCBuYW1lIGluIGl0XG4gICAgaWYoZmlsZVBhdGguaW5jbHVkZXMoJy5oYW5kbGVyJykgfHwgZmlsZVBhdGguaW5jbHVkZXMoJy5kZWZhdWx0JykpIHtcbiAgICAgIGNvbnNvbGUuZXJyb3IoYFtTZXJ2ZXJsZXNzXSBXQVJOSU5HOiBmaWxlUGF0aCBzdGlsbCBjb250YWlucyBleHBvcnQgbmFtZSEgZmlsZVBhdGg6IFwiJHtmaWxlUGF0aH1cImApO1xuICAgICAgLy8gVHJ5IHRvIGZpeCBpdCAtIHJlbW92ZSB0aGUgbGFzdCBwYXJ0IGlmIGl0IGxvb2tzIGxpa2UgYW4gZXhwb3J0IG5hbWVcbiAgICAgIGNvbnN0IHBhdGhQYXJ0cyA9IGZpbGVQYXRoLnNwbGl0KCcuJyk7XG4gICAgICBpZihwYXRoUGFydHMubGVuZ3RoID4gMSkge1xuICAgICAgICBjb25zdCBsYXN0UGFydCA9IHBhdGhQYXJ0c1twYXRoUGFydHMubGVuZ3RoIC0gMV07XG4gICAgICAgIGlmKFsnaGFuZGxlcicsICdkZWZhdWx0JywgJ2luZGV4J10uaW5jbHVkZXMobGFzdFBhcnQpICYmICFleHBvcnROYW1lKSB7XG4gICAgICAgICAgZXhwb3J0TmFtZSA9IGxhc3RQYXJ0O1xuICAgICAgICAgIGZpbGVQYXRoID0gcGF0aFBhcnRzLnNsaWNlKDAsIC0xKS5qb2luKCcuJyk7XG4gICAgICAgICAgY29uc29sZS5sb2coYFtTZXJ2ZXJsZXNzXSBGaXhlZCAtIGZpbGVQYXRoOiBcIiR7ZmlsZVBhdGh9XCIsIGV4cG9ydE5hbWU6IFwiJHtleHBvcnROYW1lfVwiYCk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG5cbiAgICAvLyBIYW5kbGUgYm90aCByZWxhdGl2ZSBwYXRocyBhbmQgYWJzb2x1dGUgcGF0aHNcbiAgICBsZXQgZnVsbFBhdGg6IHN0cmluZztcbiAgICBpZihpc0Fic29sdXRlKGZpbGVQYXRoKSkge1xuICAgICAgZnVsbFBhdGggPSBmaWxlUGF0aDtcbiAgICB9IGVsc2Uge1xuICAgICAgZnVsbFBhdGggPSBwYXRoUmVzb2x2ZShvdXRwdXREaXIsIGZpbGVQYXRoKTtcbiAgICB9XG4gICAgY29uc29sZS5sb2coYFtTZXJ2ZXJsZXNzXSBSZXNvbHZlZCBmdWxsUGF0aCAoYmVmb3JlIGV4dGVuc2lvbnMpOiAke2Z1bGxQYXRofWApO1xuXG4gICAgLy8gVHJ5IGRpZmZlcmVudCBleHRlbnNpb25zIGlmIGZpbGUgZG9lc24ndCBleGlzdFxuICAgIGlmKCFleGlzdHNTeW5jKGZ1bGxQYXRoKSkge1xuICAgICAgY29uc3QgZXh0ZW5zaW9ucyA9IFsnLmpzJywgJy5tanMnLCAnLmNqcyddO1xuICAgICAgY29uc3QgcGF0aFdpdGhvdXRFeHQgPSBmdWxsUGF0aC5yZXBsYWNlKC9cXC4oanN8bWpzfGNqcykkLywgJycpO1xuICAgICAgY29uc29sZS5sb2coYFtTZXJ2ZXJsZXNzXSBUcnlpbmcgZXh0ZW5zaW9ucy4gQmFzZSBwYXRoOiAke3BhdGhXaXRob3V0RXh0fWApO1xuICAgICAgZm9yKGNvbnN0IGV4dCBvZiBleHRlbnNpb25zKSB7XG4gICAgICAgIGNvbnN0IGNhbmRpZGF0ZVBhdGggPSBwYXRoV2l0aG91dEV4dCArIGV4dDtcbiAgICAgICAgY29uc29sZS5sb2coYFtTZXJ2ZXJsZXNzXSBDaGVja2luZzogJHtjYW5kaWRhdGVQYXRofSAoZXhpc3RzOiAke2V4aXN0c1N5bmMoY2FuZGlkYXRlUGF0aCl9KWApO1xuICAgICAgICBpZihleGlzdHNTeW5jKGNhbmRpZGF0ZVBhdGgpKSB7XG4gICAgICAgICAgZnVsbFBhdGggPSBjYW5kaWRhdGVQYXRoO1xuICAgICAgICAgIGNvbnNvbGUubG9nKGBbU2VydmVybGVzc10gRm91bmQgZmlsZSB3aXRoIGV4dGVuc2lvbjogJHtmdWxsUGF0aH1gKTtcbiAgICAgICAgICBicmVhaztcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH0gZWxzZSB7XG4gICAgICBjb25zb2xlLmxvZyhgW1NlcnZlcmxlc3NdIEZpbGUgZXhpc3RzIHdpdGhvdXQgdHJ5aW5nIGV4dGVuc2lvbnM6ICR7ZnVsbFBhdGh9YCk7XG4gICAgfVxuXG4gICAgY29uc29sZS5sb2coYFtTZXJ2ZXJsZXNzXSBGaW5hbCBmdWxsUGF0aDogJHtmdWxsUGF0aH1gKTtcbiAgICBjb25zb2xlLmxvZyhgW1NlcnZlcmxlc3NdIEV4cG9ydCBuYW1lOiAke2V4cG9ydE5hbWUgfHwgJ2RlZmF1bHQvaGFuZGxlcid9YCk7XG4gICAgY29uc29sZS5sb2coYFtTZXJ2ZXJsZXNzXSBGaWxlIGV4aXN0czogJHtleGlzdHNTeW5jKGZ1bGxQYXRoKX1gKTtcblxuICAgIGlmKCFleGlzdHNTeW5jKGZ1bGxQYXRoKSkge1xuICAgICAgY29uc29sZS5lcnJvcihgW1NlcnZlcmxlc3NdIEhhbmRsZXIgZmlsZSBub3QgZm91bmQ6ICR7ZnVsbFBhdGh9YCk7XG4gICAgICBjb25zb2xlLmVycm9yKGBbU2VydmVybGVzc10gT3V0cHV0IGRpcmVjdG9yeTogJHtvdXRwdXREaXJ9YCk7XG4gICAgICBjb25zb2xlLmVycm9yKGBbU2VydmVybGVzc10gSGFuZGxlciBwYXRoIGZyb20gY29uZmlnOiAke2hhbmRsZXJQYXRofWApO1xuICAgICAgdGhyb3cgbmV3IEVycm9yKGBIYW5kbGVyIGZpbGUgbm90IGZvdW5kOiAke2Z1bGxQYXRofWApO1xuICAgIH1cblxuICAgIC8vIER5bmFtaWMgaW1wb3J0IG9mIHRoZSBoYW5kbGVyIHdpdGggYmV0dGVyIGVycm9yIGhhbmRsaW5nXG4gICAgLy8gQWRkIC5qcyBleHRlbnNpb24gaWYgaW1wb3J0aW5nIFR5cGVTY3JpcHQgY29tcGlsZWQgb3V0cHV0XG4gICAgY29uc3QgaW1wb3J0UGF0aCA9IGZ1bGxQYXRoLmVuZHNXaXRoKCcudHMnKSA/IGZ1bGxQYXRoLnJlcGxhY2UoL1xcLnRzJC8sICcuanMnKSA6IGZ1bGxQYXRoO1xuXG4gICAgdHJ5IHtcbiAgICAgIC8vIENvbnZlcnQgdG8gZmlsZTovLyBVUkwgZm9yIEVTIG1vZHVsZSBpbXBvcnRzIChyZXF1aXJlZCBmb3IgYWJzb2x1dGUgcGF0aHMpXG4gICAgICAvLyBVc2UgcGF0aFRvRmlsZVVSTCB0byBlbnN1cmUgcHJvcGVyIGZpbGU6Ly8gVVJMIGZvcm1hdFxuICAgICAgY29uc3QgaW1wb3J0VXJsID0gcGF0aFRvRmlsZVVSTChpbXBvcnRQYXRoKS5ocmVmO1xuICAgICAgY29uc29sZS5sb2coYFtTZXJ2ZXJsZXNzXSBJbXBvcnRpbmcgaGFuZGxlciBmcm9tOiAke2ltcG9ydFVybH1gKTtcbiAgICAgIGNvbnNvbGUubG9nKGBbU2VydmVybGVzc10gRmlsZSBwYXRoOiAke2ltcG9ydFBhdGh9YCk7XG5cbiAgICAgIC8vIFVzZSBpbXBvcnQoKSB3aXRoIHRoZSBmaWxlIFVSTFxuICAgICAgLy8gTm90ZTogSWYgdGhlIGhhbmRsZXIgZmlsZSBoYXMgaW1wb3J0IGVycm9ycyAobGlrZSBtaXNzaW5nIGRlcGVuZGVuY2llcyksXG4gICAgICAvLyB0aG9zZSB3aWxsIHN1cmZhY2UgaGVyZSwgYnV0IHRoYXQncyBhIGhhbmRsZXIgY29kZSBpc3N1ZSwgbm90IGEgbG9hZGVyIGlzc3VlXG4gICAgICBjb25zdCBoYW5kbGVyTW9kdWxlID0gYXdhaXQgaW1wb3J0KGltcG9ydFVybCk7XG