document360-capture 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import{Command as Ie}from"commander";import{createRequire as Oe}from"node:module";import{input as Gt,confirm as zt}from"@inquirer/prompts";import{existsSync as Y,readFileSync as Xt}from"node:fs";import{basename as Yt,join as Kt}from"node:path";import{readFileSync as H,writeFileSync as Et,existsSync as J}from"node:fs";import{basename as Lt,join as W}from"node:path";var Mt=".d360-capture.json",j={captureDir:"user-docs/_capture",outputDir:"user-docs/_screenshots",viewport:{width:1440,height:900},loginTimeoutMs:0,annotation:{enabled:!0}},b=class extends Error{};function d(t=process.cwd()){return W(t,Mt)}function m(t){if(!J(t))throw new Error(`No project config at ${t}. Run \`d360-capture init\` to create one.`);let e=JSON.parse(H(t,"utf8").replace(/^/,""));return _t(e,t)}function O(t,e){Et(t,JSON.stringify(e,null,2)+`
3
- `,"utf8")}function Bt(t){try{return"^"+new URL(t).origin.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}catch{return"."}}function _t(t,e,o=process.cwd()){Ft(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||Ht(o)||Lt(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||Bt(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 Ft(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 Ht(t){let e=W(t,"package.json");if(!J(e))return null;try{return JSON.parse(H(e,"utf8")).name??null}catch{return null}}import{existsSync as q,readFileSync as Jt}from"node:fs";import{join as Wt}from"node:path";var qt=".d360-writer.json";function G(t=process.cwd()){return Wt(t,qt)}function z(t=process.cwd()){return q(G(t))}function X(t=process.cwd()){let e=G(t);if(!q(e))return{};try{let o=JSON.parse(Jt(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 K(t={}){let e=process.cwd(),o=d(e),r=t.yes===!0;if(Y(o)&&!r&&!await zt({message:`${o} already exists. Overwrite?`,default:!1})){console.log("Init cancelled.");return}let n=X(e),i=n.projectId||Vt(e)||Yt(e),s=[n.projectId&&"project name",n.captureDir&&"directories"].filter(Boolean);s.length>0?(console.log(""),console.log(`Found .d360-writer.json \u2014 inheriting ${s.join(" + ")}.`)):z(e)&&(console.log(""),console.log("Found .d360-writer.json (could not read defaults from it \u2014 using built-in defaults)."));let a=r?"https://example.com/":await Gt({message:"Live app URL (where captures start):",default:"https://example.com/",validate:p=>p.startsWith("http")?!0:"Must be a full URL"}),l={projectId:i,...n.captureDir?{captureDir:n.captureDir}:{},...n.outputDir?{outputDir:n.outputDir}:{},profiles:{staging:{connection:{startUrl:a}}},defaultProfile:"staging"};O(o,l),console.log(""),console.log(`\u2713 Wrote ${o}`),r&&a==="https://example.com/"&&console.log(" \u26A0 Set the staging 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 a prod profile or tune the auth boundary later by editing .d360-capture.json."),console.log("")}function Vt(t){let e=Kt(t,"package.json");if(!Y(e))return null;try{return JSON.parse(Xt(e,"utf8")).name??null}catch{return null}}import{chromium as ee}from"playwright";import{existsSync as V,mkdirSync as Qt,statSync as Zt}from"node:fs";import{homedir as te}from"node:os";import{join as U,dirname as Ve}from"node:path";function Q(){return U(te(),".document360-capture","auth-states")}function R(t,e){return U(Q(),t,`${e}.json`)}function Z(t){let e=U(Q(),t);V(e)||Qt(e,{recursive:!0})}function x(t,e){let o=R(t,e);if(!V(o))return{path:o,exists:!1};let r=Zt(o);return{path:o,exists:!0,ageMs:Date.now()-r.mtimeMs}}async function tt(t){let e=m(d()),o=$(e,t.profile);Z(e.projectId);let r=R(e.projectId,o.name);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 ee.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 oe();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 oe(){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 $e,readdirSync as xe}from"node:fs";import{isAbsolute as vt,resolve as $t}from"node:path";import{writeFileSync as et,mkdirSync as re,existsSync as ot}from"node:fs";import{createRequire as ne}from"node:module";import{join as k,isAbsolute as rt,resolve as nt}from"node:path";var N=ne(import.meta.url);function it(t){let{cwd:e,profileName:o,captureDir:r,viewport:n,storageStatePath:i}=t,s=k(e,".d360-capture-cache");ot(s)||re(s,{recursive:!0});let a=k(s,`playwright.${o}.config.cjs`),l=rt(r)?r:nt(e,r),p=rt(i)?i:nt(e,i),f=N.resolve("@playwright/test"),c=`// Generated by document360-capture \u2014 do not edit by hand.
2
+ import{Command as Ue}from"commander";import{createRequire as Ne}from"node:module";import{input as Xt,confirm as Yt}from"@inquirer/prompts";import{existsSync as K,readFileSync as Kt}from"node:fs";import{basename as Vt,join as Qt}from"node:path";import{readFileSync as J,writeFileSync as Lt,existsSync as W,mkdirSync as Mt}from"node:fs";import{basename as Bt,dirname as _t,join as U}from"node:path";var Ft=U(".d360-writer","capture.json"),j={captureDir:"user-docs/_capture",outputDir:"user-docs/_screenshots",viewport:{width:1440,height:900},loginTimeoutMs:0,annotation:{enabled:!0}},b=class extends Error{};function d(t=process.cwd()){return U(t,Ft)}function m(t){if(!W(t))throw new Error(`No project config at ${t}. Run \`d360-capture init\` to create one.`);let e=JSON.parse(J(t,"utf8").replace(/^/,""));return Jt(e,t)}function O(t,e){Mt(_t(t),{recursive:!0}),Lt(t,JSON.stringify(e,null,2)+`
3
+ `,"utf8")}function Ht(t){try{return"^"+new URL(t).origin.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}catch{return"."}}function Jt(t,e,o=process.cwd()){Wt(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||qt(o)||Bt(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||Ht(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 Wt(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 qt(t){let e=U(t,"package.json");if(!W(e))return null;try{return JSON.parse(J(e,"utf8")).name??null}catch{return null}}import{existsSync as q,readFileSync as Gt}from"node:fs";import{join as G}from"node:path";var zt=G(".d360-writer","config.json");function z(t=process.cwd()){return G(t,zt)}function X(t=process.cwd()){return q(z(t))}function Y(t=process.cwd()){let e=z(t);if(!q(e))return{};try{let o=JSON.parse(Gt(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 V(t={}){let e=process.cwd(),o=d(e),r=t.yes===!0;if(K(o)&&!r&&!await Yt({message:`${o} already exists. Overwrite?`,default:!1})){console.log("Init cancelled.");return}let n=Y(e),i=n.projectId||Zt(e)||Vt(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(" + ")}.`)):X(e)&&(console.log(""),console.log("Found .d360-writer/config.json (could not read defaults from it \u2014 using built-in defaults)."));let a=r?"https://example.com/":await Xt({message:"Live app URL (where captures start):",default:"https://example.com/",validate:p=>p.startsWith("http")?!0:"Must be a full URL"}),l={projectId:i,...n.captureDir?{captureDir:n.captureDir}:{},...n.outputDir?{outputDir:n.outputDir}:{},profiles:{staging:{connection:{startUrl:a}}},defaultProfile:"staging"};O(o,l),console.log(""),console.log(`\u2713 Wrote ${o}`),r&&a==="https://example.com/"&&console.log(" \u26A0 Set the staging 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 a prod profile or tune the auth boundary later by editing .d360-writer/capture.json."),console.log("")}function Zt(t){let e=Qt(t,"package.json");if(!K(e))return null;try{return JSON.parse(Kt(e,"utf8")).name??null}catch{return null}}import{chromium as re}from"playwright";import{existsSync as Q,mkdirSync as te,statSync as ee}from"node:fs";import{homedir as oe}from"node:os";import{join as N,dirname as Ze}from"node:path";function Z(){return N(oe(),".document360-capture","auth-states")}function R(t,e){return N(Z(),t,`${e}.json`)}function tt(t){let e=N(Z(),t);Q(e)||te(e,{recursive:!0})}function x(t,e){let o=R(t,e);if(!Q(o))return{path:o,exists:!1};let r=ee(o);return{path:o,exists:!0,ageMs:Date.now()-r.mtimeMs}}async function et(t){let e=m(d()),o=$(e,t.profile);tt(e.projectId);let r=R(e.projectId,o.name);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 re.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 ne();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 ne(){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 be,readdirSync as De}from"node:fs";import{isAbsolute as $t,resolve as xt}from"node:path";import{writeFileSync as ot,mkdirSync as ie,existsSync as rt}from"node:fs";import{createRequire as se}from"node:module";import{join as k,isAbsolute as nt,resolve as it}from"node:path";var T=se(import.meta.url);function st(t){let{cwd:e,profileName:o,captureDir:r,viewport:n,storageStatePath:i}=t,s=k(e,".d360-writer","cache");rt(s)||ie(s,{recursive:!0});let a=k(s,`playwright.${o}.config.cjs`),l=nt(r)?r:it(e,r),p=nt(i)?i:it(e,i),f=T.resolve("@playwright/test"),c=`// Generated by document360-capture \u2014 do not edit by hand.
4
4
  const { defineConfig } = require(${JSON.stringify(f)});
5
5
  module.exports = defineConfig({
6
6
  testDir: ${JSON.stringify(l)},
@@ -18,8 +18,8 @@ module.exports = defineConfig({
18
18
  contextOptions: { reducedMotion: 'reduce' },
19
19
  },
20
20
  });
21
- `;et(a,c,"utf8");let u=k(s,`tsconfig.${o}.json`),P=A=>A.replace(/\\/g,"/"),y=N.resolve("document360-capture/helpers"),w=k(N.resolve("@playwright/test/package.json"),".."),h=k(w,"index.mjs"),D=ot(h)?h:f,S={compilerOptions:{baseUrl:".",paths:{"document360-capture/helpers":[P(y)],"@playwright/test":[P(D)]}}};return et(u,JSON.stringify(S,null,2),"utf8"),{configPath:a,tsconfigPath:u}}import{spawn as ie}from"node:child_process";import{existsSync as se}from"node:fs";import{createRequire as ae}from"node:module";import{fileURLToPath as ce}from"node:url";import{delimiter as le,dirname as T,join as st}from"node:path";function pe(t,e){return t!==0?"failed":/\b\d+\s+skipped\b/.test(e)&&!/\b\d+\s+passed\b/.test(e)?"skipped":"passed"}var ue=ae(import.meta.url);async function at(t){let{cwd:e,configPath:o,tsconfigPath:r,startUrl:n,authBoundaryUrlPattern:i,specId:s,captureDirAbs:a}=t,l=de();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(fe(`${s}.spec.ts`)),new Promise(f=>{let c="",u="",P=ie(process.execPath,p,{cwd:e,env:{...process.env,CAPTURE_START_URL:n,CAPTURE_AUTH_BOUNDARY:i,CAPTURE_SCOPE:JSON.stringify(t.scope??{}),NODE_PATH:ge()},shell:!1}),y=()=>{try{P.kill()}catch{}};t.signal&&(t.signal.aborted?y():t.signal.addEventListener("abort",y,{once:!0})),P.stdout.on("data",w=>{let h=w.toString();c+=h,process.stdout.write(h)}),P.stderr.on("data",w=>{let h=w.toString();u+=h,process.stderr.write(h)}),P.on("error",w=>{t.signal?.removeEventListener("abort",y),u+=`
22
- [spawn error] ${w.message}`,f({exitCode:127,stdout:c,stderr:u,outcome:"failed"})}),P.on("close",w=>{t.signal?.removeEventListener("abort",y);let h=w??1;f({exitCode:h,stdout:c,stderr:u,outcome:pe(h,c)})})})}function fe(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function ge(){let t=T(ce(import.meta.url));for(let o=0;o<4&&!se(st(t,"package.json"));o++)t=T(t);let e=[st(t,"node_modules"),T(t)];return process.env.NODE_PATH&&e.push(process.env.NODE_PATH),e.join(le)}function de(){let t=["@playwright/test/cli","@playwright/test/cli.js","playwright/cli","playwright/cli.js"];for(let e of t)try{return ue.resolve(e)}catch{}return null}var me=[/login\.microsoftonline\.com/i,/login\.live\.com/i,/accounts\.google\.com/i,/\/oauth(2)?\/authorize/i,/\/sign[- ]?in/i,/\bauthn\b/i],he=[/waitForURL.*timed?[- ]?out/i,/page\.goto.*redirected/i,/storageState.*invalid/i];function ct(t){return me.some(e=>e.test(t))?!0:he.some(e=>e.test(t))}import{readdirSync as ht,readFileSync as we,existsSync as yt,copyFileSync as Pe}from"node:fs";import{join as wt}from"node:path";import M from"sharp";function E(t){return t.replace(/[<>&"']/g,e=>({"<":"&lt;",">":"&gt;","&":"&amp;",'"':"&quot;","'":"&apos;"})[e])}function pt(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 ut(t){return`<rect x="${t.x}" y="${t.y}" width="${t.width}" height="${t.height}" fill="#1f2328"/>`}var L="-apple-system,Segoe UI,sans-serif",lt=(t,e,o)=>Math.max(e,Math.min(o,t)),ye=13;function ft(t,e,o,r){let n=ye,i=lt(e.x,n,Math.max(n,o-n)),s=lt(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="${L}" font-size="13" font-weight="700" fill="#fff" text-anchor="middle">${E(t)}</text>`}function gt(t,e){return t===0?0:e.pad*2+t*e.lineHeight}function dt(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,f)=>{let c=o+n+f*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="${L}" font-size="12" font-weight="700" fill="#fff" text-anchor="middle">${E(p.label)}</text>`,`<text x="${a+s+12}" y="${c+5}" font-family="${L}" font-size="14" fill="#1f2328">${E(p.text)}</text>`)}),l.join("")}async function ve(t){let e=JSON.parse(we(t,"utf8"));if(!yt(e.rawPath))return{id:e.id,ok:!1,reason:`raw missing: ${e.rawPath}`};let o=await M(e.rawPath).metadata(),r=o.width??1440,n=o.height??900,i={pad:16,lineHeight:30},s=gt(e.annotations.length,i),a=n+s,l=[];for(let c of e.highlight)l.push(pt(c.bbox));for(let c of e.redact)l.push(ut(c.bbox));for(let c of e.annotations)l.push(ft(c.label,c.bbox,r,n));if(s>0&&l.push(`<rect x="0" y="${n}" width="${r}" height="${s}" fill="#ffffff"/>`,dt(e.annotations.map(c=>({label:c.label,text:c.text})),{top:n,width:r,...i})),l.length===0)return await M(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>`,f=M(e.rawPath);return s>0&&(f=f.extend({bottom:s,background:"#ffffff"})),await f.composite([{input:Buffer.from(p),top:0,left:0}]).toFile(e.finalPath),{id:e.id,ok:!0,finalPath:e.finalPath}}async function mt(t){let e=ht(t).filter(r=>r.endsWith(".raw.png")),o=[];for(let r of e){let n=wt(t,r),i=n.replace(/\.raw\.png$/,".png");try{Pe(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 Pt(t,e){if(!yt(t))return[];if(!e)return mt(t);let o=ht(t).filter(n=>n.endsWith(".annotations.json"));if(o.length===0)return mt(t);let r=[];for(let n of o){let i=wt(t,n);try{r.push(await ve(i))}catch(s){r.push({id:n,ok:!1,reason:String(s)})}}return r}async function xt(t,e){let o=process.cwd(),r=m(d(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 a=vt(n.captureDir)?n.captureDir:$t(o,n.captureDir);if(!$e(a))return console.error(`\u2717 Capture directory missing: ${a}`),console.error(" Generate at least one spec there before running capture."),2;let{configPath:l,tsconfigPath:p}=it({cwd:o,profileName:i,captureDir:n.captureDir,viewport:n.viewport,storageStatePath:R(r.projectId,i)}),f=t?[t]:be(a);if(f.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 ${f.length} spec${f.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,u=null,P=De(()=>{c?u?.abort():(c=!0,console.log(`
23
- \u23F8 Stopping after the current spec\u2026 (press again to stop now)`))}),y=[],w=!1;try{for(let g of f){if(c)break;u=new AbortController,console.log(`
24
- \u25B6 ${g}`);let I=await at({cwd:o,configPath:l,tsconfigPath:p,startUrl:n.connection.startUrl,authBoundaryUrlPattern:n.connection.authBoundaryUrlPattern,specId:g,captureDirAbs:a,signal:u.signal,scope:n.scope});if(y.push({id:g,outcome:I.outcome}),I.exitCode!==0&&!w&&ct(`${I.stdout}
25
- ${I.stderr}`)&&(w=!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.)")),u.signal.aborted)break}}finally{P(),u=null}let h=y.filter(g=>g.outcome==="passed").length,D=y.filter(g=>g.outcome==="failed"),S=y.filter(g=>g.outcome==="skipped"),A=f.length-y.length,F=!e.noAnnotate&&r.annotation.enabled,Tt=vt(n.outputDir)?n.outputDir:$t(o,n.outputDir);console.log(""),console.log(F?"\u25B6 Annotating screenshots\u2026":"\u25B6 Skipping annotation (--no-annotate or config off)");let C=(await Pt(Tt,F)).filter(g=>!g.ok);console.log(""),console.log(`\u2713 ${h} captured`+(S.length?` \xB7 \u2298 ${S.length} skipped`:"")+(D.length?` \xB7 \u2717 ${D.length} failed`:"")+(A?` \xB7 ${A} not run (stopped)`:"")+` \u2192 ${n.outputDir}`);for(let g of S)console.log(` \u2298 ${g.id}`);for(let g of D)console.log(` \u2717 ${g.id}`);if(C.length>0){console.log(`\u2717 ${C.length} annotation failure${C.length===1?"":"s"}:`);for(let g of C)console.log(` - ${g.id}: ${g.reason}`)}return D.length===0&&C.length===0?0:1}function be(t){return xe(t).filter(e=>e.endsWith(".spec.ts")).map(e=>e.slice(0,-8)).sort()}function De(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 bt,readdirSync as Se,statSync as Ce}from"node:fs";import{isAbsolute as Dt,join as je,resolve as St}from"node:path";async function Ct(){let t=process.cwd(),e=m(d(t)),o=Dt(e.captureDir)?e.captureDir:St(t,e.captureDir),r=Dt(e.outputDir)?e.outputDir:St(t,e.outputDir);if(!bt(o)){console.log(`Capture dir does not exist: ${o}`);return}let n=Se(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=je(r,`${s}.png`),l=bt(a)?new Date(Ce(a).mtimeMs).toISOString().slice(0,19).replace("T"," "):"(never)";console.log(` ${s.padEnd(47)} ${l}`)}console.log("")}import{existsSync as jt,readFileSync as Rt}from"node:fs";import{isAbsolute as Re,resolve as ke}from"node:path";import{createRequire as Ae}from"node:module";var kt=Ae(import.meta.url);async function At(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=d(e);if(!jt(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(c){return n(`Config invalid: ${c.message}`),2}let a;try{a=$(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=Re(a.captureDir)?a.captureDir:ke(e,a.captureDir);jt(l)?r(`Capture dir exists: ${a.captureDir}`):n(`Capture dir missing: ${a.captureDir}`);let p=x(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 u=JSON.parse(Rt(p.path,"utf8"));u&&typeof u=="object"?r("Auth-state parses as JSON"):n("Auth-state is not a valid object")}catch(u){n(`Auth-state unreadable: ${u.message}`)}}else n(`Auth-state missing for "${a.name}" \u2014 run \`d360-capture auth --profile ${a.name}\``);try{let c=kt.resolve("playwright/package.json"),u=JSON.parse(Rt(c,"utf8"));r(`playwright@${u.version} installed`)}catch{n("playwright not resolvable \u2014 reinstall document360-capture")}return["@playwright/test/cli","@playwright/test/cli.js"].some(c=>{try{return kt.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 It(){let t=m(d());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 Ot(){let t=m(d());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 Ut(t){let e=d(),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 Nt(t){let e=m(d()),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}var Ue=Oe(import.meta.url),Ne=Ue("../package.json"),v=new Ie;function B(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(Ne.version);v.command("init").description("Setup. Inherits from .d360-writer.json when present; asks only the app URL. Writes .d360-capture.json.").option("-y, --yes","Non-interactive: scaffold with defaults + a placeholder URL, no prompts").action(async t=>{await K(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=>{B(t),await tt(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)=>{B(e);let o=await xt(t,e);o!==0&&process.exit(o)});v.command("list").description("List discovered specs and their last-capture times.").action(async()=>{await Ct()});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=>{B(t);let e=await At(t);e!==0&&process.exit(e)});v.command("status").description("Print resolved config and auth-state freshness.").action(async()=>{await It()});var _=v.command("profile").description("Manage capture profiles for the current repo");_.command("list",{isDefault:!0}).description("List profiles (\u25CF = default)").action(()=>Ot());_.command("use <name>").description("Set the default profile for this repo").action(t=>process.exit(Ut(t)));_.command("show [name]").description("Print the resolved profile").action(t=>process.exit(Nt(t)));v.parseAsync(process.argv).catch(t=>{console.error(""),console.error(`\u2717 ${t.message}`),process.exit(1)});
21
+ `;ot(a,c,"utf8");let u=k(s,`tsconfig.${o}.json`),P=A=>A.replace(/\\/g,"/"),w=T.resolve("document360-capture/helpers"),y=k(T.resolve("@playwright/test/package.json"),".."),h=k(y,"index.mjs"),D=rt(h)?h:f,S={compilerOptions:{baseUrl:".",paths:{"document360-capture/helpers":[P(w)],"@playwright/test":[P(D)]}}};return ot(u,JSON.stringify(S,null,2),"utf8"),{configPath:a,tsconfigPath:u}}import{spawn as ae}from"node:child_process";import{existsSync as ce}from"node:fs";import{createRequire as le}from"node:module";import{fileURLToPath as pe}from"node:url";import{delimiter as ue,dirname as E,join as at}from"node:path";function fe(t,e){return t!==0?"failed":/\b\d+\s+skipped\b/.test(e)&&!/\b\d+\s+passed\b/.test(e)?"skipped":"passed"}var ge=le(import.meta.url);async function ct(t){let{cwd:e,configPath:o,tsconfigPath:r,startUrl:n,authBoundaryUrlPattern:i,specId:s,captureDirAbs:a}=t,l=he();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(f=>{let c="",u="",P=ae(process.execPath,p,{cwd:e,env:{...process.env,CAPTURE_START_URL:n,CAPTURE_AUTH_BOUNDARY:i,CAPTURE_SCOPE:JSON.stringify(t.scope??{}),NODE_PATH:me()},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();c+=h,process.stdout.write(h)}),P.stderr.on("data",y=>{let h=y.toString();u+=h,process.stderr.write(h)}),P.on("error",y=>{t.signal?.removeEventListener("abort",w),u+=`
22
+ [spawn error] ${y.message}`,f({exitCode:127,stdout:c,stderr:u,outcome:"failed"})}),P.on("close",y=>{t.signal?.removeEventListener("abort",w);let h=y??1;f({exitCode:h,stdout:c,stderr:u,outcome:fe(h,c)})})})}function de(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function me(){let t=E(pe(import.meta.url));for(let o=0;o<4&&!ce(at(t,"package.json"));o++)t=E(t);let e=[at(t,"node_modules"),E(t)];return process.env.NODE_PATH&&e.push(process.env.NODE_PATH),e.join(ue)}function he(){let t=["@playwright/test/cli","@playwright/test/cli.js","playwright/cli","playwright/cli.js"];for(let e of t)try{return ge.resolve(e)}catch{}return null}var we=[/login\.microsoftonline\.com/i,/login\.live\.com/i,/accounts\.google\.com/i,/\/oauth(2)?\/authorize/i,/\/sign[- ]?in/i,/\bauthn\b/i],ye=[/waitForURL.*timed?[- ]?out/i,/page\.goto.*redirected/i,/storageState.*invalid/i];function lt(t){return we.some(e=>e.test(t))?!0:ye.some(e=>e.test(t))}import{readdirSync as wt,readFileSync as ve,existsSync as yt,copyFileSync as $e}from"node:fs";import{join as Pt}from"node:path";import B from"sharp";function L(t){return t.replace(/[<>&"']/g,e=>({"<":"&lt;",">":"&gt;","&":"&amp;",'"':"&quot;","'":"&apos;"})[e])}function ut(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 ft(t){return`<rect x="${t.x}" y="${t.y}" width="${t.width}" height="${t.height}" fill="#1f2328"/>`}var M="-apple-system,Segoe UI,sans-serif",pt=(t,e,o)=>Math.max(e,Math.min(o,t)),Pe=13;function gt(t,e,o,r){let n=Pe,i=pt(e.x,n,Math.max(n,o-n)),s=pt(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="${M}" font-size="13" font-weight="700" fill="#fff" text-anchor="middle">${L(t)}</text>`}function dt(t,e){return t===0?0:e.pad*2+t*e.lineHeight}function mt(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,f)=>{let c=o+n+f*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="${M}" font-size="12" font-weight="700" fill="#fff" text-anchor="middle">${L(p.label)}</text>`,`<text x="${a+s+12}" y="${c+5}" font-family="${M}" font-size="14" fill="#1f2328">${L(p.text)}</text>`)}),l.join("")}async function xe(t){let e=JSON.parse(ve(t,"utf8"));if(!yt(e.rawPath))return{id:e.id,ok:!1,reason:`raw missing: ${e.rawPath}`};let o=await B(e.rawPath).metadata(),r=o.width??1440,n=o.height??900,i={pad:16,lineHeight:30},s=dt(e.annotations.length,i),a=n+s,l=[];for(let c of e.highlight)l.push(ut(c.bbox));for(let c of e.redact)l.push(ft(c.bbox));for(let c of e.annotations)l.push(gt(c.label,c.bbox,r,n));if(s>0&&l.push(`<rect x="0" y="${n}" width="${r}" height="${s}" fill="#ffffff"/>`,mt(e.annotations.map(c=>({label:c.label,text:c.text})),{top:n,width:r,...i})),l.length===0)return await B(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>`,f=B(e.rawPath);return s>0&&(f=f.extend({bottom:s,background:"#ffffff"})),await f.composite([{input:Buffer.from(p),top:0,left:0}]).toFile(e.finalPath),{id:e.id,ok:!0,finalPath:e.finalPath}}async function ht(t){let e=wt(t).filter(r=>r.endsWith(".raw.png")),o=[];for(let r of e){let n=Pt(t,r),i=n.replace(/\.raw\.png$/,".png");try{$e(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 vt(t,e){if(!yt(t))return[];if(!e)return ht(t);let o=wt(t).filter(n=>n.endsWith(".annotations.json"));if(o.length===0)return ht(t);let r=[];for(let n of o){let i=Pt(t,n);try{r.push(await xe(i))}catch(s){r.push({id:n,ok:!1,reason:String(s)})}}return r}async function bt(t,e){let o=process.cwd(),r=m(d(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 a=$t(n.captureDir)?n.captureDir:xt(o,n.captureDir);if(!be(a))return console.error(`\u2717 Capture directory missing: ${a}`),console.error(" Generate at least one spec there before running capture."),2;let{configPath:l,tsconfigPath:p}=st({cwd:o,profileName:i,captureDir:n.captureDir,viewport:n.viewport,storageStatePath:R(r.projectId,i)}),f=t?[t]:Se(a);if(f.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 ${f.length} spec${f.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,u=null,P=Ce(()=>{c?u?.abort():(c=!0,console.log(`
23
+ \u23F8 Stopping after the current spec\u2026 (press again to stop now)`))}),w=[],y=!1;try{for(let g of f){if(c)break;u=new AbortController,console.log(`
24
+ \u25B6 ${g}`);let I=await ct({cwd:o,configPath:l,tsconfigPath:p,startUrl:n.connection.startUrl,authBoundaryUrlPattern:n.connection.authBoundaryUrlPattern,specId:g,captureDirAbs:a,signal:u.signal,scope:n.scope});if(w.push({id:g,outcome:I.outcome}),I.exitCode!==0&&!y&&lt(`${I.stdout}
25
+ ${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.)")),u.signal.aborted)break}}finally{P(),u=null}let h=w.filter(g=>g.outcome==="passed").length,D=w.filter(g=>g.outcome==="failed"),S=w.filter(g=>g.outcome==="skipped"),A=f.length-w.length,H=!e.noAnnotate&&r.annotation.enabled,Et=$t(n.outputDir)?n.outputDir:xt(o,n.outputDir);console.log(""),console.log(H?"\u25B6 Annotating screenshots\u2026":"\u25B6 Skipping annotation (--no-annotate or config off)");let C=(await vt(Et,H)).filter(g=>!g.ok);console.log(""),console.log(`\u2713 ${h} captured`+(S.length?` \xB7 \u2298 ${S.length} skipped`:"")+(D.length?` \xB7 \u2717 ${D.length} failed`:"")+(A?` \xB7 ${A} not run (stopped)`:"")+` \u2192 ${n.outputDir}`);for(let g of S)console.log(` \u2298 ${g.id}`);for(let g of D)console.log(` \u2717 ${g.id}`);if(C.length>0){console.log(`\u2717 ${C.length} annotation failure${C.length===1?"":"s"}:`);for(let g of C)console.log(` - ${g.id}: ${g.reason}`)}return D.length===0&&C.length===0?0:1}function Se(t){return De(t).filter(e=>e.endsWith(".spec.ts")).map(e=>e.slice(0,-8)).sort()}function Ce(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 Dt,readdirSync as je,statSync as Re}from"node:fs";import{isAbsolute as St,join as ke,resolve as Ct}from"node:path";async function jt(){let t=process.cwd(),e=m(d(t)),o=St(e.captureDir)?e.captureDir:Ct(t,e.captureDir),r=St(e.outputDir)?e.outputDir:Ct(t,e.outputDir);if(!Dt(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=ke(r,`${s}.png`),l=Dt(a)?new Date(Re(a).mtimeMs).toISOString().slice(0,19).replace("T"," "):"(never)";console.log(` ${s.padEnd(47)} ${l}`)}console.log("")}import{existsSync as Rt,readFileSync as kt}from"node:fs";import{isAbsolute as Ae,resolve as Ie}from"node:path";import{createRequire as Oe}from"node:module";var At=Oe(import.meta.url);async function It(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=d(e);if(!Rt(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(c){return n(`Config invalid: ${c.message}`),2}let a;try{a=$(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=Ae(a.captureDir)?a.captureDir:Ie(e,a.captureDir);Rt(l)?r(`Capture dir exists: ${a.captureDir}`):n(`Capture dir missing: ${a.captureDir}`);let p=x(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 u=JSON.parse(kt(p.path,"utf8"));u&&typeof u=="object"?r("Auth-state parses as JSON"):n("Auth-state is not a valid object")}catch(u){n(`Auth-state unreadable: ${u.message}`)}}else n(`Auth-state missing for "${a.name}" \u2014 run \`d360-capture auth --profile ${a.name}\``);try{let c=At.resolve("playwright/package.json"),u=JSON.parse(kt(c,"utf8"));r(`playwright@${u.version} installed`)}catch{n("playwright not resolvable \u2014 reinstall document360-capture")}return["@playwright/test/cli","@playwright/test/cli.js"].some(c=>{try{return At.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 Ot(){let t=m(d());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 Ut(){let t=m(d());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 Nt(t){let e=d(),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 Tt(t){let e=m(d()),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}var Te=Ne(import.meta.url),Ee=Te("../package.json"),v=new Ue;function _(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(Ee.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 V(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=>{_(t),await et(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)=>{_(e);let o=await bt(t,e);o!==0&&process.exit(o)});v.command("list").description("List discovered specs and their last-capture times.").action(async()=>{await jt()});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=>{_(t);let e=await It(t);e!==0&&process.exit(e)});v.command("status").description("Print resolved config and auth-state freshness.").action(async()=>{await Ot()});var F=v.command("profile").description("Manage capture profiles for the current repo");F.command("list",{isDefault:!0}).description("List profiles (\u25CF = default)").action(()=>Ut());F.command("use <name>").description("Set the default profile for this repo").action(t=>process.exit(Nt(t)));F.command("show [name]").description("Print the resolved profile").action(t=>process.exit(Tt(t)));v.parseAsync(process.argv).catch(t=>{console.error(""),console.error(`\u2717 ${t.message}`),process.exit(1)});
@@ -81,7 +81,7 @@ export type ResolvedProfile = {
81
81
  doctor surfaces this so a too-loose/tight pattern is refinable before it bites. */
82
82
  authBoundaryDefaulted: boolean;
83
83
  };
84
- export declare const PROJECT_CONFIG_FILENAME = ".d360-capture.json";
84
+ export declare const PROJECT_CONFIG_FILENAME: string;
85
85
  /** Filled in for any field a minimal config omits. */
86
86
  export declare const DEFAULTS: {
87
87
  readonly captureDir: "user-docs/_capture";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document360-capture",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
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": {