@curl-runner/cli 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +24 -7
- package/package.json +1 -1
- package/src/ci-exit.test.ts +215 -0
- package/src/cli.ts +122 -2
- package/src/types/config.ts +30 -0
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
var
|
|
4
|
-
`).filter((w)=>w.includes(":"));for(let w of
|
|
5
|
-
`);if(
|
|
6
|
-
`),w=this.shouldShowRequestDetails()?1/0:10,
|
|
7
|
-
`)})}if(this.shouldShowMetrics()&&$.metrics&&K==="detailed"){let D=$.metrics,J=[];if(J.push({label:"Request Duration",value:this.formatDuration(D.duration),color:"cyan"}),D.size!==void 0)J.push({label:"Response Size",value:this.formatSize(D.size),color:"cyan"});if(D.dnsLookup)J.push({label:"DNS Lookup",value:this.formatDuration(D.dnsLookup),color:"cyan"});if(D.tcpConnection)J.push({label:"TCP Connection",value:this.formatDuration(D.tcpConnection),color:"cyan"});if(D.tlsHandshake)J.push({label:"TLS Handshake",value:this.formatDuration(D.tlsHandshake),color:"cyan"});if(D.firstByte)J.push({label:"Time to First Byte",value:this.formatDuration(D.firstByte),color:"cyan"});z.push({label:"Metrics",children:J})}if(W.render(z),$.error)console.log(),this.logValidationErrors($.error);console.log()}logSummary($,K=!1){if(this.config.format==="raw")return;if(this.config.format==="json"){let D={summary:{total:$.total,successful:$.successful,failed:$.failed,duration:$.duration},results:$.results.map((J)=>({request:{name:J.request.name,url:J.request.url,method:J.request.method||"GET"},success:J.success,status:J.status,...this.shouldShowHeaders()&&J.headers?{headers:J.headers}:{},...this.shouldShowBody()&&J.body?{body:J.body}:{},...J.error?{error:J.error}:{},...this.shouldShowMetrics()&&J.metrics?{metrics:J.metrics}:{}}))};console.log(JSON.stringify(D,null,2));return}if(!this.shouldShowOutput())return;let Q=this.config.prettyLevel||"minimal";if(K)console.log();if(Q==="minimal"){let D=$.failed===0?"green":"red",J=$.failed===0?`${$.total} request${$.total===1?"":"s"} completed successfully`:`${$.successful}/${$.total} request${$.total===1?"":"s"} completed, ${$.failed} failed`;console.log(`${K?"\u25C6 Global Summary":"Summary"}: ${this.color(J,D)}`);return}let X=($.successful/$.total*100).toFixed(1),Z=$.failed===0?"green":"red",z=$.failed===0?`${$.total} request${$.total===1?"":"s"} completed successfully`:`${$.successful}/${$.total} request${$.total===1?"":"s"} completed, ${$.failed} failed`,W=K?"\u25C6 Global Summary":"Summary";if(console.log(),console.log(`${W}: ${this.color(z,Z)} (${this.color(this.formatDuration($.duration),"cyan")})`),$.failed>0&&this.shouldShowRequestDetails())$.results.filter((D)=>!D.success).forEach((D)=>{let J=D.request.name||D.request.url;console.log(` ${this.color("\u2022","red")} ${J}: ${D.error}`)})}logError($){console.error(this.color(`\u2717 ${$}`,"red"))}logWarning($){console.warn(this.color(`\u26A0 ${$}`,"yellow"))}logInfo($){console.log(this.color(`\u2139 ${$}`,"blue"))}logSuccess($){console.log(this.color(`\u2713 ${$}`,"green"))}logFileHeader($,K){if(!this.shouldShowOutput()||this.config.format!=="pretty")return;let Q=$.replace(/.*\//,"").replace(".yaml","");console.log(),console.log(this.color(`\u25B6 ${Q}.yaml`,"bright")+this.color(` (${K} request${K===1?"":"s"})`,"dim"))}}function g($,K){let Q=K.split("."),X=$;for(let Z of Q){if(X===null||X===void 0)return;if(typeof X!=="object")return;let z=Z.match(/^(\w+)\[(\d+)\]$/);if(z){let[,W,D]=z,J=Number.parseInt(D,10);if(X=X[W],Array.isArray(X))X=X[J];else return}else if(/^\d+$/.test(Z)&&Array.isArray(X))X=X[Number.parseInt(Z,10)];else X=X[Z]}return X}function p($){if($===void 0||$===null)return"";if(typeof $==="string")return $;if(typeof $==="number"||typeof $==="boolean")return String($);return JSON.stringify($)}function N($,K){let Q={},X={status:$.status,headers:$.headers||{},body:$.body,metrics:$.metrics};for(let[Z,z]of Object.entries(K)){let W=g(X,z);Q[Z]=p(W)}return Q}function P(){return{}}class B{logger;globalConfig;constructor($={}){this.globalConfig=$,this.logger=new Y($.output)}mergeOutputConfig($){return{...this.globalConfig.output,...$.sourceOutputConfig}}async executeRequest($,K=0){let Q=performance.now(),X=this.mergeOutputConfig($),Z=new Y(X);Z.logRequestStart($,K);let z=M.buildCommand($);Z.logCommand(z);let W=0,D,J=($.retry?.count||0)+1;while(W<J){if(W>0){if(Z.logRetry(W,J-1),$.retry?.delay)await Bun.sleep($.retry.delay)}let O=await M.executeCurl(z);if(O.success){let U=O.body;try{if(O.headers?.["content-type"]?.includes("application/json")||U&&(U.trim().startsWith("{")||U.trim().startsWith("[")))U=JSON.parse(U)}catch(I){}let G={request:$,success:!0,status:O.status,headers:O.headers,body:U,metrics:{...O.metrics,duration:performance.now()-Q}};if($.expect){let I=this.validateResponse(G,$.expect);if(!I.success)G.success=!1,G.error=I.error}return Z.logRequestComplete(G),G}D=O.error,W++}let w={request:$,success:!1,error:D,metrics:{duration:performance.now()-Q}};return Z.logRequestComplete(w),w}validateResponse($,K){if(!K)return{success:!0};let Q=[];if(K.status!==void 0){let Z=Array.isArray(K.status)?K.status:[K.status];if(!Z.includes($.status||0))Q.push(`Expected status ${Z.join(" or ")}, got ${$.status}`)}if(K.headers)for(let[Z,z]of Object.entries(K.headers)){let W=$.headers?.[Z]||$.headers?.[Z.toLowerCase()];if(W!==z)Q.push(`Expected header ${Z}="${z}", got "${W}"`)}if(K.body!==void 0){let Z=this.validateBodyProperties($.body,K.body,"");if(Z.length>0)Q.push(...Z)}if(K.responseTime!==void 0&&$.metrics){let Z=$.metrics.duration;if(!this.validateRangePattern(Z,K.responseTime))Q.push(`Expected response time to match ${K.responseTime}ms, got ${Z.toFixed(2)}ms`)}let X=Q.length>0;if(K.failure===!0){if(X)return{success:!1,error:Q.join("; ")};let Z=$.status||0;if(Z>=400)return{success:!0};else return{success:!1,error:`Expected request to fail (4xx/5xx) but got status ${Z}`}}else if(X)return{success:!1,error:Q.join("; ")};else return{success:!0}}validateBodyProperties($,K,Q){let X=[];if(typeof K!=="object"||K===null){let Z=this.validateValue($,K,Q||"body");if(!Z.isValid)X.push(Z.error);return X}if(Array.isArray(K)){let Z=this.validateValue($,K,Q||"body");if(!Z.isValid)X.push(Z.error);return X}for(let[Z,z]of Object.entries(K)){let W=Q?`${Q}.${Z}`:Z,D;if(Array.isArray($)&&this.isArraySelector(Z))D=this.getArrayValue($,Z);else D=$?.[Z];if(typeof z==="object"&&z!==null&&!Array.isArray(z)){let J=this.validateBodyProperties(D,z,W);X.push(...J)}else{let J=this.validateValue(D,z,W);if(!J.isValid)X.push(J.error)}}return X}validateValue($,K,Q){if(K==="*")return{isValid:!0};if(Array.isArray(K)){if(!K.some((Z)=>{if(Z==="*")return!0;if(typeof Z==="string"&&this.isRegexPattern(Z))return this.validateRegexPattern($,Z);if(typeof Z==="string"&&this.isRangePattern(Z))return this.validateRangePattern($,Z);return $===Z}))return{isValid:!1,error:`Expected ${Q} to match one of ${JSON.stringify(K)}, got ${JSON.stringify($)}`};return{isValid:!0}}if(typeof K==="string"&&this.isRegexPattern(K)){if(!this.validateRegexPattern($,K))return{isValid:!1,error:`Expected ${Q} to match pattern ${K}, got ${JSON.stringify($)}`};return{isValid:!0}}if(typeof K==="string"&&this.isRangePattern(K)){if(!this.validateRangePattern($,K))return{isValid:!1,error:`Expected ${Q} to match range ${K}, got ${JSON.stringify($)}`};return{isValid:!0}}if(K==="null"||K===null){if($!==null)return{isValid:!1,error:`Expected ${Q} to be null, got ${JSON.stringify($)}`};return{isValid:!0}}if($!==K)return{isValid:!1,error:`Expected ${Q} to be ${JSON.stringify(K)}, got ${JSON.stringify($)}`};return{isValid:!0}}isRegexPattern($){return $.startsWith("^")||$.endsWith("$")||$.includes("\\d")||$.includes("\\w")||$.includes("\\s")||$.includes("[")||$.includes("*")||$.includes("+")||$.includes("?")}validateRegexPattern($,K){let Q=String($);try{return new RegExp(K).test(Q)}catch{return!1}}isRangePattern($){return/^(>=?|<=?|>|<)\s*[\d.-]+(\s*,\s*(>=?|<=?|>|<)\s*[\d.-]+)*$/.test($)}validateRangePattern($,K){let Q=Number($);if(Number.isNaN(Q))return!1;let X=K.match(/^([\d.-]+)\s*-\s*([\d.-]+)$/);if(X){let z=Number(X[1]),W=Number(X[2]);return Q>=z&&Q<=W}return K.split(",").map((z)=>z.trim()).every((z)=>{let W=z.match(/^(>=?|<=?|>|<)\s*([\d.-]+)$/);if(!W)return!1;let D=W[1],J=Number(W[2]);switch(D){case">":return Q>J;case">=":return Q>=J;case"<":return Q<J;case"<=":return Q<=J;default:return!1}})}isArraySelector($){return/^\[.*\]$/.test($)||$==="*"||$.startsWith("slice(")}getArrayValue($,K){if(K==="*")return $;if(K.startsWith("[")&&K.endsWith("]")){let Q=K.slice(1,-1);if(Q==="*")return $;let X=Number(Q);if(!Number.isNaN(X))return X>=0?$[X]:$[$.length+X]}if(K.startsWith("slice(")){let Q=K.match(/slice\((\d+)(?:,(\d+))?\)/);if(Q){let X=Number(Q[1]),Z=Q[2]?Number(Q[2]):void 0;return $.slice(X,Z)}}return}async executeSequential($){let K=performance.now(),Q=[],X=P();for(let Z=0;Z<$.length;Z++){let z=this.interpolateStoreVariables($[Z],X),W=await this.executeRequest(z,Z+1);if(Q.push(W),W.success&&z.store){let D=N(W,z.store);Object.assign(X,D),this.logStoredValues(D)}if(!W.success&&!this.globalConfig.continueOnError){this.logger.logError("Stopping execution due to error");break}}return this.createSummary(Q,performance.now()-K)}interpolateStoreVariables($,K){if(Object.keys(K).length===0)return $;return H.interpolateVariables($,{},K)}logStoredValues($){if(Object.keys($).length===0)return;let K=Object.entries($);for(let[Q,X]of K){let Z=X.length>50?`${X.substring(0,50)}...`:X;this.logger.logInfo(`Stored: ${Q} = "${Z}"`)}}async executeParallel($){let K=performance.now(),Q=$.map((Z,z)=>this.executeRequest(Z,z+1)),X=await Promise.all(Q);return this.createSummary(X,performance.now()-K)}async execute($){this.logger.logExecutionStart($.length,this.globalConfig.execution||"sequential");let K=this.globalConfig.execution==="parallel"?await this.executeParallel($):await this.executeSequential($);if(this.logger.logSummary(K),this.globalConfig.output?.saveToFile)await this.saveSummaryToFile(K);return K}createSummary($,K){let Q=$.filter((Z)=>Z.success).length,X=$.filter((Z)=>!Z.success).length;return{total:$.length,successful:Q,failed:X,duration:K,results:$}}async saveSummaryToFile($){let K=this.globalConfig.output?.saveToFile;if(!K)return;let Q=JSON.stringify($,null,2);await Bun.write(K,Q),this.logger.logInfo(`Results saved to ${K}`)}}function A(){if(typeof BUILD_VERSION<"u")return BUILD_VERSION;if(process.env.CURL_RUNNER_VERSION)return process.env.CURL_RUNNER_VERSION;try{let $=["../package.json","./package.json","../../package.json"];for(let K of $)try{let Q=S(K);if(Q.name==="@curl-runner/cli"&&Q.version)return Q.version}catch{}return"0.0.0"}catch{return"0.0.0"}}var h={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",underscore:"\x1B[4m",black:"\x1B[30m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",cyan:"\x1B[36m",white:"\x1B[37m",bgBlack:"\x1B[40m",bgRed:"\x1B[41m",bgGreen:"\x1B[42m",bgYellow:"\x1B[43m",bgBlue:"\x1B[44m",bgMagenta:"\x1B[45m",bgCyan:"\x1B[46m",bgWhite:"\x1B[47m"};function F($,K){return`${h[K]}${$}${h.reset}`}var b=`${process.env.HOME}/.curl-runner-version-cache.json`,d=86400000,m="https://registry.npmjs.org/@curl-runner/cli/latest";class L{async checkForUpdates($=!1){try{if(process.env.CI)return;let K=A();if(K==="0.0.0")return;if(!$){let X=await this.getCachedVersion();if(X&&Date.now()-X.lastCheck<d){this.compareVersions(K,X.latestVersion);return}}let Q=await this.fetchLatestVersion();if(Q)await this.setCachedVersion(Q),this.compareVersions(K,Q)}catch{}}async fetchLatestVersion(){try{let $=await fetch(m,{signal:AbortSignal.timeout(3000)});if(!$.ok)return null;return(await $.json()).version}catch{return null}}compareVersions($,K){if(this.isNewerVersion($,K))console.log(),console.log(F("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E","yellow")),console.log(F("\u2502","yellow")+" "+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" "+F("\uD83D\uDCE6 New version available!","bright")+` ${F($,"red")} \u2192 ${F(K,"green")} `+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" "+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" Update with: "+F("npm install -g @curl-runner/cli","cyan")+" "+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" or: "+F("bun install -g @curl-runner/cli","cyan")+" "+F("\u2502","yellow")),console.log(F("\u2502","yellow")+" "+F("\u2502","yellow")),console.log(F("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F","yellow")),console.log()}isNewerVersion($,K){try{let Q=$.replace(/^v/,""),X=K.replace(/^v/,""),Z=Q.split(".").map(Number),z=X.split(".").map(Number);for(let W=0;W<Math.max(Z.length,z.length);W++){let D=Z[W]||0,J=z[W]||0;if(J>D)return!0;if(J<D)return!1}return!1}catch{return!1}}async getCachedVersion(){try{let $=Bun.file(b);if(await $.exists())return JSON.parse(await $.text())}catch{}return null}async setCachedVersion($){try{let K={lastCheck:Date.now(),latestVersion:$};await Bun.write(b,JSON.stringify(K))}catch{}}}class V{logger=new Y;async loadConfigFile(){let $=["curl-runner.yaml","curl-runner.yml",".curl-runner.yaml",".curl-runner.yml"];for(let K of $)try{if(await Bun.file(K).exists()){let X=await H.parseFile(K),Z=X.global||X;return this.logger.logInfo(`Loaded configuration from ${K}`),Z}}catch(Q){this.logger.logWarning(`Failed to load configuration from ${K}: ${Q}`)}return{}}loadEnvironmentVariables(){let $={};if(process.env.CURL_RUNNER_TIMEOUT)$.defaults={...$.defaults,timeout:Number.parseInt(process.env.CURL_RUNNER_TIMEOUT,10)};if(process.env.CURL_RUNNER_RETRIES)$.defaults={...$.defaults,retry:{...$.defaults?.retry,count:Number.parseInt(process.env.CURL_RUNNER_RETRIES,10)}};if(process.env.CURL_RUNNER_RETRY_DELAY)$.defaults={...$.defaults,retry:{...$.defaults?.retry,delay:Number.parseInt(process.env.CURL_RUNNER_RETRY_DELAY,10)}};if(process.env.CURL_RUNNER_VERBOSE)$.output={...$.output,verbose:process.env.CURL_RUNNER_VERBOSE.toLowerCase()==="true"};if(process.env.CURL_RUNNER_EXECUTION)$.execution=process.env.CURL_RUNNER_EXECUTION;if(process.env.CURL_RUNNER_CONTINUE_ON_ERROR)$.continueOnError=process.env.CURL_RUNNER_CONTINUE_ON_ERROR.toLowerCase()==="true";if(process.env.CURL_RUNNER_OUTPUT_FORMAT){let K=process.env.CURL_RUNNER_OUTPUT_FORMAT;if(["json","pretty","raw"].includes(K))$.output={...$.output,format:K}}if(process.env.CURL_RUNNER_PRETTY_LEVEL){let K=process.env.CURL_RUNNER_PRETTY_LEVEL;if(["minimal","standard","detailed"].includes(K))$.output={...$.output,prettyLevel:K}}if(process.env.CURL_RUNNER_OUTPUT_FILE)$.output={...$.output,saveToFile:process.env.CURL_RUNNER_OUTPUT_FILE};return $}async run($){try{let{files:K,options:Q}=this.parseArguments($);if(!Q.version&&!Q.help)new L().checkForUpdates().catch(()=>{});if(Q.help){this.showHelp();return}if(Q.version){console.log(`curl-runner v${A()}`);return}let X=this.loadEnvironmentVariables(),Z=await this.loadConfigFile(),z=await this.findYamlFiles(K,Q);if(z.length===0)this.logger.logError("No YAML files found"),process.exit(1);this.logger.logInfo(`Found ${z.length} YAML file(s)`);let W=this.mergeGlobalConfigs(X,Z),D=[],J=[];for(let U of z){this.logger.logInfo(`Processing: ${U}`);let{requests:G,config:I}=await this.processYamlFile(U),E=I?.output||{},_=G.map((T)=>({...T,sourceOutputConfig:E,sourceFile:U}));if(I){let{...T}=I;W=this.mergeGlobalConfigs(W,T)}J.push({file:U,requests:_,config:I}),D.push(..._)}if(Q.execution)W.execution=Q.execution;if(Q.continueOnError!==void 0)W.continueOnError=Q.continueOnError;if(Q.verbose!==void 0)W.output={...W.output,verbose:Q.verbose};if(Q.quiet!==void 0)W.output={...W.output,verbose:!1};if(Q.output)W.output={...W.output,saveToFile:Q.output};if(Q.outputFormat)W.output={...W.output,format:Q.outputFormat};if(Q.prettyLevel)W.output={...W.output,prettyLevel:Q.prettyLevel};if(Q.showHeaders!==void 0)W.output={...W.output,showHeaders:Q.showHeaders};if(Q.showBody!==void 0)W.output={...W.output,showBody:Q.showBody};if(Q.showMetrics!==void 0)W.output={...W.output,showMetrics:Q.showMetrics};if(Q.timeout)W.defaults={...W.defaults,timeout:Q.timeout};if(Q.retries||Q.noRetry){let U=Q.noRetry?0:Q.retries||0;W.defaults={...W.defaults,retry:{...W.defaults?.retry,count:U}}}if(Q.retryDelay)W.defaults={...W.defaults,retry:{...W.defaults?.retry,delay:Q.retryDelay}};if(D.length===0)this.logger.logError("No requests found in YAML files"),process.exit(1);let w=new B(W),O;if(J.length>1){let U=[],G=0;for(let _=0;_<J.length;_++){let T=J[_];this.logger.logFileHeader(T.file,T.requests.length);let R=await w.execute(T.requests);if(U.push(...R.results),G+=R.duration,_<J.length-1)console.log()}let I=U.filter((_)=>_.success).length,E=U.filter((_)=>!_.success).length;O={total:U.length,successful:I,failed:E,duration:G,results:U},w.logger.logSummary(O,!0)}else O=await w.execute(D);process.exit(O.failed>0&&!W.continueOnError?1:0)}catch(K){this.logger.logError(K instanceof Error?K.message:String(K)),process.exit(1)}}parseArguments($){let K={},Q=[];for(let X=0;X<$.length;X++){let Z=$[X];if(Z.startsWith("--")){let z=Z.slice(2),W=$[X+1];if(z==="help"||z==="version")K[z]=!0;else if(z==="no-retry")K.noRetry=!0;else if(z==="quiet")K.quiet=!0;else if(z==="show-headers")K.showHeaders=!0;else if(z==="show-body")K.showBody=!0;else if(z==="show-metrics")K.showMetrics=!0;else if(W&&!W.startsWith("--")){if(z==="continue-on-error")K.continueOnError=W==="true";else if(z==="verbose")K.verbose=W==="true";else if(z==="timeout")K.timeout=Number.parseInt(W,10);else if(z==="retries")K.retries=Number.parseInt(W,10);else if(z==="retry-delay")K.retryDelay=Number.parseInt(W,10);else if(z==="output-format"){if(["json","pretty","raw"].includes(W))K.outputFormat=W}else if(z==="pretty-level"){if(["minimal","standard","detailed"].includes(W))K.prettyLevel=W}else K[z]=W;X++}else K[z]=!0}else if(Z.startsWith("-")){let z=Z.slice(1);for(let W of z)switch(W){case"h":K.help=!0;break;case"v":K.verbose=!0;break;case"p":K.execution="parallel";break;case"c":K.continueOnError=!0;break;case"q":K.quiet=!0;break;case"o":{let D=$[X+1];if(D&&!D.startsWith("-"))K.output=D,X++;break}}}else Q.push(Z)}return{files:Q,options:K}}async findYamlFiles($,K){let Q=new Set,X=[];if($.length===0)X=K.all?["**/*.yaml","**/*.yml"]:["*.yaml","*.yml"];else for(let Z of $)try{let W=await(await import("fs/promises")).stat(Z);if(W.isDirectory()){if(X.push(`${Z}/*.yaml`,`${Z}/*.yml`),K.all)X.push(`${Z}/**/*.yaml`,`${Z}/**/*.yml`)}else if(W.isFile())X.push(Z)}catch{X.push(Z)}for(let Z of X){let z=new c(Z);for await(let W of z.scan("."))if(W.endsWith(".yaml")||W.endsWith(".yml"))Q.add(W)}return Array.from(Q).sort()}async processYamlFile($){let K=await H.parseFile($),Q=[],X;if(K.global)X=K.global;let Z={...K.global?.variables,...K.collection?.variables},z={...K.global?.defaults,...K.collection?.defaults};if(K.request){let W=this.prepareRequest(K.request,Z,z);Q.push(W)}if(K.requests)for(let W of K.requests){let D=this.prepareRequest(W,Z,z);Q.push(D)}if(K.collection?.requests)for(let W of K.collection.requests){let D=this.prepareRequest(W,Z,z);Q.push(D)}return{requests:Q,config:X}}prepareRequest($,K,Q){let X=H.interpolateVariables($,K);return H.mergeConfigs(Q,X)}mergeGlobalConfigs($,K){return{...$,...K,variables:{...$.variables,...K.variables},output:{...$.output,...K.output},defaults:{...$.defaults,...K.defaults}}}showHelp(){console.log(`
|
|
3
|
+
var v=Object.create;var{getPrototypeOf:x,defineProperty:q,getOwnPropertyNames:y}=Object;var f=Object.prototype.hasOwnProperty;var g=($,K,Q)=>{Q=$!=null?v(x($)):{};let Z=K||!$||!$.__esModule?q(Q,"default",{value:$,enumerable:!0}):Q;for(let X of y($))if(!f.call(Z,X))q(Z,X,{get:()=>$[X],enumerable:!0});return Z};var E=import.meta.require;var{Glob:i}=globalThis.Bun;var{YAML:N}=globalThis.Bun;class H{static async parseFile($){let Q=await Bun.file($).text();return N.parse(Q)}static parse($){return N.parse($)}static interpolateVariables($,K,Q){if(typeof $==="string"){let Z=$.match(/^\$\{([^}]+)\}$/);if(Z){let X=Z[1],z=H.resolveVariable(X,K,Q);return z!==null?z:$}return $.replace(/\$\{([^}]+)\}/g,(X,z)=>{let W=H.resolveVariable(z,K,Q);return W!==null?W:X})}if(Array.isArray($))return $.map((Z)=>H.interpolateVariables(Z,K,Q));if($&&typeof $==="object"){let Z={};for(let[X,z]of Object.entries($))Z[X]=H.interpolateVariables(z,K,Q);return Z}return $}static resolveVariable($,K,Q){if($.startsWith("store.")&&Q){let X=$.slice(6);if(X in Q)return Q[X];return null}let Z=H.resolveDynamicVariable($);if(Z!==null)return Z;if($ in K)return K[$];return null}static resolveDynamicVariable($){if($==="UUID")return crypto.randomUUID();if($==="CURRENT_TIME"||$==="TIMESTAMP")return Date.now().toString();if($.startsWith("DATE:")){let K=$.slice(5);return H.formatDate(new Date,K)}if($.startsWith("TIME:")){let K=$.slice(5);return H.formatTime(new Date,K)}return null}static formatDate($,K){let Q=$.getFullYear(),Z=String($.getMonth()+1).padStart(2,"0"),X=String($.getDate()).padStart(2,"0");return K.replace("YYYY",Q.toString()).replace("MM",Z).replace("DD",X)}static formatTime($,K){let Q=String($.getHours()).padStart(2,"0"),Z=String($.getMinutes()).padStart(2,"0"),X=String($.getSeconds()).padStart(2,"0");return K.replace("HH",Q).replace("mm",Z).replace("ss",X)}static mergeConfigs($,K){return{...$,...K,headers:{...$.headers,...K.headers},params:{...$.params,...K.params},variables:{...$.variables,...K.variables}}}}class S{static buildCommand($){let K=["curl"];if(K.push("-X",$.method||"GET"),K.push("-w",'"\\n__CURL_METRICS_START__%{json}__CURL_METRICS_END__"'),$.headers)for(let[Z,X]of Object.entries($.headers))K.push("-H",`"${Z}: ${X}"`);if($.auth){if($.auth.type==="basic"&&$.auth.username&&$.auth.password)K.push("-u",`"${$.auth.username}:${$.auth.password}"`);else if($.auth.type==="bearer"&&$.auth.token)K.push("-H",`"Authorization: Bearer ${$.auth.token}"`)}if($.body){let Z=typeof $.body==="string"?$.body:JSON.stringify($.body);if(K.push("-d",`'${Z.replace(/'/g,"'\\''")}'`),!$.headers?.["Content-Type"])K.push("-H",'"Content-Type: application/json"')}if($.timeout)K.push("--max-time",$.timeout.toString());if($.followRedirects!==!1){if(K.push("-L"),$.maxRedirects)K.push("--max-redirs",$.maxRedirects.toString())}if($.proxy)K.push("-x",$.proxy);if($.insecure)K.push("-k");if($.output)K.push("-o",$.output);K.push("-s","-S");let Q=$.url;if($.params&&Object.keys($.params).length>0){let Z=new URLSearchParams($.params).toString();Q+=(Q.includes("?")?"&":"?")+Z}return K.push(`"${Q}"`),K.join(" ")}static async executeCurl($){try{let K=Bun.spawn(["sh","-c",$],{stdout:"pipe",stderr:"pipe"}),Q=await new Response(K.stdout).text(),Z=await new Response(K.stderr).text();if(await K.exited,K.exitCode!==0&&!Q)return{success:!1,error:Z||`Command failed with exit code ${K.exitCode}`};let X=Q,z={},W=Q.match(/__CURL_METRICS_START__(.+?)__CURL_METRICS_END__/);if(W){X=Q.replace(/__CURL_METRICS_START__.+?__CURL_METRICS_END__/,"").trim();try{z=JSON.parse(W[1])}catch(U){}}let J={};if(z.response_code){let U=Z.split(`
|
|
4
|
+
`).filter((w)=>w.includes(":"));for(let w of U){let[D,..._]=w.split(":");if(D&&_.length>0)J[D.trim()]=_.join(":").trim()}}return{success:!0,status:z.response_code||z.http_code,headers:J,body:X,metrics:{duration:(z.time_total||0)*1000,size:z.size_download,dnsLookup:(z.time_namelookup||0)*1000,tcpConnection:(z.time_connect||0)*1000,tlsHandshake:(z.time_appconnect||0)*1000,firstByte:(z.time_starttransfer||0)*1000,download:(z.time_total||0)*1000}}}catch(K){return{success:!1,error:K instanceof Error?K.message:String(K)}}}}class A{colors;constructor($){this.colors=$}color($,K){if(!K||!this.colors[K])return $;return`${this.colors[K]}${$}${this.colors.reset}`}render($,K=" "){$.forEach((Q,Z)=>{let X=Z===$.length-1,z=X?`${K}\u2514\u2500`:`${K}\u251C\u2500`;if(Q.label&&Q.value){let W=Q.color?this.color(Q.value,Q.color):Q.value,J=W.split(`
|
|
5
|
+
`);if(J.length===1)console.log(`${z} ${Q.label}: ${W}`);else{console.log(`${z} ${Q.label}:`);let U=X?`${K} `:`${K}\u2502 `;J.forEach((w)=>{console.log(`${U}${w}`)})}}else if(Q.label&&!Q.value)console.log(`${z} ${Q.label}:`);else if(!Q.label&&Q.value){let W=X?`${K} `:`${K}\u2502 `;console.log(`${W}${Q.value}`)}if(Q.children&&Q.children.length>0){let W=X?`${K} `:`${K}\u2502 `;this.render(Q.children,W)}})}}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($,K){return`${this.colors[K]}${$}${this.colors.reset}`}getShortFilename($){return $.replace(/.*\//,"").replace(".yaml","")}shouldShowOutput(){if(this.config.format==="raw")return!1;if(this.config.format==="pretty")return!0;return this.config.verbose!==!1}shouldShowHeaders(){if(this.config.format!=="pretty")return this.config.showHeaders||!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.showHeaders||!1;case"detailed":return!0;default:return this.config.showHeaders||!1}}shouldShowBody(){if(this.config.format!=="pretty")return this.config.showBody!==!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.showBody!==!1;case"detailed":return!0;default:return this.config.showBody!==!1}}shouldShowMetrics(){if(this.config.format!=="pretty")return this.config.showMetrics||!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.showMetrics||!1;case"detailed":return!0;default:return this.config.showMetrics||!1}}shouldShowRequestDetails(){if(this.config.format!=="pretty")return this.config.verbose||!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.verbose||!1;case"detailed":return!0;default:return this.config.verbose||!1}}shouldShowSeparators(){if(this.config.format!=="pretty")return!0;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return!0;case"detailed":return!0;default:return!0}}colorStatusCode($){return this.color($,"yellow")}logValidationErrors($){let K=$.split("; ");if(K.length===1){let Q=K[0].trim(),Z=Q.match(/^Expected status (.+?), got (.+)$/);if(Z){let[,X,z]=Z,W=this.colorStatusCode(X.replace(" or ","|")),J=this.color(z,"red");console.log(` ${this.color("\u2717","red")} ${this.color("Error:","red")} Expected ${this.color("status","yellow")} ${W}, got ${J}`)}else console.log(` ${this.color("\u2717","red")} ${this.color("Error:","red")} ${Q}`)}else{console.log(` ${this.color("\u2717","red")} ${this.color("Validation Errors:","red")}`);for(let Q of K){let Z=Q.trim();if(Z)if(Z.startsWith("Expected ")){let X=Z.match(/^Expected status (.+?), got (.+)$/);if(X){let[,z,W]=X,J=this.colorStatusCode(z.replace(" or ","|")),U=this.color(W,"red");console.log(` ${this.color("\u2022","red")} ${this.color("status","yellow")}: expected ${J}, got ${U}`)}else{let z=Z.match(/^Expected (.+?) to be (.+?), got (.+)$/);if(z){let[,W,J,U]=z;console.log(` ${this.color("\u2022","red")} ${this.color(W,"yellow")}: expected ${this.color(J,"green")}, got ${this.color(U,"red")}`)}else console.log(` ${this.color("\u2022","red")} ${Z}`)}}else console.log(` ${this.color("\u2022","red")} ${Z}`)}}}formatJson($){if(this.config.format==="raw")return typeof $==="string"?$:JSON.stringify($);if(this.config.format==="json")return JSON.stringify($);return JSON.stringify($,null,2)}formatDuration($){if($<1000)return`${$.toFixed(0)}ms`;return`${($/1000).toFixed(2)}s`}formatSize($){if(!$)return"0 B";let K=["B","KB","MB","GB"],Q=Math.floor(Math.log($)/Math.log(1024));return`${($/1024**Q).toFixed(2)} ${K[Q]}`}logExecutionStart($,K){if(!this.shouldShowOutput())return;if(this.shouldShowSeparators())console.log(),console.log(this.color(`Executing ${$} request(s) in ${K} mode`,"dim")),console.log();else console.log()}logRequestStart($,K){return}logCommand($){if(this.shouldShowRequestDetails())console.log(this.color(" Command:","dim")),console.log(this.color(` ${$}`,"dim"))}logRetry($,K){console.log(this.color(` \u21BB Retry ${$}/${K}...`,"yellow"))}logRequestComplete($){if(this.config.format==="raw"){if($.success&&this.config.showBody&&$.body){let J=this.formatJson($.body);console.log(J)}return}if(this.config.format==="json"){let J={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(J,null,2));return}if(!this.shouldShowOutput())return;let K=this.config.prettyLevel||"minimal",Q=$.success?"green":"red",Z=$.success?"\u2713":"x",X=$.request.name||"Request";if(K==="minimal"){let J=$.request.sourceFile?this.getShortFilename($.request.sourceFile):"inline";console.log(`${this.color(Z,Q)} ${this.color(X,"bright")} [${J}]`);let U=[],w=new A(this.colors);U.push({label:$.request.method||"GET",value:$.request.url,color:"blue"});let D=$.status?`${$.status}`:"ERROR";if(U.push({label:`${Z} Status`,value:D,color:Q}),$.metrics){let _=`${this.formatDuration($.metrics.duration)} | ${this.formatSize($.metrics.size)}`;U.push({label:"Duration",value:_,color:"cyan"})}if(w.render(U),$.error)console.log(),this.logValidationErrors($.error);console.log();return}console.log(`${this.color(Z,Q)} ${this.color(X,"bright")}`);let z=[],W=new A(this.colors);if(z.push({label:"URL",value:$.request.url,color:"blue"}),z.push({label:"Method",value:$.request.method||"GET",color:"yellow"}),z.push({label:"Status",value:String($.status||"ERROR"),color:Q}),$.metrics)z.push({label:"Duration",value:this.formatDuration($.metrics.duration),color:"cyan"});if(this.shouldShowHeaders()&&$.headers&&Object.keys($.headers).length>0){let J=Object.entries($.headers).map(([U,w])=>({label:this.color(U,"dim"),value:String(w)}));z.push({label:"Headers",children:J})}if(this.shouldShowBody()&&$.body){let U=this.formatJson($.body).split(`
|
|
6
|
+
`),w=this.shouldShowRequestDetails()?1/0:10,D=U.slice(0,w);if(U.length>w)D.push(this.color(`... (${U.length-w} more lines)`,"dim"));z.push({label:"Response Body",value:D.join(`
|
|
7
|
+
`)})}if(this.shouldShowMetrics()&&$.metrics&&K==="detailed"){let J=$.metrics,U=[];if(U.push({label:"Request Duration",value:this.formatDuration(J.duration),color:"cyan"}),J.size!==void 0)U.push({label:"Response Size",value:this.formatSize(J.size),color:"cyan"});if(J.dnsLookup)U.push({label:"DNS Lookup",value:this.formatDuration(J.dnsLookup),color:"cyan"});if(J.tcpConnection)U.push({label:"TCP Connection",value:this.formatDuration(J.tcpConnection),color:"cyan"});if(J.tlsHandshake)U.push({label:"TLS Handshake",value:this.formatDuration(J.tlsHandshake),color:"cyan"});if(J.firstByte)U.push({label:"Time to First Byte",value:this.formatDuration(J.firstByte),color:"cyan"});z.push({label:"Metrics",children:U})}if(W.render(z),$.error)console.log(),this.logValidationErrors($.error);console.log()}logSummary($,K=!1){if(this.config.format==="raw")return;if(this.config.format==="json"){let J={summary:{total:$.total,successful:$.successful,failed:$.failed,duration:$.duration},results:$.results.map((U)=>({request:{name:U.request.name,url:U.request.url,method:U.request.method||"GET"},success:U.success,status:U.status,...this.shouldShowHeaders()&&U.headers?{headers:U.headers}:{},...this.shouldShowBody()&&U.body?{body:U.body}:{},...U.error?{error:U.error}:{},...this.shouldShowMetrics()&&U.metrics?{metrics:U.metrics}:{}}))};console.log(JSON.stringify(J,null,2));return}if(!this.shouldShowOutput())return;let Q=this.config.prettyLevel||"minimal";if(K)console.log();if(Q==="minimal"){let J=$.failed===0?"green":"red",U=$.failed===0?`${$.total} request${$.total===1?"":"s"} completed successfully`:`${$.successful}/${$.total} request${$.total===1?"":"s"} completed, ${$.failed} failed`;console.log(`${K?"\u25C6 Global Summary":"Summary"}: ${this.color(U,J)}`);return}let Z=($.successful/$.total*100).toFixed(1),X=$.failed===0?"green":"red",z=$.failed===0?`${$.total} request${$.total===1?"":"s"} completed successfully`:`${$.successful}/${$.total} request${$.total===1?"":"s"} completed, ${$.failed} failed`,W=K?"\u25C6 Global Summary":"Summary";if(console.log(),console.log(`${W}: ${this.color(z,X)} (${this.color(this.formatDuration($.duration),"cyan")})`),$.failed>0&&this.shouldShowRequestDetails())$.results.filter((J)=>!J.success).forEach((J)=>{let U=J.request.name||J.request.url;console.log(` ${this.color("\u2022","red")} ${U}: ${J.error}`)})}logError($){console.error(this.color(`\u2717 ${$}`,"red"))}logWarning($){console.warn(this.color(`\u26A0 ${$}`,"yellow"))}logInfo($){console.log(this.color(`\u2139 ${$}`,"blue"))}logSuccess($){console.log(this.color(`\u2713 ${$}`,"green"))}logFileHeader($,K){if(!this.shouldShowOutput()||this.config.format!=="pretty")return;let Q=$.replace(/.*\//,"").replace(".yaml","");console.log(),console.log(this.color(`\u25B6 ${Q}.yaml`,"bright")+this.color(` (${K} request${K===1?"":"s"})`,"dim"))}}function p($,K){let Q=K.split("."),Z=$;for(let X of Q){if(Z===null||Z===void 0)return;if(typeof Z!=="object")return;let z=X.match(/^(\w+)\[(\d+)\]$/);if(z){let[,W,J]=z,U=Number.parseInt(J,10);if(Z=Z[W],Array.isArray(Z))Z=Z[U];else return}else if(/^\d+$/.test(X)&&Array.isArray(Z))Z=Z[Number.parseInt(X,10)];else Z=Z[X]}return Z}function d($){if($===void 0||$===null)return"";if(typeof $==="string")return $;if(typeof $==="number"||typeof $==="boolean")return String($);return JSON.stringify($)}function P($,K){let Q={},Z={status:$.status,headers:$.headers||{},body:$.body,metrics:$.metrics};for(let[X,z]of Object.entries(K)){let W=p(Z,z);Q[X]=d(W)}return Q}function h(){return{}}class L{logger;globalConfig;constructor($={}){this.globalConfig=$,this.logger=new M($.output)}mergeOutputConfig($){return{...this.globalConfig.output,...$.sourceOutputConfig}}async executeRequest($,K=0){let Q=performance.now(),Z=this.mergeOutputConfig($),X=new M(Z);X.logRequestStart($,K);let z=S.buildCommand($);X.logCommand(z);let W=0,J,U=($.retry?.count||0)+1;while(W<U){if(W>0){if(X.logRetry(W,U-1),$.retry?.delay)await Bun.sleep($.retry.delay)}let D=await S.executeCurl(z);if(D.success){let _=D.body;try{if(D.headers?.["content-type"]?.includes("application/json")||_&&(_.trim().startsWith("{")||_.trim().startsWith("[")))_=JSON.parse(_)}catch(I){}let F={request:$,success:!0,status:D.status,headers:D.headers,body:_,metrics:{...D.metrics,duration:performance.now()-Q}};if($.expect){let I=this.validateResponse(F,$.expect);if(!I.success)F.success=!1,F.error=I.error}return X.logRequestComplete(F),F}J=D.error,W++}let w={request:$,success:!1,error:J,metrics:{duration:performance.now()-Q}};return X.logRequestComplete(w),w}validateResponse($,K){if(!K)return{success:!0};let Q=[];if(K.status!==void 0){let X=Array.isArray(K.status)?K.status:[K.status];if(!X.includes($.status||0))Q.push(`Expected status ${X.join(" or ")}, got ${$.status}`)}if(K.headers)for(let[X,z]of Object.entries(K.headers)){let W=$.headers?.[X]||$.headers?.[X.toLowerCase()];if(W!==z)Q.push(`Expected header ${X}="${z}", got "${W}"`)}if(K.body!==void 0){let X=this.validateBodyProperties($.body,K.body,"");if(X.length>0)Q.push(...X)}if(K.responseTime!==void 0&&$.metrics){let X=$.metrics.duration;if(!this.validateRangePattern(X,K.responseTime))Q.push(`Expected response time to match ${K.responseTime}ms, got ${X.toFixed(2)}ms`)}let Z=Q.length>0;if(K.failure===!0){if(Z)return{success:!1,error:Q.join("; ")};let X=$.status||0;if(X>=400)return{success:!0};else return{success:!1,error:`Expected request to fail (4xx/5xx) but got status ${X}`}}else if(Z)return{success:!1,error:Q.join("; ")};else return{success:!0}}validateBodyProperties($,K,Q){let Z=[];if(typeof K!=="object"||K===null){let X=this.validateValue($,K,Q||"body");if(!X.isValid)Z.push(X.error);return Z}if(Array.isArray(K)){let X=this.validateValue($,K,Q||"body");if(!X.isValid)Z.push(X.error);return Z}for(let[X,z]of Object.entries(K)){let W=Q?`${Q}.${X}`:X,J;if(Array.isArray($)&&this.isArraySelector(X))J=this.getArrayValue($,X);else J=$?.[X];if(typeof z==="object"&&z!==null&&!Array.isArray(z)){let U=this.validateBodyProperties(J,z,W);Z.push(...U)}else{let U=this.validateValue(J,z,W);if(!U.isValid)Z.push(U.error)}}return Z}validateValue($,K,Q){if(K==="*")return{isValid:!0};if(Array.isArray(K)){if(!K.some((X)=>{if(X==="*")return!0;if(typeof X==="string"&&this.isRegexPattern(X))return this.validateRegexPattern($,X);if(typeof X==="string"&&this.isRangePattern(X))return this.validateRangePattern($,X);return $===X}))return{isValid:!1,error:`Expected ${Q} to match one of ${JSON.stringify(K)}, got ${JSON.stringify($)}`};return{isValid:!0}}if(typeof K==="string"&&this.isRegexPattern(K)){if(!this.validateRegexPattern($,K))return{isValid:!1,error:`Expected ${Q} to match pattern ${K}, got ${JSON.stringify($)}`};return{isValid:!0}}if(typeof K==="string"&&this.isRangePattern(K)){if(!this.validateRangePattern($,K))return{isValid:!1,error:`Expected ${Q} to match range ${K}, got ${JSON.stringify($)}`};return{isValid:!0}}if(K==="null"||K===null){if($!==null)return{isValid:!1,error:`Expected ${Q} to be null, got ${JSON.stringify($)}`};return{isValid:!0}}if($!==K)return{isValid:!1,error:`Expected ${Q} to be ${JSON.stringify(K)}, got ${JSON.stringify($)}`};return{isValid:!0}}isRegexPattern($){return $.startsWith("^")||$.endsWith("$")||$.includes("\\d")||$.includes("\\w")||$.includes("\\s")||$.includes("[")||$.includes("*")||$.includes("+")||$.includes("?")}validateRegexPattern($,K){let Q=String($);try{return new RegExp(K).test(Q)}catch{return!1}}isRangePattern($){return/^(>=?|<=?|>|<)\s*[\d.-]+(\s*,\s*(>=?|<=?|>|<)\s*[\d.-]+)*$/.test($)}validateRangePattern($,K){let Q=Number($);if(Number.isNaN(Q))return!1;let Z=K.match(/^([\d.-]+)\s*-\s*([\d.-]+)$/);if(Z){let z=Number(Z[1]),W=Number(Z[2]);return Q>=z&&Q<=W}return K.split(",").map((z)=>z.trim()).every((z)=>{let W=z.match(/^(>=?|<=?|>|<)\s*([\d.-]+)$/);if(!W)return!1;let J=W[1],U=Number(W[2]);switch(J){case">":return Q>U;case">=":return Q>=U;case"<":return Q<U;case"<=":return Q<=U;default:return!1}})}isArraySelector($){return/^\[.*\]$/.test($)||$==="*"||$.startsWith("slice(")}getArrayValue($,K){if(K==="*")return $;if(K.startsWith("[")&&K.endsWith("]")){let Q=K.slice(1,-1);if(Q==="*")return $;let Z=Number(Q);if(!Number.isNaN(Z))return Z>=0?$[Z]:$[$.length+Z]}if(K.startsWith("slice(")){let Q=K.match(/slice\((\d+)(?:,(\d+))?\)/);if(Q){let Z=Number(Q[1]),X=Q[2]?Number(Q[2]):void 0;return $.slice(Z,X)}}return}async executeSequential($){let K=performance.now(),Q=[],Z=h();for(let X=0;X<$.length;X++){let z=this.interpolateStoreVariables($[X],Z),W=await this.executeRequest(z,X+1);if(Q.push(W),W.success&&z.store){let J=P(W,z.store);Object.assign(Z,J),this.logStoredValues(J)}if(!W.success&&!this.globalConfig.continueOnError){this.logger.logError("Stopping execution due to error");break}}return this.createSummary(Q,performance.now()-K)}interpolateStoreVariables($,K){if(Object.keys(K).length===0)return $;return H.interpolateVariables($,{},K)}logStoredValues($){if(Object.keys($).length===0)return;let K=Object.entries($);for(let[Q,Z]of K){let X=Z.length>50?`${Z.substring(0,50)}...`:Z;this.logger.logInfo(`Stored: ${Q} = "${X}"`)}}async executeParallel($){let K=performance.now(),Q=$.map((X,z)=>this.executeRequest(X,z+1)),Z=await Promise.all(Q);return this.createSummary(Z,performance.now()-K)}async execute($){this.logger.logExecutionStart($.length,this.globalConfig.execution||"sequential");let K=this.globalConfig.execution==="parallel"?await this.executeParallel($):await this.executeSequential($);if(this.logger.logSummary(K),this.globalConfig.output?.saveToFile)await this.saveSummaryToFile(K);return K}createSummary($,K){let Q=$.filter((X)=>X.success).length,Z=$.filter((X)=>!X.success).length;return{total:$.length,successful:Q,failed:Z,duration:K,results:$}}async saveSummaryToFile($){let K=this.globalConfig.output?.saveToFile;if(!K)return;let Q=JSON.stringify($,null,2);await Bun.write(K,Q),this.logger.logInfo(`Results saved to ${K}`)}}function j(){if(typeof BUILD_VERSION<"u")return BUILD_VERSION;if(process.env.CURL_RUNNER_VERSION)return process.env.CURL_RUNNER_VERSION;try{let $=["../package.json","./package.json","../../package.json"];for(let K of $)try{let Q=E(K);if(Q.name==="@curl-runner/cli"&&Q.version)return Q.version}catch{}return"0.0.0"}catch{return"0.0.0"}}var V={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 O($,K){return`${V[K]}${$}${V.reset}`}var b=`${process.env.HOME}/.curl-runner-version-cache.json`,m=86400000,c="https://registry.npmjs.org/@curl-runner/cli/latest";class R{async checkForUpdates($=!1){try{if(process.env.CI)return;let K=j();if(K==="0.0.0")return;if(!$){let Z=await this.getCachedVersion();if(Z&&Date.now()-Z.lastCheck<m){this.compareVersions(K,Z.latestVersion);return}}let Q=await this.fetchLatestVersion();if(Q)await this.setCachedVersion(Q),this.compareVersions(K,Q)}catch{}}async fetchLatestVersion(){try{let $=await fetch(c,{signal:AbortSignal.timeout(3000)});if(!$.ok)return null;return(await $.json()).version}catch{return null}}compareVersions($,K){if(this.isNewerVersion($,K))console.log(),console.log(O("\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(O("\u2502","yellow")+" "+O("\u2502","yellow")),console.log(O("\u2502","yellow")+" "+O("\uD83D\uDCE6 New version available!","bright")+` ${O($,"red")} \u2192 ${O(K,"green")} `+O("\u2502","yellow")),console.log(O("\u2502","yellow")+" "+O("\u2502","yellow")),console.log(O("\u2502","yellow")+" Update with: "+O("npm install -g @curl-runner/cli","cyan")+" "+O("\u2502","yellow")),console.log(O("\u2502","yellow")+" or: "+O("bun install -g @curl-runner/cli","cyan")+" "+O("\u2502","yellow")),console.log(O("\u2502","yellow")+" "+O("\u2502","yellow")),console.log(O("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F","yellow")),console.log()}isNewerVersion($,K){try{let Q=$.replace(/^v/,""),Z=K.replace(/^v/,""),X=Q.split(".").map(Number),z=Z.split(".").map(Number);for(let W=0;W<Math.max(X.length,z.length);W++){let J=X[W]||0,U=z[W]||0;if(U>J)return!0;if(U<J)return!1}return!1}catch{return!1}}async getCachedVersion(){try{let $=Bun.file(b);if(await $.exists())return JSON.parse(await $.text())}catch{}return null}async setCachedVersion($){try{let K={lastCheck:Date.now(),latestVersion:$};await Bun.write(b,JSON.stringify(K))}catch{}}}class C{logger=new M;async loadConfigFile(){let $=["curl-runner.yaml","curl-runner.yml",".curl-runner.yaml",".curl-runner.yml"];for(let K of $)try{if(await Bun.file(K).exists()){let Z=await H.parseFile(K),X=Z.global||Z;return this.logger.logInfo(`Loaded configuration from ${K}`),X}}catch(Q){this.logger.logWarning(`Failed to load configuration from ${K}: ${Q}`)}return{}}loadEnvironmentVariables(){let $={};if(process.env.CURL_RUNNER_TIMEOUT)$.defaults={...$.defaults,timeout:Number.parseInt(process.env.CURL_RUNNER_TIMEOUT,10)};if(process.env.CURL_RUNNER_RETRIES)$.defaults={...$.defaults,retry:{...$.defaults?.retry,count:Number.parseInt(process.env.CURL_RUNNER_RETRIES,10)}};if(process.env.CURL_RUNNER_RETRY_DELAY)$.defaults={...$.defaults,retry:{...$.defaults?.retry,delay:Number.parseInt(process.env.CURL_RUNNER_RETRY_DELAY,10)}};if(process.env.CURL_RUNNER_VERBOSE)$.output={...$.output,verbose:process.env.CURL_RUNNER_VERBOSE.toLowerCase()==="true"};if(process.env.CURL_RUNNER_EXECUTION)$.execution=process.env.CURL_RUNNER_EXECUTION;if(process.env.CURL_RUNNER_CONTINUE_ON_ERROR)$.continueOnError=process.env.CURL_RUNNER_CONTINUE_ON_ERROR.toLowerCase()==="true";if(process.env.CURL_RUNNER_OUTPUT_FORMAT){let K=process.env.CURL_RUNNER_OUTPUT_FORMAT;if(["json","pretty","raw"].includes(K))$.output={...$.output,format:K}}if(process.env.CURL_RUNNER_PRETTY_LEVEL){let K=process.env.CURL_RUNNER_PRETTY_LEVEL;if(["minimal","standard","detailed"].includes(K))$.output={...$.output,prettyLevel:K}}if(process.env.CURL_RUNNER_OUTPUT_FILE)$.output={...$.output,saveToFile:process.env.CURL_RUNNER_OUTPUT_FILE};if(process.env.CURL_RUNNER_STRICT_EXIT)$.ci={...$.ci,strictExit:process.env.CURL_RUNNER_STRICT_EXIT.toLowerCase()==="true"};if(process.env.CURL_RUNNER_FAIL_ON)$.ci={...$.ci,failOn:Number.parseInt(process.env.CURL_RUNNER_FAIL_ON,10)};if(process.env.CURL_RUNNER_FAIL_ON_PERCENTAGE){let K=Number.parseFloat(process.env.CURL_RUNNER_FAIL_ON_PERCENTAGE);if(K>=0&&K<=100)$.ci={...$.ci,failOnPercentage:K}}return $}async run($){try{let{files:K,options:Q}=this.parseArguments($);if(!Q.version&&!Q.help)new R().checkForUpdates().catch(()=>{});if(Q.help){this.showHelp();return}if(Q.version){console.log(`curl-runner v${j()}`);return}let Z=this.loadEnvironmentVariables(),X=await this.loadConfigFile(),z=await this.findYamlFiles(K,Q);if(z.length===0)this.logger.logError("No YAML files found"),process.exit(1);this.logger.logInfo(`Found ${z.length} YAML file(s)`);let W=this.mergeGlobalConfigs(Z,X),J=[],U=[];for(let F of z){this.logger.logInfo(`Processing: ${F}`);let{requests:I,config:Y}=await this.processYamlFile(F),B=Y?.output||{},G=I.map((T)=>({...T,sourceOutputConfig:B,sourceFile:F}));if(Y){let{...T}=Y;W=this.mergeGlobalConfigs(W,T)}U.push({file:F,requests:G,config:Y}),J.push(...G)}if(Q.execution)W.execution=Q.execution;if(Q.continueOnError!==void 0)W.continueOnError=Q.continueOnError;if(Q.verbose!==void 0)W.output={...W.output,verbose:Q.verbose};if(Q.quiet!==void 0)W.output={...W.output,verbose:!1};if(Q.output)W.output={...W.output,saveToFile:Q.output};if(Q.outputFormat)W.output={...W.output,format:Q.outputFormat};if(Q.prettyLevel)W.output={...W.output,prettyLevel:Q.prettyLevel};if(Q.showHeaders!==void 0)W.output={...W.output,showHeaders:Q.showHeaders};if(Q.showBody!==void 0)W.output={...W.output,showBody:Q.showBody};if(Q.showMetrics!==void 0)W.output={...W.output,showMetrics:Q.showMetrics};if(Q.timeout)W.defaults={...W.defaults,timeout:Q.timeout};if(Q.retries||Q.noRetry){let F=Q.noRetry?0:Q.retries||0;W.defaults={...W.defaults,retry:{...W.defaults?.retry,count:F}}}if(Q.retryDelay)W.defaults={...W.defaults,retry:{...W.defaults?.retry,delay:Q.retryDelay}};if(Q.strictExit!==void 0)W.ci={...W.ci,strictExit:Q.strictExit};if(Q.failOn!==void 0)W.ci={...W.ci,failOn:Q.failOn};if(Q.failOnPercentage!==void 0)W.ci={...W.ci,failOnPercentage:Q.failOnPercentage};if(J.length===0)this.logger.logError("No requests found in YAML files"),process.exit(1);let w=new L(W),D;if(U.length>1){let F=[],I=0;for(let G=0;G<U.length;G++){let T=U[G];this.logger.logFileHeader(T.file,T.requests.length);let k=await w.execute(T.requests);if(F.push(...k.results),I+=k.duration,G<U.length-1)console.log()}let Y=F.filter((G)=>G.success).length,B=F.filter((G)=>!G.success).length;D={total:F.length,successful:Y,failed:B,duration:I,results:F},w.logger.logSummary(D,!0)}else D=await w.execute(J);let _=this.determineExitCode(D,W);process.exit(_)}catch(K){this.logger.logError(K instanceof Error?K.message:String(K)),process.exit(1)}}parseArguments($){let K={},Q=[];for(let Z=0;Z<$.length;Z++){let X=$[Z];if(X.startsWith("--")){let z=X.slice(2),W=$[Z+1];if(z==="help"||z==="version")K[z]=!0;else if(z==="no-retry")K.noRetry=!0;else if(z==="quiet")K.quiet=!0;else if(z==="show-headers")K.showHeaders=!0;else if(z==="show-body")K.showBody=!0;else if(z==="show-metrics")K.showMetrics=!0;else if(z==="strict-exit")K.strictExit=!0;else if(W&&!W.startsWith("--")){if(z==="continue-on-error")K.continueOnError=W==="true";else if(z==="verbose")K.verbose=W==="true";else if(z==="timeout")K.timeout=Number.parseInt(W,10);else if(z==="retries")K.retries=Number.parseInt(W,10);else if(z==="retry-delay")K.retryDelay=Number.parseInt(W,10);else if(z==="fail-on")K.failOn=Number.parseInt(W,10);else if(z==="fail-on-percentage"){let J=Number.parseFloat(W);if(J>=0&&J<=100)K.failOnPercentage=J}else if(z==="output-format"){if(["json","pretty","raw"].includes(W))K.outputFormat=W}else if(z==="pretty-level"){if(["minimal","standard","detailed"].includes(W))K.prettyLevel=W}else K[z]=W;Z++}else K[z]=!0}else if(X.startsWith("-")){let z=X.slice(1);for(let W of z)switch(W){case"h":K.help=!0;break;case"v":K.verbose=!0;break;case"p":K.execution="parallel";break;case"c":K.continueOnError=!0;break;case"q":K.quiet=!0;break;case"o":{let J=$[Z+1];if(J&&!J.startsWith("-"))K.output=J,Z++;break}}}else Q.push(X)}return{files:Q,options:K}}async findYamlFiles($,K){let Q=new Set,Z=[];if($.length===0)Z=K.all?["**/*.yaml","**/*.yml"]:["*.yaml","*.yml"];else for(let X of $)try{let W=await(await import("fs/promises")).stat(X);if(W.isDirectory()){if(Z.push(`${X}/*.yaml`,`${X}/*.yml`),K.all)Z.push(`${X}/**/*.yaml`,`${X}/**/*.yml`)}else if(W.isFile())Z.push(X)}catch{Z.push(X)}for(let X of Z){let z=new i(X);for await(let W of z.scan("."))if(W.endsWith(".yaml")||W.endsWith(".yml"))Q.add(W)}return Array.from(Q).sort()}async processYamlFile($){let K=await H.parseFile($),Q=[],Z;if(K.global)Z=K.global;let X={...K.global?.variables,...K.collection?.variables},z={...K.global?.defaults,...K.collection?.defaults};if(K.request){let W=this.prepareRequest(K.request,X,z);Q.push(W)}if(K.requests)for(let W of K.requests){let J=this.prepareRequest(W,X,z);Q.push(J)}if(K.collection?.requests)for(let W of K.collection.requests){let J=this.prepareRequest(W,X,z);Q.push(J)}return{requests:Q,config:Z}}prepareRequest($,K,Q){let Z=H.interpolateVariables($,K);return H.mergeConfigs(Q,Z)}mergeGlobalConfigs($,K){return{...$,...K,variables:{...$.variables,...K.variables},output:{...$.output,...K.output},defaults:{...$.defaults,...K.defaults},ci:{...$.ci,...K.ci}}}determineExitCode($,K){let{failed:Q,total:Z}=$,X=K.ci;if(Q===0)return 0;if(X){if(X.strictExit)return 1;if(X.failOn!==void 0&&Q>X.failOn)return 1;if(X.failOnPercentage!==void 0&&Z>0){if(Q/Z*100>X.failOnPercentage)return 1}if(X.failOn!==void 0||X.failOnPercentage!==void 0)return 0}return!K.continueOnError?1:0}showHelp(){console.log(`
|
|
8
8
|
${this.logger.color("\uD83D\uDE80 CURL RUNNER","bright")}
|
|
9
9
|
|
|
10
10
|
${this.logger.color("USAGE:","yellow")}
|
|
@@ -29,6 +29,11 @@ ${this.logger.color("OPTIONS:","yellow")}
|
|
|
29
29
|
--show-metrics Include performance metrics in output
|
|
30
30
|
--version Show version
|
|
31
31
|
|
|
32
|
+
${this.logger.color("CI/CD OPTIONS:","yellow")}
|
|
33
|
+
--strict-exit Exit with code 1 if any validation fails (for CI/CD)
|
|
34
|
+
--fail-on <count> Exit with code 1 if failures exceed this count
|
|
35
|
+
--fail-on-percentage <pct> Exit with code 1 if failure percentage exceeds this value
|
|
36
|
+
|
|
32
37
|
${this.logger.color("EXAMPLES:","yellow")}
|
|
33
38
|
# Run all YAML files in current directory
|
|
34
39
|
curl-runner
|
|
@@ -57,6 +62,18 @@ ${this.logger.color("EXAMPLES:","yellow")}
|
|
|
57
62
|
# Run with detailed pretty output (show all information)
|
|
58
63
|
curl-runner --output-format pretty --pretty-level detailed test.yaml
|
|
59
64
|
|
|
65
|
+
# CI/CD: Fail if any validation fails (strict mode)
|
|
66
|
+
curl-runner tests/ --strict-exit
|
|
67
|
+
|
|
68
|
+
# CI/CD: Run all tests but fail if any validation fails
|
|
69
|
+
curl-runner tests/ --continue-on-error --strict-exit
|
|
70
|
+
|
|
71
|
+
# CI/CD: Allow up to 2 failures
|
|
72
|
+
curl-runner tests/ --fail-on 2
|
|
73
|
+
|
|
74
|
+
# CI/CD: Allow up to 10% failures
|
|
75
|
+
curl-runner tests/ --fail-on-percentage 10
|
|
76
|
+
|
|
60
77
|
${this.logger.color("YAML STRUCTURE:","yellow")}
|
|
61
78
|
Single request:
|
|
62
79
|
request:
|
|
@@ -79,7 +96,7 @@ ${this.logger.color("YAML STRUCTURE:","yellow")}
|
|
|
79
96
|
requests:
|
|
80
97
|
- url: \${BASE_URL}/users
|
|
81
98
|
method: GET
|
|
82
|
-
`)}}var
|
|
99
|
+
`)}}var n=new C;n.run(process.argv.slice(2));
|
|
83
100
|
|
|
84
|
-
//# debugId=
|
|
101
|
+
//# debugId=1C214F4C836B615D64756E2164756E21
|
|
85
102
|
//# sourceMappingURL=cli.js.map
|
package/package.json
CHANGED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { ExecutionSummary, GlobalConfig } from './types/config';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Determines the appropriate exit code based on execution results and CI configuration.
|
|
6
|
+
* This is a standalone function for testing purposes.
|
|
7
|
+
*/
|
|
8
|
+
function determineExitCode(summary: ExecutionSummary, config: GlobalConfig): number {
|
|
9
|
+
const { failed, total } = summary;
|
|
10
|
+
const ci = config.ci;
|
|
11
|
+
|
|
12
|
+
// If no failures, always exit with 0
|
|
13
|
+
if (failed === 0) {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Check CI exit code options
|
|
18
|
+
if (ci) {
|
|
19
|
+
// strictExit: exit 1 if ANY failures occur
|
|
20
|
+
if (ci.strictExit) {
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// failOn: exit 1 if failures exceed the threshold
|
|
25
|
+
if (ci.failOn !== undefined && failed > ci.failOn) {
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// failOnPercentage: exit 1 if failure percentage exceeds the threshold
|
|
30
|
+
if (ci.failOnPercentage !== undefined && total > 0) {
|
|
31
|
+
const failurePercentage = (failed / total) * 100;
|
|
32
|
+
if (failurePercentage > ci.failOnPercentage) {
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// If any CI option is set but thresholds not exceeded, exit 0
|
|
38
|
+
if (ci.failOn !== undefined || ci.failOnPercentage !== undefined) {
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Default behavior: exit 1 if failures AND continueOnError is false
|
|
44
|
+
return !config.continueOnError ? 1 : 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a mock execution summary for testing
|
|
49
|
+
*/
|
|
50
|
+
function createSummary(total: number, failed: number): ExecutionSummary {
|
|
51
|
+
return {
|
|
52
|
+
total,
|
|
53
|
+
successful: total - failed,
|
|
54
|
+
failed,
|
|
55
|
+
duration: 1000,
|
|
56
|
+
results: [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('CI Exit Code', () => {
|
|
61
|
+
describe('default behavior (no CI options)', () => {
|
|
62
|
+
test('should exit 0 when no failures', () => {
|
|
63
|
+
const summary = createSummary(10, 0);
|
|
64
|
+
const config: GlobalConfig = {};
|
|
65
|
+
expect(determineExitCode(summary, config)).toBe(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('should exit 1 when failures exist and continueOnError is false', () => {
|
|
69
|
+
const summary = createSummary(10, 2);
|
|
70
|
+
const config: GlobalConfig = {};
|
|
71
|
+
expect(determineExitCode(summary, config)).toBe(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('should exit 0 when failures exist and continueOnError is true', () => {
|
|
75
|
+
const summary = createSummary(10, 2);
|
|
76
|
+
const config: GlobalConfig = { continueOnError: true };
|
|
77
|
+
expect(determineExitCode(summary, config)).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('--strict-exit flag', () => {
|
|
82
|
+
test('should exit 1 when strictExit is true and any failures exist', () => {
|
|
83
|
+
const summary = createSummary(10, 1);
|
|
84
|
+
const config: GlobalConfig = { ci: { strictExit: true } };
|
|
85
|
+
expect(determineExitCode(summary, config)).toBe(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('should exit 0 when strictExit is true but no failures', () => {
|
|
89
|
+
const summary = createSummary(10, 0);
|
|
90
|
+
const config: GlobalConfig = { ci: { strictExit: true } };
|
|
91
|
+
expect(determineExitCode(summary, config)).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('should exit 1 when strictExit is true even with continueOnError', () => {
|
|
95
|
+
const summary = createSummary(10, 1);
|
|
96
|
+
const config: GlobalConfig = {
|
|
97
|
+
continueOnError: true,
|
|
98
|
+
ci: { strictExit: true },
|
|
99
|
+
};
|
|
100
|
+
expect(determineExitCode(summary, config)).toBe(1);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('--fail-on threshold', () => {
|
|
105
|
+
test('should exit 0 when failures are at or below threshold', () => {
|
|
106
|
+
const summary = createSummary(10, 2);
|
|
107
|
+
const config: GlobalConfig = { ci: { failOn: 2 } };
|
|
108
|
+
expect(determineExitCode(summary, config)).toBe(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('should exit 1 when failures exceed threshold', () => {
|
|
112
|
+
const summary = createSummary(10, 3);
|
|
113
|
+
const config: GlobalConfig = { ci: { failOn: 2 } };
|
|
114
|
+
expect(determineExitCode(summary, config)).toBe(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('should exit 0 when failOn is 0 and no failures', () => {
|
|
118
|
+
const summary = createSummary(10, 0);
|
|
119
|
+
const config: GlobalConfig = { ci: { failOn: 0 } };
|
|
120
|
+
expect(determineExitCode(summary, config)).toBe(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('should exit 1 when failOn is 0 and any failures exist', () => {
|
|
124
|
+
const summary = createSummary(10, 1);
|
|
125
|
+
const config: GlobalConfig = { ci: { failOn: 0 } };
|
|
126
|
+
expect(determineExitCode(summary, config)).toBe(1);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('--fail-on-percentage threshold', () => {
|
|
131
|
+
test('should exit 0 when failure percentage is at or below threshold', () => {
|
|
132
|
+
const summary = createSummary(100, 10);
|
|
133
|
+
const config: GlobalConfig = { ci: { failOnPercentage: 10 } };
|
|
134
|
+
expect(determineExitCode(summary, config)).toBe(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('should exit 1 when failure percentage exceeds threshold', () => {
|
|
138
|
+
const summary = createSummary(100, 11);
|
|
139
|
+
const config: GlobalConfig = { ci: { failOnPercentage: 10 } };
|
|
140
|
+
expect(determineExitCode(summary, config)).toBe(1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('should exit 0 when failOnPercentage is 50 and failures are 50%', () => {
|
|
144
|
+
const summary = createSummary(10, 5);
|
|
145
|
+
const config: GlobalConfig = { ci: { failOnPercentage: 50 } };
|
|
146
|
+
expect(determineExitCode(summary, config)).toBe(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('should exit 1 when failOnPercentage is 50 and failures are 51%', () => {
|
|
150
|
+
const summary = createSummary(100, 51);
|
|
151
|
+
const config: GlobalConfig = { ci: { failOnPercentage: 50 } };
|
|
152
|
+
expect(determineExitCode(summary, config)).toBe(1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('should handle edge case with 0 total requests', () => {
|
|
156
|
+
const summary = createSummary(0, 0);
|
|
157
|
+
const config: GlobalConfig = { ci: { failOnPercentage: 10 } };
|
|
158
|
+
expect(determineExitCode(summary, config)).toBe(0);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('combined options', () => {
|
|
163
|
+
test('strictExit takes precedence over failOn', () => {
|
|
164
|
+
const summary = createSummary(10, 1);
|
|
165
|
+
const config: GlobalConfig = {
|
|
166
|
+
ci: { strictExit: true, failOn: 5 },
|
|
167
|
+
};
|
|
168
|
+
expect(determineExitCode(summary, config)).toBe(1);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('strictExit takes precedence over failOnPercentage', () => {
|
|
172
|
+
const summary = createSummary(100, 1);
|
|
173
|
+
const config: GlobalConfig = {
|
|
174
|
+
ci: { strictExit: true, failOnPercentage: 50 },
|
|
175
|
+
};
|
|
176
|
+
expect(determineExitCode(summary, config)).toBe(1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('failOn checked before failOnPercentage', () => {
|
|
180
|
+
const summary = createSummary(100, 6); // 6% failure
|
|
181
|
+
const config: GlobalConfig = {
|
|
182
|
+
ci: { failOn: 5, failOnPercentage: 10 }, // Would pass percentage but fail count
|
|
183
|
+
};
|
|
184
|
+
expect(determineExitCode(summary, config)).toBe(1);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('should exit 0 when all thresholds pass', () => {
|
|
188
|
+
const summary = createSummary(100, 5); // 5% failure
|
|
189
|
+
const config: GlobalConfig = {
|
|
190
|
+
ci: { failOn: 5, failOnPercentage: 10 },
|
|
191
|
+
};
|
|
192
|
+
expect(determineExitCode(summary, config)).toBe(0);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('CI option with continueOnError', () => {
|
|
197
|
+
test('should still respect CI thresholds even with continueOnError', () => {
|
|
198
|
+
const summary = createSummary(10, 3);
|
|
199
|
+
const config: GlobalConfig = {
|
|
200
|
+
continueOnError: true,
|
|
201
|
+
ci: { failOn: 2 },
|
|
202
|
+
};
|
|
203
|
+
expect(determineExitCode(summary, config)).toBe(1);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('should exit 0 when threshold not exceeded with continueOnError', () => {
|
|
207
|
+
const summary = createSummary(10, 2);
|
|
208
|
+
const config: GlobalConfig = {
|
|
209
|
+
continueOnError: true,
|
|
210
|
+
ci: { failOn: 2 },
|
|
211
|
+
};
|
|
212
|
+
expect(determineExitCode(summary, config)).toBe(0);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Glob } from 'bun';
|
|
4
4
|
import { RequestExecutor } from './executor/request-executor';
|
|
5
5
|
import { YamlParser } from './parser/yaml';
|
|
6
|
-
import type { GlobalConfig, RequestConfig } from './types/config';
|
|
6
|
+
import type { ExecutionSummary, GlobalConfig, RequestConfig } from './types/config';
|
|
7
7
|
import { Logger } from './utils/logger';
|
|
8
8
|
import { VersionChecker } from './utils/version-checker';
|
|
9
9
|
import { getVersion } from './version';
|
|
@@ -105,6 +105,31 @@ class CurlRunnerCLI {
|
|
|
105
105
|
envConfig.output = { ...envConfig.output, saveToFile: process.env.CURL_RUNNER_OUTPUT_FILE };
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// CI exit code configuration
|
|
109
|
+
if (process.env.CURL_RUNNER_STRICT_EXIT) {
|
|
110
|
+
envConfig.ci = {
|
|
111
|
+
...envConfig.ci,
|
|
112
|
+
strictExit: process.env.CURL_RUNNER_STRICT_EXIT.toLowerCase() === 'true',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (process.env.CURL_RUNNER_FAIL_ON) {
|
|
117
|
+
envConfig.ci = {
|
|
118
|
+
...envConfig.ci,
|
|
119
|
+
failOn: Number.parseInt(process.env.CURL_RUNNER_FAIL_ON, 10),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (process.env.CURL_RUNNER_FAIL_ON_PERCENTAGE) {
|
|
124
|
+
const percentage = Number.parseFloat(process.env.CURL_RUNNER_FAIL_ON_PERCENTAGE);
|
|
125
|
+
if (percentage >= 0 && percentage <= 100) {
|
|
126
|
+
envConfig.ci = {
|
|
127
|
+
...envConfig.ci,
|
|
128
|
+
failOnPercentage: percentage,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
108
133
|
return envConfig;
|
|
109
134
|
}
|
|
110
135
|
|
|
@@ -233,6 +258,20 @@ class CurlRunnerCLI {
|
|
|
233
258
|
};
|
|
234
259
|
}
|
|
235
260
|
|
|
261
|
+
// Apply CI exit code options
|
|
262
|
+
if (options.strictExit !== undefined) {
|
|
263
|
+
globalConfig.ci = { ...globalConfig.ci, strictExit: options.strictExit as boolean };
|
|
264
|
+
}
|
|
265
|
+
if (options.failOn !== undefined) {
|
|
266
|
+
globalConfig.ci = { ...globalConfig.ci, failOn: options.failOn as number };
|
|
267
|
+
}
|
|
268
|
+
if (options.failOnPercentage !== undefined) {
|
|
269
|
+
globalConfig.ci = {
|
|
270
|
+
...globalConfig.ci,
|
|
271
|
+
failOnPercentage: options.failOnPercentage as number,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
236
275
|
if (allRequests.length === 0) {
|
|
237
276
|
this.logger.logError('No requests found in YAML files');
|
|
238
277
|
process.exit(1);
|
|
@@ -283,7 +322,9 @@ class CurlRunnerCLI {
|
|
|
283
322
|
summary = await executor.execute(allRequests);
|
|
284
323
|
}
|
|
285
324
|
|
|
286
|
-
|
|
325
|
+
// Determine exit code based on CI configuration
|
|
326
|
+
const exitCode = this.determineExitCode(summary, globalConfig);
|
|
327
|
+
process.exit(exitCode);
|
|
287
328
|
} catch (error) {
|
|
288
329
|
this.logger.logError(error instanceof Error ? error.message : String(error));
|
|
289
330
|
process.exit(1);
|
|
@@ -313,6 +354,8 @@ class CurlRunnerCLI {
|
|
|
313
354
|
options.showBody = true;
|
|
314
355
|
} else if (key === 'show-metrics') {
|
|
315
356
|
options.showMetrics = true;
|
|
357
|
+
} else if (key === 'strict-exit') {
|
|
358
|
+
options.strictExit = true;
|
|
316
359
|
} else if (nextArg && !nextArg.startsWith('--')) {
|
|
317
360
|
if (key === 'continue-on-error') {
|
|
318
361
|
options.continueOnError = nextArg === 'true';
|
|
@@ -324,6 +367,13 @@ class CurlRunnerCLI {
|
|
|
324
367
|
options.retries = Number.parseInt(nextArg, 10);
|
|
325
368
|
} else if (key === 'retry-delay') {
|
|
326
369
|
options.retryDelay = Number.parseInt(nextArg, 10);
|
|
370
|
+
} else if (key === 'fail-on') {
|
|
371
|
+
options.failOn = Number.parseInt(nextArg, 10);
|
|
372
|
+
} else if (key === 'fail-on-percentage') {
|
|
373
|
+
const percentage = Number.parseFloat(nextArg);
|
|
374
|
+
if (percentage >= 0 && percentage <= 100) {
|
|
375
|
+
options.failOnPercentage = percentage;
|
|
376
|
+
}
|
|
327
377
|
} else if (key === 'output-format') {
|
|
328
378
|
if (['json', 'pretty', 'raw'].includes(nextArg)) {
|
|
329
379
|
options.outputFormat = nextArg;
|
|
@@ -485,9 +535,62 @@ class CurlRunnerCLI {
|
|
|
485
535
|
variables: { ...base.variables, ...override.variables },
|
|
486
536
|
output: { ...base.output, ...override.output },
|
|
487
537
|
defaults: { ...base.defaults, ...override.defaults },
|
|
538
|
+
ci: { ...base.ci, ...override.ci },
|
|
488
539
|
};
|
|
489
540
|
}
|
|
490
541
|
|
|
542
|
+
/**
|
|
543
|
+
* Determines the appropriate exit code based on execution results and CI configuration.
|
|
544
|
+
*
|
|
545
|
+
* Exit code logic:
|
|
546
|
+
* - If strictExit is enabled: exit 1 if ANY failures occur
|
|
547
|
+
* - If failOn is set: exit 1 if failures exceed the threshold
|
|
548
|
+
* - If failOnPercentage is set: exit 1 if failure percentage exceeds the threshold
|
|
549
|
+
* - Default behavior: exit 1 only if failures exist AND continueOnError is false
|
|
550
|
+
*
|
|
551
|
+
* @param summary - The execution summary containing success/failure counts
|
|
552
|
+
* @param config - Global configuration including CI exit options
|
|
553
|
+
* @returns 0 for success, 1 for failure
|
|
554
|
+
*/
|
|
555
|
+
private determineExitCode(summary: ExecutionSummary, config: GlobalConfig): number {
|
|
556
|
+
const { failed, total } = summary;
|
|
557
|
+
const ci = config.ci;
|
|
558
|
+
|
|
559
|
+
// If no failures, always exit with 0
|
|
560
|
+
if (failed === 0) {
|
|
561
|
+
return 0;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Check CI exit code options
|
|
565
|
+
if (ci) {
|
|
566
|
+
// strictExit: exit 1 if ANY failures occur
|
|
567
|
+
if (ci.strictExit) {
|
|
568
|
+
return 1;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// failOn: exit 1 if failures exceed the threshold
|
|
572
|
+
if (ci.failOn !== undefined && failed > ci.failOn) {
|
|
573
|
+
return 1;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// failOnPercentage: exit 1 if failure percentage exceeds the threshold
|
|
577
|
+
if (ci.failOnPercentage !== undefined && total > 0) {
|
|
578
|
+
const failurePercentage = (failed / total) * 100;
|
|
579
|
+
if (failurePercentage > ci.failOnPercentage) {
|
|
580
|
+
return 1;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// If any CI option is set but thresholds not exceeded, exit 0
|
|
585
|
+
if (ci.failOn !== undefined || ci.failOnPercentage !== undefined) {
|
|
586
|
+
return 0;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Default behavior: exit 1 if failures AND continueOnError is false
|
|
591
|
+
return !config.continueOnError ? 1 : 0;
|
|
592
|
+
}
|
|
593
|
+
|
|
491
594
|
private showHelp(): void {
|
|
492
595
|
console.log(`
|
|
493
596
|
${this.logger.color('🚀 CURL RUNNER', 'bright')}
|
|
@@ -514,6 +617,11 @@ ${this.logger.color('OPTIONS:', 'yellow')}
|
|
|
514
617
|
--show-metrics Include performance metrics in output
|
|
515
618
|
--version Show version
|
|
516
619
|
|
|
620
|
+
${this.logger.color('CI/CD OPTIONS:', 'yellow')}
|
|
621
|
+
--strict-exit Exit with code 1 if any validation fails (for CI/CD)
|
|
622
|
+
--fail-on <count> Exit with code 1 if failures exceed this count
|
|
623
|
+
--fail-on-percentage <pct> Exit with code 1 if failure percentage exceeds this value
|
|
624
|
+
|
|
517
625
|
${this.logger.color('EXAMPLES:', 'yellow')}
|
|
518
626
|
# Run all YAML files in current directory
|
|
519
627
|
curl-runner
|
|
@@ -542,6 +650,18 @@ ${this.logger.color('EXAMPLES:', 'yellow')}
|
|
|
542
650
|
# Run with detailed pretty output (show all information)
|
|
543
651
|
curl-runner --output-format pretty --pretty-level detailed test.yaml
|
|
544
652
|
|
|
653
|
+
# CI/CD: Fail if any validation fails (strict mode)
|
|
654
|
+
curl-runner tests/ --strict-exit
|
|
655
|
+
|
|
656
|
+
# CI/CD: Run all tests but fail if any validation fails
|
|
657
|
+
curl-runner tests/ --continue-on-error --strict-exit
|
|
658
|
+
|
|
659
|
+
# CI/CD: Allow up to 2 failures
|
|
660
|
+
curl-runner tests/ --fail-on 2
|
|
661
|
+
|
|
662
|
+
# CI/CD: Allow up to 10% failures
|
|
663
|
+
curl-runner tests/ --fail-on-percentage 10
|
|
664
|
+
|
|
545
665
|
${this.logger.color('YAML STRUCTURE:', 'yellow')}
|
|
546
666
|
Single request:
|
|
547
667
|
request:
|
package/src/types/config.ts
CHANGED
|
@@ -79,9 +79,39 @@ export interface CollectionConfig {
|
|
|
79
79
|
requests: RequestConfig[];
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* CI exit code configuration options.
|
|
84
|
+
* These options control how curl-runner exits in CI/CD pipelines.
|
|
85
|
+
*/
|
|
86
|
+
export interface CIExitConfig {
|
|
87
|
+
/**
|
|
88
|
+
* When true, exit with code 1 if any validation failures occur,
|
|
89
|
+
* regardless of the continueOnError setting.
|
|
90
|
+
* This is useful for CI/CD pipelines that need strict validation.
|
|
91
|
+
*/
|
|
92
|
+
strictExit?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Maximum number of failures allowed before exiting with code 1.
|
|
95
|
+
* If set to 0, any failure will cause a non-zero exit.
|
|
96
|
+
* If undefined and strictExit is true, any failure causes non-zero exit.
|
|
97
|
+
*/
|
|
98
|
+
failOn?: number;
|
|
99
|
+
/**
|
|
100
|
+
* Maximum percentage of failures allowed before exiting with code 1.
|
|
101
|
+
* Value should be between 0 and 100.
|
|
102
|
+
* If set to 10, up to 10% of requests can fail without causing a non-zero exit.
|
|
103
|
+
*/
|
|
104
|
+
failOnPercentage?: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
82
107
|
export interface GlobalConfig {
|
|
83
108
|
execution?: 'sequential' | 'parallel';
|
|
84
109
|
continueOnError?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* CI/CD exit code configuration.
|
|
112
|
+
* Controls when curl-runner should exit with non-zero status codes.
|
|
113
|
+
*/
|
|
114
|
+
ci?: CIExitConfig;
|
|
85
115
|
variables?: Record<string, string>;
|
|
86
116
|
output?: {
|
|
87
117
|
verbose?: boolean;
|