@curl-runner/cli 1.2.0 → 1.3.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 +7 -7
- package/package.json +1 -1
- package/src/executor/request-executor.ts +53 -0
- package/src/types/config.ts +53 -0
- package/src/utils/curl-builder.test.ts +165 -0
- package/src/utils/curl-builder.ts +35 -2
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 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(`
|
|
3
|
+
var x=Object.create;var{getPrototypeOf:y,defineProperty:q,getOwnPropertyNames:f}=Object;var p=Object.prototype.hasOwnProperty;var g=($,K,Q)=>{Q=$!=null?x(y($)):{};let Z=K||!$||!$.__esModule?q(Q,"default",{value:$,enumerable:!0}):Q;for(let X of f($))if(!p.call(Z,X))q(Z,X,{get:()=>$[X],enumerable:!0});return Z};var E=import.meta.require;var{Glob:u}=globalThis.Bun;var{YAML:N}=globalThis.Bun;class G{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=G.resolveVariable(X,K,Q);return z!==null?z:$}return $.replace(/\$\{([^}]+)\}/g,(X,z)=>{let W=G.resolveVariable(z,K,Q);return W!==null?W:X})}if(Array.isArray($))return $.map((Z)=>G.interpolateVariables(Z,K,Q));if($&&typeof $==="object"){let Z={};for(let[X,z]of Object.entries($))Z[X]=G.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=G.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 G.formatDate(new Date,K)}if($.startsWith("TIME:")){let K=$.slice(5);return G.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}}}}function d($){return typeof $==="object"&&$!==null&&"file"in $}function P($){return $.replace(/'/g,"'\\''")}class j{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($.formData)for(let[Z,X]of Object.entries($.formData))if(d(X)){let z=`@${X.file}`;if(X.filename)z+=`;filename=${X.filename}`;if(X.contentType)z+=`;type=${X.contentType}`;K.push("-F",`'${Z}=${P(z)}'`)}else{let z=String(X);K.push("-F",`'${Z}=${P(z)}'`)}else 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(J){}}let U={};if(z.response_code){let J=Z.split(`
|
|
4
|
+
`).filter((w)=>w.includes(":"));for(let w of J){let[_,...H]=w.split(":");if(_&&H.length>0)U[_.trim()]=H.join(":").trim()}}return{success:!0,status:z.response_code||z.http_code,headers:U,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 L{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,U=W.split(`
|
|
5
|
+
`);if(U.length===1)console.log(`${z} ${Q.label}: ${W}`);else{console.log(`${z} ${Q.label}:`);let J=X?`${K} `:`${K}\u2502 `;U.forEach((w)=>{console.log(`${J}${w}`)})}}else if(Q.label&&!Q.value)console.log(`${z} ${Q.label}:`);else if(!Q.label&&Q.value){let W=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 ","|")),U=this.color(z,"red");console.log(` ${this.color("\u2717","red")} ${this.color("Error:","red")} Expected ${this.color("status","yellow")} ${W}, got ${U}`)}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,U=this.colorStatusCode(z.replace(" or ","|")),J=this.color(W,"red");console.log(` ${this.color("\u2022","red")} ${this.color("status","yellow")}: expected ${U}, got ${J}`)}else{let z=Z.match(/^Expected (.+?) to be (.+?), got (.+)$/);if(z){let[,W,U,J]=z;console.log(` ${this.color("\u2022","red")} ${this.color(W,"yellow")}: expected ${this.color(U,"green")}, got ${this.color(J,"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 U=this.formatJson($.body);console.log(U)}return}if(this.config.format==="json"){let U={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(U,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 U=$.request.sourceFile?this.getShortFilename($.request.sourceFile):"inline";console.log(`${this.color(Z,Q)} ${this.color(X,"bright")} [${U}]`);let J=[],w=new L(this.colors);J.push({label:$.request.method||"GET",value:$.request.url,color:"blue"});let _=$.status?`${$.status}`:"ERROR";if(J.push({label:`${Z} Status`,value:_,color:Q}),$.metrics){let H=`${this.formatDuration($.metrics.duration)} | ${this.formatSize($.metrics.size)}`;J.push({label:"Duration",value:H,color:"cyan"})}if(w.render(J),$.error)console.log(),this.logValidationErrors($.error);console.log();return}console.log(`${this.color(Z,Q)} ${this.color(X,"bright")}`);let z=[],W=new L(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 U=Object.entries($.headers).map(([J,w])=>({label:this.color(J,"dim"),value:String(w)}));z.push({label:"Headers",children:U})}if(this.shouldShowBody()&&$.body){let J=this.formatJson($.body).split(`
|
|
6
|
+
`),w=this.shouldShowRequestDetails()?1/0:10,_=J.slice(0,w);if(J.length>w)_.push(this.color(`... (${J.length-w} more lines)`,"dim"));z.push({label:"Response Body",value:_.join(`
|
|
7
|
+
`)})}if(this.shouldShowMetrics()&&$.metrics&&K==="detailed"){let U=$.metrics,J=[];if(J.push({label:"Request Duration",value:this.formatDuration(U.duration),color:"cyan"}),U.size!==void 0)J.push({label:"Response Size",value:this.formatSize(U.size),color:"cyan"});if(U.dnsLookup)J.push({label:"DNS Lookup",value:this.formatDuration(U.dnsLookup),color:"cyan"});if(U.tcpConnection)J.push({label:"TCP Connection",value:this.formatDuration(U.tcpConnection),color:"cyan"});if(U.tlsHandshake)J.push({label:"TLS Handshake",value:this.formatDuration(U.tlsHandshake),color:"cyan"});if(U.firstByte)J.push({label:"Time to First Byte",value:this.formatDuration(U.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 U={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(U,null,2));return}if(!this.shouldShowOutput())return;let Q=this.config.prettyLevel||"minimal";if(K)console.log();if(Q==="minimal"){let U=$.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,U)}`);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((U)=>!U.success).forEach((U)=>{let J=U.request.name||U.request.url;console.log(` ${this.color("\u2022","red")} ${J}: ${U.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 m($,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,U]=z,J=Number.parseInt(U,10);if(Z=Z[W],Array.isArray(Z))Z=Z[J];else return}else if(/^\d+$/.test(X)&&Array.isArray(Z))Z=Z[Number.parseInt(X,10)];else Z=Z[X]}return Z}function c($){if($===void 0||$===null)return"";if(typeof $==="string")return $;if(typeof $==="number"||typeof $==="boolean")return String($);return JSON.stringify($)}function h($,K){let Q={},Z={status:$.status,headers:$.headers||{},body:$.body,metrics:$.metrics};for(let[X,z]of Object.entries(K)){let W=m(Z,z);Q[X]=c(W)}return Q}function b(){return{}}class S{logger;globalConfig;constructor($={}){this.globalConfig=$,this.logger=new M($.output)}mergeOutputConfig($){return{...this.globalConfig.output,...$.sourceOutputConfig}}isFileAttachment($){return typeof $==="object"&&$!==null&&"file"in $}async validateFileAttachments($){if(!$.formData)return;let K=[];for(let[Q,Z]of Object.entries($.formData))if(this.isFileAttachment(Z)){let X=Z.file;if(!await Bun.file(X).exists())K.push(`${Q}: ${X}`)}if(K.length>0)return`File(s) not found: ${K.join(", ")}`;return}async executeRequest($,K=0){let Q=performance.now(),Z=this.mergeOutputConfig($),X=new M(Z);X.logRequestStart($,K);let z=await this.validateFileAttachments($);if(z){let H={request:$,success:!1,error:z,metrics:{duration:performance.now()-Q}};return X.logRequestComplete(H),H}let W=j.buildCommand($);X.logCommand(W);let U=0,J,w=($.retry?.count||0)+1;while(U<w){if(U>0){if(X.logRetry(U,w-1),$.retry?.delay)await Bun.sleep($.retry.delay)}let H=await j.executeCurl(W);if(H.success){let D=H.body;try{if(H.headers?.["content-type"]?.includes("application/json")||D&&(D.trim().startsWith("{")||D.trim().startsWith("[")))D=JSON.parse(D)}catch(T){}let I={request:$,success:!0,status:H.status,headers:H.headers,body:D,metrics:{...H.metrics,duration:performance.now()-Q}};if($.expect){let T=this.validateResponse(I,$.expect);if(!T.success)I.success=!1,I.error=T.error}return X.logRequestComplete(I),I}J=H.error,U++}let _={request:$,success:!1,error:J,metrics:{duration:performance.now()-Q}};return X.logRequestComplete(_),_}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,U;if(Array.isArray($)&&this.isArraySelector(X))U=this.getArrayValue($,X);else U=$?.[X];if(typeof z==="object"&&z!==null&&!Array.isArray(z)){let J=this.validateBodyProperties(U,z,W);Z.push(...J)}else{let J=this.validateValue(U,z,W);if(!J.isValid)Z.push(J.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 U=W[1],J=Number(W[2]);switch(U){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 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=b();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 U=h(W,z.store);Object.assign(Z,U),this.logStoredValues(U)}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 G.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 B(){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 C=`${process.env.HOME}/.curl-runner-version-cache.json`,i=86400000,n="https://registry.npmjs.org/@curl-runner/cli/latest";class R{async checkForUpdates($=!1){try{if(process.env.CI)return;let K=B();if(K==="0.0.0")return;if(!$){let Z=await this.getCachedVersion();if(Z&&Date.now()-Z.lastCheck<i){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(n,{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 U=X[W]||0,J=z[W]||0;if(J>U)return!0;if(J<U)return!1}return!1}catch{return!1}}async getCachedVersion(){try{let $=Bun.file(C);if(await $.exists())return JSON.parse(await $.text())}catch{}return null}async setCachedVersion($){try{let K={lastCheck:Date.now(),latestVersion:$};await Bun.write(C,JSON.stringify(K))}catch{}}}class v{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 G.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${B()}`);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),U=[],J=[];for(let D of z){this.logger.logInfo(`Processing: ${D}`);let{requests:I,config:T}=await this.processYamlFile(D),A=T?.output||{},F=I.map((Y)=>({...Y,sourceOutputConfig:A,sourceFile:D}));if(T){let{...Y}=T;W=this.mergeGlobalConfigs(W,Y)}J.push({file:D,requests:F,config:T}),U.push(...F)}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 D=Q.noRetry?0:Q.retries||0;W.defaults={...W.defaults,retry:{...W.defaults?.retry,count:D}}}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(U.length===0)this.logger.logError("No requests found in YAML files"),process.exit(1);let w=new S(W),_;if(J.length>1){let D=[],I=0;for(let F=0;F<J.length;F++){let Y=J[F];this.logger.logFileHeader(Y.file,Y.requests.length);let k=await w.execute(Y.requests);if(D.push(...k.results),I+=k.duration,F<J.length-1)console.log()}let T=D.filter((F)=>F.success).length,A=D.filter((F)=>!F.success).length;_={total:D.length,successful:T,failed:A,duration:I,results:D},w.logger.logSummary(_,!0)}else _=await w.execute(U);let H=this.determineExitCode(_,W);process.exit(H)}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 U=Number.parseFloat(W);if(U>=0&&U<=100)K.failOnPercentage=U}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 U=$[Z+1];if(U&&!U.startsWith("-"))K.output=U,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 u(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 G.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 U=this.prepareRequest(W,X,z);Q.push(U)}if(K.collection?.requests)for(let W of K.collection.requests){let U=this.prepareRequest(W,X,z);Q.push(U)}return{requests:Q,config:Z}}prepareRequest($,K,Q){let Z=G.interpolateVariables($,K);return G.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")}
|
|
@@ -96,7 +96,7 @@ ${this.logger.color("YAML STRUCTURE:","yellow")}
|
|
|
96
96
|
requests:
|
|
97
97
|
- url: \${BASE_URL}/users
|
|
98
98
|
method: GET
|
|
99
|
-
`)}}var
|
|
99
|
+
`)}}var l=new v;l.run(process.argv.slice(2));
|
|
100
100
|
|
|
101
|
-
//# debugId=
|
|
101
|
+
//# debugId=EA3EA83C7A39EBEE64756E2164756E21
|
|
102
102
|
//# sourceMappingURL=cli.js.map
|
package/package.json
CHANGED
|
@@ -2,6 +2,8 @@ import { YamlParser } from '../parser/yaml';
|
|
|
2
2
|
import type {
|
|
3
3
|
ExecutionResult,
|
|
4
4
|
ExecutionSummary,
|
|
5
|
+
FileAttachment,
|
|
6
|
+
FormFieldValue,
|
|
5
7
|
GlobalConfig,
|
|
6
8
|
JsonValue,
|
|
7
9
|
RequestConfig,
|
|
@@ -28,6 +30,42 @@ export class RequestExecutor {
|
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Checks if a form field value is a file attachment.
|
|
35
|
+
*/
|
|
36
|
+
private isFileAttachment(value: FormFieldValue): value is FileAttachment {
|
|
37
|
+
return typeof value === 'object' && value !== null && 'file' in value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validates that all file attachments in formData exist.
|
|
42
|
+
* Returns an error message if any file is missing, or undefined if all files exist.
|
|
43
|
+
*/
|
|
44
|
+
private async validateFileAttachments(config: RequestConfig): Promise<string | undefined> {
|
|
45
|
+
if (!config.formData) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const missingFiles: string[] = [];
|
|
50
|
+
|
|
51
|
+
for (const [fieldName, fieldValue] of Object.entries(config.formData)) {
|
|
52
|
+
if (this.isFileAttachment(fieldValue)) {
|
|
53
|
+
const filePath = fieldValue.file;
|
|
54
|
+
const file = Bun.file(filePath);
|
|
55
|
+
const exists = await file.exists();
|
|
56
|
+
if (!exists) {
|
|
57
|
+
missingFiles.push(`${fieldName}: ${filePath}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (missingFiles.length > 0) {
|
|
63
|
+
return `File(s) not found: ${missingFiles.join(', ')}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
31
69
|
async executeRequest(config: RequestConfig, index: number = 0): Promise<ExecutionResult> {
|
|
32
70
|
const startTime = performance.now();
|
|
33
71
|
|
|
@@ -37,6 +75,21 @@ export class RequestExecutor {
|
|
|
37
75
|
|
|
38
76
|
requestLogger.logRequestStart(config, index);
|
|
39
77
|
|
|
78
|
+
// Validate file attachments exist before executing
|
|
79
|
+
const fileError = await this.validateFileAttachments(config);
|
|
80
|
+
if (fileError) {
|
|
81
|
+
const failedResult: ExecutionResult = {
|
|
82
|
+
request: config,
|
|
83
|
+
success: false,
|
|
84
|
+
error: fileError,
|
|
85
|
+
metrics: {
|
|
86
|
+
duration: performance.now() - startTime,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
requestLogger.logRequestComplete(failedResult);
|
|
90
|
+
return failedResult;
|
|
91
|
+
}
|
|
92
|
+
|
|
40
93
|
const command = CurlBuilder.buildCommand(config);
|
|
41
94
|
requestLogger.logCommand(command);
|
|
42
95
|
|
package/src/types/config.ts
CHANGED
|
@@ -4,6 +4,47 @@ export interface JsonObject {
|
|
|
4
4
|
}
|
|
5
5
|
export interface JsonArray extends Array<JsonValue> {}
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Configuration for a file attachment in a form data request.
|
|
9
|
+
*
|
|
10
|
+
* Examples:
|
|
11
|
+
* - `{ file: "./image.png" }` - Simple file attachment
|
|
12
|
+
* - `{ file: "./doc.pdf", filename: "document.pdf" }` - With custom filename
|
|
13
|
+
* - `{ file: "./data.json", contentType: "application/json" }` - With explicit content type
|
|
14
|
+
*/
|
|
15
|
+
export interface FileAttachment {
|
|
16
|
+
/** Path to the file (relative to YAML file or absolute) */
|
|
17
|
+
file: string;
|
|
18
|
+
/** Custom filename to send (defaults to actual filename) */
|
|
19
|
+
filename?: string;
|
|
20
|
+
/** Explicit content type (curl will auto-detect if not specified) */
|
|
21
|
+
contentType?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A form field value can be a string, number, boolean, or a file attachment.
|
|
26
|
+
*/
|
|
27
|
+
export type FormFieldValue = string | number | boolean | FileAttachment;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configuration for form data (multipart/form-data) requests.
|
|
31
|
+
* Each key is a form field name, and the value can be a simple value or a file attachment.
|
|
32
|
+
*
|
|
33
|
+
* Examples:
|
|
34
|
+
* ```yaml
|
|
35
|
+
* formData:
|
|
36
|
+
* name: "John Doe"
|
|
37
|
+
* age: 30
|
|
38
|
+
* avatar:
|
|
39
|
+
* file: "./avatar.png"
|
|
40
|
+
* document:
|
|
41
|
+
* file: "./report.pdf"
|
|
42
|
+
* filename: "quarterly-report.pdf"
|
|
43
|
+
* contentType: "application/pdf"
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export type FormDataConfig = Record<string, FormFieldValue>;
|
|
47
|
+
|
|
7
48
|
/**
|
|
8
49
|
* Configuration for storing response values as variables for subsequent requests.
|
|
9
50
|
* Maps a variable name to a JSON path in the response.
|
|
@@ -24,6 +65,18 @@ export interface RequestConfig {
|
|
|
24
65
|
params?: Record<string, string>;
|
|
25
66
|
sourceFile?: string; // Source YAML file for better output organization
|
|
26
67
|
body?: JsonValue;
|
|
68
|
+
/**
|
|
69
|
+
* Form data for multipart/form-data requests.
|
|
70
|
+
* Use this for file uploads or when you need to send form fields.
|
|
71
|
+
* Cannot be used together with 'body'.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* formData:
|
|
75
|
+
* username: "john"
|
|
76
|
+
* avatar:
|
|
77
|
+
* file: "./avatar.png"
|
|
78
|
+
*/
|
|
79
|
+
formData?: FormDataConfig;
|
|
27
80
|
timeout?: number;
|
|
28
81
|
followRedirects?: boolean;
|
|
29
82
|
maxRedirects?: number;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { CurlBuilder } from './curl-builder';
|
|
3
|
+
|
|
4
|
+
describe('CurlBuilder', () => {
|
|
5
|
+
describe('buildCommand', () => {
|
|
6
|
+
test('should build basic GET request', () => {
|
|
7
|
+
const command = CurlBuilder.buildCommand({
|
|
8
|
+
url: 'https://example.com/api',
|
|
9
|
+
method: 'GET',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(command).toContain('curl');
|
|
13
|
+
expect(command).toContain('-X GET');
|
|
14
|
+
expect(command).toContain('"https://example.com/api"');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('should build POST request with JSON body', () => {
|
|
18
|
+
const command = CurlBuilder.buildCommand({
|
|
19
|
+
url: 'https://example.com/api',
|
|
20
|
+
method: 'POST',
|
|
21
|
+
body: { name: 'test' },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(command).toContain('-X POST');
|
|
25
|
+
expect(command).toContain('-d \'{"name":"test"}\'');
|
|
26
|
+
expect(command).toContain('Content-Type: application/json');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should build POST request with form data', () => {
|
|
30
|
+
const command = CurlBuilder.buildCommand({
|
|
31
|
+
url: 'https://example.com/upload',
|
|
32
|
+
method: 'POST',
|
|
33
|
+
formData: {
|
|
34
|
+
username: 'john',
|
|
35
|
+
age: 30,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(command).toContain('-X POST');
|
|
40
|
+
expect(command).toContain("-F 'username=john'");
|
|
41
|
+
expect(command).toContain("-F 'age=30'");
|
|
42
|
+
expect(command).not.toContain('-d');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should build POST request with file attachment', () => {
|
|
46
|
+
const command = CurlBuilder.buildCommand({
|
|
47
|
+
url: 'https://example.com/upload',
|
|
48
|
+
method: 'POST',
|
|
49
|
+
formData: {
|
|
50
|
+
document: {
|
|
51
|
+
file: './test.pdf',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(command).toContain("-F 'document=@./test.pdf'");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('should build POST request with file attachment and custom filename', () => {
|
|
60
|
+
const command = CurlBuilder.buildCommand({
|
|
61
|
+
url: 'https://example.com/upload',
|
|
62
|
+
method: 'POST',
|
|
63
|
+
formData: {
|
|
64
|
+
document: {
|
|
65
|
+
file: './test.pdf',
|
|
66
|
+
filename: 'report.pdf',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(command).toContain("-F 'document=@./test.pdf;filename=report.pdf'");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('should build POST request with file attachment and content type', () => {
|
|
75
|
+
const command = CurlBuilder.buildCommand({
|
|
76
|
+
url: 'https://example.com/upload',
|
|
77
|
+
method: 'POST',
|
|
78
|
+
formData: {
|
|
79
|
+
data: {
|
|
80
|
+
file: './data.json',
|
|
81
|
+
contentType: 'application/json',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(command).toContain("-F 'data=@./data.json;type=application/json'");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('should build POST request with file attachment including all options', () => {
|
|
90
|
+
const command = CurlBuilder.buildCommand({
|
|
91
|
+
url: 'https://example.com/upload',
|
|
92
|
+
method: 'POST',
|
|
93
|
+
formData: {
|
|
94
|
+
document: {
|
|
95
|
+
file: './report.pdf',
|
|
96
|
+
filename: 'quarterly-report.pdf',
|
|
97
|
+
contentType: 'application/pdf',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(command).toContain(
|
|
103
|
+
"-F 'document=@./report.pdf;filename=quarterly-report.pdf;type=application/pdf'",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('should build POST request with mixed form data and files', () => {
|
|
108
|
+
const command = CurlBuilder.buildCommand({
|
|
109
|
+
url: 'https://example.com/upload',
|
|
110
|
+
method: 'POST',
|
|
111
|
+
formData: {
|
|
112
|
+
title: 'My Document',
|
|
113
|
+
description: 'Test upload',
|
|
114
|
+
file: {
|
|
115
|
+
file: './document.pdf',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(command).toContain("-F 'title=My Document'");
|
|
121
|
+
expect(command).toContain("-F 'description=Test upload'");
|
|
122
|
+
expect(command).toContain("-F 'file=@./document.pdf'");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('should escape single quotes in form field values', () => {
|
|
126
|
+
const command = CurlBuilder.buildCommand({
|
|
127
|
+
url: 'https://example.com/upload',
|
|
128
|
+
method: 'POST',
|
|
129
|
+
formData: {
|
|
130
|
+
message: "It's a test",
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(command).toContain("-F 'message=It'\\''s a test'");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('should prefer formData over body when both are present', () => {
|
|
138
|
+
const command = CurlBuilder.buildCommand({
|
|
139
|
+
url: 'https://example.com/api',
|
|
140
|
+
method: 'POST',
|
|
141
|
+
formData: {
|
|
142
|
+
field: 'value',
|
|
143
|
+
},
|
|
144
|
+
body: { name: 'test' },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(command).toContain("-F 'field=value'");
|
|
148
|
+
expect(command).not.toContain('-d');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('should handle boolean form field values', () => {
|
|
152
|
+
const command = CurlBuilder.buildCommand({
|
|
153
|
+
url: 'https://example.com/api',
|
|
154
|
+
method: 'POST',
|
|
155
|
+
formData: {
|
|
156
|
+
active: true,
|
|
157
|
+
disabled: false,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(command).toContain("-F 'active=true'");
|
|
162
|
+
expect(command).toContain("-F 'disabled=false'");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RequestConfig } from '../types/config';
|
|
1
|
+
import type { FileAttachment, FormFieldValue, RequestConfig } from '../types/config';
|
|
2
2
|
|
|
3
3
|
interface CurlMetrics {
|
|
4
4
|
response_code?: number;
|
|
@@ -11,6 +11,20 @@ interface CurlMetrics {
|
|
|
11
11
|
time_starttransfer?: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Checks if a form field value is a file attachment.
|
|
16
|
+
*/
|
|
17
|
+
function isFileAttachment(value: FormFieldValue): value is FileAttachment {
|
|
18
|
+
return typeof value === 'object' && value !== null && 'file' in value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Escapes a string value for use in curl -F flag.
|
|
23
|
+
*/
|
|
24
|
+
function escapeFormValue(value: string): string {
|
|
25
|
+
return value.replace(/'/g, "'\\''");
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
// Using class for organization, but could be refactored to functions
|
|
15
29
|
export class CurlBuilder {
|
|
16
30
|
static buildCommand(config: RequestConfig): string {
|
|
@@ -34,7 +48,26 @@ export class CurlBuilder {
|
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
50
|
|
|
37
|
-
if (config.
|
|
51
|
+
if (config.formData) {
|
|
52
|
+
// Use -F flags for multipart/form-data
|
|
53
|
+
for (const [fieldName, fieldValue] of Object.entries(config.formData)) {
|
|
54
|
+
if (isFileAttachment(fieldValue)) {
|
|
55
|
+
// File attachment: -F "field=@filepath;filename=name;type=mimetype"
|
|
56
|
+
let fileSpec = `@${fieldValue.file}`;
|
|
57
|
+
if (fieldValue.filename) {
|
|
58
|
+
fileSpec += `;filename=${fieldValue.filename}`;
|
|
59
|
+
}
|
|
60
|
+
if (fieldValue.contentType) {
|
|
61
|
+
fileSpec += `;type=${fieldValue.contentType}`;
|
|
62
|
+
}
|
|
63
|
+
parts.push('-F', `'${fieldName}=${escapeFormValue(fileSpec)}'`);
|
|
64
|
+
} else {
|
|
65
|
+
// Regular form field: -F "field=value"
|
|
66
|
+
const strValue = String(fieldValue);
|
|
67
|
+
parts.push('-F', `'${fieldName}=${escapeFormValue(strValue)}'`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} else if (config.body) {
|
|
38
71
|
const bodyStr = typeof config.body === 'string' ? config.body : JSON.stringify(config.body);
|
|
39
72
|
parts.push('-d', `'${bodyStr.replace(/'/g, "'\\''")}'`);
|
|
40
73
|
|