@powerduck/openapi-cli 0.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.
@@ -0,0 +1 @@
1
+ import e from"fs";import t from"path";import r from"http";import s from"https";import{dereference as n}from"@powerduck/openapi-parser";var o={outputDir:"./openapi-cli-report",formats:["json","cli","html"],concurrency:5,timeout:3e4,failOnError:!0,grpcReflection:!0,mcpTransport:"streamable-http"};function a(r){const s=r.config?function(r){const s=t.resolve(r);if(!e.existsSync(s))throw new Error(`Config file not found: ${s}`);const n=e.readFileSync(s,"utf-8");try{return JSON.parse(n)}catch(e){throw new Error(`Invalid config file (must be JSON): ${e.message}`)}}(r.config):{},n={specPath:r.spec??s.specPath??"",serverUrl:r.server??s.serverUrl,outputDir:r.output??s.outputDir??o.outputDir,formats:i(r.format??s.formats),filter:c(r,s),concurrency:r.concurrency??s.concurrency??o.concurrency,timeout:r.timeout??s.timeout??o.timeout,proxy:r.proxy??s.proxy,tls:{caCert:r.ca??s.tls?.caCert,clientCert:r.cert??s.tls?.clientCert,clientKey:r.key??s.tls?.clientKey,strictSSL:void 0!==r.insecure?!r.insecure:s.tls?.strictSSL},headers:p(r.header,s.headers),auth:d(r,s),variables:l(r.variable,s.variables),failOnError:r.failOnError??s.failOnError??o.failOnError,envFile:r.env??s.envFile,grpcReflection:r.grpcReflection??s.grpcReflection??o.grpcReflection,grpcProtoPaths:r.grpcProto??s.grpcProtoPaths,mcpTransport:r.mcpTransport??s.mcpTransport??o.mcpTransport,mcpCommand:r.mcpCommand??s.mcpCommand,mcpArgs:r.mcpArgs?r.mcpArgs.split(/[\s,]+/).filter(Boolean):s.mcpArgs,mcpCwd:r.mcpCwd??s.mcpCwd};if(n.envFile&&function(r){const s=t.resolve(r);if(!e.existsSync(s))return;const n=e.readFileSync(s,"utf-8").split("\n");for(const e of n){const t=e.trim();if(!t||t.startsWith("#"))continue;const r=t.indexOf("=");if(r>0){const e=t.slice(0,r).trim(),s=t.slice(r+1).trim().replace(/^["']|["']$/g,"");e in process.env||(process.env[e]=s)}}}(n.envFile),function(e){const t=e=>"string"!=typeof e?e:e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(e.serverUrl=t(e.serverUrl),e.proxy=t(e.proxy),e.headers)for(const[r,s]of Object.entries(e.headers))e.headers[r]=t(s);e.auth?.token&&(e.auth.token=t(e.auth.token));if(e.variables)for(const[r,s]of Object.entries(e.variables))e.variables[r]=t(s)}(n),!n.specPath)throw new Error("No OpenAPI spec path provided. Use --spec <path> or set specPath in config.");if(n.proxy)try{new URL(n.proxy)}catch{throw new Error(`Invalid proxy URL: ${n.proxy}`)}return n}function i(e){return Array.isArray(e)?e:"string"==typeof e?e.split(",").map(e=>e.trim().toLowerCase()).filter(Boolean):o.formats}function c(e,t){const r=e.method?e.method.split(",").map(e=>e.trim().toLowerCase()):t.filter?.methods,s=e.path?e.path.split(",").map(e=>e.trim()):t.filter?.paths,n=e.tag?e.tag.split(",").map(e=>e.trim()):t.filter?.tags,o=e.operationId?e.operationId.split(",").map(e=>e.trim()):t.filter?.operationIds;if(r||s||n||o)return{methods:r,paths:s,tags:n,operationIds:o}}function p(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf(":");if(e>0){const s=t.slice(0,e).trim(),n=t.slice(e+1).trim();s&&(r[s]=n)}}return Object.keys(r).length?r:void 0}function d(e,t){return e.bearer?{type:"bearer",token:e.bearer}:t.auth}function l(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf("=");e>0&&(r[t.slice(0,e).trim()]=t.slice(e+1).trim())}return Object.keys(r).length?r:void 0}function u(e){return/^https?:\/\//i.test(e.trim())}async function f(a){const i=a.specPath.trim();let c;if(u(i))c=await function(e,t){let n=0;return new Promise((o,a)=>{const i=c=>{const p=("https:"===new URL(c).protocol?s:r).get(c,{timeout:t,headers:{Accept:"application/json, text/plain, */*","User-Agent":"@powerduck/openapi-cli"}},t=>{if(t.statusCode&&t.statusCode>=300&&t.statusCode<400&&t.headers.location){if(++n>5)return t.resume(),void a(new Error(`Too many redirects (>5) while fetching spec: ${e}`));const r=new URL(t.headers.location,c).toString();return t.resume(),void i(r)}if(200!==t.statusCode)return t.resume(),void a(new Error(`Failed to fetch spec from ${c}: HTTP ${t.statusCode} ${t.statusMessage??""}`.trim()));const r=[];t.on("data",e=>r.push(e)),t.on("end",()=>o(Buffer.concat(r).toString("utf-8"))),t.on("error",e=>a(new Error(`Error reading spec response: ${e.message}`)))});p.on("timeout",()=>{p.destroy(new Error(`Timed out after ${t}ms while fetching spec: ${e}`))}),p.on("error",t=>a(new Error(`Network error fetching spec ${e}: ${t.message}`)))};i(e)})}(i,a.timeout??o.timeout);else{const r=t.resolve(i);if(!e.existsSync(r))throw new Error(`OpenAPI spec not found: ${r}`);if(!e.statSync(r).isFile())throw new Error(`OpenAPI spec path is not a file: ${r}`);c=e.readFileSync(r,"utf-8")}const p=function(e,t){let r;try{r=JSON.parse(e)}catch(e){throw new Error(`Spec from ${t} is not valid JSON: ${e.message}`)}if(!r||"object"!=typeof r||Array.isArray(r))throw new Error(`Spec from ${t} is not a JSON object`);if(!r.paths||"object"!=typeof r.paths)throw new Error(`Spec from ${t} does not contain a valid 'paths' object`);return r}(c,i);return async function(e){try{const t=await n(e);return t.errors&&t.errors.length>0?e:t.schema??t.specification??e}catch(t){return e}}(p)}function m(e,t){return e.map(e=>{try{const r=function(e,t){switch(e.assert){case"status":if(void 0===e.value)throw new Error("status assertion requires 'value'");return t.status===e.value;case"header":{if(!e.key)throw new Error("header assertion requires 'key'");const r=function(e,t){const r=t.toLowerCase();for(const[t,s]of Object.entries(e))if(t.toLowerCase()===r)return s;return}(t.headers??{},e.key);return void 0!==r&&(void 0===e.contains||r.toLowerCase().includes(e.contains.toLowerCase()))}case"bodyContains":if(void 0===e.contains)throw new Error("bodyContains assertion requires 'contains'");return(t.bodyText??JSON.stringify(t.body??"")).includes(e.contains);case"bodyEquals":if(void 0===e.body)throw new Error("bodyEquals assertion requires 'body'");return(t.bodyText??JSON.stringify(t.body??""))===e.body;case"jsonPath":{if(!e.path)throw new Error("jsonPath assertion requires 'path'");const r=h(t.body,e.path);return void 0!==e.exists?e.exists?void 0!==r:void 0===r:void 0!==e.equals?v(r,e.equals):void 0!==r}case"responseTime":if(void 0===e.max)throw new Error("responseTime assertion requires 'max'");return t.durationMs<=e.max;default:throw new Error(`Unknown assertion type: ${e.assert}`)}}(e,t);return{name:e.name,passed:r}}catch(t){return{name:e.name,passed:!1,error:t.message}}})}function h(e,t){const r=t.replace(/^\$\.?/,"");if(!r)return e;const s=r.split(/\.|\[(\d+)\]/).filter(e=>void 0!==e&&""!==e);let n=e;for(const e of s){if(null==n)return;n=/^\d+$/.test(e)?n[Number(e)]:n[e]}return n}function v(e,t){if(e===t)return!0;if(typeof e!=typeof t)return!1;if(null===e||null===t)return e===t;if("object"!=typeof e)return e===t;if(Array.isArray(e)!==Array.isArray(t))return!1;if(Array.isArray(e)&&Array.isArray(t))return e.length===t.length&&e.every((e,r)=>v(e,t[r]));const r=Object.keys(e),s=Object.keys(t);return r.length===s.length&&r.every(r=>v(e[r],t[r]))}function g(e,t){const r=e?.["x-tests"],s=t?.["x-tests"];return[...Array.isArray(s)?s:[],...Array.isArray(r)?r:[]].filter(e=>e&&"object"==typeof e&&e.assert&&e.name)}function b(e){const t=e?.["x-postman-scripts"];if(t&&"object"==typeof t&&"string"==typeof t.test)return t.test}import{createClient as y}from"@powerduck/request";import x from"fs";function w(e){if(!e)return;if("string"==typeof e)return e;if(e instanceof Error)return e.message;const t=e;if("string"==typeof t.message)return t.message;if("string"==typeof t.msg)return t.msg;try{return JSON.stringify(e)}catch{return String(e)}}async function $(e){const t=await f(e),r=y(),s=function(e,t){const r=[],s=["get","post","put","delete","patch","head","options"];for(const[n,o]of Object.entries(e.paths??{})){const e=o;for(const o of s){const s=e[o];if(!s||"object"!=typeof s)continue;const a={path:n,method:o,operation:s,pathItem:e,operationId:s.operationId??`${o.toUpperCase()} ${n}`,protocol:k(s,e)};S(a,t.filter)&&r.push(a)}}return r}(t,e),n=Date.now(),o=[],a=[...s],i=Math.min(e.concurrency??5,a.length),c=[];for(let s=0;s<i;s++)c.push((async()=>{for(;a.length>0;){const s=a.shift();if(!s)break;const n=await C(r,s,e,t);o.push(n)}})());await Promise.all(c);const p=Date.now()-n,d=function(e,t){const r=e.length,s=e.filter(e=>"passed"===e.status).length,n=e.filter(e=>"failed"===e.status).length,o=e.filter(e=>"error"===e.status).length,a=e.filter(e=>"skipped"===e.status).length;return{total:r,passed:s,failed:n,errors:o,skipped:a,durationMs:t,passRate:r>0?Math.round(s/r*1e3)/10:0}}(o,p);return{summary:d,results:o.sort((e,t)=>e.path.localeCompare(t.path)||e.method.localeCompare(t.method)),config:e,generatedAt:(new Date).toISOString(),version:"0.1.0"}}function k(e,t){const r=e["x-protocol"]??t?.["x-protocol"];if(r)return String(r).toLowerCase();const s=e.responses??{};for(const e of Object.values(s)){if((e?.content??{})["text/event-stream"])return"sse"}return e["x-response-stream"]?"sse":"http"}function S(e,t){if(!t)return!0;if(t.methods?.length&&!t.methods.includes(e.method.toLowerCase()))return!1;if(t.operationIds?.length&&!t.operationIds.includes(e.operationId))return!1;if(t.tags?.length){const r=e.operation.tags??[];if(!t.tags.some(e=>r.includes(e)))return!1}if(t.paths?.length){if(!t.paths.some(t=>{try{return new RegExp(t).test(e.path)}catch{return e.path.includes(t)}}))return!1}return!0}async function C(e,t,r,s){const n=Date.now(),o={operationId:t.operationId,path:t.path,method:t.method.toUpperCase(),protocol:t.protocol};try{const a=g(t.operation,t.pathItem),i=b(t.operation),c=[];let p;switch(t.protocol){case"grpc":p=await async function(e,t,r){const s=t.operation["x-grpc"]??{},n=s.address??r.serverUrl?.replace(/^https?:\/\//,"");if(!n)throw new Error("gRPC operation requires x-grpc.address or --server");const o=e.connect({kind:"grpc",address:n,service:s.service,method:s.method??t.operationId,reflection:r.grpcReflection,protoPaths:r.grpcProtoPaths,metadata:r.headers});await o.open();const a=o.kind,i=function(e){const t=e["x-grpc"]??{};if(t.sample&&"object"==typeof t.sample)return t.sample;const r=e.requestBody?.content?.["application/json"]?.schema;if(r?.properties){const e={};for(const[t,s]of Object.entries(r.properties))e[t]=s.default??E(s.type);return e}return{}}(t.operation);"unary"===a||"server_streaming"===a?await o.send(i):(await o.send(i),await o.close());const c=o.events??[],p=c.find(e=>"status"===e.kind),d=c.filter(e=>"data"===e.kind&&"inbound"===e.direction);"unary"!==a&&"server_streaming"!==a||await o.close();return{status:p?.meta?.code??0,statusText:p?.meta?.statusName,body:1===d.length?d[0].data:d.map(e=>e.data),events:c.map(e=>({kind:e.kind,direction:e.direction,data:e.data,meta:e.meta}))}}(e,t,r);break;case"websocket":p=await async function(e,t,r){const s=t.operation["x-ws"]??{},n=s.url??r.serverUrl?.replace(/^http/,"ws");if(!n)throw new Error("WebSocket operation requires x-ws.url or --server");const o=e.connect({kind:"websocket",url:n,subprotocols:s.subprotocols,headers:r.headers});await o.open();const a=s.send??[];for(const e of a.slice(0,3))await o.send(e);await new Promise(e=>setTimeout(e,500)),await o.close();const i=o.events??[];return{status:101,statusText:"Switching Protocols",body:i.filter(e=>"in"===e.direction&&"open"!==e.kind).map(e=>e.data),events:i.map(e=>({kind:e.kind,direction:e.direction,data:e.data})),streaming:!0}}(e,t,r);break;case"mcp":p=await async function(e,t,r){const s=t.operation["x-mcp"]??{},n=s.endpoint??r.serverUrl,o=s.method??"tools/call",a=s.name,i=s.arguments??{};if("stdio"===r.mcpTransport){if(!r.mcpCommand)throw new Error("MCP stdio requires --mcp-command");const t=e.connect({kind:"mcp",transport:"stdio",command:r.mcpCommand,args:r.mcpArgs,cwd:r.mcpCwd,timeoutMs:r.timeout});await t.open();const s=await t.request(o,{name:a,arguments:i});return await t.close(),{status:200,statusText:"OK",body:s}}const c={openapi:"3.2.0",info:{title:"mcp",version:"1.0.0"},paths:{"/mcp":{post:{operationId:t.operationId,"x-protocol":"mcp","x-mcp":{endpoint:n,method:o,name:a,arguments:i},responses:{200:{description:"ok"}}}}}},p=await e.send({spec:c,target:{path:"/mcp",method:"post"},timeout:r.timeout,mcp:r.headers?{headers:r.headers}:void 0});return{status:p.response?.status,statusText:p.response?.statusText,body:p.response?.body}}(e,t,r);break;default:p=await async function(e,t,r,s,n,o){const a={};if(r.proxy)try{const e=new URL(r.proxy);a.proxies=[{match:"*://*/*",host:e.hostname,port:e.port?Number(e.port):"https:"===e.protocol?443:80,tunnel:!0}]}catch{throw new Error(`Invalid proxy URL: ${r.proxy}`)}const i=r.tls;if(i){const e={...a.requester??{}};!1===i.strictSSL&&(e.strictSSL=!1),i.caCert&&(e.ca=x.readFileSync(i.caCert)),i.clientCert&&(e.cert=x.readFileSync(i.clientCert)),i.clientKey&&(e.key=x.readFileSync(i.clientKey)),Object.keys(e).length&&(a.requester=e)}const c=await e.send({spec:s,target:{path:t.path,method:t.method},serverUrl:r.serverUrl,values:r.headers?{header:r.headers}:void 0,auth:r.auth,variables:r.variables,timeout:r.timeout,runner:Object.keys(a).length?a:void 0,scripts:n?{test:{exec:n},fromSpecExtensions:!1}:void 0,onAssertion:e=>{o.push({name:e.name??"assertion",passed:e.passed,error:w(e.error)})},maxStreamMs:r.timeout,maxEvents:50});return{status:c.response?.status,statusText:c.response?.statusText,contentType:c.response?.contentType,sizeBytes:c.response?.sizeBytes,body:c.response?.body,text:c.response?.text,headers:c.response?.headers,events:c.response?.events,streaming:c.response?.streaming}}(e,t,r,s,i,c)}const d=Date.now()-n,l=a.length>0||c.length>0?[]:function(e,t){const r=[];if("http"===e||"sse"===e){const e=t?.status,s="number"==typeof e&&e>=200&&e<300;r.push({name:"HTTP status is 2xx (implicit)",passed:s,error:s?void 0:`expected 2xx, got ${e??"no status"}`})}else if("graphql"===e){const e=t?.status,s="number"==typeof e&&e>=200&&e<300,n=t?.body,o=Array.isArray(n?.errors)&&n.errors.length>0,a=s&&!o;let i;s?o&&(i=`GraphQL response contains ${n.errors.length} error(s)`):i=`expected HTTP 2xx, got ${e??"no status"}`,r.push({name:"GraphQL success (implicit)",passed:a,error:i})}else if("grpc"===e){const e=t?.statusCode??t?.code,s=t?.statusName??t?.status,n=0===e;r.push({name:"gRPC status is OK (implicit)",passed:n,error:n?void 0:`expected code 0 (OK), got ${e??"unknown"}${s?` (${s})`:""}`})}else if("mcp"===e){const e=t?.body??{},s=void 0!==e.result,n=void 0!==e.error||void 0!==t?.error,o=s&&!n;let a;n?a="MCP JSON-RPC response contains an error":s||(a="MCP JSON-RPC response missing 'result'"),r.push({name:"MCP JSON-RPC success (implicit)",passed:o,error:a})}else if("websocket"===e){const e=t?.events??[],s=e.some(e=>"open"===e.kind||"open"===e.state),n=e.some(e=>"error"===e.kind||"error"===e.state),o=s&&!n;r.push({name:"WebSocket connected (implicit)",passed:o,error:o?void 0:n?"WebSocket connection errored":"WebSocket did not reach open state"})}return r}(t.protocol,p),u=a.length?m(a,{status:p?.status,headers:p?p.headers:void 0,body:p?.body,bodyText:p?p.text:void 0,durationMs:d}):[],f=new Set,h=c.filter(e=>{const t=`${e.name}|${e.passed}|${e.error??""}`;return!f.has(t)&&(f.add(t),!0)}),v=[...u,...h,...l],y=v.some(e=>!e.passed);return{...o,status:y?"failed":"passed",durationMs:d,response:p,assertions:v.length?v:void 0,timestamp:(new Date).toISOString()}}catch(e){const t=Date.now()-n;return{...o,status:"error",durationMs:t,error:e.message??String(e),timestamp:(new Date).toISOString()}}}function E(e){switch(e){case"string":default:return"";case"number":case"integer":return 0;case"boolean":return!1;case"array":return[];case"object":return{}}}import O from"fs";import T from"path";function P(e,t){const r=T.resolve(t);O.mkdirSync(r,{recursive:!0});const s=T.join(r,"report.json");return O.writeFileSync(s,JSON.stringify(e,null,2),"utf-8"),s}var A="",j="",R="",I="",z="",L="",M="",F="",U="",q="",N="";function D(e){if(null==e||!Number.isFinite(e))return`${R}--ms${A}`;return`${e>500?`${j}${L}`:j}${e}${A}${R}ms${A}`}function J(e){const t=[],{summary:r,results:s}=e;t.push(""),t.push(`${j}${M}╔══════════════════════════════════════════════════════╗${A}`),t.push(`${j}${M}║ Powerduck Test Report ║${A}`),t.push(`${j}${M}╚══════════════════════════════════════════════════════╝${A}`),t.push("");const n=r.passRate>=80?I:r.passRate>=50?L:z;t.push(`${j} Total:${A} ${r.total} ${I}Passed:${A} ${r.passed} ${z}Failed:${A} ${r.failed} ${L}Errors:${A} ${r.errors} ${n}Rate:${A} ${r.passRate}% ${R}Time:${A} ${D(r.durationMs)}`),t.push("");const o=r.total>0?Math.round(r.passed/r.total*50):0,a=r.total>0?Math.round(r.failed/r.total*50):0,i=r.total>0?Math.round(r.errors/r.total*50):0,c=I+"█".repeat(o)+z+"█".repeat(a)+L+"█".repeat(i)+F+"░".repeat(Math.max(0,50-o-a-i))+A;t.push(` [${c}]`),t.push("");for(const e of s)t.push(B(e));return t.push(""),r.failed>0||r.errors>0?t.push(`${j}${z} ✗ ${r.failed+r.errors} test(s) did not pass${A}`):t.push(`${j}${I} ✓ All ${r.total} test(s) passed${A}`),t.push(` ${R}Generated: ${e.generatedAt}${A}`),t.push(""),t.join("\n")}function B(e){const t="passed"===e.status?`${I}✓${A}`:"failed"===e.status?`${z}✗${A}`:"error"===e.status?`${L}!${A}`:`${F}-${A}`,r="passed"===e.status?`${U}${j} PASS ${A}`:"failed"===e.status?`${q}${j} FAIL ${A}`:"error"===e.status?`${N}${j} ERROR ${A}`:`${F} SKIP ${A}`,s=e.protocol?` ${M}[${e.protocol}]${A}`:"",n=null!=e.response?.status?` ${R}${e.response.status}${A}`:"";let o=` ${t} ${r} ${j}${e.method}${A} ${e.path}${s}${n} ${D(e.durationMs)}`;if(e.error&&(o+=`\n ${z}${e.error}${A}`),e.assertions?.length)for(const t of e.assertions){o+=`\n ${t.passed?`${I} ✓${A}`:`${z} ✗${A}`} ${t.name}${t.error?` — ${z}${t.error}${A}`:""}`}return o}function K(e){const t=J(e);return console.log(t),t}import H from"fs";import W from"path";function G(e,t){const r=W.resolve(t);H.mkdirSync(r,{recursive:!0});const s=W.join(r,"report.html");return H.writeFileSync(s,function(e){const{summary:t,results:r}=e,s=JSON.stringify({summary:t,results:r});return`<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>Test Report — Powerduck</title>\n<style>\n *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n :root{\n --bg:#ffffff;--fg:#0f172a;--muted:#64748b;--border:#e2e8f0;\n --bg-subtle:#f8fafc;--bg-hover:#f1f5f9;\n --green:#16a34a;--green-bg:#f0fdf4;--green-border:#bbf7d0;\n --red:#dc2626;--red-bg:#fef2f2;--red-border:#fecaca;\n --amber:#d97706;--amber-bg:#fffbeb;--amber-border:#fde68a;\n --blue:#2563eb;--mono:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;\n --sans:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,Inter,sans-serif;\n }\n @media (prefers-color-scheme:dark){\n :root{\n --bg:#0f172a;--fg:#f1f5f9;--muted:#94a3b8;--border:#1e293b;\n --bg-subtle:#1e293b;--bg-hover:#334155;\n --green:#4ade80;--green-bg:rgba(74,222,128,.1);--green-border:rgba(74,222,128,.25);\n --red:#f87171;--red-bg:rgba(248,113,113,.1);--red-border:rgba(248,113,113,.25);\n --amber:#fbbf24;--amber-bg:rgba(251,191,36,.1);--amber-border:rgba(251,191,36,.25);\n --blue:#60a5fa;\n }\n }\n body{font-family:var(--sans);background:var(--bg);color:var(--fg);line-height:1.5;font-size:14px;-webkit-font-smoothing:antialiased}\n .wrap{max-width:960px;margin:0 auto;padding:32px 24px 64px}\n\n /* Header */\n .hdr{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--border)}\n .hdr h1{font-size:18px;font-weight:600;letter-spacing:-.01em}\n .hdr .meta{font-size:12px;color:var(--muted);font-family:var(--mono)}\n\n /* Summary strip */\n .strip{display:grid;grid-template-columns:repeat(6,1fr);gap:0;border:1px solid var(--border);border-radius:8px;overflow:hidden;margin-bottom:24px}\n .stat{padding:14px 16px;border-right:1px solid var(--border);text-align:center}\n .stat:last-child{border-right:none}\n .stat .v{font-size:22px;font-weight:700;letter-spacing:-.02em;line-height:1.2}\n .stat .l{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-top:2px}\n .stat.total .v{color:var(--fg)}\n .stat.pass .v{color:var(--green)}\n .stat.fail .v{color:var(--red)}\n .stat.err .v{color:var(--amber)}\n .stat.rate .v{color:var(--blue)}\n .stat.time .v{font-size:16px;font-weight:600}\n\n /* Progress */\n .bar{height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:24px;display:flex}\n .bar>div{height:100%}\n .bar .p{background:var(--green)}\n .bar .f{background:var(--red)}\n .bar .e{background:var(--amber)}\n\n /* Filters */\n .filters{display:flex;gap:4px;margin-bottom:16px}\n .fbtn{padding:4px 12px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--muted);cursor:pointer;font-size:12px;font-weight:500;transition:all .12s}\n .fbtn:hover{color:var(--fg);background:var(--bg-hover)}\n .fbtn.active{background:var(--bg-subtle);color:var(--fg);border-color:var(--border)}\n .fbtn .n{color:var(--muted);margin-left:4px;font-variant-numeric:tabular-nums}\n\n /* Test rows */\n .row{border:1px solid var(--border);border-radius:8px;margin-bottom:6px;overflow:hidden;transition:border-color .12s}\n .row:hover{border-color:var(--muted)}\n .row-head{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;user-select:none}\n .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n .dot.pass{background:var(--green)}\n .dot.fail{background:var(--red)}\n .dot.err{background:var(--amber)}\n .method{font-family:var(--mono);font-size:11px;font-weight:600;color:var(--muted);min-width:40px;text-transform:uppercase}\n .path{font-family:var(--mono);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .proto{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);padding:2px 8px;border:1px solid var(--border);border-radius:4px}\n .dur{font-family:var(--mono);font-size:12px;color:var(--muted);min-width:56px;text-align:right;font-variant-numeric:tabular-nums}\n .chev{color:var(--muted);font-size:10px;transition:transform .15s;flex-shrink:0}\n .row.open .chev{transform:rotate(90deg)}\n\n /* Row body */\n .row-body{display:none;padding:0 14px 12px 32px;border-top:1px solid var(--border)}\n .row.open .row-body{display:block}\n .sect{margin-top:10px}\n .sect-t{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600;margin-bottom:6px}\n .asrt{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:13px}\n .asrt .ck{width:14px;height:14px;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;flex-shrink:0;color:#fff}\n .asrt.pass .ck{background:var(--green)}\n .asrt.fail .ck{background:var(--red)}\n .asrt .an{color:var(--fg)}\n .asrt .ae{color:var(--red);font-size:12px;margin-left:6px;font-family:var(--mono)}\n .rmeta{display:flex;gap:20px;flex-wrap:wrap;font-size:12px}\n .rmeta span{color:var(--muted)}\n .rmeta strong{color:var(--fg);font-weight:600;font-family:var(--mono)}\n .errbox{background:var(--red-bg);border:1px solid var(--red-border);border-radius:6px;padding:8px 12px;color:var(--red);font-size:12px;font-family:var(--mono);margin-top:10px}\n .bprev{background:var(--bg-subtle);border:1px solid var(--border);border-radius:6px;padding:10px 12px;font-family:var(--mono);font-size:11px;color:var(--muted);max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-all;margin-top:6px}\n\n footer{margin-top:32px;padding-top:16px;border-top:1px solid var(--border);font-size:11px;color:var(--muted);display:flex;justify-content:space-between}\n\n @media(max-width:640px){\n .strip{grid-template-columns:repeat(3,1fr)}\n .stat:nth-child(3){border-right:none}\n .stat:nth-child(4),.stat:nth-child(5){border-top:1px solid var(--border)}\n .path{font-size:11px}\n }\n</style>\n</head>\n<body>\n<div class="wrap">\n <div class="hdr">\n <h1>Test Report</h1>\n <span class="meta">${e.generatedAt} · v${e.version}</span>\n </div>\n\n <div class="strip">\n <div class="stat total"><div class="v">${t.total}</div><div class="l">Total</div></div>\n <div class="stat pass"><div class="v">${t.passed}</div><div class="l">Passed</div></div>\n <div class="stat fail"><div class="v">${t.failed}</div><div class="l">Failed</div></div>\n <div class="stat err"><div class="v">${t.errors}</div><div class="l">Errors</div></div>\n <div class="stat rate"><div class="v">${t.passRate}%</div><div class="l">Pass Rate</div></div>\n <div class="stat time"><div class="v">${(t.durationMs/1e3).toFixed(2)}s</div><div class="l">Duration</div></div>\n </div>\n\n <div class="bar">\n <div class="p" style="width:${t.total?t.passed/t.total*100:0}%"></div>\n <div class="f" style="width:${t.total?t.failed/t.total*100:0}%"></div>\n <div class="e" style="width:${t.total?t.errors/t.total*100:0}%"></div>\n </div>\n\n <div class="filters">\n <button class="fbtn active" data-f="all">All<span class="n">${t.total}</span></button>\n <button class="fbtn" data-f="passed">Passed<span class="n">${t.passed}</span></button>\n <button class="fbtn" data-f="failed">Failed<span class="n">${t.failed}</span></button>\n <button class="fbtn" data-f="error">Errors<span class="n">${t.errors}</span></button>\n </div>\n\n <div id="list"></div>\n\n <footer>\n <span>@powerduck/openapi-cli v${e.version}</span>\n <span>${t.passed}/${t.total} passed</span>\n </footer>\n</div>\n\n<script>\nconst D=${s};\nfunction esc(s){const d=document.createElement('div');d.textContent=String(s??'');return d.innerHTML}\nfunction render(f){\n const list=document.getElementById('list');\n const items=f==='all'?D.results:D.results.filter(r=>r.status===f);\n list.innerHTML=items.map(r=>{\n const as=r.assertions?r.assertions.map(a=>\`\n <div class="asrt \${a.passed?'pass':'fail'}">\n <div class="ck">\${a.passed?'✓':'✗'}</div>\n <span class="an">\${esc(a.name)}</span>\n \${a.error?'<span class="ae">'+esc(a.error)+'</span>':''}\n </div>\`).join(''):'';\n const bp=r.response?.body?'<div class="bprev">'+esc(JSON.stringify(r.response.body,null,2).slice(0,3000))+'</div>':'';\n const eb=r.error?'<div class="errbox">'+esc(r.error)+'</div>':'';\n const rm=r.response?\`\n <div class="rmeta">\n \${r.response.status!=null?'<span>Status <strong>'+r.response.status+(r.response.statusText?' '+r.response.statusText:'')+'</strong></span>':''}\n \${r.response.contentType?'<span>Type <strong>'+esc(r.response.contentType)+'</strong></span>':''}\n \${r.response.sizeBytes!=null?'<span>Size <strong>'+r.response.sizeBytes+'B</strong></span>':''}\n \${r.response.streaming?'<span>Streaming <strong>yes</strong></span>':''}\n </div>\`:'';\n return \`\n <div class="row \${r.status}" data-s="\${r.status}">\n <div class="row-head" onclick="this.parentElement.classList.toggle('open')">\n <div class="dot \${r.status==='passed'?'pass':r.status==='failed'?'fail':'err'}"></div>\n <span class="method">\${r.method}</span>\n <span class="path">\${esc(r.path)}</span>\n \${r.protocol?'<span class="proto">'+r.protocol+'</span>':''}\n <span class="dur">\${r.durationMs}ms</span>\n <span class="chev">▶</span>\n </div>\n <div class="row-body">\n \${eb}\n \${rm}\n \${as?'<div class="sect"><div class="sect-t">Assertions</div>'+as+'</div>':''}\n \${bp?'<div class="sect"><div class="sect-t">Response</div>'+bp+'</div>':''}\n </div>\n </div>\`;\n }).join('');\n}\ndocument.querySelectorAll('.fbtn').forEach(b=>b.addEventListener('click',()=>{\n document.querySelectorAll('.fbtn').forEach(x=>x.classList.remove('active'));\n b.classList.add('active');render(b.dataset.f);\n}));\nrender('all');\n<\/script>\n</body>\n</html>`}(e),"utf-8"),s}export{o as DEFAULT_CONFIG,a as resolveConfig,u as isRemoteSpec,f as loadSpec,m as runDeclarativeAssertions,h as resolveJsonPath,g as extractAssertions,b as extractPostmanScripts,$ as runTests,P as generateJsonReport,J as generateCliReport,K as printCliReport,G as generateHtmlReport};
package/dist/cli.cjs ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ "use strict";var e=Object.create,t=Object.defineProperty,r=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,s=Object.getPrototypeOf,n=Object.prototype.hasOwnProperty,a=(a,i,c)=>(c=null!=a?e(s(a)):{},((e,s,a,i)=>{if(s&&"object"==typeof s||"function"==typeof s)for(let c of o(s))n.call(e,c)||c===a||t(e,c,{get:()=>s[c],enumerable:!(i=r(s,c))||i.enumerable});return e})(!i&&a&&a.__esModule?c:t(c,"default",{value:a,enumerable:!0}),a)),i=require("commander"),c=a(require("fs"),1),p=a(require("path"),1),d=a(require("http"),1),l=a(require("https"),1),u=require("@powerduck/openapi-parser"),f="./openapi-cli-report",m=["json","cli","html"],h=5,v=3e4,g=!0,b=!0,y="streamable-http";function x(e){const t=e.config?function(e){const t=p.default.resolve(e);if(!c.default.existsSync(t))throw new Error(`Config file not found: ${t}`);const r=c.default.readFileSync(t,"utf-8");try{return JSON.parse(r)}catch(e){throw new Error(`Invalid config file (must be JSON): ${e.message}`)}}(e.config):{},r={specPath:e.spec??t.specPath??"",serverUrl:e.server??t.serverUrl,outputDir:e.output??t.outputDir??f,formats:w(e.format??t.formats),filter:$(e,t),concurrency:e.concurrency??t.concurrency??h,timeout:e.timeout??t.timeout??v,proxy:e.proxy??t.proxy,tls:{caCert:e.ca??t.tls?.caCert,clientCert:e.cert??t.tls?.clientCert,clientKey:e.key??t.tls?.clientKey,strictSSL:void 0!==e.insecure?!e.insecure:t.tls?.strictSSL},headers:k(e.header,t.headers),auth:S(e,t),variables:P(e.variable,t.variables),failOnError:e.failOnError??t.failOnError??g,envFile:e.env??t.envFile,grpcReflection:e.grpcReflection??t.grpcReflection??b,grpcProtoPaths:e.grpcProto??t.grpcProtoPaths,mcpTransport:e.mcpTransport??t.mcpTransport??y,mcpCommand:e.mcpCommand??t.mcpCommand,mcpArgs:e.mcpArgs?e.mcpArgs.split(/[\s,]+/).filter(Boolean):t.mcpArgs,mcpCwd:e.mcpCwd??t.mcpCwd};if(r.envFile&&function(e){const t=p.default.resolve(e);if(!c.default.existsSync(t))return;const r=c.default.readFileSync(t,"utf-8").split("\n");for(const e of r){const t=e.trim();if(!t||t.startsWith("#"))continue;const r=t.indexOf("=");if(r>0){const e=t.slice(0,r).trim(),o=t.slice(r+1).trim().replace(/^["']|["']$/g,"");e in process.env||(process.env[e]=o)}}}(r.envFile),function(e){const t=e=>"string"!=typeof e?e:e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(e.serverUrl=t(e.serverUrl),e.proxy=t(e.proxy),e.headers)for(const[r,o]of Object.entries(e.headers))e.headers[r]=t(o);e.auth?.token&&(e.auth.token=t(e.auth.token));if(e.variables)for(const[r,o]of Object.entries(e.variables))e.variables[r]=t(o)}(r),!r.specPath)throw new Error("No OpenAPI spec path provided. Use --spec <path> or set specPath in config.");if(r.proxy)try{new URL(r.proxy)}catch{throw new Error(`Invalid proxy URL: ${r.proxy}`)}return r}function w(e){return Array.isArray(e)?e:"string"==typeof e?e.split(",").map(e=>e.trim().toLowerCase()).filter(Boolean):m}function $(e,t){const r=e.method?e.method.split(",").map(e=>e.trim().toLowerCase()):t.filter?.methods,o=e.path?e.path.split(",").map(e=>e.trim()):t.filter?.paths,s=e.tag?e.tag.split(",").map(e=>e.trim()):t.filter?.tags,n=e.operationId?e.operationId.split(",").map(e=>e.trim()):t.filter?.operationIds;if(r||o||s||n)return{methods:r,paths:o,tags:s,operationIds:n}}function k(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf(":");if(e>0){const o=t.slice(0,e).trim(),s=t.slice(e+1).trim();o&&(r[o]=s)}}return Object.keys(r).length?r:void 0}function S(e,t){return e.bearer?{type:"bearer",token:e.bearer}:t.auth}function P(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf("=");e>0&&(r[t.slice(0,e).trim()]=t.slice(e+1).trim())}return Object.keys(r).length?r:void 0}async function C(e){const t=e.specPath.trim();let r;if(/^https?:\/\//i.test(t.trim()))r=await function(e,t){let r=0;return new Promise((o,s)=>{const n=a=>{const i=("https:"===new URL(a).protocol?l.default:d.default).get(a,{timeout:t,headers:{Accept:"application/json, text/plain, */*","User-Agent":"@powerduck/openapi-cli"}},t=>{if(t.statusCode&&t.statusCode>=300&&t.statusCode<400&&t.headers.location){if(++r>5)return t.resume(),void s(new Error(`Too many redirects (>5) while fetching spec: ${e}`));const o=new URL(t.headers.location,a).toString();return t.resume(),void n(o)}if(200!==t.statusCode)return t.resume(),void s(new Error(`Failed to fetch spec from ${a}: HTTP ${t.statusCode} ${t.statusMessage??""}`.trim()));const i=[];t.on("data",e=>i.push(e)),t.on("end",()=>o(Buffer.concat(i).toString("utf-8"))),t.on("error",e=>s(new Error(`Error reading spec response: ${e.message}`)))});i.on("timeout",()=>{i.destroy(new Error(`Timed out after ${t}ms while fetching spec: ${e}`))}),i.on("error",t=>s(new Error(`Network error fetching spec ${e}: ${t.message}`)))};n(e)})}(t,e.timeout??v);else{const e=p.default.resolve(t);if(!c.default.existsSync(e))throw new Error(`OpenAPI spec not found: ${e}`);if(!c.default.statSync(e).isFile())throw new Error(`OpenAPI spec path is not a file: ${e}`);r=c.default.readFileSync(e,"utf-8")}const o=function(e,t){let r;try{r=JSON.parse(e)}catch(e){throw new Error(`Spec from ${t} is not valid JSON: ${e.message}`)}if(!r||"object"!=typeof r||Array.isArray(r))throw new Error(`Spec from ${t} is not a JSON object`);if(!r.paths||"object"!=typeof r.paths)throw new Error(`Spec from ${t} does not contain a valid 'paths' object`);return r}(r,t);return async function(e){try{const t=await(0,u.dereference)(e);return t.errors&&t.errors.length>0?e:t.schema??t.specification??e}catch(t){return e}}(o)}var O=require("@powerduck/request"),E=a(require("fs"),1);function T(e,t){return e.map(e=>{try{const r=function(e,t){switch(e.assert){case"status":if(void 0===e.value)throw new Error("status assertion requires 'value'");return t.status===e.value;case"header":{if(!e.key)throw new Error("header assertion requires 'key'");const r=function(e,t){const r=t.toLowerCase();for(const[t,o]of Object.entries(e))if(t.toLowerCase()===r)return o;return}(t.headers??{},e.key);return void 0!==r&&(void 0===e.contains||r.toLowerCase().includes(e.contains.toLowerCase()))}case"bodyContains":if(void 0===e.contains)throw new Error("bodyContains assertion requires 'contains'");return(t.bodyText??JSON.stringify(t.body??"")).includes(e.contains);case"bodyEquals":if(void 0===e.body)throw new Error("bodyEquals assertion requires 'body'");return(t.bodyText??JSON.stringify(t.body??""))===e.body;case"jsonPath":{if(!e.path)throw new Error("jsonPath assertion requires 'path'");const r=function(e,t){const r=t.replace(/^\$\.?/,"");if(!r)return e;const o=r.split(/\.|\[(\d+)\]/).filter(e=>void 0!==e&&""!==e);let s=e;for(const e of o){if(null==s)return;s=/^\d+$/.test(e)?s[Number(e)]:s[e]}return s}(t.body,e.path);return void 0!==e.exists?e.exists?void 0!==r:void 0===r:void 0!==e.equals?j(r,e.equals):void 0!==r}case"responseTime":if(void 0===e.max)throw new Error("responseTime assertion requires 'max'");return t.durationMs<=e.max;default:throw new Error(`Unknown assertion type: ${e.assert}`)}}(e,t);return{name:e.name,passed:r}}catch(t){return{name:e.name,passed:!1,error:t.message}}})}function j(e,t){if(e===t)return!0;if(typeof e!=typeof t)return!1;if(null===e||null===t)return e===t;if("object"!=typeof e)return e===t;if(Array.isArray(e)!==Array.isArray(t))return!1;if(Array.isArray(e)&&Array.isArray(t))return e.length===t.length&&e.every((e,r)=>j(e,t[r]));const r=Object.keys(e),o=Object.keys(t);return r.length===o.length&&r.every(r=>j(e[r],t[r]))}function A(e){if(!e)return;if("string"==typeof e)return e;if(e instanceof Error)return e.message;const t=e;if("string"==typeof t.message)return t.message;if("string"==typeof t.msg)return t.msg;try{return JSON.stringify(e)}catch{return String(e)}}async function R(e){const t=await C(e),r=(0,O.createClient)(),o=function(e,t){const r=[],o=["get","post","put","delete","patch","head","options"];for(const[s,n]of Object.entries(e.paths??{})){const e=n;for(const n of o){const o=e[n];if(!o||"object"!=typeof o)continue;const a={path:s,method:n,operation:o,pathItem:e,operationId:o.operationId??`${n.toUpperCase()} ${s}`,protocol:I(o,e)};L(a,t.filter)&&r.push(a)}}return r}(t,e),s=Date.now(),n=[],a=[...o],i=Math.min(e.concurrency??5,a.length),c=[];for(let o=0;o<i;o++)c.push((async()=>{for(;a.length>0;){const o=a.shift();if(!o)break;const s=await M(r,o,e,t);n.push(s)}})());await Promise.all(c);const p=Date.now()-s,d=function(e,t){const r=e.length,o=e.filter(e=>"passed"===e.status).length,s=e.filter(e=>"failed"===e.status).length,n=e.filter(e=>"error"===e.status).length,a=e.filter(e=>"skipped"===e.status).length;return{total:r,passed:o,failed:s,errors:n,skipped:a,durationMs:t,passRate:r>0?Math.round(o/r*1e3)/10:0}}(n,p);return{summary:d,results:n.sort((e,t)=>e.path.localeCompare(t.path)||e.method.localeCompare(t.method)),config:e,generatedAt:(new Date).toISOString(),version:"0.1.0"}}function I(e,t){const r=e["x-protocol"]??t?.["x-protocol"];if(r)return String(r).toLowerCase();const o=e.responses??{};for(const e of Object.values(o)){if((e?.content??{})["text/event-stream"])return"sse"}return e["x-response-stream"]?"sse":"http"}function L(e,t){if(!t)return!0;if(t.methods?.length&&!t.methods.includes(e.method.toLowerCase()))return!1;if(t.operationIds?.length&&!t.operationIds.includes(e.operationId))return!1;if(t.tags?.length){const r=e.operation.tags??[];if(!t.tags.some(e=>r.includes(e)))return!1}if(t.paths?.length){if(!t.paths.some(t=>{try{return new RegExp(t).test(e.path)}catch{return e.path.includes(t)}}))return!1}return!0}async function M(e,t,r,o){const s=Date.now(),n={operationId:t.operationId,path:t.path,method:t.method.toUpperCase(),protocol:t.protocol};try{const a=function(e,t){const r=e?.["x-tests"],o=t?.["x-tests"];return[...Array.isArray(o)?o:[],...Array.isArray(r)?r:[]].filter(e=>e&&"object"==typeof e&&e.assert&&e.name)}(t.operation,t.pathItem),i=function(e){const t=e?.["x-postman-scripts"];if(t&&"object"==typeof t&&"string"==typeof t.test)return t.test}(t.operation),c=[];let p;switch(t.protocol){case"grpc":p=await async function(e,t,r){const o=t.operation["x-grpc"]??{},s=o.address??r.serverUrl?.replace(/^https?:\/\//,"");if(!s)throw new Error("gRPC operation requires x-grpc.address or --server");const n=e.connect({kind:"grpc",address:s,service:o.service,method:o.method??t.operationId,reflection:r.grpcReflection,protoPaths:r.grpcProtoPaths,metadata:r.headers});await n.open();const a=n.kind,i=function(e){const t=e["x-grpc"]??{};if(t.sample&&"object"==typeof t.sample)return t.sample;const r=e.requestBody?.content?.["application/json"]?.schema;if(r?.properties){const e={};for(const[t,o]of Object.entries(r.properties))e[t]=o.default??q(o.type);return e}return{}}(t.operation);"unary"===a||"server_streaming"===a?await n.send(i):(await n.send(i),await n.close());const c=n.events??[],p=c.find(e=>"status"===e.kind),d=c.filter(e=>"data"===e.kind&&"inbound"===e.direction);"unary"!==a&&"server_streaming"!==a||await n.close();return{status:p?.meta?.code??0,statusText:p?.meta?.statusName,body:1===d.length?d[0].data:d.map(e=>e.data),events:c.map(e=>({kind:e.kind,direction:e.direction,data:e.data,meta:e.meta}))}}(e,t,r);break;case"websocket":p=await async function(e,t,r){const o=t.operation["x-ws"]??{},s=o.url??r.serverUrl?.replace(/^http/,"ws");if(!s)throw new Error("WebSocket operation requires x-ws.url or --server");const n=e.connect({kind:"websocket",url:s,subprotocols:o.subprotocols,headers:r.headers});await n.open();const a=o.send??[];for(const e of a.slice(0,3))await n.send(e);await new Promise(e=>setTimeout(e,500)),await n.close();const i=n.events??[];return{status:101,statusText:"Switching Protocols",body:i.filter(e=>"in"===e.direction&&"open"!==e.kind).map(e=>e.data),events:i.map(e=>({kind:e.kind,direction:e.direction,data:e.data})),streaming:!0}}(e,t,r);break;case"mcp":p=await async function(e,t,r){const o=t.operation["x-mcp"]??{},s=o.endpoint??r.serverUrl,n=o.method??"tools/call",a=o.name,i=o.arguments??{};if("stdio"===r.mcpTransport){if(!r.mcpCommand)throw new Error("MCP stdio requires --mcp-command");const t=e.connect({kind:"mcp",transport:"stdio",command:r.mcpCommand,args:r.mcpArgs,cwd:r.mcpCwd,timeoutMs:r.timeout});await t.open();const o=await t.request(n,{name:a,arguments:i});return await t.close(),{status:200,statusText:"OK",body:o}}const c={openapi:"3.2.0",info:{title:"mcp",version:"1.0.0"},paths:{"/mcp":{post:{operationId:t.operationId,"x-protocol":"mcp","x-mcp":{endpoint:s,method:n,name:a,arguments:i},responses:{200:{description:"ok"}}}}}},p=await e.send({spec:c,target:{path:"/mcp",method:"post"},timeout:r.timeout,mcp:r.headers?{headers:r.headers}:void 0});return{status:p.response?.status,statusText:p.response?.statusText,body:p.response?.body}}(e,t,r);break;default:p=await async function(e,t,r,o,s,n){const a={};if(r.proxy)try{const e=new URL(r.proxy);a.proxies=[{match:"*://*/*",host:e.hostname,port:e.port?Number(e.port):"https:"===e.protocol?443:80,tunnel:!0}]}catch{throw new Error(`Invalid proxy URL: ${r.proxy}`)}const i=r.tls;if(i){const e={...a.requester??{}};!1===i.strictSSL&&(e.strictSSL=!1),i.caCert&&(e.ca=E.default.readFileSync(i.caCert)),i.clientCert&&(e.cert=E.default.readFileSync(i.clientCert)),i.clientKey&&(e.key=E.default.readFileSync(i.clientKey)),Object.keys(e).length&&(a.requester=e)}const c=await e.send({spec:o,target:{path:t.path,method:t.method},serverUrl:r.serverUrl,values:r.headers?{header:r.headers}:void 0,auth:r.auth,variables:r.variables,timeout:r.timeout,runner:Object.keys(a).length?a:void 0,scripts:s?{test:{exec:s},fromSpecExtensions:!1}:void 0,onAssertion:e=>{n.push({name:e.name??"assertion",passed:e.passed,error:A(e.error)})},maxStreamMs:r.timeout,maxEvents:50});return{status:c.response?.status,statusText:c.response?.statusText,contentType:c.response?.contentType,sizeBytes:c.response?.sizeBytes,body:c.response?.body,text:c.response?.text,headers:c.response?.headers,events:c.response?.events,streaming:c.response?.streaming}}(e,t,r,o,i,c)}const d=Date.now()-s,l=a.length>0||c.length>0?[]:function(e,t){const r=[];if("http"===e||"sse"===e){const e=t?.status,o="number"==typeof e&&e>=200&&e<300;r.push({name:"HTTP status is 2xx (implicit)",passed:o,error:o?void 0:`expected 2xx, got ${e??"no status"}`})}else if("graphql"===e){const e=t?.status,o="number"==typeof e&&e>=200&&e<300,s=t?.body,n=Array.isArray(s?.errors)&&s.errors.length>0,a=o&&!n;let i;o?n&&(i=`GraphQL response contains ${s.errors.length} error(s)`):i=`expected HTTP 2xx, got ${e??"no status"}`,r.push({name:"GraphQL success (implicit)",passed:a,error:i})}else if("grpc"===e){const e=t?.statusCode??t?.code,o=t?.statusName??t?.status,s=0===e;r.push({name:"gRPC status is OK (implicit)",passed:s,error:s?void 0:`expected code 0 (OK), got ${e??"unknown"}${o?` (${o})`:""}`})}else if("mcp"===e){const e=t?.body??{},o=void 0!==e.result,s=void 0!==e.error||void 0!==t?.error,n=o&&!s;let a;s?a="MCP JSON-RPC response contains an error":o||(a="MCP JSON-RPC response missing 'result'"),r.push({name:"MCP JSON-RPC success (implicit)",passed:n,error:a})}else if("websocket"===e){const e=t?.events??[],o=e.some(e=>"open"===e.kind||"open"===e.state),s=e.some(e=>"error"===e.kind||"error"===e.state),n=o&&!s;r.push({name:"WebSocket connected (implicit)",passed:n,error:n?void 0:s?"WebSocket connection errored":"WebSocket did not reach open state"})}return r}(t.protocol,p),u=a.length?T(a,{status:p?.status,headers:p?p.headers:void 0,body:p?.body,bodyText:p?p.text:void 0,durationMs:d}):[],f=new Set,m=c.filter(e=>{const t=`${e.name}|${e.passed}|${e.error??""}`;return!f.has(t)&&(f.add(t),!0)}),h=[...u,...m,...l],v=h.some(e=>!e.passed);return{...n,status:v?"failed":"passed",durationMs:d,response:p,assertions:h.length?h:void 0,timestamp:(new Date).toISOString()}}catch(e){const t=Date.now()-s;return{...n,status:"error",durationMs:t,error:e.message??String(e),timestamp:(new Date).toISOString()}}}function q(e){switch(e){case"string":default:return"";case"number":case"integer":return 0;case"boolean":return!1;case"array":return[];case"object":return{}}}var F=a(require("fs"),1),z=a(require("path"),1);var N="",U="",D="",J="",B="",H="",K="",W="",G="",_="",Q="";function V(e){if(null==e||!Number.isFinite(e))return`${D}--ms${N}`;return`${e>500?`${U}${H}`:U}${e}${N}${D}ms${N}`}function Y(e){const t="passed"===e.status?`${J}✓${N}`:"failed"===e.status?`${B}✗${N}`:"error"===e.status?`${H}!${N}`:`${W}-${N}`,r="passed"===e.status?`${G}${U} PASS ${N}`:"failed"===e.status?`${_}${U} FAIL ${N}`:"error"===e.status?`${Q}${U} ERROR ${N}`:`${W} SKIP ${N}`,o=e.protocol?` ${K}[${e.protocol}]${N}`:"",s=null!=e.response?.status?` ${D}${e.response.status}${N}`:"";let n=` ${t} ${r} ${U}${e.method}${N} ${e.path}${o}${s} ${V(e.durationMs)}`;if(e.error&&(n+=`\n ${B}${e.error}${N}`),e.assertions?.length)for(const t of e.assertions){n+=`\n ${t.passed?`${J} ✓${N}`:`${B} ✗${N}`} ${t.name}${t.error?` — ${B}${t.error}${N}`:""}`}return n}function X(e){const t=function(e){const t=[],{summary:r,results:o}=e;t.push(""),t.push(`${U}${K}╔══════════════════════════════════════════════════════╗${N}`),t.push(`${U}${K}║ Powerduck Test Report ║${N}`),t.push(`${U}${K}╚══════════════════════════════════════════════════════╝${N}`),t.push("");const s=r.passRate>=80?J:r.passRate>=50?H:B;t.push(`${U} Total:${N} ${r.total} ${J}Passed:${N} ${r.passed} ${B}Failed:${N} ${r.failed} ${H}Errors:${N} ${r.errors} ${s}Rate:${N} ${r.passRate}% ${D}Time:${N} ${V(r.durationMs)}`),t.push("");const n=r.total>0?Math.round(r.passed/r.total*50):0,a=r.total>0?Math.round(r.failed/r.total*50):0,i=r.total>0?Math.round(r.errors/r.total*50):0,c=J+"█".repeat(n)+B+"█".repeat(a)+H+"█".repeat(i)+W+"░".repeat(Math.max(0,50-n-a-i))+N;t.push(` [${c}]`),t.push("");for(const e of o)t.push(Y(e));return t.push(""),r.failed>0||r.errors>0?t.push(`${U}${B} ✗ ${r.failed+r.errors} test(s) did not pass${N}`):t.push(`${U}${J} ✓ All ${r.total} test(s) passed${N}`),t.push(` ${D}Generated: ${e.generatedAt}${N}`),t.push(""),t.join("\n")}(e);return console.log(t),t}var Z=a(require("fs"),1),ee=a(require("path"),1);function te(e,t){const r=ee.default.resolve(t);Z.default.mkdirSync(r,{recursive:!0});const o=ee.default.join(r,"report.html");return Z.default.writeFileSync(o,function(e){const{summary:t,results:r}=e,o=JSON.stringify({summary:t,results:r});return`<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>Test Report — Powerduck</title>\n<style>\n *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n :root{\n --bg:#ffffff;--fg:#0f172a;--muted:#64748b;--border:#e2e8f0;\n --bg-subtle:#f8fafc;--bg-hover:#f1f5f9;\n --green:#16a34a;--green-bg:#f0fdf4;--green-border:#bbf7d0;\n --red:#dc2626;--red-bg:#fef2f2;--red-border:#fecaca;\n --amber:#d97706;--amber-bg:#fffbeb;--amber-border:#fde68a;\n --blue:#2563eb;--mono:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;\n --sans:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,Inter,sans-serif;\n }\n @media (prefers-color-scheme:dark){\n :root{\n --bg:#0f172a;--fg:#f1f5f9;--muted:#94a3b8;--border:#1e293b;\n --bg-subtle:#1e293b;--bg-hover:#334155;\n --green:#4ade80;--green-bg:rgba(74,222,128,.1);--green-border:rgba(74,222,128,.25);\n --red:#f87171;--red-bg:rgba(248,113,113,.1);--red-border:rgba(248,113,113,.25);\n --amber:#fbbf24;--amber-bg:rgba(251,191,36,.1);--amber-border:rgba(251,191,36,.25);\n --blue:#60a5fa;\n }\n }\n body{font-family:var(--sans);background:var(--bg);color:var(--fg);line-height:1.5;font-size:14px;-webkit-font-smoothing:antialiased}\n .wrap{max-width:960px;margin:0 auto;padding:32px 24px 64px}\n\n /* Header */\n .hdr{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--border)}\n .hdr h1{font-size:18px;font-weight:600;letter-spacing:-.01em}\n .hdr .meta{font-size:12px;color:var(--muted);font-family:var(--mono)}\n\n /* Summary strip */\n .strip{display:grid;grid-template-columns:repeat(6,1fr);gap:0;border:1px solid var(--border);border-radius:8px;overflow:hidden;margin-bottom:24px}\n .stat{padding:14px 16px;border-right:1px solid var(--border);text-align:center}\n .stat:last-child{border-right:none}\n .stat .v{font-size:22px;font-weight:700;letter-spacing:-.02em;line-height:1.2}\n .stat .l{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-top:2px}\n .stat.total .v{color:var(--fg)}\n .stat.pass .v{color:var(--green)}\n .stat.fail .v{color:var(--red)}\n .stat.err .v{color:var(--amber)}\n .stat.rate .v{color:var(--blue)}\n .stat.time .v{font-size:16px;font-weight:600}\n\n /* Progress */\n .bar{height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:24px;display:flex}\n .bar>div{height:100%}\n .bar .p{background:var(--green)}\n .bar .f{background:var(--red)}\n .bar .e{background:var(--amber)}\n\n /* Filters */\n .filters{display:flex;gap:4px;margin-bottom:16px}\n .fbtn{padding:4px 12px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--muted);cursor:pointer;font-size:12px;font-weight:500;transition:all .12s}\n .fbtn:hover{color:var(--fg);background:var(--bg-hover)}\n .fbtn.active{background:var(--bg-subtle);color:var(--fg);border-color:var(--border)}\n .fbtn .n{color:var(--muted);margin-left:4px;font-variant-numeric:tabular-nums}\n\n /* Test rows */\n .row{border:1px solid var(--border);border-radius:8px;margin-bottom:6px;overflow:hidden;transition:border-color .12s}\n .row:hover{border-color:var(--muted)}\n .row-head{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;user-select:none}\n .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n .dot.pass{background:var(--green)}\n .dot.fail{background:var(--red)}\n .dot.err{background:var(--amber)}\n .method{font-family:var(--mono);font-size:11px;font-weight:600;color:var(--muted);min-width:40px;text-transform:uppercase}\n .path{font-family:var(--mono);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .proto{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);padding:2px 8px;border:1px solid var(--border);border-radius:4px}\n .dur{font-family:var(--mono);font-size:12px;color:var(--muted);min-width:56px;text-align:right;font-variant-numeric:tabular-nums}\n .chev{color:var(--muted);font-size:10px;transition:transform .15s;flex-shrink:0}\n .row.open .chev{transform:rotate(90deg)}\n\n /* Row body */\n .row-body{display:none;padding:0 14px 12px 32px;border-top:1px solid var(--border)}\n .row.open .row-body{display:block}\n .sect{margin-top:10px}\n .sect-t{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600;margin-bottom:6px}\n .asrt{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:13px}\n .asrt .ck{width:14px;height:14px;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;flex-shrink:0;color:#fff}\n .asrt.pass .ck{background:var(--green)}\n .asrt.fail .ck{background:var(--red)}\n .asrt .an{color:var(--fg)}\n .asrt .ae{color:var(--red);font-size:12px;margin-left:6px;font-family:var(--mono)}\n .rmeta{display:flex;gap:20px;flex-wrap:wrap;font-size:12px}\n .rmeta span{color:var(--muted)}\n .rmeta strong{color:var(--fg);font-weight:600;font-family:var(--mono)}\n .errbox{background:var(--red-bg);border:1px solid var(--red-border);border-radius:6px;padding:8px 12px;color:var(--red);font-size:12px;font-family:var(--mono);margin-top:10px}\n .bprev{background:var(--bg-subtle);border:1px solid var(--border);border-radius:6px;padding:10px 12px;font-family:var(--mono);font-size:11px;color:var(--muted);max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-all;margin-top:6px}\n\n footer{margin-top:32px;padding-top:16px;border-top:1px solid var(--border);font-size:11px;color:var(--muted);display:flex;justify-content:space-between}\n\n @media(max-width:640px){\n .strip{grid-template-columns:repeat(3,1fr)}\n .stat:nth-child(3){border-right:none}\n .stat:nth-child(4),.stat:nth-child(5){border-top:1px solid var(--border)}\n .path{font-size:11px}\n }\n</style>\n</head>\n<body>\n<div class="wrap">\n <div class="hdr">\n <h1>Test Report</h1>\n <span class="meta">${e.generatedAt} · v${e.version}</span>\n </div>\n\n <div class="strip">\n <div class="stat total"><div class="v">${t.total}</div><div class="l">Total</div></div>\n <div class="stat pass"><div class="v">${t.passed}</div><div class="l">Passed</div></div>\n <div class="stat fail"><div class="v">${t.failed}</div><div class="l">Failed</div></div>\n <div class="stat err"><div class="v">${t.errors}</div><div class="l">Errors</div></div>\n <div class="stat rate"><div class="v">${t.passRate}%</div><div class="l">Pass Rate</div></div>\n <div class="stat time"><div class="v">${(t.durationMs/1e3).toFixed(2)}s</div><div class="l">Duration</div></div>\n </div>\n\n <div class="bar">\n <div class="p" style="width:${t.total?t.passed/t.total*100:0}%"></div>\n <div class="f" style="width:${t.total?t.failed/t.total*100:0}%"></div>\n <div class="e" style="width:${t.total?t.errors/t.total*100:0}%"></div>\n </div>\n\n <div class="filters">\n <button class="fbtn active" data-f="all">All<span class="n">${t.total}</span></button>\n <button class="fbtn" data-f="passed">Passed<span class="n">${t.passed}</span></button>\n <button class="fbtn" data-f="failed">Failed<span class="n">${t.failed}</span></button>\n <button class="fbtn" data-f="error">Errors<span class="n">${t.errors}</span></button>\n </div>\n\n <div id="list"></div>\n\n <footer>\n <span>@powerduck/openapi-cli v${e.version}</span>\n <span>${t.passed}/${t.total} passed</span>\n </footer>\n</div>\n\n<script>\nconst D=${o};\nfunction esc(s){const d=document.createElement('div');d.textContent=String(s??'');return d.innerHTML}\nfunction render(f){\n const list=document.getElementById('list');\n const items=f==='all'?D.results:D.results.filter(r=>r.status===f);\n list.innerHTML=items.map(r=>{\n const as=r.assertions?r.assertions.map(a=>\`\n <div class="asrt \${a.passed?'pass':'fail'}">\n <div class="ck">\${a.passed?'✓':'✗'}</div>\n <span class="an">\${esc(a.name)}</span>\n \${a.error?'<span class="ae">'+esc(a.error)+'</span>':''}\n </div>\`).join(''):'';\n const bp=r.response?.body?'<div class="bprev">'+esc(JSON.stringify(r.response.body,null,2).slice(0,3000))+'</div>':'';\n const eb=r.error?'<div class="errbox">'+esc(r.error)+'</div>':'';\n const rm=r.response?\`\n <div class="rmeta">\n \${r.response.status!=null?'<span>Status <strong>'+r.response.status+(r.response.statusText?' '+r.response.statusText:'')+'</strong></span>':''}\n \${r.response.contentType?'<span>Type <strong>'+esc(r.response.contentType)+'</strong></span>':''}\n \${r.response.sizeBytes!=null?'<span>Size <strong>'+r.response.sizeBytes+'B</strong></span>':''}\n \${r.response.streaming?'<span>Streaming <strong>yes</strong></span>':''}\n </div>\`:'';\n return \`\n <div class="row \${r.status}" data-s="\${r.status}">\n <div class="row-head" onclick="this.parentElement.classList.toggle('open')">\n <div class="dot \${r.status==='passed'?'pass':r.status==='failed'?'fail':'err'}"></div>\n <span class="method">\${r.method}</span>\n <span class="path">\${esc(r.path)}</span>\n \${r.protocol?'<span class="proto">'+r.protocol+'</span>':''}\n <span class="dur">\${r.durationMs}ms</span>\n <span class="chev">▶</span>\n </div>\n <div class="row-body">\n \${eb}\n \${rm}\n \${as?'<div class="sect"><div class="sect-t">Assertions</div>'+as+'</div>':''}\n \${bp?'<div class="sect"><div class="sect-t">Response</div>'+bp+'</div>':''}\n </div>\n </div>\`;\n }).join('');\n}\ndocument.querySelectorAll('.fbtn').forEach(b=>b.addEventListener('click',()=>{\n document.querySelectorAll('.fbtn').forEach(x=>x.classList.remove('active'));\n b.classList.add('active');render(b.dataset.f);\n}));\nrender('all');\n<\/script>\n</body>\n</html>`}(e),"utf-8"),o}var re=new i.Command;re.name("openapi-cli").description("CI-ready CLI for batch-testing OpenAPI 3.2 documents across HTTP, SSE, WebSocket, GraphQL, gRPC and MCP").version("0.2.0").requiredOption("-s, --spec <path-or-url>","Path or URL (http/https) to OpenAPI 3.2 JSON spec").option("-S, --server <url>","Override server URL from the spec").option("-o, --output <dir>","Output directory for reports","./openapi-cli-report").option("-f, --format <formats>","Comma-separated report formats: json,cli,html","json,cli,html").option("-c, --config <path>","Path to JSON config file (CLI args take precedence)").option("--method <methods>","Filter by HTTP methods (comma-separated)").option("--path <patterns>","Filter by path regex patterns (comma-separated)").option("--tag <tags>","Filter by OpenAPI tags (comma-separated)").option("--operation-id <ids>","Filter by operationId (comma-separated)").option("-n, --concurrency <n>","Max concurrent requests","5").option("-t, --timeout <ms>","Per-request timeout in ms","30000").option("-p, --proxy <url>","HTTP(S) proxy URL").option("--ca <path>","CA certificate bundle (PEM)").option("--cert <path>","Client certificate (PEM) for mTLS").option("--key <path>","Client private key (PEM) for mTLS").option("-k, --insecure","Skip TLS certificate verification").option("-H, --header <headers...>","Extra headers (Key: Value, repeatable)").option("--bearer <token>","Bearer token for Authorization header").option("-v, --variable <vars...>","Postman variables (KEY=VALUE, repeatable)").option("--env <path>","Path to .env file").option("--no-fail-on-error","Exit 0 even if tests fail").option("--grpc-no-reflection","Disable gRPC server reflection (use proto files)").option("--grpc-proto <paths...>","Paths to .proto files or directories").option("--mcp-transport <transport>","MCP transport: streamable-http or stdio","streamable-http").option("--mcp-command <command>","MCP stdio command").option("--mcp-args <args>","MCP stdio arguments (space or comma separated)").option("--mcp-cwd <dir>","MCP stdio working directory"),re.parse(process.argv);var oe=re.opts();(async function(){const e={spec:oe.spec,server:oe.server,output:oe.output,format:oe.format,config:oe.config,method:oe.method,path:oe.path,tag:oe.tag,operationId:oe.operationId,concurrency:Number(oe.concurrency),timeout:Number(oe.timeout),proxy:oe.proxy,ca:oe.ca,cert:oe.cert,key:oe.key,insecure:oe.insecure,header:oe.header,bearer:oe.bearer,variable:oe.variable,env:oe.env,failOnError:!process.argv.includes("--no-fail-on-error")&&void 0,grpcReflection:!oe.grpcNoReflection&&void 0,grpcProto:oe.grpcProto,mcpTransport:oe.mcpTransport,mcpCommand:oe.mcpCommand,mcpArgs:oe.mcpArgs,mcpCwd:oe.mcpCwd};let t,r;try{t=x(e)}catch(e){console.error(`Error: ${e.message}`),process.exit(2)}console.log(`openapi-cli v0.2.0 — testing ${t.specPath}`),console.log(`Server: ${t.serverUrl??"(from spec)"} | Concurrency: ${t.concurrency} | Timeout: ${t.timeout}ms\n`);try{r=await R(t)}catch(e){console.error(`Fatal error: ${e.message}`),process.exit(2)}const o=t.formats??["json","cli","html"];if(o.includes("cli")&&X(r),o.includes("json")){const e=function(e,t){const r=z.default.resolve(t);F.default.mkdirSync(r,{recursive:!0});const o=z.default.join(r,"report.json");return F.default.writeFileSync(o,JSON.stringify(e,null,2),"utf-8"),o}(r,t.outputDir??"./openapi-cli-report");console.log(`JSON report: ${e}`)}if(o.includes("html")){const e=te(r,t.outputDir??"./openapi-cli-report");console.log(`HTML report: ${e}`)}(r.summary.failed>0||r.summary.errors>0)&&!1!==t.failOnError&&process.exit(1)})().catch(e=>{console.error(`Unexpected error: ${e.message}`),console.error(e.stack),process.exit(2)});
package/dist/cli.d.cts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{generateHtmlReport as e,generateJsonReport as o,printCliReport as r,resolveConfig as t,runTests as a}from"./chunk-XORDZRWZ.js";import{Command as i}from"commander";var p=new i;p.name("openapi-cli").description("CI-ready CLI for batch-testing OpenAPI 3.2 documents across HTTP, SSE, WebSocket, GraphQL, gRPC and MCP").version("0.2.0").requiredOption("-s, --spec <path-or-url>","Path or URL (http/https) to OpenAPI 3.2 JSON spec").option("-S, --server <url>","Override server URL from the spec").option("-o, --output <dir>","Output directory for reports","./openapi-cli-report").option("-f, --format <formats>","Comma-separated report formats: json,cli,html","json,cli,html").option("-c, --config <path>","Path to JSON config file (CLI args take precedence)").option("--method <methods>","Filter by HTTP methods (comma-separated)").option("--path <patterns>","Filter by path regex patterns (comma-separated)").option("--tag <tags>","Filter by OpenAPI tags (comma-separated)").option("--operation-id <ids>","Filter by operationId (comma-separated)").option("-n, --concurrency <n>","Max concurrent requests","5").option("-t, --timeout <ms>","Per-request timeout in ms","30000").option("-p, --proxy <url>","HTTP(S) proxy URL").option("--ca <path>","CA certificate bundle (PEM)").option("--cert <path>","Client certificate (PEM) for mTLS").option("--key <path>","Client private key (PEM) for mTLS").option("-k, --insecure","Skip TLS certificate verification").option("-H, --header <headers...>","Extra headers (Key: Value, repeatable)").option("--bearer <token>","Bearer token for Authorization header").option("-v, --variable <vars...>","Postman variables (KEY=VALUE, repeatable)").option("--env <path>","Path to .env file").option("--no-fail-on-error","Exit 0 even if tests fail").option("--grpc-no-reflection","Disable gRPC server reflection (use proto files)").option("--grpc-proto <paths...>","Paths to .proto files or directories").option("--mcp-transport <transport>","MCP transport: streamable-http or stdio","streamable-http").option("--mcp-command <command>","MCP stdio command").option("--mcp-args <args>","MCP stdio arguments (space or comma separated)").option("--mcp-cwd <dir>","MCP stdio working directory"),p.parse(process.argv);var n=p.opts();(async function(){const i={spec:n.spec,server:n.server,output:n.output,format:n.format,config:n.config,method:n.method,path:n.path,tag:n.tag,operationId:n.operationId,concurrency:Number(n.concurrency),timeout:Number(n.timeout),proxy:n.proxy,ca:n.ca,cert:n.cert,key:n.key,insecure:n.insecure,header:n.header,bearer:n.bearer,variable:n.variable,env:n.env,failOnError:!process.argv.includes("--no-fail-on-error")&&void 0,grpcReflection:!n.grpcNoReflection&&void 0,grpcProto:n.grpcProto,mcpTransport:n.mcpTransport,mcpCommand:n.mcpCommand,mcpArgs:n.mcpArgs,mcpCwd:n.mcpCwd};let p,c;try{p=t(i)}catch(e){console.error(`Error: ${e.message}`),process.exit(2)}console.log(`openapi-cli v0.2.0 — testing ${p.specPath}`),console.log(`Server: ${p.serverUrl??"(from spec)"} | Concurrency: ${p.concurrency} | Timeout: ${p.timeout}ms\n`);try{c=await a(p)}catch(e){console.error(`Fatal error: ${e.message}`),process.exit(2)}const s=p.formats??["json","cli","html"];if(s.includes("cli")&&r(c),s.includes("json")){const e=o(c,p.outputDir??"./openapi-cli-report");console.log(`JSON report: ${e}`)}if(s.includes("html")){const o=e(c,p.outputDir??"./openapi-cli-report");console.log(`HTML report: ${o}`)}(c.summary.failed>0||c.summary.errors>0)&&!1!==p.failOnError&&process.exit(1)})().catch(e=>{console.error(`Unexpected error: ${e.message}`),console.error(e.stack),process.exit(2)});
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";var e,t=Object.create,r=Object.defineProperty,s=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,o=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,i=(e,t,o,i)=>{if(t&&"object"==typeof t||"function"==typeof t)for(let c of n(t))a.call(e,c)||c===o||r(e,c,{get:()=>t[c],enumerable:!(i=s(t,c))||i.enumerable});return e},c=(e,s,n)=>(n=null!=e?t(o(e)):{},i(!s&&e&&e.__esModule?n:r(n,"default",{value:e,enumerable:!0}),e)),d={};((e,t)=>{for(var s in t)r(e,s,{get:t[s],enumerable:!0})})(d,{DEFAULT_CONFIG:()=>v,extractAssertions:()=>T,extractPostmanScripts:()=>j,generateCliReport:()=>B,generateHtmlReport:()=>W,generateJsonReport:()=>U,isRemoteSpec:()=>S,loadSpec:()=>O,printCliReport:()=>H,resolveConfig:()=>b,resolveJsonPath:()=>P,runDeclarativeAssertions:()=>C,runTests:()=>q}),module.exports=(e=d,i(r({},"__esModule",{value:!0}),e));var p=require("@powerduck/request"),l=c(require("fs"),1),u=c(require("fs"),1),f=c(require("path"),1),m=c(require("http"),1),h=c(require("https"),1),g=require("@powerduck/openapi-parser"),v={outputDir:"./openapi-cli-report",formats:["json","cli","html"],concurrency:5,timeout:3e4,failOnError:!0,grpcReflection:!0,mcpTransport:"streamable-http"};function b(e){const t=e.config?function(e){const t=f.default.resolve(e);if(!u.default.existsSync(t))throw new Error(`Config file not found: ${t}`);const r=u.default.readFileSync(t,"utf-8");try{return JSON.parse(r)}catch(e){throw new Error(`Invalid config file (must be JSON): ${e.message}`)}}(e.config):{},r={specPath:e.spec??t.specPath??"",serverUrl:e.server??t.serverUrl,outputDir:e.output??t.outputDir??v.outputDir,formats:y(e.format??t.formats),filter:x(e,t),concurrency:e.concurrency??t.concurrency??v.concurrency,timeout:e.timeout??t.timeout??v.timeout,proxy:e.proxy??t.proxy,tls:{caCert:e.ca??t.tls?.caCert,clientCert:e.cert??t.tls?.clientCert,clientKey:e.key??t.tls?.clientKey,strictSSL:void 0!==e.insecure?!e.insecure:t.tls?.strictSSL},headers:w(e.header,t.headers),auth:$(e,t),variables:k(e.variable,t.variables),failOnError:e.failOnError??t.failOnError??v.failOnError,envFile:e.env??t.envFile,grpcReflection:e.grpcReflection??t.grpcReflection??v.grpcReflection,grpcProtoPaths:e.grpcProto??t.grpcProtoPaths,mcpTransport:e.mcpTransport??t.mcpTransport??v.mcpTransport,mcpCommand:e.mcpCommand??t.mcpCommand,mcpArgs:e.mcpArgs?e.mcpArgs.split(/[\s,]+/).filter(Boolean):t.mcpArgs,mcpCwd:e.mcpCwd??t.mcpCwd};if(r.envFile&&function(e){const t=f.default.resolve(e);if(!u.default.existsSync(t))return;const r=u.default.readFileSync(t,"utf-8").split("\n");for(const e of r){const t=e.trim();if(!t||t.startsWith("#"))continue;const r=t.indexOf("=");if(r>0){const e=t.slice(0,r).trim(),s=t.slice(r+1).trim().replace(/^["']|["']$/g,"");e in process.env||(process.env[e]=s)}}}(r.envFile),function(e){const t=e=>"string"!=typeof e?e:e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(e.serverUrl=t(e.serverUrl),e.proxy=t(e.proxy),e.headers)for(const[r,s]of Object.entries(e.headers))e.headers[r]=t(s);e.auth?.token&&(e.auth.token=t(e.auth.token));if(e.variables)for(const[r,s]of Object.entries(e.variables))e.variables[r]=t(s)}(r),!r.specPath)throw new Error("No OpenAPI spec path provided. Use --spec <path> or set specPath in config.");if(r.proxy)try{new URL(r.proxy)}catch{throw new Error(`Invalid proxy URL: ${r.proxy}`)}return r}function y(e){return Array.isArray(e)?e:"string"==typeof e?e.split(",").map(e=>e.trim().toLowerCase()).filter(Boolean):v.formats}function x(e,t){const r=e.method?e.method.split(",").map(e=>e.trim().toLowerCase()):t.filter?.methods,s=e.path?e.path.split(",").map(e=>e.trim()):t.filter?.paths,n=e.tag?e.tag.split(",").map(e=>e.trim()):t.filter?.tags,o=e.operationId?e.operationId.split(",").map(e=>e.trim()):t.filter?.operationIds;if(r||s||n||o)return{methods:r,paths:s,tags:n,operationIds:o}}function w(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf(":");if(e>0){const s=t.slice(0,e).trim(),n=t.slice(e+1).trim();s&&(r[s]=n)}}return Object.keys(r).length?r:void 0}function $(e,t){return e.bearer?{type:"bearer",token:e.bearer}:t.auth}function k(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf("=");e>0&&(r[t.slice(0,e).trim()]=t.slice(e+1).trim())}return Object.keys(r).length?r:void 0}function S(e){return/^https?:\/\//i.test(e.trim())}async function O(e){const t=e.specPath.trim();let r;if(S(t))r=await function(e,t){let r=0;return new Promise((s,n)=>{const o=a=>{const i=("https:"===new URL(a).protocol?h.default:m.default).get(a,{timeout:t,headers:{Accept:"application/json, text/plain, */*","User-Agent":"@powerduck/openapi-cli"}},t=>{if(t.statusCode&&t.statusCode>=300&&t.statusCode<400&&t.headers.location){if(++r>5)return t.resume(),void n(new Error(`Too many redirects (>5) while fetching spec: ${e}`));const s=new URL(t.headers.location,a).toString();return t.resume(),void o(s)}if(200!==t.statusCode)return t.resume(),void n(new Error(`Failed to fetch spec from ${a}: HTTP ${t.statusCode} ${t.statusMessage??""}`.trim()));const i=[];t.on("data",e=>i.push(e)),t.on("end",()=>s(Buffer.concat(i).toString("utf-8"))),t.on("error",e=>n(new Error(`Error reading spec response: ${e.message}`)))});i.on("timeout",()=>{i.destroy(new Error(`Timed out after ${t}ms while fetching spec: ${e}`))}),i.on("error",t=>n(new Error(`Network error fetching spec ${e}: ${t.message}`)))};o(e)})}(t,e.timeout??v.timeout);else{const e=f.default.resolve(t);if(!u.default.existsSync(e))throw new Error(`OpenAPI spec not found: ${e}`);if(!u.default.statSync(e).isFile())throw new Error(`OpenAPI spec path is not a file: ${e}`);r=u.default.readFileSync(e,"utf-8")}const s=function(e,t){let r;try{r=JSON.parse(e)}catch(e){throw new Error(`Spec from ${t} is not valid JSON: ${e.message}`)}if(!r||"object"!=typeof r||Array.isArray(r))throw new Error(`Spec from ${t} is not a JSON object`);if(!r.paths||"object"!=typeof r.paths)throw new Error(`Spec from ${t} does not contain a valid 'paths' object`);return r}(r,t);return async function(e){try{const t=await(0,g.dereference)(e);return t.errors&&t.errors.length>0?e:t.schema??t.specification??e}catch(t){return e}}(s)}function C(e,t){return e.map(e=>{try{const r=function(e,t){switch(e.assert){case"status":if(void 0===e.value)throw new Error("status assertion requires 'value'");return t.status===e.value;case"header":{if(!e.key)throw new Error("header assertion requires 'key'");const r=function(e,t){const r=t.toLowerCase();for(const[t,s]of Object.entries(e))if(t.toLowerCase()===r)return s;return}(t.headers??{},e.key);return void 0!==r&&(void 0===e.contains||r.toLowerCase().includes(e.contains.toLowerCase()))}case"bodyContains":if(void 0===e.contains)throw new Error("bodyContains assertion requires 'contains'");return(t.bodyText??JSON.stringify(t.body??"")).includes(e.contains);case"bodyEquals":if(void 0===e.body)throw new Error("bodyEquals assertion requires 'body'");return(t.bodyText??JSON.stringify(t.body??""))===e.body;case"jsonPath":{if(!e.path)throw new Error("jsonPath assertion requires 'path'");const r=P(t.body,e.path);return void 0!==e.exists?e.exists?void 0!==r:void 0===r:void 0!==e.equals?E(r,e.equals):void 0!==r}case"responseTime":if(void 0===e.max)throw new Error("responseTime assertion requires 'max'");return t.durationMs<=e.max;default:throw new Error(`Unknown assertion type: ${e.assert}`)}}(e,t);return{name:e.name,passed:r}}catch(t){return{name:e.name,passed:!1,error:t.message}}})}function P(e,t){const r=t.replace(/^\$\.?/,"");if(!r)return e;const s=r.split(/\.|\[(\d+)\]/).filter(e=>void 0!==e&&""!==e);let n=e;for(const e of s){if(null==n)return;n=/^\d+$/.test(e)?n[Number(e)]:n[e]}return n}function E(e,t){if(e===t)return!0;if(typeof e!=typeof t)return!1;if(null===e||null===t)return e===t;if("object"!=typeof e)return e===t;if(Array.isArray(e)!==Array.isArray(t))return!1;if(Array.isArray(e)&&Array.isArray(t))return e.length===t.length&&e.every((e,r)=>E(e,t[r]));const r=Object.keys(e),s=Object.keys(t);return r.length===s.length&&r.every(r=>E(e[r],t[r]))}function T(e,t){const r=e?.["x-tests"],s=t?.["x-tests"];return[...Array.isArray(s)?s:[],...Array.isArray(r)?r:[]].filter(e=>e&&"object"==typeof e&&e.assert&&e.name)}function j(e){const t=e?.["x-postman-scripts"];if(t&&"object"==typeof t&&"string"==typeof t.test)return t.test}var A="0.1.0";function R(e){if(!e)return;if("string"==typeof e)return e;if(e instanceof Error)return e.message;const t=e;if("string"==typeof t.message)return t.message;if("string"==typeof t.msg)return t.msg;try{return JSON.stringify(e)}catch{return String(e)}}async function q(e){const t=await O(e),r=(0,p.createClient)(),s=function(e,t){const r=[],s=["get","post","put","delete","patch","head","options"];for(const[n,o]of Object.entries(e.paths??{})){const e=o;for(const o of s){const s=e[o];if(!s||"object"!=typeof s)continue;const a={path:n,method:o,operation:s,pathItem:e,operationId:s.operationId??`${o.toUpperCase()} ${n}`,protocol:I(s,e)};M(a,t.filter)&&r.push(a)}}return r}(t,e),n=Date.now(),o=[],a=[...s],i=Math.min(e.concurrency??5,a.length),c=[];for(let s=0;s<i;s++)c.push((async()=>{for(;a.length>0;){const s=a.shift();if(!s)break;const n=await L(r,s,e,t);o.push(n)}})());await Promise.all(c);const d=Date.now()-n,l=function(e,t){const r=e.length,s=e.filter(e=>"passed"===e.status).length,n=e.filter(e=>"failed"===e.status).length,o=e.filter(e=>"error"===e.status).length,a=e.filter(e=>"skipped"===e.status).length;return{total:r,passed:s,failed:n,errors:o,skipped:a,durationMs:t,passRate:r>0?Math.round(s/r*1e3)/10:0}}(o,d);return{summary:l,results:o.sort((e,t)=>e.path.localeCompare(t.path)||e.method.localeCompare(t.method)),config:e,generatedAt:(new Date).toISOString(),version:A}}function I(e,t){const r=e["x-protocol"]??t?.["x-protocol"];if(r)return String(r).toLowerCase();const s=e.responses??{};for(const e of Object.values(s)){if((e?.content??{})["text/event-stream"])return"sse"}return e["x-response-stream"]?"sse":"http"}function M(e,t){if(!t)return!0;if(t.methods?.length&&!t.methods.includes(e.method.toLowerCase()))return!1;if(t.operationIds?.length&&!t.operationIds.includes(e.operationId))return!1;if(t.tags?.length){const r=e.operation.tags??[];if(!t.tags.some(e=>r.includes(e)))return!1}if(t.paths?.length){if(!t.paths.some(t=>{try{return new RegExp(t).test(e.path)}catch{return e.path.includes(t)}}))return!1}return!0}async function L(e,t,r,s){const n=Date.now(),o={operationId:t.operationId,path:t.path,method:t.method.toUpperCase(),protocol:t.protocol};try{const a=T(t.operation,t.pathItem),i=j(t.operation),c=[];let d;switch(t.protocol){case"grpc":d=await async function(e,t,r){const s=t.operation["x-grpc"]??{},n=s.address??r.serverUrl?.replace(/^https?:\/\//,"");if(!n)throw new Error("gRPC operation requires x-grpc.address or --server");const o=e.connect({kind:"grpc",address:n,service:s.service,method:s.method??t.operationId,reflection:r.grpcReflection,protoPaths:r.grpcProtoPaths,metadata:r.headers});await o.open();const a=o.kind,i=function(e){const t=e["x-grpc"]??{};if(t.sample&&"object"==typeof t.sample)return t.sample;const r=e.requestBody?.content?.["application/json"]?.schema;if(r?.properties){const e={};for(const[t,s]of Object.entries(r.properties))e[t]=s.default??z(s.type);return e}return{}}(t.operation);"unary"===a||"server_streaming"===a?await o.send(i):(await o.send(i),await o.close());const c=o.events??[],d=c.find(e=>"status"===e.kind),p=c.filter(e=>"data"===e.kind&&"inbound"===e.direction);"unary"!==a&&"server_streaming"!==a||await o.close();return{status:d?.meta?.code??0,statusText:d?.meta?.statusName,body:1===p.length?p[0].data:p.map(e=>e.data),events:c.map(e=>({kind:e.kind,direction:e.direction,data:e.data,meta:e.meta}))}}(e,t,r);break;case"websocket":d=await async function(e,t,r){const s=t.operation["x-ws"]??{},n=s.url??r.serverUrl?.replace(/^http/,"ws");if(!n)throw new Error("WebSocket operation requires x-ws.url or --server");const o=e.connect({kind:"websocket",url:n,subprotocols:s.subprotocols,headers:r.headers});await o.open();const a=s.send??[];for(const e of a.slice(0,3))await o.send(e);await new Promise(e=>setTimeout(e,500)),await o.close();const i=o.events??[];return{status:101,statusText:"Switching Protocols",body:i.filter(e=>"in"===e.direction&&"open"!==e.kind).map(e=>e.data),events:i.map(e=>({kind:e.kind,direction:e.direction,data:e.data})),streaming:!0}}(e,t,r);break;case"mcp":d=await async function(e,t,r){const s=t.operation["x-mcp"]??{},n=s.endpoint??r.serverUrl,o=s.method??"tools/call",a=s.name,i=s.arguments??{};if("stdio"===r.mcpTransport){if(!r.mcpCommand)throw new Error("MCP stdio requires --mcp-command");const t=e.connect({kind:"mcp",transport:"stdio",command:r.mcpCommand,args:r.mcpArgs,cwd:r.mcpCwd,timeoutMs:r.timeout});await t.open();const s=await t.request(o,{name:a,arguments:i});return await t.close(),{status:200,statusText:"OK",body:s}}const c={openapi:"3.2.0",info:{title:"mcp",version:"1.0.0"},paths:{"/mcp":{post:{operationId:t.operationId,"x-protocol":"mcp","x-mcp":{endpoint:n,method:o,name:a,arguments:i},responses:{200:{description:"ok"}}}}}},d=await e.send({spec:c,target:{path:"/mcp",method:"post"},timeout:r.timeout,mcp:r.headers?{headers:r.headers}:void 0});return{status:d.response?.status,statusText:d.response?.statusText,body:d.response?.body}}(e,t,r);break;default:d=await async function(e,t,r,s,n,o){const a={};if(r.proxy)try{const e=new URL(r.proxy);a.proxies=[{match:"*://*/*",host:e.hostname,port:e.port?Number(e.port):"https:"===e.protocol?443:80,tunnel:!0}]}catch{throw new Error(`Invalid proxy URL: ${r.proxy}`)}const i=r.tls;if(i){const e={...a.requester??{}};!1===i.strictSSL&&(e.strictSSL=!1),i.caCert&&(e.ca=l.default.readFileSync(i.caCert)),i.clientCert&&(e.cert=l.default.readFileSync(i.clientCert)),i.clientKey&&(e.key=l.default.readFileSync(i.clientKey)),Object.keys(e).length&&(a.requester=e)}const c=await e.send({spec:s,target:{path:t.path,method:t.method},serverUrl:r.serverUrl,values:r.headers?{header:r.headers}:void 0,auth:r.auth,variables:r.variables,timeout:r.timeout,runner:Object.keys(a).length?a:void 0,scripts:n?{test:{exec:n},fromSpecExtensions:!1}:void 0,onAssertion:e=>{o.push({name:e.name??"assertion",passed:e.passed,error:R(e.error)})},maxStreamMs:r.timeout,maxEvents:50});return{status:c.response?.status,statusText:c.response?.statusText,contentType:c.response?.contentType,sizeBytes:c.response?.sizeBytes,body:c.response?.body,text:c.response?.text,headers:c.response?.headers,events:c.response?.events,streaming:c.response?.streaming}}(e,t,r,s,i,c)}const p=Date.now()-n,u=a.length>0||c.length>0?[]:function(e,t){const r=[];if("http"===e||"sse"===e){const e=t?.status,s="number"==typeof e&&e>=200&&e<300;r.push({name:"HTTP status is 2xx (implicit)",passed:s,error:s?void 0:`expected 2xx, got ${e??"no status"}`})}else if("graphql"===e){const e=t?.status,s="number"==typeof e&&e>=200&&e<300,n=t?.body,o=Array.isArray(n?.errors)&&n.errors.length>0,a=s&&!o;let i;s?o&&(i=`GraphQL response contains ${n.errors.length} error(s)`):i=`expected HTTP 2xx, got ${e??"no status"}`,r.push({name:"GraphQL success (implicit)",passed:a,error:i})}else if("grpc"===e){const e=t?.statusCode??t?.code,s=t?.statusName??t?.status,n=0===e;r.push({name:"gRPC status is OK (implicit)",passed:n,error:n?void 0:`expected code 0 (OK), got ${e??"unknown"}${s?` (${s})`:""}`})}else if("mcp"===e){const e=t?.body??{},s=void 0!==e.result,n=void 0!==e.error||void 0!==t?.error,o=s&&!n;let a;n?a="MCP JSON-RPC response contains an error":s||(a="MCP JSON-RPC response missing 'result'"),r.push({name:"MCP JSON-RPC success (implicit)",passed:o,error:a})}else if("websocket"===e){const e=t?.events??[],s=e.some(e=>"open"===e.kind||"open"===e.state),n=e.some(e=>"error"===e.kind||"error"===e.state),o=s&&!n;r.push({name:"WebSocket connected (implicit)",passed:o,error:o?void 0:n?"WebSocket connection errored":"WebSocket did not reach open state"})}return r}(t.protocol,d),f=a.length?C(a,{status:d?.status,headers:d?d.headers:void 0,body:d?.body,bodyText:d?d.text:void 0,durationMs:p}):[],m=new Set,h=c.filter(e=>{const t=`${e.name}|${e.passed}|${e.error??""}`;return!m.has(t)&&(m.add(t),!0)}),g=[...f,...h,...u],v=g.some(e=>!e.passed);return{...o,status:v?"failed":"passed",durationMs:p,response:d,assertions:g.length?g:void 0,timestamp:(new Date).toISOString()}}catch(e){const t=Date.now()-n;return{...o,status:"error",durationMs:t,error:e.message??String(e),timestamp:(new Date).toISOString()}}}function z(e){switch(e){case"string":default:return"";case"number":case"integer":return 0;case"boolean":return!1;case"array":return[];case"object":return{}}}var F=c(require("fs"),1),N=c(require("path"),1);function U(e,t){const r=N.default.resolve(t);F.default.mkdirSync(r,{recursive:!0});const s=N.default.join(r,"report.json");return F.default.writeFileSync(s,JSON.stringify(e,null,2),"utf-8"),s}var D={reset:"",bold:"",dim:"",green:"",red:"",yellow:"",blue:"",cyan:"",gray:"",bgGreen:"",bgRed:"",bgYellow:""};function J(e){if(null==e||!Number.isFinite(e))return`${D.dim}--ms${D.reset}`;return`${e>500?`${D.bold}${D.yellow}`:D.bold}${e}${D.reset}${D.dim}ms${D.reset}`}function B(e){const t=[],{summary:r,results:s}=e;t.push(""),t.push(`${D.bold}${D.cyan}╔══════════════════════════════════════════════════════╗${D.reset}`),t.push(`${D.bold}${D.cyan}║ Powerduck Test Report ║${D.reset}`),t.push(`${D.bold}${D.cyan}╚══════════════════════════════════════════════════════╝${D.reset}`),t.push("");const n=r.passRate>=80?D.green:r.passRate>=50?D.yellow:D.red;t.push(`${D.bold} Total:${D.reset} ${r.total} ${D.green}Passed:${D.reset} ${r.passed} ${D.red}Failed:${D.reset} ${r.failed} ${D.yellow}Errors:${D.reset} ${r.errors} ${n}Rate:${D.reset} ${r.passRate}% ${D.dim}Time:${D.reset} ${J(r.durationMs)}`),t.push("");const o=r.total>0?Math.round(r.passed/r.total*50):0,a=r.total>0?Math.round(r.failed/r.total*50):0,i=r.total>0?Math.round(r.errors/r.total*50):0,c=D.green+"█".repeat(o)+D.red+"█".repeat(a)+D.yellow+"█".repeat(i)+D.gray+"░".repeat(Math.max(0,50-o-a-i))+D.reset;t.push(` [${c}]`),t.push("");for(const e of s)t.push(K(e));return t.push(""),r.failed>0||r.errors>0?t.push(`${D.bold}${D.red} ✗ ${r.failed+r.errors} test(s) did not pass${D.reset}`):t.push(`${D.bold}${D.green} ✓ All ${r.total} test(s) passed${D.reset}`),t.push(` ${D.dim}Generated: ${e.generatedAt}${D.reset}`),t.push(""),t.join("\n")}function K(e){const t="passed"===e.status?`${D.green}✓${D.reset}`:"failed"===e.status?`${D.red}✗${D.reset}`:"error"===e.status?`${D.yellow}!${D.reset}`:`${D.gray}-${D.reset}`,r="passed"===e.status?`${D.bgGreen}${D.bold} PASS ${D.reset}`:"failed"===e.status?`${D.bgRed}${D.bold} FAIL ${D.reset}`:"error"===e.status?`${D.bgYellow}${D.bold} ERROR ${D.reset}`:`${D.gray} SKIP ${D.reset}`,s=e.protocol?` ${D.cyan}[${e.protocol}]${D.reset}`:"",n=null!=e.response?.status?` ${D.dim}${e.response.status}${D.reset}`:"";let o=` ${t} ${r} ${D.bold}${e.method}${D.reset} ${e.path}${s}${n} ${J(e.durationMs)}`;if(e.error&&(o+=`\n ${D.red}${e.error}${D.reset}`),e.assertions?.length)for(const t of e.assertions){o+=`\n ${t.passed?`${D.green} ✓${D.reset}`:`${D.red} ✗${D.reset}`} ${t.name}${t.error?` — ${D.red}${t.error}${D.reset}`:""}`}return o}function H(e){const t=B(e);return console.log(t),t}var _=c(require("fs"),1),G=c(require("path"),1);function W(e,t){const r=G.default.resolve(t);_.default.mkdirSync(r,{recursive:!0});const s=G.default.join(r,"report.html");return _.default.writeFileSync(s,function(e){const{summary:t,results:r}=e,s=JSON.stringify({summary:t,results:r});return`<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>Test Report — Powerduck</title>\n<style>\n *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n :root{\n --bg:#ffffff;--fg:#0f172a;--muted:#64748b;--border:#e2e8f0;\n --bg-subtle:#f8fafc;--bg-hover:#f1f5f9;\n --green:#16a34a;--green-bg:#f0fdf4;--green-border:#bbf7d0;\n --red:#dc2626;--red-bg:#fef2f2;--red-border:#fecaca;\n --amber:#d97706;--amber-bg:#fffbeb;--amber-border:#fde68a;\n --blue:#2563eb;--mono:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;\n --sans:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,Inter,sans-serif;\n }\n @media (prefers-color-scheme:dark){\n :root{\n --bg:#0f172a;--fg:#f1f5f9;--muted:#94a3b8;--border:#1e293b;\n --bg-subtle:#1e293b;--bg-hover:#334155;\n --green:#4ade80;--green-bg:rgba(74,222,128,.1);--green-border:rgba(74,222,128,.25);\n --red:#f87171;--red-bg:rgba(248,113,113,.1);--red-border:rgba(248,113,113,.25);\n --amber:#fbbf24;--amber-bg:rgba(251,191,36,.1);--amber-border:rgba(251,191,36,.25);\n --blue:#60a5fa;\n }\n }\n body{font-family:var(--sans);background:var(--bg);color:var(--fg);line-height:1.5;font-size:14px;-webkit-font-smoothing:antialiased}\n .wrap{max-width:960px;margin:0 auto;padding:32px 24px 64px}\n\n /* Header */\n .hdr{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--border)}\n .hdr h1{font-size:18px;font-weight:600;letter-spacing:-.01em}\n .hdr .meta{font-size:12px;color:var(--muted);font-family:var(--mono)}\n\n /* Summary strip */\n .strip{display:grid;grid-template-columns:repeat(6,1fr);gap:0;border:1px solid var(--border);border-radius:8px;overflow:hidden;margin-bottom:24px}\n .stat{padding:14px 16px;border-right:1px solid var(--border);text-align:center}\n .stat:last-child{border-right:none}\n .stat .v{font-size:22px;font-weight:700;letter-spacing:-.02em;line-height:1.2}\n .stat .l{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-top:2px}\n .stat.total .v{color:var(--fg)}\n .stat.pass .v{color:var(--green)}\n .stat.fail .v{color:var(--red)}\n .stat.err .v{color:var(--amber)}\n .stat.rate .v{color:var(--blue)}\n .stat.time .v{font-size:16px;font-weight:600}\n\n /* Progress */\n .bar{height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:24px;display:flex}\n .bar>div{height:100%}\n .bar .p{background:var(--green)}\n .bar .f{background:var(--red)}\n .bar .e{background:var(--amber)}\n\n /* Filters */\n .filters{display:flex;gap:4px;margin-bottom:16px}\n .fbtn{padding:4px 12px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--muted);cursor:pointer;font-size:12px;font-weight:500;transition:all .12s}\n .fbtn:hover{color:var(--fg);background:var(--bg-hover)}\n .fbtn.active{background:var(--bg-subtle);color:var(--fg);border-color:var(--border)}\n .fbtn .n{color:var(--muted);margin-left:4px;font-variant-numeric:tabular-nums}\n\n /* Test rows */\n .row{border:1px solid var(--border);border-radius:8px;margin-bottom:6px;overflow:hidden;transition:border-color .12s}\n .row:hover{border-color:var(--muted)}\n .row-head{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;user-select:none}\n .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n .dot.pass{background:var(--green)}\n .dot.fail{background:var(--red)}\n .dot.err{background:var(--amber)}\n .method{font-family:var(--mono);font-size:11px;font-weight:600;color:var(--muted);min-width:40px;text-transform:uppercase}\n .path{font-family:var(--mono);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .proto{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);padding:2px 8px;border:1px solid var(--border);border-radius:4px}\n .dur{font-family:var(--mono);font-size:12px;color:var(--muted);min-width:56px;text-align:right;font-variant-numeric:tabular-nums}\n .chev{color:var(--muted);font-size:10px;transition:transform .15s;flex-shrink:0}\n .row.open .chev{transform:rotate(90deg)}\n\n /* Row body */\n .row-body{display:none;padding:0 14px 12px 32px;border-top:1px solid var(--border)}\n .row.open .row-body{display:block}\n .sect{margin-top:10px}\n .sect-t{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600;margin-bottom:6px}\n .asrt{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:13px}\n .asrt .ck{width:14px;height:14px;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;flex-shrink:0;color:#fff}\n .asrt.pass .ck{background:var(--green)}\n .asrt.fail .ck{background:var(--red)}\n .asrt .an{color:var(--fg)}\n .asrt .ae{color:var(--red);font-size:12px;margin-left:6px;font-family:var(--mono)}\n .rmeta{display:flex;gap:20px;flex-wrap:wrap;font-size:12px}\n .rmeta span{color:var(--muted)}\n .rmeta strong{color:var(--fg);font-weight:600;font-family:var(--mono)}\n .errbox{background:var(--red-bg);border:1px solid var(--red-border);border-radius:6px;padding:8px 12px;color:var(--red);font-size:12px;font-family:var(--mono);margin-top:10px}\n .bprev{background:var(--bg-subtle);border:1px solid var(--border);border-radius:6px;padding:10px 12px;font-family:var(--mono);font-size:11px;color:var(--muted);max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-all;margin-top:6px}\n\n footer{margin-top:32px;padding-top:16px;border-top:1px solid var(--border);font-size:11px;color:var(--muted);display:flex;justify-content:space-between}\n\n @media(max-width:640px){\n .strip{grid-template-columns:repeat(3,1fr)}\n .stat:nth-child(3){border-right:none}\n .stat:nth-child(4),.stat:nth-child(5){border-top:1px solid var(--border)}\n .path{font-size:11px}\n }\n</style>\n</head>\n<body>\n<div class="wrap">\n <div class="hdr">\n <h1>Test Report</h1>\n <span class="meta">${e.generatedAt} · v${e.version}</span>\n </div>\n\n <div class="strip">\n <div class="stat total"><div class="v">${t.total}</div><div class="l">Total</div></div>\n <div class="stat pass"><div class="v">${t.passed}</div><div class="l">Passed</div></div>\n <div class="stat fail"><div class="v">${t.failed}</div><div class="l">Failed</div></div>\n <div class="stat err"><div class="v">${t.errors}</div><div class="l">Errors</div></div>\n <div class="stat rate"><div class="v">${t.passRate}%</div><div class="l">Pass Rate</div></div>\n <div class="stat time"><div class="v">${(t.durationMs/1e3).toFixed(2)}s</div><div class="l">Duration</div></div>\n </div>\n\n <div class="bar">\n <div class="p" style="width:${t.total?t.passed/t.total*100:0}%"></div>\n <div class="f" style="width:${t.total?t.failed/t.total*100:0}%"></div>\n <div class="e" style="width:${t.total?t.errors/t.total*100:0}%"></div>\n </div>\n\n <div class="filters">\n <button class="fbtn active" data-f="all">All<span class="n">${t.total}</span></button>\n <button class="fbtn" data-f="passed">Passed<span class="n">${t.passed}</span></button>\n <button class="fbtn" data-f="failed">Failed<span class="n">${t.failed}</span></button>\n <button class="fbtn" data-f="error">Errors<span class="n">${t.errors}</span></button>\n </div>\n\n <div id="list"></div>\n\n <footer>\n <span>@powerduck/openapi-cli v${e.version}</span>\n <span>${t.passed}/${t.total} passed</span>\n </footer>\n</div>\n\n<script>\nconst D=${s};\nfunction esc(s){const d=document.createElement('div');d.textContent=String(s??'');return d.innerHTML}\nfunction render(f){\n const list=document.getElementById('list');\n const items=f==='all'?D.results:D.results.filter(r=>r.status===f);\n list.innerHTML=items.map(r=>{\n const as=r.assertions?r.assertions.map(a=>\`\n <div class="asrt \${a.passed?'pass':'fail'}">\n <div class="ck">\${a.passed?'✓':'✗'}</div>\n <span class="an">\${esc(a.name)}</span>\n \${a.error?'<span class="ae">'+esc(a.error)+'</span>':''}\n </div>\`).join(''):'';\n const bp=r.response?.body?'<div class="bprev">'+esc(JSON.stringify(r.response.body,null,2).slice(0,3000))+'</div>':'';\n const eb=r.error?'<div class="errbox">'+esc(r.error)+'</div>':'';\n const rm=r.response?\`\n <div class="rmeta">\n \${r.response.status!=null?'<span>Status <strong>'+r.response.status+(r.response.statusText?' '+r.response.statusText:'')+'</strong></span>':''}\n \${r.response.contentType?'<span>Type <strong>'+esc(r.response.contentType)+'</strong></span>':''}\n \${r.response.sizeBytes!=null?'<span>Size <strong>'+r.response.sizeBytes+'B</strong></span>':''}\n \${r.response.streaming?'<span>Streaming <strong>yes</strong></span>':''}\n </div>\`:'';\n return \`\n <div class="row \${r.status}" data-s="\${r.status}">\n <div class="row-head" onclick="this.parentElement.classList.toggle('open')">\n <div class="dot \${r.status==='passed'?'pass':r.status==='failed'?'fail':'err'}"></div>\n <span class="method">\${r.method}</span>\n <span class="path">\${esc(r.path)}</span>\n \${r.protocol?'<span class="proto">'+r.protocol+'</span>':''}\n <span class="dur">\${r.durationMs}ms</span>\n <span class="chev">▶</span>\n </div>\n <div class="row-body">\n \${eb}\n \${rm}\n \${as?'<div class="sect"><div class="sect-t">Assertions</div>'+as+'</div>':''}\n \${bp?'<div class="sect"><div class="sect-t">Response</div>'+bp+'</div>':''}\n </div>\n </div>\`;\n }).join('');\n}\ndocument.querySelectorAll('.fbtn').forEach(b=>b.addEventListener('click',()=>{\n document.querySelectorAll('.fbtn').forEach(x=>x.classList.remove('active'));\n b.classList.add('active');render(b.dataset.f);\n}));\nrender('all');\n<\/script>\n</body>\n</html>`}(e),"utf-8"),s}
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Public types for @powerduck/openapi-cli.
3
+ */
4
+ type ProtocolName = "http" | "sse" | "websocket" | "graphql" | "grpc" | "mcp";
5
+ type ReportFormat = "json" | "cli" | "html";
6
+ type TestStatus = "passed" | "failed" | "skipped" | "error";
7
+ interface AuthConfig {
8
+ type: "bearer" | "basic" | "apikey" | "none";
9
+ token?: string;
10
+ username?: string;
11
+ password?: string;
12
+ key?: string;
13
+ value?: string;
14
+ in?: "header" | "query";
15
+ }
16
+ interface TlsConfig {
17
+ /** Path to a CA certificate bundle (PEM). */
18
+ caCert?: string;
19
+ /** Path to a client certificate (PEM) for mTLS. */
20
+ clientCert?: string;
21
+ /** Path to a client private key (PEM) for mTLS. */
22
+ clientKey?: string;
23
+ /** Skip TLS certificate verification (insecure). */
24
+ strictSSL?: boolean;
25
+ }
26
+ interface FilterConfig {
27
+ /** Only test these HTTP methods (e.g. ["get","post"]). */
28
+ methods?: string[];
29
+ /** Only test paths matching any of these regex patterns. */
30
+ paths?: string[];
31
+ /** Only test operations with these tags. */
32
+ tags?: string[];
33
+ /** Only test these operationIds. */
34
+ operationIds?: string[];
35
+ }
36
+ interface CliConfig {
37
+ /** Path to the OpenAPI 3.2 JSON file. */
38
+ specPath: string;
39
+ /** Override the server URL from the spec. */
40
+ serverUrl?: string;
41
+ /** Directory to write report files. Default: "./openapi-cli-report". */
42
+ outputDir?: string;
43
+ /** Report formats to generate. Default: ["json","cli","html"]. */
44
+ formats?: ReportFormat[];
45
+ /** Filter which operations to test. */
46
+ filter?: FilterConfig;
47
+ /** Max concurrent requests. Default: 5. */
48
+ concurrency?: number;
49
+ /** Per-request timeout in ms. Default: 30000. */
50
+ timeout?: number;
51
+ /** HTTP(S) proxy URL (e.g. http://proxy:8080). */
52
+ proxy?: string;
53
+ /** TLS / certificate configuration. */
54
+ tls?: TlsConfig;
55
+ /** Extra headers sent with every request. */
56
+ headers?: Record<string, string>;
57
+ /** Authentication configuration. */
58
+ auth?: AuthConfig;
59
+ /** Postman variables ({{name}}) to inject. */
60
+ variables?: Record<string, string>;
61
+ /** Exit with non-zero code if any test fails. Default: true. */
62
+ failOnError?: boolean;
63
+ /** Path to a .env file (KEY=VALUE per line). */
64
+ envFile?: string;
65
+ /** gRPC-specific: use server reflection. Default: true. */
66
+ grpcReflection?: boolean;
67
+ /** gRPC-specific: paths to .proto files or directories. */
68
+ grpcProtoPaths?: string[];
69
+ /** MCP-specific: transport for MCP operations. Default: "streamable-http". */
70
+ mcpTransport?: "streamable-http" | "stdio";
71
+ /** MCP stdio: command to spawn. */
72
+ mcpCommand?: string;
73
+ /** MCP stdio: command arguments. */
74
+ mcpArgs?: string[];
75
+ /** MCP stdio: working directory. */
76
+ mcpCwd?: string;
77
+ }
78
+ interface AssertionResult {
79
+ name: string;
80
+ passed: boolean;
81
+ error?: string;
82
+ }
83
+ interface TestResult {
84
+ operationId: string;
85
+ path: string;
86
+ method: string;
87
+ protocol: ProtocolName | string;
88
+ status: TestStatus;
89
+ durationMs: number;
90
+ response?: {
91
+ status?: number;
92
+ statusText?: string;
93
+ contentType?: string;
94
+ sizeBytes?: number;
95
+ body?: unknown;
96
+ text?: string;
97
+ headers?: Record<string, string>;
98
+ events?: unknown[];
99
+ streaming?: boolean;
100
+ };
101
+ assertions?: AssertionResult[];
102
+ error?: string;
103
+ timestamp: string;
104
+ }
105
+ interface TestSummary {
106
+ total: number;
107
+ passed: number;
108
+ failed: number;
109
+ errors: number;
110
+ skipped: number;
111
+ durationMs: number;
112
+ passRate: number;
113
+ }
114
+ interface TestReport {
115
+ summary: TestSummary;
116
+ results: TestResult[];
117
+ config: CliConfig;
118
+ generatedAt: string;
119
+ version: string;
120
+ }
121
+
122
+ /**
123
+ * Run all tests defined in the spec.
124
+ */
125
+ declare function runTests(config: CliConfig): Promise<TestReport>;
126
+
127
+ declare const DEFAULT_CONFIG: Required<Pick<CliConfig, "outputDir" | "formats" | "concurrency" | "timeout" | "failOnError" | "grpcReflection" | "mcpTransport">>;
128
+ interface CliArgs {
129
+ spec?: string;
130
+ server?: string;
131
+ output?: string;
132
+ format?: string;
133
+ method?: string;
134
+ path?: string;
135
+ tag?: string;
136
+ operationId?: string;
137
+ concurrency?: number;
138
+ timeout?: number;
139
+ proxy?: string;
140
+ ca?: string;
141
+ cert?: string;
142
+ key?: string;
143
+ insecure?: boolean;
144
+ header?: string[];
145
+ bearer?: string;
146
+ variable?: string[];
147
+ config?: string;
148
+ env?: string;
149
+ failOnError?: boolean;
150
+ grpcReflection?: boolean;
151
+ grpcProto?: string[];
152
+ mcpTransport?: string;
153
+ mcpCommand?: string;
154
+ mcpArgs?: string;
155
+ mcpCwd?: string;
156
+ }
157
+ /**
158
+ * Resolve the final CliConfig from CLI args, an optional config file,
159
+ * environment variables, and defaults.
160
+ */
161
+ declare function resolveConfig(args: CliArgs): CliConfig;
162
+ /**
163
+ * Check whether a spec path is a remote URL.
164
+ */
165
+ declare function isRemoteSpec(specPath: string): boolean;
166
+ /**
167
+ * Load and parse the OpenAPI spec from a local file or remote URL.
168
+ */
169
+ declare function loadSpec(config: CliConfig): Promise<any>;
170
+
171
+ /**
172
+ * Assertion engine. Supports two sources:
173
+ * 1. Declarative `x-tests` array on the operation (simple, JSON-serializable).
174
+ * 2. Postman `x-postman-scripts.test` string (full pm.* API, executed by
175
+ * @powerduck/request's script sandbox and surfaced via onAssertion).
176
+ */
177
+
178
+ interface DeclarativeAssertion {
179
+ name: string;
180
+ /** What to assert. */
181
+ assert: "status" | "header" | "bodyContains" | "jsonPath" | "responseTime" | "bodyEquals";
182
+ /** Expected status code (for assert=status). */
183
+ value?: number;
184
+ /** Header name (for assert=header). */
185
+ key?: string;
186
+ /** Substring to check (for assert=header / bodyContains). */
187
+ contains?: string;
188
+ /** JSONPath expression (for assert=jsonPath). Simple dot-notation supported. */
189
+ path?: string;
190
+ /** Expected value at path (for assert=jsonPath). */
191
+ equals?: unknown;
192
+ /** Whether the path must exist (for assert=jsonPath). */
193
+ exists?: boolean;
194
+ /** Max response time in ms (for assert=responseTime). */
195
+ max?: number;
196
+ /** Expected body string (for assert=bodyEquals). */
197
+ body?: string;
198
+ }
199
+ interface AssertionContext {
200
+ status?: number;
201
+ headers?: Record<string, string>;
202
+ body?: unknown;
203
+ bodyText?: string;
204
+ durationMs: number;
205
+ }
206
+ /**
207
+ * Run declarative assertions against a response context.
208
+ */
209
+ declare function runDeclarativeAssertions(assertions: DeclarativeAssertion[], ctx: AssertionContext): AssertionResult[];
210
+ /**
211
+ * Minimal JSONPath resolver supporting dot notation and array indices.
212
+ * e.g. "$.data.items[0].id" or "data.user.name"
213
+ */
214
+ declare function resolveJsonPath(obj: unknown, path: string): unknown;
215
+ /**
216
+ * Extract declarative assertions from an OpenAPI operation.
217
+ * Looks for `x-tests` on the operation or path item.
218
+ */
219
+ declare function extractAssertions(operation: any, pathItem?: any): DeclarativeAssertion[];
220
+ /**
221
+ * Extract Postman test scripts from an operation.
222
+ * Looks for `x-postman-scripts.test` on the operation.
223
+ */
224
+ declare function extractPostmanScripts(operation: any): string | undefined;
225
+
226
+ declare function generateJsonReport(report: TestReport, outputDir: string): string;
227
+
228
+ /**
229
+ * CLI reporter: prints a colored, human-readable summary to stdout.
230
+ * No external dependencies — uses ANSI escape codes directly.
231
+ */
232
+
233
+ declare function generateCliReport(report: TestReport): string;
234
+ /**
235
+ * Print the CLI report to stdout and return it.
236
+ */
237
+ declare function printCliReport(report: TestReport): string;
238
+
239
+ declare function generateHtmlReport(report: TestReport, outputDir: string): string;
240
+
241
+ export { type AssertionResult, type AuthConfig, type CliArgs, type CliConfig, DEFAULT_CONFIG, type DeclarativeAssertion, type FilterConfig, type ProtocolName, type ReportFormat, type TestReport, type TestResult, type TestStatus, type TestSummary, type TlsConfig, extractAssertions, extractPostmanScripts, generateCliReport, generateHtmlReport, generateJsonReport, isRemoteSpec, loadSpec, printCliReport, resolveConfig, resolveJsonPath, runDeclarativeAssertions, runTests };