@curl-runner/cli 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
- var h=Object.create;var{getPrototypeOf:b,defineProperty:N,getOwnPropertyNames:C}=Object;var v=Object.prototype.hasOwnProperty;var V=($,w,K)=>{K=$!=null?h(b($)):{};let X=w||!$||!$.__esModule?N(K,"default",{value:$,enumerable:!0}):K;for(let Q of C($))if(!v.call(X,Q))N(X,Q,{get:()=>$[Q],enumerable:!0});return X};var j=import.meta.require;var{Glob:d}=globalThis.Bun;class L{static buildCommand($){let w=["curl"];if(w.push("-X",$.method||"GET"),w.push("-w",'"\\n__CURL_METRICS_START__%{json}__CURL_METRICS_END__"'),$.headers)for(let[X,Q]of Object.entries($.headers))w.push("-H",`"${X}: ${Q}"`);if($.auth){if($.auth.type==="basic"&&$.auth.username&&$.auth.password)w.push("-u",`"${$.auth.username}:${$.auth.password}"`);else if($.auth.type==="bearer"&&$.auth.token)w.push("-H",`"Authorization: Bearer ${$.auth.token}"`)}if($.body){let X=typeof $.body==="string"?$.body:JSON.stringify($.body);if(w.push("-d",`'${X.replace(/'/g,"'\\''")}'`),!$.headers?.["Content-Type"])w.push("-H",'"Content-Type: application/json"')}if($.timeout)w.push("--max-time",$.timeout.toString());if($.followRedirects!==!1){if(w.push("-L"),$.maxRedirects)w.push("--max-redirs",$.maxRedirects.toString())}if($.proxy)w.push("-x",$.proxy);if($.insecure)w.push("-k");if($.output)w.push("-o",$.output);w.push("-s","-S");let K=$.url;if($.params&&Object.keys($.params).length>0){let X=new URLSearchParams($.params).toString();K+=(K.includes("?")?"&":"?")+X}return w.push(`"${K}"`),w.join(" ")}static async executeCurl($){try{let w=Bun.spawn(["sh","-c",$],{stdout:"pipe",stderr:"pipe"}),K=await new Response(w.stdout).text(),X=await new Response(w.stderr).text();if(await w.exited,w.exitCode!==0&&!K)return{success:!1,error:X||`Command failed with exit code ${w.exitCode}`};let Q=K,Z={},W=K.match(/__CURL_METRICS_START__(.+?)__CURL_METRICS_END__/);if(W){Q=K.replace(/__CURL_METRICS_START__.+?__CURL_METRICS_END__/,"").trim();try{Z=JSON.parse(W[1])}catch(D){}}let z={};if(Z.response_code){let D=X.split(`
4
- `).filter((G)=>G.includes(":"));for(let G of D){let[O,...J]=G.split(":");if(O&&J.length>0)z[O.trim()]=J.join(":").trim()}}return{success:!0,status:Z.response_code||Z.http_code,headers:z,body:Q,metrics:{duration:(Z.time_total||0)*1000,size:Z.size_download,dnsLookup:(Z.time_namelookup||0)*1000,tcpConnection:(Z.time_connect||0)*1000,tlsHandshake:(Z.time_appconnect||0)*1000,firstByte:(Z.time_starttransfer||0)*1000,download:(Z.time_total||0)*1000}}}catch(w){return{success:!1,error:w instanceof Error?w.message:String(w)}}}}class Y{config;colors={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",underscore:"\x1B[4m",black:"\x1B[30m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",cyan:"\x1B[36m",white:"\x1B[37m",bgBlack:"\x1B[40m",bgRed:"\x1B[41m",bgGreen:"\x1B[42m",bgYellow:"\x1B[43m",bgBlue:"\x1B[44m",bgMagenta:"\x1B[45m",bgCyan:"\x1B[46m",bgWhite:"\x1B[47m"};constructor($={}){this.config={verbose:!1,showHeaders:!1,showBody:!0,showMetrics:!1,format:"pretty",prettyLevel:"standard",...$}}color($,w){return`${this.colors[w]}${$}${this.colors.reset}`}getShortFilename($){return $.replace(/.*\//,"").replace(".yaml","")}shouldShowOutput(){if(this.config.format==="raw")return!1;if(this.config.format==="pretty")return!0;return this.config.verbose!==!1}shouldShowHeaders(){if(this.config.format!=="pretty")return this.config.showHeaders||!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.showHeaders||!1;case"detailed":return!0;default:return this.config.showHeaders||!1}}shouldShowBody(){if(this.config.format!=="pretty")return this.config.showBody!==!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.showBody!==!1;case"detailed":return!0;default:return this.config.showBody!==!1}}shouldShowMetrics(){if(this.config.format!=="pretty")return this.config.showMetrics||!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.showMetrics||!1;case"detailed":return!0;default:return this.config.showMetrics||!1}}shouldShowRequestDetails(){if(this.config.format!=="pretty")return this.config.verbose||!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.verbose||!1;case"detailed":return!0;default:return this.config.verbose||!1}}shouldShowSeparators(){if(this.config.format!=="pretty")return!0;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return!0;case"detailed":return!0;default:return!0}}colorStatusCode($){return this.color($,"yellow")}logValidationErrors($){let w=$.split("; ");if(w.length===1){let K=w[0].trim(),X=K.match(/^Expected status (.+?), got (.+)$/);if(X){let[,Q,Z]=X,W=this.colorStatusCode(Q.replace(" or ","|")),z=this.color(Z,"red");console.log(` ${this.color("\u2717","red")} ${this.color("Error:","red")} Expected ${this.color("status","yellow")} ${W}, got ${z}`)}else console.log(` ${this.color("\u2717","red")} ${this.color("Error:","red")} ${K}`)}else{console.log(` ${this.color("\u2717","red")} ${this.color("Validation Errors:","red")}`);for(let K of w){let X=K.trim();if(X)if(X.startsWith("Expected ")){let Q=X.match(/^Expected status (.+?), got (.+)$/);if(Q){let[,Z,W]=Q,z=this.colorStatusCode(Z.replace(" or ","|")),D=this.color(W,"red");console.log(` ${this.color("\u2022","red")} ${this.color("status","yellow")}: expected ${z}, got ${D}`)}else{let Z=X.match(/^Expected (.+?) to be (.+?), got (.+)$/);if(Z){let[,W,z,D]=Z;console.log(` ${this.color("\u2022","red")} ${this.color(W,"yellow")}: expected ${this.color(z,"green")}, got ${this.color(D,"red")}`)}else console.log(` ${this.color("\u2022","red")} ${X}`)}}else console.log(` ${this.color("\u2022","red")} ${X}`)}}}formatJson($){if(this.config.format==="raw")return typeof $==="string"?$:JSON.stringify($);if(this.config.format==="json")return JSON.stringify($);return JSON.stringify($,null,2)}formatDuration($){if($<1000)return`${$.toFixed(0)}ms`;return`${($/1000).toFixed(2)}s`}formatSize($){if(!$)return"0 B";let w=["B","KB","MB","GB"],K=Math.floor(Math.log($)/Math.log(1024));return`${($/1024**K).toFixed(2)} ${w[K]}`}printSeparator($="\u2500",w=60){console.log(this.color($.repeat(w),"dim"))}logExecutionStart($,w){if(!this.shouldShowOutput())return;if(this.shouldShowSeparators())this.printSeparator("\u2550"),console.log(this.color("\uD83D\uDE80 CURL RUNNER","bright")),console.log(this.color(`Executing ${$} request(s) in ${w} mode`,"cyan")),this.printSeparator("\u2550"),console.log()}logRequestStart($,w){if(!this.shouldShowOutput())return;let K=$.name||`Request #${w}`,X=$.sourceFile?` ${this.color(`[${this.getShortFilename($.sourceFile)}]`,"cyan")}`:"";if(console.log(this.color(`\u25B6 ${K}`,"bright")+X),console.log(` ${this.color($.method||"GET","yellow")} ${this.color($.url,"blue")}`),this.shouldShowRequestDetails()&&$.headers&&Object.keys($.headers).length>0){console.log(this.color(" Headers:","dim"));for(let[Q,Z]of Object.entries($.headers))console.log(` ${Q}: ${Z}`)}if(this.shouldShowRequestDetails()&&$.body){console.log(this.color(" Body:","dim"));let Q=this.formatJson($.body);for(let Z of Q.split(`
5
- `))console.log(` ${Z}`)}}logCommand($){if(this.shouldShowRequestDetails())console.log(this.color(" Command:","dim")),console.log(this.color(` ${$}`,"dim"))}logRetry($,w){console.log(this.color(` \u21BB Retry ${$}/${w}...`,"yellow"))}logRequestComplete($){if(this.config.format==="raw"){if($.success&&this.config.showBody&&$.body){let X=this.formatJson($.body);console.log(X)}return}if(this.config.format==="json"){let X={request:{name:$.request.name,url:$.request.url,method:$.request.method||"GET"},success:$.success,status:$.status,...this.shouldShowHeaders()&&$.headers?{headers:$.headers}:{},...this.shouldShowBody()&&$.body?{body:$.body}:{},...$.error?{error:$.error}:{},...this.shouldShowMetrics()&&$.metrics?{metrics:$.metrics}:{}};console.log(JSON.stringify(X,null,2));return}if(!this.shouldShowOutput())return;let w=$.success?"green":"red",K=$.success?"\u2713":"\u2717";if(console.log(` ${this.color(K,w)} Status: ${this.color(String($.status||"ERROR"),w)}`),$.error)this.logValidationErrors($.error);if(this.shouldShowMetrics()&&$.metrics){let X=$.metrics,Q=[`Duration: ${this.color(this.formatDuration(X.duration),"cyan")}`];if(X.size!==void 0)Q.push(`Size: ${this.color(this.formatSize(X.size),"cyan")}`);if(this.shouldShowRequestDetails()){if(X.dnsLookup)Q.push(`DNS: ${this.formatDuration(X.dnsLookup)}`);if(X.tcpConnection)Q.push(`TCP: ${this.formatDuration(X.tcpConnection)}`);if(X.tlsHandshake)Q.push(`TLS: ${this.formatDuration(X.tlsHandshake)}`);if(X.firstByte)Q.push(`TTFB: ${this.formatDuration(X.firstByte)}`)}console.log(` ${Q.join(" | ")}`)}if(this.shouldShowHeaders()&&$.headers&&Object.keys($.headers).length>0){console.log(this.color(" Response Headers:","dim"));for(let[X,Q]of Object.entries($.headers))console.log(` ${X}: ${Q}`)}if(this.shouldShowBody()&&$.body){console.log(this.color(" Response Body:","dim"));let Q=this.formatJson($.body).split(`
6
- `),Z=this.shouldShowRequestDetails()?1/0:10;for(let W of Q.slice(0,Z))console.log(` ${W}`);if(Q.length>Z)console.log(this.color(` ... (${Q.length-Z} more lines)`,"dim"))}console.log()}logSummary($,w=!1){if(this.config.format==="raw")return;if(this.config.format==="json"){let Q={summary:{total:$.total,successful:$.successful,failed:$.failed,duration:$.duration},results:$.results.map((Z)=>({request:{name:Z.request.name,url:Z.request.url,method:Z.request.method||"GET"},success:Z.success,status:Z.status,...this.shouldShowHeaders()&&Z.headers?{headers:Z.headers}:{},...this.shouldShowBody()&&Z.body?{body:Z.body}:{},...Z.error?{error:Z.error}:{},...this.shouldShowMetrics()&&Z.metrics?{metrics:Z.metrics}:{}}))};console.log(JSON.stringify(Q,null,2));return}if(!this.shouldShowOutput())return;if(this.shouldShowSeparators()){this.printSeparator("\u2550");let Q=w?"\uD83C\uDFAF OVERALL SUMMARY":"\uD83D\uDCCA EXECUTION SUMMARY";console.log(this.color(Q,"bright")),this.printSeparator()}let K=($.successful/$.total*100).toFixed(1),X=$.failed===0?"green":$.successful===0?"red":"yellow";if(console.log(` Total Requests: ${this.color(String($.total),"cyan")}`),console.log(` Successful: ${this.color(String($.successful),"green")}`),console.log(` Failed: ${this.color(String($.failed),"red")}`),console.log(` Success Rate: ${this.color(`${K}%`,X)}`),console.log(` Total Duration: ${this.color(this.formatDuration($.duration),"cyan")}`),$.failed>0&&this.shouldShowRequestDetails())console.log(),console.log(this.color(" Failed Requests:","red")),$.results.filter((Q)=>!Q.success).forEach((Q)=>{let Z=Q.request.name||Q.request.url;console.log(` \u2022 ${Z}: ${Q.error}`)});if(this.shouldShowSeparators())this.printSeparator("\u2550")}logError($){console.error(this.color(`\u274C ${$}`,"red"))}logWarning($){console.warn(this.color(`\u26A0\uFE0F ${$}`,"yellow"))}logInfo($){console.log(this.color(`\u2139\uFE0F ${$}`,"blue"))}logSuccess($){console.log(this.color(`\u2705 ${$}`,"green"))}logFileHeader($,w){if(!this.shouldShowOutput()||this.config.format!=="pretty")return;let K=$.replace(/.*\//,"").replace(".yaml","");console.log(),this.printSeparator("\u2500"),console.log(this.color(`\uD83D\uDCC4 ${K}.yaml`,"bright")+this.color(` (${w} request${w===1?"":"s"})`,"dim")),this.printSeparator("\u2500")}}class A{logger;globalConfig;constructor($={}){this.globalConfig=$,this.logger=new Y($.output)}mergeOutputConfig($){return{...this.globalConfig.output,...$.sourceOutputConfig}}async executeRequest($,w=0){let K=performance.now(),X=this.mergeOutputConfig($),Q=new Y(X);Q.logRequestStart($,w);let Z=L.buildCommand($);Q.logCommand(Z);let W=0,z,D=($.retry?.count||0)+1;while(W<D){if(W>0){if(Q.logRetry(W,D-1),$.retry?.delay)await Bun.sleep($.retry.delay)}let O=await L.executeCurl(Z);if(O.success){let J=O.body;try{if(O.headers?.["content-type"]?.includes("application/json")||J&&(J.trim().startsWith("{")||J.trim().startsWith("[")))J=JSON.parse(J)}catch(T){}let _={request:$,success:!0,status:O.status,headers:O.headers,body:J,metrics:{...O.metrics,duration:performance.now()-K}};if($.expect){let T=this.validateResponse(_,$.expect);if(!T.success)_.success=!1,_.error=T.error}return Q.logRequestComplete(_),_}z=O.error,W++}let G={request:$,success:!1,error:z,metrics:{duration:performance.now()-K}};return Q.logRequestComplete(G),G}validateResponse($,w){if(!w)return{success:!0};let K=[];if(w.status!==void 0){let Q=Array.isArray(w.status)?w.status:[w.status];if(!Q.includes($.status||0))K.push(`Expected status ${Q.join(" or ")}, got ${$.status}`)}if(w.headers)for(let[Q,Z]of Object.entries(w.headers)){let W=$.headers?.[Q]||$.headers?.[Q.toLowerCase()];if(W!==Z)K.push(`Expected header ${Q}="${Z}", got "${W}"`)}if(w.body!==void 0){let Q=this.validateBodyProperties($.body,w.body,"");if(Q.length>0)K.push(...Q)}if(w.responseTime!==void 0&&$.metrics){let Q=$.metrics.duration;if(!this.validateRangePattern(Q,w.responseTime))K.push(`Expected response time to match ${w.responseTime}ms, got ${Q.toFixed(2)}ms`)}let X=K.length>0;if(w.failure===!0){if(X)return{success:!1,error:K.join("; ")};let Q=$.status||0;if(Q>=400)return{success:!0};else return{success:!1,error:`Expected request to fail (4xx/5xx) but got status ${Q}`}}else if(X)return{success:!1,error:K.join("; ")};else return{success:!0}}validateBodyProperties($,w,K){let X=[];if(typeof w!=="object"||w===null){let Q=this.validateValue($,w,K||"body");if(!Q.isValid)X.push(Q.error);return X}if(Array.isArray(w)){let Q=this.validateValue($,w,K||"body");if(!Q.isValid)X.push(Q.error);return X}for(let[Q,Z]of Object.entries(w)){let W=K?`${K}.${Q}`:Q,z;if(Array.isArray($)&&this.isArraySelector(Q))z=this.getArrayValue($,Q);else z=$?.[Q];if(typeof Z==="object"&&Z!==null&&!Array.isArray(Z)){let D=this.validateBodyProperties(z,Z,W);X.push(...D)}else{let D=this.validateValue(z,Z,W);if(!D.isValid)X.push(D.error)}}return X}validateValue($,w,K){if(w==="*")return{isValid:!0};if(Array.isArray(w)){if(!w.some((Q)=>{if(Q==="*")return!0;if(typeof Q==="string"&&this.isRegexPattern(Q))return this.validateRegexPattern($,Q);if(typeof Q==="string"&&this.isRangePattern(Q))return this.validateRangePattern($,Q);return $===Q}))return{isValid:!1,error:`Expected ${K} to match one of ${JSON.stringify(w)}, got ${JSON.stringify($)}`};return{isValid:!0}}if(typeof w==="string"&&this.isRegexPattern(w)){if(!this.validateRegexPattern($,w))return{isValid:!1,error:`Expected ${K} to match pattern ${w}, got ${JSON.stringify($)}`};return{isValid:!0}}if(typeof w==="string"&&this.isRangePattern(w)){if(!this.validateRangePattern($,w))return{isValid:!1,error:`Expected ${K} to match range ${w}, got ${JSON.stringify($)}`};return{isValid:!0}}if(w==="null"||w===null){if($!==null)return{isValid:!1,error:`Expected ${K} to be null, got ${JSON.stringify($)}`};return{isValid:!0}}if($!==w)return{isValid:!1,error:`Expected ${K} to be ${JSON.stringify(w)}, got ${JSON.stringify($)}`};return{isValid:!0}}isRegexPattern($){return $.startsWith("^")||$.endsWith("$")||$.includes("\\d")||$.includes("\\w")||$.includes("\\s")||$.includes("[")||$.includes("*")||$.includes("+")||$.includes("?")}validateRegexPattern($,w){let K=String($);try{return new RegExp(w).test(K)}catch{return!1}}isRangePattern($){return/^(>=?|<=?|>|<)\s*[\d.-]+(\s*,\s*(>=?|<=?|>|<)\s*[\d.-]+)*$/.test($)}validateRangePattern($,w){let K=Number($);if(Number.isNaN(K))return!1;let X=w.match(/^([\d.-]+)\s*-\s*([\d.-]+)$/);if(X){let Z=Number(X[1]),W=Number(X[2]);return K>=Z&&K<=W}return w.split(",").map((Z)=>Z.trim()).every((Z)=>{let W=Z.match(/^(>=?|<=?|>|<)\s*([\d.-]+)$/);if(!W)return!1;let z=W[1],D=Number(W[2]);switch(z){case">":return K>D;case">=":return K>=D;case"<":return K<D;case"<=":return K<=D;default:return!1}})}isArraySelector($){return/^\[.*\]$/.test($)||$==="*"||$.startsWith("slice(")}getArrayValue($,w){if(w==="*")return $;if(w.startsWith("[")&&w.endsWith("]")){let K=w.slice(1,-1);if(K==="*")return $;let X=Number(K);if(!Number.isNaN(X))return X>=0?$[X]:$[$.length+X]}if(w.startsWith("slice(")){let K=w.match(/slice\((\d+)(?:,(\d+))?\)/);if(K){let X=Number(K[1]),Q=K[2]?Number(K[2]):void 0;return $.slice(X,Q)}}return}async executeSequential($){let w=performance.now(),K=[];for(let X=0;X<$.length;X++){let Q=await this.executeRequest($[X],X+1);if(K.push(Q),!Q.success&&!this.globalConfig.continueOnError){this.logger.logError("Stopping execution due to error");break}}return this.createSummary(K,performance.now()-w)}async executeParallel($){let w=performance.now(),K=$.map((Q,Z)=>this.executeRequest(Q,Z+1)),X=await Promise.all(K);return this.createSummary(X,performance.now()-w)}async execute($){this.logger.logExecutionStart($.length,this.globalConfig.execution||"sequential");let w=this.globalConfig.execution==="parallel"?await this.executeParallel($):await this.executeSequential($);if(this.logger.logSummary(w),this.globalConfig.output?.saveToFile)await this.saveSummaryToFile(w);return w}createSummary($,w){let K=$.filter((Q)=>Q.success).length,X=$.filter((Q)=>!Q.success).length;return{total:$.length,successful:K,failed:X,duration:w,results:$}}async saveSummaryToFile($){let w=this.globalConfig.output?.saveToFile;if(!w)return;let K=JSON.stringify($,null,2);await Bun.write(w,K),this.logger.logInfo(`Results saved to ${w}`)}}var{YAML:R}=globalThis.Bun;class H{static async parseFile($){let K=await Bun.file($).text();return R.parse(K)}static parse($){return R.parse($)}static interpolateVariables($,w){if(typeof $==="string"){let K=$.match(/^\$\{([^}]+)\}$/);if(K){let X=K[1],Q=H.resolveDynamicVariable(X);return Q!==null?Q:w[X]||$}return $.replace(/\$\{([^}]+)\}/g,(X,Q)=>{let Z=H.resolveDynamicVariable(Q);return Z!==null?Z:w[Q]||X})}if(Array.isArray($))return $.map((K)=>H.interpolateVariables(K,w));if($&&typeof $==="object"){let K={};for(let[X,Q]of Object.entries($))K[X]=H.interpolateVariables(Q,w);return K}return $}static resolveDynamicVariable($){if($==="UUID")return crypto.randomUUID();if($==="CURRENT_TIME"||$==="TIMESTAMP")return Date.now().toString();if($.startsWith("DATE:")){let w=$.slice(5);return H.formatDate(new Date,w)}if($.startsWith("TIME:")){let w=$.slice(5);return H.formatTime(new Date,w)}return null}static formatDate($,w){let K=$.getFullYear(),X=String($.getMonth()+1).padStart(2,"0"),Q=String($.getDate()).padStart(2,"0");return w.replace("YYYY",K.toString()).replace("MM",X).replace("DD",Q)}static formatTime($,w){let K=String($.getHours()).padStart(2,"0"),X=String($.getMinutes()).padStart(2,"0"),Q=String($.getSeconds()).padStart(2,"0");return w.replace("HH",K).replace("mm",X).replace("ss",Q)}static mergeConfigs($,w){return{...$,...w,headers:{...$.headers,...w.headers},params:{...$.params,...w.params},variables:{...$.variables,...w.variables}}}}function M(){if(typeof BUILD_VERSION!=="undefined")return BUILD_VERSION;if(process.env.CURL_RUNNER_VERSION)return process.env.CURL_RUNNER_VERSION;try{let $=["../package.json","./package.json","../../package.json"];for(let w of $)try{let K=j(w);if(K.name==="@curl-runner/cli"&&K.version)return K.version}catch{}return"0.0.0"}catch{return"0.0.0"}}var P={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",underscore:"\x1B[4m",black:"\x1B[30m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",cyan:"\x1B[36m",white:"\x1B[37m",bgBlack:"\x1B[40m",bgRed:"\x1B[41m",bgGreen:"\x1B[42m",bgYellow:"\x1B[43m",bgBlue:"\x1B[44m",bgMagenta:"\x1B[45m",bgCyan:"\x1B[46m",bgWhite:"\x1B[47m"};function U($,w){return`${P[w]}${$}${P.reset}`}var k=`${process.env.HOME}/.curl-runner-version-cache.json`,x=86400000,y="https://registry.npmjs.org/@curl-runner/cli/latest";class B{async checkForUpdates($=!1){try{if(process.env.CI)return;let w=M();if(w==="0.0.0")return;if(!$){let X=await this.getCachedVersion();if(X&&Date.now()-X.lastCheck<x){this.compareVersions(w,X.latestVersion);return}}let K=await this.fetchLatestVersion();if(K)await this.setCachedVersion(K),this.compareVersions(w,K)}catch{}}async fetchLatestVersion(){try{let $=await fetch(y,{signal:AbortSignal.timeout(3000)});if(!$.ok)return null;return(await $.json()).version}catch{return null}}compareVersions($,w){if(this.isNewerVersion($,w))console.log(),console.log(U("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E","yellow")),console.log(U("\u2502","yellow")+" "+U("\u2502","yellow")),console.log(U("\u2502","yellow")+" "+U("\uD83D\uDCE6 New version available!","bright")+` ${U($,"red")} \u2192 ${U(w,"green")} `+U("\u2502","yellow")),console.log(U("\u2502","yellow")+" "+U("\u2502","yellow")),console.log(U("\u2502","yellow")+" Update with: "+U("npm install -g @curl-runner/cli","cyan")+" "+U("\u2502","yellow")),console.log(U("\u2502","yellow")+" or: "+U("bun install -g @curl-runner/cli","cyan")+" "+U("\u2502","yellow")),console.log(U("\u2502","yellow")+" "+U("\u2502","yellow")),console.log(U("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F","yellow")),console.log()}isNewerVersion($,w){try{let K=$.replace(/^v/,""),X=w.replace(/^v/,""),Q=K.split(".").map(Number),Z=X.split(".").map(Number);for(let W=0;W<Math.max(Q.length,Z.length);W++){let z=Q[W]||0,D=Z[W]||0;if(D>z)return!0;if(D<z)return!1}return!1}catch{return!1}}async getCachedVersion(){try{let $=Bun.file(k);if(await $.exists())return JSON.parse(await $.text())}catch{}return null}async setCachedVersion($){try{let w={lastCheck:Date.now(),latestVersion:$};await Bun.write(k,JSON.stringify(w))}catch{}}}class q{logger=new Y;async loadConfigFile(){let $=["curl-runner.yaml","curl-runner.yml",".curl-runner.yaml",".curl-runner.yml"];for(let w of $)try{if(await Bun.file(w).exists()){let X=await H.parseFile(w),Q=X.global||X;return this.logger.logInfo(`Loaded configuration from ${w}`),Q}}catch(K){this.logger.logWarning(`Failed to load configuration from ${w}: ${K}`)}return{}}loadEnvironmentVariables(){let $={};if(process.env.CURL_RUNNER_TIMEOUT)$.defaults={...$.defaults,timeout:Number.parseInt(process.env.CURL_RUNNER_TIMEOUT,10)};if(process.env.CURL_RUNNER_RETRIES)$.defaults={...$.defaults,retry:{...$.defaults?.retry,count:Number.parseInt(process.env.CURL_RUNNER_RETRIES,10)}};if(process.env.CURL_RUNNER_RETRY_DELAY)$.defaults={...$.defaults,retry:{...$.defaults?.retry,delay:Number.parseInt(process.env.CURL_RUNNER_RETRY_DELAY,10)}};if(process.env.CURL_RUNNER_VERBOSE)$.output={...$.output,verbose:process.env.CURL_RUNNER_VERBOSE.toLowerCase()==="true"};if(process.env.CURL_RUNNER_EXECUTION)$.execution=process.env.CURL_RUNNER_EXECUTION;if(process.env.CURL_RUNNER_CONTINUE_ON_ERROR)$.continueOnError=process.env.CURL_RUNNER_CONTINUE_ON_ERROR.toLowerCase()==="true";if(process.env.CURL_RUNNER_OUTPUT_FORMAT){let w=process.env.CURL_RUNNER_OUTPUT_FORMAT;if(["json","pretty","raw"].includes(w))$.output={...$.output,format:w}}if(process.env.CURL_RUNNER_PRETTY_LEVEL){let w=process.env.CURL_RUNNER_PRETTY_LEVEL;if(["minimal","standard","detailed"].includes(w))$.output={...$.output,prettyLevel:w}}if(process.env.CURL_RUNNER_OUTPUT_FILE)$.output={...$.output,saveToFile:process.env.CURL_RUNNER_OUTPUT_FILE};return $}async run($){try{let{files:w,options:K}=this.parseArguments($);if(!K.version&&!K.help)new B().checkForUpdates().catch(()=>{});if(K.help){this.showHelp();return}if(K.version){console.log(`curl-runner v${M()}`);return}let X=this.loadEnvironmentVariables(),Q=await this.loadConfigFile(),Z=await this.findYamlFiles(w,K);if(Z.length===0)this.logger.logError("No YAML files found"),process.exit(1);this.logger.logInfo(`Found ${Z.length} YAML file(s)`);let W=this.mergeGlobalConfigs(X,Q),z=[],D=[];for(let J of Z){this.logger.logInfo(`Processing: ${J}`);let{requests:_,config:T}=await this.processYamlFile(J),S=T?.output||{},F=_.map((I)=>({...I,sourceOutputConfig:S,sourceFile:J}));if(T){let{...I}=T;W=this.mergeGlobalConfigs(W,I)}D.push({file:J,requests:F,config:T}),z.push(...F)}if(K.execution)W.execution=K.execution;if(K.continueOnError!==void 0)W.continueOnError=K.continueOnError;if(K.verbose!==void 0)W.output={...W.output,verbose:K.verbose};if(K.quiet!==void 0)W.output={...W.output,verbose:!1};if(K.output)W.output={...W.output,saveToFile:K.output};if(K.outputFormat)W.output={...W.output,format:K.outputFormat};if(K.prettyLevel)W.output={...W.output,prettyLevel:K.prettyLevel};if(K.showHeaders!==void 0)W.output={...W.output,showHeaders:K.showHeaders};if(K.showBody!==void 0)W.output={...W.output,showBody:K.showBody};if(K.showMetrics!==void 0)W.output={...W.output,showMetrics:K.showMetrics};if(K.timeout)W.defaults={...W.defaults,timeout:K.timeout};if(K.retries||K.noRetry){let J=K.noRetry?0:K.retries||0;W.defaults={...W.defaults,retry:{...W.defaults?.retry,count:J}}}if(K.retryDelay)W.defaults={...W.defaults,retry:{...W.defaults?.retry,delay:K.retryDelay}};if(z.length===0)this.logger.logError("No requests found in YAML files"),process.exit(1);let G=new A(W),O;if(D.length>1){let J=[],_=0;for(let F=0;F<D.length;F++){let I=D[F];this.logger.logFileHeader(I.file,I.requests.length);let E=await G.execute(I.requests);if(J.push(...E.results),_+=E.duration,F<D.length-1)console.log()}let T=J.filter((F)=>F.success).length,S=J.filter((F)=>!F.success).length;O={total:J.length,successful:T,failed:S,duration:_,results:J},G.logger.logSummary(O,!0)}else O=await G.execute(z);process.exit(O.failed>0&&!W.continueOnError?1:0)}catch(w){this.logger.logError(w instanceof Error?w.message:String(w)),process.exit(1)}}parseArguments($){let w={},K=[];for(let X=0;X<$.length;X++){let Q=$[X];if(Q.startsWith("--")){let Z=Q.slice(2),W=$[X+1];if(Z==="help"||Z==="version")w[Z]=!0;else if(Z==="no-retry")w.noRetry=!0;else if(Z==="quiet")w.quiet=!0;else if(Z==="show-headers")w.showHeaders=!0;else if(Z==="show-body")w.showBody=!0;else if(Z==="show-metrics")w.showMetrics=!0;else if(W&&!W.startsWith("--")){if(Z==="continue-on-error")w.continueOnError=W==="true";else if(Z==="verbose")w.verbose=W==="true";else if(Z==="timeout")w.timeout=Number.parseInt(W,10);else if(Z==="retries")w.retries=Number.parseInt(W,10);else if(Z==="retry-delay")w.retryDelay=Number.parseInt(W,10);else if(Z==="output-format"){if(["json","pretty","raw"].includes(W))w.outputFormat=W}else if(Z==="pretty-level"){if(["minimal","standard","detailed"].includes(W))w.prettyLevel=W}else w[Z]=W;X++}else w[Z]=!0}else if(Q.startsWith("-")){let Z=Q.slice(1);for(let W of Z)switch(W){case"h":w.help=!0;break;case"v":w.verbose=!0;break;case"p":w.execution="parallel";break;case"c":w.continueOnError=!0;break;case"q":w.quiet=!0;break;case"o":{let z=$[X+1];if(z&&!z.startsWith("-"))w.output=z,X++;break}}}else K.push(Q)}return{files:K,options:w}}async findYamlFiles($,w){let K=new Set,X=[];if($.length===0)X=w.all?["**/*.yaml","**/*.yml"]:["*.yaml","*.yml"];else for(let Q of $)try{let W=await(await import("fs/promises")).stat(Q);if(W.isDirectory()){if(X.push(`${Q}/*.yaml`,`${Q}/*.yml`),w.all)X.push(`${Q}/**/*.yaml`,`${Q}/**/*.yml`)}else if(W.isFile())X.push(Q)}catch{X.push(Q)}for(let Q of X){let Z=new d(Q);for await(let W of Z.scan("."))if(W.endsWith(".yaml")||W.endsWith(".yml"))K.add(W)}return Array.from(K).sort()}async processYamlFile($){let w=await H.parseFile($),K=[],X;if(w.global)X=w.global;let Q={...w.global?.variables,...w.collection?.variables},Z={...w.global?.defaults,...w.collection?.defaults};if(w.request){let W=this.prepareRequest(w.request,Q,Z);K.push(W)}if(w.requests)for(let W of w.requests){let z=this.prepareRequest(W,Q,Z);K.push(z)}if(w.collection?.requests)for(let W of w.collection.requests){let z=this.prepareRequest(W,Q,Z);K.push(z)}return{requests:K,config:X}}prepareRequest($,w,K){let X=H.interpolateVariables($,w);return H.mergeConfigs(K,X)}mergeGlobalConfigs($,w){return{...$,...w,variables:{...$.variables,...w.variables},output:{...$.output,...w.output},defaults:{...$.defaults,...w.defaults}}}showHelp(){console.log(`
3
+ var C=Object.create;var{getPrototypeOf:v,defineProperty:k,getOwnPropertyNames:x}=Object;var y=Object.prototype.hasOwnProperty;var f=($,K,Q)=>{Q=$!=null?C(v($)):{};let X=K||!$||!$.__esModule?k(Q,"default",{value:$,enumerable:!0}):Q;for(let Z of x($))if(!y.call(X,Z))k(X,Z,{get:()=>$[Z],enumerable:!0});return X};var S=import.meta.require;var{Glob:c}=globalThis.Bun;var{YAML:q}=globalThis.Bun;class H{static async parseFile($){let Q=await Bun.file($).text();return q.parse(Q)}static parse($){return q.parse($)}static interpolateVariables($,K,Q){if(typeof $==="string"){let X=$.match(/^\$\{([^}]+)\}$/);if(X){let Z=X[1],z=H.resolveVariable(Z,K,Q);return z!==null?z:$}return $.replace(/\$\{([^}]+)\}/g,(Z,z)=>{let W=H.resolveVariable(z,K,Q);return W!==null?W:Z})}if(Array.isArray($))return $.map((X)=>H.interpolateVariables(X,K,Q));if($&&typeof $==="object"){let X={};for(let[Z,z]of Object.entries($))X[Z]=H.interpolateVariables(z,K,Q);return X}return $}static resolveVariable($,K,Q){if($.startsWith("store.")&&Q){let Z=$.slice(6);if(Z in Q)return Q[Z];return null}let X=H.resolveDynamicVariable($);if(X!==null)return X;if($ in K)return K[$];return null}static resolveDynamicVariable($){if($==="UUID")return crypto.randomUUID();if($==="CURRENT_TIME"||$==="TIMESTAMP")return Date.now().toString();if($.startsWith("DATE:")){let K=$.slice(5);return H.formatDate(new Date,K)}if($.startsWith("TIME:")){let K=$.slice(5);return H.formatTime(new Date,K)}return null}static formatDate($,K){let Q=$.getFullYear(),X=String($.getMonth()+1).padStart(2,"0"),Z=String($.getDate()).padStart(2,"0");return K.replace("YYYY",Q.toString()).replace("MM",X).replace("DD",Z)}static formatTime($,K){let Q=String($.getHours()).padStart(2,"0"),X=String($.getMinutes()).padStart(2,"0"),Z=String($.getSeconds()).padStart(2,"0");return K.replace("HH",Q).replace("mm",X).replace("ss",Z)}static mergeConfigs($,K){return{...$,...K,headers:{...$.headers,...K.headers},params:{...$.params,...K.params},variables:{...$.variables,...K.variables}}}}class M{static buildCommand($){let K=["curl"];if(K.push("-X",$.method||"GET"),K.push("-w",'"\\n__CURL_METRICS_START__%{json}__CURL_METRICS_END__"'),$.headers)for(let[X,Z]of Object.entries($.headers))K.push("-H",`"${X}: ${Z}"`);if($.auth){if($.auth.type==="basic"&&$.auth.username&&$.auth.password)K.push("-u",`"${$.auth.username}:${$.auth.password}"`);else if($.auth.type==="bearer"&&$.auth.token)K.push("-H",`"Authorization: Bearer ${$.auth.token}"`)}if($.body){let X=typeof $.body==="string"?$.body:JSON.stringify($.body);if(K.push("-d",`'${X.replace(/'/g,"'\\''")}'`),!$.headers?.["Content-Type"])K.push("-H",'"Content-Type: application/json"')}if($.timeout)K.push("--max-time",$.timeout.toString());if($.followRedirects!==!1){if(K.push("-L"),$.maxRedirects)K.push("--max-redirs",$.maxRedirects.toString())}if($.proxy)K.push("-x",$.proxy);if($.insecure)K.push("-k");if($.output)K.push("-o",$.output);K.push("-s","-S");let Q=$.url;if($.params&&Object.keys($.params).length>0){let X=new URLSearchParams($.params).toString();Q+=(Q.includes("?")?"&":"?")+X}return K.push(`"${Q}"`),K.join(" ")}static async executeCurl($){try{let K=Bun.spawn(["sh","-c",$],{stdout:"pipe",stderr:"pipe"}),Q=await new Response(K.stdout).text(),X=await new Response(K.stderr).text();if(await K.exited,K.exitCode!==0&&!Q)return{success:!1,error:X||`Command failed with exit code ${K.exitCode}`};let Z=Q,z={},W=Q.match(/__CURL_METRICS_START__(.+?)__CURL_METRICS_END__/);if(W){Z=Q.replace(/__CURL_METRICS_START__.+?__CURL_METRICS_END__/,"").trim();try{z=JSON.parse(W[1])}catch(J){}}let D={};if(z.response_code){let J=X.split(`
4
+ `).filter((w)=>w.includes(":"));for(let w of J){let[O,...U]=w.split(":");if(O&&U.length>0)D[O.trim()]=U.join(":").trim()}}return{success:!0,status:z.response_code||z.http_code,headers:D,body:Z,metrics:{duration:(z.time_total||0)*1000,size:z.size_download,dnsLookup:(z.time_namelookup||0)*1000,tcpConnection:(z.time_connect||0)*1000,tlsHandshake:(z.time_appconnect||0)*1000,firstByte:(z.time_starttransfer||0)*1000,download:(z.time_total||0)*1000}}}catch(K){return{success:!1,error:K instanceof Error?K.message:String(K)}}}}class j{colors;constructor($){this.colors=$}color($,K){if(!K||!this.colors[K])return $;return`${this.colors[K]}${$}${this.colors.reset}`}render($,K=" "){$.forEach((Q,X)=>{let Z=X===$.length-1,z=Z?`${K}\u2514\u2500`:`${K}\u251C\u2500`;if(Q.label&&Q.value){let W=Q.color?this.color(Q.value,Q.color):Q.value,D=W.split(`
5
+ `);if(D.length===1)console.log(`${z} ${Q.label}: ${W}`);else{console.log(`${z} ${Q.label}:`);let J=Z?`${K} `:`${K}\u2502 `;D.forEach((w)=>{console.log(`${J}${w}`)})}}else if(Q.label&&!Q.value)console.log(`${z} ${Q.label}:`);else if(!Q.label&&Q.value){let W=Z?`${K} `:`${K}\u2502 `;console.log(`${W}${Q.value}`)}if(Q.children&&Q.children.length>0){let W=Z?`${K} `:`${K}\u2502 `;this.render(Q.children,W)}})}}class Y{config;colors={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",underscore:"\x1B[4m",black:"\x1B[30m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",cyan:"\x1B[36m",white:"\x1B[37m",bgBlack:"\x1B[40m",bgRed:"\x1B[41m",bgGreen:"\x1B[42m",bgYellow:"\x1B[43m",bgBlue:"\x1B[44m",bgMagenta:"\x1B[45m",bgCyan:"\x1B[46m",bgWhite:"\x1B[47m"};constructor($={}){this.config={verbose:!1,showHeaders:!1,showBody:!0,showMetrics:!1,format:"pretty",prettyLevel:"minimal",...$}}color($,K){return`${this.colors[K]}${$}${this.colors.reset}`}getShortFilename($){return $.replace(/.*\//,"").replace(".yaml","")}shouldShowOutput(){if(this.config.format==="raw")return!1;if(this.config.format==="pretty")return!0;return this.config.verbose!==!1}shouldShowHeaders(){if(this.config.format!=="pretty")return this.config.showHeaders||!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.showHeaders||!1;case"detailed":return!0;default:return this.config.showHeaders||!1}}shouldShowBody(){if(this.config.format!=="pretty")return this.config.showBody!==!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.showBody!==!1;case"detailed":return!0;default:return this.config.showBody!==!1}}shouldShowMetrics(){if(this.config.format!=="pretty")return this.config.showMetrics||!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.showMetrics||!1;case"detailed":return!0;default:return this.config.showMetrics||!1}}shouldShowRequestDetails(){if(this.config.format!=="pretty")return this.config.verbose||!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.verbose||!1;case"detailed":return!0;default:return this.config.verbose||!1}}shouldShowSeparators(){if(this.config.format!=="pretty")return!0;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return!0;case"detailed":return!0;default:return!0}}colorStatusCode($){return this.color($,"yellow")}logValidationErrors($){let K=$.split("; ");if(K.length===1){let Q=K[0].trim(),X=Q.match(/^Expected status (.+?), got (.+)$/);if(X){let[,Z,z]=X,W=this.colorStatusCode(Z.replace(" or ","|")),D=this.color(z,"red");console.log(` ${this.color("\u2717","red")} ${this.color("Error:","red")} Expected ${this.color("status","yellow")} ${W}, got ${D}`)}else console.log(` ${this.color("\u2717","red")} ${this.color("Error:","red")} ${Q}`)}else{console.log(` ${this.color("\u2717","red")} ${this.color("Validation Errors:","red")}`);for(let Q of K){let X=Q.trim();if(X)if(X.startsWith("Expected ")){let Z=X.match(/^Expected status (.+?), got (.+)$/);if(Z){let[,z,W]=Z,D=this.colorStatusCode(z.replace(" or ","|")),J=this.color(W,"red");console.log(` ${this.color("\u2022","red")} ${this.color("status","yellow")}: expected ${D}, got ${J}`)}else{let z=X.match(/^Expected (.+?) to be (.+?), got (.+)$/);if(z){let[,W,D,J]=z;console.log(` ${this.color("\u2022","red")} ${this.color(W,"yellow")}: expected ${this.color(D,"green")}, got ${this.color(J,"red")}`)}else console.log(` ${this.color("\u2022","red")} ${X}`)}}else console.log(` ${this.color("\u2022","red")} ${X}`)}}}formatJson($){if(this.config.format==="raw")return typeof $==="string"?$:JSON.stringify($);if(this.config.format==="json")return JSON.stringify($);return JSON.stringify($,null,2)}formatDuration($){if($<1000)return`${$.toFixed(0)}ms`;return`${($/1000).toFixed(2)}s`}formatSize($){if(!$)return"0 B";let K=["B","KB","MB","GB"],Q=Math.floor(Math.log($)/Math.log(1024));return`${($/1024**Q).toFixed(2)} ${K[Q]}`}logExecutionStart($,K){if(!this.shouldShowOutput())return;if(this.shouldShowSeparators())console.log(),console.log(this.color(`Executing ${$} request(s) in ${K} mode`,"dim")),console.log();else console.log()}logRequestStart($,K){return}logCommand($){if(this.shouldShowRequestDetails())console.log(this.color(" Command:","dim")),console.log(this.color(` ${$}`,"dim"))}logRetry($,K){console.log(this.color(` \u21BB Retry ${$}/${K}...`,"yellow"))}logRequestComplete($){if(this.config.format==="raw"){if($.success&&this.config.showBody&&$.body){let D=this.formatJson($.body);console.log(D)}return}if(this.config.format==="json"){let D={request:{name:$.request.name,url:$.request.url,method:$.request.method||"GET"},success:$.success,status:$.status,...this.shouldShowHeaders()&&$.headers?{headers:$.headers}:{},...this.shouldShowBody()&&$.body?{body:$.body}:{},...$.error?{error:$.error}:{},...this.shouldShowMetrics()&&$.metrics?{metrics:$.metrics}:{}};console.log(JSON.stringify(D,null,2));return}if(!this.shouldShowOutput())return;let K=this.config.prettyLevel||"minimal",Q=$.success?"green":"red",X=$.success?"\u2713":"x",Z=$.request.name||"Request";if(K==="minimal"){let D=$.request.sourceFile?this.getShortFilename($.request.sourceFile):"inline";console.log(`${this.color(X,Q)} ${this.color(Z,"bright")} [${D}]`);let J=[],w=new j(this.colors);J.push({label:$.request.method||"GET",value:$.request.url,color:"blue"});let O=$.status?`${$.status}`:"ERROR";if(J.push({label:`${X} Status`,value:O,color:Q}),$.metrics){let U=`${this.formatDuration($.metrics.duration)} | ${this.formatSize($.metrics.size)}`;J.push({label:"Duration",value:U,color:"cyan"})}if(w.render(J),$.error)console.log(),this.logValidationErrors($.error);console.log();return}console.log(`${this.color(X,Q)} ${this.color(Z,"bright")}`);let z=[],W=new j(this.colors);if(z.push({label:"URL",value:$.request.url,color:"blue"}),z.push({label:"Method",value:$.request.method||"GET",color:"yellow"}),z.push({label:"Status",value:String($.status||"ERROR"),color:Q}),$.metrics)z.push({label:"Duration",value:this.formatDuration($.metrics.duration),color:"cyan"});if(this.shouldShowHeaders()&&$.headers&&Object.keys($.headers).length>0){let D=Object.entries($.headers).map(([J,w])=>({label:this.color(J,"dim"),value:String(w)}));z.push({label:"Headers",children:D})}if(this.shouldShowBody()&&$.body){let J=this.formatJson($.body).split(`
6
+ `),w=this.shouldShowRequestDetails()?1/0:10,O=J.slice(0,w);if(J.length>w)O.push(this.color(`... (${J.length-w} more lines)`,"dim"));z.push({label:"Response Body",value:O.join(`
7
+ `)})}if(this.shouldShowMetrics()&&$.metrics&&K==="detailed"){let D=$.metrics,J=[];if(J.push({label:"Request Duration",value:this.formatDuration(D.duration),color:"cyan"}),D.size!==void 0)J.push({label:"Response Size",value:this.formatSize(D.size),color:"cyan"});if(D.dnsLookup)J.push({label:"DNS Lookup",value:this.formatDuration(D.dnsLookup),color:"cyan"});if(D.tcpConnection)J.push({label:"TCP Connection",value:this.formatDuration(D.tcpConnection),color:"cyan"});if(D.tlsHandshake)J.push({label:"TLS Handshake",value:this.formatDuration(D.tlsHandshake),color:"cyan"});if(D.firstByte)J.push({label:"Time to First Byte",value:this.formatDuration(D.firstByte),color:"cyan"});z.push({label:"Metrics",children:J})}if(W.render(z),$.error)console.log(),this.logValidationErrors($.error);console.log()}logSummary($,K=!1){if(this.config.format==="raw")return;if(this.config.format==="json"){let D={summary:{total:$.total,successful:$.successful,failed:$.failed,duration:$.duration},results:$.results.map((J)=>({request:{name:J.request.name,url:J.request.url,method:J.request.method||"GET"},success:J.success,status:J.status,...this.shouldShowHeaders()&&J.headers?{headers:J.headers}:{},...this.shouldShowBody()&&J.body?{body:J.body}:{},...J.error?{error:J.error}:{},...this.shouldShowMetrics()&&J.metrics?{metrics:J.metrics}:{}}))};console.log(JSON.stringify(D,null,2));return}if(!this.shouldShowOutput())return;let Q=this.config.prettyLevel||"minimal";if(K)console.log();if(Q==="minimal"){let D=$.failed===0?"green":"red",J=$.failed===0?`${$.total} request${$.total===1?"":"s"} completed successfully`:`${$.successful}/${$.total} request${$.total===1?"":"s"} completed, ${$.failed} failed`;console.log(`${K?"\u25C6 Global Summary":"Summary"}: ${this.color(J,D)}`);return}let X=($.successful/$.total*100).toFixed(1),Z=$.failed===0?"green":"red",z=$.failed===0?`${$.total} request${$.total===1?"":"s"} completed successfully`:`${$.successful}/${$.total} request${$.total===1?"":"s"} completed, ${$.failed} failed`,W=K?"\u25C6 Global Summary":"Summary";if(console.log(),console.log(`${W}: ${this.color(z,Z)} (${this.color(this.formatDuration($.duration),"cyan")})`),$.failed>0&&this.shouldShowRequestDetails())$.results.filter((D)=>!D.success).forEach((D)=>{let J=D.request.name||D.request.url;console.log(` ${this.color("\u2022","red")} ${J}: ${D.error}`)})}logError($){console.error(this.color(`\u2717 ${$}`,"red"))}logWarning($){console.warn(this.color(`\u26A0 ${$}`,"yellow"))}logInfo($){console.log(this.color(`\u2139 ${$}`,"blue"))}logSuccess($){console.log(this.color(`\u2713 ${$}`,"green"))}logFileHeader($,K){if(!this.shouldShowOutput()||this.config.format!=="pretty")return;let Q=$.replace(/.*\//,"").replace(".yaml","");console.log(),console.log(this.color(`\u25B6 ${Q}.yaml`,"bright")+this.color(` (${K} request${K===1?"":"s"})`,"dim"))}}function g($,K){let Q=K.split("."),X=$;for(let Z of Q){if(X===null||X===void 0)return;if(typeof X!=="object")return;let z=Z.match(/^(\w+)\[(\d+)\]$/);if(z){let[,W,D]=z,J=Number.parseInt(D,10);if(X=X[W],Array.isArray(X))X=X[J];else return}else if(/^\d+$/.test(Z)&&Array.isArray(X))X=X[Number.parseInt(Z,10)];else X=X[Z]}return X}function p($){if($===void 0||$===null)return"";if(typeof $==="string")return $;if(typeof $==="number"||typeof $==="boolean")return String($);return JSON.stringify($)}function N($,K){let Q={},X={status:$.status,headers:$.headers||{},body:$.body,metrics:$.metrics};for(let[Z,z]of Object.entries(K)){let W=g(X,z);Q[Z]=p(W)}return Q}function P(){return{}}class B{logger;globalConfig;constructor($={}){this.globalConfig=$,this.logger=new Y($.output)}mergeOutputConfig($){return{...this.globalConfig.output,...$.sourceOutputConfig}}async executeRequest($,K=0){let Q=performance.now(),X=this.mergeOutputConfig($),Z=new Y(X);Z.logRequestStart($,K);let z=M.buildCommand($);Z.logCommand(z);let W=0,D,J=($.retry?.count||0)+1;while(W<J){if(W>0){if(Z.logRetry(W,J-1),$.retry?.delay)await Bun.sleep($.retry.delay)}let O=await M.executeCurl(z);if(O.success){let U=O.body;try{if(O.headers?.["content-type"]?.includes("application/json")||U&&(U.trim().startsWith("{")||U.trim().startsWith("[")))U=JSON.parse(U)}catch(I){}let G={request:$,success:!0,status:O.status,headers:O.headers,body:U,metrics:{...O.metrics,duration:performance.now()-Q}};if($.expect){let I=this.validateResponse(G,$.expect);if(!I.success)G.success=!1,G.error=I.error}return Z.logRequestComplete(G),G}D=O.error,W++}let w={request:$,success:!1,error:D,metrics:{duration:performance.now()-Q}};return Z.logRequestComplete(w),w}validateResponse($,K){if(!K)return{success:!0};let Q=[];if(K.status!==void 0){let Z=Array.isArray(K.status)?K.status:[K.status];if(!Z.includes($.status||0))Q.push(`Expected status ${Z.join(" or ")}, got ${$.status}`)}if(K.headers)for(let[Z,z]of Object.entries(K.headers)){let W=$.headers?.[Z]||$.headers?.[Z.toLowerCase()];if(W!==z)Q.push(`Expected header ${Z}="${z}", got "${W}"`)}if(K.body!==void 0){let Z=this.validateBodyProperties($.body,K.body,"");if(Z.length>0)Q.push(...Z)}if(K.responseTime!==void 0&&$.metrics){let Z=$.metrics.duration;if(!this.validateRangePattern(Z,K.responseTime))Q.push(`Expected response time to match ${K.responseTime}ms, got ${Z.toFixed(2)}ms`)}let X=Q.length>0;if(K.failure===!0){if(X)return{success:!1,error:Q.join("; ")};let Z=$.status||0;if(Z>=400)return{success:!0};else return{success:!1,error:`Expected request to fail (4xx/5xx) but got status ${Z}`}}else if(X)return{success:!1,error:Q.join("; ")};else return{success:!0}}validateBodyProperties($,K,Q){let X=[];if(typeof K!=="object"||K===null){let Z=this.validateValue($,K,Q||"body");if(!Z.isValid)X.push(Z.error);return X}if(Array.isArray(K)){let Z=this.validateValue($,K,Q||"body");if(!Z.isValid)X.push(Z.error);return X}for(let[Z,z]of Object.entries(K)){let W=Q?`${Q}.${Z}`:Z,D;if(Array.isArray($)&&this.isArraySelector(Z))D=this.getArrayValue($,Z);else D=$?.[Z];if(typeof z==="object"&&z!==null&&!Array.isArray(z)){let J=this.validateBodyProperties(D,z,W);X.push(...J)}else{let J=this.validateValue(D,z,W);if(!J.isValid)X.push(J.error)}}return X}validateValue($,K,Q){if(K==="*")return{isValid:!0};if(Array.isArray(K)){if(!K.some((Z)=>{if(Z==="*")return!0;if(typeof Z==="string"&&this.isRegexPattern(Z))return this.validateRegexPattern($,Z);if(typeof Z==="string"&&this.isRangePattern(Z))return this.validateRangePattern($,Z);return $===Z}))return{isValid:!1,error:`Expected ${Q} to match one of ${JSON.stringify(K)}, got ${JSON.stringify($)}`};return{isValid:!0}}if(typeof K==="string"&&this.isRegexPattern(K)){if(!this.validateRegexPattern($,K))return{isValid:!1,error:`Expected ${Q} to match pattern ${K}, got ${JSON.stringify($)}`};return{isValid:!0}}if(typeof K==="string"&&this.isRangePattern(K)){if(!this.validateRangePattern($,K))return{isValid:!1,error:`Expected ${Q} to match range ${K}, got ${JSON.stringify($)}`};return{isValid:!0}}if(K==="null"||K===null){if($!==null)return{isValid:!1,error:`Expected ${Q} to be null, got ${JSON.stringify($)}`};return{isValid:!0}}if($!==K)return{isValid:!1,error:`Expected ${Q} to be ${JSON.stringify(K)}, got ${JSON.stringify($)}`};return{isValid:!0}}isRegexPattern($){return $.startsWith("^")||$.endsWith("$")||$.includes("\\d")||$.includes("\\w")||$.includes("\\s")||$.includes("[")||$.includes("*")||$.includes("+")||$.includes("?")}validateRegexPattern($,K){let Q=String($);try{return new RegExp(K).test(Q)}catch{return!1}}isRangePattern($){return/^(>=?|<=?|>|<)\s*[\d.-]+(\s*,\s*(>=?|<=?|>|<)\s*[\d.-]+)*$/.test($)}validateRangePattern($,K){let Q=Number($);if(Number.isNaN(Q))return!1;let X=K.match(/^([\d.-]+)\s*-\s*([\d.-]+)$/);if(X){let z=Number(X[1]),W=Number(X[2]);return Q>=z&&Q<=W}return K.split(",").map((z)=>z.trim()).every((z)=>{let W=z.match(/^(>=?|<=?|>|<)\s*([\d.-]+)$/);if(!W)return!1;let D=W[1],J=Number(W[2]);switch(D){case">":return Q>J;case">=":return Q>=J;case"<":return Q<J;case"<=":return Q<=J;default:return!1}})}isArraySelector($){return/^\[.*\]$/.test($)||$==="*"||$.startsWith("slice(")}getArrayValue($,K){if(K==="*")return $;if(K.startsWith("[")&&K.endsWith("]")){let Q=K.slice(1,-1);if(Q==="*")return $;let X=Number(Q);if(!Number.isNaN(X))return X>=0?$[X]:$[$.length+X]}if(K.startsWith("slice(")){let Q=K.match(/slice\((\d+)(?:,(\d+))?\)/);if(Q){let X=Number(Q[1]),Z=Q[2]?Number(Q[2]):void 0;return $.slice(X,Z)}}return}async executeSequential($){let K=performance.now(),Q=[],X=P();for(let Z=0;Z<$.length;Z++){let z=this.interpolateStoreVariables($[Z],X),W=await this.executeRequest(z,Z+1);if(Q.push(W),W.success&&z.store){let D=N(W,z.store);Object.assign(X,D),this.logStoredValues(D)}if(!W.success&&!this.globalConfig.continueOnError){this.logger.logError("Stopping execution due to error");break}}return this.createSummary(Q,performance.now()-K)}interpolateStoreVariables($,K){if(Object.keys(K).length===0)return $;return H.interpolateVariables($,{},K)}logStoredValues($){if(Object.keys($).length===0)return;let K=Object.entries($);for(let[Q,X]of K){let Z=X.length>50?`${X.substring(0,50)}...`:X;this.logger.logInfo(`Stored: ${Q} = "${Z}"`)}}async executeParallel($){let K=performance.now(),Q=$.map((Z,z)=>this.executeRequest(Z,z+1)),X=await Promise.all(Q);return this.createSummary(X,performance.now()-K)}async execute($){this.logger.logExecutionStart($.length,this.globalConfig.execution||"sequential");let K=this.globalConfig.execution==="parallel"?await this.executeParallel($):await this.executeSequential($);if(this.logger.logSummary(K),this.globalConfig.output?.saveToFile)await this.saveSummaryToFile(K);return K}createSummary($,K){let Q=$.filter((Z)=>Z.success).length,X=$.filter((Z)=>!Z.success).length;return{total:$.length,successful:Q,failed:X,duration:K,results:$}}async saveSummaryToFile($){let K=this.globalConfig.output?.saveToFile;if(!K)return;let Q=JSON.stringify($,null,2);await Bun.write(K,Q),this.logger.logInfo(`Results saved to ${K}`)}}function A(){if(typeof BUILD_VERSION<"u")return BUILD_VERSION;if(process.env.CURL_RUNNER_VERSION)return process.env.CURL_RUNNER_VERSION;try{let $=["../package.json","./package.json","../../package.json"];for(let K of $)try{let Q=S(K);if(Q.name==="@curl-runner/cli"&&Q.version)return Q.version}catch{}return"0.0.0"}catch{return"0.0.0"}}var h={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",underscore:"\x1B[4m",black:"\x1B[30m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",cyan:"\x1B[36m",white:"\x1B[37m",bgBlack:"\x1B[40m",bgRed:"\x1B[41m",bgGreen:"\x1B[42m",bgYellow:"\x1B[43m",bgBlue:"\x1B[44m",bgMagenta:"\x1B[45m",bgCyan:"\x1B[46m",bgWhite:"\x1B[47m"};function F($,K){return`${h[K]}${$}${h.reset}`}var b=`${process.env.HOME}/.curl-runner-version-cache.json`,d=86400000,m="https://registry.npmjs.org/@curl-runner/cli/latest";class L{async checkForUpdates($=!1){try{if(process.env.CI)return;let K=A();if(K==="0.0.0")return;if(!$){let X=await this.getCachedVersion();if(X&&Date.now()-X.lastCheck<d){this.compareVersions(K,X.latestVersion);return}}let Q=await this.fetchLatestVersion();if(Q)await this.setCachedVersion(Q),this.compareVersions(K,Q)}catch{}}async fetchLatestVersion(){try{let $=await fetch(m,{signal:AbortSignal.timeout(3000)});if(!$.ok)return null;return(await $.json()).version}catch{return null}}compareVersions($,K){if(this.isNewerVersion($,K))console.log(),console.log(F("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E","yellow")),console.log(F("\u2502","yellow")+" "+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" "+F("\uD83D\uDCE6 New version available!","bright")+` ${F($,"red")} \u2192 ${F(K,"green")} `+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" "+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" Update with: "+F("npm install -g @curl-runner/cli","cyan")+" "+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" or: "+F("bun install -g @curl-runner/cli","cyan")+" "+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" "+F("\u2502","yellow")),console.log(F("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F","yellow")),console.log()}isNewerVersion($,K){try{let Q=$.replace(/^v/,""),X=K.replace(/^v/,""),Z=Q.split(".").map(Number),z=X.split(".").map(Number);for(let W=0;W<Math.max(Z.length,z.length);W++){let D=Z[W]||0,J=z[W]||0;if(J>D)return!0;if(J<D)return!1}return!1}catch{return!1}}async getCachedVersion(){try{let $=Bun.file(b);if(await $.exists())return JSON.parse(await $.text())}catch{}return null}async setCachedVersion($){try{let K={lastCheck:Date.now(),latestVersion:$};await Bun.write(b,JSON.stringify(K))}catch{}}}class V{logger=new Y;async loadConfigFile(){let $=["curl-runner.yaml","curl-runner.yml",".curl-runner.yaml",".curl-runner.yml"];for(let K of $)try{if(await Bun.file(K).exists()){let X=await H.parseFile(K),Z=X.global||X;return this.logger.logInfo(`Loaded configuration from ${K}`),Z}}catch(Q){this.logger.logWarning(`Failed to load configuration from ${K}: ${Q}`)}return{}}loadEnvironmentVariables(){let $={};if(process.env.CURL_RUNNER_TIMEOUT)$.defaults={...$.defaults,timeout:Number.parseInt(process.env.CURL_RUNNER_TIMEOUT,10)};if(process.env.CURL_RUNNER_RETRIES)$.defaults={...$.defaults,retry:{...$.defaults?.retry,count:Number.parseInt(process.env.CURL_RUNNER_RETRIES,10)}};if(process.env.CURL_RUNNER_RETRY_DELAY)$.defaults={...$.defaults,retry:{...$.defaults?.retry,delay:Number.parseInt(process.env.CURL_RUNNER_RETRY_DELAY,10)}};if(process.env.CURL_RUNNER_VERBOSE)$.output={...$.output,verbose:process.env.CURL_RUNNER_VERBOSE.toLowerCase()==="true"};if(process.env.CURL_RUNNER_EXECUTION)$.execution=process.env.CURL_RUNNER_EXECUTION;if(process.env.CURL_RUNNER_CONTINUE_ON_ERROR)$.continueOnError=process.env.CURL_RUNNER_CONTINUE_ON_ERROR.toLowerCase()==="true";if(process.env.CURL_RUNNER_OUTPUT_FORMAT){let K=process.env.CURL_RUNNER_OUTPUT_FORMAT;if(["json","pretty","raw"].includes(K))$.output={...$.output,format:K}}if(process.env.CURL_RUNNER_PRETTY_LEVEL){let K=process.env.CURL_RUNNER_PRETTY_LEVEL;if(["minimal","standard","detailed"].includes(K))$.output={...$.output,prettyLevel:K}}if(process.env.CURL_RUNNER_OUTPUT_FILE)$.output={...$.output,saveToFile:process.env.CURL_RUNNER_OUTPUT_FILE};return $}async run($){try{let{files:K,options:Q}=this.parseArguments($);if(!Q.version&&!Q.help)new L().checkForUpdates().catch(()=>{});if(Q.help){this.showHelp();return}if(Q.version){console.log(`curl-runner v${A()}`);return}let X=this.loadEnvironmentVariables(),Z=await this.loadConfigFile(),z=await this.findYamlFiles(K,Q);if(z.length===0)this.logger.logError("No YAML files found"),process.exit(1);this.logger.logInfo(`Found ${z.length} YAML file(s)`);let W=this.mergeGlobalConfigs(X,Z),D=[],J=[];for(let U of z){this.logger.logInfo(`Processing: ${U}`);let{requests:G,config:I}=await this.processYamlFile(U),E=I?.output||{},_=G.map((T)=>({...T,sourceOutputConfig:E,sourceFile:U}));if(I){let{...T}=I;W=this.mergeGlobalConfigs(W,T)}J.push({file:U,requests:_,config:I}),D.push(..._)}if(Q.execution)W.execution=Q.execution;if(Q.continueOnError!==void 0)W.continueOnError=Q.continueOnError;if(Q.verbose!==void 0)W.output={...W.output,verbose:Q.verbose};if(Q.quiet!==void 0)W.output={...W.output,verbose:!1};if(Q.output)W.output={...W.output,saveToFile:Q.output};if(Q.outputFormat)W.output={...W.output,format:Q.outputFormat};if(Q.prettyLevel)W.output={...W.output,prettyLevel:Q.prettyLevel};if(Q.showHeaders!==void 0)W.output={...W.output,showHeaders:Q.showHeaders};if(Q.showBody!==void 0)W.output={...W.output,showBody:Q.showBody};if(Q.showMetrics!==void 0)W.output={...W.output,showMetrics:Q.showMetrics};if(Q.timeout)W.defaults={...W.defaults,timeout:Q.timeout};if(Q.retries||Q.noRetry){let U=Q.noRetry?0:Q.retries||0;W.defaults={...W.defaults,retry:{...W.defaults?.retry,count:U}}}if(Q.retryDelay)W.defaults={...W.defaults,retry:{...W.defaults?.retry,delay:Q.retryDelay}};if(D.length===0)this.logger.logError("No requests found in YAML files"),process.exit(1);let w=new B(W),O;if(J.length>1){let U=[],G=0;for(let _=0;_<J.length;_++){let T=J[_];this.logger.logFileHeader(T.file,T.requests.length);let R=await w.execute(T.requests);if(U.push(...R.results),G+=R.duration,_<J.length-1)console.log()}let I=U.filter((_)=>_.success).length,E=U.filter((_)=>!_.success).length;O={total:U.length,successful:I,failed:E,duration:G,results:U},w.logger.logSummary(O,!0)}else O=await w.execute(D);process.exit(O.failed>0&&!W.continueOnError?1:0)}catch(K){this.logger.logError(K instanceof Error?K.message:String(K)),process.exit(1)}}parseArguments($){let K={},Q=[];for(let X=0;X<$.length;X++){let Z=$[X];if(Z.startsWith("--")){let z=Z.slice(2),W=$[X+1];if(z==="help"||z==="version")K[z]=!0;else if(z==="no-retry")K.noRetry=!0;else if(z==="quiet")K.quiet=!0;else if(z==="show-headers")K.showHeaders=!0;else if(z==="show-body")K.showBody=!0;else if(z==="show-metrics")K.showMetrics=!0;else if(W&&!W.startsWith("--")){if(z==="continue-on-error")K.continueOnError=W==="true";else if(z==="verbose")K.verbose=W==="true";else if(z==="timeout")K.timeout=Number.parseInt(W,10);else if(z==="retries")K.retries=Number.parseInt(W,10);else if(z==="retry-delay")K.retryDelay=Number.parseInt(W,10);else if(z==="output-format"){if(["json","pretty","raw"].includes(W))K.outputFormat=W}else if(z==="pretty-level"){if(["minimal","standard","detailed"].includes(W))K.prettyLevel=W}else K[z]=W;X++}else K[z]=!0}else if(Z.startsWith("-")){let z=Z.slice(1);for(let W of z)switch(W){case"h":K.help=!0;break;case"v":K.verbose=!0;break;case"p":K.execution="parallel";break;case"c":K.continueOnError=!0;break;case"q":K.quiet=!0;break;case"o":{let D=$[X+1];if(D&&!D.startsWith("-"))K.output=D,X++;break}}}else Q.push(Z)}return{files:Q,options:K}}async findYamlFiles($,K){let Q=new Set,X=[];if($.length===0)X=K.all?["**/*.yaml","**/*.yml"]:["*.yaml","*.yml"];else for(let Z of $)try{let W=await(await import("fs/promises")).stat(Z);if(W.isDirectory()){if(X.push(`${Z}/*.yaml`,`${Z}/*.yml`),K.all)X.push(`${Z}/**/*.yaml`,`${Z}/**/*.yml`)}else if(W.isFile())X.push(Z)}catch{X.push(Z)}for(let Z of X){let z=new c(Z);for await(let W of z.scan("."))if(W.endsWith(".yaml")||W.endsWith(".yml"))Q.add(W)}return Array.from(Q).sort()}async processYamlFile($){let K=await H.parseFile($),Q=[],X;if(K.global)X=K.global;let Z={...K.global?.variables,...K.collection?.variables},z={...K.global?.defaults,...K.collection?.defaults};if(K.request){let W=this.prepareRequest(K.request,Z,z);Q.push(W)}if(K.requests)for(let W of K.requests){let D=this.prepareRequest(W,Z,z);Q.push(D)}if(K.collection?.requests)for(let W of K.collection.requests){let D=this.prepareRequest(W,Z,z);Q.push(D)}return{requests:Q,config:X}}prepareRequest($,K,Q){let X=H.interpolateVariables($,K);return H.mergeConfigs(Q,X)}mergeGlobalConfigs($,K){return{...$,...K,variables:{...$.variables,...K.variables},output:{...$.output,...K.output},defaults:{...$.defaults,...K.defaults}}}showHelp(){console.log(`
7
8
  ${this.logger.color("\uD83D\uDE80 CURL RUNNER","bright")}
8
9
 
9
10
  ${this.logger.color("USAGE:","yellow")}
@@ -78,7 +79,7 @@ ${this.logger.color("YAML STRUCTURE:","yellow")}
78
79
  requests:
79
80
  - url: \${BASE_URL}/users
80
81
  method: GET
81
- `)}}var f=new q;f.run(process.argv.slice(2));
82
+ `)}}var i=new V;i.run(process.argv.slice(2));
82
83
 
83
- //# debugId=809AF9F0078090B864756E2164756E21
84
+ //# debugId=8CFAFA63D966D23664756E2164756E21
84
85
  //# sourceMappingURL=cli.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curl-runner/cli",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "A powerful CLI tool for HTTP request management using YAML configuration",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "start": "bun run dist/cli.js",
12
- "dev": "bun --watch src/cli.ts",
12
+ "dev": "bun run src/cli.ts",
13
13
  "build": "bun run scripts/build-with-version.ts",
14
14
  "format": "biome format --write .",
15
15
  "lint": "biome lint .",
@@ -1,12 +1,15 @@
1
+ import { YamlParser } from '../parser/yaml';
1
2
  import type {
2
3
  ExecutionResult,
3
4
  ExecutionSummary,
4
5
  GlobalConfig,
5
6
  JsonValue,
6
7
  RequestConfig,
8
+ ResponseStoreContext,
7
9
  } from '../types/config';
8
10
  import { CurlBuilder } from '../utils/curl-builder';
9
11
  import { Logger } from '../utils/logger';
12
+ import { createStoreContext, extractStoreValues } from '../utils/response-store';
10
13
 
11
14
  export class RequestExecutor {
12
15
  private logger: Logger;
@@ -423,11 +426,21 @@ export class RequestExecutor {
423
426
  async executeSequential(requests: RequestConfig[]): Promise<ExecutionSummary> {
424
427
  const startTime = performance.now();
425
428
  const results: ExecutionResult[] = [];
429
+ const storeContext = createStoreContext();
426
430
 
427
431
  for (let i = 0; i < requests.length; i++) {
428
- const result = await this.executeRequest(requests[i], i + 1);
432
+ // Interpolate store variables before execution
433
+ const interpolatedRequest = this.interpolateStoreVariables(requests[i], storeContext);
434
+ const result = await this.executeRequest(interpolatedRequest, i + 1);
429
435
  results.push(result);
430
436
 
437
+ // Store values from successful responses
438
+ if (result.success && interpolatedRequest.store) {
439
+ const storedValues = extractStoreValues(result, interpolatedRequest.store);
440
+ Object.assign(storeContext, storedValues);
441
+ this.logStoredValues(storedValues);
442
+ }
443
+
431
444
  if (!result.success && !this.globalConfig.continueOnError) {
432
445
  this.logger.logError('Stopping execution due to error');
433
446
  break;
@@ -437,6 +450,39 @@ export class RequestExecutor {
437
450
  return this.createSummary(results, performance.now() - startTime);
438
451
  }
439
452
 
453
+ /**
454
+ * Interpolates store variables (${store.variableName}) in a request config.
455
+ * This is called at execution time to resolve values from previous responses.
456
+ */
457
+ private interpolateStoreVariables(
458
+ request: RequestConfig,
459
+ storeContext: ResponseStoreContext,
460
+ ): RequestConfig {
461
+ // Only interpolate if there are stored values
462
+ if (Object.keys(storeContext).length === 0) {
463
+ return request;
464
+ }
465
+
466
+ // Re-interpolate the request with store context
467
+ // We pass empty variables since static variables were already resolved
468
+ return YamlParser.interpolateVariables(request, {}, storeContext) as RequestConfig;
469
+ }
470
+
471
+ /**
472
+ * Logs stored values for debugging purposes.
473
+ */
474
+ private logStoredValues(values: ResponseStoreContext): void {
475
+ if (Object.keys(values).length === 0) {
476
+ return;
477
+ }
478
+
479
+ const entries = Object.entries(values);
480
+ for (const [key, value] of entries) {
481
+ const displayValue = value.length > 50 ? `${value.substring(0, 50)}...` : value;
482
+ this.logger.logInfo(`Stored: ${key} = "${displayValue}"`);
483
+ }
484
+ }
485
+
440
486
  async executeParallel(requests: RequestConfig[]): Promise<ExecutionSummary> {
441
487
  const startTime = performance.now();
442
488
 
@@ -0,0 +1,176 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { YamlParser } from './yaml';
3
+
4
+ describe('YamlParser.interpolateVariables with store context', () => {
5
+ test('should resolve store variables', () => {
6
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
7
+ const obj = { url: 'https://api.example.com/users/${store.userId}' };
8
+ const variables = {};
9
+ const storeContext = { userId: '123' };
10
+
11
+ const result = YamlParser.interpolateVariables(obj, variables, storeContext);
12
+ expect(result).toEqual({ url: 'https://api.example.com/users/123' });
13
+ });
14
+
15
+ test('should resolve multiple store variables in one string', () => {
16
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
17
+ const obj = { url: 'https://api.example.com/users/${store.userId}/posts/${store.postId}' };
18
+ const variables = {};
19
+ const storeContext = { userId: '123', postId: '456' };
20
+
21
+ const result = YamlParser.interpolateVariables(obj, variables, storeContext);
22
+ expect(result).toEqual({ url: 'https://api.example.com/users/123/posts/456' });
23
+ });
24
+
25
+ test('should resolve store variables in nested objects', () => {
26
+ const obj = {
27
+ headers: {
28
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
29
+ Authorization: 'Bearer ${store.token}',
30
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
31
+ 'X-User-Id': '${store.userId}',
32
+ },
33
+ };
34
+ const variables = {};
35
+ const storeContext = { token: 'jwt-token', userId: '123' };
36
+
37
+ const result = YamlParser.interpolateVariables(obj, variables, storeContext);
38
+ expect(result).toEqual({
39
+ headers: {
40
+ Authorization: 'Bearer jwt-token',
41
+ 'X-User-Id': '123',
42
+ },
43
+ });
44
+ });
45
+
46
+ test('should resolve store variables in arrays', () => {
47
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
48
+ const obj = { ids: ['${store.id1}', '${store.id2}'] };
49
+ const variables = {};
50
+ const storeContext = { id1: '1', id2: '2' };
51
+
52
+ const result = YamlParser.interpolateVariables(obj, variables, storeContext);
53
+ expect(result).toEqual({ ids: ['1', '2'] });
54
+ });
55
+
56
+ test('should keep unresolved store variables as-is', () => {
57
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
58
+ const obj = { url: 'https://api.example.com/users/${store.missing}' };
59
+ const variables = {};
60
+ const storeContext = {};
61
+
62
+ const result = YamlParser.interpolateVariables(obj, variables, storeContext);
63
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
64
+ expect(result).toEqual({ url: 'https://api.example.com/users/${store.missing}' });
65
+ });
66
+
67
+ test('should mix store variables with static variables', () => {
68
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
69
+ const obj = { url: '${BASE_URL}/users/${store.userId}' };
70
+ const variables = { BASE_URL: 'https://api.example.com' };
71
+ const storeContext = { userId: '123' };
72
+
73
+ const result = YamlParser.interpolateVariables(obj, variables, storeContext);
74
+ expect(result).toEqual({ url: 'https://api.example.com/users/123' });
75
+ });
76
+
77
+ test('should work without store context (backwards compatibility)', () => {
78
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
79
+ const obj = { url: '${BASE_URL}/users' };
80
+ const variables = { BASE_URL: 'https://api.example.com' };
81
+
82
+ const result = YamlParser.interpolateVariables(obj, variables);
83
+ expect(result).toEqual({ url: 'https://api.example.com/users' });
84
+ });
85
+
86
+ test('should resolve single store variable as exact value', () => {
87
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
88
+ const obj = { userId: '${store.userId}' };
89
+ const variables = {};
90
+ const storeContext = { userId: '123' };
91
+
92
+ const result = YamlParser.interpolateVariables(obj, variables, storeContext);
93
+ expect(result).toEqual({ userId: '123' });
94
+ });
95
+
96
+ test('should mix store variables with dynamic variables', () => {
97
+ const obj = {
98
+ headers: {
99
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
100
+ 'X-Request-ID': '${UUID}',
101
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
102
+ Authorization: 'Bearer ${store.token}',
103
+ },
104
+ };
105
+ const variables = {};
106
+ const storeContext = { token: 'my-token' };
107
+
108
+ const result = YamlParser.interpolateVariables(obj, variables, storeContext) as typeof obj;
109
+ // UUID should be a valid UUID string
110
+ expect(result.headers['X-Request-ID']).toMatch(
111
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
112
+ );
113
+ expect(result.headers.Authorization).toBe('Bearer my-token');
114
+ });
115
+
116
+ test('should resolve store variables in request body', () => {
117
+ const obj = {
118
+ body: {
119
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
120
+ userId: '${store.userId}',
121
+ name: 'Test User',
122
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${store.xxx} interpolation
123
+ parentId: '${store.parentId}',
124
+ },
125
+ };
126
+ const variables = {};
127
+ const storeContext = { userId: '123', parentId: '456' };
128
+
129
+ const result = YamlParser.interpolateVariables(obj, variables, storeContext);
130
+ expect(result).toEqual({
131
+ body: {
132
+ userId: '123',
133
+ name: 'Test User',
134
+ parentId: '456',
135
+ },
136
+ });
137
+ });
138
+ });
139
+
140
+ describe('YamlParser.resolveVariable', () => {
141
+ test('should resolve store variable', () => {
142
+ const storeContext = { userId: '123' };
143
+ const result = YamlParser.resolveVariable('store.userId', {}, storeContext);
144
+ expect(result).toBe('123');
145
+ });
146
+
147
+ test('should return null for missing store variable', () => {
148
+ const storeContext = { other: 'value' };
149
+ const result = YamlParser.resolveVariable('store.missing', {}, storeContext);
150
+ expect(result).toBeNull();
151
+ });
152
+
153
+ test('should resolve dynamic variable', () => {
154
+ const result = YamlParser.resolveVariable('UUID', {}, {});
155
+ expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
156
+ });
157
+
158
+ test('should resolve static variable', () => {
159
+ const variables = { BASE_URL: 'https://api.example.com' };
160
+ const result = YamlParser.resolveVariable('BASE_URL', variables, {});
161
+ expect(result).toBe('https://api.example.com');
162
+ });
163
+
164
+ test('should return null for unknown variable', () => {
165
+ const result = YamlParser.resolveVariable('UNKNOWN', {}, {});
166
+ expect(result).toBeNull();
167
+ });
168
+
169
+ test('should prioritize store context over static variables', () => {
170
+ // This test ensures store.X prefix is properly handled
171
+ const variables = { 'store.userId': 'static-value' };
172
+ const storeContext = { userId: 'store-value' };
173
+ const result = YamlParser.resolveVariable('store.userId', variables, storeContext);
174
+ expect(result).toBe('store-value');
175
+ });
176
+ });
@@ -1,5 +1,5 @@
1
1
  import { YAML } from 'bun';
2
- import type { RequestConfig, YamlFile } from '../types/config';
2
+ import type { RequestConfig, ResponseStoreContext, YamlFile } from '../types/config';
3
3
 
4
4
  // Using class for organization, but could be refactored to functions
5
5
  export class YamlParser {
@@ -13,31 +13,45 @@ export class YamlParser {
13
13
  return YAML.parse(content) as YamlFile;
14
14
  }
15
15
 
16
- static interpolateVariables(obj: unknown, variables: Record<string, string>): unknown {
16
+ /**
17
+ * Interpolates variables in an object, supporting:
18
+ * - Static variables: ${VAR_NAME}
19
+ * - Dynamic variables: ${UUID}, ${TIMESTAMP}, ${DATE:format}, ${TIME:format}
20
+ * - Stored response values: ${store.variableName}
21
+ *
22
+ * @param obj - The object to interpolate
23
+ * @param variables - Static variables map
24
+ * @param storeContext - Optional stored response values from previous requests
25
+ */
26
+ static interpolateVariables(
27
+ obj: unknown,
28
+ variables: Record<string, string>,
29
+ storeContext?: ResponseStoreContext,
30
+ ): unknown {
17
31
  if (typeof obj === 'string') {
18
32
  // Check if it's a single variable like ${VAR} (no other characters)
19
33
  const singleVarMatch = obj.match(/^\$\{([^}]+)\}$/);
20
34
  if (singleVarMatch) {
21
35
  const varName = singleVarMatch[1];
22
- const dynamicValue = YamlParser.resolveDynamicVariable(varName);
23
- return dynamicValue !== null ? dynamicValue : variables[varName] || obj;
36
+ const resolvedValue = YamlParser.resolveVariable(varName, variables, storeContext);
37
+ return resolvedValue !== null ? resolvedValue : obj;
24
38
  }
25
39
 
26
40
  // Handle multiple variables in the string using regex replacement
27
41
  return obj.replace(/\$\{([^}]+)\}/g, (match, varName) => {
28
- const dynamicValue = YamlParser.resolveDynamicVariable(varName);
29
- return dynamicValue !== null ? dynamicValue : variables[varName] || match;
42
+ const resolvedValue = YamlParser.resolveVariable(varName, variables, storeContext);
43
+ return resolvedValue !== null ? resolvedValue : match;
30
44
  });
31
45
  }
32
46
 
33
47
  if (Array.isArray(obj)) {
34
- return obj.map((item) => YamlParser.interpolateVariables(item, variables));
48
+ return obj.map((item) => YamlParser.interpolateVariables(item, variables, storeContext));
35
49
  }
36
50
 
37
51
  if (obj && typeof obj === 'object') {
38
52
  const result: Record<string, unknown> = {};
39
53
  for (const [key, value] of Object.entries(obj)) {
40
- result[key] = YamlParser.interpolateVariables(value, variables);
54
+ result[key] = YamlParser.interpolateVariables(value, variables, storeContext);
41
55
  }
42
56
  return result;
43
57
  }
@@ -45,6 +59,38 @@ export class YamlParser {
45
59
  return obj;
46
60
  }
47
61
 
62
+ /**
63
+ * Resolves a single variable reference.
64
+ * Priority: store context > dynamic variables > static variables
65
+ */
66
+ static resolveVariable(
67
+ varName: string,
68
+ variables: Record<string, string>,
69
+ storeContext?: ResponseStoreContext,
70
+ ): string | null {
71
+ // Check for store variable (${store.variableName})
72
+ if (varName.startsWith('store.') && storeContext) {
73
+ const storeVarName = varName.slice(6); // Remove 'store.' prefix
74
+ if (storeVarName in storeContext) {
75
+ return storeContext[storeVarName];
76
+ }
77
+ return null; // Store variable not found, return null to keep original
78
+ }
79
+
80
+ // Check for dynamic variable
81
+ const dynamicValue = YamlParser.resolveDynamicVariable(varName);
82
+ if (dynamicValue !== null) {
83
+ return dynamicValue;
84
+ }
85
+
86
+ // Check for static variable
87
+ if (varName in variables) {
88
+ return variables[varName];
89
+ }
90
+
91
+ return null;
92
+ }
93
+
48
94
  static resolveDynamicVariable(varName: string): string | null {
49
95
  // UUID generation
50
96
  if (varName === 'UUID') {
@@ -4,6 +4,18 @@ export interface JsonObject {
4
4
  }
5
5
  export interface JsonArray extends Array<JsonValue> {}
6
6
 
7
+ /**
8
+ * Configuration for storing response values as variables for subsequent requests.
9
+ * Maps a variable name to a JSON path in the response.
10
+ *
11
+ * Examples:
12
+ * - `{ "userId": "body.id" }` - Store response body's id field as ${store.userId}
13
+ * - `{ "token": "body.data.token" }` - Store nested field
14
+ * - `{ "statusCode": "status" }` - Store HTTP status code
15
+ * - `{ "contentType": "headers.content-type" }` - Store response header
16
+ */
17
+ export type StoreConfig = Record<string, string>;
18
+
7
19
  export interface RequestConfig {
8
20
  name?: string;
9
21
  url: string;
@@ -29,6 +41,18 @@ export interface RequestConfig {
29
41
  delay?: number;
30
42
  };
31
43
  variables?: Record<string, string>;
44
+ /**
45
+ * Store response values as variables for subsequent requests.
46
+ * Use JSON path syntax to extract values from the response.
47
+ *
48
+ * @example
49
+ * store:
50
+ * userId: body.id
51
+ * token: body.data.accessToken
52
+ * statusCode: status
53
+ * contentType: headers.content-type
54
+ */
55
+ store?: StoreConfig;
32
56
  expect?: {
33
57
  failure?: boolean; // If true, expect the request to fail (for negative testing)
34
58
  status?: number | number[];
@@ -104,3 +128,9 @@ export interface ExecutionSummary {
104
128
  duration: number;
105
129
  results: ExecutionResult[];
106
130
  }
131
+
132
+ /**
133
+ * Context for storing response values between sequential requests.
134
+ * Values are stored as strings and can be referenced using ${store.variableName} syntax.
135
+ */
136
+ export type ResponseStoreContext = Record<string, string>;