@seldonqa/cli 0.1.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.
Files changed (2) hide show
  1. package/dist/bin.js +16 -0
  2. package/package.json +33 -0
package/dist/bin.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import{BlokSDK as mt}from"@blok/sdk";function W(n){let e=n.slice(2);for(let t=0;t<e.length;t+=1){let i=e[t];if(i==="--base-url"||i==="--api-key"){t+=1;continue}if(!i.startsWith("-"))return i==="api"}return!1}import{readFileSync as Ge}from"fs";import{createChangedFilesArchive as Qe,createCodebaseArchive as Ve,deltaBaseline as He,generateBaseline as Xe,getActionableReport as oe,getBaselinePlan as re,getCaseStatus as Ze,listBaselines as et,listBaselineVersions as tt,loadSeldonYamlConfig as C,promoteBaseline as nt,readSeldonYamlText as ae,renderReportMarkdown as it,seldonYamlPath as st,renderReportText as ot,renderTable as le,renderUpdateReportMarkdown as rt,renderUpdateReportText as at,runBaseline as lt,updateBaseline as dt,waitForBaselineReady as ct,waitForBaselineReport as pt}from"@blok/sdk";import{Command as ut}from"commander";var fe="\x1B[0m",ge={bold:"1",dim:"2",inverse:"7",red:"31",green:"32",yellow:"33",blue:"34",magenta:"35",cyan:"36",gray:"90"};function me(){return process.env.NO_COLOR!==void 0&&process.env.NO_COLOR!==""?!1:process.env.FORCE_COLOR!==void 0&&process.env.FORCE_COLOR!==""?!0:!!process.stdout.isTTY}function A(n,...e){return!me()||e.length===0?n:`${e.map(i=>`\x1B[${ge[i]}m`).join("")}${n}${fe}`}var j=n=>A(n,"bold"),J=n=>A(n,"dim");import{mkdirSync as he,readFileSync as be,writeFileSync as ye}from"fs";import{homedir as we}from"os";import{join as q}from"path";function G(){return process.env.SELDON_CONFIG_DIR??process.env.BLOK_CONFIG_DIR??q(we(),".config","seldon")}function Q(){return q(G(),"config.json")}function x(){try{return JSON.parse(be(Q(),"utf8"))}catch{return{}}}function $(n){let e={...x(),...n};return he(G(),{recursive:!0,mode:448}),ye(Q(),`${JSON.stringify(e,null,2)}
3
+ `,{encoding:"utf8",mode:384}),e}function V(n,e,t){return{baseUrl:n.baseUrl??t.SELDON_BASE_URL??t.BLOK_BASE_URL??e.baseUrl,apiKey:n.apiKey??t.SELDON_API_KEY??t.BLOK_API_KEY??e.apiKey}}import{defaultSeldonYamlConfig as Se,getBaselinePlan as Ie,loadSeldonYamlConfig as Ce}from"@blok/sdk";import{defaultSeldonYamlConfig as ve,loadSeldonYamlConfig as U,saveSeldonYamlConfig as Re}from"@blok/sdk";function S(n,e){if(n.id)return n.id;let t=U(e),i=t?.baseline?.baselineId;if(i)return i;let s=t?.baseline?.experimentId;if(s)return process.stderr.write(`note: seldon.yml uses the legacy 'baseline.experimentId' \u2014 rename it to 'baseline.baselineId'.
4
+ `),s;throw new Error("No baseline found here. Run `seldon baseline generate` first, or pass --id <baselineId>.")}function H(n,e){if(n.id)return n.id;let t=U(e),i=t?.baseline?.lastRunExperimentId;if(i)return i;let s=t?.baseline?.experimentId;if(s)return s;throw new Error("No run found here yet. Run `seldon baseline run` first, or pass --id <experimentId>.")}function I(n,e){let t=U(n)??ve(),i=e(t);return Re(n,i),i}var ke="Other",xe=3,Ae=3,$e=3,_=["none","skip","only","fixed"],Oe={none:" ",skip:"s",only:"o",fixed:"f"};function Ee(){return{get isTTY(){return!!process.stdin.isTTY},get columns(){return process.stdout.columns||80},get rows(){return process.stdout.rows||24},write:n=>process.stdout.write(n),onData:n=>process.stdin.on("data",n),offData:n=>process.stdin.removeListener("data",n),onResize:n=>process.stdout.on("resize",n),offResize:n=>process.stdout.removeListener("resize",n),setRawMode:n=>process.stdin.setRawMode?.(n),resume:()=>{process.stdin.setEncoding("utf8"),process.stdin.resume()},pause:()=>process.stdin.pause()}}function Te(n){let e=new Map;for(let i of n){let s=i.area?.trim()||ke;(e.get(s)??e.set(s,[]).get(s)).push(i)}let t=[];for(let[i,s]of e){t.push({kind:"header",text:`${i} (${s.length} case${s.length===1?"":"s"})`});for(let l of s)t.push({kind:"case",testCase:l})}return t}function Pe(n,e){let t=(Ce(e)??Se()).testCases,i=new Set(n.map(l=>l.id)),s=new Map(n.map(l=>[l.id,"none"]));for(let l of t?.fixed??[])i.has(l)&&s.set(l,"fixed");for(let l of t?.skip??[])i.has(l)&&s.set(l,"skip");for(let l of t?.only??[])i.has(l)&&s.set(l,"only");return s}function De(n,e){return e<=1||n.length<=e?n:`${n.slice(0,Math.max(0,e-1))}\u2026`}var L=class{constructor(e,t,i,s){this.planTitle=e;this.io=s;this.rows=Te(t),this.caseRowIndices=this.rows.reduce((l,f,g)=>f.kind==="case"?[...l,g]:l,[]),this.statuses=Pe(t,i),this.cursor=0}planTitle;io;rows;caseRowIndices;statuses;cursor;linesWrittenLastRender=0;resolveFn;get columns(){return this.io.columns}get availableRows(){return Math.max($e,this.io.rows-xe-Ae)}currentCase(){let e=this.rows[this.caseRowIndices[this.cursor]];if(e?.kind!=="case")throw new Error("cursor is not on a case row");return e.testCase}setStatus(e){let t=this.currentCase().id;this.statuses.set(t,this.statuses.get(t)===e?"none":e)}cycleStatus(){let e=this.currentCase().id,t=this.statuses.get(e)??"none",i=_[(_.indexOf(t)+1)%_.length];this.statuses.set(e,i)}moveCursor(e){let t=this.caseRowIndices.length;this.cursor=((this.cursor+e)%t+t)%t}counts(){let e={none:0,skip:0,only:0,fixed:0};for(let t of this.statuses.values())e[t]+=1;return e}viewportRange(){let e=this.rows.length,t=this.availableRows;if(e<=t)return[0,e];let i=this.caseRowIndices[this.cursor],s=Math.max(0,i-Math.floor(t/2));return s=Math.min(s,e-t),[s,s+t]}render(){let e=this.columns,t=[],i=(g,...m)=>{t.push({text:g,colors:m.length?m:void 0})};i(`Configure test cases \u2014 ${this.planTitle}`,"bold"),i("\u2191/\u2193 move \xB7 s/o/f set \xB7 space cycle \xB7 enter save \xB7 esc/q cancel","dim"),i("");let[s,l]=this.viewportRange();s>0&&i(` \u2191 ${s} more above`,"dim");for(let g=s;g<l;g+=1){let m=this.rows[g];if(m.kind==="header"){i(m.text,"bold");continue}let b=this.caseRowIndices[this.cursor]===g,w=this.statuses.get(m.testCase.id)??"none",v=`${b?"\u276F":" "} [${Oe[w]}] ${m.testCase.id}: ${m.testCase.title}`;b?i(v,"cyan","bold"):w==="skip"?i(v,"yellow"):w==="only"?i(v,"cyan"):w==="fixed"?i(v,"green"):i(v)}l<this.rows.length&&i(` \u2193 ${this.rows.length-l} more below`,"dim");let f=this.counts();i(""),i("Legend: [s]=skip [o]=only [f]=fixed [ ]=normal","dim"),i(`skip: ${f.skip} only: ${f.only} fixed: ${f.fixed}`,"dim"),this.linesWrittenLastRender>0&&this.io.write(`\x1B[${this.linesWrittenLastRender}A`);for(let{text:g,colors:m}of t){let b=De(g,e);this.io.write(`\r\x1B[2K${m?A(b,...m):b}
5
+ `)}this.linesWrittenLastRender=t.length}finish(e){this.io.write(`
6
+ `),this.cleanup(),this.resolveFn?.(e)}onData=e=>{if(e===""||e==="q"||e==="\x1B")return this.finish(!1);if(e==="\r"||e===`
7
+ `)return this.finish(!0);if(e==="\x1B[A"||e==="k")return this.rerenderAfter(()=>this.moveCursor(-1));if(e==="\x1B[B"||e==="j")return this.rerenderAfter(()=>this.moveCursor(1));if(e===" ")return this.rerenderAfter(()=>this.cycleStatus());if(e==="s")return this.rerenderAfter(()=>this.setStatus("skip"));if(e==="o")return this.rerenderAfter(()=>this.setStatus("only"));if(e==="f")return this.rerenderAfter(()=>this.setStatus("fixed"))};rerenderAfter(e){e(),this.render()}onResize=()=>{this.linesWrittenLastRender=0,this.render()};cleanup(){this.io.setRawMode(!1),this.io.pause(),this.io.offData(this.onData),this.io.offResize(this.onResize),this.io.write("\x1B[?25h")}async run(){if(!this.io.isTTY)throw new Error("`seldon baseline configure` needs an interactive terminal (TTY). Run it directly in a shell.");return this.io.write("\x1B[?25l"),this.render(),this.io.setRawMode(!0),this.io.resume(),this.io.onData(this.onData),this.io.onResize(this.onResize),new Promise(e=>{this.resolveFn=e})}result(){let e=[],t=[],i=[];for(let[s,l]of this.statuses)l==="skip"?e.push(s):l==="only"?t.push(s):l==="fixed"&&i.push(s);return{skip:e,only:t,fixed:i}}};async function N(n,e,t,i){let{status:s}=i,l=await Ie(n,t),f=l.cases??[];if(f.length===0){s("This baseline has no test cases yet \u2014 wait for generation to finish, then try again.");return}let g=new L(l.title??"Baseline",f,e,i.io??Ee());if(!await g.run()){s("Aborted \u2014 no changes saved.");return}let{skip:b,only:w,fixed:k}=g.result();I(e,v=>({...v,testCases:{skip:b,only:w,fixed:k}})),s(`Saved to seldon.yml \u2014 skip: ${b.length}, only: ${w.length}, fixed: ${k.length}`)}import{existsSync as je}from"fs";import{buildInitialConfig as Ue,collectCodebaseFiles as _e,formatBytes as Le,saveSeldonYamlConfig as O,seldonYamlPath as X,summarizeCollectedFiles as Ne,summarizeSkipped as Be,uniqueIgnoredDirs as Fe}from"@blok/sdk";import{cancel as B,confirm as Ke,intro as Ye,isCancel as E,outro as Z,text as F}from"@clack/prompts";var K=12,ee=48,Y=8;function te(n){if(n.length<=ee)return n;let e=ee-1,t=Math.ceil(e/2),i=Math.floor(e/2);return`${n.slice(0,t)}\u2026${n.slice(n.length-i)}`}function Me(n,e){let t=[];n["too-large"]&&t.push(`${n["too-large"]} too large`),n.binary&&t.push(`${n.binary} binary`),n.hidden&&t.push(`${n.hidden} hidden`),n["root-file"]&&t.push(`${n["root-file"]} root file(s)`),t.length&&e(`Excluded from upload: ${t.join(", ")}`)}function ne(n,e,t,i){i(""),i(`Files that will be included: ${n.totalFiles} file(s), ${Le(n.totalBytes)}`);for(let s of n.rootEntries.slice(0,K)){let l=te(s.name);i(s.type==="dir"?` ${l}/ (${s.fileCount} file${s.fileCount===1?"":"s"})`:` ${l}`)}if(n.rootEntries.length>K&&i(` \u2026 and ${n.rootEntries.length-K} more`),e.length>0){let s=e.slice(0,Y).map(te).join(", "),l=e.length>Y?` (+${e.length-Y} more)`:"";i(`Automatically ignoring: ${s}${l}`)}Me(t,i)}function ze(n){let e=n.split(",").map(t=>t.trim().replace(/^\/+|\/+$/g,"")).filter(Boolean);return Array.from(new Set(e))}async function We(n,e,t){let{files:i,skipped:s,totalBytes:l}=await _e(n,{ignoreDirs:e});ne(Ne(i,l),Fe(s),Be(s),t)}async function ie(n,e){let{status:t}=e;if(Ye("Welcome to Seldon!"),je(X(n))){let m=await Ke({message:"seldon.yml already exists here. Reset it to defaults?",initialValue:!1});if(E(m)||!m){Z("Keeping the existing seldon.yml. Nothing changed.");return}}let i=await Ue(n),s=i.config;O(n,s),t("Created seldon.yml with smart defaults."),t("Analyzing your repository..."),ne(i.preview,i.ignored,i.skippedByReason,t);let l=await F({message:"Add more folders to ignore? (folder names, comma-separated \u2014 files aren't excluded here)",placeholder:"e.g. vendor, tmp (leave empty to skip)"});if(E(l)){B("Stopped \u2014 seldon.yml has the smart defaults saved above.");return}if(l.trim()){let m=ze(l);s={...s,baseline:{...s.baseline,generate:{...s.baseline?.generate,ignoreDirs:Array.from(new Set([...s.baseline?.generate?.ignoreDirs??[],...m]))}}},O(n,s),t(""),t("Updated \u2014 here's what will be included now:"),await We(n,s.baseline?.generate?.ignoreDirs??[],t)}let f=await F({message:"Default staging URL? (optional)",placeholder:"https://staging.example.com"});if(E(f)){B("Stopped \u2014 seldon.yml has been saved with what you've entered so far.");return}f.trim()&&(s={...s,baseline:{...s.baseline,run:{...s.baseline?.run,url:f.trim()}}});let g=await F({message:"Scope for baseline generation? (optional, e.g. 'checkout flow')",placeholder:"leave empty for full codebase analysis"});if(E(g)){B("Stopped \u2014 seldon.yml has been saved with what you've entered so far."),O(n,s);return}g.trim()&&(s={...s,baseline:{...s.baseline,generate:{...s.baseline?.generate,scope:g.trim()}}}),O(n,s),Z(["Configuration complete!","",`Saved to: ${X(n)}`,"","Next steps:"," 1. Generate your baseline: seldon baseline generate"," 2. Configure test cases: seldon baseline configure"," 3. Run against staging: seldon baseline run"," 4. After code changes: seldon baseline update"].join(`
8
+ `))}import{spinner as Je}from"@clack/prompts";var se=["Still working","Reticulating splines","Reading through the codebase","Thinking it through","Almost there","Hang tight"];function qe(n){let e=Math.floor(n/1e3),t=Math.floor(e/60),i=e%60;return t>0?`${t}m ${i}s`:`${i}s`}function T(n){let e=Je(),t=0;return e.start(n),{onTick:s=>{let l=se[t%se.length];t+=1,e.message(`${n} (${qe(s)}) \u2014 ${l}...`)},stop:s=>e.stop(s)}}async function M(n,e){let t=T(n),i=Date.now(),s=setInterval(()=>t.onTick(Date.now()-i),2e3);try{return await e()}finally{clearInterval(s),t.stop("")}}function P(n,e){return n.json?"json":n.md?"markdown":e?.cli?.reportFormat??"text"}var ft={only:"o",skip:"s",fixed:"f",none:" "};function gt(n,e,t){return ft[Ze(n,e?.testCases,t)]}function de(n){let e={};for(let t of n??[]){let i=t.indexOf("=");i>0&&(e[t.slice(0,i)]=t.slice(i+1))}return e}function ce(n,e=[]){let t=n.out??(o=>process.stdout.write(`${o}
9
+ `)),i=n.status??(o=>process.stderr.write(`${o}
10
+ `)),s=new ut("seldon");s.description(["Seldon \u2014 generate QA baselines from any codebase and run them from your terminal or CI.","","Authenticate once:"," seldon login seldon_<your-key> (or: seldon config set-key <key>)","","Typical flow, from inside a project:"," seldon init # create seldon.yml, preview what will be analyzed"," seldon baseline generate # read this codebase, build a QA baseline"," seldon baseline cases # review the generated test cases"," seldon baseline configure # mark cases as skip / only / fixed"," seldon baseline run --url https://staging.example.com"," seldon baseline report --md # clean, actionable results","","When the code changes, re-evaluate the baseline (fixed cases are always kept):"," seldon baseline update # whole-repo re-eval: kept / re-anchored / nullified / added"," seldon baseline delta # PR-style: stage a DRAFT from your changes + auto-run","","Inspect and manage versions:"," seldon baseline list # every baseline your key can see (all sources)"," seldon baseline versions # this lineage's committed head + draft proposals"," seldon baseline promote <versionId> # make a draft the committed head runs resolve to","","Configuration lives in seldon.yml at the project root (config-as-code, no server copy).","","Everything else is reachable via: seldon api <operation> [--path k=v] [--query k=v] [--data '{json}']"].join(`
11
+ `)).option("--base-url <url>","Seldon API base URL (overrides SELDON_BASE_URL / config)").option("--api-key <key>","API key (overrides SELDON_API_KEY / config)").addHelpText("after",["","Environment variables:"," SELDON_API_KEY Your API key. Set this and you're good to go (BLOK_API_KEY also accepted)."," SELDON_BASE_URL Override the API base URL (defaults to the baked-in production URL).","","Get an API key: Settings \u2192 API keys in the Seldon product."].join(`
12
+ `));let l=()=>{let o=s.opts();return n.makeSdk({baseUrl:o.baseUrl,apiKey:o.apiKey})},f=o=>t(JSON.stringify(o,null,2)),g=(o,r,a)=>{r==="json"?f(o):t(r==="markdown"?it(o,{full:a}):ot(o,{full:a}))},m=(o,r)=>{r==="json"?f(o):t(r==="markdown"?rt(o):at(o))},b=async o=>{let r=o.path??process.cwd(),a=S(o,r),c=C(r),u=o.url||c?.baseline?.run?.url||void 0;i(`Launching QA run for baseline ${a} ...`);let{experimentId:d}=await lt(l(),a,{url:u,credentials:o.credentials});if(I(r,p=>({...p,baseline:{...p.baseline,lastRunExperimentId:d,...o.url?{run:{...p.baseline?.run,url:o.url}}:{}}})),i(`Run started \u2014 experiment ${d}. View it in the app under QA, or: seldon baseline report`),!o.wait)return;let h=T("Running test cases");await pt(l(),d,{onTick:h.onTick}),h.stop(""),i("Test run complete."),g(await oe(l(),d),P(o,c),o.full)},w=o=>o.option("--url <url>","Live product URL to test against (default: seldon.yml baseline.run.url)").option("--path <dir>","Project directory (default: current directory)").option("--id <baselineId>","Run a specific baseline instead of the saved one").option("--credentials <creds>","Inline login, e.g. 'username: a@b.com, password: secret'").option("--no-wait","Submit the run without waiting for results").option("--md","Render the report as Markdown").option("--json","Render the report as JSON").option("--full","Show every failure/blocked case's full detail block (default: capped at 5, ledger always lists every case)"),k=async o=>{let r=o.path??process.cwd(),a=H(o,r),c=C(r);g(await oe(l(),a),P(o,c),o.full)},v=o=>o.option("--path <dir>","Project directory (default: current directory)").option("--id <experimentId>","Report on a specific run instead of the last one").option("--md","Render as Markdown (great for PRs / agents)").option("--json","Render as JSON (stable schema for agents)").option("--full","Show every failure/blocked case's full detail block (default: capped at 5, ledger always lists every case)"),D=s.command("config").description("Manage local CLI configuration.");D.command("show").description("Print the saved config (api key redacted).").action(()=>{let o=x();f({baseUrl:o.baseUrl,apiKey:o.apiKey?"****":void 0})}),D.command("set-key <key>").description("Save an API key to the config file.").action(o=>{$({apiKey:o}),i("Saved API key.")}),D.command("set-url <url>").description("Save the API base URL to the config file.").action(o=>{$({baseUrl:o}),i("Saved base URL.")}),s.command("login <key>").description("Save your API key (alias for `config set-key`).").action(o=>{$({apiKey:o}),i("Saved API key. You're ready to go.")}),s.command("init [path]").description("Create seldon.yml for this project and preview what a baseline would include.").action(async o=>{let r=o??process.cwd();await ie(r,{status:i})});let y=s.command("baseline").description("Generate and inspect a QA baseline for the current codebase.");y.command("list").description("List every baseline your API key can see (across every source), not just the one saved locally.").option("--json","Output as JSON").action(async o=>{let{baselines:r}=await et(l());if(o.json){f({baselines:r});return}if(r.length===0){t("No baselines found for this API key. Generate one with: seldon baseline generate");return}t(j(`${r.length} baseline${r.length===1?"":"s"}`)+`
13
+ `),t(le(r,[{header:"ID",cell:a=>a.id},{header:"NAME",cell:a=>a.displayName},{header:"SOURCE",cell:a=>a.createdVia},{header:"CASES",align:"right",cell:a=>String(a.caseCount)},{header:"STATUS",cell:a=>a.status},{header:"UPDATED",cell:a=>a.createdAt?new Date(a.createdAt).toISOString().slice(0,10):"?"}]))}),y.command("generate [path]").description("Read a local codebase, build a QA baseline (test plan), and tie it to this folder.").option("--scope <text>","Free-text emphasis for the analysis (e.g. 'focus on checkout')").option("--name <name>","Baseline name shown in the app (unique per tenant)").option("--no-wait","Return immediately without waiting for the plan to finish generating").option("--json","Output the baseline handle as JSON").action(async(o,r)=>{let a=o??process.cwd(),u=C(a)?.baseline?.generate;i(`Reading codebase at ${a} ...`);let d=await Xe(l(),a,{scope:r.scope??u?.scope,label:r.name,filename:u?.filename,ignoreDirs:u?.ignoreDirs});I(a,p=>({...p,baseline:{...p.baseline,lineageId:d.lineageId}})),i(`Uploaded ${d.fileCount} file(s); baseline created (lineage ${d.lineageId}).`);let h={lineageId:d.lineageId};if(r.wait){let p=T("Generating test plan (this can take a few minutes)"),R=await ct(l(),d.lineageId,{onTick:p.onTick});p.stop(""),I(a,z=>({...z,baseline:{...z.baseline,baselineId:R.id}})),h={lineageId:d.lineageId,baselineId:R.id,caseCount:R.caseCount},i(`Baseline ready: ${R.caseCount} test case(s). It now appears in the app under QA Baselines.`),i("Run it with: seldon baseline run --url <url>")}r.json?f({...d,...h}):i(`Saved baseline id to ${st(a)}`)}),y.command("show").description("Show the baseline tied to this folder and its status.").option("--path <dir>","Project directory (default: current directory)").option("--id <baselineId>","Inspect a specific baseline instead of the saved one").action(async o=>{let r=o.path??process.cwd(),a=S(o,r),c=await re(l(),a),u=C(r);f({baselineId:c.baseline_id??a,lineageId:c.lineage_id,planTitle:c.title,caseCount:c.case_count??c.cases?.length??0,status:c.status,targetUrl:c.target_url||void 0,lastRunExperimentId:u?.baseline?.lastRunExperimentId||void 0,defaultUrl:u?.baseline?.run?.url||void 0})});let ue=async o=>{let r=o.path??process.cwd(),a=S(o,r),c=await re(l(),a),u=c.cases??[];if(o.json){f(u);return}let d=C(r),h=u.map(p=>p.id);t(`${c.title??"Baseline"} \u2014 ${u.length} case(s)`);for(let p of u)t(` [${gt(p.id,d,h)}] [${p.id}] ${p.title} (${p.priority??"?"} \xB7 ${p.area??"general"})`),p.acceptance_criterion&&t(` ${p.acceptance_criterion}`);t(""),t("Legend: [s]=skip [o]=only [f]=fixed [ ]=run normally \xB7 seldon baseline configure")};if(y.command("cases").description("Browse and mark test cases (interactive in a terminal; use --list/--json for a static dump).").option("--path <dir>","Project directory (default: current directory)").option("--id <baselineId>","Inspect a specific baseline instead of the saved one").option("--json","Output full case objects as JSON (always static, never interactive)").option("--list","Force the static text listing even in an interactive terminal").action(async o=>{if(!o.json&&!o.list&&process.stdin.isTTY){let r=o.path??process.cwd(),a=S(o,r);await N(l(),r,a,{status:i});return}await ue(o)}),y.command("configure").description("Alias for `seldon baseline cases` \u2014 interactively mark test cases as skip / only / fixed.").option("--path <dir>","Project directory (default: current directory)").option("--id <baselineId>","Configure a specific baseline instead of the saved one").action(async o=>{let r=o.path??process.cwd(),a=S(o,r);await N(l(),r,a,{status:i})}),y.command("update [path]").description("Re-evaluate the baseline against the current code. Fixed cases are always kept.").option("--id <baselineId>","Update a specific baseline instead of the saved one").option("--name <filename>","Archive name reported to the API").option("--md","Render the diff as Markdown").option("--json","Render the diff as JSON").action(async(o,r)=>{let a=o??process.cwd(),c=S(r,a),u=C(a),d=await M(`Re-evaluating baseline against ${a}`,()=>dt(l(),a,c,{seldonYaml:ae(a),filename:r.name??u?.baseline?.generate?.filename,ignoreDirs:u?.baseline?.generate?.ignoreDirs}));i(`Update complete: ${d.summary.headline}`),m(d,P(r,u))}),y.command("delta [path]").description("Reassess the baseline from your code changes: writes a DRAFT and auto-runs the affected cases.").option("--id <baselineId>","Reassess a specific baseline instead of the saved one").option("--zip <file>","Use a prebuilt zip (scoped changed-files, or a repo zip with --full) instead of building from git").option("--full","Re-map the whole repo (adds cases for new files) instead of only the changed area").option("--base <ref>","Scoped mode: diff <base>...HEAD instead of the working tree vs HEAD").option("--url <url>","Target URL for the auto-run of the affected cases").option("--no-auto-run","Write the draft without running the affected cases").option("--md","Render the diff as Markdown").option("--json","Render the diff as JSON").action(async(o,r)=>{let a=o??process.cwd(),c=S(r,a),u=C(a),d,h=!!r.full;if(r.zip)d=new Uint8Array(Ge(r.zip));else if(r.full)d=(await Ve(a,{ignoreDirs:u?.baseline?.generate?.ignoreDirs})).bytes,h=!0;else{let R=await Qe(a,{base:r.base});i(`Reassessing ${R.fileCount} changed file(s) against the baseline ...`),d=R.bytes}let p=await M("Reassessing baseline against the changes",()=>He(l(),c,{zipBytes:d,full:h,targetUrl:r.url,seldonYaml:ae(a),autoRun:r.autoRun}));i(`Delta complete: ${p.report.summary.headline}`),I(a,R=>({...R,baseline:{...R.baseline,...p.experimentId?{lastRunExperimentId:p.experimentId}:{}}})),p.experimentId?i(`Draft ${p.draftBaselineId} \u2014 running affected cases as experiment ${p.experimentId} (view under QA).`):i(`Draft ${p.draftBaselineId} \u2014 no affected cases ran (nothing affected, or no target URL).`),m(p.report,P(r,u))}),y.command("versions").description("List every version of this baseline's lineage \u2014 the committed head and any draft proposals.").option("--path <dir>","Project directory (default: current directory)").option("--id <baselineId>","Inspect a specific lineage (any version id) instead of the saved one").option("--json","Output as JSON").action(async o=>{let r=o.path??process.cwd(),a=S(o,r),{versions:c,committedHeadId:u}=await tt(l(),a);if(o.json){f({committedHeadId:u,versions:c});return}if(c.length===0){t("No versions found for this baseline.");return}t(j(`${c.length} version${c.length===1?"":"s"} \u2014 committed head: ${u??"none"}`)+`
14
+ `),t(le(c,[{header:"",cell:d=>d.isHead?"*":d.status==="draft"?"d":" "},{header:"ID",cell:d=>d.id},{header:"STATUS",cell:d=>d.status},{header:"SOURCE",cell:d=>d.createdVia},{header:"CASES",align:"right",cell:d=>String(d.caseCount)},{header:"UPDATED",cell:d=>d.createdAt?new Date(d.createdAt).toISOString().slice(0,10):"?"}])),t(J(`
15
+ * = committed head (what runs use) \xB7 d = draft \xB7 promote a draft: seldon baseline promote <id>`))}),y.command("promote [versionId]").description("Promote a draft version to the committed head (the version runs resolve to).").option("--path <dir>","Project directory (default: current directory)").option("--id <versionId>","Promote a specific draft version instead of the positional arg / saved one").action(async(o,r)=>{let a=r.path??process.cwd(),c=o??S(r,a),u=await nt(l(),c),d=u.baseline_id??c;I(a,h=>({...h,baseline:{...h.baseline,baselineId:d}})),i(`Promoted to committed head ${d} (${u.case_count??u.cases?.length??0} case(s)). Runs now resolve to it.`)}),w(y.command("run").description("Run the current baseline against a live URL and print the report.")).action(b),v(y.command("report").description("Show the latest QA report for the current baseline (clean and actionable).")).action(k),w(s.command("run").description("Alias for `seldon baseline run`.")).action(b),v(s.command("report").description("Alias for `seldon baseline report`.")).action(k),e.length>0){let o=s.command("api").description("Call any API operation directly by name (auto-generated from the live OpenAPI spec).\n seldon api <operation> [--path key=value] [--query key=value] [--data '{json}']\n Run `seldon api --help` to see all available operations.");for(let r of e)o.command(r.name).description(r.summary??`${r.method} ${r.path}`).option("--path <pairs...>","Path params as key=value").option("--query <pairs...>","Query params as key=value").option("--data <json>","JSON request body").action(async a=>{f(await l().call(r.name,{path:de(a.path),query:de(a.query),body:a.data?JSON.parse(a.data):void 0}))})}return s}function pe(n){let e=V(n,x(),process.env);return new mt(e)}async function ht(){try{return await pe({}).apiKeyOps()}catch{return[]}}async function bt(){let n=W(process.argv)?await ht():[];await ce({makeSdk:pe},n).parseAsync(process.argv)}bt().catch(n=>{process.stderr.write(`${n.message}
16
+ `),process.exit(1)});
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@seldonqa/cli",
3
+ "version": "0.1.0",
4
+ "description": "Seldon - generate QA baselines from any codebase and run them from your terminal (thin client over @blok/sdk).",
5
+ "type": "module",
6
+ "bin": {
7
+ "seldon": "./dist/bin.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=20"
14
+ },
15
+ "dependencies": {
16
+ "@clack/prompts": "^0.10.1",
17
+ "commander": "^12.1.0",
18
+ "@blok/sdk": "0.1.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^20.14.0",
22
+ "tsup": "^8.1.0",
23
+ "typescript": "^5.5.0",
24
+ "vitest": "^2.0.0"
25
+ },
26
+ "scripts": {
27
+ "build": "tsup && node -e \"const fs=require('fs');const f='dist/bin.js';const c=fs.readFileSync(f,'utf8');if(!c.startsWith('#!/usr/bin/env node')){fs.writeFileSync(f,'#!/usr/bin/env node\\n'+c,'utf8')}fs.chmodSync(f,0o755)\"",
28
+ "typecheck": "tsc --noEmit",
29
+ "lint": "tsc --noEmit",
30
+ "test": "vitest run",
31
+ "test:live": "BLOK_LIVE=1 vitest run"
32
+ }
33
+ }