@curl-runner/cli 1.0.1 → 1.0.2

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 ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bun
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(`
7
+ ${this.logger.color("\uD83D\uDE80 CURL RUNNER","bright")}
8
+
9
+ ${this.logger.color("USAGE:","yellow")}
10
+ curl-runner [files...] [options]
11
+
12
+ ${this.logger.color("OPTIONS:","yellow")}
13
+ -h, --help Show this help message
14
+ -v, --verbose Enable verbose output
15
+ -q, --quiet Suppress non-error output
16
+ -p, --execution parallel Execute requests in parallel
17
+ -c, --continue-on-error Continue execution on errors
18
+ -o, --output <file> Save results to file
19
+ --all Find all YAML files recursively
20
+ --timeout <ms> Set request timeout in milliseconds
21
+ --retries <count> Set maximum retry attempts
22
+ --retry-delay <ms> Set delay between retries in milliseconds
23
+ --no-retry Disable retry mechanism
24
+ --output-format <format> Set output format (json|pretty|raw)
25
+ --pretty-level <level> Set pretty format level (minimal|standard|detailed)
26
+ --show-headers Include response headers in output
27
+ --show-body Include response body in output
28
+ --show-metrics Include performance metrics in output
29
+ --version Show version
30
+
31
+ ${this.logger.color("EXAMPLES:","yellow")}
32
+ # Run all YAML files in current directory
33
+ curl-runner
34
+
35
+ # Run specific file
36
+ curl-runner api-tests.yaml
37
+
38
+ # Run all files in a directory
39
+ curl-runner examples/
40
+
41
+ # Run all files in multiple directories
42
+ curl-runner tests/ examples/
43
+
44
+ # Run all files recursively in parallel
45
+ curl-runner --all -p
46
+
47
+ # Run directory recursively
48
+ curl-runner --all examples/
49
+
50
+ # Run with verbose output and continue on errors
51
+ curl-runner tests/*.yaml -vc
52
+
53
+ # Run with minimal pretty output (only status and errors)
54
+ curl-runner --output-format pretty --pretty-level minimal test.yaml
55
+
56
+ # Run with detailed pretty output (show all information)
57
+ curl-runner --output-format pretty --pretty-level detailed test.yaml
58
+
59
+ ${this.logger.color("YAML STRUCTURE:","yellow")}
60
+ Single request:
61
+ request:
62
+ url: https://api.example.com
63
+ method: GET
64
+
65
+ Multiple requests:
66
+ requests:
67
+ - url: https://api.example.com/users
68
+ method: GET
69
+ - url: https://api.example.com/posts
70
+ method: POST
71
+ body: { title: "Test" }
72
+
73
+ With global config:
74
+ global:
75
+ execution: parallel
76
+ variables:
77
+ BASE_URL: https://api.example.com
78
+ requests:
79
+ - url: \${BASE_URL}/users
80
+ method: GET
81
+ `)}}var f=new q;f.run(process.argv.slice(2));
82
+
83
+ //# debugId=809AF9F0078090B864756E2164756E21
84
+ //# sourceMappingURL=cli.js.map
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@curl-runner/cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "A powerful CLI tool for HTTP request management using YAML configuration",
5
5
  "type": "module",
6
- "main": "./src/cli.ts",
6
+ "main": "./dist/cli.js",
7
7
  "bin": {
8
- "curl-runner": "./src/cli.ts"
8
+ "curl-runner": "./dist/cli.js"
9
9
  },
10
10
  "scripts": {
11
- "start": "bun run src/cli.ts",
11
+ "start": "bun run dist/cli.js",
12
12
  "dev": "bun --watch src/cli.ts",
13
- "build": "bun build src/cli.ts --compile --outfile ./curl-runner",
13
+ "build": "bun run scripts/build-with-version.ts",
14
14
  "format": "biome format --write .",
15
15
  "lint": "biome lint .",
16
16
  "check": "biome check --write .",
package/src/cli.ts CHANGED
@@ -5,6 +5,8 @@ import { RequestExecutor } from './executor/request-executor';
5
5
  import { YamlParser } from './parser/yaml';
6
6
  import type { GlobalConfig, RequestConfig } from './types/config';
7
7
  import { Logger } from './utils/logger';
8
+ import { VersionChecker } from './utils/version-checker';
9
+ import { getVersion } from './version';
8
10
 
9
11
  class CurlRunnerCLI {
10
12
  private logger = new Logger();
@@ -110,13 +112,21 @@ class CurlRunnerCLI {
110
112
  try {
111
113
  const { files, options } = this.parseArguments(args);
112
114
 
115
+ // Check for updates in the background (non-blocking)
116
+ if (!options.version && !options.help) {
117
+ const versionChecker = new VersionChecker();
118
+ versionChecker.checkForUpdates().catch(() => {
119
+ // Silently ignore any errors
120
+ });
121
+ }
122
+
113
123
  if (options.help) {
114
124
  this.showHelp();
115
125
  return;
116
126
  }
117
127
 
118
128
  if (options.version) {
119
- console.log('curl-runner v1.0.0');
129
+ console.log(`curl-runner v${getVersion()}`);
120
130
  return;
121
131
  }
122
132
 
@@ -0,0 +1,30 @@
1
+ const colors = {
2
+ reset: '\x1b[0m',
3
+ bright: '\x1b[1m',
4
+ dim: '\x1b[2m',
5
+ underscore: '\x1b[4m',
6
+
7
+ black: '\x1b[30m',
8
+ red: '\x1b[31m',
9
+ green: '\x1b[32m',
10
+ yellow: '\x1b[33m',
11
+ blue: '\x1b[34m',
12
+ magenta: '\x1b[35m',
13
+ cyan: '\x1b[36m',
14
+ white: '\x1b[37m',
15
+
16
+ bgBlack: '\x1b[40m',
17
+ bgRed: '\x1b[41m',
18
+ bgGreen: '\x1b[42m',
19
+ bgYellow: '\x1b[43m',
20
+ bgBlue: '\x1b[44m',
21
+ bgMagenta: '\x1b[45m',
22
+ bgCyan: '\x1b[46m',
23
+ bgWhite: '\x1b[47m',
24
+ };
25
+
26
+ export function color(text: string, colorName: keyof typeof colors): string {
27
+ return `${colors[colorName]}${text}${colors.reset}`;
28
+ }
29
+
30
+ export type Color = keyof typeof colors;
@@ -0,0 +1,165 @@
1
+ import { getVersion } from '../version';
2
+ import { color } from './colors';
3
+
4
+ interface VersionCheckCache {
5
+ lastCheck: number;
6
+ latestVersion: string;
7
+ }
8
+
9
+ const CACHE_FILE = `${process.env.HOME}/.curl-runner-version-cache.json`;
10
+ const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
11
+ const NPM_REGISTRY_URL = 'https://registry.npmjs.org/@curl-runner/cli/latest';
12
+
13
+ export class VersionChecker {
14
+ async checkForUpdates(skipCache = false): Promise<void> {
15
+ try {
16
+ // Don't check in CI environments
17
+ if (process.env.CI) {
18
+ return;
19
+ }
20
+
21
+ const currentVersion = getVersion();
22
+
23
+ // Don't check for development versions
24
+ if (currentVersion === '0.0.0') {
25
+ return;
26
+ }
27
+
28
+ // Check cache first
29
+ if (!skipCache) {
30
+ const cached = await this.getCachedVersion();
31
+ if (cached && Date.now() - cached.lastCheck < CACHE_DURATION) {
32
+ this.compareVersions(currentVersion, cached.latestVersion);
33
+ return;
34
+ }
35
+ }
36
+
37
+ // Fetch latest version from npm registry
38
+ const latestVersion = await this.fetchLatestVersion();
39
+ if (latestVersion) {
40
+ // Update cache
41
+ await this.setCachedVersion(latestVersion);
42
+
43
+ // Compare versions
44
+ this.compareVersions(currentVersion, latestVersion);
45
+ }
46
+ } catch {
47
+ // Silently fail - we don't want to interrupt the CLI usage
48
+ // due to version check failures
49
+ }
50
+ }
51
+
52
+ private async fetchLatestVersion(): Promise<string | null> {
53
+ try {
54
+ const response = await fetch(NPM_REGISTRY_URL, {
55
+ signal: AbortSignal.timeout(3000), // 3 second timeout
56
+ });
57
+
58
+ if (!response.ok) {
59
+ return null;
60
+ }
61
+
62
+ const data = (await response.json()) as { version: string };
63
+ return data.version;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ private compareVersions(current: string, latest: string): void {
70
+ if (this.isNewerVersion(current, latest)) {
71
+ console.log();
72
+ console.log(color('╭────────────────────────────────────────────────────────╮', 'yellow'));
73
+ console.log(
74
+ color('│', 'yellow') +
75
+ ' ' +
76
+ color('│', 'yellow'),
77
+ );
78
+ console.log(
79
+ color('│', 'yellow') +
80
+ ' ' +
81
+ color('📦 New version available!', 'bright') +
82
+ ` ${color(current, 'red')} → ${color(latest, 'green')}` +
83
+ ' ' +
84
+ color('│', 'yellow'),
85
+ );
86
+ console.log(
87
+ color('│', 'yellow') +
88
+ ' ' +
89
+ color('│', 'yellow'),
90
+ );
91
+ console.log(
92
+ color('│', 'yellow') +
93
+ ' Update with: ' +
94
+ color('npm install -g @curl-runner/cli', 'cyan') +
95
+ ' ' +
96
+ color('│', 'yellow'),
97
+ );
98
+ console.log(
99
+ color('│', 'yellow') +
100
+ ' or: ' +
101
+ color('bun install -g @curl-runner/cli', 'cyan') +
102
+ ' ' +
103
+ color('│', 'yellow'),
104
+ );
105
+ console.log(
106
+ color('│', 'yellow') +
107
+ ' ' +
108
+ color('│', 'yellow'),
109
+ );
110
+ console.log(color('╰────────────────────────────────────────────────────────╯', 'yellow'));
111
+ console.log();
112
+ }
113
+ }
114
+
115
+ private isNewerVersion(current: string, latest: string): boolean {
116
+ try {
117
+ // Remove 'v' prefix if present
118
+ const currentVersion = current.replace(/^v/, '');
119
+ const latestVersion = latest.replace(/^v/, '');
120
+
121
+ const currentParts = currentVersion.split('.').map(Number);
122
+ const latestParts = latestVersion.split('.').map(Number);
123
+
124
+ for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
125
+ const currentPart = currentParts[i] || 0;
126
+ const latestPart = latestParts[i] || 0;
127
+
128
+ if (latestPart > currentPart) {
129
+ return true;
130
+ }
131
+ if (latestPart < currentPart) {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ return false;
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ private async getCachedVersion(): Promise<VersionCheckCache | null> {
143
+ try {
144
+ const file = Bun.file(CACHE_FILE);
145
+ if (await file.exists()) {
146
+ return JSON.parse(await file.text());
147
+ }
148
+ } catch {
149
+ // Ignore cache read errors
150
+ }
151
+ return null;
152
+ }
153
+
154
+ private async setCachedVersion(latestVersion: string): Promise<void> {
155
+ try {
156
+ const cache: VersionCheckCache = {
157
+ lastCheck: Date.now(),
158
+ latestVersion,
159
+ };
160
+ await Bun.write(CACHE_FILE, JSON.stringify(cache));
161
+ } catch {
162
+ // Ignore cache write errors
163
+ }
164
+ }
165
+ }
package/src/version.ts ADDED
@@ -0,0 +1,43 @@
1
+ // Version management for curl-runner CLI
2
+ // This file handles version detection for both development and compiled binaries
3
+
4
+ declare const BUILD_VERSION: string | undefined;
5
+
6
+ export function getVersion(): string {
7
+ // Check compile-time constant first (set by --define flag during build)
8
+ if (typeof BUILD_VERSION !== 'undefined') {
9
+ return BUILD_VERSION;
10
+ }
11
+
12
+ // Check environment variable (for local builds with our script)
13
+ if (process.env.CURL_RUNNER_VERSION) {
14
+ return process.env.CURL_RUNNER_VERSION;
15
+ }
16
+
17
+ // In development or npm installation, try to read from package.json
18
+ try {
19
+ // Try multiple paths to find package.json
20
+ const possiblePaths = [
21
+ '../package.json', // Development or npm installation
22
+ './package.json', // In case we're at root
23
+ '../../package.json', // In case of different directory structure
24
+ ];
25
+
26
+ for (const path of possiblePaths) {
27
+ try {
28
+ const packageJson = require(path);
29
+ if (packageJson.name === '@curl-runner/cli' && packageJson.version) {
30
+ return packageJson.version;
31
+ }
32
+ } catch {
33
+ // Try next path
34
+ }
35
+ }
36
+
37
+ // If no package.json found, return default
38
+ return '0.0.0';
39
+ } catch {
40
+ // If all else fails, return a default version
41
+ return '0.0.0';
42
+ }
43
+ }