@verirun/cli 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +224 -15
- package/dist/cli.js +61 -28
- package/dist/gate.mjs +1 -1
- package/man/verirun.1 +248 -26
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,42 +1,75 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import{createRequire as __cr}from'node:module';const require=__cr(import.meta.url);
|
|
3
|
-
|
|
4
|
-
${f}`:" Check your connection."}`)}if(!u.ok)throw new Error(u.status===429?`${l} is rate limiting this machine (429). Wait a few minutes and try again.`:`${l} could not start the login (HTTP ${u.status}). This is a server-side problem, not your connection.`);let p=await u.json(),m=`${p.verifyUrl}?code=${encodeURIComponent(p.userCode)}`;n.info(""),n.info("To connect this machine to your Verirun account:"),n.info(""),n.info(` 1. Open ${n.bold(p.verifyUrl)}`),n.info(` 2. Enter ${n.bold(p.userCode)}`),n.info(""),n.info(n.dim(`(trying to open ${m} for you)`)),N(m),n.info(""),n.step("Waiting for approval in the browser..."),e.server&&await o({licenseServer:l});let g=Date.now()+p.expiresIn*1e3,v=Math.max(2,p.interval)*1e3;for(;Date.now()<g;){await Te(v);let w=await C(`${l}/api/cli/link/poll`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({deviceCode:p.deviceCode,fingerprint:d})}).catch(()=>{});if(!w)continue;if(w.status===429){await Te(v);continue}let f=await w.json().catch(()=>({status:"server_error"}));if(f.status==="ok"&&f.token){await o({token:f.token,licenseKey:void 0}),await t();let h={kind:"token",value:f.token,server:l},$=f.license?.lease;if($){await a(h,$);let E=f.license,j=E?.planName??E?.plan??"trial",A=typeof E?.seats=="number"&&E.seats>1?`, ${E.seats} machine seats`:"";n.success(`Connected. Plan: ${j} (${E?.status??"ok"})${A}.`)}else f.license&&f.license.valid===!1?(n.success("Connected."),f.license.reason==="trial_device_limit"?n.warn("This machine has already been used for the free trial on two other accounts, so record and run are blocked. Email support to request another trial, or subscribe from your account."):n.warn(`The account is connected but not active (${f.license.reason??f.license.status??"unknown"}). Check your subscription at your account page.`)):n.success("Connected.");return}if(f.status!=="pending"){if(f.status==="expired")throw new Error("The code expired. Run `verirun login` again.");if(f.status==="denied")throw new Error("The request was denied in the browser.");if(f.status==="no_license")throw new Error("That account has no license. Contact support.");if(f.status==="fingerprint_mismatch")throw new Error("This machine does not match the one that started the login.")}}throw new Error("Timed out waiting for approval. Run `verirun login` again.")}async function Le(){let{loadGlobalConfig:e,resolveServer:r,saveGlobalConfig:o,clearLicenseState:t}=await ce(),i=await e(),s=r(i.licenseServer);i.token&&await C(`${s}/api/cli/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:i.token})}).catch(()=>{}),await o({token:void 0,licenseKey:void 0}),await t(),n.success("Disconnected this machine. Run `verirun login` to reconnect.")}async function Pe(e){let{resolveCredential:r,checkWithServer:o,persistLease:t,readStoredLease:i,getMachineId:s}=await ce(),a=await r();if(n.info(""),!a){n.info(n.bold("Verirun: not connected")),n.info(""),n.info(`Run ${n.bold("verirun login")} to connect this machine.`),n.info("");return}let c=e.server?e.server.replace(/\/+$/,""):a.server,l={...a,server:c};n.info(n.bold("Verirun")),n.info(` credential ${a.kind==="token"?"device token":"license key"}`+(process.env.VERIRUN_LICENSE_KEY?n.dim(" (from $VERIRUN_LICENSE_KEY)"):"")),n.info(` server ${c}`),n.info(` machine ${(await s()).slice(0,24)}\u2026`);let d=await o(l);if(d.reachable&&d.lease){await t(l,d.lease);let g=(await i(l))?.payload;if(n.info(` status ${n.bold(g?.status??"valid")}`),g?.planName||g?.plan){let v=g.planName&&g.plan&&g.planName!==g.plan?n.dim(` (${g.plan})`):"";n.info(` plan ${g.planName??g.plan}${v}`)}typeof g?.seats=="number"&&n.info(` seats ${g.seats>1e3?"unlimited":`${g.seats} machine${g.seats===1?"":"s"}`}`),g?.exp&&n.info(` lease until ${new Date(g.exp*1e3).toISOString()}`),n.info(""),n.info(n.dim("Checked just now, lease signature verified.")),n.info("");return}if(d.reachable&&d.result&&!d.result.valid){n.info(` status ${d.result.status??"invalid"} (${d.result.reason??"\u2014"})`),n.info(""),n.info("record and run will not proceed."),n.info("");return}n.warn(" the license server is not reachable right now."),d.detail&&n.info(` reason ${d.detail}`);let u=G();u&&!d.detail?.includes(u)&&n.info(` network using proxy ${u}`);let p=await i(l);if(p){let m=Math.floor((Date.now()-p.payload.iat*1e3)/Br);n.info(` lease valid, issued ${m} day${m===1?"":"s"} ago`),n.info(n.dim(" Nags after 15 days offline, stops after 30."))}else n.info(n.dim(" No valid lease stored. Connect to the internet and run this again."));n.info("")}import{existsSync as Me}from"node:fs";import{mkdir as et,readdir as Be,writeFile as rt}from"node:fs/promises";import{join as We}from"node:path";import{existsSync as de}from"node:fs";import{mkdir as zr,readFile as Oe,writeFile as Xr}from"node:fs/promises";import Qr from"node:path";import{execFile as Gr}from"node:child_process";import{chmod as Hr}from"node:fs/promises";import Jr from"node:os";import{promisify as Kr}from"node:util";var Yr=Kr(Gr);async function le(e,r){if(process.platform!=="win32")try{return await Hr(e,r==="dir"?448:384),!0}catch{return!1}let o=qr();if(!o)return!1;let t=r==="dir"?`${o}:(OI)(CI)F`:`${o}:F`;try{return await Yr("icacls",[e,"/inheritance:r","/grant:r",t],{timeout:1e4,windowsHide:!0}),!0}catch{return!1}}function qr(){let e=process.env.USERNAME;if(!e)try{e=Jr.userInfo().username}catch{return}if(!e)return;let r=process.env.USERDOMAIN;return r&&r!==e?`${r}\\${e}`:e}var _e="data-testid";async function T(e){let r={};if(de(e)){let a=await Oe(e,"utf8");try{r=JSON.parse(a)}catch(c){throw new Error(`Config file is not valid JSON: ${e}`,{cause:c})}}let o=process.env.VERIRUN_SUPABASE_ACCESS_TOKEN;o&&(r.supabase={...r.supabase,accessToken:o});let t=process.env.VERIRUN_RAILWAY_TOKEN;t&&(r.railway={...r.railway,token:t});let i=process.env.VERIRUN_VERCEL_DRAIN_SECRET;i&&(r.vercel={...r.vercel,drainSecret:i});let s=process.env.VERIRUN_VERCEL_TOKEN?.trim();return s&&(r.vercel={...r.vercel,token:s}),r}async function De(e,r){let o={};if(de(e))try{o=JSON.parse(await Oe(e,"utf8"))}catch(a){throw new Error(`Config file is not valid JSON: ${e}`,{cause:a})}let t={...o,...r,supabase:r.supabase?{...o.supabase,...r.supabase}:o.supabase,vercel:r.vercel?{...o.vercel,...r.vercel}:o.vercel,railway:r.railway?{...o.railway,...r.railway}:o.railway},i=Qr.dirname(e),s=de(i);return await zr(i,{recursive:!0,mode:448}),s||await le(i,"dir"),await Xr(e,`${JSON.stringify(t,null,2)}
|
|
5
|
-
`,{mode:384}),le(e,"file")}import{existsSync as Ue}from"node:fs";import I from"node:path";var H=".verirun";function Zr(e=process.cwd()){let r=I.resolve(e),o;for(;;){if(Ue(I.join(r,".git")))return r;!o&&Ue(I.join(r,"package.json"))&&(o=r);let t=I.dirname(r);if(t===r)break;r=t}return o??I.resolve(e)}function y(e){let r=Zr(e),o=I.join(r,H);return{repoRoot:r,dataDir:o,workflowsDir:I.join(o,"workflows"),runsDir:I.join(o,"runs"),configPath:I.join(o,"config.json")}}function O(e,r){return I.join(e.workflowsDir,`${r}.json`)}function Ve(e=new Date){return e.toISOString().replace(/[:.]/g,"-")}function Fe(e,r,o){return I.join(e.runsDir,`${r}-${o}`)}var J=1;function tt(e){return e.commands.filter(r=>r.name()!=="help").map(r=>{let o=r.registeredArguments.map(t=>t.required?`<${t.name()}>`:`[${t.name()}]`).join(" ");return{name:r.name(),usage:`verirun ${r.name()}${o?` ${o}`:""}${r.options.length?" [options]":""}`,description:r.description(),options:r.options.map(t=>({flags:t.flags,description:t.description}))}})}async function nt(e){try{return(await Be(e)).filter(r=>r.endsWith(".json")).map(r=>r.slice(0,-5)).sort()}catch{return[]}}async function ot(e,r){try{return(await Be(e,{withFileTypes:!0})).filter(t=>t.isDirectory()).map(t=>t.name).sort().slice(-r).reverse()}catch{return[]}}async function it(){let e=y(),r={};try{r=await T(e.configPath)}catch{}let o=["supabase","vercel","railway"].filter(t=>r[t]);return{repoRoot:e.repoRoot,dataDir:e.dataDir,dataDirExists:Me(e.dataDir),configPath:e.configPath,configExists:Me(e.configPath),logSources:[...o],userFilter:typeof r.userFilter=="string"?r.userFilter:null,workflows:await nt(e.workflowsDir),recentRuns:await ot(e.runsDir,5)}}var ue="verirun records a browser workflow once, replays those exact steps headless on demand, and captures the backend logs produced during the replay. Deterministic: the same recording gives the same run and the same answer. `verirun map` is the static half: it reads a React/TypeScript repo and emits the interaction graph \u2014 every page, the interactive elements on it, where they navigate, and the backend calls they make \u2014 without running anything.",fe=["```jsonc","{",' "schemaVersion": 1,',' "name": "<same as the filename>",',' "createdAt": "<ISO>",',' "startUrl": "https://app.example.com/login", // optional',' "browser": "chromium", // chromium | firefox | webkit',' "channel": "chrome", // optional: installed browser',' "steps": [',' { "index": 0, "action": "navigate", "params": { "url": "https://app.example.com/login" }, "signals": [] },',' { "index": 1, "action": "fill", "selector": "role=textbox[name=\\"Email\\"i]", "params": { "text": "ada@example.com" }, "signals": [] },',' { "index": 2, "action": "click", "selector": "role=button[name=\\"Log in\\"i]", "params": {}, "signals": [] }'," ]","}","```","","**Click your way through the app; do not walk it by URL.** Use `navigate` for the","first step, and after that reach a page by clicking the thing that goes there \u2014 a","workflow that jumps between URLs proves nothing about the UI, and a dead link or a","button that stopped submitting still passes. `verirun map --bfs <route> --depth 2`","prints each reachable page with the element that gets you there, and every element","in `map --json` carries the `selector` to click. Navigate mid-workflow only for a","page with no path to it, or to deliberately deep-link into a state.","","Actions and their required `params`: `navigate` (`url`, absolute), `click` /","`dblclick`, `fill` (`text`), `press` (`key`), `check` / `uncheck`, `select`","(`values`), `setInputFiles` (`files`). Everything except `navigate` needs a","`selector`. An unknown action stops the run; `assert*` is skipped with a warning.","A step that opens a browser dialog (alert / confirm / prompt) may carry",'`"dialog": { "action": "accept" | "dismiss", "promptText"?: "..." }` \u2014 `accept` is',"OK, `dismiss` is Cancel. Without it the run answers with `--dialog` (default accept).","In `map --json` such an element carries `dialogs`. `gates: true` means the handler","reads the answer, so pick the path on purpose: with `dismiss` the guarded backend","call does not happen (its `guardSource` names the dialog). After `verirun map","--report`, `workflow check` warns about a dialog-opening step with no `dialog`.","`selector` goes straight to `page.locator()`, so it must be Playwright selector",'syntax \u2014 `role=button[name="Log in"i]`, `[data-testid="x"]`, or CSS.'],pe=["Anything behind a login needs a browser profile that is already signed in.","Pass the profile to `record` and `run`:","","```bash","# macOS / Linux \u2014 the shell expands ~ before verirun sees it","verirun run <name> --channel chrome --user-data-dir ~/.verirun-profile","verirun record <name> --channel chrome --user-data-dir ~/.verirun-profile","```","","```powershell","# Windows \u2014 cmd and PowerShell do NOT expand ~ inside this argument, so pass an",'# absolute path. A literal ~ creates a directory named "~" and the run silently',"# starts logged out.",'verirun run <name> --channel chrome --user-data-dir "$env:USERPROFILE\\.verirun-profile"',"```","","verirun never expands `~` itself \u2014 the path goes straight through to Playwright.","","The profile has to be created once **by a human**, because identity providers","refuse to authenticate an automation-launched browser:","","```bash","# macOS / Linux",'google-chrome --user-data-dir="$HOME/.verirun-profile" --password-store=basic',"```","","```powershell","# Windows (PowerShell)",'& "$env:ProgramFiles\\Google\\Chrome\\Application\\chrome.exe" `',' --user-data-dir="$env:USERPROFILE\\.verirun-profile"',"```","","Sign in to the app in that window, then close it.","","`--password-store=basic` (and `--use-mock-keychain` on macOS) matter on Unix:","Playwright passes both on every platform, and cookies written under the OS keyring","cannot be decrypted without them \u2014 the run would silently start logged out.","Both switches are Unix-only, so on Windows they are ignored and the profile just","has to be created by the same Windows user that runs verirun. The Windows path is","untested \u2014 if a run there starts logged out despite a signed-in profile, report","that rather than guessing.","","On every platform, do not point `--user-data-dir` at the human's everyday Chrome","profile.","","If you are asked to test a signed-in flow and no such profile exists, say so and","give the command above rather than trying to script the login."],me=["A human records the workflow once: `verirun record <name> --url <app-url>` (needs a display).","After a code change, run it: `verirun run <name>`. Add `--user <email>` to keep only that user's log lines.","Exit code 0 = every step replayed. Exit code 1 = a step failed, or a licensing / setup error.","Read `.verirun/runs/<name>-<newest>/logs.json`: check `status`; on `failed` read `error` and `steps.failedAt`; then scan `entries` for the backend's side \u2014 a 200 that hid an exception, a write that never landed, a job that threw after the screen said done.","`verirun run` writes logs.json even when replay fails, so partial logs survive.","Do not edit files under `.verirun/workflows/` by hand unless you understand the selector format; re-record instead.","`verirun map` needs no recording and no run: use it to find what a page can do before writing a workflow, `--json` for the whole graph, `--bfs <route> --depth N` for what is reachable from one page.","If `map` reports every page with 0 elements and 0 effects, its import aliases did not resolve \u2014 it prints the `--alias` flag to re-run with. That is a config gap, not an empty app."];function st(e,r){let o=[],t=(i="")=>{o.push(i)};t("# verirun \u2014 agent reference"),t(),t(ue),t(),t(`Version ${L}. Full docs: https://get-verirun.duely.in and the bundled README.md / \`man verirun\`.`),t(),t("## This checkout"),t(),t(`- repo root: ${e.repoRoot}`),t(`- data dir: ${e.dataDir} ${e.dataDirExists?"(exists)":"(not created yet)"}`),t(`- config: ${e.configPath} ${e.configExists?"(present)":"(absent)"}`),t(`- log sources: ${e.logSources.length?e.logSources.join(", "):"none configured \u2014 `verirun run` still replays, just with no backend logs"}`),t(`- user filter: ${e.userFilter??"none"}`),t(`- workflows: ${e.workflows.length?e.workflows.join(", "):"none recorded yet"}`),t(`- recent runs: ${e.recentRuns.length?e.recentRuns.join(", "):"none yet"}`),t(),t("Account: `record`, `run` and `map` need a connected account (`verirun login`, or `$VERIRUN_LICENSE_KEY` in CI). `verirun whoami` shows status. `--help`, `--version`, `verirun log`, and `verirun agent` do not need one."),t(),t("## Commands");for(let i of r){t(),t(`### ${i.usage}`),i.description&&t(i.description);for(let s of i.options)t(`- \`${s.flags}\` \u2014 ${s.description}`)}t(),t("## `.verirun/` layout (at the repo root shown above)"),t(),t("```"),t(".verirun/"),t(" config.json log sources, userFilter, testIdAttribute"),t(" workflows/<name>.json one recorded workflow each"),t(" runs/<name>-<timestamp>/logs.json captured logs, one directory per run"),t("```"),t(),t("Per-user, not per-project: `~/.verirun/config.json` (device token), `~/.verirun/state.json` (signed license lease)."),t(),t(`## logs.json (schemaVersion ${J})`),t(),t("```jsonc"),t("{"),t(` "schemaVersion": ${J},`),t(' "workflow": "<name>",'),t(' "status": "ok" | "failed", // "failed" => process exit code 1'),t(' "window": { "startedAt": "<ISO>", "endedAt": "<ISO>" },'),t(' "steps": { "total": N, "replayed": N, "failedAt"?: N },'),t(' "error"?: "<message>", // present only when status is "failed"'),t(' "filter"?: { "user": "<needle>" }, // present when --user / config.userFilter narrowed entries'),t(' "dialogs": [ // browser dialogs the replay answered, in order'),t(' { "step"?: N, "type": "alert"|"confirm"|"prompt"|"beforeunload", "message": "...",'),t(' "defaultValue"?: "...", "action": "accept"|"dismiss", "promptText"?: "...", "at": "<ISO>" }'),t(" ],"),t(' "sources": [ { "id": "supabase"|"vercel"|"railway", "name": "...", "entries": N } ],'),t(' "entries": ['),t(" {"),t(' "source": "supabase"|"vercel"|"railway",'),t(' "stream": "<sub-stream, e.g. edge_logs / lambda>",'),t(' "timestamp": "<ISO>",'),t(' "level": "<info|warn|error|...>",'),t(' "message": "<rendered log line>",'),t(' "raw": { /* provider payload, untouched */ }'),t(" }"),t(" ]"),t("}"),t("```"),t(),t("`entries` is every source merged and sorted by `timestamp`. When a `--user` filter is active, `entries` and every `sources[].entries` count are post-filter."),t(),t("## Writing a workflow yourself"),t(),t("`verirun map --json` gives every page element a `selector`, a `selectorConfidence` (`high` / `medium` / `low`) and a suggested `action`. That is enough to author `.verirun/workflows/<name>.json` without recording:"),t();for(let i of fe)t(i);t(),t("Then `verirun workflow check <name>` (add `--json` for machine-readable findings) validates shape, actions, selectors and params without opening a browser. It cannot tell you a selector matches the live page \u2014 only `verirun run <name>` can. Prefer `high`-confidence selectors; a `low` one is a guess. A recorded workflow is still more faithful, so do not overwrite one with a generated file: pick a different name."),t(),t("## Signed-in runs"),t();for(let i of pe)t(i);return t(),t("## Recommended agent loop"),t(),me.forEach((i,s)=>t(`${s+1}. ${i}`)),t(),t("Save this into your agent context: `verirun agent > .verirun/AGENT.md`, or paste it into `CLAUDE.md` / an AGENTS.md. `verirun agent --skill --install [claude|cursor|all]` installs it as a skill the agent loads on its own. `verirun agent --json` emits the same information as JSON."),t(),o.join(`
|
|
6
|
-
`)}
|
|
7
|
-
`)}
|
|
8
|
-
`).
|
|
9
|
-
`)}var Ge={claude:{label:"Claude Code",path:[".claude","skills","verirun","SKILL.md"],note:"Claude Code loads it automatically when a task looks like app testing.",render:e=>e},cursor:{label:"Cursor",path:[".cursor","rules","verirun.mdc"],note:"Cursor loads it when a task matches its description. Restart Cursor, or reopen the project, to pick it up.",render:lt}};function dt(e){if(e===!0)return["claude"];let r=String(e).toLowerCase();if(r==="all"||r==="both")return["claude","cursor"];if(r in Ge)return[r];throw new Error(`Unknown --install target "${e}". Use claude, cursor, all, or --install on its own for Claude Code.`)}async function He(e,r){let o=await it(),t=tt(e);if(r.json){process.stdout.write(`${JSON.stringify({about:ue,version:L,docs:"https://get-verirun.duely.in",checkout:o,commands:t,layout:{config:"<repo>/.verirun/config.json",workflows:"<repo>/.verirun/workflows/<name>.json",runs:"<repo>/.verirun/runs/<name>-<timestamp>/logs.json",userConfig:"~/.verirun/config.json",userState:"~/.verirun/state.json"},logsJsonSchemaVersion:J,signedIn:pe.join(`
|
|
10
|
-
`),
|
|
11
|
-
`),
|
|
12
|
-
`)
|
|
13
|
-
|
|
14
|
-
`)}
|
|
3
|
+
var jn=Object.defineProperty;var ye=(e,t,r)=>()=>{if(r)throw r[0];try{return e&&(t=e(e=0)),t}catch(n){throw r=[n],n}};var Nn=(e,t)=>{for(var r in t)jn(e,r,{get:t[r],enumerable:!0})};function qr(e){return e.includes("&")?e.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi,(t,r)=>{if(r[0]==="#"){let n=r[1]==="x"||r[1]==="X"?Number.parseInt(r.slice(2),16):Number.parseInt(r.slice(1),10);if(!Number.isFinite(n)||n<=0||n>1114111)return t;try{return String.fromCodePoint(n)}catch{return t}}return Ei[r.toLowerCase()]??t}):e}function Ii(e){let t=[],r=0,n=e.indexOf("data:");for(;n!==-1;){let o=n+5;for(;o<e.length;){let i=e.charCodeAt(o);if(i===34||i===39||i===41||i===60||i===62||i<=32)break;o+=1}o-n>Ci&&(t.push(e.slice(r,n+5)),r=o),n=e.indexOf("data:",o)}return r===0?e:(t.push(e.slice(r)),t.join(""))}function Jr(e){return e.replace(/[A-Z]+/g,t=>t.toLowerCase())}function Hr(e,t,r=[]){let n=Jr(e),o=e.length,i=[],c={},a=0;for(;a<o;){let l=n.indexOf("<",a);if(l===-1){i.push(e.slice(a));break}if(i.push(e.slice(a,l)),n.startsWith("<!--",l)){let f=n.indexOf("-->",l+4);if(i.push(" "),f===-1)break;a=f+3;continue}let d=t.find(f=>{if(!n.startsWith(f,l+1))return!1;let v=n[l+1+f.length];return v===void 0||v===">"||v==="/"||/\s/.test(v)});if(!d){i.push("<"),a=l+1;continue}c[d]=(c[d]??0)+1;let u=n.indexOf(">",l);if(u===-1)break;i.push(r.includes(d)?`${e.slice(l,u+1)}</${d}>`:" ");let m=n.indexOf(`</${d}`,u+1);if(m===-1)break;let p=n.indexOf(">",m);if(p===-1)break;a=p+1}return{text:i.join(""),counts:c}}function Ti(e,t){let r=/^<([a-zA-Z][a-zA-Z0-9:-]*)/.exec(e.slice(t,t+64));if(!r?.[1])return;let n=new Map,o=t+r[0].length,i=e.length;for(;o<i;){for(;o<i&&/[\s/]/.test(e[o]??"");)o+=1;if(o>=i)return;if(e[o]===">")return{name:r[1].toLowerCase(),attrs:n,end:o+1};let c=o;for(;c<i&&!/[\s=>/]/.test(e[c]??"");)c+=1;let a=e.slice(o,c).toLowerCase();for(o=c;o<i&&/\s/.test(e[o]??"");)o+=1;let l="";if(e[o]==="="){for(o+=1;o<i&&/\s/.test(e[o]??"");)o+=1;let d=e[o];if(d==='"'||d==="'"){let u=e.indexOf(d,o+1);if(u===-1)return;l=e.slice(o+1,u),o=u+1}else{let u=o;for(;u<i&&!/[\s>]/.test(e[u]??"");)u+=1;l=e.slice(o,u),o=u}}a&&!n.has(a)&&n.set(a,qr(l))}}function Kr(e){let t=[],r=0;for(;r<e.length;){let n=e.indexOf("<",r);if(n===-1){t.push(e.slice(r));break}t.push(e.slice(r,n)," ");let o=e.indexOf(">",n+1);if(o===-1)break;r=o+1}return qr(t.join("")).replace(/\s+/g," ").trim()}function Ve(e){let t=Ii(e.length>vt?e.slice(0,vt):e),r=e.length>vt||t.length>Fe,n=t.length>Fe?t.slice(0,Fe):t,o=Hr(n,["script","style","template"],["script"]),i=o.text,c=Jr(i),a={links:[],targets:new Set,hasMain:!1,resources:[],textLength:0,scriptCount:o.counts.script??0,emptyMount:!1,complete:!r},l=i.indexOf("<"),d=-1;for(;l!==-1&&!(d<l&&(d=i.indexOf(">",l),d===-1));){let p=Ti(i,l);if(!p){l=i.indexOf("<",l+1);continue}let{name:f,attrs:v}=p,b=v.get("id");if(b&&(a.targets.size<Br?a.targets.add(b):a.targets.has(b)||(a.complete=!1)),b&&!a.emptyMount&&_i.test(b)){let h=p.end,g=Math.min(i.length,h+200);for(;h<g&&/\s/.test(i[h]??"");)h+=1;c.startsWith(`</${f}`,h)&&(a.emptyMount=!0)}if((f==="main"||v.get("role")?.toLowerCase()==="main")&&(a.hasMain=!0),f==="a"){let h=v.get("name");h&&(a.targets.size<Br?a.targets.add(h):a.targets.has(h)||(a.complete=!1));let g=v.get("href");if(g!==void 0&&a.links.length<Ai){let R=c.slice(p.end,p.end+Li).indexOf("</a"),j=R===-1?"":i.slice(p.end,p.end+R),C=Kr(j)||v.get("aria-label")||v.get("title")||"";a.links.push({href:g.trim(),text:C.slice(0,80),download:v.has("download")})}}else if(f==="base"){let h=v.get("href");h&&!a.baseHref&&(a.baseHref=h.trim())}else if(f==="script"||f==="img"){let h=v.get("src");h&&a.resources.length<Gr&&a.resources.push(h.trim())}else if(f==="link"){let h=(v.get("rel")??"").toLowerCase().split(/\s+/),g=v.get("href");g&&h.includes("stylesheet")&&a.resources.length<Gr&&a.resources.push(g.trim())}l=i.indexOf("<",p.end)}let u=c.search(/<body[\s>]/),m=Hr(u===-1?i:i.slice(u),["noscript"]).text;return a.textLength=Kr(m).length,a}function Yr(e){return e.scriptCount===0?!1:e.textLength<40&&e.links.length<3?!0:e.emptyMount&&!e.hasMain&&e.textLength<400&&e.links.length<6}var Ei,Fe,vt,Ci,Ai,Br,Gr,Li,_i,kt=ye(()=>{"use strict";Ei={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:" "};Fe=3e6,vt=8*Fe,Ci=64;Ai=5e3,Br=2e4,Gr=2e3,Li=2e3;_i=/^(root|app|__next|__nuxt|__app|__layout|svelte|react-root|app-root|application|main-app|q-app|gatsby-focus-wrapper|___gatsby)$/i});function ji(e){let t=e.endsWith("$"),r=(t?e.slice(0,-1):e).split("*").map(n=>n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join(".*");return new RegExp(`^${r}${t?"$":""}`)}function zr(e,t){let r=[],n,o=!1,i=[];for(let m of e.split(/\r?\n/)){let p=m.replace(/#.*$/,"").trim();if(!p)continue;let f=p.indexOf(":");if(f===-1)continue;let v=p.slice(0,f).trim().toLowerCase(),b=p.slice(f+1).trim();if(v==="sitemap"){b&&!i.includes(b)&&i.push(b);continue}if(v==="user-agent"){(!n||!o)&&(n={agents:[],rules:[]},r.push(n)),n.agents.push(b.toLowerCase()),o=!0;continue}if(o=!1,!!n){if(v==="allow"||v==="disallow"){if(!b)continue;n.rules.push({allow:v==="allow",pattern:b,regex:ji(b)})}else if(v==="crawl-delay"){let h=Number(b);Number.isFinite(h)&&h>0&&(n.crawlDelayMs=Math.round(h*1e3))}}}let c=t.toLowerCase(),a=r.filter(m=>m.agents.some(p=>p!=="*"&&c.includes(p))),l=a.length>0?a:r.filter(m=>m.agents.includes("*")),d=l.flatMap(m=>m.rules),u=l.map(m=>m.crawlDelayMs).find(m=>m!==void 0);return{found:!0,sitemaps:i,...u!==void 0?{crawlDelayMs:u}:{},isAllowed(m){let p;for(let f of d)f.regex.test(m)&&(!p||f.pattern.length>p.pattern.length||f.pattern.length===p.pattern.length&&f.allow&&!p.allow)&&(p=f);return p?p.allow:!0}}}var Xr,Qr=ye(()=>{"use strict";Xr={found:!1,sitemaps:[],isAllowed:()=>!0}});function Ui(e){let t=e.trim();if(!t)return;let r=/^[a-z][a-z0-9+.-]*:\/\//i.test(t)?t:`https://${t}`,n;try{n=new URL(r)}catch{return}if(!(n.protocol!=="http:"&&n.protocol!=="https:")&&!(n.username||n.password)&&n.hostname)return n.hash="",n}function $t(e){let t=e.pathname+e.search;try{t=decodeURIComponent(t)}catch{}return Mi.test(t)}function Z(e){return`${e.origin}${e.pathname}${e.search}`}function nn(e){let t=e.toLowerCase().replace(/\.$/,"");if(/^[\d.]+$/.test(t)||t.includes(":"))return t;let r=t.split(".");if(r.length<=2)return t;let n=r[r.length-1]??"",o=r[r.length-2]??"",i=n.length===2&&Wi.has(o)?3:2;return r.slice(-i).join(".")}function Bi(e,t){if(!e.url)return e.kind==="js_exception"||e.kind==="console_error";let r;try{r=new URL(e.url)}catch{return!1}return r.protocol!=="http:"&&r.protocol!=="https:"?!1:nn(r.hostname)===nn(t.hostname)?!Vi.test(r.pathname):Fi.test(r.hostname)?!1:(e.resourceType==="fetch"||e.resourceType==="xhr")&&(e.kind==="http_error"||e.kind==="request_failed")}function Gi(e){return e.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g,"").replace(/\s+/g," ").trim()}function T(e,t=200){let r=Gi(e);return r.length>t?`${r.slice(0,t-1)}\u2026`:r}function le(e,t){return e.startsWith(t)?e.slice(t.length)||"/":e}function qi(e){let t=e.toLowerCase(),r=/<sitemapindex[\s>]/.test(t),n=[],o=0;for(;n.length<Ki;){let i=t.indexOf("<loc>",o);if(i===-1)break;let c=t.indexOf("</loc>",i+5);if(c===-1)break;if(o=c+6,c-i-5>on+32)continue;let a=e.slice(i+5,c).trim();if(a.startsWith("<![CDATA[")&&a.endsWith("]]>")&&(a=a.slice(9,-3).trim()),!a||a.length>on||a.includes("<"))continue;let l=a.replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'").replace(/&/g,"&");n.push(l)}return{index:r,locs:n}}function sn(e){return!/time(d)?\s*out|timeout/i.test(e??"")}function Ji(e){return e===404||e===410}function an(e){return e===401||e===403||e===429}async function cn(e,t,r={}){let n=r.now??Date.now,o=r.sleep??Yi,i=Math.max(1,r.maxPages??Rt),c=Math.max(0,r.maxDepth??Et),a=Math.max(0,r.delayMs??Ct),l=Math.max(0,r.maxProbes??Pi),d=n(),u=r.timeBudgetMs?d+r.timeBudgetMs:Number.POSITIVE_INFINITY,m=r.onEvent??(()=>{}),p=Ui(e),f={schemaVersion:Ni,tool:"verirun-scan",startUrl:p?.href??e,origin:p?.origin??"",mode:t.mode,startedAt:new Date(d).toISOString(),durationMs:0,status:"clean",limits:{maxPages:i,maxDepth:c,delayMs:a},robots:{found:!1},summary:{pages:0,findings:0,occurrences:0,byKind:{},linksChecked:0,linksUnchecked:0,skippedUnsafe:0,skippedByRobots:0},pages:[],findings:[]},v=(w,k)=>(k=T(k,300),f.status="error",f.error={code:w,message:k},f.durationMs=n()-d,f);if(!p)return v("invalid_url",`"${T(e,80)}" is not an http(s) URL.`);let b=p.origin,h=Xr,g=await t.fetchText(`${b}/robots.txt`);if(g.status===null)return v("unreachable",`${b} did not respond.`);if(g.status>=500)return v("robots_unavailable",`${b}/robots.txt returned ${g.status}, so the scan cannot tell what it may fetch.`);g.status>=200&&g.status<300&&!/^\s*</.test(g.text)&&(h=zr(g.text,St)),f.robots={found:h.found,...h.crawlDelayMs!==void 0?{crawlDelayMs:h.crawlDelayMs}:{}},m({type:"robots",...f.robots});let x=w=>h.isAllowed(w.pathname+w.search);if(!x(p))return v("robots_disallowed",`robots.txt on ${p.host} does not allow ${St} to fetch ${p.pathname}.`);t.setPolicy?.(x);let R=Math.max(a,h.crawlDelayMs??0);f.limits.delayMs=R;let j=new Map,C=(w,k,y,S)=>{let $=T(k,300),A=S===void 0?void 0:T(S,500),I=`${w}
|
|
4
|
+
${$}`,F=j.get(I);if(F){F.count+=1,F.pages.includes(y)||F.pages.push(y);return}j.set(I,{kind:w,message:$,...A?{evidence:A}:{},pages:[y],count:1})},G=new Set([Z(p)]),K=[{url:p,depth:0}],L=new Map,ee=new Map,J=[],oe=new Set,N=new Set,D=new Map,Y=0,se=0,Rn=(w,k)=>{let y=Z(w),S=ee.get(y);S?S.referrers.includes(k)||S.referrers.push(k):ee.set(y,{url:w,referrers:[k]})},En=w=>{if(!w.hash||w.hash==="#")return;let k=w.hash.slice(1);try{k=decodeURIComponent(k)}catch{}if(!(/^top$/i.test(k)||/^[/!]/.test(k)||k.startsWith(":~:")))return k},Cn=(w,k,y)=>{let S=k.href,$=Ve(w.html??""),A=!w.rendered&&Yr($),I=!$.complete,F=!A&&!I,we=k;if($.baseHref)try{we=new URL($.baseHref,k)}catch{}L.set(Z(k),{status:w.status,...F?{targets:$.targets}:{}}),F&&!$.hasMain&&C("missing_main","page has no <main> landmark",S,'no <main> or role="main" element');for(let E of $.links){if(!E.href||/^(mailto|tel|sms|javascript|data|blob):/i.test(E.href))continue;let _;try{_=new URL(E.href,we)}catch{continue}if(_.protocol!=="http:"&&_.protocol!=="https:"||_.origin!==b)continue;let X=Z(_),O=En(_);if(X===Z(k)){if(O!==void 0&&F&&!$.targets.has(O)){let te=/\bskip\b/i.test(E.text)||/^(main|content|main-?content|skip.*)$/i.test(O);C(te?"missing_skip_target":"missing_anchor",`#${T(O,60)} has no element with that id`,S,`<a href="${T(E.href,80)}">${T(E.text,40)}</a>`)}continue}if($t(_)){oe.add(X);continue}if(!x(_)){N.add(X);continue}if(Rn(_,S),O!==void 0&&J.push({page:S,key:X,href:E.href,fragment:O}),!(E.download||rn.test(_.pathname))&&y<c&&!G.has(X)){G.add(X);let te=new URL(_.href);te.hash="",K.push({url:te,depth:y+1})}}for(let E of w.events){if(!Bi(E,p))continue;let _=E.url?le(E.url,b):void 0;switch(E.kind){case"js_exception":C("js_exception",T(E.message),S,_);break;case"console_error":C("console_error",T(E.message),S,_);break;case"http_error":if(E.status===429){se+=1;break}C("http_error",`${E.status??"?"} ${E.method??"GET"} ${T(_??"",120)}`,S,E.url);break;case"request_failed":{let X=`${E.method??"GET"} ${T(_??"",120)} failed: ${T(E.message,80)}`,O;try{O=E.url?new URL(E.url):void 0}catch{O=void 0}if(O&&O.origin===b&&(E.method??"GET")==="GET"&&x(O)&&!$t(O)){O.hash="";let te=D.get(O.href)??[];te.push({message:X,page:S,evidence:O.href}),D.set(O.href,te);break}C("request_failed",X,S,E.url);break}}}if(A)return"page renders client-side; anchor and landmark checks skipped (no browser in this scan)";if(I)return"page is larger than the scanner reads; anchor and landmark checks skipped"},ge=h.crawlDelayMs!==void 0?1:Math.max(1,r.concurrency??Oi);f.limits.concurrency=ge;let Lt=async(w,k)=>{let y=await t.loadPage(w.url.href);if(y.status===null&&sn(y.error)&&n()<u){await o(en);let F=await t.loadPage(w.url.href);F.status!==null&&(Y+=1),y=F}let S=Z(w.url),$;try{$=new URL(y.finalUrl||w.url.href)}catch{$=w.url}$.hash="";let A=Z($);if(k){if(y.status===null)return v("unreachable",`${w.url.href} could not be loaded: ${T(y.error??"no response",160)}`);if($.origin!==b)return v("offsite_redirect",`${w.url.href} redirects to ${$.origin}. Scan that address instead.`);if(y.status>=400)return v("start_http_error",`${w.url.href} returned ${y.status}.`);if(y.html===void 0)return v("not_html",`${w.url.href} is not an HTML page (${y.contentType??"unknown content type"}).`)}if(A!==S&&L.has(A)){L.set(S,L.get(A)??{status:y.status});return}let I;y.status===null?(L.set(S,{status:null,error:y.error??"no response"}),I=`did not load: ${T(y.error??"no response",80)}`):$.origin!==b?(L.set(S,{status:y.status}),I=`redirects off-site to ${$.host}`):x($)?an(y.status)?(L.set(S,{status:y.status}),y.status===429&&(se+=1),I=y.status===429?"rate limited the scanner":"requires sign-in"):y.status>=400?(L.set(S,{status:y.status}),I=`returned ${y.status}`):y.html===void 0?(L.set(S,{status:y.status}),I="not an HTML page"):(G.add(A),I=Cn(y,$,w.depth),A!==S&&L.set(S,L.get(A)??{status:y.status})):(L.set(S,{status:y.status}),N.add(A),I="redirects to a path robots.txt disallows"),f.pages.push({url:$.origin===b?$.href:w.url.href,status:y.status,depth:w.depth,findings:0,...I?{note:I}:{}}),m({type:"page",url:w.url.href,status:y.status,depth:w.depth,...I?{note:I}:{}})},In=K.shift(),Tt=await Lt(In,!0);if(Tt)return Tt;if((r.useSitemap??!0)&&c>=1){let w=A=>{try{let I=new URL(A,b);return I.origin===b?I:void 0}catch{return}},k=h.sitemaps.map(w).filter(A=>A!==void 0);k.length===0&&k.push(new URL("/sitemap.xml",b));let y=new Set,S=!1,$=0;for(;k.length>0&&y.size<Di&&$<Zr&&n()<u;){let A=k.shift();if(y.has(A.href)||!x(A))continue;y.add(A.href);let I=await t.fetchText(A.href);if(I.status===null||I.status>=400||!/<(urlset|sitemapindex)[\s>]/i.test(I.text))continue;S=!0;let F=qi(I.text.slice(0,Hi));for(let we of F.locs){let E=w(we);if(!E||E.protocol!=="http:"&&E.protocol!=="https:")continue;if(F.index){k.push(E);continue}if($>=Zr)break;E.hash="";let _=Z(E);G.has(_)||L.has(_)||$t(E)||!x(E)||rn.test(E.pathname)||(G.add(_),K.push({url:E,depth:1}),$+=1)}}S&&(f.sitemap={urls:$})}let he=0,He=[],An=()=>new Promise(w=>He.push(w)),Ln=async()=>{for(;;){if(n()>=u){K.length>0&&(f.truncated??="time budget reached");return}if(f.pages.length+he>=i)return;let w=K.shift();if(!w){if(he===0)return;await An();continue}he+=1;try{await o(R),await Lt(w,!1)}finally{he-=1;let k=He;He=[];for(let y of k)y()}}};await Promise.all(Array.from({length:ge},()=>Ln())),!f.truncated&&K.length>0&&(f.truncated=`page cap of ${i} reached`);let _t=[...ee.entries()].filter(([w])=>!L.has(w)),Ke=_t.slice(0,l);Ke.length>0&&m({type:"probing",count:Ke.length});let jt=h.crawlDelayMs??Math.min(R,150),Nt=0,Ot=[...Ke],Tn=async()=>{let w=!0;for(let k=Ot.shift();k;k=Ot.shift()){if(n()>=u){f.truncated??="time budget reached";return}w||await o(jt),w=!1;let[y,S]=k,$=await t.probe(S.url.href);$.status===null&&sn($.error)&&n()<u&&(await o(en),$=await t.probe(S.url.href),$.status!==null&&(Y+=1)),L.set(y,{status:$.status,...$.error?{error:$.error}:{}}),$.status===429&&(se+=1),Nt+=1}};await Promise.all(Array.from({length:ge},()=>Tn())),f.summary.linksUnchecked=_t.length-Nt,f.summary.linksUnchecked>0&&m({type:"stopped",reason:f.truncated??"probe cap reached"});let Pt=[...D.entries()],_n=async()=>{for(let w=Pt.shift();w;w=Pt.shift()){let[k,y]=w,S;if(n()<u&&(await o(jt),S=await t.probe(k)),S&&S.status!==null&&S.status<400){Y+=1;continue}if(S?.status===429){se+=1;continue}for(let $ of y)C("request_failed",$.message,$.page,$.evidence)}};await Promise.all(Array.from({length:ge},()=>_n())),Y>0&&(f.recovered=Y),se>0&&(f.rateLimited=se);for(let[w,k]of ee){let y=L.get(w);if(!y)continue;f.summary.linksChecked+=1;let S=le(k.url.href.replace(/#.*$/,""),b);for(let $ of k.referrers)y.status===null?C("request_failed",`link to ${T(S,120)} did not load: ${T(y.error??"no response",80)}`,$,k.url.href):Ji(y.status)?C("dead_link",`link to ${T(S,120)} returns ${y.status}`,$,k.url.href):y.status>=400&&!an(y.status)&&C("http_error",`link to ${T(S,120)} returns ${y.status}`,$,k.url.href)}for(let w of J){let k=L.get(w.key)?.targets;!k||k.has(w.fragment)||C("missing_anchor",`${T(le(w.key,b),80)}#${T(w.fragment,60)} has no element with that id`,w.page,`<a href="${T(w.href,80)}">`)}f.findings=[...j.values()].sort((w,k)=>tn.indexOf(w.kind)-tn.indexOf(k.kind));for(let w of f.findings){f.summary.byKind[w.kind]=(f.summary.byKind[w.kind]??0)+1,f.summary.occurrences+=w.count;for(let k of w.pages){let y=f.pages.find(S=>S.url===k);y&&(y.findings+=1)}}return f.summary.pages=f.pages.length,f.summary.findings=f.findings.length,f.summary.skippedUnsafe=oe.size,f.summary.skippedByRobots=N.size,f.status=f.findings.length>0?"findings":"clean",f.durationMs=n()-d,f}var Ni,St,xt,We,Rt,Et,Ct,Oi,Pi,Di,Zr,en,tn,It,Mi,rn,Fi,Vi,Wi,Hi,Ki,on,Yi,Be=ye(()=>{"use strict";kt();Qr();Ni=1,St="VerirunScan",xt="https://get-verirun.duely.in/scanner",We=`Mozilla/5.0 (compatible; ${St}/1.0; +${xt})`,Rt=20,Et=2,Ct=500,Oi=4,Pi=40,Di=4,Zr=200,en=1e3,tn=["js_exception","http_error","request_failed","dead_link","console_error","missing_anchor","missing_skip_target","missing_main"],It={js_exception:"JS exception",http_error:"HTTP error",request_failed:"failed request",dead_link:"dead link",console_error:"console error",missing_anchor:"missing anchor",missing_skip_target:"broken skip link",missing_main:"no main landmark"};Mi=/(log|sign)[-_ ]?(out|off)|delete|remove|destroy|unsubscribe|deactivate|cancel|revoke|purge|erase|wipe/i;rn=/\.(pdf|zip|gz|tgz|tar|rar|7z|dmg|exe|msi|pkg|deb|rpm|apk|png|jpe?g|gif|webp|avif|svg|ico|bmp|mp[34]|m4[av]|mov|avi|webm|ogg|wav|woff2?|ttf|otf|eot|css|js|mjs|map|json|xml|rss|atom|txt|csv|xlsx?|docx?|pptx?)$/i;Fi=new RegExp(["google-analytics\\.com","googletagmanager\\.com","googlesyndication\\.com","googleadservices\\.com","doubleclick\\.net","google\\.com$","gstatic\\.com","facebook\\.(com|net)","fbcdn\\.net","connect\\.facebook","twitter\\.com","ads-twitter\\.com","t\\.co$","linkedin\\.com","licdn\\.com","tiktok\\.com","snapchat\\.com","pinterest\\.com","reddit(static)?\\.com","bing\\.com","clarity\\.ms","hotjar\\.(com|io)","fullstory\\.com","logrocket\\.(com|io)","lr-in(gest)?\\.","mouseflow\\.com","segment\\.(com|io)","mixpanel\\.com","amplitude\\.com","heap(analytics)?\\.(com|io)","posthog\\.com","plausible\\.io","usefathom\\.com","simpleanalytics","matomo\\.","cloudflareinsights\\.com","vercel-insights\\.com","vitals\\.vercel","sentry\\.io","bugsnag\\.com","rollbar\\.com","datadoghq\\.","browser-intake","newrelic\\.com","nr-data\\.net","intercom(cdn)?\\.(io|com)","crisp\\.chat","drift(t)?\\.com","tawk\\.to","zdassets\\.com","zendesk\\.com","zopim\\.com","hubspot\\.com","hs-(scripts|analytics|banner)\\.","hsforms\\.","livechatinc\\.com","olark\\.com","freshchat\\.com","optimizely\\.com","onesignal\\.com","pusher(app)?\\.com","adsrvr\\.org","adnxs\\.com","criteo\\.","taboola\\.com","outbrain\\.com","amazon-adsystem\\.com","cookiebot\\.com","onetrust\\.com","cookielaw\\.org","recaptcha","hcaptcha\\.com","challenges\\.cloudflare\\.com","stripe\\.network","[mr]\\.stripe\\.com"].join("|"),"i"),Vi=/^\/(_vercel\/(insights|speed-insights)|cdn-cgi\/(rum|beacon|challenge-platform))/i,Wi=new Set(["co","com","org","net","gov","edu","ac"]);Hi=2e6,Ki=5e3,on=2048;Yi=e=>new Promise(t=>{setTimeout(t,e)})});var hn={};Nn(hn,{createBrowserLoader:()=>ra});import{chromium as Zi}from"playwright";function At(e){let t=e instanceof Error?e.message:String(e);return(t.split(`
|
|
5
|
+
`)[0]??t).replace(/^page\.goto: /,"")}function ea(e){return/https?:\/\/[^\s)]+?(?=:\d+:\d+|\s|\)|$)/.exec(e??"")?.[0]}function ta(e,t){e.on("pageerror",r=>{let n=ea(r.stack);t.push({kind:"js_exception",message:r.name&&!r.message.startsWith(r.name)?`${r.name}: ${r.message}`:r.message,...n?{url:n}:{}})}),e.on("console",r=>{if(r.type()!=="error")return;let n=r.text();if(/^Failed to load resource/i.test(n))return;let o=r.location().url;t.push({kind:"console_error",message:n,...o?{url:o}:{}})}),e.on("requestfailed",r=>{let n=r.failure()?.errorText??"failed";/ERR_ABORTED/i.test(n)||r.isNavigationRequest()&&r.frame()===e.mainFrame()||t.push({kind:"request_failed",message:n,url:r.url(),method:r.method(),resourceType:r.resourceType()})}),e.on("response",r=>{if(r.status()<400)return;let n=r.request();n.isNavigationRequest()&&n.frame()===e.mainFrame()||t.push({kind:"http_error",message:`${r.status()}`,url:r.url(),status:r.status(),method:n.method(),resourceType:n.resourceType()})})}async function ra(e={}){let t=e.timeoutMs??2e4,r=e.settleMs??2500,n;try{n=await Zi.launch({headless:!0})}catch(c){throw new Error(`Could not start the browser for the scan: ${At(c)}
|
|
6
|
+
Install it with \`npx playwright install chromium\`, or scan without one: --no-browser`)}let o=await n.newContext({userAgent:We,serviceWorkers:"block",acceptDownloads:!1}),i=async c=>{try{let a={timeout:t,failOnStatusCode:!1,maxRedirects:5},l=await o.request.head(c,a);(l.status()===405||l.status()===501||l.status()===400)&&(l=await o.request.get(c,a));let d=l.status();return await l.dispose(),{status:d}}catch(a){return{status:null,error:At(a)}}};return{mode:"browser",async loadPage(c){let a=await o.newPage(),l=[];ta(a,l);try{let d=Date.now(),u=await a.goto(c,{waitUntil:"domcontentloaded",timeout:t}),m=Math.max(1e3,t-(Date.now()-d));await a.waitForLoadState("load",{timeout:m}).catch(()=>{}),await a.waitForLoadState("networkidle",{timeout:r}).catch(()=>{});let p=u?.status()??null,f=u?.headers()["content-type"],v=!f||/\b(text\/html|application\/xhtml\+xml)\b/i.test(f);return{url:c,finalUrl:a.url(),status:p,...f?{contentType:f}:{},...v?{html:await a.content()}:{},rendered:!0,events:l}}catch(d){let u=At(d);if(/Download is starting/i.test(u)){let m=await i(c);if(m.status!==null)return{url:c,finalUrl:c,status:m.status,contentType:"attachment",rendered:!0,events:l}}return{url:c,finalUrl:c,status:null,error:u,rendered:!0,events:l}}finally{await a.close().catch(()=>{})}},probe:i,async fetchText(c){try{let a=await o.request.get(c,{timeout:t,failOnStatusCode:!1,maxRedirects:5}),l=a.status(),d=l<400?await a.text():"";return await a.dispose(),{status:l,text:d}}catch{return{status:null,text:""}}},async close(){await o.close().catch(()=>{}),await n.close().catch(()=>{})}}}var wn=ye(()=>{"use strict";Be()});import{createPublicKey as ga,verify as ha}from"node:crypto";import{readFileSync as wa}from"node:fs";import $n from"node:path";import{fileURLToPath as ya}from"node:url";import{Command as ba,CommanderError as va}from"commander";import Je from"node:os";import{setTimeout as Vt}from"node:timers/promises";var Dt=!1,On=["https_proxy","HTTPS_PROXY","http_proxy","HTTP_PROXY"];function be(){for(let e of On){let t=process.env[e]?.trim();if(t)return t}}function Pn(e,t=process.env.NO_PROXY??process.env.no_proxy){let r=t?.trim();if(!r)return!1;if(r==="*")return!0;let n=e.toLowerCase();return r.split(",").map(o=>o.trim().toLowerCase().replace(/^\./,"").split(":")[0]??"").filter(Boolean).some(o=>n===o||n.endsWith(`.${o}`))}async function Dn(e){if(Dt)return;Dt=!0;let t=be();if(!t)return;let r;try{r=(typeof e=="string"?new URL(e):e).hostname}catch{return}if(!Pn(r))try{let{ProxyAgent:n,setGlobalDispatcher:o}=await import("undici");o(new n(t))}catch{}}async function B(e,t){return await Dn(e),fetch(e,t)}function ve(e){let t=Un(e);if(t)switch(t){case"ENOTFOUND":case"EAI_AGAIN":return`${t} \u2014 the address could not be resolved. A DNS filter or a captive portal will do this.`;case"ECONNREFUSED":case"ECONNRESET":case"EHOSTUNREACH":case"ENETUNREACH":return`${t} \u2014 the connection was blocked. ${Ut()}`;case"UND_ERR_CONNECT_TIMEOUT":case"ETIMEDOUT":return`${t} \u2014 the connection timed out. ${Ut()}`;case"UNABLE_TO_GET_ISSUER_CERT_LOCALLY":case"SELF_SIGNED_CERT_IN_CHAIN":case"DEPTH_ZERO_SELF_SIGNED_CERT":case"CERT_HAS_EXPIRED":return`${t} \u2014 the TLS certificate was not trusted. A proxy that inspects traffic causes this; point NODE_EXTRA_CA_CERTS at your organisation's CA file.`;default:return t}}function Ut(){let e=be();return e?`Reached through the proxy ${e}, so check that the proxy is correct and running.`:"If this network needs a proxy, set HTTPS_PROXY."}function Un(e){for(let t=e,r=0;t&&r<4;r++){if(typeof t=="object"&&"code"in t&&typeof t.code=="string")return t.code;t=typeof t=="object"&&"cause"in t?t.cause:void 0}}import{spawn as Mn}from"node:child_process";function re(e){let t=process.platform==="darwin"?"open":process.platform==="win32"?"cmd":"xdg-open",r=process.platform==="win32"?["/c","start","",e]:[e];try{let n=Mn(t,r,{stdio:"ignore",detached:!0});n.on("error",()=>{}),n.unref()}catch{}}var Fn=!!process.stdout.isTTY&&!process.env.NO_COLOR,z=(e,t)=>Fn?`\x1B[${e}m${t}\x1B[0m`:t,Mt=!1,qe=e=>Mt?console.error(e):console.log(e),Ft=!1,Vn=e=>{Ft?process.stderr.isTTY&&console.error(e):qe(e)},U={ok:0,failed:1,error:2},s={machineOutput:()=>{Mt=!0},quietSteps:()=>{Ft=!0},info:e=>qe(e),step:e=>Vn(`${z("90","\xB7")} ${e}`),success:e=>qe(`${z("32","\u2713")} ${e}`),warn:e=>console.warn(`${z("33","!")} ${e}`),error:e=>console.error(`${z("31","\u2717")} ${e}`),dim:e=>z("90",e),bold:e=>z("1",e),green:e=>z("32",e),red:e=>z("31",e),yellow:e=>z("33",e)};var ne="0.3.0";var Wn=1440*60*1e3;function Ye(){return import("./gate.mjs")}async function Wt(e){let{getMachineId:t,saveGlobalConfig:r,clearLicenseState:n,loadGlobalConfig:o,resolveServer:i,persistLease:c}=await Ye(),a=await o(),l=e.server?e.server.replace(/\/+$/,""):i(a.licenseServer);if(a.token&&!e.force){s.info("This machine is already connected. Run `verirun whoami`, or"),s.info("`verirun login --force` to connect it to a different account.");return}let d=await t(),u;try{u=await B(`${l}/api/cli/link`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({fingerprint:d,hostname:Je.hostname(),os:`${Je.platform()} ${Je.release()}`,cliVersion:ne})})}catch(b){let h=ve(b);throw new Error(`Could not reach ${l}.${h?`
|
|
7
|
+
${h}`:" Check your connection."}`)}if(!u.ok)throw new Error(u.status===429?`${l} is rate limiting this machine (429). Wait a few minutes and try again.`:`${l} could not start the login (HTTP ${u.status}). This is a server-side problem, not your connection.`);let m=await u.json(),p=`${m.verifyUrl}?code=${encodeURIComponent(m.userCode)}`;s.info(""),s.info("To connect this machine to your Verirun account:"),s.info(""),s.info(` 1. Open ${s.bold(m.verifyUrl)}`),s.info(` 2. Enter ${s.bold(m.userCode)}`),s.info(""),s.info(s.dim(`(trying to open ${p} for you)`)),re(p),s.info(""),s.step("Waiting for approval in the browser..."),e.server&&await r({licenseServer:l});let f=Date.now()+m.expiresIn*1e3,v=Math.max(2,m.interval)*1e3;for(;Date.now()<f;){await Vt(v);let b=await B(`${l}/api/cli/link/poll`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({deviceCode:m.deviceCode,fingerprint:d})}).catch(()=>{});if(!b)continue;if(b.status===429){await Vt(v);continue}let h=await b.json().catch(()=>({status:"server_error"}));if(h.status==="ok"&&h.token){await r({token:h.token,licenseKey:void 0}),await n();let g={kind:"token",value:h.token,server:l},x=h.license?.lease;if(x){await c(g,x);let R=h.license,j=R?.planName??R?.plan??"trial",C=typeof R?.seats=="number"&&R.seats>1?`, ${R.seats} machine seats`:"";s.success(`Connected. Plan: ${j} (${R?.status??"ok"})${C}.`)}else h.license&&h.license.valid===!1?(s.success("Connected."),h.license.reason==="trial_device_limit"?s.warn("This machine has already been used for the free trial on two other accounts, so record and run are blocked. Email support to request another trial, or buy lifetime access from your account page."):s.warn(`The account is connected but not active (${h.license.reason??h.license.status??"unknown"}). Check your account page.`)):s.success("Connected.");return}if(h.status!=="pending"){if(h.status==="expired")throw new Error("The code expired. Run `verirun login` again.");if(h.status==="denied")throw new Error("The request was denied in the browser.");if(h.status==="no_license")throw new Error("That account has no license. Contact support.");if(h.status==="fingerprint_mismatch")throw new Error("This machine does not match the one that started the login.")}}throw new Error("Timed out waiting for approval. Run `verirun login` again.")}async function Bt(){let{loadGlobalConfig:e,resolveServer:t,saveGlobalConfig:r,clearLicenseState:n}=await Ye(),o=await e(),i=t(o.licenseServer);o.token&&await B(`${i}/api/cli/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:o.token})}).catch(()=>{}),await r({token:void 0,licenseKey:void 0}),await n(),s.success("Disconnected this machine. Run `verirun login` to reconnect.")}var Bn=1;async function Gt(e){let{resolveCredential:t,checkWithServer:r,persistLease:n,readStoredLease:o,getMachineId:i}=await Ye();e.json&&s.machineOutput();let c=await t(),a=h=>{e.json&&process.stdout.write(`${JSON.stringify({schemaVersion:Bn,tool:"verirun-whoami",...h})}
|
|
8
|
+
`),process.exitCode=h.licensed?U.ok:U.failed};if(!c){s.info(`${s.bold("Verirun: not connected")}. Run ${s.bold("verirun login")} to connect this machine.`),a({connected:!1,licensed:!1});return}let l=e.server?e.server.replace(/\/+$/,""):c.server,d={...c,server:l},u={connected:!0,credential:c.kind,server:l},m=[`credential ${c.kind==="token"?"device token":"license key"}`+(process.env.VERIRUN_LICENSE_KEY?s.dim(" (from $VERIRUN_LICENSE_KEY)"):""),`server ${l}`,`machine ${(await i()).slice(0,24)}\u2026`],p=h=>{s.info(h);for(let g of m)s.info(` ${g}`)},f=await r(d);if(f.reachable&&f.lease){await n(d,f.lease);let g=(await o(d))?.payload,x=g?.status??"valid";if(g?.planName||g?.plan){let C=g.planName&&g.plan&&g.planName!==g.plan?s.dim(` (${g.plan})`):"";m.push(`plan ${g.planName??g.plan}${C}`)}let R=typeof g?.seats=="number"&&g.seats>1e3;typeof g?.seats=="number"&&m.push(`seats ${R?"unlimited":`${g.seats} machine${g.seats===1?"":"s"}`}`);let j=g?.exp?new Date(g.exp*1e3).toISOString():void 0;j&&m.push(`lease until ${j}`),p(s.green(`OK Verirun license ${x}, checked just now, lease signature verified`)),a({...u,licensed:!0,reachable:!0,status:x,...g?.plan?{plan:g.plan}:{},...g?.planName?{planName:g.planName}:{},...typeof g?.seats=="number"?{seats:R?null:g.seats}:{},...j?{leaseExpiresAt:j}:{}});return}if(f.reachable&&f.result&&!f.result.valid){let h=f.result.status??"invalid",g=f.result.reason??void 0;p(s.red(`NOT LICENSED ${h}${g?` (${g})`:""}: record, run, map and scan will not proceed`)),a({...u,licensed:!1,reachable:!0,status:h,...g?{reason:g}:{}});return}f.detail&&m.push(`reason ${f.detail}`);let v=be();v&&!f.detail?.includes(v)&&m.push(`network using proxy ${v}`);let b=await o(d);if(b){let h=Math.floor((Date.now()-b.payload.iat*1e3)/Wn),g=h<30;m.push(`lease issued ${h} day${h===1?"":"s"} ago (nags after 15 days offline, stops after 30)`),p(g?s.yellow("OK (offline) the license server is not reachable; the stored lease is still valid"):s.red("NOT LICENSED the license server is not reachable and the stored lease is over 30 days old")),a({...u,licensed:g,reachable:!1,offlineDays:h,...b.payload.status?{status:b.payload.status}:{}})}else p(s.red("NOT LICENSED the license server is not reachable and no valid lease is stored")),a({...u,licensed:!1,reachable:!1})}import{existsSync as Qe}from"node:fs";import{mkdir as eo,readdir as Qt,readFile as to,writeFile as zt}from"node:fs/promises";import{dirname as ro,join as no}from"node:path";import{existsSync as ze}from"node:fs";import{mkdir as Xn,readFile as Ht,writeFile as zn}from"node:fs/promises";import Qn from"node:path";import{execFile as Gn}from"node:child_process";import{chmod as Hn}from"node:fs/promises";import Kn from"node:os";import{promisify as qn}from"node:util";var Jn=qn(Gn);async function Xe(e,t){if(process.platform!=="win32")try{return await Hn(e,t==="dir"?448:384),!0}catch{return!1}let r=Yn();if(!r)return!1;let n=t==="dir"?`${r}:(OI)(CI)F`:`${r}:F`;try{return await Jn("icacls",[e,"/inheritance:r","/grant:r",n],{timeout:1e4,windowsHide:!0}),!0}catch{return!1}}function Yn(){let e=process.env.USERNAME;if(!e)try{e=Kn.userInfo().username}catch{return}if(!e)return;let t=process.env.USERDOMAIN;return t&&t!==e?`${t}\\${e}`:e}var Kt="data-testid";async function Q(e){let t={};if(ze(e)){let c=await Ht(e,"utf8");try{t=JSON.parse(c)}catch(a){throw new Error(`Config file is not valid JSON: ${e}`,{cause:a})}}let r=process.env.VERIRUN_SUPABASE_ACCESS_TOKEN;r&&(t.supabase={...t.supabase,accessToken:r});let n=process.env.VERIRUN_RAILWAY_TOKEN;n&&(t.railway={...t.railway,token:n});let o=process.env.VERIRUN_VERCEL_DRAIN_SECRET;o&&(t.vercel={...t.vercel,drainSecret:o});let i=process.env.VERIRUN_VERCEL_TOKEN?.trim();return i&&(t.vercel={...t.vercel,token:i}),t}async function qt(e,t){let r={};if(ze(e))try{r=JSON.parse(await Ht(e,"utf8"))}catch(c){throw new Error(`Config file is not valid JSON: ${e}`,{cause:c})}let n={...r,...t,supabase:t.supabase?{...r.supabase,...t.supabase}:r.supabase,vercel:t.vercel?{...r.vercel,...t.vercel}:r.vercel,railway:t.railway?{...r.railway,...t.railway}:r.railway},o=Qn.dirname(e),i=ze(o);return await Xn(o,{recursive:!0,mode:448}),i||await Xe(o,"dir"),await zn(e,`${JSON.stringify(n,null,2)}
|
|
9
|
+
`,{mode:384}),Xe(e,"file")}import{existsSync as Jt}from"node:fs";import q from"node:path";var ke=".verirun";function Zn(e=process.cwd()){let t=q.resolve(e),r;for(;;){if(Jt(q.join(t,".git")))return t;!r&&Jt(q.join(t,"package.json"))&&(r=t);let n=q.dirname(t);if(n===t)break;t=n}return r??q.resolve(e)}function P(e){let t=Zn(e),r=q.join(t,ke);return{repoRoot:t,dataDir:r,workflowsDir:q.join(r,"workflows"),runsDir:q.join(r,"runs"),configPath:q.join(r,"config.json")}}function ie(e,t){return q.join(e.workflowsDir,`${t}.json`)}function Yt(e=new Date){return e.toISOString().replace(/[:.]/g,"-")}function Xt(e,t,r){return q.join(e.runsDir,`${t}-${r}`)}var tt=1,oo=1;function so(e){return e.commands.filter(t=>t.name()!=="help").map(t=>{let r=t.registeredArguments.map(n=>n.required?`<${n.name()}>`:`[${n.name()}]`).join(" ");return{name:t.name(),usage:`verirun ${t.name()}${r?` ${r}`:""}${t.options.length?" [options]":""}`,description:t.description(),options:t.options.map(n=>({flags:n.flags,description:n.description}))}})}async function io(e){try{return(await Qt(e)).filter(t=>t.endsWith(".json")).map(t=>t.slice(0,-5)).sort()}catch{return[]}}async function ao(e,t){try{return(await Qt(e,{withFileTypes:!0})).filter(n=>n.isDirectory()).map(n=>n.name).sort().slice(-t).reverse()}catch{return[]}}async function co(){let e=P(),t={};try{t=await Q(e.configPath)}catch{}let r=["supabase","vercel","railway"].filter(n=>t[n]);return{repoRoot:e.repoRoot,dataDir:e.dataDir,dataDirExists:Qe(e.dataDir),configPath:e.configPath,configExists:Qe(e.configPath),logSources:[...r],userFilter:typeof t.userFilter=="string"?t.userFilter:null,workflows:await io(e.workflowsDir),recentRuns:await ao(e.runsDir,5)}}var Se="verirun tests a web app from four angles. `record` saves a browser workflow once and `run` replays those exact steps headless, then captures the backend logs (Vercel, Supabase, Railway) the replay produced. Replay is deterministic: no model is involved, so a run has no AI cost. `map` reads a React/TypeScript repo and emits the interaction graph (pages, interactive elements, navigation, backend calls) without running anything. `scan <url>` crawls a live site read-only and reports JS errors, failed requests, dead links and missing anchors; it needs no source and no logs.",rt='Exit codes, every command: 0 ok. 1 the check failed (a replay step, scan findings, workflow errors, `whoami` not licensed). 2 could not run (usage, setup, license, unreachable target). With `--json` an exit-2 failure also prints `{"status":"error","error":{"message"}}` on stdout.',nt=["```jsonc",'{ "schemaVersion": 1, "name": "<same as the filename>", "createdAt": "<ISO>",',' "startUrl": "https://app.example.com/login", // optional',' "browser": "chromium", // chromium | firefox | webkit',' "channel": "chrome", // optional: installed browser',' "steps": [',' { "index": 0, "action": "navigate", "params": { "url": "https://app.example.com/login" }, "signals": [] },',' { "index": 1, "action": "fill", "selector": "role=textbox[name=\\"Email\\"i]", "params": { "text": "ada@example.com" }, "signals": [] },',' { "index": 2, "action": "click", "selector": "role=button[name=\\"Log in\\"i]", "params": {}, "signals": [] }'," ] }","```","","- Actions and required `params`: `navigate` (`url`, absolute), `click` / `dblclick`, `fill` (`text`), `press` (`key`), `check` / `uncheck`, `select` (`values`), `setInputFiles` (`files`). All but `navigate` need a `selector`. An unknown action stops the run; `assert*` is skipped with a warning.",'- `selector` goes straight to `page.locator()`: Playwright syntax, e.g. `role=button[name="Log in"i]`, `[data-testid="x"]`, or CSS.',"- **Click through the app, do not walk it by URL.** `navigate` first, then reach each page by clicking what leads there; a workflow that jumps between URLs still passes when a link is dead. `verirun map --bfs <route> --depth 2` lists each reachable page with the element that gets you there. Navigate mid-workflow only for a page nothing links to, or to deep-link into a state on purpose.",'- A step that opens a browser dialog may carry `"dialog": { "action": "accept" | "dismiss", "promptText"?: "..." }` (`accept` is OK, `dismiss` is Cancel); without it the run answers with `--dialog` (default accept). In `map --json` such an element carries `dialogs`; `gates: true` means the handler reads the answer, so with `dismiss` the guarded backend call does not happen (its `guardSource` names the dialog). After `verirun map --report`, `workflow check` warns about a dialog-opening step with no `dialog`.'],ot=["A flow behind a login needs a browser profile that is already signed in. Pass it to both `record` and `run`:","","```bash","verirun run <name> --channel chrome --user-data-dir ~/.verirun-profile","```","",'- verirun never expands `~`. macOS / Linux shells do; cmd and PowerShell do not, so on Windows pass an absolute path (`"$env:USERPROFILE\\.verirun-profile"`). A literal `~` creates a directory named `~` and the run silently starts logged out.','- A human creates the profile once, because identity providers refuse an automation-launched browser. macOS / Linux: `google-chrome --user-data-dir="$HOME/.verirun-profile" --password-store=basic` (add `--use-mock-keychain` on macOS). Windows: `& "$env:ProgramFiles\\Google\\Chrome\\Application\\chrome.exe" --user-data-dir="$env:USERPROFILE\\.verirun-profile"`. Sign in to the app in that window, then close it.',"- Those two switches matter on Unix: Playwright passes both, and cookies written under the OS keyring cannot be decrypted without them, so the run would start logged out. On Windows they are ignored; the profile must be created by the same Windows user that runs verirun. The Windows path is untested: if a run there starts logged out despite a signed-in profile, report that instead of guessing.","- Never point `--user-data-dir` at the human's everyday Chrome profile.","- If asked to test a signed-in flow and no such profile exists, say so and give the command above. Do not script the login."],st=["A human records the workflow once: `verirun record <name> --url <app-url>` (needs a display).","After a code change: `verirun run <name>`. Add `--user <email>` to keep only that user's log lines.","Read the output, not the log file. Line 1 is `PASS` or `FAIL`. On `FAIL` the failed step, the error and the selector follow. The `logs` lines list error-level backend entries (and HTTP 5xx) from the run window, repeats folded as `xN`. A `PASS` with error lines under `logs` is a real finding: the screen said done and the backend threw.","`verirun run <name> --json` prints the same as one JSON object, for branching in code.","Open `.verirun/runs/<name>-<newest>/logs.json` only when you need an entry the summary does not show (info-level lines, the provider's raw payload). It is written even when replay fails. It is large: filter it (`jq`, `grep`), do not read it whole.","Do not hand-edit a recorded file under `.verirun/workflows/`; re-record, or write a new workflow under a different name.","`verirun map` needs no recording and no run: use it to find what a page can do before writing a workflow. `--json` is the whole graph on one line, `--bfs <route> --depth N` what is reachable from one page.","If `map` reports every page with 0 elements and 0 effects, its import aliases did not resolve; it prints the `--alias` flag to re-run with. That is a config gap, not an empty app.","`verirun scan <url>` checks a deployed site from outside: `--json` for the report, `--md` for a paste-ready summary. It only sends GET and HEAD, never clicks or submits, skips logout / delete style links and obeys robots.txt. It sees what a visitor's browser sees; it cannot see backend logs or the source-derived map."],Zt=["```jsonc",'{ "schemaVersion": 1, "tool": "verirun-run", "workflow": "<name>", "status": "ok" | "failed",',' "durationMs": N, "window": { "startedAt": "<ISO>", "endedAt": "<ISO>" },',' "steps": { "total": N, "replayed": N, "failedAt"?: N },',' "failedStep"?: { "index": N, "action": "click", "selector"?: "...", "description"?: "..." },',' "error"?: "<full message>", "dialogs": N,',' "sources": [ { "id": "supabase"|"vercel"|"railway", "name": "...", "entries": N } ],',' "logs": { "entries": N, "problems": N, "warnings": N, "groupsOmitted": N,',' "groups": [ { "source": "...", "stream"?: "...", "level"?: "...", "status"?: 500,',' "message": "<first line>", "count": N, "firstAt"?: "<ISO>", "lastAt"?: "<ISO>" } ] },',' "logsPath": ".verirun/runs/<name>-<timestamp>/logs.json" }',"```"],lo=["```jsonc",`{ "schemaVersion": ${tt}, "workflow": "<name>", "status": "ok" | "failed",`,' "window": { "startedAt": "<ISO>", "endedAt": "<ISO>" },',' "steps": { "total": N, "replayed": N, "failedAt"?: N },',' "error"?: "<message>", // only when status is "failed"',' "filter"?: { "user": "<needle>" }, // when --user / config.userFilter narrowed entries',' "dialogs": [ { "step"?: N, "type": "alert"|"confirm"|"prompt"|"beforeunload", "message": "...",',' "defaultValue"?: "...", "action": "accept"|"dismiss", "promptText"?: "...", "at": "<ISO>" } ],',' "sources": [ { "id": "supabase"|"vercel"|"railway", "name": "...", "entries": N } ],',' "entries": [ { "source": "supabase"|"vercel"|"railway", "stream": "<e.g. edge_logs / lambda>",',' "timestamp": "<ISO>", "level": "<info|warn|error|...>", "message": "<log line>",',' "raw": { /* provider payload, untouched */ } } ] }',"```","","`entries` is every source merged and sorted by `timestamp`. With a `--user` filter, `entries` and each `sources[].entries` count are post-filter."],er='`verirun scan <url> --json` prints `{ schemaVersion: 1, tool: "verirun-scan", status: "clean" | "findings" | "error", error?: { code, message }, summary: { pages, findings, byKind, ... }, pages: [...], findings: [ { kind, message, evidence?, pages: [url], count } ] }`. `kind` is one of `js_exception`, `http_error`, `request_failed`, `dead_link`, `console_error`, `missing_anchor`, `missing_skip_target`, `missing_main`. Exit 0 clean, 1 findings, 2 the scan could not run. `--no-browser` uses plain HTTP: no JavaScript runs, so no `js_exception` / `console_error`.',uo="record, run, map and scan",fo=new Set(["run","map","scan","workflow"]);function po(e,t,r){let n=e.filter(i=>r||fo.has(i.name));for(let i of n){t(),t(`**${i.usage}**: ${i.description}`);for(let c of i.options)t(`- \`${c.flags}\`: ${c.description}`)}let o=e.filter(i=>!n.includes(i));if(o.length!==0){t(),t("Flags not listed here (`verirun <command> --help` prints them):");for(let i of o)t(`- **${i.usage}**: ${i.description}`)}}var Ze=["authoring","signed-in","schemas"];function mo(e){e("## Writing a workflow yourself"),e(),e("`verirun map --json` gives every element a `selector`, a `selectorConfidence` (`high` / `medium` / `low`) and a suggested `action`: enough to author `.verirun/workflows/<name>.json` without recording."),e();for(let t of nt)e(t);e(),e("Then `verirun workflow check <name>` (`--json` for findings) validates shape, actions, selectors and params without a browser. Only `verirun run <name>` can tell you a selector matches the live page. Prefer `high` confidence; `low` is a guess. A recording is ground truth: never overwrite one with a generated file, pick a different name."),e()}function go(e){e("## Signed-in runs"),e();for(let t of ot)e(t);e()}function ho(e){e("## `run --json` (schemaVersion 1)"),e();for(let t of Zt)e(t);e(),e(`## logs.json (schemaVersion ${tt})`),e();for(let t of lo)e(t);e(),e("## Scan report"),e(),e(er),e()}var tr={authoring:mo,"signed-in":go,schemas:ho};function wo(e){let t=[];return tr[e]((r="")=>{t.push(r)}),t.join(`
|
|
10
|
+
`)}function yo(e,t,r){let n=[],o=(i="")=>{n.push(i)};if(o("# verirun: agent reference"),o(),o(Se),o(),o(`Version ${ne}. Full docs: https://get-verirun.duely.in, the bundled README.md, \`man verirun\`.`),o(),o("## This checkout"),o(),o(`- repo root: ${e.repoRoot}`),o(`- config: ${e.configPath} ${e.configExists?"(present)":"(absent)"}`),o(`- log sources: ${e.logSources.length?e.logSources.join(", "):"none configured, so `verirun run` replays but captures no backend logs"}`),e.userFilter&&o(`- user filter: ${e.userFilter}`),o(`- workflows: ${e.workflows.length?e.workflows.join(", "):"none recorded yet"}`),o(`- recent runs: ${e.recentRuns.length?e.recentRuns.join(", "):"none yet"}`),o(),o(`Account: ${uo} need a connected account (\`verirun login\`, or \`$VERIRUN_LICENSE_KEY\` in CI). \`verirun whoami\` exits 0 when they would run. Everything else works without one.`),o(),o(rt),o(),o("## Agent loop"),o(),st.forEach((i,c)=>o(`${c+1}. ${i}`)),o(),o("## Commands"),po(t,o,r),o(),o("## Files"),o(),o("`.verirun/` at the repo root: `config.json` (log sources, userFilter, testIdAttribute), `workflows/<name>.json` (one workflow each), `runs/<name>-<timestamp>/logs.json` (one directory per run; `entries[]` of `{ source, stream, timestamp, level, message, raw }`). Per user: `~/.verirun/config.json` (device token), `~/.verirun/state.json` (signed license lease)."),o(),r)for(let i of Ze)tr[i](o);else o("## More, when the task needs it"),o(),o("- `verirun agent --topic authoring`: write a workflow file yourself from `map --json` (file format, actions, selectors, dialogs)."),o("- `verirun agent --topic signed-in`: test a flow behind a login (browser profile, Windows paths)."),o("- `verirun agent --topic schemas`: full shapes of `run --json`, `logs.json` and the scan report."),o("- `verirun agent --full`: all of it in one document, for saving into `.verirun/AGENT.md` or an AGENTS.md. `--skill --install [claude|cursor|codex|all]` installs it as a skill. `--json` is the same as JSON."),o();return n.join(`
|
|
11
|
+
`)}function bo(e,t){let r=[],n=(o="")=>{r.push(o)};n("---"),n("name: verirun"),n("description: >-"),n(" Test this app end to end with verirun: map the interaction graph to find what a"),n(" page does, replay a recorded browser workflow, read the backend logs that the"),n(" replay produced, and scan a deployed site from outside. Use when asked to test a"),n(" user flow, verify a change end to end, reproduce a bug, check a live site for"),n(" errors, or find out what a page can do and which API calls it makes."),n("---"),n(),n("# verirun"),n(),n(Se),n(),n("## Decide which command you need"),n(),n("| You want to | Command | Needs a recording? |"),n("|---|---|---|"),n("| Know what a page does / which endpoints it calls | `verirun map` | no |"),n("| Know what is reachable from a page | `verirun map --bfs /route --depth 2` | no |"),n("| Re-run a user flow after a change | `verirun run <name>` | yes |"),n("| Write a test for a flow yourself | `verirun map --json` then author the workflow | no |"),n("| Validate what you wrote | `verirun workflow check <name>` | no |"),n("| Check a deployed site from outside | `verirun scan <url>` | no |"),n("| Capture a flow faithfully (incl. auth) | ask the human to run `verirun record <name>` | n/a |"),n(),n(rt),n(),n("## Static analysis first: `verirun map`"),n(),n("`map` reads the source and needs no browser, so reach for it first. It reports every page, its interactive elements, where they navigate and the backend calls each one makes. Limits: a target computed at runtime shows as `unresolved`, an endpoint passed through a variable as `?`, and Supabase `.auth.*` calls are not classified yet. `verirun map --reach` shows how much of each page it attached; check it before trusting a page that looks empty."),n(),n("## Replay: `verirun run <name>`"),n(),n("A workflow is either recorded by a human (ground truth) or written by you from the map (a guess until `run` confirms it). Recording needs a display and a human, so ask for it."),n(),st.slice(1).forEach((o,i)=>n(`${i+1}. ${o}`)),n(),n("## Writing a workflow from the map"),n(),n("`verirun map --json` gives each element a `selector`, a `selectorConfidence` (`high`/`medium`/`low`) and a suggested `action`, so you can author `.verirun/workflows/<name>.json` yourself:"),n();for(let o of nt)n(o);n(),n("Loop: write the file, `verirun workflow check <name>` (static, no browser), fix, then `verirun run <name>` to learn whether the selectors match. Prefer `high` confidence; `low` matches more than one element. Never overwrite a recorded workflow with a generated one: use a new name."),n(),n("## Signed-in runs"),n();for(let o of ot)n(o);return n(),n("## This checkout"),n(),n(`- repo root: ${e.repoRoot}`),n(`- log sources configured: ${e.logSources.length?e.logSources.join(", "):"none, so `run` replays but captures nothing"}`),n(`- workflows: ${e.workflows.length?e.workflows.join(", "):"none recorded yet"}`),n(),n(`Full reference: \`verirun agent\` (all ${t.length} commands and flags, the \`run --json\`, \`logs.json\` and scan schemas). \`verirun <command> --help\` works for any single one.`),n(),r.join(`
|
|
12
|
+
`)}function vo(e){let t=/^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(e);if(!t)return{description:"",body:e};let[,r="",n=""]=t,o=/description:\s*>-?\n([\s\S]*?)(?:\n\S|$)/.exec(r),i=/description:\s*(.+)/.exec(r);return{description:o?.[1]?o[1].split(`
|
|
13
|
+
`).map(a=>a.trim()).join(" ").trim():i?.[1]?.trim()??"",body:n}}function ko(e){let{description:t,body:r}=vo(e);return["---",`description: ${t}`,"globs:","alwaysApply: false","---","",r.replace(/^\n+/,"")].join(`
|
|
14
|
+
`)}var et="<!-- verirun:start -->",$e="<!-- verirun:end -->";function $o(){return[et,"<!-- Generated by `verirun agent --skill --install codex`. Edit outside these markers; re-running replaces only this section. -->","## verirun (end-to-end app testing)","",Se,"","When a task is about testing a user flow, verifying a change end to end, reproducing a bug or checking a live site, use the `verirun` skill (`.agents/skills/verirun/SKILL.md`). Run `verirun agent` for the full command reference, or `verirun <command> --help` for one command.","",rt,$e].join(`
|
|
15
|
+
`)}function So(e,t){let r=e.indexOf(et),n=e.indexOf($e);if(r===-1&&n===-1){if(e.trim()==="")return`${t}
|
|
16
|
+
`;let o=e.endsWith(`
|
|
17
|
+
|
|
18
|
+
`)?"":e.endsWith(`
|
|
19
|
+
`)?`
|
|
20
|
+
`:`
|
|
21
|
+
|
|
22
|
+
`;return`${e}${o}${t}
|
|
23
|
+
`}if(r===-1||n===-1||n<r)throw new Error(`AGENTS.md has a broken verirun section (expected ${et} before ${$e}). Fix or remove the markers and re-run.`);return e.slice(0,r)+t+e.slice(n+$e.length)}var it={claude:{label:"Claude Code",files:[{path:[".claude","skills","verirun","SKILL.md"],mode:"replace",render:e=>e}],note:"Claude Code loads it automatically when a task looks like app testing."},cursor:{label:"Cursor",files:[{path:[".cursor","rules","verirun.mdc"],mode:"replace",render:ko}],note:"Cursor loads it when a task matches its description. Restart Cursor, or reopen the project, to pick it up."},codex:{label:"Codex",files:[{path:[".agents","skills","verirun","SKILL.md"],mode:"replace",render:e=>e},{path:["AGENTS.md"],mode:"section",render:()=>$o()}],note:"Codex loads the skill when a task matches its description; AGENTS.md only points at it. Start a new Codex session to pick it up."}},xo=Object.keys(it);function Ro(e){if(e===!0)return["claude"];let t=String(e).toLowerCase();if(t==="all"||t==="both")return[...xo];if(Object.hasOwn(it,t))return[t];throw new Error(`Unknown --install target "${e}". Use claude, cursor, codex, all, or --install on its own for Claude Code.`)}async function Eo(e,t,r){let n=[];for(let o of t){let i=it[o],c=[];for(let a of i.files){let l=no(e,...a.path);if(await eo(ro(l),{recursive:!0}),a.mode==="section"){let d=Qe(l)?await to(l,"utf8"):"";await zt(l,So(d,a.render(r)))}else await zt(l,a.render(r));c.push(l)}n.push({target:o,label:i.label,note:i.note,files:c})}return n}async function rr(e,t){let r=await co(),n=so(e);if(t.json){process.stdout.write(`${JSON.stringify({schemaVersion:oo,about:Se,version:ne,docs:"https://get-verirun.duely.in",exitCodes:{0:"ok",1:"the check failed",2:"could not run"},checkout:r,commands:n,layout:{config:"<repo>/.verirun/config.json",workflows:"<repo>/.verirun/workflows/<name>.json",runs:"<repo>/.verirun/runs/<name>-<timestamp>/logs.json",userConfig:"~/.verirun/config.json",userState:"~/.verirun/state.json"},logsJsonSchemaVersion:tt,runSummarySchema:Zt.join(`
|
|
24
|
+
`),scanReport:er,signedIn:ot.join(`
|
|
25
|
+
`),workflowSchema:{version:1,authoring:nt.join(`
|
|
26
|
+
`),validate:"verirun workflow check <name> --json"},agentLoop:st})}
|
|
27
|
+
`);return}if(t.skill){let o=`${bo(r,n)}
|
|
28
|
+
`;if(!t.install){process.stdout.write(o);return}let{repoRoot:i}=P();for(let c of await Eo(i,Ro(t.install),o)){for(let a of c.files)s.success(`Wrote ${a} ${s.dim(`(${c.label})`)}`);s.info(s.dim(` ${c.note}`))}return}if(t.topic!==void 0){if(!Ze.includes(t.topic))throw new Error(`Unknown --topic "${t.topic}". Use one of: ${Ze.join(", ")}.`);process.stdout.write(`${wo(t.topic)}
|
|
29
|
+
`);return}process.stdout.write(`${yo(r,n,!!t.full)}
|
|
30
|
+
`)}import{spawnSync as Wo}from"node:child_process";import{existsSync as Bo,readFileSync as Go}from"node:fs";import Ho from"node:path";import{createInterface as Ko}from"node:readline/promises";var Co="https://backboard.railway.com/graphql/v2",ct="https://railway.com/account/tokens";async function nr(e,t,r){let n;try{n=await B(Co,{method:"POST",headers:{Authorization:`Bearer ${e}`,"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({query:t,variables:r})})}catch(i){return{error:i instanceof Error?i.message:String(i)}}let o=await n.json().catch(()=>{});return n.ok?o?.errors?.length?{error:o.errors.map(i=>i.message).filter(Boolean).join("; ")||"GraphQL error"}:o?.data?{data:o.data}:{error:"Railway returned no data."}:{error:o?.errors?.[0]?.message??`HTTP ${n.status}`}}async function xe(e){if(e.deploymentId)return{deploymentId:e.deploymentId};if(!e.token||!e.projectId||!e.environmentId||!e.serviceId)return{error:"set railway.deploymentId, or railway.projectId + railway.environmentId + railway.serviceId"};let r=await nr(e.token,`
|
|
15
31
|
query($input: JSON!) {
|
|
16
32
|
deployments(input: $input, first: 1) {
|
|
17
33
|
edges { node { id status createdAt } }
|
|
18
34
|
}
|
|
19
|
-
}`,{input:{projectId:e.projectId,environmentId:e.environmentId,serviceId:e.serviceId}});if("error"in
|
|
35
|
+
}`,{input:{projectId:e.projectId,environmentId:e.environmentId,serviceId:e.serviceId}});if("error"in r)return r;let n=r.data.deployments.edges[0]?.node;return n?{deploymentId:n.id}:{error:"no deployments found for that project / environment / service"}}async function lt(e,t,r,n={}){let o=n.userNeedle?.trim(),i=o?`"${o.replace(/"/g,"")}"`:void 0,a=await nr(e,`
|
|
20
36
|
query($id: String!, $start: DateTime, $end: DateTime, $filter: String) {
|
|
21
37
|
deploymentLogs(deploymentId: $id, startDate: $start, endDate: $end, filter: $filter, limit: 1000) {
|
|
22
38
|
timestamp message severity tags attributes
|
|
23
39
|
}
|
|
24
|
-
}`,{id:
|
|
25
|
-
`);
|
|
26
|
-
`).map(o=>o.trim()).filter(Boolean).map(o=>JSON.parse(o))}function Et(e){return{source:"vercel",stream:typeof e.source=="string"?e.source:void 0,timestamp:typeof e.timestamp=="number"?new Date(e.timestamp).toISOString():void 0,level:typeof e.level=="string"?e.level:void 0,message:typeof e.message=="string"?e.message:void 0,raw:e}}function Ze(e){if(e.vercel)return e.vercel.token?new q(e.vercel):new V(e.vercel)}var er=["vercel","supabase","railway"];async function rr(e,r){if(e!==void 0&&!er.includes(e))throw new Error(`Unknown log source "${e}". Use one of: ${er.join(", ")}.`);let o=y(),t=await T(o.configPath);if(e===void 0){Tt(t);return}if(e==="vercel"){await _t(t,r);return}if(e==="railway"){await Ht(t,r);return}await Wt(t,r)}function Tt(e){let r=e.vercel,o=e.supabase,t=e.railway;n.info(""),n.info(n.bold("Log sources")),n.info(""),n.info(` ${"vercel".padEnd(10)} ${r?.token&&r.projectId?`configured project ${r.projectId}`:r?`drain mode listener :${r.listenerPort??Z}${r.listenerPath??ee}`:n.dim("not configured")}`),n.info(` ${"supabase".padEnd(10)} ${o?`configured project ${o.projectRef??n.dim("(no projectRef)")}`:n.dim("not configured")}`),n.info(` ${"railway".padEnd(10)} ${t?`configured ${t.deploymentId?`deployment ${t.deploymentId}`:`service ${t.serviceId??n.dim("(none)")}`}`:n.dim("not configured")}`),n.info(""),n.info(`Run ${n.bold("verirun log vercel")}, ${n.bold("verirun log supabase")} or ${n.bold("verirun log railway")} to connect one.`)}var Nt="https://vercel.com/d?to=%2F%5Bteam%5D%2F~%2Fsettings%2Flog-drains&title=Log%20Drains";function Lt(e){let r=jt.join(e,".vercel","project.json");if(It(r))try{let o=JSON.parse(xt(r,"utf8"));return o.projectId?{projectId:o.projectId,...o.orgId?.startsWith("team_")?{teamId:o.orgId}:{}}:void 0}catch{return}}async function Pt(e){let r=await Qe(e),o;if("error"in r)n.warn(` Could not list teams: ${r.error}`);else if(r.teams.length===1)o=r.teams[0].id,n.info(` team ${r.teams[0].name??o}`);else if(r.teams.length>1){n.info(" Which team?"),n.info(` 1. ${n.dim("(personal account)")}`),r.teams.forEach((c,l)=>n.info(` ${l+2}. ${c.name??c.slug??c.id}`));let a=Number.parseInt(await b(" Number: "),10)-2;r.teams[a]&&(o=r.teams[a].id)}let t=await Xe(e,o);if("error"in t){n.error(` Could not list projects: ${t.error}`);return}if(t.projects.length===0){n.warn(" That token can see no projects.");return}if(t.projects.length===1){let a=t.projects[0];return n.info(` project ${a.name} (${a.id})`),{projectId:a.id,...o?{teamId:o}:{}}}n.info(" Which project?"),t.projects.forEach((a,c)=>n.info(` ${c+1}. ${a.name}`));let i=Number.parseInt(await b(" Number: "),10)-1,s=t.projects[i];if(!s){n.warn(" Not one of the choices.");return}return{projectId:s.id,...o?{teamId:o}:{}}}async function Ot(e){if(!e.token||!e.projectId)throw new Error("--test needs a connected project. Run `verirun log vercel` first.");let r=await X(e);if("error"in r){n.error(` ${r.error}`),process.exitCode=1;return}n.info(""),n.info(`Reading runtime logs for ${n.bold(r.label)}\u2026`);let o=await Q(e,r.deploymentId,e.readMs??8e3);if("error"in o){n.error(` ${o.error}`),process.exitCode=1;return}let t=Date.now()-5*6e4,i=o.rows.filter(s=>typeof s.timestampInMs!="number"||s.timestampInMs>=t);n.success(` ${o.rows.length} line(s) on the stream, ${i.length} from the last 5 minutes.`);for(let s of i.slice(-5)){let a=Se(s);n.info(` ${n.dim(a.timestamp??"")} ${a.message??""}`)}o.rows.length===0&&n.info(n.dim(" No lines yet \u2014 hit the deployed app once, then re-run with --test."))}async function _t(e,r){let{configPath:o,repoRoot:t}=y(),i=e.vercel??{},s=process.env.VERIRUN_VERCEL_TOKEN;if(r.drain||r.listen){await Dt(e,r);return}if(r.test){await Ot(i);return}if(i.token&&i.projectId&&!r.reconnect){n.info(""),n.info(n.bold("Vercel logs \u2014 connected")),n.info(` project ${i.projectId}`),i.teamId&&n.info(` team ${i.teamId}`),n.info(` token ${s?"from $VERIRUN_VERCEL_TOKEN":"from config.json"}`),n.info(` deployment ${i.deploymentId??n.dim(`newest ready ${i.environment??"production"}`)}`),n.info(""),n.info(n.dim("--reconnect to replace it \xB7 --test to read the last 5 minutes"));return}n.info(""),n.info(n.bold("Connect Vercel logs"));let a=s??i.token;if(!a||r.reconnect){if(!process.stdin.isTTY){n.info(""),n.info("Not an interactive shell. Set these and re-run:"),n.info(` export VERIRUN_VERCEL_TOKEN=... ${n.dim(`(create at ${z})`)}`),n.info(` ${n.dim("and vercel.projectId (+ vercel.teamId) in .verirun/config.json")}`);return}n.info(` Opening ${n.bold(z)}`),n.info(" Create a token, copy it, come back here."),N(z),n.info("");let g=await b(" Paste the token: ");if(!g){n.warn(" Nothing pasted. No changes made.");return}a=g}let c=r.reconnect?void 0:Lt(t);c&&n.info(` project ${c.projectId} ${n.dim("\u2014 from .vercel/project.json")}`);let l=c??await Pt(a);if(!l){n.warn(" No project chosen. No changes made.");return}let d={projectId:l.projectId,...l.teamId?{teamId:l.teamId}:{}};s||(d.token=a),await re(o,{vercel:d}),n.info(""),n.info(` Saved to ${n.dim(o)}`),n.info(" Checking it works\u2026");let u={...i,...d,token:a},p=await X(u);if("error"in p){n.error(` ${p.error}`),n.info(n.dim(" Token or project looks wrong. Re-run `verirun log vercel --reconnect`.")),process.exitCode=1;return}let m=await Q(u,p.deploymentId,4e3);if("error"in m){n.error(` Reading logs failed: ${m.error}`),n.info(n.dim(" The token may lack access to this project. Re-run with --reconnect.")),process.exitCode=1;return}n.success(` Connected to ${p.label}. \`verirun run\` now includes Vercel logs.`),m.rows.length===0&&n.info(n.dim(" (No log lines in the stream right now \u2014 that is normal for an idle app.)"))}async function Dt(e,r){let{configPath:o}=y(),t=e.vercel??{},i=t.listenerPort??Z,s=t.listenerPath??ee,a=t.drainGraceMs??ke;if(r.listen){await Ut({...t,listenerPort:i,listenerPath:s});return}let c=process.env.VERIRUN_VERCEL_DRAIN_SECRET;if((t.drainSecret||c)&&!r.reconnect){n.info(""),n.info(n.bold("Vercel logs \u2014 drain mode")),n.info(` endpoint http://localhost:${i}${s}`),n.info(` drain secret ${c?"from $VERIRUN_VERCEL_DRAIN_SECRET":"from config.json"}`),n.info(` grace ${a}ms`),n.info(` project ${t.project??n.dim("all projects on the drain")}`),n.info(""),n.info(n.dim("--reconnect to change it \xB7 --listen to run the verification listener")),n.info(n.dim("`verirun log vercel` (no --drain) switches to the simpler API mode"));return}if(n.info(""),n.info(n.bold("Connect Vercel logs the old way (drain) \u2014 do these in order:")),n.info(n.dim(" You probably want `verirun log vercel` instead: a token, no tunnel.")),n.info(""),n.info(" 1. New terminal. Start a tunnel to port "+i+" and copy the https URL it prints:"),n.info(` ${n.bold(`ngrok http ${i}`)}`),n.info(n.dim(` (or: cloudflared tunnel --url http://localhost:${i})`)),n.info(""),n.info(" 2. Another new terminal. Start the listener and leave it running:"),n.info(` ${n.bold("verirun log vercel --listen")}`),n.info(""),n.info(" 3. Vercel page opening now (your team \u2192 Settings \u2192 Log Drains) \u2192 Add a drain:"),n.info(" Sources ......... check "+n.bold("Edge")+" and "+n.bold("Serverless")+", uncheck the rest"),n.info(" Delivery format . "+n.bold("JSON")),n.info(` Endpoint ........ ${n.bold(`<https URL from step 1>${s}`)}`),n.info(" then Save."),n.info(""),n.info(" 4. Vercel shows a Signing Secret. Copy it, paste it below."),!process.stdin.isTTY){n.info(""),n.info(n.dim(" Then: VERIRUN_VERCEL_DRAIN_SECRET=<secret> (or vercel.drainSecret in .verirun/config.json)"));return}N(Nt),n.info("");let l=await b(" Signing secret: ");if(!l){n.warn(" No secret. Deliveries would NOT be verified \u2014 anyone who finds the tunnel URL"),n.warn(" could feed fake logs into your runs.");let p=(await b(" Save without a secret anyway? [y/N]: ")).toLowerCase();if(p!=="y"&&p!=="yes"){n.info(" Nothing saved. Re-run `verirun log vercel --drain` once you have the secret.");return}l=""}let d=await b(" Only one project's logs? Paste its name or prj_\u2026 ID, or press Enter for all: "),u={};l&&(u.drainSecret=l),d&&(u.project=d),await re(o,{vercel:u}),n.info(""),n.info(` Saved to ${n.dim(o)}`),n.success(" Done. `verirun run` starts its own listener \u2014 you only need --listen for tailing.")}async function Ut(e){let r=new V(e,{onEntry:o=>{let t=o.timestamp??new Date().toISOString(),i=(o.level??"log").padEnd(5),s=o.stream?`${o.stream} `:"";n.info(`${n.dim(t)} ${i} ${n.dim(s)}${o.message??""}`)}});await r.start(),e.project&&n.info(n.dim(`Filtering to project "${e.project}".`)),n.info(n.dim("Waiting for deliveries. Press Ctrl+C to stop.")),n.info(""),await new Promise(o=>{let t=()=>{process.off("SIGINT",t),process.off("SIGTERM",t),n.info(""),r.dispose().then(o)};process.on("SIGINT",t),process.on("SIGTERM",t)}),n.step("Listener stopped.")}var Re="https://supabase.com/dashboard/account/tokens";async function b(e){if(!process.stdin.isTTY)return"";let r=At({input:process.stdin,output:process.stdout});try{return(await r.question(e)).trim()}finally{r.close()}}async function re(e,r){await De(e,r)||n.warn(`Could not restrict ${e} to your account. It holds an API token \u2014 check its permissions.`)}function Vt(){let e=Ct("supabase",["projects","list","--output","json"],{encoding:"utf8",shell:process.platform==="win32"});if(!(e.error||e.status!==0))try{let o=JSON.parse(e.stdout).map(t=>({ref:String(t.id??t.ref??t.reference_id??""),name:String(t.name??"")})).filter(t=>t.ref!=="");return o.length>0?o:void 0}catch{return}}async function Ft(e){if(e)return e;let r=Vt();if(r&&r.length===1){let t=r[0];return n.info(` project ${t.name} (${t.ref}) ${n.dim("\u2014 from the supabase CLI")}`),t.ref}if(r&&r.length>1){n.info(" Which project?"),r.forEach((i,s)=>n.info(` ${s+1}. ${i.name} (${i.ref})`));let t=Number.parseInt(await b(" Number: "),10)-1;if(r[t])return r[t].ref;n.warn(" Not one of the choices.")}return await b(" Project ref (the <ref> in dashboard/project/<ref>): ")||void 0}async function Mt(e,r,o){if(!e.projectRef)throw new Error("--test needs a connected project. Run `verirun log supabase` first.");if(!o)throw new Error("--test needs an access token. Run `verirun log supabase`, or set $VERIRUN_SUPABASE_ACCESS_TOKEN.");let t=new Date,i={startedAt:new Date(t.getTime()-5*6e4).toISOString(),endedAt:t.toISOString()};n.info(""),n.info(`Querying ${r.length} source${r.length===1?"":"s"} for the last 5 minutes\u2026`),n.info("");let s=!1;for(let a of r){let c=await Y(e.projectRef,o,a,i);"error"in c?(s=!0,n.error(`${a.padEnd(16)} ${c.error}`)):n.success(`${a.padEnd(16)} ${c.rows.length} row${c.rows.length===1?"":"s"}`)}s&&(process.exitCode=1)}async function Wt(e,r){let{configPath:o}=y(),t=e.supabase??{},i=t.sources&&t.sources.length>0?t.sources:ve,s=process.env.VERIRUN_SUPABASE_ACCESS_TOKEN,a=s??t.accessToken;if(r.test){await Mt(t,i,a);return}if(t.projectRef&&a&&!r.reconnect){n.info(""),n.info(n.bold("Supabase logs \u2014 connected")),n.info(` project ${t.projectRef}`),n.info(` token ${s?"from $VERIRUN_SUPABASE_ACCESS_TOKEN":"from config.json"}`),n.info(` sources ${i.join(", ")}`),n.info(""),n.info(n.dim("--reconnect to replace the token \xB7 --test to query the last 5 minutes"));return}if(n.info(""),n.info(n.bold("Connect Supabase logs")),!a||r.reconnect){if(!process.stdin.isTTY){n.info(""),n.info("Not an interactive shell. Set these and re-run:"),n.info(` export VERIRUN_SUPABASE_ACCESS_TOKEN=sbp_... ${n.dim(`(create at ${Re})`)}`),n.info(` ${n.dim("and supabase.projectRef in .verirun/config.json")}`);return}n.info(` Opening ${n.bold(Re)}`),n.info(' Click "Generate new token", copy it, come back here.'),N(Re),n.info("");let m=await b(" Paste the token: ");if(!m){n.warn(" Nothing pasted. No changes made.");return}m.startsWith("sbp_")||n.warn(" That does not look like a Supabase token (expected sbp_\u2026). Saving it anyway."),a=m}let c=await Ft(r.reconnect?void 0:t.projectRef);if(!c){n.warn(" No project ref given. No changes made.");return}let l={projectRef:c};s||(l.accessToken=a),await re(o,{supabase:l}),n.info(""),n.info(` Saved to ${n.dim(o)}`),n.info(" Checking it works\u2026");let d=new Date,u={startedAt:new Date(d.getTime()-5*6e4).toISOString(),endedAt:d.toISOString()},p=await Y(c,a,i[0],u);if("error"in p){n.error(` ${i[0]} query failed: ${p.error}`),n.info(n.dim(" Token or project ref looks wrong. Re-run `verirun log supabase --reconnect`.")),process.exitCode=1;return}n.success(` Connected. \`verirun run\` now includes Supabase logs (${i.join(", ")}).`)}function tr(e){return!!(e.deploymentId||e.projectId&&e.environmentId&&e.serviceId)}function Bt(e){try{let r=new URL(e.trim());if(!/(^|\.)railway\.(com|app)$/.test(r.hostname))return;let o=r.pathname.match(/\/project\/([^/]+)\/service\/([^/?#]+)/),t=r.searchParams.get("environmentId");return!o||!o[1]||!o[2]||!t?void 0:{projectId:o[1],environmentId:t,serviceId:o[2]}}catch{return}}async function Gt(e,r){if(!r)throw new Error("--test needs a token. Run `verirun log railway`, or set $VERIRUN_RAILWAY_TOKEN.");if(!tr(e))throw new Error("--test needs a connected deployment. Run `verirun log railway` first.");let o=await K({...e,token:r});if("error"in o)throw new Error(`Railway: ${o.error}`);let t=new Date,i={startedAt:new Date(t.getTime()-5*6e4).toISOString(),endedAt:t.toISOString()};n.info(""),n.info(`Querying deployment ${o.deploymentId} for the last 5 minutes\u2026`);let s=await we(r,o.deploymentId,i);if("error"in s){n.error(`railway ${s.error}`),process.exitCode=1;return}n.success(`railway ${s.entries.length} row${s.entries.length===1?"":"s"}`)}async function Ht(e,r){let{configPath:o}=y(),t=e.railway??{},i=process.env.VERIRUN_RAILWAY_TOKEN,s=i??t.token;if(r.test){await Gt(t,s);return}if(s&&tr(t)&&!r.reconnect){n.info(""),n.info(n.bold("Railway logs \u2014 connected")),n.info(` token ${i?"from $VERIRUN_RAILWAY_TOKEN":"from config.json"}`),t.deploymentId?n.info(` deployment ${t.deploymentId} ${n.dim("(pinned)")}`):(n.info(` project ${t.projectId}`),n.info(` environment ${t.environmentId}`),n.info(` service ${t.serviceId} ${n.dim("(latest deployment resolved per run)")}`)),n.info(""),n.info(n.dim("--reconnect to change it \xB7 --test to query the last 5 minutes"));return}if(n.info(""),n.info(n.bold("Connect Railway logs \u2014 2 things:")),n.info(""),n.info(` 1. On the page opening now (${he}):`),n.info(' "Create Token" \u2192 name it \u2192 pick your team \u2192 Create \u2192 copy the value.'),n.info(" 2. In Railway, open the service you want logs from. Copy its URL from the"),n.info(" address bar \u2014 looks like:"),n.info(" https://railway.com/project/PROJECT/service/SERVICE?environmentId=ENV"),!process.stdin.isTTY){n.info(""),n.info(n.dim("Then: VERIRUN_RAILWAY_TOKEN=<token>, and railway.projectId / environmentId / serviceId (from that URL) in .verirun/config.json."));return}if(!s||r.reconnect){N(he),n.info("");let d=await b(" Railway token: ");if(!d){n.warn(" Nothing pasted. No changes made.");return}s=d}let a={};i||(a.token=s);let c=Bt(await b(" Service URL (paste the whole thing): "));if(c)a.projectId=c.projectId,a.environmentId=c.environmentId,a.serviceId=c.serviceId;else{n.warn(" Couldn't read that URL. Enter the three IDs from it by hand:");let d=await b(" Project ID: "),u=await b(" Environment ID: "),p=await b(" Service ID: ");if(!d||!u||!p){n.warn(" Need all three. No changes made.");return}a.projectId=d,a.environmentId=u,a.serviceId=p}await re(o,{railway:a}),n.info(""),n.info(` Saved to ${n.dim(o)}`),n.info(" Checking it works\u2026");let l=await K({...t,...a,token:s});if("error"in l){n.error(` ${l.error}`),n.info(n.dim(" Re-run `verirun log railway --reconnect` with the right token / IDs.")),process.exitCode=1;return}n.success(` Connected. Deployment ${l.deploymentId}. \`verirun run\` now includes Railway logs.`)}import{existsSync as or,readFileSync as ir}from"node:fs";import{mkdir as Jt,readdir as Kt,writeFile as nr}from"node:fs/promises";import k from"node:path";var Yt=new Set(["node_modules",".git",".next",".verirun","dist","build","out","coverage",".turbo",".vercel"]);async function qt(e){let r=[],o=async t=>{for(let i of await Kt(t,{withFileTypes:!0})){if(i.name.startsWith(".")&&i.name!==".")continue;let s=k.join(t,i.name);if(i.isDirectory()){if(Yt.has(i.name))continue;await o(s)}else/\.tsx?$/.test(i.name)&&!i.name.endsWith(".d.ts")&&r.push({file:k.relative(e,s).split(k.sep).join("/"),source:ir(s,"utf8")})}};return await o(e),r}function zt(e){if(or(e))try{let r=ir(e,"utf8").replace(/\/\*[\s\S]*?\*\//g,"").replace(/(^|[^:])\/\/.*$/gm,"$1").replace(/,(\s*[}\]])/g,"$1");return JSON.parse(r)}catch{return}}function sr(e,r=0){if(r>5)return{};let o=zt(e);if(!o)return{};let t=o.compilerOptions??{},i=o.extends;if(typeof i!="string"||!i.startsWith("."))return t;let s=k.resolve(k.dirname(e),i),a=sr(/\.json$/.test(s)?s:`${s}.json`,r+1);return{...a,...t,paths:t.paths??a.paths}}function Xt(e){let{baseUrl:r,paths:o}=sr(k.join(e,"tsconfig.json")),t=(r??"").replace(/^\.\/?/,"").replace(/\/$/,""),i={};for(let[s,a]of Object.entries(o??{})){let c=a[0];if(!s.endsWith("/*")||!c?.endsWith("*"))continue;let l=c.slice(0,-1).replace(/\/$/,"").replace(/^\.\//,"");i[s.slice(0,-1)]=[t,l].filter(Boolean).join("/")}return i}function Qt(e,r){let o={...r};for(let t of e??[]){let[i,s]=t.split("=");i&&(o[i]=s??"")}return o}var Zt=/(?:from|import)\s*\(?\s*['"]([^'"]+)['"]/g;function en(e,r){let o=new Set(e.map(i=>i.file)),t=new Map;for(let i of e)for(let[,s]of i.source.matchAll(Zt)){if(!s||s.startsWith(".")||Object.keys(r).some(d=>s.startsWith(d)))continue;let a=s.indexOf("/");if(a<=0)continue;let c=`${s.slice(0,a)}/`;if(t.has(c))continue;let l=s.slice(a+1);for(let d of o){let u=d.replace(/\.tsx?$/,"").replace(/\/index$/,"");if(u===l||!u.endsWith(`/${l}`)){if(u===l)t.set(c,"");else continue;break}t.set(c,u.slice(0,u.length-l.length-1));break}}return[...t].map(([i,s])=>`--alias ${i}=${s}`)}function rn(e,r,o){let t=e.nodes.pages.filter(s=>!s.inferred);n.info(`${n.bold(String(t.length))} pages \xB7 ${n.bold(String(e.nodes.leaves.length))} backend effects \xB7 ${n.bold(String(e.edges.length))} edges ${n.dim(`(${r.length} files)`)}`),n.info("");for(let s of e.nodes.pages){let a=e.edges.filter(m=>m.from===s.id),c=a.filter(m=>m.kind==="nav").length,l=a.filter(m=>m.kind==="effect").length,d=new Set([...s.elements,...a.map(m=>m.trigger)].filter(m=>m.dialogs&&m.dialogs.length>0).map(m=>m.id)).size,u=d>0?`, ${d} opening a dialog`:"",p=s.inferred?n.dim(" (inferred)"):"";n.info(` ${s.path}${p} ${n.dim(`\u2014 ${s.component}, ${s.elements.length} element(s), ${c} nav, ${l} effect${u}`)}`)}let i=e.diagnostics;i.parseErrors.length>0&&n.warn(`${i.parseErrors.length} file(s) failed to parse`);for(let s of o)n.info(""),n.warn(`Imports like ${n.bold(s.split(" ")[1]?.split("=")[0]??"")} could not be resolved, so components they point at were not analysed. Re-run with:`),n.info(` verirun map ${s}`);i.unresolvedTargets.length>0&&(n.info(""),n.info(n.dim(`${i.unresolvedTargets.length} nav target(s) could not be resolved to a page`)))}async function ar(e){let{assertLicensed:r}=await import("./gate.mjs"),{assembleTree:o,bfs:t,buildCoverage:i,measureReach:s,renderBfs:a,renderMarkdown:c,renderReachTable:l}=await import("./graph.mjs"),d=await r();d.note&&n.warn(d.note);let u=y(),p=e.dir?k.resolve(e.dir):u.repoRoot;if(!or(p))throw new Error(`no such directory: ${p}`);let m=await qt(p);if(m.length===0)throw new Error(`no .ts/.tsx files found under ${p}`);let g=Qt(e.alias,Xt(p)),v={aliases:g,...e.followChildren===!1?{followChildren:!1}:{},...e.appFile?{appFile:e.appFile}:{}};if(e.bfs){let f=o(m,v),h=e.kind==="nav"||e.kind==="effect"?[e.kind]:void 0,$=t(f,e.bfs,{depth:e.depth?Number(e.depth):2,...h?{kinds:h}:{}});n.info(n.dim(`${$.order.length-1} node(s) within ${$.maxDepth} step(s) of ${$.from}`)),n.info(a($));return}if(e.reach){let f=s(m,v);n.info(l(f));for(let h of f.unreachedFiles)n.info(n.dim(`unreached: ${h.file} (${h.elements} elements)`));return}let w=o(m,v);if(e.json){process.stdout.write(`${JSON.stringify(w,null,2)}
|
|
27
|
-
`);return}if(e.
|
|
28
|
-
`),await
|
|
29
|
-
`);let
|
|
30
|
-
`).filter(
|
|
31
|
-
`).map(
|
|
40
|
+
}`,{id:t,start:r.startedAt,end:r.endedAt,filter:i});return"error"in a?a:{entries:Array.isArray(a.data.deploymentLogs)?a.data.deploymentLogs:[]}}function Io(e){let t=typeof e.severity=="string"?e.severity:void 0;return{source:"railway",stream:t?t.toLowerCase():"deploy",timestamp:typeof e.timestamp=="string"?e.timestamp:void 0,level:t,message:typeof e.message=="string"?e.message:void 0,raw:e}}var at=class{id="railway";name="Railway";cfg;constructor(t){this.cfg=t}async start(){if(!this.cfg.token)throw new Error("Railway log capture needs an API token. Set VERIRUN_RAILWAY_TOKEN or railway.token in .verirun/config.json.");if(!this.cfg.deploymentId&&!(this.cfg.projectId&&this.cfg.environmentId&&this.cfg.serviceId))throw new Error("Railway log capture needs railway.deploymentId, or railway.projectId + railway.environmentId + railway.serviceId, in .verirun/config.json.")}async collect(t,r){let n=await xe(this.cfg);if("error"in n)return s.warn(`Railway: ${n.error}`),[];let o=await lt(this.cfg.token,n.deploymentId,r,{userNeedle:t.userFilter});if("error"in o)return s.warn(`Railway: ${o.error}`),[];let i=o.entries.map(Io);return i.sort((c,a)=>(c.timestamp??"").localeCompare(a.timestamp??"")),i}async dispose(){}};function or(e){return e.railway?new at(e.railway):void 0}var Ao="https://api.supabase.com",ut=["postgres_logs","edge_logs","auth_logs"],dt=class{id="supabase";name="Supabase";projectRef;accessToken;sources;constructor(t){this.projectRef=t.projectRef,this.accessToken=t.accessToken,this.sources=t.sources&&t.sources.length>0?t.sources:ut}async start(){if(!this.projectRef)throw new Error("Supabase log capture needs supabase.projectRef in .verirun/config.json.");if(!this.accessToken)throw new Error("Supabase log capture needs an access token. Set VERIRUN_SUPABASE_ACCESS_TOKEN or supabase.accessToken in .verirun/config.json.")}async collect(t,r){let n=t.userFilter,o=[],i=!1;for(let c of this.sources){let a=await Re(this.projectRef,this.accessToken,c,r,{userNeedle:n});if("error"in a){s.warn(`Supabase ${c}: ${a.error}`);continue}n&&!a.serverFiltered&&(i=!0);for(let l of a.rows)o.push(To(c,l))}return n&&i&&s.info(s.dim("Supabase: user filter applied client-side for a source (more rows pulled).")),o.sort((c,a)=>(c.timestamp??"").localeCompare(a.timestamp??"")),o}async dispose(){}},Lo=/^[A-Za-z0-9._@%+-]{1,256}$/;async function sr(e,t,r,n){let o=new URL(`/v1/projects/${e}/analytics/endpoints/logs.all`,Ao);o.searchParams.set("sql",r),o.searchParams.set("iso_timestamp_start",n.startedAt),o.searchParams.set("iso_timestamp_end",n.endedAt);let i;try{i=await B(o,{headers:{Authorization:`Bearer ${t}`,Accept:"application/json"}})}catch(a){return{error:a instanceof Error?a.message:String(a)}}let c=await i.json().catch(()=>{});if(!i.ok){let a=(typeof c?.error=="string"?c.error:c?.error?.message)??c?.message??`HTTP ${i.status}`;return{error:`${a}${a.startsWith("HTTP")?"":` (HTTP ${i.status})`}`}}return{rows:Array.isArray(c?.result)?c.result:[]}}async function Re(e,t,r,n,o={}){let i=o.userNeedle?.trim().toLowerCase(),c=i&&Lo.test(i)?i:void 0,a=u=>{let m=u&&c?` where strpos(lower(event_message), '${c}') > 0 or strpos(lower(to_json_string(metadata)), '${c}') > 0`:"";return`select id, timestamp, event_message, metadata from ${r}${m} order by timestamp asc limit 1000`},l=await sr(e,t,a(!0),n);if(!("error"in l))return{rows:l.rows,serverFiltered:!!c};if(!c)return l;let d=await sr(e,t,a(!1),n);return"error"in d?d:{rows:d.rows,serverFiltered:!1}}function To(e,t){return{source:"supabase",stream:e,timestamp:_o(t.timestamp),message:typeof t.event_message=="string"?t.event_message:void 0,raw:t}}function _o(e){if(typeof e=="string")return e;if(typeof e=="number"&&Number.isFinite(e)){let t=e>1e14?e/1e3:e;return new Date(t).toISOString()}}function ir(e){return e.supabase?new dt(e.supabase):void 0}import{createHmac as Po,timingSafeEqual as Do}from"node:crypto";import{createServer as Uo}from"node:http";import{setTimeout as Mo}from"node:timers/promises";var ar="https://api.vercel.com",Ce="https://vercel.com/account/settings/tokens",jo=8e3,No=1500,Oo=3e3;function ft(e){return e?`&teamId=${encodeURIComponent(e)}`:""}async function pt(e,t){let r;try{r=await B(`${ar}${e}`,{headers:{Authorization:`Bearer ${t}`,Accept:"application/json"}})}catch(o){return{error:o instanceof Error?o.message:String(o)}}let n=await r.json().catch(()=>{});return r.ok?{data:n}:{error:n?.error?.message??`HTTP ${r.status}`}}async function cr(e,t){let r=await pt(`/v9/projects?limit=100${ft(t)}`,e);return"error"in r?r:{projects:r.data.projects??[]}}async function lr(e){let t=await pt("/v2/teams?limit=100",e);return"error"in t?t:{teams:t.data.teams??[]}}async function Ie(e){if(e.deploymentId)return{deploymentId:e.deploymentId,label:e.deploymentId};if(!e.token)return{error:"no token"};if(!e.projectId)return{error:"no projectId"};let t=e.environment??"production",r=await pt(`/v6/deployments?projectId=${encodeURIComponent(e.projectId)}&limit=1&state=READY&target=${encodeURIComponent(t)}${ft(e.teamId)}`,e.token);if("error"in r)return{error:r.error};let n=r.data.deployments?.[0];return n?{deploymentId:n.uid,label:n.url??n.uid}:{error:`no READY ${t} deployment found for this project`}}async function Ae(e,t,r){let n=`/v1/projects/${encodeURIComponent(e.projectId)}/deployments/${encodeURIComponent(t)}/runtime-logs?limit=1000${ft(e.teamId)}`,o=new AbortController,i=setTimeout(()=>o.abort(),r),c,a=()=>{c&&clearTimeout(c),c=setTimeout(()=>o.abort(),No)},l=[];try{let d=await B(`${ar}${n}`,{headers:{Authorization:`Bearer ${e.token}`,Accept:"application/x-ndjson, application/json"},signal:o.signal});if(!d.ok)return{error:(await d.json().catch(()=>{}))?.error?.message??`HTTP ${d.status}`};if(!d.body)return{rows:l};a();let u=new TextDecoder,m="";for await(let p of d.body){m+=u.decode(p,{stream:!0});let f=m.split(`
|
|
41
|
+
`);m=f.pop()??"";for(let v of f){let b=v.trim();if(b)try{let h=JSON.parse(b);Array.isArray(h)?l.push(...h):l.push(h)}catch{}}a()}}catch(d){if(!(d instanceof Error&&d.name==="AbortError"))return{error:d instanceof Error?d.message:String(d)}}finally{clearTimeout(i),c&&clearTimeout(c)}return{rows:l}}function mt(e){let t=e.requestPath?` ${e.requestMethod??""} ${e.requestPath}`.trim():"",r=e.responseStatusCode?` (${e.responseStatusCode})`:"";return{source:"vercel",...e.source?{stream:e.source}:{},...e.timestampInMs?{timestamp:new Date(e.timestampInMs).toISOString()}:{},...e.level?{level:e.level}:{},message:e.message??`${t}${r}`.trim(),raw:e}}var Ee=class{id="vercel";name="Vercel";config;constructor(t){this.config=t}async start(){if(!this.config.token)throw new Error("Vercel log capture needs an API token. Run `verirun log vercel`, or set VERIRUN_VERCEL_TOKEN / vercel.token in .verirun/config.json.");if(!this.config.projectId)throw new Error("Vercel log capture needs vercel.projectId. Run `verirun log vercel`.")}async collect(t,r){let n=await Ie(this.config);if("error"in n)return s.warn(`Vercel: ${n.error}`),[];let o=await Ae(this.config,n.deploymentId,this.config.readMs??jo);if("error"in o)return s.warn(`Vercel: ${o.error}`),[];let i=Date.parse(r.startedAt),c=Date.parse(r.endedAt)+(this.config.tailGraceMs??Oo);return o.rows.filter(a=>typeof a.timestampInMs!="number"?!0:a.timestampInMs>=i&&a.timestampInMs<=c).map(mt)}async dispose(){}};var Le=4319,Te="/drain",gt=3e3,de=class{id="vercel";name="Vercel";port;path;secret;graceMs;project;onEntry;server;received=[];droppedByProject=0;constructor(t,r={}){this.port=t.listenerPort??Le,this.path=t.listenerPath??Te,this.secret=t.drainSecret,this.graceMs=t.drainGraceMs??gt,this.project=t.project?.trim()||void 0,this.onEntry=r.onEntry}projectMatches(t){return this.project?t.projectId===this.project||t.projectName===this.project:!0}async start(){let t=Uo((r,n)=>this.handle(r,n));this.server=t,await new Promise((r,n)=>{let o=i=>{n(i.code==="EADDRINUSE"?new Error(`Vercel log listener: port ${this.port} is already in use.`):i)};t.once("error",o),t.listen(this.port,()=>{t.off("error",o),r()})}),s.step(`Vercel log listener on http://localhost:${this.port}${this.path}`)}async collect(){return this.graceMs>0&&await Mo(this.graceMs),this.received.length===0&&this.droppedByProject>0&&s.warn(`Vercel: ${this.droppedByProject} log line(s) arrived but none matched project "${this.project}". Check the name/ID in .verirun/config.json (vercel.project).`),[...this.received]}async dispose(){let t=this.server;t&&(this.server=void 0,await new Promise(r=>t.close(()=>r())))}handle(t,r){let n=t.headers["x-vercel-verify"];if(typeof n=="string"&&r.setHeader("x-vercel-verify",n),t.method!=="POST"||!this.matchesPath(t.url)){r.statusCode=t.method==="POST"?404:200,r.end();return}let o=[];t.on("data",i=>o.push(i)),t.on("error",()=>{r.statusCode=400,r.end()}),t.on("end",()=>{let i=Buffer.concat(o);if(!this.verifySignature(t,i)){s.warn("Vercel log drain: rejected a delivery with a bad signature."),r.statusCode=401,r.end();return}try{for(let c of Fo(i.toString("utf8"))){if(!this.projectMatches(c)){this.droppedByProject+=1;continue}let a=Vo(c);this.received.push(a),this.onEntry?.(a)}}catch{s.warn("Vercel log drain: could not parse a delivery body.")}r.statusCode=200,r.end()})}matchesPath(t){return t?(t.split("?")[0]??"")===this.path:!1}verifySignature(t,r){if(!this.secret)return!0;let n=t.headers["x-vercel-signature"];if(typeof n!="string")return!1;let o=Po("sha1",this.secret).update(r).digest("hex"),i=Buffer.from(o),c=Buffer.from(n);return i.length===c.length&&Do(i,c)}};function Fo(e){let t=e.trim();if(!t)return[];if(t.startsWith("[")){let r=JSON.parse(t);return Array.isArray(r)?r:[]}return t.split(`
|
|
42
|
+
`).map(r=>r.trim()).filter(Boolean).map(r=>JSON.parse(r))}function Vo(e){return{source:"vercel",stream:typeof e.source=="string"?e.source:void 0,timestamp:typeof e.timestamp=="number"?new Date(e.timestamp).toISOString():void 0,level:typeof e.level=="string"?e.level:void 0,message:typeof e.message=="string"?e.message:void 0,raw:e}}function dr(e){if(e.vercel)return e.vercel.token?new Ee(e.vercel):new de(e.vercel)}var ur=["vercel","supabase","railway"];async function fr(e,t){if(e!==void 0&&!ur.includes(e))throw new Error(`Unknown log source "${e}". Use one of: ${ur.join(", ")}.`);let r=P(),n=await Q(r.configPath);if(e===void 0){qo(n);return}if(e==="vercel"){await Qo(n,t);return}if(e==="railway"){await as(n,t);return}await os(n,t)}function qo(e){let t=e.vercel,r=e.supabase,n=e.railway;s.info(""),s.info(s.bold("Log sources")),s.info(""),s.info(` ${"vercel".padEnd(10)} ${t?.token&&t.projectId?`configured project ${t.projectId}`:t?`drain mode listener :${t.listenerPort??Le}${t.listenerPath??Te}`:s.dim("not configured")}`),s.info(` ${"supabase".padEnd(10)} ${r?`configured project ${r.projectRef??s.dim("(no projectRef)")}`:s.dim("not configured")}`),s.info(` ${"railway".padEnd(10)} ${n?`configured ${n.deploymentId?`deployment ${n.deploymentId}`:`service ${n.serviceId??s.dim("(none)")}`}`:s.dim("not configured")}`),s.info(""),s.info(`Run ${s.bold("verirun log vercel")}, ${s.bold("verirun log supabase")} or ${s.bold("verirun log railway")} to connect one.`)}var Jo="https://vercel.com/d?to=%2F%5Bteam%5D%2F~%2Fsettings%2Flog-drains&title=Log%20Drains";function Yo(e){let t=Ho.join(e,".vercel","project.json");if(Bo(t))try{let r=JSON.parse(Go(t,"utf8"));return r.projectId?{projectId:r.projectId,...r.orgId?.startsWith("team_")?{teamId:r.orgId}:{}}:void 0}catch{return}}async function Xo(e){let t=await lr(e),r;if("error"in t)s.warn(` Could not list teams: ${t.error}`);else if(t.teams.length===1)r=t.teams[0].id,s.info(` team ${t.teams[0].name??r}`);else if(t.teams.length>1){s.info(" Which team?"),s.info(` 1. ${s.dim("(personal account)")}`),t.teams.forEach((a,l)=>s.info(` ${l+2}. ${a.name??a.slug??a.id}`));let c=Number.parseInt(await V(" Number: "),10)-2;t.teams[c]&&(r=t.teams[c].id)}let n=await cr(e,r);if("error"in n){s.error(` Could not list projects: ${n.error}`);return}if(n.projects.length===0){s.warn(" That token can see no projects.");return}if(n.projects.length===1){let c=n.projects[0];return s.info(` project ${c.name} (${c.id})`),{projectId:c.id,...r?{teamId:r}:{}}}s.info(" Which project?"),n.projects.forEach((c,a)=>s.info(` ${a+1}. ${c.name}`));let o=Number.parseInt(await V(" Number: "),10)-1,i=n.projects[o];if(!i){s.warn(" Not one of the choices.");return}return{projectId:i.id,...r?{teamId:r}:{}}}async function zo(e){if(!e.token||!e.projectId)throw new Error("--test needs a connected project. Run `verirun log vercel` first.");let t=await Ie(e);if("error"in t){s.error(` ${t.error}`),process.exitCode=1;return}s.info(""),s.info(`Reading runtime logs for ${s.bold(t.label)}\u2026`);let r=await Ae(e,t.deploymentId,e.readMs??8e3);if("error"in r){s.error(` ${r.error}`),process.exitCode=1;return}let n=Date.now()-5*6e4,o=r.rows.filter(i=>typeof i.timestampInMs!="number"||i.timestampInMs>=n);s.success(` ${r.rows.length} line(s) on the stream, ${o.length} from the last 5 minutes.`);for(let i of o.slice(-5)){let c=mt(i);s.info(` ${s.dim(c.timestamp??"")} ${c.message??""}`)}r.rows.length===0&&s.info(s.dim(" No lines yet \u2014 hit the deployed app once, then re-run with --test."))}async function Qo(e,t){let{configPath:r,repoRoot:n}=P(),o=e.vercel??{},i=process.env.VERIRUN_VERCEL_TOKEN;if(t.drain||t.listen){await Zo(e,t);return}if(t.test){await zo(o);return}if(o.token&&o.projectId&&!t.reconnect){s.info(""),s.info(s.bold("Vercel logs \u2014 connected")),s.info(` project ${o.projectId}`),o.teamId&&s.info(` team ${o.teamId}`),s.info(` token ${i?"from $VERIRUN_VERCEL_TOKEN":"from config.json"}`),s.info(` deployment ${o.deploymentId??s.dim(`newest ready ${o.environment??"production"}`)}`),s.info(""),s.info(s.dim("--reconnect to replace it \xB7 --test to read the last 5 minutes"));return}s.info(""),s.info(s.bold("Connect Vercel logs"));let c=i??o.token;if(!c||t.reconnect){if(!process.stdin.isTTY){s.info(""),s.info("Not an interactive shell. Set these and re-run:"),s.info(` export VERIRUN_VERCEL_TOKEN=... ${s.dim(`(create at ${Ce})`)}`),s.info(` ${s.dim("and vercel.projectId (+ vercel.teamId) in .verirun/config.json")}`);return}s.info(` Opening ${s.bold(Ce)}`),s.info(" Create a token, copy it, come back here."),re(Ce),s.info("");let f=await V(" Paste the token: ");if(!f){s.warn(" Nothing pasted. No changes made.");return}c=f}let a=t.reconnect?void 0:Yo(n);a&&s.info(` project ${a.projectId} ${s.dim("\u2014 from .vercel/project.json")}`);let l=a??await Xo(c);if(!l){s.warn(" No project chosen. No changes made.");return}let d={projectId:l.projectId,...l.teamId?{teamId:l.teamId}:{}};i||(d.token=c),await _e(r,{vercel:d}),s.info(""),s.info(` Saved to ${s.dim(r)}`),s.info(" Checking it works\u2026");let u={...o,...d,token:c},m=await Ie(u);if("error"in m){s.error(` ${m.error}`),s.info(s.dim(" Token or project looks wrong. Re-run `verirun log vercel --reconnect`.")),process.exitCode=1;return}let p=await Ae(u,m.deploymentId,4e3);if("error"in p){s.error(` Reading logs failed: ${p.error}`),s.info(s.dim(" The token may lack access to this project. Re-run with --reconnect.")),process.exitCode=1;return}s.success(` Connected to ${m.label}. \`verirun run\` now includes Vercel logs.`),p.rows.length===0&&s.info(s.dim(" (No log lines in the stream right now \u2014 that is normal for an idle app.)"))}async function Zo(e,t){let{configPath:r}=P(),n=e.vercel??{},o=n.listenerPort??Le,i=n.listenerPath??Te,c=n.drainGraceMs??gt;if(t.listen){await es({...n,listenerPort:o,listenerPath:i});return}let a=process.env.VERIRUN_VERCEL_DRAIN_SECRET;if((n.drainSecret||a)&&!t.reconnect){s.info(""),s.info(s.bold("Vercel logs \u2014 drain mode")),s.info(` endpoint http://localhost:${o}${i}`),s.info(` drain secret ${a?"from $VERIRUN_VERCEL_DRAIN_SECRET":"from config.json"}`),s.info(` grace ${c}ms`),s.info(` project ${n.project??s.dim("all projects on the drain")}`),s.info(""),s.info(s.dim("--reconnect to change it \xB7 --listen to run the verification listener")),s.info(s.dim("`verirun log vercel` (no --drain) switches to the simpler API mode"));return}if(s.info(""),s.info(s.bold("Connect Vercel logs the old way (drain) \u2014 do these in order:")),s.info(s.dim(" You probably want `verirun log vercel` instead: a token, no tunnel.")),s.info(""),s.info(" 1. New terminal. Start a tunnel to port "+o+" and copy the https URL it prints:"),s.info(` ${s.bold(`ngrok http ${o}`)}`),s.info(s.dim(` (or: cloudflared tunnel --url http://localhost:${o})`)),s.info(""),s.info(" 2. Another new terminal. Start the listener and leave it running:"),s.info(` ${s.bold("verirun log vercel --listen")}`),s.info(""),s.info(" 3. Vercel page opening now (your team \u2192 Settings \u2192 Log Drains) \u2192 Add a drain:"),s.info(" Sources ......... check "+s.bold("Edge")+" and "+s.bold("Serverless")+", uncheck the rest"),s.info(" Delivery format . "+s.bold("JSON")),s.info(` Endpoint ........ ${s.bold(`<https URL from step 1>${i}`)}`),s.info(" then Save."),s.info(""),s.info(" 4. Vercel shows a Signing Secret. Copy it, paste it below."),!process.stdin.isTTY){s.info(""),s.info(s.dim(" Then: VERIRUN_VERCEL_DRAIN_SECRET=<secret> (or vercel.drainSecret in .verirun/config.json)"));return}re(Jo),s.info("");let l=await V(" Signing secret: ");if(!l){s.warn(" No secret. Deliveries would NOT be verified \u2014 anyone who finds the tunnel URL"),s.warn(" could feed fake logs into your runs.");let m=(await V(" Save without a secret anyway? [y/N]: ")).toLowerCase();if(m!=="y"&&m!=="yes"){s.info(" Nothing saved. Re-run `verirun log vercel --drain` once you have the secret.");return}l=""}let d=await V(" Only one project's logs? Paste its name or prj_\u2026 ID, or press Enter for all: "),u={};l&&(u.drainSecret=l),d&&(u.project=d),await _e(r,{vercel:u}),s.info(""),s.info(` Saved to ${s.dim(r)}`),s.success(" Done. `verirun run` starts its own listener \u2014 you only need --listen for tailing.")}async function es(e){let t=new de(e,{onEntry:r=>{let n=r.timestamp??new Date().toISOString(),o=(r.level??"log").padEnd(5),i=r.stream?`${r.stream} `:"";s.info(`${s.dim(n)} ${o} ${s.dim(i)}${r.message??""}`)}});await t.start(),e.project&&s.info(s.dim(`Filtering to project "${e.project}".`)),s.info(s.dim("Waiting for deliveries. Press Ctrl+C to stop.")),s.info(""),await new Promise(r=>{let n=()=>{process.off("SIGINT",n),process.off("SIGTERM",n),s.info(""),t.dispose().then(r)};process.on("SIGINT",n),process.on("SIGTERM",n)}),s.step("Listener stopped.")}var ht="https://supabase.com/dashboard/account/tokens";async function V(e){if(!process.stdin.isTTY)return"";let t=Ko({input:process.stdin,output:process.stdout});try{return(await t.question(e)).trim()}finally{t.close()}}async function _e(e,t){await qt(e,t)||s.warn(`Could not restrict ${e} to your account. It holds an API token \u2014 check its permissions.`)}function ts(){let e=Wo("supabase",["projects","list","--output","json"],{encoding:"utf8",shell:process.platform==="win32"});if(!(e.error||e.status!==0))try{let r=JSON.parse(e.stdout).map(n=>({ref:String(n.id??n.ref??n.reference_id??""),name:String(n.name??"")})).filter(n=>n.ref!=="");return r.length>0?r:void 0}catch{return}}async function rs(e){if(e)return e;let t=ts();if(t&&t.length===1){let n=t[0];return s.info(` project ${n.name} (${n.ref}) ${s.dim("\u2014 from the supabase CLI")}`),n.ref}if(t&&t.length>1){s.info(" Which project?"),t.forEach((o,i)=>s.info(` ${i+1}. ${o.name} (${o.ref})`));let n=Number.parseInt(await V(" Number: "),10)-1;if(t[n])return t[n].ref;s.warn(" Not one of the choices.")}return await V(" Project ref (the <ref> in dashboard/project/<ref>): ")||void 0}async function ns(e,t,r){if(!e.projectRef)throw new Error("--test needs a connected project. Run `verirun log supabase` first.");if(!r)throw new Error("--test needs an access token. Run `verirun log supabase`, or set $VERIRUN_SUPABASE_ACCESS_TOKEN.");let n=new Date,o={startedAt:new Date(n.getTime()-5*6e4).toISOString(),endedAt:n.toISOString()};s.info(""),s.info(`Querying ${t.length} source${t.length===1?"":"s"} for the last 5 minutes\u2026`),s.info("");let i=!1;for(let c of t){let a=await Re(e.projectRef,r,c,o);"error"in a?(i=!0,s.error(`${c.padEnd(16)} ${a.error}`)):s.success(`${c.padEnd(16)} ${a.rows.length} row${a.rows.length===1?"":"s"}`)}i&&(process.exitCode=1)}async function os(e,t){let{configPath:r}=P(),n=e.supabase??{},o=n.sources&&n.sources.length>0?n.sources:ut,i=process.env.VERIRUN_SUPABASE_ACCESS_TOKEN,c=i??n.accessToken;if(t.test){await ns(n,o,c);return}if(n.projectRef&&c&&!t.reconnect){s.info(""),s.info(s.bold("Supabase logs \u2014 connected")),s.info(` project ${n.projectRef}`),s.info(` token ${i?"from $VERIRUN_SUPABASE_ACCESS_TOKEN":"from config.json"}`),s.info(` sources ${o.join(", ")}`),s.info(""),s.info(s.dim("--reconnect to replace the token \xB7 --test to query the last 5 minutes"));return}if(s.info(""),s.info(s.bold("Connect Supabase logs")),!c||t.reconnect){if(!process.stdin.isTTY){s.info(""),s.info("Not an interactive shell. Set these and re-run:"),s.info(` export VERIRUN_SUPABASE_ACCESS_TOKEN=sbp_... ${s.dim(`(create at ${ht})`)}`),s.info(` ${s.dim("and supabase.projectRef in .verirun/config.json")}`);return}s.info(` Opening ${s.bold(ht)}`),s.info(' Click "Generate new token", copy it, come back here.'),re(ht),s.info("");let p=await V(" Paste the token: ");if(!p){s.warn(" Nothing pasted. No changes made.");return}p.startsWith("sbp_")||s.warn(" That does not look like a Supabase token (expected sbp_\u2026). Saving it anyway."),c=p}let a=await rs(t.reconnect?void 0:n.projectRef);if(!a){s.warn(" No project ref given. No changes made.");return}let l={projectRef:a};i||(l.accessToken=c),await _e(r,{supabase:l}),s.info(""),s.info(` Saved to ${s.dim(r)}`),s.info(" Checking it works\u2026");let d=new Date,u={startedAt:new Date(d.getTime()-5*6e4).toISOString(),endedAt:d.toISOString()},m=await Re(a,c,o[0],u);if("error"in m){s.error(` ${o[0]} query failed: ${m.error}`),s.info(s.dim(" Token or project ref looks wrong. Re-run `verirun log supabase --reconnect`.")),process.exitCode=1;return}s.success(` Connected. \`verirun run\` now includes Supabase logs (${o.join(", ")}).`)}function pr(e){return!!(e.deploymentId||e.projectId&&e.environmentId&&e.serviceId)}function ss(e){try{let t=new URL(e.trim());if(!/(^|\.)railway\.(com|app)$/.test(t.hostname))return;let r=t.pathname.match(/\/project\/([^/]+)\/service\/([^/?#]+)/),n=t.searchParams.get("environmentId");return!r||!r[1]||!r[2]||!n?void 0:{projectId:r[1],environmentId:n,serviceId:r[2]}}catch{return}}async function is(e,t){if(!t)throw new Error("--test needs a token. Run `verirun log railway`, or set $VERIRUN_RAILWAY_TOKEN.");if(!pr(e))throw new Error("--test needs a connected deployment. Run `verirun log railway` first.");let r=await xe({...e,token:t});if("error"in r)throw new Error(`Railway: ${r.error}`);let n=new Date,o={startedAt:new Date(n.getTime()-5*6e4).toISOString(),endedAt:n.toISOString()};s.info(""),s.info(`Querying deployment ${r.deploymentId} for the last 5 minutes\u2026`);let i=await lt(t,r.deploymentId,o);if("error"in i){s.error(`railway ${i.error}`),process.exitCode=1;return}s.success(`railway ${i.entries.length} row${i.entries.length===1?"":"s"}`)}async function as(e,t){let{configPath:r}=P(),n=e.railway??{},o=process.env.VERIRUN_RAILWAY_TOKEN,i=o??n.token;if(t.test){await is(n,i);return}if(i&&pr(n)&&!t.reconnect){s.info(""),s.info(s.bold("Railway logs \u2014 connected")),s.info(` token ${o?"from $VERIRUN_RAILWAY_TOKEN":"from config.json"}`),n.deploymentId?s.info(` deployment ${n.deploymentId} ${s.dim("(pinned)")}`):(s.info(` project ${n.projectId}`),s.info(` environment ${n.environmentId}`),s.info(` service ${n.serviceId} ${s.dim("(latest deployment resolved per run)")}`)),s.info(""),s.info(s.dim("--reconnect to change it \xB7 --test to query the last 5 minutes"));return}if(s.info(""),s.info(s.bold("Connect Railway logs \u2014 2 things:")),s.info(""),s.info(` 1. On the page opening now (${ct}):`),s.info(' "Create Token" \u2192 name it \u2192 pick your team \u2192 Create \u2192 copy the value.'),s.info(" 2. In Railway, open the service you want logs from. Copy its URL from the"),s.info(" address bar \u2014 looks like:"),s.info(" https://railway.com/project/PROJECT/service/SERVICE?environmentId=ENV"),!process.stdin.isTTY){s.info(""),s.info(s.dim("Then: VERIRUN_RAILWAY_TOKEN=<token>, and railway.projectId / environmentId / serviceId (from that URL) in .verirun/config.json."));return}if(!i||t.reconnect){re(ct),s.info("");let d=await V(" Railway token: ");if(!d){s.warn(" Nothing pasted. No changes made.");return}i=d}let c={};o||(c.token=i);let a=ss(await V(" Service URL (paste the whole thing): "));if(a)c.projectId=a.projectId,c.environmentId=a.environmentId,c.serviceId=a.serviceId;else{s.warn(" Couldn't read that URL. Enter the three IDs from it by hand:");let d=await V(" Project ID: "),u=await V(" Environment ID: "),m=await V(" Service ID: ");if(!d||!u||!m){s.warn(" Need all three. No changes made.");return}c.projectId=d,c.environmentId=u,c.serviceId=m}await _e(r,{railway:c}),s.info(""),s.info(` Saved to ${s.dim(r)}`),s.info(" Checking it works\u2026");let l=await xe({...n,...c,token:i});if("error"in l){s.error(` ${l.error}`),s.info(s.dim(" Re-run `verirun log railway --reconnect` with the right token / IDs.")),process.exitCode=1;return}s.success(` Connected. Deployment ${l.deploymentId}. \`verirun run\` now includes Railway logs.`)}import{existsSync as hr,readFileSync as wr}from"node:fs";import{mkdir as cs,readdir as ls,writeFile as mr}from"node:fs/promises";import H from"node:path";var ds=new Set(["node_modules",".git",".next",".verirun","dist","build","out","coverage",".turbo",".vercel"]);async function us(e){let t=[],r=async n=>{for(let o of await ls(n,{withFileTypes:!0})){if(o.name.startsWith(".")&&o.name!==".")continue;let i=H.join(n,o.name);if(o.isDirectory()){if(ds.has(o.name))continue;await r(i)}else/\.tsx?$/.test(o.name)&&!o.name.endsWith(".d.ts")&&t.push({file:H.relative(e,i).split(H.sep).join("/"),source:wr(i,"utf8")})}};return await r(e),t}function fs(e){if(hr(e))try{let t=wr(e,"utf8").replace(/\/\*[\s\S]*?\*\//g,"").replace(/(^|[^:])\/\/.*$/gm,"$1").replace(/,(\s*[}\]])/g,"$1");return JSON.parse(t)}catch{return}}function yr(e,t=0){if(t>5)return{};let r=fs(e);if(!r)return{};let n=r.compilerOptions??{},o=r.extends;if(typeof o!="string"||!o.startsWith("."))return n;let i=H.resolve(H.dirname(e),o),c=yr(/\.json$/.test(i)?i:`${i}.json`,t+1);return{...c,...n,paths:n.paths??c.paths}}function ps(e){let{baseUrl:t,paths:r}=yr(H.join(e,"tsconfig.json")),n=(t??"").replace(/^\.\/?/,"").replace(/\/$/,""),o={};for(let[i,c]of Object.entries(r??{})){let a=c[0];if(!i.endsWith("/*")||!a?.endsWith("*"))continue;let l=a.slice(0,-1).replace(/\/$/,"").replace(/^\.\//,"");o[i.slice(0,-1)]=[n,l].filter(Boolean).join("/")}return o}function ms(e,t){let r={...t};for(let n of e??[]){let[o,i]=n.split("=");o&&(r[o]=i??"")}return r}var gs=/(?:from|import)\s*\(?\s*['"]([^'"]+)['"]/g;function hs(e,t){let r=new Set(e.map(o=>o.file)),n=new Map;for(let o of e)for(let[,i]of o.source.matchAll(gs)){if(!i||i.startsWith(".")||Object.keys(t).some(d=>i.startsWith(d)))continue;let c=i.indexOf("/");if(c<=0)continue;let a=`${i.slice(0,c)}/`;if(n.has(a))continue;let l=i.slice(c+1);for(let d of r){let u=d.replace(/\.tsx?$/,"").replace(/\/index$/,"");if(u===l||!u.endsWith(`/${l}`)){if(u===l)n.set(a,"");else continue;break}n.set(a,u.slice(0,u.length-l.length-1));break}}return[...n].map(([o,i])=>`--alias ${o}=${i}`)}function ws(e,t,r){let n=e.nodes.pages.filter(i=>!i.inferred);s.info(`${s.bold(String(n.length))} pages \xB7 ${s.bold(String(e.nodes.leaves.length))} backend effects \xB7 ${s.bold(String(e.edges.length))} edges ${s.dim(`(${t.length} files)`)}`),s.info("");for(let i of e.nodes.pages){let c=e.edges.filter(p=>p.from===i.id),a=c.filter(p=>p.kind==="nav").length,l=c.filter(p=>p.kind==="effect").length,d=new Set([...i.elements,...c.map(p=>p.trigger)].filter(p=>p.dialogs&&p.dialogs.length>0).map(p=>p.id)).size,u=d>0?`, ${d} opening a dialog`:"",m=i.inferred?s.dim(" (inferred)"):"";s.info(` ${i.path}${m} ${s.dim(`\u2014 ${i.component}, ${i.elements.length} element(s), ${a} nav, ${l} effect${u}`)}`)}let o=e.diagnostics;o.parseErrors.length>0&&s.warn(`${o.parseErrors.length} file(s) failed to parse`);for(let i of r)s.info(""),s.warn(`Imports like ${s.bold(i.split(" ")[1]?.split("=")[0]??"")} could not be resolved, so components they point at were not analysed. Re-run with:`),s.info(` verirun map ${i}`);o.unresolvedTargets.length>0&&(s.info(""),s.info(s.dim(`${o.unresolvedTargets.length} nav target(s) could not be resolved to a page`)))}var gr=1;async function br(e){let t=g=>{process.stdout.write(`${JSON.stringify(g,null,e.verbose?2:void 0)}
|
|
43
|
+
`)};e.json&&s.machineOutput();let{assertLicensed:r}=await import("./gate.mjs"),{assembleTree:n,bfs:o,buildCoverage:i,measureReach:c,renderBfs:a,renderMarkdown:l,renderReachTable:d}=await import("./graph.mjs"),u=await r();u.note&&s.warn(u.note);let m=P(),p=e.dir?H.resolve(e.dir):m.repoRoot;if(!hr(p))throw new Error(`no such directory: ${p}`);let f=await us(p);if(f.length===0)throw new Error(`no .ts/.tsx files found under ${p}`);let v=ms(e.alias,ps(p)),b={aliases:v,...e.followChildren===!1?{followChildren:!1}:{},...e.appFile?{appFile:e.appFile}:{}};if(e.bfs){let g=n(f,b),x=e.kind==="nav"||e.kind==="effect"?[e.kind]:void 0,R=o(g,e.bfs,{depth:e.depth?Number(e.depth):2,...x?{kinds:x}:{}});if(e.json){t({schemaVersion:gr,tool:"verirun-map-bfs",...R});return}s.info(s.dim(`${R.order.length-1} node(s) within ${R.maxDepth} step(s) of ${R.from}`)),s.info(a(R));return}if(e.reach){let g=c(f,b);if(e.json){t({schemaVersion:gr,tool:"verirun-map-reach",...g});return}s.info(d(g));for(let x of g.unreachedFiles)s.info(s.dim(`unreached: ${x.file} (${x.elements} elements)`));return}let h=n(f,b);if(e.json){t(h);return}if(e.report){let g=e.out?H.resolve(e.out):H.join(m.dataDir,"map");await cs(g,{recursive:!0});let x=i(f,h);await mr(H.join(g,"interaction-tree.json"),`${JSON.stringify(h,null,2)}
|
|
44
|
+
`),await mr(H.join(g,"coverage.md"),`${l(x,h)}
|
|
45
|
+
`);let R=H.relative(process.cwd(),g),j=R.startsWith("..")?g:R;s.success(`wrote ${j}/interaction-tree.json + coverage.md`);let C=x.bar.filter(G=>G.pass===!1).length;s.info(s.dim(`${x.bar.length-C}/${x.bar.length} coverage checks pass`));return}ws(h,f,hs(f,v))}import{existsSync as Ys}from"node:fs";import{mkdir as Xs}from"node:fs/promises";import yt from"node:path";import{createInterface as zs}from"node:readline/promises";import{spawnSync as ys}from"node:child_process";var bs={browser:"chromium",channel:"chrome",label:"Chrome",approximated:!1},vs={browser:"chromium",channel:"msedge",label:"Edge",approximated:!1},ks={browser:"firefox",label:"Firefox",approximated:!1},$s={browser:"chromium",label:"Chromium",approximated:!1};function wt(e,t){let r=e.toLowerCase(),n=o=>t?{...o,label:t}:o;if(r.includes("firefox")||r.includes("mozilla"))return n(ks);if(r.includes("msedge")||r.includes("microsoft-edge")||r.includes("edgemac")||r.includes("microsoftedge"))return n(vs);if(r.includes("google-chrome")||r.includes("com.google.chrome")||r==="chromehtml"||r.includes("chrome"))return n(bs);if(r.includes("chromium"))return n($s);if(r.includes("safari"))return{browser:"webkit",label:"Safari",approximated:!0};if(r.includes("brave"))return{browser:"chromium",label:"Brave",approximated:!0};if(r.includes("thebrowser")||r.includes("arc"))return{browser:"chromium",label:"Arc",approximated:!0};if(r.includes("vivaldi"))return{browser:"chromium",label:"Vivaldi",approximated:!0};if(r.includes("opera")||r.includes("opr"))return{browser:"chromium",label:"Opera",approximated:!0}}function je(e,t){let r=ys(e,t,{encoding:"utf8",timeout:4e3});if(!(r.error||r.status!==0))return(r.stdout||"").trim()||void 0}function Ss(){let e=je("xdg-settings",["get","default-web-browser"])??je("xdg-mime",["query","default","x-scheme-handler/https"]);return e?wt(e):void 0}function xs(){let e=je("plutil",["-convert","json","-o","-",`${process.env.HOME}/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist`]);if(e)try{let r=(JSON.parse(e).LSHandlers??[]).find(o=>o.LSHandlerURLScheme==="https"),n=typeof r?.LSHandlerRoleAll=="string"?r.LSHandlerRoleAll:void 0;return n?wt(n):void 0}catch{return}}function Rs(){let t=je("reg",["query","HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\https\\UserChoice","/v","ProgId"])?.match(/ProgId\s+REG_SZ\s+(\S+)/i)?.[1];return t?wt(t):void 0}function vr(){try{if(process.platform==="darwin")return xs();if(process.platform==="linux")return Ss();if(process.platform==="win32")return Rs()}catch{}}import{spawn as Es}from"node:child_process";import{mkdtemp as Cs,readFile as Is,rm as As}from"node:fs/promises";import{createRequire as Ls}from"node:module";import Ts from"node:os";import Ne from"node:path";var _s=new Set(["name","selector","locator","signals","pageGuid","ariaSnapshot"]);function kr(e){let t={};for(let[r,n]of Object.entries(e))!_s.has(r)&&n!==void 0&&(t[r]=n);return t}function js(){let e=Ls(import.meta.url),t=e.resolve("playwright/package.json"),r=e("playwright/package.json"),n=typeof r.bin=="string"?r.bin:r.bin?.playwright;if(!n)throw new Error("Could not locate the Playwright CLI entrypoint in playwright/package.json.");return Ne.join(Ne.dirname(t),n)}async function $r(e){let t=js(),r=await Cs(Ne.join(Ts.tmpdir(),"verirun-record-")),n=Ne.join(r,"recording.jsonl"),o=[t,"codegen","--target=jsonl",`--output=${n}`];e.browser&&o.push(`--browser=${e.browser}`),e.channel&&o.push(`--channel=${e.channel}`),e.userDataDir&&o.push(`--user-data-dir=${e.userDataDir}`),e.testIdAttribute&&o.push(`--test-id-attribute=${e.testIdAttribute}`),e.url&&o.push(e.url);try{let i=await new Promise((a,l)=>{let d=Es(process.execPath,o,{stdio:["ignore","inherit","inherit"]});d.on("error",l),d.on("close",u=>a(u))}),c=await Is(n,"utf8").catch(()=>"");return{...Ns(c),exitCode:i}}finally{await As(r,{recursive:!0,force:!0})}}function Ns(e){let t=e.split(`
|
|
46
|
+
`).filter(o=>o.trim().length>0),r={},n=[];for(let[o,i]of t.entries()){let c;try{c=JSON.parse(i)}catch{continue}if(o===0&&c&&typeof c=="object"&&!("name"in c)){r=c;continue}c&&typeof c=="object"&&"name"in c&&n.push(c)}return{header:r,actions:n}}import{existsSync as Os}from"node:fs";import{appendFile as Ps,readFile as Ds}from"node:fs/promises";import Us from"node:path";function Ms(e){return e.split(`
|
|
47
|
+
`).map(t=>t.trim()).filter(t=>t&&!t.startsWith("#")).some(t=>t.replace(/^\//,"").replace(/\/$/,"")===ke)}async function Sr(e){let t=Us.join(e,".gitignore");if(!Os(t))return"no-gitignore";let r=await Ds(t,"utf8");if(Ms(r))return"already-present";let n=r.length===0||r.endsWith(`
|
|
32
48
|
`)?"":`
|
|
33
|
-
`;return await
|
|
49
|
+
`;return await Ps(t,`${n}
|
|
34
50
|
# verirun: recorded workflows and captured run logs (may contain secrets)
|
|
35
|
-
${
|
|
36
|
-
`,"utf8"),"added"}function
|
|
37
|
-
`,"utf8")}async function oe(e){let r=await Nn(e,"utf8"),o;try{o=JSON.parse(r)}catch(i){throw new Error(`Workflow file is not valid JSON: ${e}`,{cause:i})}let t=o;if(!t||typeof t!="object"||!Array.isArray(t.steps))throw new Error(`Workflow file is malformed (no steps array): ${e}`);if(t.schemaVersion!==U)throw new Error(`Workflow "${t.name??mr.basename(e)}" uses schema version ${t.schemaVersion}, but this build expects ${U}. Re-record it.`);return t}function Dn(e){return!(e.name==="openPage"&&e.url==="about:blank")}function Un(e){let r=e.length;for(;r>0&&e[r-1]?.name==="closePage";)r-=1;return e.slice(0,r)}function Vn(e){return Array.isArray(e.signals)&&e.signals.some(r=>!!r&&typeof r=="object"&&r.name==="dialog")}function Fn(e,r){let o={index:r,action:e.name,params:lr(e),signals:Array.isArray(e.signals)?e.signals:[]};e.selector&&(o.selector=e.selector),e.locator&&(o.locator=e.locator);let t=fr(e.locator);return t&&(o.description=t),e.pageGuid&&(o.pageGuid=e.pageGuid),Vn(e)&&(o.dialog={action:"accept"}),o}function wr(e){let r=e.description??e.selector??"";switch(e.action){case"navigate":return`navigate ${String(e.params.url??"")}`;case"fill":return`fill ${r} ${n.dim(`= ${JSON.stringify(e.params.text??"")}`)}`;case"press":return`press ${r} ${n.dim(String(e.params.key??""))}`;default:return`${e.action.padEnd(9)} ${r}`.trimEnd()}}function Mn(e){let r=wr(e);return e.dialog?`${r} ${n.dim(`\u2192 dialog: ${e.dialog.action==="accept"?"OK":"Cancel"}`)}`:r}async function Wn(e){let r=e.filter(t=>t.dialog);if(r.length===0||!process.stdin.isTTY)return;n.info(""),n.info(`${r.length===1?"A step":`${r.length} steps`} opened a browser dialog. Which button did you press?`);let o=_n({input:process.stdin,output:process.stdout});try{for(let t of r)(await o.question(` ${n.dim(String(t.index).padStart(2))} ${wr(t)} [O]K / [c]ancel: `)).trim().toLowerCase().startsWith("c")&&(t.dialog={action:"dismiss"})}finally{o.close()}}async function yr(e,r){let{assertLicensed:o}=await import("./gate.mjs"),t=await o();t.note&&n.warn(t.note),D(e);let i=y(),s=await T(i.configPath),a=O(i,e);if(Pn(a)&&!r.force)throw new Error(`Workflow "${e}" already exists at ${hr.relative(i.repoRoot,a)}. Re-run with --force to overwrite it.`);await On(i.workflowsDir,{recursive:!0});let c=await ur(i.repoRoot);c==="added"?n.step("Added .verirun/ to .gitignore"):c==="no-gitignore"&&n.warn("No .gitignore at the repo root, so .verirun/ is not ignored. Recorded values and run logs may contain secrets.");let l=r.testIdAttribute??s.testIdAttribute??_e,d=r.browser,u=r.channel;if(r.defaultBrowser){(r.browser||r.channel)&&n.warn("--default-browser overrides -b / --channel.");let f=cr();f?(d=f.browser,u=f.channel,f.approximated?n.warn(`Your default browser is ${f.label}, which Playwright can't drive directly \u2014 recording in ${u??d}.`):n.info(n.dim(`Recording in your default browser: ${f.label}`))):(n.warn("Couldn't detect your default browser; recording in bundled Chromium."),d="chromium",u=void 0)}if(r.url){let f=await pr(r.url);if(f.reachable===!1)throw new Error(`Nothing is listening at ${r.url} (connection refused on ${f.host}:${f.port}). Start the app you want to record, then run this again.`)}n.info(""),n.info(`Recording ${n.bold(e)}. A browser window will open.`),n.info(n.dim("Perform the workflow, then close the browser to save.")),n.info("");let p=await dr({url:r.url,browser:d,channel:u,userDataDir:r.userDataDir,testIdAttribute:l}),m=Un(p.actions.filter(Dn));if(m.length===0){if(p.exitCode!==0)throw new Error(`Playwright codegen exited with code ${p.exitCode} before anything was recorded. Its output is above \u2014 commonly the browser failed to open, or a page the recording needed was unreachable. If it persists against a page that loads fine, the installed Playwright may no longer support the \`jsonl\` codegen target that recording depends on.`);n.warn("No actions were recorded, so nothing was saved.");return}let g=m.map(Fn);await Wn(g);let v=g.find(f=>f.action==="navigate"),w={schemaVersion:U,name:e,createdAt:new Date().toISOString(),startUrl:r.url??v?.params.url,browser:p.header.browserName??d??"chromium",channel:u,testIdAttribute:l,steps:g};await gr(a,w),n.info("");for(let f of g)n.info(` ${n.dim(String(f.index).padStart(2))} ${Mn(f)}`);n.info(""),n.success(`Saved ${g.length} step${g.length===1?"":"s"} to ${hr.relative(i.repoRoot,a)}`),g.some(f=>f.dialog)&&n.info(n.dim('Steps marked "\u2192 dialog" answer it on replay. To answer a prompt(), set "dialog": { "action": "accept", "promptText": "..." } on that step.')),g.some(f=>f.action==="fill"&&f.params.text)&&n.warn("Recorded steps include text you typed. Check the workflow file before sharing it.")}import{existsSync as qn}from"node:fs";import{mkdir as zn,writeFile as Xn}from"node:fs/promises";import Ir from"node:path";import{chromium as M,firefox as Bn,selectors as Gn,webkit as Hn}from"playwright";var Jn={chromium:M,firefox:Bn,webkit:Hn};async function Kn(e,r){let o=!r.headed,t=r.slowMoMs&&r.slowMoMs>0?r.slowMoMs:void 0;if(r.cdpEndpoint){let c=await M.connectOverCDP(r.cdpEndpoint,{slowMo:t}),l=c.contexts()[0]??await c.newContext(),d=l.pages()[0]??await l.newPage();return{context:l,page:d,close:()=>c.close()}}if(r.userDataDir){let c=await M.launchPersistentContext(r.userDataDir,{headless:o,channel:r.channel,slowMo:t}),l=c.pages()[0]??await c.newPage();return{context:c,page:l,close:()=>c.close()}}if(r.channel){let c=await M.launch({headless:o,channel:r.channel,slowMo:t}),l=await c.newContext();return{context:l,page:await l.newPage(),close:()=>c.close()}}let s=await(Jn[e.browser]??M).launch({headless:o,slowMo:t}),a=await s.newContext();return{context:a,page:await a.newPage(),close:()=>s.close()}}function vr(e){let r=typeof e=="number"?e:0,o=[];return r&1&&o.push("Alt"),r&2&&o.push("Control"),r&4&&o.push("Meta"),r&8&&o.push("Shift"),o}function ie(e){return typeof e=="string"?e:void 0}function br(e){return Array.isArray(e)?e.filter(r=>typeof r=="string"):[]}async function Yn(e,r){let o=()=>{if(!r.selector)throw new Error(`Step ${r.index} (${r.action}) has no selector to act on.`);return e.locator(r.selector)};switch(r.action){case"navigate":{let t=ie(r.params.url);if(!t)throw new Error(`Step ${r.index} (navigate) has no url.`);await e.goto(t);return}case"openPage":return;case"closePage":await e.close();return;case"click":case"dblclick":{await o().click({button:ie(r.params.button)??"left",clickCount:typeof r.params.clickCount=="number"?r.params.clickCount:r.action==="dblclick"?2:1,modifiers:vr(r.params.modifiers)});return}case"fill":await o().fill(ie(r.params.text)??"");return;case"press":{let t=ie(r.params.key);if(!t)throw new Error(`Step ${r.index} (press) has no key.`);await o().press([...vr(r.params.modifiers),t].join("+"));return}case"check":await o().check();return;case"uncheck":await o().uncheck();return;case"select":await o().selectOption(br(r.params.options));return;case"setInputFiles":await o().setInputFiles(br(r.params.files));return;default:if(r.action.startsWith("assert")){n.warn(`Step ${r.index}: skipping ${r.action} (assertions are not replayed).`);return}throw new Error(`Step ${r.index}: don't know how to replay a "${r.action}" action.`)}}async function $r(e,r={}){e.testIdAttribute&&Gn.setTestIdAttribute(e.testIdAttribute);let o={...r,channel:r.channel??e.channel},t=await Kn(e,o),i=0,s=[],a,c=l=>{let d=a?.dialog??{action:r.dialogDefault??"accept"},u=l.type()==="prompt",p=u&&d.action==="accept"?d.promptText??l.defaultValue():void 0;s.push({...a?{step:a.index}:{},type:l.type(),message:l.message(),...u?{defaultValue:l.defaultValue()}:{},action:d.action,...p!==void 0?{promptText:p}:{},at:new Date().toISOString()}),n.step(` ${n.dim(`${l.type()} ${JSON.stringify(l.message())} \u2192 ${d.action}`)}`+(p!==void 0?n.dim(` ${JSON.stringify(p)}`):"")),(d.action==="accept"?l.accept(p):l.dismiss()).catch(()=>{})};try{let{context:l}=t;r.timeoutMs!==void 0&&l.setDefaultTimeout(r.timeoutMs);let d=t.page;d.on("dialog",c),l.on("page",u=>{u.on("dialog",c),d=u});for(let u of e.steps){n.step(`${String(u.index).padStart(2)} ${u.action}`+(u.description?` ${n.dim(u.description)}`:"")),a=u;try{await Yn(d,u)}catch(p){return{stepsTotal:e.steps.length,stepsReplayed:i,dialogs:s,error:p instanceof Error?p:new Error(String(p)),failedStepIndex:u.index}}if(i+=1,d.isClosed()){let p=l.pages().at(-1);p&&(d=p)}}return{stepsTotal:e.steps.length,stepsReplayed:i,dialogs:s}}finally{await t.close()}}function Sr(e,r){let o=r.toLowerCase();if(!o||typeof e.message=="string"&&e.message.toLowerCase().includes(o))return!0;if(e.raw!==void 0)try{return JSON.stringify(e.raw).toLowerCase().includes(o)}catch{return!1}return!1}var kr=[];function se(e){kr.push(e)}function Rr(e){return kr.map(r=>r(e)).filter(r=>!!r)}var Er=!1;function Cr(){Er||(Er=!0,se(qe),se(Ze),se(Ke))}var Qn=1,Zn=500;function eo(e){if(e.slowMo!==void 0){let r=Number(e.slowMo);if(!Number.isFinite(r)||r<0)throw new Error(`--slow-mo needs a number of milliseconds, got "${e.slowMo}".`);return r}return e.headed?Zn:0}function W(e){return e instanceof Error?e.message:String(e)}function ro(e){return e.replace(/\x1b\[[0-9;]*m/g,"")}async function xr(e){for(let r of[...e].reverse())try{await r.dispose()}catch(o){n.warn(`${r.name}: dispose failed: ${W(o)}`)}}async function jr(e,r){let{assertLicensed:o}=await import("./gate.mjs"),t=await o();t.note&&n.warn(t.note),D(e);let i=y(),s=await T(i.configPath),a=O(i,e);if(!qn(a))throw new Error(`No workflow named "${e}". Record it first with: verirun record ${e}`);let c=await oe(a),l;if(r.timeout!==void 0&&(l=Number(r.timeout),!Number.isFinite(l)||l<0))throw new Error(`--timeout must be a non-negative number of milliseconds, got "${r.timeout}".`);if(r.dialog!==void 0&&r.dialog!=="accept"&&r.dialog!=="dismiss")throw new Error(`--dialog must be "accept" or "dismiss", got "${r.dialog}".`);let d=r.dialog,u=(r.user??s.userFilter)?.trim()||void 0;Cr();let p=Rr(s),m=Ve(),g=Fe(i,e,m);await zn(g,{recursive:!0});let v={config:s,runDir:g,workflowName:e,userFilter:u};n.info(""),n.info(`Running ${n.bold(e)} ${n.dim(`${c.steps.length} steps \xB7 ${c.browser}`)}`),p.length===0?n.warn("No log sources configured. Add a supabase or vercel block to .verirun/config.json."):n.info(n.dim(`Log sources: ${p.map(f=>f.name).join(", ")}`)),u&&n.info(n.dim(`User filter: entries mentioning "${u}"`)),n.info("");let w=[];try{for(let f of p)await f.start(v),w.push(f)}catch(f){let h=p[w.length];throw await xr(w),new Error(`Log source "${h?.name??"?"}" failed to start: ${W(f)}`)}try{let f=new Date().toISOString(),h=await $r(c,{headed:r.headed,slowMoMs:eo(r),timeoutMs:l,channel:r.channel,userDataDir:r.userDataDir,cdpEndpoint:r.cdp,dialogDefault:d}),$=new Date().toISOString(),E={startedAt:f,endedAt:$};h.error?n.error(`Replay failed at step ${h.failedStepIndex}: ${W(h.error)}`):n.success(`Replayed ${h.stepsReplayed} step${h.stepsReplayed===1?"":"s"}`);let j=[],A=[],B=0;for(let S of w)try{let x=await S.collect(v,E);B+=x.length,u&&(x=x.filter(_r=>Sr(_r,u))),A.push(...x),j.push({id:S.id,name:S.name,entries:x.length})}catch(x){n.warn(`${S.name}: log collection failed: ${W(x)}`),j.push({id:S.id,name:S.name,entries:0})}u&&A.length===0&&B>0&&n.warn(`${B} log entr${B===1?"y":"ies"} collected, none mention "${u}".`),A.sort((S,x)=>(S.timestamp??"").localeCompare(x.timestamp??""));let Or={schemaVersion:Qn,workflow:e,status:h.error?"failed":"ok",window:E,steps:{total:h.stepsTotal,replayed:h.stepsReplayed,...h.failedStepIndex!==void 0?{failedAt:h.failedStepIndex}:{}},...h.error?{error:ro(W(h.error))}:{},...u?{filter:{user:u}}:{},dialogs:h.dialogs,sources:j,entries:A},Ie=Ir.join(g,"logs.json");await Xn(Ie,`${JSON.stringify(Or,null,2)}
|
|
38
|
-
|
|
39
|
-
`),
|
|
51
|
+
${ke}/
|
|
52
|
+
`,"utf8"),"added"}function ae(e){return`'${e.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/\n/g,"\\n")}'`}function ue(e){if(e&&typeof e=="object"&&"source"in e){let t=e;return`/${t.source??""}/${t.flags??""}`}return ae(String(e))}function Fs(e){let t=e.options??{},r=e.body;switch(e.kind){case"role":{let n=[];t.name!==void 0&&n.push(`name: ${ue(t.name)}`),t.exact===!0&&n.push("exact: true");for(let o of t.attrs??[])n.push(`${o.name}: ${JSON.stringify(o.value)}`);return n.length?`getByRole(${ae(r)}, { ${n.join(", ")} })`:`getByRole(${ae(r)})`}case"test-id":return`getByTestId(${ae(r)})`;case"text":case"label":case"placeholder":case"alt":case"title":{let n={text:"getByText",label:"getByLabel",placeholder:"getByPlaceholder",alt:"getByAltText",title:"getByTitle"}[e.kind];return t.exact===!0?`${n}(${ue(r)}, { exact: true })`:`${n}(${ue(r)})`}case"nth":return`nth(${r})`;case"first":return"first()";case"last":return"last()";case"visible":return`filter({ visible: ${r==="true"} })`;case"frame":return"contentFrame()";case"frame-locator":return`frameLocator(${ae(r)})`;case"has-text":return`filter({ hasText: ${ue(r)} })`;case"has-not-text":return`filter({ hasNotText: ${ue(r)} })`;default:return`locator(${ae(r)})`}}function xr(e){if(!e)return;let t=[],r=e;for(;r;)t.push(Fs(r)),r=r.next;return t.join(".")}var Vs=/^[A-Za-z0-9][A-Za-z0-9._-]*$/;var Ws=/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;function ce(e){if(!e)throw new Error("Workflow name is required.");if(e.length>64)throw new Error("Workflow name must be 64 characters or fewer.");if(!Vs.test(e))throw new Error(`Invalid workflow name "${e}". Use letters, digits, dot, dash or underscore, starting with a letter or digit.`);if(e==="."||e==="..")throw new Error(`Invalid workflow name "${e}".`);if(e.endsWith("."))throw new Error(`Invalid workflow name "${e}". A trailing dot is dropped on Windows, so "${e}" and "${e.replace(/\.+$/,"")}" would be the same file.`);let t=e.split(".")[0]??"";if(Ws.test(t))throw new Error(`Invalid workflow name "${e}". "${t}" is a reserved device name on Windows and cannot be used as a filename.`)}import Bs from"node:net";var Gs=2e3,Hs={"http:":80,"https:":443};async function Rr(e){let t;try{t=new URL(e)}catch{return{reachable:"unknown"}}let r=Hs[t.protocol];if(r===void 0)return{reachable:"unknown"};let n=t.hostname.replace(/^\[|\]$/g,""),o=t.port?Number(t.port):r;return new Promise(i=>{let c=!1,a=d=>{c||(c=!0,l.destroy(),i(d))},l=Bs.connect({host:n,port:o,autoSelectFamily:!0});l.setTimeout(Gs),l.on("connect",()=>a({reachable:!0})),l.on("timeout",()=>a({reachable:"unknown"})),l.on("error",d=>{a(d.code==="ECONNREFUSED"?{reachable:!1,host:n,port:o}:{reachable:"unknown"})})})}import{mkdir as Ks,readFile as qs,writeFile as Js}from"node:fs/promises";import Er from"node:path";var fe=1;async function Cr(e,t){await Ks(Er.dirname(e),{recursive:!0}),await Js(e,`${JSON.stringify(t,null,2)}
|
|
53
|
+
`,"utf8")}async function Oe(e){let t=await qs(e,"utf8"),r;try{r=JSON.parse(t)}catch(o){throw new Error(`Workflow file is not valid JSON: ${e}`,{cause:o})}let n=r;if(!n||typeof n!="object"||!Array.isArray(n.steps))throw new Error(`Workflow file is malformed (no steps array): ${e}`);if(n.schemaVersion!==fe)throw new Error(`Workflow "${n.name??Er.basename(e)}" uses schema version ${n.schemaVersion}, but this build expects ${fe}. Re-record it.`);return n}var Ir=1;function Qs(e){return!(e.name==="openPage"&&e.url==="about:blank")}function Zs(e){let t=e.length;for(;t>0&&e[t-1]?.name==="closePage";)t-=1;return e.slice(0,t)}function ei(e){return Array.isArray(e.signals)&&e.signals.some(t=>!!t&&typeof t=="object"&&t.name==="dialog")}function ti(e,t){let r={index:t,action:e.name,params:kr(e),signals:Array.isArray(e.signals)?e.signals:[]};e.selector&&(r.selector=e.selector),e.locator&&(r.locator=e.locator);let n=xr(e.locator);return n&&(r.description=n),e.pageGuid&&(r.pageGuid=e.pageGuid),ei(e)&&(r.dialog={action:"accept"}),r}function Ar(e){let t=e.description??e.selector??"";switch(e.action){case"navigate":return`navigate ${String(e.params.url??"")}`;case"fill":return`fill ${t} ${s.dim(`= ${JSON.stringify(e.params.text??"")}`)}`;case"press":return`press ${t} ${s.dim(String(e.params.key??""))}`;default:return`${e.action.padEnd(9)} ${t}`.trimEnd()}}function ri(e){let t=Ar(e);return e.dialog?`${t} ${s.dim(`\u2192 dialog: ${e.dialog.action==="accept"?"OK":"Cancel"}`)}`:t}async function ni(e){let t=e.filter(n=>n.dialog);if(t.length===0||!process.stdin.isTTY)return;s.info(""),s.info(`${t.length===1?"A step":`${t.length} steps`} opened a browser dialog. Which button did you press?`);let r=zs({input:process.stdin,output:process.stderr});try{for(let n of t)(await r.question(` ${s.dim(String(n.index).padStart(2))} ${Ar(n)} [O]K / [c]ancel: `)).trim().toLowerCase().startsWith("c")&&(n.dialog={action:"dismiss"})}finally{r.close()}}async function Lr(e,t){let{assertLicensed:r}=await import("./gate.mjs"),n=await r();n.note&&s.warn(n.note),ce(e),t.json&&s.machineOutput();let o=P(),i=await Q(o.configPath),c=ie(o,e);if(Ys(c)&&!t.force)throw new Error(`Workflow "${e}" already exists at ${yt.relative(o.repoRoot,c)}. Re-run with --force to overwrite it.`);await Xs(o.workflowsDir,{recursive:!0});let a=await Sr(o.repoRoot);a==="added"?s.step("Added .verirun/ to .gitignore"):a==="no-gitignore"&&s.warn("No .gitignore at the repo root, so .verirun/ is not ignored. Recorded values and run logs may contain secrets.");let l=t.testIdAttribute??i.testIdAttribute??Kt,d=t.browser,u=t.channel;if(t.defaultBrowser){(t.browser||t.channel)&&s.warn("--default-browser overrides -b / --channel.");let g=vr();g?(d=g.browser,u=g.channel,g.approximated?s.warn(`Your default browser is ${g.label}, which Playwright can't drive directly \u2014 recording in ${u??d}.`):s.info(s.dim(`Recording in your default browser: ${g.label}`))):(s.warn("Couldn't detect your default browser; recording in bundled Chromium."),d="chromium",u=void 0)}if(t.url){let g=await Rr(t.url);if(g.reachable===!1)throw new Error(`Nothing is listening at ${t.url} (connection refused on ${g.host}:${g.port}). Start the app you want to record, then run this again.`)}s.info(""),s.info(`Recording ${s.bold(e)}. A browser window will open.`),s.info(s.dim("Perform the workflow, then close the browser to save.")),s.info("");let m=await $r({url:t.url,browser:d,channel:u,userDataDir:t.userDataDir,testIdAttribute:l}),p=Zs(m.actions.filter(Qs));if(p.length===0){if(m.exitCode!==0)throw new Error(`Playwright codegen exited with code ${m.exitCode} before anything was recorded. Its output is above \u2014 commonly the browser failed to open, or a page the recording needed was unreachable. If it persists against a page that loads fine, the installed Playwright may no longer support the \`jsonl\` codegen target that recording depends on.`);s.warn("No actions were recorded, so nothing was saved."),t.json&&process.stdout.write(`${JSON.stringify({schemaVersion:Ir,tool:"verirun-record",name:e,saved:!1,steps:0})}
|
|
54
|
+
`),process.exitCode=U.failed;return}let f=p.map(ti);await ni(f);let v=f.find(g=>g.action==="navigate"),b={schemaVersion:fe,name:e,createdAt:new Date().toISOString(),startUrl:t.url??v?.params.url,browser:m.header.browserName??d??"chromium",channel:u,testIdAttribute:l,steps:f};await Cr(c,b);let h=yt.relative(o.repoRoot,c).split(yt.sep).join("/");if(s.success(`Saved ${f.length} step${f.length===1?"":"s"} to ${h}`),t.verbose)for(let g of f)s.info(` ${s.dim(String(g.index).padStart(2))} ${ri(g)}`);t.json&&process.stdout.write(`${JSON.stringify({schemaVersion:Ir,tool:"verirun-record",name:e,saved:!0,steps:f.length,path:h,workflowSchemaVersion:fe})}
|
|
55
|
+
`),f.some(g=>g.dialog)&&s.info(s.dim('Steps that opened a dialog answer it on replay. To answer a prompt(), set "dialog": { "action": "accept", "promptText": "..." } on that step.')),f.some(g=>g.action==="fill"&&g.params.text)&&s.warn("Recorded steps include text you typed. Check the workflow file before sharing it.")}import{existsSync as gi}from"node:fs";import{mkdir as hi,writeFile as wi}from"node:fs/promises";import bt from"node:path";import{chromium as pe,firefox as oi,selectors as si,webkit as ii}from"playwright";var ai={chromium:pe,firefox:oi,webkit:ii};async function ci(e,t){let r=!t.headed,n=t.slowMoMs&&t.slowMoMs>0?t.slowMoMs:void 0;if(t.cdpEndpoint){let a=await pe.connectOverCDP(t.cdpEndpoint,{slowMo:n}),l=a.contexts()[0]??await a.newContext(),d=l.pages()[0]??await l.newPage();return{context:l,page:d,close:()=>a.close()}}if(t.userDataDir){let a=await pe.launchPersistentContext(t.userDataDir,{headless:r,channel:t.channel,slowMo:n}),l=a.pages()[0]??await a.newPage();return{context:a,page:l,close:()=>a.close()}}if(t.channel){let a=await pe.launch({headless:r,channel:t.channel,slowMo:n}),l=await a.newContext();return{context:l,page:await l.newPage(),close:()=>a.close()}}let i=await(ai[e.browser]??pe).launch({headless:r,slowMo:n}),c=await i.newContext();return{context:c,page:await c.newPage(),close:()=>i.close()}}function Tr(e){let t=typeof e=="number"?e:0,r=[];return t&1&&r.push("Alt"),t&2&&r.push("Control"),t&4&&r.push("Meta"),t&8&&r.push("Shift"),r}function Pe(e){return typeof e=="string"?e:void 0}function _r(e){return Array.isArray(e)?e.filter(t=>typeof t=="string"):[]}async function li(e,t){let r=()=>{if(!t.selector)throw new Error(`Step ${t.index} (${t.action}) has no selector to act on.`);return e.locator(t.selector)};switch(t.action){case"navigate":{let n=Pe(t.params.url);if(!n)throw new Error(`Step ${t.index} (navigate) has no url.`);await e.goto(n);return}case"openPage":return;case"closePage":await e.close();return;case"click":case"dblclick":{await r().click({button:Pe(t.params.button)??"left",clickCount:typeof t.params.clickCount=="number"?t.params.clickCount:t.action==="dblclick"?2:1,modifiers:Tr(t.params.modifiers)});return}case"fill":await r().fill(Pe(t.params.text)??"");return;case"press":{let n=Pe(t.params.key);if(!n)throw new Error(`Step ${t.index} (press) has no key.`);await r().press([...Tr(t.params.modifiers),n].join("+"));return}case"check":await r().check();return;case"uncheck":await r().uncheck();return;case"select":await r().selectOption(_r(t.params.options));return;case"setInputFiles":await r().setInputFiles(_r(t.params.files));return;default:if(t.action.startsWith("assert")){s.warn(`Step ${t.index}: skipping ${t.action} (assertions are not replayed).`);return}throw new Error(`Step ${t.index}: don't know how to replay a "${t.action}" action.`)}}async function jr(e,t={}){e.testIdAttribute&&si.setTestIdAttribute(e.testIdAttribute);let r={...t,channel:t.channel??e.channel},n=await ci(e,r),o=0,i=[],c,a=l=>{let d=c?.dialog??{action:t.dialogDefault??"accept"},u=l.type()==="prompt",m=u&&d.action==="accept"?d.promptText??l.defaultValue():void 0;i.push({...c?{step:c.index}:{},type:l.type(),message:l.message(),...u?{defaultValue:l.defaultValue()}:{},action:d.action,...m!==void 0?{promptText:m}:{},at:new Date().toISOString()}),s.step(` ${s.dim(`${l.type()} ${JSON.stringify(l.message())} \u2192 ${d.action}`)}`+(m!==void 0?s.dim(` ${JSON.stringify(m)}`):"")),(d.action==="accept"?l.accept(m):l.dismiss()).catch(()=>{})};try{let{context:l}=n;t.timeoutMs!==void 0&&l.setDefaultTimeout(t.timeoutMs);let d=n.page;d.on("dialog",a),l.on("page",u=>{u.on("dialog",a),d=u});for(let u of e.steps){s.step(`${String(u.index).padStart(2)} ${u.action}`+(u.description?` ${s.dim(u.description)}`:"")),c=u;try{await li(d,u)}catch(m){return{stepsTotal:e.steps.length,stepsReplayed:o,dialogs:i,error:m instanceof Error?m:new Error(String(m)),failedStepIndex:u.index}}if(o+=1,d.isClosed()){let m=l.pages().at(-1);m&&(d=m)}}return{stepsTotal:e.steps.length,stepsReplayed:o,dialogs:i}}finally{await n.close()}}function Nr(e,t){let r=t.toLowerCase();if(!r||typeof e.message=="string"&&e.message.toLowerCase().includes(r))return!0;if(e.raw!==void 0)try{return JSON.stringify(e.raw).toLowerCase().includes(r)}catch{return!1}return!1}var Or=[];function De(e){Or.push(e)}function Pr(e){return Or.map(t=>t(e)).filter(t=>!!t)}var Dr=!1;function Ur(){Dr||(Dr=!0,De(ir),De(dr),De(or))}var di=/^(error|err|fatal|critical|crit|panic|alert|emerg|emergency|severe)$/i,ui=/^(warn|warning)$/i;function me(e,...t){let r=e;for(let o of t){if(Array.isArray(r)&&(r=r[0]),typeof r!="object"||r===null)return;r=r[o]}Array.isArray(r)&&(r=r[0]);let n=typeof r=="string"&&/^\d{3}$/.test(r)?Number(r):r;return typeof n=="number"&&Number.isInteger(n)&&n>=100&&n<=599?n:void 0}function Mr(e){let t=e.raw;return me(t,"statusCode")??me(t,"responseStatusCode")??me(t,"proxy","statusCode")??me(t,"status_code")??me(t,"metadata","response","status_code")}function fi(e){if(e.level&&di.test(e.level))return!0;let t=Mr(e);return t!==void 0&&t>=500}function pi(e){return(e.split(`
|
|
56
|
+
`)[0]??"").replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,"<id>").replace(/\b[0-9a-f]{12,}\b/gi,"<hex>").replace(/\d+/g,"#").replace(/\s+/g," ").trim().slice(0,300)}function mi(e,t){let r=(e.split(`
|
|
57
|
+
`)[0]??"").replace(/\s+/g," ").trim();return r.length>t?`${r.slice(0,t-1)}\u2026`:r}function Fr(e,t={}){let r=t.maxGroups??8,n=t.maxMessage??200,o=new Map,i=0,c=0;for(let l of e){if(l.level&&ui.test(l.level)&&(c+=1),!fi(l))continue;i+=1;let d=Mr(l),u=l.message??"(no message)",m=[l.source,l.stream??"",(l.level??"").toLowerCase(),pi(u)].join("\0"),p=o.get(m);p?(p.count+=1,l.timestamp&&(p.lastAt=l.timestamp)):o.set(m,{source:l.source,...l.stream?{stream:l.stream}:{},...l.level?{level:l.level}:{},...d!==void 0?{status:d}:{},message:mi(u,n),count:1,...l.timestamp?{firstAt:l.timestamp,lastAt:l.timestamp}:{}})}let a=[...o.values()].sort((l,d)=>d.count-l.count);return{entries:e.length,problems:i,warnings:c,groups:a.slice(0,r),groupsOmitted:Math.max(0,a.length-r)}}var yi=1,bi=1,vi=500;function ki(e){if(e.slowMo!==void 0){let t=Number(e.slowMo);if(!Number.isFinite(t)||t<0)throw new Error(`--slow-mo needs a number of milliseconds, got "${e.slowMo}".`);return t}return e.headed?vi:0}function Ue(e,t,r=`${t}s`){return`${e} ${e===1?t:r}`}function $i(e,t=3){let r=e.split(`
|
|
58
|
+
`).map(a=>a.trim()).filter(Boolean),n=r[0]??"unknown error",o=r.findIndex(a=>/^call log:?$/i.test(a));if(o===-1)return[n];let i=new Map;for(let a of r.slice(o+1)){let l=a.replace(/^-\s*/,"");i.set(l,(i.get(l)??0)+1)}let c=[...i].slice(-t);return[n,...c.map(([a,l])=>l>1?`${a} (x${l})`:a)]}function Si(e,t){let n=[e.stream?`${e.source}/${e.stream}`:e.source,e.level,e.status].filter(o=>o!==void 0&&o!=="");return` ${`x${e.count}`.padEnd(t)} ${n.join(" ")} ${e.message}`}function xi(e,t={}){let r=[],n=`${(e.durationMs/1e3).toFixed(1)}s`;if(e.status==="ok")r.push(`PASS ${e.workflow} ${e.steps.replayed}/${e.steps.total} steps in ${n}`);else{let i=e.failedStep;r.push(`FAIL ${e.workflow} step ${e.steps.failedAt??"?"} failed${i?`: ${i.action}${i.description?` ${i.description}`:""}`:""} (${e.steps.replayed}/${e.steps.total} steps replayed, ${n})`);let c=e.error??"unknown error",a=t.verbose?c.split(`
|
|
59
|
+
`).filter(l=>l.trim()):$i(c);for(let l of a)r.push(` ${l}`);i?.selector&&r.push(` selector: ${i.selector}`)}let o=e.logs;if(e.sources.length===0)r.push("logs none captured: no log source is configured (verirun log <source>)");else{let i=e.sources.map(a=>`${a.name} ${a.entries}`).join(", ");r.push(`logs ${Ue(o.entries,"entry","entries")}, ${Ue(o.problems,"error")}${o.warnings>0?`, ${Ue(o.warnings,"warning")}`:""} (${i})`);let c=Math.max(...o.groups.map(a=>`x${a.count}`.length),2);for(let a of o.groups)r.push(Si(a,c));o.groupsOmitted>0&&r.push(` +${Ue(o.groupsOmitted,"more distinct error")} (--verbose shows all)`)}return e.dialogs>0&&r.push(`dialogs ${e.dialogs} answered`),r.push(`full log: ${e.logsPath}`),r}function Me(e){return e instanceof Error?e.message:String(e)}function Ri(e){return e.replace(/\x1b\[[0-9;]*m/g,"")}async function Vr(e){for(let t of[...e].reverse())try{await t.dispose()}catch(r){s.warn(`${t.name}: dispose failed: ${Me(r)}`)}}async function Wr(e,t){let{assertLicensed:r}=await import("./gate.mjs"),n=await r();n.note&&s.warn(n.note),ce(e);let o=P(),i=await Q(o.configPath),c=ie(o,e);if(!gi(c))throw new Error(`No workflow named "${e}". Record it first with: verirun record ${e}`);let a=await Oe(c),l;if(t.timeout!==void 0&&(l=Number(t.timeout),!Number.isFinite(l)||l<0))throw new Error(`--timeout must be a non-negative number of milliseconds, got "${t.timeout}".`);if(t.dialog!==void 0&&t.dialog!=="accept"&&t.dialog!=="dismiss")throw new Error(`--dialog must be "accept" or "dismiss", got "${t.dialog}".`);let d=t.dialog,u=(t.user??i.userFilter)?.trim()||void 0;t.json&&s.machineOutput(),(!t.verbose||t.json)&&s.quietSteps(),Ur();let m=Pr(i),p=Yt(),f=Xt(o,e,p);await hi(f,{recursive:!0});let v={config:i,runDir:f,workflowName:e,userFilter:u};s.step(`Running ${s.bold(e)} ${s.dim(`${a.steps.length} steps \xB7 ${a.browser}`)}`),m.length>0&&s.step(s.dim(`Log sources: ${m.map(h=>h.name).join(", ")}`)),u&&s.step(s.dim(`User filter: entries mentioning "${u}"`));let b=[];try{for(let h of m)await h.start(v),b.push(h)}catch(h){let g=m[b.length];throw await Vr(b),new Error(`Log source "${g?.name??"?"}" failed to start: ${Me(h)}`)}try{let h=new Date().toISOString(),g=await jr(a,{headed:t.headed,slowMoMs:ki(t),timeoutMs:l,channel:t.channel,userDataDir:t.userDataDir,cdpEndpoint:t.cdp,dialogDefault:d}),x=new Date().toISOString(),R={startedAt:h,endedAt:x},j=[],C=[],G=0;for(let N of b)try{let D=await N.collect(v,R);G+=D.length,u&&(D=D.filter(Y=>Nr(Y,u))),C.push(...D),j.push({id:N.id,name:N.name,entries:D.length})}catch(D){s.warn(`${N.name}: log collection failed: ${Me(D)}`),j.push({id:N.id,name:N.name,entries:0})}u&&C.length===0&&G>0&&s.warn(`${G} log entr${G===1?"y":"ies"} collected, none mention "${u}".`),C.sort((N,D)=>(N.timestamp??"").localeCompare(D.timestamp??""));let K=g.error?Ri(Me(g.error)):void 0,L={schemaVersion:yi,workflow:e,status:g.error?"failed":"ok",window:R,steps:{total:g.stepsTotal,replayed:g.stepsReplayed,...g.failedStepIndex!==void 0?{failedAt:g.failedStepIndex}:{}},...K!==void 0?{error:K}:{},...u?{filter:{user:u}}:{},dialogs:g.dialogs,sources:j,entries:C},ee=bt.join(f,"logs.json");await wi(ee,`${JSON.stringify(L,null,2)}
|
|
60
|
+
`,"utf8");let J=a.steps.find(N=>N.index===g.failedStepIndex),oe={schemaVersion:bi,tool:"verirun-run",workflow:e,status:L.status,durationMs:Date.parse(x)-Date.parse(h),window:R,steps:L.steps,...J?{failedStep:{index:J.index,action:J.action,...J.selector?{selector:J.selector}:{},...J.description?{description:J.description}:{}}}:{},...K!==void 0?{error:K}:{},dialogs:g.dialogs.length,sources:j,logs:Fr(C,t.verbose?{maxGroups:200,maxMessage:1e3}:{}),logsPath:bt.relative(o.repoRoot,ee).split(bt.sep).join("/")};if(t.json)process.stdout.write(`${JSON.stringify(oe,null,t.verbose?2:void 0)}
|
|
61
|
+
`);else{let[N="",...D]=xi(oe,{verbose:t.verbose});s.info(oe.status==="ok"?s.green(N):s.red(N));for(let Y of D)s.info(Y)}process.exitCode=g.error?U.failed:U.ok}finally{await Vr(b)}}Be();kt();function ln(e){return e instanceof Error?e.message:String(e)}function dn(e,t={}){let r=t.maxHtmlBytes??2e6,n=t.maxResourcesPerPage??12,o=t.maxResourcesTotal??60,i=new Map,c=()=>!0,a=async l=>{try{let d=await e(l,{method:"HEAD",maxBytes:0});return(d.status===405||d.status===501||d.status===400)&&(d=await e(l,{method:"GET",maxBytes:1024})),{status:d.status}}catch(d){return{status:null,error:ln(d)}}};return{mode:"fetch",setPolicy(l){c=l},async loadPage(l){let d;try{d=await e(l,{method:"GET",maxBytes:r})}catch(g){return{url:l,finalUrl:l,status:null,error:ln(g),rendered:!1,events:[]}}let u=/\b(text\/html|application\/xhtml\+xml)\b/i.test(d.contentType??""),m={url:l,finalUrl:d.finalUrl,status:d.status,...d.contentType?{contentType:d.contentType}:{},...u&&d.body!==void 0?{html:d.body}:{},rendered:!1,events:[]};if(m.html===void 0||d.status>=400)return m;let p;try{p=new URL(d.finalUrl)}catch{return m}let f=Ve(m.html),v=p;if(f.baseHref)try{v=new URL(f.baseHref,p)}catch{}let b=0,h=[];for(let g of new Set(f.resources)){if(b>=n)break;let x;try{x=new URL(g,v)}catch{continue}if(x.origin!==p.origin||!c(x))continue;x.hash="";let R=i.get(x.href);if(!R){if(i.size>=o)break;R=await a(x.href),i.set(x.href,R)}b+=1,R.status===null?h.push({kind:"request_failed",message:R.error??"no response",url:x.href,method:"GET"}):R.status>=400&&h.push({kind:"http_error",message:`${R.status}`,url:x.href,status:R.status,method:"GET"})}return m.events=h,m},probe:a,async fetchText(l){try{let d=await e(l,{method:"GET",maxBytes:5e5});return{status:d.status,text:d.body??""}}catch{return{status:null,text:""}}}}}Be();var Xi=12;function M(e,t,r=`${t}s`){return`${e} ${e===1?t:r}`}function zi(e){return`${(e/1e3).toFixed(1)}s`}function fn(e,t,r){let n=e.pages.slice(0,r).map(i=>le(i,t)),o=e.pages.length-n.length;return n.join(", ")+(o>0?` (+${o} more)`:"")}function pn(e){let t=e.summary,r=[],n=[`${M(t.linksChecked,"internal link")} checked`];t.linksUnchecked>0&&n.push(`${t.linksUnchecked} not checked`),r.push(n.join(", ")),e.sitemap&&r.push(`${M(e.sitemap.urls,"page")} queued from the sitemap`),t.linksChecked===0&&t.pages<=1&&!e.sitemap&&r.push("no <a href> links found on the start page and no sitemap.xml, so there was nothing else to crawl"),e.rateLimited&&r.push(`${M(e.rateLimited,"request")} rate limited by the site (429), so not checked`),e.recovered&&r.push(`${M(e.recovered,"request")} got no response at first but answered on a retry (not reported)`);let o=[];t.skippedUnsafe>0&&o.push(`${M(t.skippedUnsafe,"destructive-looking link")}`),t.skippedByRobots>0&&o.push(`${M(t.skippedByRobots,"URL")} disallowed by robots.txt`),o.length>0&&r.push(`never requested: ${o.join(", ")}`),e.truncated&&r.push(`stopped early: ${e.truncated}`),e.mode==="fetch"&&r.push("no browser in this scan: JavaScript did not run, so JS exceptions and console errors were not checked");let i=e.pages.filter(a=>a.note?.startsWith("page renders client-side")).length;i>0&&r.push(`${M(i,"page")} ${i===1?"renders":"render"} client-side: anchor and landmark checks skipped there`);let c=e.pages.filter(a=>a.note?.startsWith("page is larger than")).length;return c>0&&r.push(`${M(c,"page")} larger than the scanner reads: anchor and landmark checks skipped there`),r}function Qi(e){if(e.status==="error")return{tone:"fail",text:`ERROR ${e.startUrl} ${e.error?.message??"scan failed"}`};let t=`on ${M(e.summary.pages,"page")}`,r=`(${e.mode}, ${zi(e.durationMs)})`;return e.status==="clean"?{tone:"pass",text:`PASS ${e.startUrl} no findings ${t} ${r}`}:{tone:"fail",text:`FAIL ${e.startUrl} ${M(e.summary.findings,"finding")} ${t} ${r}`}}function mn(e,t={}){let r=[Qi(e)];if(e.status==="error")return r;let n=t.verbose?e.findings:e.findings.slice(0,Xi);for(let i of n){let c=i.count>i.pages.length?` x${i.count}`:"";r.push({tone:"fail",text:` ${It[i.kind].padEnd(17)} ${i.message}${c}`});let a=`on ${fn(i,e.origin,t.verbose?50:2)}`,l=t.verbose&&i.evidence?` ${i.evidence}`:"";r.push({tone:"dim",text:` ${"".padEnd(17)} ${a}${l}`})}let o=e.findings.length-n.length;if(o>0&&r.push({tone:"dim",text:` +${M(o,"more finding")} (--verbose shows all)`}),t.verbose){r.push({tone:"plain",text:""}),r.push({tone:"plain",text:"pages"});for(let i of e.pages){let c=i.findings>0?M(i.findings,"finding"):i.note?"":"ok",a=i.note?c?` (${i.note})`:i.note:"";r.push({tone:i.findings>0?"fail":"dim",text:` ${String(i.status??"---").padEnd(4)} ${le(i.url,e.origin)} ${c}${a}`})}}for(let i of pn(e))r.push({tone:"dim",text:i});return r}function un(e){return e.replace(/[\\`*_[\]<>|!#]/g,t=>`\\${t}`)}function gn(e){let t=[],r=e.startedAt.slice(0,10);if(e.status==="error")return t.push(`**Verirun scan of ${e.startUrl}** could not run: ${e.error?.message??"scan failed"}`),`${t.join(`
|
|
62
|
+
`)}
|
|
63
|
+
`;let n=e.summary;return t.push(e.status==="clean"?`**Verirun scan of ${e.startUrl}**: no findings on ${M(n.pages,"page")} (${r}).`:`**Verirun scan of ${e.startUrl}**: ${M(n.findings,"finding")} on ${M(n.pages,"page")} (${r}).`),t.push(""),e.findings.forEach((o,i)=>{let c=o.count>o.pages.length?` (${o.count} times)`:"";t.push(`${i+1}. **${It[o.kind]}**: ${un(o.message)}${c}`),t.push(` - on: ${un(fn(o,e.origin,5))}`),o.evidence&&t.push(` - evidence: \`${o.evidence.replace(/`/g,"'")}\``)}),e.findings.length>0&&t.push(""),t.push(`Read-only scan (GET and HEAD requests only, robots.txt respected) by [VerirunScan](${xt}). ${pn(e).map(o=>o.charAt(0).toUpperCase()+o.slice(1)).join(". ")}.`),`${t.join(`
|
|
64
|
+
`)}
|
|
65
|
+
`}var na={clean:0,findings:1,error:2};function Ge(e,t,r,n){if(t===void 0)return r;let o=Number(t);if(!Number.isInteger(o)||o<n)throw new Error(`${e} needs a whole number of at least ${n}, got "${t}".`);return o}function oa(e){return async(t,{method:r,maxBytes:n})=>{let o;try{o=await B(t,{method:r,redirect:"follow",headers:{"User-Agent":We,Accept:"text/html,application/xhtml+xml,*/*;q=0.8"},signal:AbortSignal.timeout(e)})}catch(a){let l=a instanceof Error&&a.name==="TimeoutError";throw new Error(l?`timed out after ${Math.round(e/1e3)}s`:ve(a)??(a instanceof Error?a.message:String(a)))}let i=o.headers.get("content-type")??void 0,c;if(r==="GET"&&o.body){let a=o.body.getReader(),l=[],d=0;for(;d<n;){let{done:u,value:m}=await a.read();if(u)break;l.push(m),d+=m.byteLength}await a.cancel().catch(()=>{}),c=Buffer.concat(l).subarray(0,n).toString("utf8")}else await o.body?.cancel().catch(()=>{});return{status:o.status,finalUrl:o.url||t,...i?{contentType:i}:{},...c!==void 0?{body:c}:{}}}}var sa={pass:e=>s.green(e),fail:e=>s.red(e),warn:e=>s.yellow(e),dim:e=>s.dim(e),plain:e=>e};async function yn(e,t){let{assertLicensed:r}=await import("./gate.mjs"),n=await r();n.note&&s.warn(n.note);let o=Ge("--max-pages",t.maxPages,Rt,1),i=Ge("--depth",t.depth,Et,0),c=Ge("--delay",t.delay,Ct,0),a=Ge("--timeout",t.timeout,2e4,1e3);if(t.json&&t.md)throw new Error("Pick one of --json and --md.");let d=!!!(t.json||t.md)&&!!process.stderr.isTTY,u;if(t.browser===!1)u=dn(oa(a));else{let{createBrowserLoader:p}=await Promise.resolve().then(()=>(wn(),hn));u=await p({timeoutMs:a})}let m;try{m=await cn(e,u,{maxPages:o,maxDepth:i,delayMs:c,onEvent:p=>{d&&(p.type==="page"?process.stderr.write(`${s.dim(` ${p.status??"---"} ${p.url}`)}
|
|
66
|
+
`):p.type==="probing"&&process.stderr.write(`${s.dim(` checking ${p.count} more link(s)`)}
|
|
67
|
+
`))}})}finally{await u.close?.()}if(t.json)process.stdout.write(`${JSON.stringify(m,null,t.verbose?2:void 0)}
|
|
68
|
+
`);else if(t.md)process.stdout.write(gn(m));else for(let p of mn(m,{verbose:t.verbose}))s.info(sa[p.tone](p.text));process.exitCode=na[m.status]}import{existsSync as bn}from"node:fs";import{readFile as ia}from"node:fs/promises";import vn from"node:path";var aa=new Set(["navigate","click","dblclick","fill","press","check","uncheck","select","setInputFiles","openPage","closePage"]),ca=/^assert/,la=new Set(["click","dblclick","fill","press","check","uncheck","select","setInputFiles"]),da={navigate:["url"],fill:["text"],press:["key"],select:["values"],setInputFiles:["files"]};function ua(e){let t=[],r=(o,i,c)=>{t.push({step:o,level:i,message:c})};e.length===0&&t.push({step:0,level:"error",message:"no steps"}),e.forEach((o,i)=>{let c=i+1;if(typeof o.action!="string"||o.action===""){r(c,"error",'missing "action"');return}if(ca.test(o.action)){r(c,"warning",`"${o.action}" is skipped at replay \u2014 it asserts nothing`);return}if(!aa.has(o.action)){r(c,"error",`unknown action "${o.action}" \u2014 a run stops here`);return}la.has(o.action)&&!o.selector&&r(c,"error",`"${o.action}" needs a "selector"`);for(let a of da[o.action]??[])o.params?.[a]===void 0&&r(c,"error",`"${o.action}" needs params.${a}`);if(o.action==="navigate"){let a=o.params?.url;typeof a=="string"&&!/^https?:\/\//.test(a)&&r(c,"error",`params.url must be absolute, got "${a}"`)}if(o.dialog!==void 0){let a=o.dialog;!a||typeof a!="object"||a.action!=="accept"&&a.action!=="dismiss"?r(c,"error",'dialog.action must be "accept" or "dismiss"'):a.promptText!==void 0&&typeof a.promptText!="string"?r(c,"error","dialog.promptText must be a string"):a.promptText!==void 0&&a.action==="dismiss"&&r(c,"warning","dialog.promptText is ignored when the dialog is dismissed")}o.selector?.startsWith("role=")&&!/\[name=/.test(o.selector)&&r(c,"warning",`"${o.selector}" matches every such element on the page`),typeof o.index!="number"&&r(c,"warning",`missing "index" \u2014 set it to the step's position`)}),e.length>0&&e[0]?.action!=="navigate"&&t.push({step:1,level:"warning",message:'first step is not "navigate" \u2014 replay opens a blank page, so start with one'});let n=e.map((o,i)=>({step:o,n:i+1})).filter(({step:o,n:i})=>i>1&&o.action==="navigate");for(let{n:o}of n)t.push({step:o,level:"warning",message:"navigating by URL skips the UI \u2014 click the link or button that goes there instead (`verirun map --bfs <route>` names it). Fine only for a deep link with no path to it."});return t}function fa(e,t){let r=new Map,n=i=>{if(!i.selector)return;let c=r.get(i.selector);c||r.set(i.selector,c={dialogs:new Map,allOpenOne:!0}),(!i.dialogs||i.dialogs.length===0)&&(c.allOpenOne=!1);for(let a of i.dialogs??[])c.dialogs.set(`${a.type}|${a.message??a.messageRaw??""}`,a)};for(let i of t.nodes?.pages??[])for(let c of i.elements??[])n(c);for(let i of t.edges??[])n(i.trigger);let o=[];return e.forEach((i,c)=>{if(i.dialog||!i.selector)return;let a=r.get(i.selector);if(a?.allOpenOne)for(let l of a.dialogs.values()){let d=l.message!==void 0?` ${JSON.stringify(l.message)}`:"";o.push({step:c+1,level:"warning",message:`opens ${l.type}${d} and has no "dialog" \u2014 replay answers OK. `+(l.gates?'The handler reads the answer, so set "dialog": {"action": "accept"|"dismiss"} to pick the path.':'Set "dialog" to make that explicit.')})}}),o}async function pa(e){let t=vn.join(e,"map","interaction-tree.json");if(bn(t))try{return{file:t,tree:JSON.parse(await ia(t,"utf8"))}}catch{return}}var ma=1;async function kn(e,t,r){if(e!=="check")throw new Error(`Unknown workflow action "${e}". Only "check" exists.`);if(!t)throw new Error("workflow check needs a workflow name or a path to a .json file.");let n=P(),o;if(t.endsWith(".json")?o=vn.resolve(t):(ce(t),o=ie(n,t)),!bn(o))throw new Error(`No such workflow file: ${o}`);let i=await Oe(o),c=ua(i.steps),a=await pa(n.dataDir);a&&(c.push(...fa(i.steps,a.tree)),c.sort((p,f)=>p.step-f.step));let l=c.filter(p=>p.level==="error");if(r.json){process.stdout.write(`${JSON.stringify({schemaVersion:ma,tool:"verirun-workflow-check",file:o,name:i.name,steps:i.steps.length,ok:l.length===0,findings:c})}
|
|
69
|
+
`),l.length>0&&(process.exitCode=U.failed);return}let d=c.length-l.length,u=(p,f)=>`${p} ${f}${p===1?"":"s"}`,m=u(i.steps.length,"step");l.length>0?(s.info(s.red(`FAIL ${i.name} ${u(l.length,"error")}${d>0?`, ${u(d,"warning")}`:""} in ${m}: a run would stop`)),process.exitCode=U.failed):s.info(s.green(`PASS ${i.name} ${m}, ${d>0?u(d,"warning"):"no findings"} (static check: selectors are not resolved against a page)`));for(let p of c)s.info(` step ${String(p.step).padEnd(3)} ${p.level==="error"?"error":"warn "} ${p.message}`);a||s.info(s.dim("Run `verirun map --report` once and this also flags steps that open a browser dialog."))}var Sn="yPRa0NTdWzWt6LlYHOwmu3TlkKma36/6dySVcNaFMiq8X3HEdR0lp2pu0IYjLsOU03zOIeY394W5ppbtTpdXDg==",xn="MCowBQYDK2VwAyEATFTl78zmb+BITZuy9mdPMtujnz1tAeVidJT3dccuhLg=";function ka(){if(Sn.startsWith("@@")||xn.startsWith("@@"))return!0;try{let e=$n.dirname(ya(import.meta.url)),t=wa($n.join(e,"gate.mjs")),r=ga({key:Buffer.from(xn,"base64"),format:"der",type:"spki"});return ha(null,t,r,Buffer.from(Sn,"base64"))}catch{return!1}}var W=new ba;W.exitOverride();W.name("verirun").description("Record and replay app workflows, and capture backend logs for each run.").version(ne).addHelpText("after",`
|
|
70
|
+
Exit codes: 0 ok, 1 the check failed (a replay step, scan findings, workflow errors),
|
|
71
|
+
2 could not run (usage, setup, license, unreachable target).`);W.command("record").argument("<name>","name for the workflow").description("Record a workflow in a headed browser and save it to .verirun/workflows").option("-u, --url <url>","URL to open the browser at").option("-b, --browser <browser>","browser to record in: chromium, firefox or webkit").option("--channel <channel>","use an installed browser: chrome, msedge, chrome-beta, ...").option("--default-browser","record in your OS default browser (falls back to bundled Chromium)").option("--user-data-dir <dir>","record in a persistent profile directory (keeps you logged in)").option("--test-id-attribute <attribute>","attribute to prefer for getByTestId selectors").option("-f, --force","overwrite an existing workflow with this name").option("--json","print the result as JSON (schemaVersion 1); everything else goes to stderr").option("--verbose","list every recorded step after saving").action(async(e,t)=>{await Lr(e,t)});W.command("run").argument("<name>","name of the workflow to replay").description("Replay a recorded workflow and capture Supabase / Vercel / Railway logs for the run window").option("--headed","run with a visible browser window instead of headless (paced so you can watch)").option("--slow-mo <ms>","pause before each action; default 500 when --headed, 0 otherwise").option("--timeout <ms>","per-action timeout in milliseconds (default: Playwright 30000)").option("--channel <channel>","replay in an installed browser: chrome, msedge, ...").option("--user-data-dir <dir>","replay in a persistent profile directory (keeps you logged in)").option("--cdp <url>","attach to a browser already running with --remote-debugging-port, e.g. http://localhost:9222").option("--user <id>","keep only log entries mentioning this string, e.g. an email (overrides config.userFilter)").option("--dialog <answer>","answer browser dialogs a step has no saved answer for: accept (OK, default) or dismiss (Cancel)").option("--json","print the run summary as JSON (schemaVersion 1): verdict, failed step, error, deduped error-level log lines").option("--verbose","also print each step as it replays, every distinct error line, and the full error text").action(async(e,t)=>{await Wr(e,t)});W.command("login").description("Connect this machine to your Verirun account").option("-f, --force","connect even if this machine is already linked").option("--server <url>","account server base URL (default: https://get-verirun.duely.in)").action(async e=>{await Wt(e)});W.command("logout").description("Disconnect this machine from your Verirun account").action(async()=>{await Bt()});W.command("whoami").description("Show the connected account and subscription status").option("--server <url>","account server base URL").option("--json","print the status as JSON (schemaVersion 1)").action(async e=>{await Gt(e)});W.command("map").description("Build an interaction graph of this repo: pages, the elements on them, and the backend calls they make").option("-d, --dir <dir>","directory to analyze (default: the repo root)").option("--json","print the interaction graph as JSON on one line (with --bfs or --reach: that result instead)").option("--verbose","indent the --json output").option("--report","write interaction-tree.json + coverage.md (default: .verirun/map)").option("--out <dir>","output directory for --report").option("--reach","per-route table of reachable vs attached elements").option("--bfs <node>","walk the graph from a node: a route path, page id or component name").option("--depth <n>","steps to follow for --bfs (default: 2)").option("--kind <kind>","--bfs: follow only nav or only effect edges").option("--alias <prefix=dir...>","extra import alias, e.g. @/=src (tsconfig paths are read automatically)").option("--no-follow-children","do not follow child components out of a page file").option("--app-file <file>","file declaring <Routes> (react-router apps; auto-detected by default)").action(async e=>{await br(e)});W.command("scan").argument("<url>","public URL to scan, e.g. https://example.com").description("Read-only scan of a live site: JS errors, failed requests, dead links, missing anchors. No source or logs needed").option("--max-pages <n>","stop after this many pages (default: 20)").option("--depth <n>","follow same-origin links this many hops from the start URL (default: 2)").option("--delay <ms>","pause before each page load, per connection; 4 load in parallel (default: 500; a robots.txt Crawl-delay wins and means one at a time)").option("--timeout <ms>","per-page load timeout (default: 20000)").option("--no-browser","plain HTTP instead of headless Chromium: no JavaScript runs, so no JS or console errors").option("--json","print the report as JSON (schemaVersion 1)").option("--md","print the report as Markdown, ready to paste into a reply or an issue").option("--verbose","list every finding with its evidence, and every page scanned").action(async(e,t)=>{await yn(e,t)});W.command("workflow").argument("<action>","what to do: check").argument("[name]","workflow name, or a path to a .json file").description("Check a workflow file without running it (shape, actions, selectors, params)").option("--json","emit the findings as JSON").action(async(e,t,r)=>{await kn(e,t,r)});W.command("agent").description("Print a compact reference (what to run, how to read the result) for an AI coding agent").option("--topic <name>","print one on-request section: authoring, signed-in or schemas").option("--full","print the reference with every on-request section included").option("--json","emit JSON instead of Markdown").option("--skill","emit an agent skill file (Claude Code, Cursor, Codex) instead of the plain reference").option("--install [agent]","with --skill: write it where the agent looks \u2014 claude (default), cursor, codex (.agents/skills + an AGENTS.md section), or all").action(async e=>{await rr(W,e)});W.command("log").argument("[source]","log source to connect: vercel, supabase or railway (omit for an overview)").description("Connect a log source and check its configuration").option("--test","query the last 5 minutes and report what comes back").option("--drain","vercel: set up the old push-based log drain instead of the API").option("--listen","vercel: run the drain listener until Ctrl+C (drain mode only)").option("--reconnect","re-run the connect step and replace what was saved").action(async(e,t)=>{await fr(e,t)});async function $a(){ka()||(process.stderr.write(`verirun: this installation looks modified. Reinstall it:
|
|
40
72
|
|
|
41
73
|
npm install -g @verirun/cli
|
|
42
|
-
`),process.exit(1));try{await
|
|
74
|
+
`),process.exit(1));try{await W.parseAsync(process.argv)}catch(e){if(e instanceof va){process.exitCode=e.exitCode===0?U.ok:U.error;return}let t=e instanceof Error?e.message:String(e);s.error(t),process.argv.includes("--json")&&process.stdout.write(`${JSON.stringify({schemaVersion:1,tool:"verirun",status:"error",error:{message:t}})}
|
|
75
|
+
`),process.exitCode=U.error}}$a();
|