@curl-runner/cli 1.0.1 → 1.0.3
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 +85 -0
- package/package.json +6 -6
- package/src/cli.ts +11 -1
- package/src/utils/colors.ts +30 -0
- package/src/utils/logger.ts +252 -105
- package/src/utils/version-checker.ts +165 -0
- package/src/version.ts +43 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var v=Object.create;var{getPrototypeOf:b,defineProperty:k,getOwnPropertyNames:V}=Object;var C=Object.prototype.hasOwnProperty;var x=($,w,K)=>{K=$!=null?v(b($)):{};let X=w||!$||!$.__esModule?k(K,"default",{value:$,enumerable:!0}):K;for(let Q of V($))if(!C.call(X,Q))k(X,Q,{get:()=>$[Q],enumerable:!0});return X};var A=import.meta.require;var{Glob:g}=globalThis.Bun;class T{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,W={},Z=K.match(/__CURL_METRICS_START__(.+?)__CURL_METRICS_END__/);if(Z){Q=K.replace(/__CURL_METRICS_START__.+?__CURL_METRICS_END__/,"").trim();try{W=JSON.parse(Z[1])}catch(D){}}let z={};if(W.response_code){let D=X.split(`
|
|
4
|
+
`).filter((U)=>U.includes(":"));for(let U of D){let[O,...J]=U.split(":");if(O&&J.length>0)z[O.trim()]=J.join(":").trim()}}return{success:!0,status:W.response_code||W.http_code,headers:z,body:Q,metrics:{duration:(W.time_total||0)*1000,size:W.size_download,dnsLookup:(W.time_namelookup||0)*1000,tcpConnection:(W.time_connect||0)*1000,tlsHandshake:(W.time_appconnect||0)*1000,firstByte:(W.time_starttransfer||0)*1000,download:(W.time_total||0)*1000}}}catch(w){return{success:!1,error:w instanceof Error?w.message:String(w)}}}}class B{colors;constructor($){this.colors=$}color($,w){if(!w||!this.colors[w])return $;return`${this.colors[w]}${$}${this.colors.reset}`}render($,w=" "){$.forEach((K,X)=>{let Q=X===$.length-1,W=Q?`${w}\u2514\u2500`:`${w}\u251C\u2500`;if(K.label&&K.value){let Z=K.color?this.color(K.value,K.color):K.value,z=Z.split(`
|
|
5
|
+
`);if(z.length===1)console.log(`${W} ${K.label}: ${Z}`);else{console.log(`${W} ${K.label}:`);let D=Q?`${w} `:`${w}\u2502 `;z.forEach((U)=>{console.log(`${D}${U}`)})}}else if(K.label&&!K.value)console.log(`${W} ${K.label}:`);else if(!K.label&&K.value){let Z=Q?`${w} `:`${w}\u2502 `;console.log(`${Z}${K.value}`)}if(K.children&&K.children.length>0){let Z=Q?`${w} `:`${w}\u2502 `;this.render(K.children,Z)}})}}class M{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($,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,W]=X,Z=this.colorStatusCode(Q.replace(" or ","|")),z=this.color(W,"red");console.log(` ${this.color("\u2717","red")} ${this.color("Error:","red")} Expected ${this.color("status","yellow")} ${Z}, 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[,W,Z]=Q,z=this.colorStatusCode(W.replace(" or ","|")),D=this.color(Z,"red");console.log(` ${this.color("\u2022","red")} ${this.color("status","yellow")}: expected ${z}, got ${D}`)}else{let W=X.match(/^Expected (.+?) to be (.+?), got (.+)$/);if(W){let[,Z,z,D]=W;console.log(` ${this.color("\u2022","red")} ${this.color(Z,"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]}`}logExecutionStart($,w){if(!this.shouldShowOutput())return;if(this.shouldShowSeparators())console.log(),console.log(this.color(`Executing ${$} request(s) in ${w} mode`,"dim")),console.log();else console.log()}logRequestStart($,w){return}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 z=this.formatJson($.body);console.log(z)}return}if(this.config.format==="json"){let z={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(z,null,2));return}if(!this.shouldShowOutput())return;let w=this.config.prettyLevel||"minimal",K=$.success?"green":"red",X=$.success?"\u2713":"x",Q=$.request.name||"Request";if(w==="minimal"){let z=$.request.sourceFile?this.getShortFilename($.request.sourceFile):"inline";console.log(`${this.color(X,K)} ${this.color(Q,"bright")} [${z}]`);let D=[],U=new B(this.colors);D.push({label:$.request.method||"GET",value:$.request.url,color:"blue"});let O=$.status?`${$.status}`:"ERROR";if(D.push({label:`${X} Status`,value:O,color:K}),$.metrics){let J=`${this.formatDuration($.metrics.duration)} | ${this.formatSize($.metrics.size)}`;D.push({label:"Duration",value:J,color:"cyan"})}if(U.render(D),$.error)console.log(),this.logValidationErrors($.error);console.log();return}console.log(`${this.color(X,K)} ${this.color(Q,"bright")}`);let W=[],Z=new B(this.colors);if(W.push({label:"URL",value:$.request.url,color:"blue"}),W.push({label:"Method",value:$.request.method||"GET",color:"yellow"}),W.push({label:"Status",value:String($.status||"ERROR"),color:K}),$.metrics)W.push({label:"Duration",value:this.formatDuration($.metrics.duration),color:"cyan"});if(this.shouldShowHeaders()&&$.headers&&Object.keys($.headers).length>0){let z=Object.entries($.headers).map(([D,U])=>({label:this.color(D,"dim"),value:String(U)}));W.push({label:"Headers",children:z})}if(this.shouldShowBody()&&$.body){let D=this.formatJson($.body).split(`
|
|
6
|
+
`),U=this.shouldShowRequestDetails()?1/0:10,O=D.slice(0,U);if(D.length>U)O.push(this.color(`... (${D.length-U} more lines)`,"dim"));W.push({label:"Response Body",value:O.join(`
|
|
7
|
+
`)})}if(this.shouldShowMetrics()&&$.metrics&&w==="detailed"){let z=$.metrics,D=[];if(D.push({label:"Request Duration",value:this.formatDuration(z.duration),color:"cyan"}),z.size!==void 0)D.push({label:"Response Size",value:this.formatSize(z.size),color:"cyan"});if(z.dnsLookup)D.push({label:"DNS Lookup",value:this.formatDuration(z.dnsLookup),color:"cyan"});if(z.tcpConnection)D.push({label:"TCP Connection",value:this.formatDuration(z.tcpConnection),color:"cyan"});if(z.tlsHandshake)D.push({label:"TLS Handshake",value:this.formatDuration(z.tlsHandshake),color:"cyan"});if(z.firstByte)D.push({label:"Time to First Byte",value:this.formatDuration(z.firstByte),color:"cyan"});W.push({label:"Metrics",children:D})}if(Z.render(W),$.error)console.log(),this.logValidationErrors($.error);console.log()}logSummary($,w=!1){if(this.config.format==="raw")return;if(this.config.format==="json"){let z={summary:{total:$.total,successful:$.successful,failed:$.failed,duration:$.duration},results:$.results.map((D)=>({request:{name:D.request.name,url:D.request.url,method:D.request.method||"GET"},success:D.success,status:D.status,...this.shouldShowHeaders()&&D.headers?{headers:D.headers}:{},...this.shouldShowBody()&&D.body?{body:D.body}:{},...D.error?{error:D.error}:{},...this.shouldShowMetrics()&&D.metrics?{metrics:D.metrics}:{}}))};console.log(JSON.stringify(z,null,2));return}if(!this.shouldShowOutput())return;let K=this.config.prettyLevel||"minimal";if(w)console.log();if(K==="minimal"){let z=$.failed===0?"green":"red",D=$.failed===0?`${$.total} request${$.total===1?"":"s"} completed successfully`:`${$.successful}/${$.total} request${$.total===1?"":"s"} completed, ${$.failed} failed`;console.log(`${w?"\u25C6 Global Summary":"Summary"}: ${this.color(D,z)}`);return}let X=($.successful/$.total*100).toFixed(1),Q=$.failed===0?"green":"red",W=$.failed===0?`${$.total} request${$.total===1?"":"s"} completed successfully`:`${$.successful}/${$.total} request${$.total===1?"":"s"} completed, ${$.failed} failed`,Z=w?"\u25C6 Global Summary":"Summary";if(console.log(),console.log(`${Z}: ${this.color(W,Q)} (${this.color(this.formatDuration($.duration),"cyan")})`),$.failed>0&&this.shouldShowRequestDetails())$.results.filter((z)=>!z.success).forEach((z)=>{let D=z.request.name||z.request.url;console.log(` ${this.color("\u2022","red")} ${D}: ${z.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($,w){if(!this.shouldShowOutput()||this.config.format!=="pretty")return;let K=$.replace(/.*\//,"").replace(".yaml","");console.log(),console.log(this.color(`\u25B6 ${K}.yaml`,"bright")+this.color(` (${w} request${w===1?"":"s"})`,"dim"))}}class E{logger;globalConfig;constructor($={}){this.globalConfig=$,this.logger=new M($.output)}mergeOutputConfig($){return{...this.globalConfig.output,...$.sourceOutputConfig}}async executeRequest($,w=0){let K=performance.now(),X=this.mergeOutputConfig($),Q=new M(X);Q.logRequestStart($,w);let W=T.buildCommand($);Q.logCommand(W);let Z=0,z,D=($.retry?.count||0)+1;while(Z<D){if(Z>0){if(Q.logRetry(Z,D-1),$.retry?.delay)await Bun.sleep($.retry.delay)}let O=await T.executeCurl(W);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(I){}let G={request:$,success:!0,status:O.status,headers:O.headers,body:J,metrics:{...O.metrics,duration:performance.now()-K}};if($.expect){let I=this.validateResponse(G,$.expect);if(!I.success)G.success=!1,G.error=I.error}return Q.logRequestComplete(G),G}z=O.error,Z++}let U={request:$,success:!1,error:z,metrics:{duration:performance.now()-K}};return Q.logRequestComplete(U),U}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,W]of Object.entries(w.headers)){let Z=$.headers?.[Q]||$.headers?.[Q.toLowerCase()];if(Z!==W)K.push(`Expected header ${Q}="${W}", got "${Z}"`)}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,W]of Object.entries(w)){let Z=K?`${K}.${Q}`:Q,z;if(Array.isArray($)&&this.isArraySelector(Q))z=this.getArrayValue($,Q);else z=$?.[Q];if(typeof W==="object"&&W!==null&&!Array.isArray(W)){let D=this.validateBodyProperties(z,W,Z);X.push(...D)}else{let D=this.validateValue(z,W,Z);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 W=Number(X[1]),Z=Number(X[2]);return K>=W&&K<=Z}return w.split(",").map((W)=>W.trim()).every((W)=>{let Z=W.match(/^(>=?|<=?|>|<)\s*([\d.-]+)$/);if(!Z)return!1;let z=Z[1],D=Number(Z[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,W)=>this.executeRequest(Q,W+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:N}=globalThis.Bun;class H{static async parseFile($){let K=await Bun.file($).text();return N.parse(K)}static parse($){return N.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 W=H.resolveDynamicVariable(Q);return W!==null?W: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 S(){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 w of $)try{let K=A(w);if(K.name==="@curl-runner/cli"&&K.version)return K.version}catch{}return"0.0.0"}catch{return"0.0.0"}}var q={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($,w){return`${q[w]}${$}${q.reset}`}var P=`${process.env.HOME}/.curl-runner-version-cache.json`,y=86400000,f="https://registry.npmjs.org/@curl-runner/cli/latest";class L{async checkForUpdates($=!1){try{if(process.env.CI)return;let w=S();if(w==="0.0.0")return;if(!$){let X=await this.getCachedVersion();if(X&&Date.now()-X.lastCheck<y){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(f,{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(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(w,"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($,w){try{let K=$.replace(/^v/,""),X=w.replace(/^v/,""),Q=K.split(".").map(Number),W=X.split(".").map(Number);for(let Z=0;Z<Math.max(Q.length,W.length);Z++){let z=Q[Z]||0,D=W[Z]||0;if(D>z)return!0;if(D<z)return!1}return!1}catch{return!1}}async getCachedVersion(){try{let $=Bun.file(P);if(await $.exists())return JSON.parse(await $.text())}catch{}return null}async setCachedVersion($){try{let w={lastCheck:Date.now(),latestVersion:$};await Bun.write(P,JSON.stringify(w))}catch{}}}class h{logger=new M;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 L().checkForUpdates().catch(()=>{});if(K.help){this.showHelp();return}if(K.version){console.log(`curl-runner v${S()}`);return}let X=this.loadEnvironmentVariables(),Q=await this.loadConfigFile(),W=await this.findYamlFiles(w,K);if(W.length===0)this.logger.logError("No YAML files found"),process.exit(1);this.logger.logInfo(`Found ${W.length} YAML file(s)`);let Z=this.mergeGlobalConfigs(X,Q),z=[],D=[];for(let J of W){this.logger.logInfo(`Processing: ${J}`);let{requests:G,config:I}=await this.processYamlFile(J),j=I?.output||{},_=G.map((Y)=>({...Y,sourceOutputConfig:j,sourceFile:J}));if(I){let{...Y}=I;Z=this.mergeGlobalConfigs(Z,Y)}D.push({file:J,requests:_,config:I}),z.push(..._)}if(K.execution)Z.execution=K.execution;if(K.continueOnError!==void 0)Z.continueOnError=K.continueOnError;if(K.verbose!==void 0)Z.output={...Z.output,verbose:K.verbose};if(K.quiet!==void 0)Z.output={...Z.output,verbose:!1};if(K.output)Z.output={...Z.output,saveToFile:K.output};if(K.outputFormat)Z.output={...Z.output,format:K.outputFormat};if(K.prettyLevel)Z.output={...Z.output,prettyLevel:K.prettyLevel};if(K.showHeaders!==void 0)Z.output={...Z.output,showHeaders:K.showHeaders};if(K.showBody!==void 0)Z.output={...Z.output,showBody:K.showBody};if(K.showMetrics!==void 0)Z.output={...Z.output,showMetrics:K.showMetrics};if(K.timeout)Z.defaults={...Z.defaults,timeout:K.timeout};if(K.retries||K.noRetry){let J=K.noRetry?0:K.retries||0;Z.defaults={...Z.defaults,retry:{...Z.defaults?.retry,count:J}}}if(K.retryDelay)Z.defaults={...Z.defaults,retry:{...Z.defaults?.retry,delay:K.retryDelay}};if(z.length===0)this.logger.logError("No requests found in YAML files"),process.exit(1);let U=new E(Z),O;if(D.length>1){let J=[],G=0;for(let _=0;_<D.length;_++){let Y=D[_];this.logger.logFileHeader(Y.file,Y.requests.length);let R=await U.execute(Y.requests);if(J.push(...R.results),G+=R.duration,_<D.length-1)console.log()}let I=J.filter((_)=>_.success).length,j=J.filter((_)=>!_.success).length;O={total:J.length,successful:I,failed:j,duration:G,results:J},U.logger.logSummary(O,!0)}else O=await U.execute(z);process.exit(O.failed>0&&!Z.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 W=Q.slice(2),Z=$[X+1];if(W==="help"||W==="version")w[W]=!0;else if(W==="no-retry")w.noRetry=!0;else if(W==="quiet")w.quiet=!0;else if(W==="show-headers")w.showHeaders=!0;else if(W==="show-body")w.showBody=!0;else if(W==="show-metrics")w.showMetrics=!0;else if(Z&&!Z.startsWith("--")){if(W==="continue-on-error")w.continueOnError=Z==="true";else if(W==="verbose")w.verbose=Z==="true";else if(W==="timeout")w.timeout=Number.parseInt(Z,10);else if(W==="retries")w.retries=Number.parseInt(Z,10);else if(W==="retry-delay")w.retryDelay=Number.parseInt(Z,10);else if(W==="output-format"){if(["json","pretty","raw"].includes(Z))w.outputFormat=Z}else if(W==="pretty-level"){if(["minimal","standard","detailed"].includes(Z))w.prettyLevel=Z}else w[W]=Z;X++}else w[W]=!0}else if(Q.startsWith("-")){let W=Q.slice(1);for(let Z of W)switch(Z){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 Z=await(await import("fs/promises")).stat(Q);if(Z.isDirectory()){if(X.push(`${Q}/*.yaml`,`${Q}/*.yml`),w.all)X.push(`${Q}/**/*.yaml`,`${Q}/**/*.yml`)}else if(Z.isFile())X.push(Q)}catch{X.push(Q)}for(let Q of X){let W=new g(Q);for await(let Z of W.scan("."))if(Z.endsWith(".yaml")||Z.endsWith(".yml"))K.add(Z)}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},W={...w.global?.defaults,...w.collection?.defaults};if(w.request){let Z=this.prepareRequest(w.request,Q,W);K.push(Z)}if(w.requests)for(let Z of w.requests){let z=this.prepareRequest(Z,Q,W);K.push(z)}if(w.collection?.requests)for(let Z of w.collection.requests){let z=this.prepareRequest(Z,Q,W);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(`
|
|
8
|
+
${this.logger.color("\uD83D\uDE80 CURL RUNNER","bright")}
|
|
9
|
+
|
|
10
|
+
${this.logger.color("USAGE:","yellow")}
|
|
11
|
+
curl-runner [files...] [options]
|
|
12
|
+
|
|
13
|
+
${this.logger.color("OPTIONS:","yellow")}
|
|
14
|
+
-h, --help Show this help message
|
|
15
|
+
-v, --verbose Enable verbose output
|
|
16
|
+
-q, --quiet Suppress non-error output
|
|
17
|
+
-p, --execution parallel Execute requests in parallel
|
|
18
|
+
-c, --continue-on-error Continue execution on errors
|
|
19
|
+
-o, --output <file> Save results to file
|
|
20
|
+
--all Find all YAML files recursively
|
|
21
|
+
--timeout <ms> Set request timeout in milliseconds
|
|
22
|
+
--retries <count> Set maximum retry attempts
|
|
23
|
+
--retry-delay <ms> Set delay between retries in milliseconds
|
|
24
|
+
--no-retry Disable retry mechanism
|
|
25
|
+
--output-format <format> Set output format (json|pretty|raw)
|
|
26
|
+
--pretty-level <level> Set pretty format level (minimal|standard|detailed)
|
|
27
|
+
--show-headers Include response headers in output
|
|
28
|
+
--show-body Include response body in output
|
|
29
|
+
--show-metrics Include performance metrics in output
|
|
30
|
+
--version Show version
|
|
31
|
+
|
|
32
|
+
${this.logger.color("EXAMPLES:","yellow")}
|
|
33
|
+
# Run all YAML files in current directory
|
|
34
|
+
curl-runner
|
|
35
|
+
|
|
36
|
+
# Run specific file
|
|
37
|
+
curl-runner api-tests.yaml
|
|
38
|
+
|
|
39
|
+
# Run all files in a directory
|
|
40
|
+
curl-runner examples/
|
|
41
|
+
|
|
42
|
+
# Run all files in multiple directories
|
|
43
|
+
curl-runner tests/ examples/
|
|
44
|
+
|
|
45
|
+
# Run all files recursively in parallel
|
|
46
|
+
curl-runner --all -p
|
|
47
|
+
|
|
48
|
+
# Run directory recursively
|
|
49
|
+
curl-runner --all examples/
|
|
50
|
+
|
|
51
|
+
# Run with verbose output and continue on errors
|
|
52
|
+
curl-runner tests/*.yaml -vc
|
|
53
|
+
|
|
54
|
+
# Run with minimal pretty output (only status and errors)
|
|
55
|
+
curl-runner --output-format pretty --pretty-level minimal test.yaml
|
|
56
|
+
|
|
57
|
+
# Run with detailed pretty output (show all information)
|
|
58
|
+
curl-runner --output-format pretty --pretty-level detailed test.yaml
|
|
59
|
+
|
|
60
|
+
${this.logger.color("YAML STRUCTURE:","yellow")}
|
|
61
|
+
Single request:
|
|
62
|
+
request:
|
|
63
|
+
url: https://api.example.com
|
|
64
|
+
method: GET
|
|
65
|
+
|
|
66
|
+
Multiple requests:
|
|
67
|
+
requests:
|
|
68
|
+
- url: https://api.example.com/users
|
|
69
|
+
method: GET
|
|
70
|
+
- url: https://api.example.com/posts
|
|
71
|
+
method: POST
|
|
72
|
+
body: { title: "Test" }
|
|
73
|
+
|
|
74
|
+
With global config:
|
|
75
|
+
global:
|
|
76
|
+
execution: parallel
|
|
77
|
+
variables:
|
|
78
|
+
BASE_URL: https://api.example.com
|
|
79
|
+
requests:
|
|
80
|
+
- url: \${BASE_URL}/users
|
|
81
|
+
method: GET
|
|
82
|
+
`)}}var p=new h;p.run(process.argv.slice(2));
|
|
83
|
+
|
|
84
|
+
//# debugId=A2B9AAA9B03B2F7164756E2164756E21
|
|
85
|
+
//# sourceMappingURL=cli.js.map
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@curl-runner/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "A powerful CLI tool for HTTP request management using YAML configuration",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./
|
|
6
|
+
"main": "./dist/cli.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"curl-runner": "./
|
|
8
|
+
"curl-runner": "./dist/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"start": "bun run
|
|
12
|
-
"dev": "bun
|
|
13
|
-
"build": "bun
|
|
11
|
+
"start": "bun run dist/cli.js",
|
|
12
|
+
"dev": "bun run src/cli.ts",
|
|
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(
|
|
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;
|
package/src/utils/logger.ts
CHANGED
|
@@ -5,6 +5,64 @@ import type {
|
|
|
5
5
|
RequestConfig,
|
|
6
6
|
} from '../types/config';
|
|
7
7
|
|
|
8
|
+
interface TreeNode {
|
|
9
|
+
label: string;
|
|
10
|
+
value?: string;
|
|
11
|
+
children?: TreeNode[];
|
|
12
|
+
color?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class TreeRenderer {
|
|
16
|
+
private colors: Record<string, string>;
|
|
17
|
+
|
|
18
|
+
constructor(colors: Record<string, string>) {
|
|
19
|
+
this.colors = colors;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private color(text: string, colorName: string): string {
|
|
23
|
+
if (!colorName || !this.colors[colorName]) {
|
|
24
|
+
return text;
|
|
25
|
+
}
|
|
26
|
+
return `${this.colors[colorName]}${text}${this.colors.reset}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
render(nodes: TreeNode[], basePrefix: string = ' '): void {
|
|
30
|
+
nodes.forEach((node, index) => {
|
|
31
|
+
const isLast = index === nodes.length - 1;
|
|
32
|
+
const prefix = isLast ? `${basePrefix}└─` : `${basePrefix}├─`;
|
|
33
|
+
|
|
34
|
+
if (node.label && node.value) {
|
|
35
|
+
// Regular labeled node with value
|
|
36
|
+
const displayValue = node.color ? this.color(node.value, node.color) : node.value;
|
|
37
|
+
|
|
38
|
+
// Handle multiline values (like Response Body)
|
|
39
|
+
const lines = displayValue.split('\n');
|
|
40
|
+
if (lines.length === 1) {
|
|
41
|
+
console.log(`${prefix} ${node.label}: ${displayValue}`);
|
|
42
|
+
} else {
|
|
43
|
+
console.log(`${prefix} ${node.label}:`);
|
|
44
|
+
const contentPrefix = isLast ? `${basePrefix} ` : `${basePrefix}│ `;
|
|
45
|
+
lines.forEach((line) => {
|
|
46
|
+
console.log(`${contentPrefix}${line}`);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
} else if (node.label && !node.value) {
|
|
50
|
+
// Section header (like "Headers:" or "Metrics:")
|
|
51
|
+
console.log(`${prefix} ${node.label}:`);
|
|
52
|
+
} else if (!node.label && node.value) {
|
|
53
|
+
// Content line without label (like response body lines)
|
|
54
|
+
const continuationPrefix = isLast ? `${basePrefix} ` : `${basePrefix}│ `;
|
|
55
|
+
console.log(`${continuationPrefix}${node.value}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (node.children && node.children.length > 0) {
|
|
59
|
+
const childPrefix = isLast ? `${basePrefix} ` : `${basePrefix}│ `;
|
|
60
|
+
this.render(node.children, childPrefix);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
8
66
|
export class Logger {
|
|
9
67
|
private config: GlobalConfig['output'];
|
|
10
68
|
|
|
@@ -40,7 +98,7 @@ export class Logger {
|
|
|
40
98
|
showBody: true,
|
|
41
99
|
showMetrics: false,
|
|
42
100
|
format: 'pretty',
|
|
43
|
-
prettyLevel: '
|
|
101
|
+
prettyLevel: 'minimal',
|
|
44
102
|
...config,
|
|
45
103
|
};
|
|
46
104
|
}
|
|
@@ -239,56 +297,25 @@ export class Logger {
|
|
|
239
297
|
return `${(bytes / 1024 ** i).toFixed(2)} ${sizes[i]}`;
|
|
240
298
|
}
|
|
241
299
|
|
|
242
|
-
private printSeparator(char: string = '─', length: number = 60): void {
|
|
243
|
-
console.log(this.color(char.repeat(length), 'dim'));
|
|
244
|
-
}
|
|
245
|
-
|
|
246
300
|
logExecutionStart(count: number, mode: string): void {
|
|
247
301
|
if (!this.shouldShowOutput()) {
|
|
248
302
|
return;
|
|
249
303
|
}
|
|
250
304
|
|
|
251
305
|
if (this.shouldShowSeparators()) {
|
|
252
|
-
|
|
253
|
-
console.log(this.color(
|
|
254
|
-
console.log(
|
|
255
|
-
|
|
306
|
+
console.log(); // Add spacing before the execution header
|
|
307
|
+
console.log(this.color(`Executing ${count} request(s) in ${mode} mode`, 'dim'));
|
|
308
|
+
console.log();
|
|
309
|
+
} else {
|
|
310
|
+
// For minimal format, still add spacing after processing info
|
|
256
311
|
console.log();
|
|
257
312
|
}
|
|
258
313
|
}
|
|
259
314
|
|
|
260
|
-
logRequestStart(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const name = config.name || `Request #${index}`;
|
|
266
|
-
const sourceFile = config.sourceFile
|
|
267
|
-
? ` ${this.color(`[${this.getShortFilename(config.sourceFile)}]`, 'cyan')}`
|
|
268
|
-
: '';
|
|
269
|
-
console.log(this.color(`▶ ${name}`, 'bright') + sourceFile);
|
|
270
|
-
console.log(
|
|
271
|
-
` ${this.color(config.method || 'GET', 'yellow')} ${this.color(config.url, 'blue')}`,
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
this.shouldShowRequestDetails() &&
|
|
276
|
-
config.headers &&
|
|
277
|
-
Object.keys(config.headers).length > 0
|
|
278
|
-
) {
|
|
279
|
-
console.log(this.color(' Headers:', 'dim'));
|
|
280
|
-
for (const [key, value] of Object.entries(config.headers)) {
|
|
281
|
-
console.log(` ${key}: ${value}`);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (this.shouldShowRequestDetails() && config.body) {
|
|
286
|
-
console.log(this.color(' Body:', 'dim'));
|
|
287
|
-
const bodyStr = this.formatJson(config.body);
|
|
288
|
-
for (const line of bodyStr.split('\n')) {
|
|
289
|
-
console.log(` ${line}`);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
315
|
+
logRequestStart(_config: RequestConfig, _index: number): void {
|
|
316
|
+
// In the new format, we show everything in logRequestComplete
|
|
317
|
+
// This method is kept for compatibility but simplified
|
|
318
|
+
return;
|
|
292
319
|
}
|
|
293
320
|
|
|
294
321
|
logCommand(command: string): void {
|
|
@@ -336,62 +363,173 @@ export class Logger {
|
|
|
336
363
|
return;
|
|
337
364
|
}
|
|
338
365
|
|
|
366
|
+
const level = this.config.prettyLevel || 'minimal';
|
|
339
367
|
const statusColor = result.success ? 'green' : 'red';
|
|
340
|
-
const statusIcon = result.success ? '✓' : '
|
|
368
|
+
const statusIcon = result.success ? '✓' : 'x';
|
|
369
|
+
const name = result.request.name || 'Request';
|
|
370
|
+
|
|
371
|
+
if (level === 'minimal') {
|
|
372
|
+
// Minimal format: clean tree structure but compact
|
|
373
|
+
const fileTag = result.request.sourceFile
|
|
374
|
+
? this.getShortFilename(result.request.sourceFile)
|
|
375
|
+
: 'inline';
|
|
376
|
+
console.log(
|
|
377
|
+
`${this.color(statusIcon, statusColor)} ${this.color(name, 'bright')} [${fileTag}]`,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const treeNodes: TreeNode[] = [];
|
|
381
|
+
const renderer = new TreeRenderer(this.colors);
|
|
382
|
+
|
|
383
|
+
treeNodes.push({
|
|
384
|
+
label: result.request.method || 'GET',
|
|
385
|
+
value: result.request.url,
|
|
386
|
+
color: 'blue',
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const statusText = result.status ? `${result.status}` : 'ERROR';
|
|
390
|
+
treeNodes.push({
|
|
391
|
+
label: `${statusIcon} Status`,
|
|
392
|
+
value: statusText,
|
|
393
|
+
color: statusColor,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
if (result.metrics) {
|
|
397
|
+
const durationSize = `${this.formatDuration(result.metrics.duration)} | ${this.formatSize(result.metrics.size)}`;
|
|
398
|
+
treeNodes.push({
|
|
399
|
+
label: 'Duration',
|
|
400
|
+
value: durationSize,
|
|
401
|
+
color: 'cyan',
|
|
402
|
+
});
|
|
403
|
+
}
|
|
341
404
|
|
|
342
|
-
|
|
343
|
-
` ${this.color(statusIcon, statusColor)} ` +
|
|
344
|
-
`Status: ${this.color(String(result.status || 'ERROR'), statusColor)}`,
|
|
345
|
-
);
|
|
405
|
+
renderer.render(treeNodes);
|
|
346
406
|
|
|
347
|
-
|
|
348
|
-
|
|
407
|
+
if (result.error) {
|
|
408
|
+
console.log();
|
|
409
|
+
this.logValidationErrors(result.error);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
console.log();
|
|
413
|
+
return;
|
|
349
414
|
}
|
|
350
415
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const parts = [`Duration: ${this.color(this.formatDuration(metrics.duration), 'cyan')}`];
|
|
416
|
+
// Standard and detailed formats: use clean tree structure
|
|
417
|
+
console.log(`${this.color(statusIcon, statusColor)} ${this.color(name, 'bright')}`);
|
|
354
418
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
419
|
+
// Build tree structure
|
|
420
|
+
const treeNodes: TreeNode[] = [];
|
|
421
|
+
const renderer = new TreeRenderer(this.colors);
|
|
358
422
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
parts.push(`TLS: ${this.formatDuration(metrics.tlsHandshake)}`);
|
|
368
|
-
}
|
|
369
|
-
if (metrics.firstByte) {
|
|
370
|
-
parts.push(`TTFB: ${this.formatDuration(metrics.firstByte)}`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
423
|
+
// Main info nodes
|
|
424
|
+
treeNodes.push({ label: 'URL', value: result.request.url, color: 'blue' });
|
|
425
|
+
treeNodes.push({ label: 'Method', value: result.request.method || 'GET', color: 'yellow' });
|
|
426
|
+
treeNodes.push({
|
|
427
|
+
label: 'Status',
|
|
428
|
+
value: String(result.status || 'ERROR'),
|
|
429
|
+
color: statusColor,
|
|
430
|
+
});
|
|
373
431
|
|
|
374
|
-
|
|
432
|
+
if (result.metrics) {
|
|
433
|
+
treeNodes.push({
|
|
434
|
+
label: 'Duration',
|
|
435
|
+
value: this.formatDuration(result.metrics.duration),
|
|
436
|
+
color: 'cyan',
|
|
437
|
+
});
|
|
375
438
|
}
|
|
376
439
|
|
|
440
|
+
// Add headers section if needed
|
|
377
441
|
if (this.shouldShowHeaders() && result.headers && Object.keys(result.headers).length > 0) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
442
|
+
const headerChildren: TreeNode[] = Object.entries(result.headers).map(([key, value]) => ({
|
|
443
|
+
label: this.color(key, 'dim'),
|
|
444
|
+
value: String(value),
|
|
445
|
+
}));
|
|
446
|
+
|
|
447
|
+
treeNodes.push({
|
|
448
|
+
label: 'Headers',
|
|
449
|
+
children: headerChildren,
|
|
450
|
+
});
|
|
382
451
|
}
|
|
383
452
|
|
|
453
|
+
// Add body section if needed
|
|
384
454
|
if (this.shouldShowBody() && result.body) {
|
|
385
|
-
console.log(this.color(' Response Body:', 'dim'));
|
|
386
455
|
const bodyStr = this.formatJson(result.body);
|
|
387
456
|
const lines = bodyStr.split('\n');
|
|
388
457
|
const maxLines = this.shouldShowRequestDetails() ? Infinity : 10;
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
458
|
+
const bodyLines = lines.slice(0, maxLines);
|
|
459
|
+
|
|
392
460
|
if (lines.length > maxLines) {
|
|
393
|
-
|
|
461
|
+
bodyLines.push(this.color(`... (${lines.length - maxLines} more lines)`, 'dim'));
|
|
394
462
|
}
|
|
463
|
+
|
|
464
|
+
treeNodes.push({
|
|
465
|
+
label: 'Response Body',
|
|
466
|
+
value: bodyLines.join('\n'),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Add detailed metrics section if needed
|
|
471
|
+
if (this.shouldShowMetrics() && result.metrics && level === 'detailed') {
|
|
472
|
+
const metrics = result.metrics;
|
|
473
|
+
const metricChildren: TreeNode[] = [];
|
|
474
|
+
|
|
475
|
+
metricChildren.push({
|
|
476
|
+
label: 'Request Duration',
|
|
477
|
+
value: this.formatDuration(metrics.duration),
|
|
478
|
+
color: 'cyan',
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
if (metrics.size !== undefined) {
|
|
482
|
+
metricChildren.push({
|
|
483
|
+
label: 'Response Size',
|
|
484
|
+
value: this.formatSize(metrics.size),
|
|
485
|
+
color: 'cyan',
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (metrics.dnsLookup) {
|
|
490
|
+
metricChildren.push({
|
|
491
|
+
label: 'DNS Lookup',
|
|
492
|
+
value: this.formatDuration(metrics.dnsLookup),
|
|
493
|
+
color: 'cyan',
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (metrics.tcpConnection) {
|
|
498
|
+
metricChildren.push({
|
|
499
|
+
label: 'TCP Connection',
|
|
500
|
+
value: this.formatDuration(metrics.tcpConnection),
|
|
501
|
+
color: 'cyan',
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (metrics.tlsHandshake) {
|
|
506
|
+
metricChildren.push({
|
|
507
|
+
label: 'TLS Handshake',
|
|
508
|
+
value: this.formatDuration(metrics.tlsHandshake),
|
|
509
|
+
color: 'cyan',
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (metrics.firstByte) {
|
|
514
|
+
metricChildren.push({
|
|
515
|
+
label: 'Time to First Byte',
|
|
516
|
+
value: this.formatDuration(metrics.firstByte),
|
|
517
|
+
color: 'cyan',
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
treeNodes.push({
|
|
522
|
+
label: 'Metrics',
|
|
523
|
+
children: metricChildren,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Render the tree
|
|
528
|
+
renderer.render(treeNodes);
|
|
529
|
+
|
|
530
|
+
if (result.error) {
|
|
531
|
+
console.log();
|
|
532
|
+
this.logValidationErrors(result.error);
|
|
395
533
|
}
|
|
396
534
|
|
|
397
535
|
console.log();
|
|
@@ -435,53 +573,64 @@ export class Logger {
|
|
|
435
573
|
return;
|
|
436
574
|
}
|
|
437
575
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
576
|
+
const level = this.config.prettyLevel || 'minimal';
|
|
577
|
+
|
|
578
|
+
// Add spacing for global summary
|
|
579
|
+
if (isGlobal) {
|
|
580
|
+
console.log(); // Extra spacing before global summary
|
|
443
581
|
}
|
|
444
582
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
summary.failed === 0 ? 'green' :
|
|
583
|
+
if (level === 'minimal') {
|
|
584
|
+
// Simple one-line summary for minimal, similar to docs example
|
|
585
|
+
const statusColor = summary.failed === 0 ? 'green' : 'red';
|
|
586
|
+
const successText =
|
|
587
|
+
summary.failed === 0
|
|
588
|
+
? `${summary.total} request${summary.total === 1 ? '' : 's'} completed successfully`
|
|
589
|
+
: `${summary.successful}/${summary.total} request${summary.total === 1 ? '' : 's'} completed, ${summary.failed} failed`;
|
|
448
590
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
591
|
+
const summaryPrefix = isGlobal ? '◆ Global Summary' : 'Summary';
|
|
592
|
+
console.log(`${summaryPrefix}: ${this.color(successText, statusColor)}`);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Compact summary for standard/detailed - much simpler
|
|
597
|
+
const _successRate = ((summary.successful / summary.total) * 100).toFixed(1);
|
|
598
|
+
const statusColor = summary.failed === 0 ? 'green' : 'red';
|
|
599
|
+
const successText =
|
|
600
|
+
summary.failed === 0
|
|
601
|
+
? `${summary.total} request${summary.total === 1 ? '' : 's'} completed successfully`
|
|
602
|
+
: `${summary.successful}/${summary.total} request${summary.total === 1 ? '' : 's'} completed, ${summary.failed} failed`;
|
|
603
|
+
|
|
604
|
+
const summaryPrefix = isGlobal ? '◆ Global Summary' : 'Summary';
|
|
605
|
+
console.log();
|
|
606
|
+
console.log(
|
|
607
|
+
`${summaryPrefix}: ${this.color(successText, statusColor)} (${this.color(this.formatDuration(summary.duration), 'cyan')})`,
|
|
608
|
+
);
|
|
454
609
|
|
|
455
610
|
if (summary.failed > 0 && this.shouldShowRequestDetails()) {
|
|
456
|
-
console.log();
|
|
457
|
-
console.log(this.color(' Failed Requests:', 'red'));
|
|
458
611
|
summary.results
|
|
459
612
|
.filter((r) => !r.success)
|
|
460
613
|
.forEach((r) => {
|
|
461
614
|
const name = r.request.name || r.request.url;
|
|
462
|
-
console.log(`
|
|
615
|
+
console.log(` ${this.color('•', 'red')} ${name}: ${r.error}`);
|
|
463
616
|
});
|
|
464
617
|
}
|
|
465
|
-
|
|
466
|
-
if (this.shouldShowSeparators()) {
|
|
467
|
-
this.printSeparator('═');
|
|
468
|
-
}
|
|
469
618
|
}
|
|
470
619
|
|
|
471
620
|
logError(message: string): void {
|
|
472
|
-
console.error(this.color(
|
|
621
|
+
console.error(this.color(`✗ ${message}`, 'red'));
|
|
473
622
|
}
|
|
474
623
|
|
|
475
624
|
logWarning(message: string): void {
|
|
476
|
-
console.warn(this.color(
|
|
625
|
+
console.warn(this.color(`⚠ ${message}`, 'yellow'));
|
|
477
626
|
}
|
|
478
627
|
|
|
479
628
|
logInfo(message: string): void {
|
|
480
|
-
console.log(this.color(
|
|
629
|
+
console.log(this.color(`ℹ ${message}`, 'blue'));
|
|
481
630
|
}
|
|
482
631
|
|
|
483
632
|
logSuccess(message: string): void {
|
|
484
|
-
console.log(this.color(
|
|
633
|
+
console.log(this.color(`✓ ${message}`, 'green'));
|
|
485
634
|
}
|
|
486
635
|
|
|
487
636
|
logFileHeader(fileName: string, requestCount: number): void {
|
|
@@ -491,11 +640,9 @@ export class Logger {
|
|
|
491
640
|
|
|
492
641
|
const shortName = fileName.replace(/.*\//, '').replace('.yaml', '');
|
|
493
642
|
console.log();
|
|
494
|
-
this.printSeparator('─');
|
|
495
643
|
console.log(
|
|
496
|
-
this.color(
|
|
644
|
+
this.color(`▶ ${shortName}.yaml`, 'bright') +
|
|
497
645
|
this.color(` (${requestCount} request${requestCount === 1 ? '' : 's'})`, 'dim'),
|
|
498
646
|
);
|
|
499
|
-
this.printSeparator('─');
|
|
500
647
|
}
|
|
501
648
|
}
|
|
@@ -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
|
+
}
|