document360-capture 0.2.13 → 0.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +11 -11
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import{Command as oo}from"commander";import{createRequire as ro}from"node:module";import{input as oe,confirm as tt}from"@inquirer/prompts";import{existsSync as et,readFileSync as re}from"node:fs";import{basename as ne,join as ie}from"node:path";import{readFileSync as z,writeFileSync as qt,existsSync as K,mkdirSync as Gt}from"node:fs";import{basename as zt,dirname as Kt,join as T}from"node:path";var Xt=T(".d360-writer","capture.json"),j={captureDir:".d360-writer/user-docs/_capture",outputDir:".d360-writer/user-docs/_screenshots",viewport:{width:1440,height:900},loginTimeoutMs:0,annotation:{enabled:!0}},b=class extends Error{};function g(t=process.cwd()){return T(t,Xt)}function m(t){if(!K(t))throw new Error(`No project config at ${t}. Run \`d360-capture init\` to create one.`);let e=JSON.parse(z(t,"utf8").replace(/^/,""));return Qt(e,t)}function O(t,e){Gt(Kt(t),{recursive:!0}),qt(t,JSON.stringify(e,null,2)+`
3
- `,"utf8")}function Yt(t){try{return"^"+new URL(t).origin.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}catch{return"."}}function Qt(t,e,o=process.cwd()){Vt(t,e);let r=Object.keys(t.profiles),n=t.defaultProfile??(r.length===1?r[0]:r.includes("staging")?"staging":r[0]);return{projectId:t.projectId||Zt(o)||zt(o),captureDir:t.captureDir??j.captureDir,outputDir:t.outputDir??j.outputDir,viewport:t.viewport??{...j.viewport},loginTimeoutMs:t.loginTimeoutMs??j.loginTimeoutMs,annotation:t.annotation??{...j.annotation},profiles:t.profiles,defaultProfile:n}}function $(t,e){let o=e??t.defaultProfile,r=t.profiles[o];if(!r){let i=Object.keys(t.profiles).join(", ");throw new b(`Unknown profile "${o}". Available: ${i}`)}let n=r.connection.authBoundaryUrlPattern;return{name:o,connection:{startUrl:r.connection.startUrl,authBoundaryUrlPattern:n||Yt(r.connection.startUrl)},captureDir:r.captureDir??t.captureDir,outputDir:r.outputDir??t.outputDir,viewport:r.viewport??t.viewport,production:r.production===!0,scope:r.scope??{},authBoundaryDefaulted:!n}}function Vt(t,e){if(t.environments&&!t.profiles)throw new b(`Config at ${e} uses the old "environments" shape, which is no longer supported. It is now "profiles" (each with a "connection" { startUrl }) + "defaultProfile". Re-run \`d360-capture init\`, then \`d360-capture auth --profile <name>\`.`);if(!t.profiles||Object.keys(t.profiles).length===0)throw new b(`Invalid config at ${e}: needs at least one profile under "profiles".`);for(let[o,r]of Object.entries(t.profiles))if(!r?.connection?.startUrl)throw new b(`Invalid config at ${e}: profile "${o}" is missing connection.startUrl.`);if(t.defaultProfile&&!t.profiles[t.defaultProfile])throw new b(`Invalid config at ${e}: defaultProfile "${t.defaultProfile}" is not in profiles.`)}function Zt(t){let e=T(t,"package.json");if(!K(e))return null;try{return JSON.parse(z(e,"utf8")).name??null}catch{return null}}import{existsSync as X,readFileSync as te}from"node:fs";import{join as Y}from"node:path";var ee=Y(".d360-writer","config.json");function Q(t=process.cwd()){return Y(t,ee)}function V(t=process.cwd()){return X(Q(t))}function Z(t=process.cwd()){let e=Q(t);if(!X(e))return{};try{let o=JSON.parse(te(e,"utf8").replace(/^/,"")),r=o.docsDir?.replace(/\/+$/,"");return{projectId:o.projectId,captureDir:o.captureDir??(r?`${r}/_capture`:void 0),outputDir:o.outputDir??(r?`${r}/_screenshots`:void 0)}}catch{return{}}}async function ot(t={}){let e=process.cwd(),o=g(e),r=t.yes===!0;if(et(o)&&!r&&!await tt({message:`${o} already exists. Overwrite?`,default:!1})){console.log("Init cancelled.");return}let n=Z(e),i=n.projectId||se(e)||ne(e),s=[n.projectId&&"project name",n.captureDir&&"directories"].filter(Boolean);s.length>0?(console.log(""),console.log(`Found .d360-writer/config.json \u2014 inheriting ${s.join(" + ")}.`)):V(e)&&(console.log(""),console.log("Found .d360-writer/config.json (could not read defaults from it \u2014 using built-in defaults)."));let c=r?"https://example.com/":await oe({message:"Live app URL (where captures start):",default:"https://example.com/",validate:a=>a.startsWith("http")?!0:"Must be a full URL"}),l=r?!1:await tt({message:"Is this URL your production environment?",default:!1}),p=l?"production":"staging",u={projectId:i,...n.captureDir?{captureDir:n.captureDir}:{},...n.outputDir?{outputDir:n.outputDir}:{},profiles:{[p]:{connection:{startUrl:c},...l?{production:!0}:{}}},defaultProfile:p};O(o,u),console.log(""),console.log(`\u2713 Wrote ${o}`),r&&c==="https://example.com/"&&console.log(` \u26A0 Set the ${p} profile's connection.startUrl to your real app URL.`),console.log(""),console.log("Next steps:"),console.log(" 1. d360-capture auth # log in once, no timeout"),console.log(" 2. d360-capture capture # run all specs in the capture dir"),console.log(""),console.log("Add more profiles or tune the auth boundary later by editing .d360-writer/capture.json."),console.log("")}function se(t){let e=ie(t,"package.json");if(!et(e))return null;try{return JSON.parse(re(e,"utf8")).name??null}catch{return null}}import{chromium as me}from"playwright";import{existsSync as rt,mkdirSync as ae,statSync as ce}from"node:fs";import{homedir as le}from"node:os";import{join as E,dirname as xo}from"node:path";function nt(){return E(le(),".document360-capture","auth-states")}function R(t,e){return E(nt(),t,`${e}.json`)}function it(t){let e=E(nt(),t);rt(e)||ae(e,{recursive:!0})}function x(t,e){let o=R(t,e);if(!rt(o))return{path:o,exists:!1};let r=ce(o);return{path:o,exists:!0,ageMs:Date.now()-r.mtimeMs}}import{existsSync as pe}from"node:fs";import{spawn as ue}from"node:child_process";import{createRequire as fe}from"node:module";import{chromium as de}from"playwright";var ge=fe(import.meta.url);function L(){try{return de.executablePath()}catch{return null}}function U(){let t=L();return t!=null&&pe(t)}async function N(){if(U())return;console.log(""),console.log("\u25B6 Chromium is not downloaded on this machine yet \u2014 fetching it now (one-time)\u2026");let t=ge.resolve("playwright/cli.js"),e=await new Promise((o,r)=>{let n=ue(process.execPath,[t,"install","chromium"],{stdio:"inherit"});n.on("error",r),n.on("exit",i=>o(i??1))});if(e!==0||!U())throw new Error(`Chromium download failed (exit code ${e}). Check your network/proxy and retry, or run:
4
- node "${t}" install chromium`);console.log("\u2713 Chromium ready.")}async function st(t){let e=m(g()),o=$(e,t.profile);it(e.projectId);let r=R(e.projectId,o.name);await N(),console.log(""),o.production&&console.log(`\u26A0 Profile "${o.name}" is PRODUCTION.`),console.log(`Opening Chromium against ${o.connection.startUrl}`),console.log(`Auth-state will save to: ${r}`),console.log("");let n=await me.launch({headless:!1,args:["--start-maximized"]}),i=await n.newContext({viewport:null}),s=await i.newPage();await s.goto(o.connection.startUrl,{waitUntil:"domcontentloaded"}),console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),console.log(" Log in fully (MFA included) at your own pace."),console.log(" When you reach the post-login app, come back to this"),console.log(" terminal and press ENTER to save the session."),console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),console.log("");let c=new RegExp(o.connection.authBoundaryUrlPattern),l=setInterval(()=>{c.test(s.url())&&i.storageState({path:r}).catch(()=>{})},3e4);try{for(;;){await he();let p=s.url();if(c.test(p))break;console.log(""),console.log("\u2717 Login doesn't look complete yet \u2014 the browser is still at:"),console.log(` ${p}`),console.log(" Finish logging in until you can see the app, then press ENTER again."),console.log(" (Ctrl+C aborts without saving.)")}await i.storageState({path:r}),console.log(""),console.log(`\u2713 Session saved to ${r}`),console.log(" This file contains session cookies \u2014 never commit it to git.")}finally{clearInterval(l),await n.close().catch(()=>{})}}function he(){return new Promise(t=>{let e=()=>{process.stdin.off("data",e),process.stdin.pause(),t()};process.stdin.resume(),process.stdin.once("data",e)})}import{existsSync as Te,readdirSync as Ee}from"node:fs";import{isAbsolute as Dt,resolve as jt}from"node:path";import{writeFileSync as at,mkdirSync as we,existsSync as ct}from"node:fs";import{createRequire as ye}from"node:module";import{join as k,isAbsolute as lt,resolve as pt}from"node:path";var M=ye(import.meta.url);function ut(t){let{cwd:e,profileName:o,captureDir:r,viewport:n,storageStatePath:i}=t,s=k(e,".d360-writer","cache");ct(s)||we(s,{recursive:!0});let c=k(s,`playwright.${o}.config.cjs`),l=lt(r)?r:pt(e,r),p=lt(i)?i:pt(e,i),u=M.resolve("@playwright/test"),a=`// Generated by document360-capture \u2014 do not edit by hand.
2
+ import{Command as io}from"commander";import{createRequire as so}from"node:module";import{input as ne,confirm as ot}from"@inquirer/prompts";import{existsSync as rt,readFileSync as ie}from"node:fs";import{basename as se,join as ae}from"node:path";import{readFileSync as K,writeFileSync as zt,existsSync as X,mkdirSync as Yt}from"node:fs";import{basename as Kt,dirname as Xt,join as E}from"node:path";var Qt=E(".d360-writer","capture.json"),k={captureDir:".d360-writer/user-docs/_capture",outputDir:".d360-writer/user-docs/_screenshots",viewport:{width:1440,height:900},loginTimeoutMs:0,annotation:{enabled:!0}},D=class extends Error{};function g(t=process.cwd()){return E(t,Qt)}function h(t){if(!X(t))throw new Error(`No project config at ${t}. Run \`d360-capture init\` to create one.`);let e=JSON.parse(K(t,"utf8").replace(/^/,""));return Zt(e,t)}function U(t,e){Yt(Xt(t),{recursive:!0}),zt(t,JSON.stringify(e,null,2)+`
3
+ `,"utf8")}function Vt(t){try{return"^"+new URL(t).origin.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}catch{return"."}}function Zt(t,e,o=process.cwd()){te(t,e);let r=Object.keys(t.profiles),n=t.defaultProfile??(r.length===1?r[0]:r.includes("staging")?"staging":r[0]);return{projectId:t.projectId||ee(o)||Kt(o),captureDir:t.captureDir??k.captureDir,outputDir:t.outputDir??k.outputDir,viewport:t.viewport??{...k.viewport},loginTimeoutMs:t.loginTimeoutMs??k.loginTimeoutMs,annotation:t.annotation??{...k.annotation},profiles:t.profiles,defaultProfile:n}}function S(t,e){let o=e??t.defaultProfile,r=t.profiles[o];if(!r){let i=Object.keys(t.profiles).join(", ");throw new D(`Unknown profile "${o}". Available: ${i}`)}let n=r.connection.authBoundaryUrlPattern;return{name:o,connection:{startUrl:r.connection.startUrl,authBoundaryUrlPattern:n||Vt(r.connection.startUrl)},captureDir:r.captureDir??t.captureDir,outputDir:r.outputDir??t.outputDir,viewport:r.viewport??t.viewport,production:r.production===!0,scope:r.scope??{},authBoundaryDefaulted:!n}}function te(t,e){if(t.environments&&!t.profiles)throw new D(`Config at ${e} uses the old "environments" shape, which is no longer supported. It is now "profiles" (each with a "connection" { startUrl }) + "defaultProfile". Re-run \`d360-capture init\`, then \`d360-capture auth --profile <name>\`.`);if(!t.profiles||Object.keys(t.profiles).length===0)throw new D(`Invalid config at ${e}: needs at least one profile under "profiles".`);for(let[o,r]of Object.entries(t.profiles))if(!r?.connection?.startUrl)throw new D(`Invalid config at ${e}: profile "${o}" is missing connection.startUrl.`);if(t.defaultProfile&&!t.profiles[t.defaultProfile])throw new D(`Invalid config at ${e}: defaultProfile "${t.defaultProfile}" is not in profiles.`)}function ee(t){let e=E(t,"package.json");if(!X(e))return null;try{return JSON.parse(K(e,"utf8")).name??null}catch{return null}}import{existsSync as Q,readFileSync as oe}from"node:fs";import{join as V}from"node:path";var re=V(".d360-writer","config.json");function Z(t=process.cwd()){return V(t,re)}function tt(t=process.cwd()){return Q(Z(t))}function et(t=process.cwd()){let e=Z(t);if(!Q(e))return{};try{let o=JSON.parse(oe(e,"utf8").replace(/^/,"")),r=o.docsDir?.replace(/\/+$/,"");return{projectId:o.projectId,captureDir:o.captureDir??(r?`${r}/_capture`:void 0),outputDir:o.outputDir??(r?`${r}/_screenshots`:void 0),appUrl:typeof o.appUrl=="string"&&o.appUrl.startsWith("http")?o.appUrl:void 0}}catch{return{}}}async function nt(t={}){let e=process.cwd(),o=g(e),r=t.yes===!0;if(rt(o)&&!r&&!await ot({message:`${o} already exists. Overwrite?`,default:!1})){console.log("Init cancelled.");return}let n=et(e),i=n.projectId||ce(e)||se(e),s=[n.projectId&&"project name",n.captureDir&&"directories"].filter(Boolean);s.length>0?(console.log(""),console.log(`Found .d360-writer/config.json \u2014 inheriting ${s.join(" + ")}.`)):tt(e)&&(console.log(""),console.log("Found .d360-writer/config.json (could not read defaults from it \u2014 using built-in defaults)."));let a=r?n.appUrl??"https://example.com/":await ne({message:"Live app URL (where captures start):",default:n.appUrl??"https://example.com/",validate:c=>c.startsWith("http")?!0:"Must be a full URL"}),l=r?!1:await ot({message:"Is this URL your production environment?",default:!1}),p=l?"production":"staging",u={projectId:i,...n.captureDir?{captureDir:n.captureDir}:{},...n.outputDir?{outputDir:n.outputDir}:{},profiles:{[p]:{connection:{startUrl:a},...l?{production:!0}:{}}},defaultProfile:p};U(o,u),console.log(""),console.log(`\u2713 Wrote ${o}`),r&&a==="https://example.com/"&&console.log(` \u26A0 Set the ${p} profile's connection.startUrl to your real app URL.`),console.log(""),console.log("Next steps:"),console.log(" 1. d360-capture auth # log in once, no timeout"),console.log(" 2. d360-capture capture # run all specs in the capture dir"),console.log(""),console.log("Add more profiles or tune the auth boundary later by editing .d360-writer/capture.json."),console.log("")}function ce(t){let e=ae(t,"package.json");if(!rt(e))return null;try{return JSON.parse(ie(e,"utf8")).name??null}catch{return null}}import{chromium as ye}from"playwright";import{existsSync as it,mkdirSync as le,statSync as pe}from"node:fs";import{homedir as ue}from"node:os";import{join as L,dirname as Co}from"node:path";function st(){return L(ue(),".document360-capture","auth-states")}function A(t,e){return L(st(),t,`${e}.json`)}function at(t){let e=L(st(),t);it(e)||le(e,{recursive:!0})}function C(t,e){let o=A(t,e);if(!it(o))return{path:o,exists:!1};let r=pe(o);return{path:o,exists:!0,ageMs:Date.now()-r.mtimeMs}}import{existsSync as fe}from"node:fs";import{spawn as de}from"node:child_process";import{createRequire as ge}from"node:module";import{chromium as me}from"playwright";var he=ge(import.meta.url);function M(){try{return me.executablePath()}catch{return null}}function O(){let t=M();return t!=null&&fe(t)}async function N(){if(O())return;console.log(""),console.log("\u25B6 Chromium is not downloaded on this machine yet \u2014 fetching it now (one-time)\u2026");let t=he.resolve("playwright/cli.js"),e=await new Promise((o,r)=>{let n=de(process.execPath,[t,"install","chromium"],{stdio:"inherit"});n.on("error",r),n.on("exit",i=>o(i??1))});if(e!==0||!O())throw new Error(`Chromium download failed (exit code ${e}). Check your network/proxy and retry, or run:
4
+ node "${t}" install chromium`);console.log("\u2713 Chromium ready.")}async function ct(t){let e=h(g()),o=S(e,t.profile);at(e.projectId);let r=A(e.projectId,o.name);await N(),console.log(""),o.production&&console.log(`\u26A0 Profile "${o.name}" is PRODUCTION.`),console.log(`Opening Chromium against ${o.connection.startUrl}`),console.log(`Auth-state will save to: ${r}`),console.log("");let n=await ye.launch({headless:!1,args:["--start-maximized"]}),i=await n.newContext({viewport:null}),s=await i.newPage();await s.goto(o.connection.startUrl,{waitUntil:"domcontentloaded"}),console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),console.log(" Log in fully (MFA included) at your own pace."),console.log(" When you reach the post-login app, come back to this"),console.log(" terminal and press ENTER to save the session."),console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),console.log("");let a=new RegExp(o.connection.authBoundaryUrlPattern),l=setInterval(()=>{a.test(s.url())&&i.storageState({path:r}).catch(()=>{})},3e4);try{for(;;){await we();let p=s.url();if(a.test(p))break;console.log(""),console.log("\u2717 Login doesn't look complete yet \u2014 the browser is still at:"),console.log(` ${p}`),console.log(" Finish logging in until you can see the app, then press ENTER again."),console.log(" (Ctrl+C aborts without saving.)")}await i.storageState({path:r}),console.log(""),console.log(`\u2713 Session saved to ${r}`),console.log(" This file contains session cookies \u2014 never commit it to git.")}finally{clearInterval(l),await n.close().catch(()=>{})}}function we(){return new Promise(t=>{let e=()=>{process.stdin.off("data",e),process.stdin.pause(),t()};process.stdin.resume(),process.stdin.once("data",e)})}import{existsSync as Me,readdirSync as _e}from"node:fs";import{isAbsolute as Rt,resolve as kt}from"node:path";import{writeFileSync as lt,mkdirSync as Pe,existsSync as pt}from"node:fs";import{createRequire as ve}from"node:module";import{join as I,isAbsolute as ut,resolve as ft}from"node:path";var _=ve(import.meta.url);function dt(t){let{cwd:e,profileName:o,captureDir:r,viewport:n,storageStatePath:i}=t,s=I(e,".d360-writer","cache");pt(s)||Pe(s,{recursive:!0});let a=I(s,`playwright.${o}.config.cjs`),l=ut(r)?r:ft(e,r),p=ut(i)?i:ft(e,i),u=_.resolve("@playwright/test"),c=`// Generated by document360-capture \u2014 do not edit by hand.
5
5
  const { defineConfig } = require(${JSON.stringify(u)});
6
6
  module.exports = defineConfig({
7
7
  testDir: ${JSON.stringify(l)},
@@ -19,11 +19,11 @@ module.exports = defineConfig({
19
19
  contextOptions: { reducedMotion: 'reduce' },
20
20
  },
21
21
  });
22
- `;at(c,a,"utf8");let f=k(s,`tsconfig.${o}.json`),P=A=>A.replace(/\\/g,"/"),w=M.resolve("document360-capture/helpers"),y=k(M.resolve("@playwright/test/package.json"),".."),h=k(y,"index.mjs"),C=ct(h)?h:u,S={compilerOptions:{baseUrl:".",paths:{"document360-capture/helpers":[P(w)],"@playwright/test":[P(C)]}}};return at(f,JSON.stringify(S,null,2),"utf8"),{configPath:c,tsconfigPath:f}}import{spawn as Pe}from"node:child_process";import{existsSync as ve}from"node:fs";import{createRequire as $e}from"node:module";import{fileURLToPath as xe}from"node:url";import{delimiter as be,dirname as B,join as ft}from"node:path";function Ce(t,e){return t!==0?"failed":/\b\d+\s+skipped\b/.test(e)&&!/\b\d+\s+passed\b/.test(e)?"skipped":"passed"}var Se=$e(import.meta.url);async function dt(t){let{cwd:e,configPath:o,tsconfigPath:r,startUrl:n,authBoundaryUrlPattern:i,specId:s,captureDirAbs:c}=t,l=Re();if(!l)return{exitCode:127,stdout:"",stderr:"Cannot locate @playwright/test CLI. Try: npm install -g document360-capture (reinstall)",outcome:"failed"};let p=[l,"test","--config",o,"--tsconfig",r];return s&&p.push(De(`${s}.spec.ts`)),new Promise(u=>{let a="",f="",P=Pe(process.execPath,p,{cwd:e,env:{...process.env,CAPTURE_START_URL:n,CAPTURE_AUTH_BOUNDARY:i,CAPTURE_SCOPE:JSON.stringify(t.scope??{}),NODE_PATH:je()},shell:!1}),w=()=>{try{P.kill()}catch{}};t.signal&&(t.signal.aborted?w():t.signal.addEventListener("abort",w,{once:!0})),P.stdout.on("data",y=>{let h=y.toString();a+=h,process.stdout.write(h)}),P.stderr.on("data",y=>{let h=y.toString();f+=h,process.stderr.write(h)}),P.on("error",y=>{t.signal?.removeEventListener("abort",w),f+=`
23
- [spawn error] ${y.message}`,u({exitCode:127,stdout:a,stderr:f,outcome:"failed"})}),P.on("close",y=>{t.signal?.removeEventListener("abort",w);let h=y??1;u({exitCode:h,stdout:a,stderr:f,outcome:Ce(h,a)})})})}function De(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function je(){let t=B(xe(import.meta.url));for(let o=0;o<4&&!ve(ft(t,"package.json"));o++)t=B(t);let e=[ft(t,"node_modules"),B(t)];return process.env.NODE_PATH&&e.push(process.env.NODE_PATH),e.join(be)}function Re(){let t=["@playwright/test/cli","@playwright/test/cli.js","playwright/cli","playwright/cli.js"];for(let e of t)try{return Se.resolve(e)}catch{}return null}var ke=[/login\.microsoftonline\.com/i,/login\.live\.com/i,/accounts\.google\.com/i,/\/oauth(2)?\/authorize/i,/\/sign[- ]?in/i,/\bauthn\b/i],Ae=[/waitForURL.*timed?[- ]?out/i,/page\.goto.*redirected/i,/storageState.*invalid/i];function gt(t){return ke.some(e=>e.test(t))?!0:Ae.some(e=>e.test(t))}import{readdirSync as xt,readFileSync as Oe,existsSync as bt,copyFileSync as Ue}from"node:fs";import{join as Ct}from"node:path";import J from"sharp";function _(t){return t.replace(/[<>&"']/g,e=>({"<":"&lt;",">":"&gt;","&":"&amp;",'"':"&quot;","'":"&apos;"})[e])}function ht(t){let o=Math.max(0,t.x-4),r=Math.max(0,t.y-4),n=t.width+8,i=t.height+8;return`<rect x="${o}" y="${r}" width="${n}" height="${i}" rx="6" ry="6" fill="rgba(255,213,0,0.18)" stroke="#f5a623" stroke-width="2.5"/>`}function wt(t){return`<rect x="${t.x}" y="${t.y}" width="${t.width}" height="${t.height}" fill="#1f2328"/>`}var F="-apple-system,Segoe UI,sans-serif",mt=(t,e,o)=>Math.max(e,Math.min(o,t)),Ie=13;function yt(t,e,o,r){let n=Ie,i=mt(e.x,n,Math.max(n,o-n)),s=mt(e.y,n,Math.max(n,r-n));return`<circle cx="${i}" cy="${s}" r="${n}" fill="#d1242f" stroke="#fff" stroke-width="2"/><text x="${i}" y="${s+4}" font-family="${F}" font-size="13" font-weight="700" fill="#fff" text-anchor="middle">${_(t)}</text>`}function Pt(t,e){return t===0?0:e.pad*2+t*e.lineHeight}function vt(t,e){let{top:o,width:r,pad:n,lineHeight:i}=e,s=11,c=20+s,l=[`<line x1="0" y1="${o}" x2="${r}" y2="${o}" stroke="#d0d7de" stroke-width="1"/>`];return t.forEach((p,u)=>{let a=o+n+u*i+i/2;l.push(`<circle cx="${c}" cy="${a}" r="${s}" fill="#d1242f" stroke="#fff" stroke-width="1.5"/>`,`<text x="${c}" y="${a+4}" font-family="${F}" font-size="12" font-weight="700" fill="#fff" text-anchor="middle">${_(p.label)}</text>`,`<text x="${c+s+12}" y="${a+5}" font-family="${F}" font-size="14" fill="#1f2328">${_(p.text)}</text>`)}),l.join("")}async function Ne(t){let e=JSON.parse(Oe(t,"utf8"));if(!bt(e.rawPath))return{id:e.id,ok:!1,reason:`raw missing: ${e.rawPath}`};let o=await J(e.rawPath).metadata(),r=o.width??1440,n=o.height??900,i={pad:16,lineHeight:30},s=Pt(e.annotations.length,i),c=n+s,l=[];for(let a of e.highlight)l.push(ht(a.bbox));for(let a of e.redact)l.push(wt(a.bbox));for(let a of e.annotations)l.push(yt(a.label,a.bbox,r,n));if(s>0&&l.push(`<rect x="0" y="${n}" width="${r}" height="${s}" fill="#ffffff"/>`,vt(e.annotations.map(a=>({label:a.label,text:a.text})),{top:n,width:r,...i})),l.length===0)return await J(e.rawPath).toFile(e.finalPath),{id:e.id,ok:!0,finalPath:e.finalPath};let p=`<svg xmlns="http://www.w3.org/2000/svg" width="${r}" height="${c}">${l.join("")}</svg>`,u=J(e.rawPath);return s>0&&(u=u.extend({bottom:s,background:"#ffffff"})),await u.composite([{input:Buffer.from(p),top:0,left:0}]).toFile(e.finalPath),{id:e.id,ok:!0,finalPath:e.finalPath}}async function $t(t){let e=xt(t).filter(r=>r.endsWith(".raw.png")),o=[];for(let r of e){let n=Ct(t,r),i=n.replace(/\.raw\.png$/,".png");try{Ue(n,i),o.push({id:r.replace(/\.raw\.png$/,""),ok:!0,finalPath:i})}catch(s){o.push({id:r,ok:!1,reason:String(s)})}}return o}async function St(t,e){if(!bt(t))return[];if(!e)return $t(t);let o=xt(t).filter(n=>n.endsWith(".annotations.json"));if(o.length===0)return $t(t);let r=[];for(let n of o){let i=Ct(t,n);try{r.push(await Ne(i))}catch(s){r.push({id:n,ok:!1,reason:String(s)})}}return r}async function Rt(t,e){let o=process.cwd(),r=m(g(o)),n=$(r,e.profile),i=n.name,s=x(r.projectId,i);if(!s.exists)return console.error(""),console.error(`\u2717 No saved session for profile "${i}".`),console.error(` Expected at: ${s.path}`),console.error(` Run: d360-capture auth --profile ${i}`),2;let c=Dt(n.captureDir)?n.captureDir:jt(o,n.captureDir);if(!Te(c))return console.error(`\u2717 Capture directory missing: ${c}`),console.error(" Generate at least one spec there before running capture."),2;await N();let{configPath:l,tsconfigPath:p}=ut({cwd:o,profileName:i,captureDir:n.captureDir,viewport:n.viewport,storageStatePath:R(r.projectId,i)}),u=t?[t]:Le(c);if(u.length===0)return console.error(`\u2717 No .spec.ts files in ${n.captureDir}.`),2;console.log(""),n.production&&console.log(`\u26A0 Profile "${i}" is PRODUCTION (read-only capture).`),console.log(`\u25B6 Running ${u.length} spec${u.length===1?"":"s"} against ${i} (${n.connection.startUrl})`),t||console.log(" Press Esc (or Ctrl+C) in this terminal to stop after the current spec; press again to stop now.");let a=!1,f=null,P=Me(()=>{a?f?.abort():(a=!0,console.log(`
24
- \u23F8 Stopping after the current spec\u2026 (press again to stop now)`))}),w=[],y=!1;try{for(let d of u){if(a)break;f=new AbortController,console.log(`
25
- \u25B6 ${d}`);let I=await dt({cwd:o,configPath:l,tsconfigPath:p,startUrl:n.connection.startUrl,authBoundaryUrlPattern:n.connection.authBoundaryUrlPattern,specId:d,captureDirAbs:c,signal:f.signal,scope:n.scope});if(w.push({id:d,outcome:I.outcome}),I.exitCode!==0&&!y&&gt(`${I.stdout}
26
- ${I.stderr}`)&&(y=!0,console.error(""),console.error(" Output mentions a login page \u2014 if specs that used to pass now fail this way, your"),console.error(` saved session may have expired. Re-auth: d360-capture auth --profile ${i}`),console.error(" (Selector/route timeouts are spec bugs, not auth bugs \u2014 inspect the failure above.)")),f.signal.aborted)break}}finally{P(),f=null}let h=w.filter(d=>d.outcome==="passed").length,C=w.filter(d=>d.outcome==="failed"),S=w.filter(d=>d.outcome==="skipped"),A=u.length-w.length,G=!e.noAnnotate&&r.annotation.enabled,Wt=Dt(n.outputDir)?n.outputDir:jt(o,n.outputDir);console.log(""),console.log(G?"\u25B6 Annotating screenshots\u2026":"\u25B6 Skipping annotation (--no-annotate or config off)");let D=(await St(Wt,G)).filter(d=>!d.ok);console.log(""),console.log(`\u2713 ${h} captured`+(S.length?` \xB7 \u2298 ${S.length} skipped`:"")+(C.length?` \xB7 \u2717 ${C.length} failed`:"")+(A?` \xB7 ${A} not run (stopped)`:"")+` \u2192 ${n.outputDir}`);for(let d of S)console.log(` \u2298 ${d.id}`);for(let d of C)console.log(` \u2717 ${d.id}`);if(D.length>0){console.log(`\u2717 ${D.length} annotation failure${D.length===1?"":"s"}:`);for(let d of D)console.log(` - ${d.id}: ${d.reason}`)}return C.length===0&&D.length===0?0:1}function Le(t){return Ee(t).filter(e=>e.endsWith(".spec.ts")).map(e=>e.slice(0,-8)).sort()}function Me(t){let e=process.stdin;if(e.isTTY){let r=n=>{let i=n[0];(i===27||i===3||i===113||i===81)&&t()};return e.setRawMode(!0),e.resume(),e.on("data",r),()=>{e.off("data",r);try{e.setRawMode(!1)}catch{}e.pause()}}let o=()=>t();return process.on("SIGINT",o),()=>{process.off("SIGINT",o)}}import{existsSync as kt,readdirSync as Be,statSync as _e}from"node:fs";import{isAbsolute as At,join as Fe,resolve as It}from"node:path";async function Ot(){let t=process.cwd(),e=m(g(t)),o=At(e.captureDir)?e.captureDir:It(t,e.captureDir),r=At(e.outputDir)?e.outputDir:It(t,e.outputDir);if(!kt(o)){console.log(`Capture dir does not exist: ${o}`);return}let n=Be(o).filter(i=>i.endsWith(".spec.ts")).sort();if(n.length===0){console.log(`No specs found in ${e.captureDir}.`);return}console.log(""),console.log(" Spec ID Last capture"),console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");for(let i of n){let s=i.replace(/\.spec\.ts$/,""),c=Fe(r,`${s}.png`),l=kt(c)?new Date(_e(c).mtimeMs).toISOString().slice(0,19).replace("T"," "):"(never)";console.log(` ${s.padEnd(47)} ${l}`)}console.log("")}import{existsSync as Ut,readFileSync as Nt}from"node:fs";import{isAbsolute as Je,resolve as He}from"node:path";import{createRequire as We}from"node:module";var Tt=We(import.meta.url);async function Et(t){let e=process.cwd(),o=0,r=a=>console.log(` \u2713 ${a}`),n=a=>{console.log(` \u2717 ${a}`),o++};console.log(""),console.log("Checking document360-capture setup..."),console.log("");let i=g(e);if(!Ut(i))return n(`Project config missing at ${i} \u2014 run \`d360-capture init\``),2;r(`Project config: ${i}`);let s;try{s=m(i),r(`Config parses; projectId="${s.projectId}"`)}catch(a){return n(`Config invalid: ${a.message}`),2}let c;try{c=$(s,t.profile),r(`Profile "${c.name}" resolved (startUrl=${c.connection.startUrl})${c.production?" \u26A0 PRODUCTION":""}`)}catch(a){return n(`Profile resolve failed: ${a.message}`),1}c.authBoundaryDefaulted&&r(`Auth boundary: ${c.connection.authBoundaryUrlPattern} (auto-derived from start URL; set connection.authBoundaryUrlPattern to override)`);let l=Je(c.captureDir)?c.captureDir:He(e,c.captureDir);Ut(l)?r(`Capture dir exists: ${c.captureDir}`):r(`Capture dir not created yet: ${c.captureDir} (created with your first spec \u2014 author specs in d360-writer via /screenshot)`);let p=x(s.projectId,c.name);if(p.exists){let a=p.ageMs!=null?Math.floor(p.ageMs/864e5):null;r(`Auth-state present (${a}d old): ${p.path}`);try{let f=JSON.parse(Nt(p.path,"utf8"));f&&typeof f=="object"?r("Auth-state parses as JSON"):n("Auth-state is not a valid object")}catch(f){n(`Auth-state unreadable: ${f.message}`)}}else n(`Auth-state missing for "${c.name}" \u2014 run \`d360-capture auth --profile ${c.name}\``);try{let a=Tt.resolve("playwright/package.json"),f=JSON.parse(Nt(a,"utf8"));r(`playwright@${f.version} installed`)}catch{n("playwright not resolvable \u2014 reinstall document360-capture")}return U()?r(`Chromium browser downloaded: ${L()}`):n("Chromium browser not downloaded \u2014 it auto-downloads on the next `d360-capture auth` or `capture` run"),["@playwright/test/cli","@playwright/test/cli.js"].some(a=>{try{return Tt.resolve(a),!0}catch{return!1}})?r("@playwright/test CLI resolvable"):n("@playwright/test CLI not resolvable \u2014 reinstall document360-capture"),console.log(""),console.log(o===0?"All checks passed.":`${o} check${o===1?"":"s"} failed.`),console.log(""),o===0?0:1}async function Lt(){let t=m(g());console.log(""),console.log(`projectId: ${t.projectId}`),console.log(`captureDir: ${t.captureDir} (default)`),console.log(`outputDir: ${t.outputDir} (default)`),console.log(`viewport: ${t.viewport.width}x${t.viewport.height} (default)`),console.log(`annotation enabled: ${t.annotation.enabled}`),console.log(`defaultProfile: ${t.defaultProfile}`),console.log(""),console.log("Profiles:");for(let[e,o]of Object.entries(t.profiles)){let r=x(t.projectId,e),n=r.exists?`auth saved ${r.ageMs!=null?Math.floor(r.ageMs/864e5)+"d ago":""}`:"no auth-state",i=`${e===t.defaultProfile?" \u25CF":""}${o.production?" \u26A0 PRODUCTION":""}`;console.log(` ${e.padEnd(10)} ${o.connection.startUrl}${i}`),o.captureDir&&console.log(` captureDir: ${o.captureDir}`),console.log(` ${n}`)}console.log("")}function Mt(){let t=m(g());console.log("");for(let[e,o]of Object.entries(t.profiles)){let r=e===t.defaultProfile?"\u25CF ":" ",n=o.production?" \u26A0 PRODUCTION":"",s=x(t.projectId,e).exists?"auth saved":"no auth-state";console.log(`${r}${e} \u2192 ${o.connection.startUrl}${n} [${s}]`)}console.log(""),console.log("\u25CF = default. Switch with: d360-capture profile use <name>"),console.log("")}function Bt(t){let e=g(),o=m(e);if(!o.profiles[t])return console.error(`\u2717 Unknown profile "${t}". Available: ${Object.keys(o.profiles).join(", ")}`),1;o.defaultProfile=t,O(e,o);let r=o.profiles[t].production?" \u26A0 PRODUCTION":"";return console.log(`\u2713 Default profile is now "${t}"${r}`),0}function _t(t){let e=m(g()),o=$(e,t);console.log(""),console.log(`Profile ${o.name}${o.production?" \u26A0 PRODUCTION":""}`),console.log(` startUrl: ${o.connection.startUrl}`),console.log(` authBoundary:${o.connection.authBoundaryUrlPattern}`),console.log(` captureDir: ${o.captureDir}`),console.log(` outputDir: ${o.outputDir}`),console.log(` viewport: ${o.viewport.width}x${o.viewport.height}`);let r=Object.keys(o.scope);return r.length>0&&console.log(` scope: ${r.map(n=>`${n}=${o.scope[n]}`).join(", ")}`),console.log(""),0}import{homedir as qe}from"node:os";import{join as Ge,dirname as ze}from"node:path";import{mkdirSync as Ke,readFileSync as Xe,writeFileSync as Ye}from"node:fs";var Ft="document360-capture",H=Ge(qe(),".document360-capture","update-cache.json"),Qe=10800*1e3;function Ve(t,e){let o=i=>{let[s="",c=""]=i.trim().replace(/^v/,"").split("-",2),l=s.split(".").map(p=>parseInt(p,10)||0);return{n:[l[0]??0,l[1]??0,l[2]??0],pre:c}},r=o(t),n=o(e);for(let i=0;i<3;i++)if(r.n[i]!==n.n[i])return r.n[i]<n.n[i]?-1:1;return r.pre===n.pre?0:r.pre?n.pre?r.pre<n.pre?-1:1:-1:1}function Ze(){try{return JSON.parse(Xe(H,"utf8"))}catch{return null}}function to(){let t=(process.argv[1]??"").replace(/\\/g,"/");return/\/_npx\//.test(t)||/\/packages\/capture\//.test(t)?!1:/\/node_modules\/document360-capture\//.test(t)||!/\/packages\//.test(t)}async function eo(){try{let t=new AbortController,e=setTimeout(()=>t.abort(),2500),o=await fetch(`https://registry.npmjs.org/${Ft}/latest`,{signal:t.signal,headers:{accept:"application/json"}});if(clearTimeout(e),!o.ok)return;let r=await o.json(),n=typeof r.version=="string"?r.version:null;if(!n)return;Ke(ze(H),{recursive:!0}),Ye(H,`${JSON.stringify({checkedAt:new Date().toISOString(),latest:n})}
27
- `)}catch{}}function Jt(t){try{if(!to())return;let e=Ze();e?.latest&&Ve(t,e.latest)<0&&console.error(`
28
- \u2B06 d360-capture ${t} \u2192 ${e.latest} available \u2014 run: npm i -g ${Ft}@latest
29
- `),e?.checkedAt&&Date.now()-new Date(e.checkedAt).getTime()<Qe||eo()}catch{}}var no=ro(import.meta.url),Ht=no("../package.json");Jt(Ht.version);var v=new oo;function W(t){t.env&&(console.error("\u2717 --env was replaced by --profile (connection profiles). Use: --profile <name>"),process.exit(2))}v.name("d360-capture").description("Capture product screenshots from Playwright specs. Login once, capture many.").version(Ht.version);v.command("init").description("Setup. Inherits from .d360-writer/config.json when present; asks only the app URL. Writes .d360-writer/capture.json.").option("-y, --yes","Non-interactive: scaffold with defaults + a placeholder URL, no prompts").action(async t=>{await ot(t)});v.command("auth").description("Open Chromium for interactive login. Press ENTER when done to save the session.").option("-p, --profile <name>","Profile name (default: defaultProfile from config)").option("-e, --env <name>",!1).action(async t=>{W(t),await st(t)});v.command("capture [spec-id]").description("Run one spec by id, or all specs if omitted.").option("-p, --profile <name>","Profile name (default: defaultProfile from config)").option("-e, --env <name>",!1).option("--no-annotate","Skip annotation overlay; emit raw screenshots only").action(async(t,e)=>{W(e);let o=await Rt(t,e);o!==0&&process.exit(o)});v.command("list").description("List discovered specs and their last-capture times.").action(async()=>{await Ot()});v.command("doctor").description("Validate config, auth-state, and browser install.").option("-p, --profile <name>","Profile to check").option("-e, --env <name>",!1).action(async t=>{W(t);let e=await Et(t);e!==0&&process.exit(e)});v.command("status").description("Print resolved config and auth-state freshness.").action(async()=>{await Lt()});var q=v.command("profile").description("Manage capture profiles for the current repo");q.command("list",{isDefault:!0}).description("List profiles (\u25CF = default)").action(()=>Mt());q.command("use <name>").description("Set the default profile for this repo").action(t=>process.exit(Bt(t)));q.command("show [name]").description("Print the resolved profile").action(t=>process.exit(_t(t)));v.parseAsync(process.argv).catch(t=>{console.error(""),console.error(`\u2717 ${t.message}`),process.exit(1)});
22
+ `;lt(a,c,"utf8");let f=I(s,`tsconfig.${o}.json`),P=j=>j.replace(/\\/g,"/"),y=_.resolve("document360-capture/helpers"),v=I(_.resolve("@playwright/test/package.json"),".."),w=I(v,"index.mjs"),m=pt(w)?w:u,x={compilerOptions:{baseUrl:".",paths:{"document360-capture/helpers":[P(y)],"@playwright/test":[P(m)]}}};return lt(f,JSON.stringify(x,null,2),"utf8"),{configPath:a,tsconfigPath:f}}import{spawn as $e}from"node:child_process";import{existsSync as gt,readFileSync as xe,unlinkSync as be}from"node:fs";import{createRequire as Se}from"node:module";import{fileURLToPath as Ce}from"node:url";import{delimiter as je,dirname as T,join as B}from"node:path";function F(t){if(Array.isArray(t)){for(let e of t){let o=F(e);if(o)return o}return null}if(t&&typeof t=="object"){let e=t;if((e.type==="skip"||e.type==="fixme")&&typeof e.description=="string"&&e.description)return e.description;for(let o of Object.values(e)){let r=F(o);if(r)return r}}return null}function De(t,e){return t!==0?"failed":/\b\d+\s+skipped\b/.test(e)&&!/\b\d+\s+passed\b/.test(e)?"skipped":"passed"}var Re=Se(import.meta.url);async function mt(t){let{cwd:e,configPath:o,tsconfigPath:r,startUrl:n,authBoundaryUrlPattern:i,specId:s,captureDirAbs:a}=t,l=Ie();if(!l)return{exitCode:127,stdout:"",stderr:"Cannot locate @playwright/test CLI. Try: npm install -g document360-capture (reinstall)",outcome:"failed"};let p=B(T(o),`pw-report-${s??"all"}-${process.pid}.json`),u=[l,"test","--config",o,"--tsconfig",r,"--reporter=line,json"];return s&&u.push(ke(`${s}.spec.ts`)),new Promise(c=>{let f="",P="",y=$e(process.execPath,u,{cwd:e,env:{...process.env,CAPTURE_START_URL:n,CAPTURE_AUTH_BOUNDARY:i,CAPTURE_SCOPE:JSON.stringify(t.scope??{}),PLAYWRIGHT_JSON_OUTPUT_NAME:p,NODE_PATH:Ae()},shell:!1}),v=()=>{try{y.kill()}catch{}};t.signal&&(t.signal.aborted?v():t.signal.addEventListener("abort",v,{once:!0})),y.stdout.on("data",w=>{let m=w.toString();f+=m,process.stdout.write(m)}),y.stderr.on("data",w=>{let m=w.toString();P+=m,process.stderr.write(m)}),y.on("error",w=>{t.signal?.removeEventListener("abort",v),P+=`
23
+ [spawn error] ${w.message}`,c({exitCode:127,stdout:f,stderr:P,outcome:"failed"})}),y.on("close",w=>{t.signal?.removeEventListener("abort",v);let m=w??1,x=De(m,f),j;try{gt(p)&&(x==="skipped"&&(j=F(JSON.parse(xe(p,"utf8")))??void 0),be(p))}catch{}c({exitCode:m,stdout:f,stderr:P,outcome:x,skipReason:j})})})}function ke(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function Ae(){let t=T(Ce(import.meta.url));for(let o=0;o<4&&!gt(B(t,"package.json"));o++)t=T(t);let e=[B(t,"node_modules"),T(t)];return process.env.NODE_PATH&&e.push(process.env.NODE_PATH),e.join(je)}function Ie(){let t=["@playwright/test/cli","@playwright/test/cli.js","playwright/cli","playwright/cli.js"];for(let e of t)try{return Re.resolve(e)}catch{}return null}var Ue=[/login\.microsoftonline\.com/i,/login\.live\.com/i,/accounts\.google\.com/i,/\/oauth(2)?\/authorize/i,/\/sign[- ]?in/i,/\bauthn\b/i],Oe=[/waitForURL.*timed?[- ]?out/i,/page\.goto.*redirected/i,/storageState.*invalid/i];function ht(t){return Ue.some(e=>e.test(t))?!0:Oe.some(e=>e.test(t))}import{readdirSync as St,readFileSync as Te,existsSync as Ct,copyFileSync as Ee}from"node:fs";import{join as jt}from"node:path";import W from"sharp";function J(t){return t.replace(/[<>&"']/g,e=>({"<":"&lt;",">":"&gt;","&":"&amp;",'"':"&quot;","'":"&apos;"})[e])}function wt(t){let o=Math.max(0,t.x-4),r=Math.max(0,t.y-4),n=t.width+8,i=t.height+8;return`<rect x="${o}" y="${r}" width="${n}" height="${i}" rx="6" ry="6" fill="rgba(255,213,0,0.18)" stroke="#f5a623" stroke-width="2.5"/>`}function Pt(t){return`<rect x="${t.x}" y="${t.y}" width="${t.width}" height="${t.height}" fill="#1f2328"/>`}var H="-apple-system,Segoe UI,sans-serif",yt=(t,e,o)=>Math.max(e,Math.min(o,t)),Ne=13;function vt(t,e,o,r){let n=Ne,i=yt(e.x,n,Math.max(n,o-n)),s=yt(e.y,n,Math.max(n,r-n));return`<circle cx="${i}" cy="${s}" r="${n}" fill="#d1242f" stroke="#fff" stroke-width="2"/><text x="${i}" y="${s+4}" font-family="${H}" font-size="13" font-weight="700" fill="#fff" text-anchor="middle">${J(t)}</text>`}function $t(t,e){return t===0?0:e.pad*2+t*e.lineHeight}function xt(t,e){let{top:o,width:r,pad:n,lineHeight:i}=e,s=11,a=20+s,l=[`<line x1="0" y1="${o}" x2="${r}" y2="${o}" stroke="#d0d7de" stroke-width="1"/>`];return t.forEach((p,u)=>{let c=o+n+u*i+i/2;l.push(`<circle cx="${a}" cy="${c}" r="${s}" fill="#d1242f" stroke="#fff" stroke-width="1.5"/>`,`<text x="${a}" y="${c+4}" font-family="${H}" font-size="12" font-weight="700" fill="#fff" text-anchor="middle">${J(p.label)}</text>`,`<text x="${a+s+12}" y="${c+5}" font-family="${H}" font-size="14" fill="#1f2328">${J(p.text)}</text>`)}),l.join("")}async function Le(t){let e=JSON.parse(Te(t,"utf8"));if(!Ct(e.rawPath))return{id:e.id,ok:!1,reason:`raw missing: ${e.rawPath}`};let o=await W(e.rawPath).metadata(),r=o.width??1440,n=o.height??900,i={pad:16,lineHeight:30},s=$t(e.annotations.length,i),a=n+s,l=[];for(let c of e.highlight)l.push(wt(c.bbox));for(let c of e.redact)l.push(Pt(c.bbox));for(let c of e.annotations)l.push(vt(c.label,c.bbox,r,n));if(s>0&&l.push(`<rect x="0" y="${n}" width="${r}" height="${s}" fill="#ffffff"/>`,xt(e.annotations.map(c=>({label:c.label,text:c.text})),{top:n,width:r,...i})),l.length===0)return await W(e.rawPath).toFile(e.finalPath),{id:e.id,ok:!0,finalPath:e.finalPath};let p=`<svg xmlns="http://www.w3.org/2000/svg" width="${r}" height="${a}">${l.join("")}</svg>`,u=W(e.rawPath);return s>0&&(u=u.extend({bottom:s,background:"#ffffff"})),await u.composite([{input:Buffer.from(p),top:0,left:0}]).toFile(e.finalPath),{id:e.id,ok:!0,finalPath:e.finalPath}}async function bt(t){let e=St(t).filter(r=>r.endsWith(".raw.png")),o=[];for(let r of e){let n=jt(t,r),i=n.replace(/\.raw\.png$/,".png");try{Ee(n,i),o.push({id:r.replace(/\.raw\.png$/,""),ok:!0,finalPath:i})}catch(s){o.push({id:r,ok:!1,reason:String(s)})}}return o}async function Dt(t,e){if(!Ct(t))return[];if(!e)return bt(t);let o=St(t).filter(n=>n.endsWith(".annotations.json"));if(o.length===0)return bt(t);let r=[];for(let n of o){let i=jt(t,n);try{r.push(await Le(i))}catch(s){r.push({id:n,ok:!1,reason:String(s)})}}return r}async function At(t,e){let o=process.cwd(),r=h(g(o)),n=S(r,e.profile),i=n.name,s=C(r.projectId,i);if(!s.exists)return console.error(""),console.error(`\u2717 No saved session for profile "${i}".`),console.error(` Expected at: ${s.path}`),console.error(` Run: d360-capture auth --profile ${i}`),2;let a=Rt(n.captureDir)?n.captureDir:kt(o,n.captureDir);if(!Me(a))return console.error(`\u2717 Capture directory missing: ${a}`),console.error(" Generate at least one spec there before running capture."),2;await N();let{configPath:l,tsconfigPath:p}=dt({cwd:o,profileName:i,captureDir:n.captureDir,viewport:n.viewport,storageStatePath:A(r.projectId,i)}),u=t?[t]:Be(a);if(u.length===0)return console.error(`\u2717 No .spec.ts files in ${n.captureDir}.`),2;console.log(""),n.production&&console.log(`\u26A0 Profile "${i}" is PRODUCTION (read-only capture).`),console.log(`\u25B6 Running ${u.length} spec${u.length===1?"":"s"} against ${i} (${n.connection.startUrl})`),t||console.log(" Press Esc (or Ctrl+C) in this terminal to stop after the current spec; press again to stop now.");let c=!1,f=null,P=Fe(()=>{c?f?.abort():(c=!0,console.log(`
24
+ \u23F8 Stopping after the current spec\u2026 (press again to stop now)`))}),y=[],v=!1;try{for(let d of u){if(c)break;f=new AbortController,console.log(`
25
+ \u25B6 ${d}`);let b=await mt({cwd:o,configPath:l,tsconfigPath:p,startUrl:n.connection.startUrl,authBoundaryUrlPattern:n.connection.authBoundaryUrlPattern,specId:d,captureDirAbs:a,signal:f.signal,scope:n.scope});if(y.push({id:d,outcome:b.outcome,skipReason:b.skipReason}),b.outcome==="skipped"&&b.skipReason&&console.log(` \u2298 skipped \u2014 ${b.skipReason}`),b.exitCode!==0&&!v&&ht(`${b.stdout}
26
+ ${b.stderr}`)&&(v=!0,console.error(""),console.error(" Output mentions a login page \u2014 if specs that used to pass now fail this way, your"),console.error(` saved session may have expired. Re-auth: d360-capture auth --profile ${i}`),console.error(" (Selector/route timeouts are spec bugs, not auth bugs \u2014 inspect the failure above.)")),f.signal.aborted)break}}finally{P(),f=null}let w=y.filter(d=>d.outcome==="passed").length,m=y.filter(d=>d.outcome==="failed"),x=y.filter(d=>d.outcome==="skipped"),j=u.length-y.length,Y=!e.noAnnotate&&r.annotation.enabled,Gt=Rt(n.outputDir)?n.outputDir:kt(o,n.outputDir);console.log(""),console.log(Y?"\u25B6 Annotating screenshots\u2026":"\u25B6 Skipping annotation (--no-annotate or config off)");let R=(await Dt(Gt,Y)).filter(d=>!d.ok);console.log(""),console.log(`\u2713 ${w} captured`+(x.length?` \xB7 \u2298 ${x.length} skipped`:"")+(m.length?` \xB7 \u2717 ${m.length} failed`:"")+(j?` \xB7 ${j} not run (stopped)`:"")+` \u2192 ${n.outputDir}`);for(let d of x)console.log(` \u2298 ${d.id}${d.skipReason?` \u2014 ${d.skipReason}`:""}`);for(let d of m)console.log(` \u2717 ${d.id}`);if(R.length>0){console.log(`\u2717 ${R.length} annotation failure${R.length===1?"":"s"}:`);for(let d of R)console.log(` - ${d.id}: ${d.reason}`)}return m.length===0&&R.length===0?0:1}function Be(t){return _e(t).filter(e=>e.endsWith(".spec.ts")).map(e=>e.slice(0,-8)).sort()}function Fe(t){let e=process.stdin;if(e.isTTY){let r=n=>{let i=n[0];(i===27||i===3||i===113||i===81)&&t()};return e.setRawMode(!0),e.resume(),e.on("data",r),()=>{e.off("data",r);try{e.setRawMode(!1)}catch{}e.pause()}}let o=()=>t();return process.on("SIGINT",o),()=>{process.off("SIGINT",o)}}import{existsSync as It,readdirSync as Je,statSync as He}from"node:fs";import{isAbsolute as Ut,join as We,resolve as Ot}from"node:path";async function Nt(){let t=process.cwd(),e=h(g(t)),o=Ut(e.captureDir)?e.captureDir:Ot(t,e.captureDir),r=Ut(e.outputDir)?e.outputDir:Ot(t,e.outputDir);if(!It(o)){console.log(`Capture dir does not exist: ${o}`);return}let n=Je(o).filter(i=>i.endsWith(".spec.ts")).sort();if(n.length===0){console.log(`No specs found in ${e.captureDir}.`);return}console.log(""),console.log(" Spec ID Last capture"),console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");for(let i of n){let s=i.replace(/\.spec\.ts$/,""),a=We(r,`${s}.png`),l=It(a)?new Date(He(a).mtimeMs).toISOString().slice(0,19).replace("T"," "):"(never)";console.log(` ${s.padEnd(47)} ${l}`)}console.log("")}import{existsSync as Tt,readFileSync as Et}from"node:fs";import{isAbsolute as qe,resolve as Ge}from"node:path";import{createRequire as ze}from"node:module";var Lt=ze(import.meta.url);async function Mt(t){let e=process.cwd(),o=0,r=c=>console.log(` \u2713 ${c}`),n=c=>{console.log(` \u2717 ${c}`),o++};console.log(""),console.log("Checking document360-capture setup..."),console.log("");let i=g(e);if(!Tt(i))return n(`Project config missing at ${i} \u2014 run \`d360-capture init\``),2;r(`Project config: ${i}`);let s;try{s=h(i),r(`Config parses; projectId="${s.projectId}"`)}catch(c){return n(`Config invalid: ${c.message}`),2}let a;try{a=S(s,t.profile),r(`Profile "${a.name}" resolved (startUrl=${a.connection.startUrl})${a.production?" \u26A0 PRODUCTION":""}`)}catch(c){return n(`Profile resolve failed: ${c.message}`),1}a.authBoundaryDefaulted&&r(`Auth boundary: ${a.connection.authBoundaryUrlPattern} (auto-derived from start URL; set connection.authBoundaryUrlPattern to override)`);let l=qe(a.captureDir)?a.captureDir:Ge(e,a.captureDir);Tt(l)?r(`Capture dir exists: ${a.captureDir}`):r(`Capture dir not created yet: ${a.captureDir} (created with your first spec \u2014 author specs in d360-writer via /screenshot)`);let p=C(s.projectId,a.name);if(p.exists){let c=p.ageMs!=null?Math.floor(p.ageMs/864e5):null;r(`Auth-state present (${c}d old): ${p.path}`);try{let f=JSON.parse(Et(p.path,"utf8"));f&&typeof f=="object"?r("Auth-state parses as JSON"):n("Auth-state is not a valid object")}catch(f){n(`Auth-state unreadable: ${f.message}`)}}else n(`Auth-state missing for "${a.name}" \u2014 run \`d360-capture auth --profile ${a.name}\``);try{let c=Lt.resolve("playwright/package.json"),f=JSON.parse(Et(c,"utf8"));r(`playwright@${f.version} installed`)}catch{n("playwright not resolvable \u2014 reinstall document360-capture")}return O()?r(`Chromium browser downloaded: ${M()}`):n("Chromium browser not downloaded \u2014 it auto-downloads on the next `d360-capture auth` or `capture` run"),["@playwright/test/cli","@playwright/test/cli.js"].some(c=>{try{return Lt.resolve(c),!0}catch{return!1}})?r("@playwright/test CLI resolvable"):n("@playwright/test CLI not resolvable \u2014 reinstall document360-capture"),console.log(""),console.log(o===0?"All checks passed.":`${o} check${o===1?"":"s"} failed.`),console.log(""),o===0?0:1}async function _t(){let t=h(g());console.log(""),console.log(`projectId: ${t.projectId}`),console.log(`captureDir: ${t.captureDir} (default)`),console.log(`outputDir: ${t.outputDir} (default)`),console.log(`viewport: ${t.viewport.width}x${t.viewport.height} (default)`),console.log(`annotation enabled: ${t.annotation.enabled}`),console.log(`defaultProfile: ${t.defaultProfile}`),console.log(""),console.log("Profiles:");for(let[e,o]of Object.entries(t.profiles)){let r=C(t.projectId,e),n=r.exists?`auth saved ${r.ageMs!=null?Math.floor(r.ageMs/864e5)+"d ago":""}`:"no auth-state",i=`${e===t.defaultProfile?" \u25CF":""}${o.production?" \u26A0 PRODUCTION":""}`;console.log(` ${e.padEnd(10)} ${o.connection.startUrl}${i}`),o.captureDir&&console.log(` captureDir: ${o.captureDir}`),console.log(` ${n}`)}console.log("")}function Bt(){let t=h(g());console.log("");for(let[e,o]of Object.entries(t.profiles)){let r=e===t.defaultProfile?"\u25CF ":" ",n=o.production?" \u26A0 PRODUCTION":"",s=C(t.projectId,e).exists?"auth saved":"no auth-state";console.log(`${r}${e} \u2192 ${o.connection.startUrl}${n} [${s}]`)}console.log(""),console.log("\u25CF = default. Switch with: d360-capture profile use <name>"),console.log("")}function Ft(t){let e=g(),o=h(e);if(!o.profiles[t])return console.error(`\u2717 Unknown profile "${t}". Available: ${Object.keys(o.profiles).join(", ")}`),1;o.defaultProfile=t,U(e,o);let r=o.profiles[t].production?" \u26A0 PRODUCTION":"";return console.log(`\u2713 Default profile is now "${t}"${r}`),0}function Jt(t){let e=h(g()),o=S(e,t);console.log(""),console.log(`Profile ${o.name}${o.production?" \u26A0 PRODUCTION":""}`),console.log(` startUrl: ${o.connection.startUrl}`),console.log(` authBoundary:${o.connection.authBoundaryUrlPattern}`),console.log(` captureDir: ${o.captureDir}`),console.log(` outputDir: ${o.outputDir}`),console.log(` viewport: ${o.viewport.width}x${o.viewport.height}`);let r=Object.keys(o.scope);return r.length>0&&console.log(` scope: ${r.map(n=>`${n}=${o.scope[n]}`).join(", ")}`),console.log(""),0}import{homedir as Ye}from"node:os";import{join as Ke,dirname as Xe}from"node:path";import{mkdirSync as Qe,readFileSync as Ve,writeFileSync as Ze}from"node:fs";var Ht="document360-capture",q=Ke(Ye(),".document360-capture","update-cache.json"),to=10800*1e3;function eo(t,e){let o=i=>{let[s="",a=""]=i.trim().replace(/^v/,"").split("-",2),l=s.split(".").map(p=>parseInt(p,10)||0);return{n:[l[0]??0,l[1]??0,l[2]??0],pre:a}},r=o(t),n=o(e);for(let i=0;i<3;i++)if(r.n[i]!==n.n[i])return r.n[i]<n.n[i]?-1:1;return r.pre===n.pre?0:r.pre?n.pre?r.pre<n.pre?-1:1:-1:1}function oo(){try{return JSON.parse(Ve(q,"utf8"))}catch{return null}}function ro(){let t=(process.argv[1]??"").replace(/\\/g,"/");return/\/_npx\//.test(t)||/\/packages\/capture\//.test(t)?!1:/\/node_modules\/document360-capture\//.test(t)||!/\/packages\//.test(t)}async function no(){try{let t=new AbortController,e=setTimeout(()=>t.abort(),2500),o=await fetch(`https://registry.npmjs.org/${Ht}/latest`,{signal:t.signal,headers:{accept:"application/json"}});if(clearTimeout(e),!o.ok)return;let r=await o.json(),n=typeof r.version=="string"?r.version:null;if(!n)return;Qe(Xe(q),{recursive:!0}),Ze(q,`${JSON.stringify({checkedAt:new Date().toISOString(),latest:n})}
27
+ `)}catch{}}function Wt(t){try{if(!ro())return;let e=oo();e?.latest&&eo(t,e.latest)<0&&console.error(`
28
+ \u2B06 d360-capture ${t} \u2192 ${e.latest} available \u2014 run: npm i -g ${Ht}@latest
29
+ `),e?.checkedAt&&Date.now()-new Date(e.checkedAt).getTime()<to||no()}catch{}}var ao=so(import.meta.url),qt=ao("../package.json");Wt(qt.version);var $=new io;function G(t){t.env&&(console.error("\u2717 --env was replaced by --profile (connection profiles). Use: --profile <name>"),process.exit(2))}$.name("d360-capture").description("Capture product screenshots from Playwright specs. Login once, capture many.").version(qt.version);$.command("init").description("Setup. Inherits from .d360-writer/config.json when present; asks only the app URL. Writes .d360-writer/capture.json.").option("-y, --yes","Non-interactive: scaffold with defaults + a placeholder URL, no prompts").action(async t=>{await nt(t)});$.command("auth").description("Open Chromium for interactive login. Press ENTER when done to save the session.").option("-p, --profile <name>","Profile name (default: defaultProfile from config)").option("-e, --env <name>",!1).action(async t=>{G(t),await ct(t)});$.command("capture [spec-id]").description("Run one spec by id, or all specs if omitted.").option("-p, --profile <name>","Profile name (default: defaultProfile from config)").option("-e, --env <name>",!1).option("--no-annotate","Skip annotation overlay; emit raw screenshots only").action(async(t,e)=>{G(e);let o=await At(t,e);o!==0&&process.exit(o)});$.command("list").description("List discovered specs and their last-capture times.").action(async()=>{await Nt()});$.command("doctor").description("Validate config, auth-state, and browser install.").option("-p, --profile <name>","Profile to check").option("-e, --env <name>",!1).action(async t=>{G(t);let e=await Mt(t);e!==0&&process.exit(e)});$.command("status").description("Print resolved config and auth-state freshness.").action(async()=>{await _t()});var z=$.command("profile").description("Manage capture profiles for the current repo");z.command("list",{isDefault:!0}).description("List profiles (\u25CF = default)").action(()=>Bt());z.command("use <name>").description("Set the default profile for this repo").action(t=>process.exit(Ft(t)));z.command("show [name]").description("Print the resolved profile").action(t=>process.exit(Jt(t)));$.parseAsync(process.argv).catch(t=>{console.error(""),console.error(`\u2717 ${t.message}`),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document360-capture",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "CLI that captures product screenshots from Playwright specs. Login once, capture many. Companion to document360-writer.",
5
5
  "type": "module",
6
6
  "bin": {