document360-capture 0.2.3 → 0.2.5

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.
@@ -2,26 +2,31 @@ import type { Bbox } from '../helpers/types.js';
2
2
  export declare function escapeXml(s: string): string;
3
3
  export declare function highlightSvg(bbox: Bbox): string;
4
4
  export declare function redactSvg(bbox: Bbox): string;
5
- /** The numbered pin sits just right of the target; its geometry anchors the caption too. */
6
- export declare function pinGeom(bbox: Bbox): {
7
- cx: number;
8
- cy: number;
9
- r: number;
10
- rect: Bbox;
5
+ /**
6
+ * Just the numbered pin, placed on the target's top-left corner (clamped into the image). Pins
7
+ * only MARK targets — the explanation lives in the legend band — so they stay small and never
8
+ * cover the surrounding UI, which is what doomed inline captions on busy screens.
9
+ */
10
+ export declare function pinSvg(label: string, bbox: Bbox, canvasW: number, canvasH: number): string;
11
+ export type LegendItem = {
12
+ label: string;
13
+ text: string;
11
14
  };
12
- /** Just the numbered pin (circle + label). Rendered in a first pass so captions can avoid ALL
13
- pins, not only the ones placed before them otherwise a wide caption runs under a later pin. */
14
- export declare function pinSvg(label: string, bbox: Bbox): {
15
- svg: string;
16
- rect: Bbox;
15
+ export type LegendLayout = {
16
+ /** Y where the band starts (== original image height). */
17
+ top: number;
18
+ width: number;
19
+ /** Vertical padding above the first / below the last entry. */
20
+ pad: number;
21
+ /** Per-entry row height. */
22
+ lineHeight: number;
17
23
  };
24
+ /** Height a legend band needs for `n` entries — the canvas is extended by exactly this much. */
25
+ export declare function legendBandHeight(n: number, layout: Pick<LegendLayout, 'pad' | 'lineHeight'>): number;
18
26
  /**
19
- * The caption box for a callout, positioned to clear every box in `placed` (highlights,
20
- * redactions, all pins, and prior captions). Without this, a row of targets produces captions
21
- * that overlap and paint over each other (the variables-page truncation). The label is prefixed
22
- * into the text so a caption stacked away from its pin is still tied to it by number.
27
+ * The legend band: a numbered list in the appended strip below the screenshot. Deterministic,
28
+ * always-safe placement (the band is added space, so nothing on the page is ever covered) no
29
+ * collision detection or whitespace-hunting required. Each row reuses the pin's numbered chip so
30
+ * the legend entry and its on-image pin read as the same marker.
23
31
  */
24
- export declare function captionSvg(label: string, text: string, bbox: Bbox, canvasW: number, canvasH: number, placed?: Bbox[]): {
25
- svg: string;
26
- rect: Bbox;
27
- };
32
+ export declare function legendSvg(items: LegendItem[], layout: LegendLayout): string;
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import{Command as So}from"commander";import{createRequire as Ro}from"node:module";import{input as zt,confirm as Gt}from"@inquirer/prompts";import{existsSync as q,readFileSync as Ht}from"node:fs";import{basename as Yt,join as Xt}from"node:path";import{readFileSync as E,writeFileSync as Tt,existsSync as M}from"node:fs";import{basename as Bt,join as L}from"node:path";var Et=".d360-capture.json",S={captureDir:"user-docs/_capture",outputDir:"user-docs/_screenshots",viewport:{width:1440,height:900},loginTimeoutMs:0,annotation:{enabled:!0}},D=class extends Error{};function w(t=process.cwd()){return L(t,Et)}function y(t){if(!M(t))throw new Error(`No project config at ${t}. Run \`d360-capture init\` to create one.`);let o=JSON.parse(E(t,"utf8").replace(/^/,""));return Lt(o,t)}function k(t,o){Tt(t,JSON.stringify(o,null,2)+`
3
- `,"utf8")}function Mt(t){try{return"^"+new URL(t).origin.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}catch{return"."}}function Lt(t,o,e=process.cwd()){_t(t,o);let r=Object.keys(t.profiles),n=t.defaultProfile??(r.length===1?r[0]:r.includes("staging")?"staging":r[0]);return{projectId:t.projectId||Ft(e)||Bt(e),captureDir:t.captureDir??S.captureDir,outputDir:t.outputDir??S.outputDir,viewport:t.viewport??{...S.viewport},loginTimeoutMs:t.loginTimeoutMs??S.loginTimeoutMs,annotation:t.annotation??{...S.annotation},profiles:t.profiles,defaultProfile:n}}function v(t,o){let e=o??t.defaultProfile,r=t.profiles[e];if(!r){let i=Object.keys(t.profiles).join(", ");throw new D(`Unknown profile "${e}". Available: ${i}`)}let n=r.connection.authBoundaryUrlPattern;return{name:e,connection:{startUrl:r.connection.startUrl,authBoundaryUrlPattern:n||Mt(r.connection.startUrl)},captureDir:r.captureDir??t.captureDir,outputDir:r.outputDir??t.outputDir,viewport:r.viewport??t.viewport,production:r.production===!0,authBoundaryDefaulted:!n}}function _t(t,o){if(t.environments&&!t.profiles)throw new D(`Config at ${o} 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 ${o}: needs at least one profile under "profiles".`);for(let[e,r]of Object.entries(t.profiles))if(!r?.connection?.startUrl)throw new D(`Invalid config at ${o}: profile "${e}" is missing connection.startUrl.`);if(t.defaultProfile&&!t.profiles[t.defaultProfile])throw new D(`Invalid config at ${o}: defaultProfile "${t.defaultProfile}" is not in profiles.`)}function Ft(t){let o=L(t,"package.json");if(!M(o))return null;try{return JSON.parse(E(o,"utf8")).name??null}catch{return null}}import{existsSync as _,readFileSync as Wt}from"node:fs";import{join as Jt}from"node:path";var qt=".d360-writer.json";function F(t=process.cwd()){return Jt(t,qt)}function W(t=process.cwd()){return _(F(t))}function J(t=process.cwd()){let o=F(t);if(!_(o))return{};try{let e=JSON.parse(Wt(o,"utf8").replace(/^/,"")),r=e.docsDir?.replace(/\/+$/,"");return{projectId:e.projectId,captureDir:e.captureDir??(r?`${r}/_capture`:void 0),outputDir:e.outputDir??(r?`${r}/_screenshots`:void 0)}}catch{return{}}}async function z(t={}){let o=process.cwd(),e=w(o),r=t.yes===!0;if(q(e)&&!r&&!await Gt({message:`${e} already exists. Overwrite?`,default:!1})){console.log("Init cancelled.");return}let n=J(o),i=n.projectId||Vt(o)||Yt(o),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(" + ")}.`)):W(o)&&(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 zt({message:"Live app URL (where captures start):",default:"https://example.com/",validate:p=>p.startsWith("http")?!0:"Must be a full URL"}),d={projectId:i,...n.captureDir?{captureDir:n.captureDir}:{},...n.outputDir?{outputDir:n.outputDir}:{},profiles:{staging:{connection:{startUrl:a}}},defaultProfile:"staging"};k(e,d),console.log(""),console.log(`\u2713 Wrote ${e}`),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 o=Xt(t,"package.json");if(!q(o))return null;try{return JSON.parse(Ht(o,"utf8")).name??null}catch{return null}}import{chromium as to}from"playwright";import{existsSync as G,mkdirSync as Kt,statSync as Qt}from"node:fs";import{homedir as Zt}from"node:os";import{join as U,dirname as Ho}from"node:path";function H(){return U(Zt(),".document360-capture","auth-states")}function R(t,o){return U(H(),t,`${o}.json`)}function Y(t){let o=U(H(),t);G(o)||Kt(o,{recursive:!0})}function $(t,o){let e=R(t,o);if(!G(e))return{path:e,exists:!1};let r=Qt(e);return{path:e,exists:!0,ageMs:Date.now()-r.mtimeMs}}async function X(t){let o=y(w()),e=v(o,t.profile);Y(o.projectId);let r=R(o.projectId,e.name);console.log(""),e.production&&console.log(`\u26A0 Profile "${e.name}" is PRODUCTION.`),console.log(`Opening Chromium against ${e.connection.startUrl}`),console.log(`Auth-state will save to: ${r}`),console.log("");let n=await to.launch({headless:!1,args:["--start-maximized"]}),i=await n.newContext({viewport:null}),s=await i.newPage();await s.goto(e.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(e.connection.authBoundaryUrlPattern),d=setInterval(()=>{a.test(s.url())&&i.storageState({path:r}).catch(()=>{})},3e4);try{for(;;){await oo();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(d),await n.close().catch(()=>{})}}function oo(){return new Promise(t=>{let o=()=>{process.stdin.off("data",o),process.stdin.pause(),t()};process.stdin.resume(),process.stdin.once("data",o)})}import{existsSync as xo}from"node:fs";import{isAbsolute as wt,resolve as yt}from"node:path";import{writeFileSync as V,mkdirSync as eo,existsSync as K}from"node:fs";import{createRequire as ro}from"node:module";import{join as A,isAbsolute as Q,resolve as Z}from"node:path";var I=ro(import.meta.url);function tt(t){let{cwd:o,profileName:e,captureDir:r,viewport:n,storageStatePath:i}=t,s=A(o,".d360-capture-cache");K(s)||eo(s,{recursive:!0});let a=A(s,`playwright.${e}.config.cjs`),d=Q(r)?r:Z(o,r),p=Q(i)?i:Z(o,i),h=I.resolve("@playwright/test"),l=`// Generated by document360-capture \u2014 do not edit by hand.
4
- const { defineConfig } = require(${JSON.stringify(h)});
2
+ import{Command as ke}from"commander";import{createRequire as Ie}from"node:module";import{input as qt,confirm as Gt}from"@inquirer/prompts";import{existsSync as X,readFileSync as zt}from"node:fs";import{basename as Xt,join as Yt}from"node:path";import{readFileSync as F,writeFileSync as Tt,existsSync as H}from"node:fs";import{basename as Et,join as J}from"node:path";var Lt=".d360-capture.json",C={captureDir:"user-docs/_capture",outputDir:"user-docs/_screenshots",viewport:{width:1440,height:900},loginTimeoutMs:0,annotation:{enabled:!0}},b=class extends Error{};function g(t=process.cwd()){return J(t,Lt)}function m(t){if(!H(t))throw new Error(`No project config at ${t}. Run \`d360-capture init\` to create one.`);let e=JSON.parse(F(t,"utf8").replace(/^/,""));return Bt(e,t)}function U(t,e){Tt(t,JSON.stringify(e,null,2)+`
3
+ `,"utf8")}function Mt(t){try{return"^"+new URL(t).origin.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}catch{return"."}}function Bt(t,e,o=process.cwd()){_t(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||Ft(o)||Et(o),captureDir:t.captureDir??C.captureDir,outputDir:t.outputDir??C.outputDir,viewport:t.viewport??{...C.viewport},loginTimeoutMs:t.loginTimeoutMs??C.loginTimeoutMs,annotation:t.annotation??{...C.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||Mt(r.connection.startUrl)},captureDir:r.captureDir??t.captureDir,outputDir:r.outputDir??t.outputDir,viewport:r.viewport??t.viewport,production:r.production===!0,authBoundaryDefaulted:!n}}function _t(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 Ft(t){let e=J(t,"package.json");if(!H(e))return null;try{return JSON.parse(F(e,"utf8")).name??null}catch{return null}}import{existsSync as W,readFileSync as Ht}from"node:fs";import{join as Jt}from"node:path";var Wt=".d360-writer.json";function q(t=process.cwd()){return Jt(t,Wt)}function G(t=process.cwd()){return W(q(t))}function z(t=process.cwd()){let e=q(t);if(!W(e))return{};try{let o=JSON.parse(Ht(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 Y(t={}){let e=process.cwd(),o=g(e),r=t.yes===!0;if(X(o)&&!r&&!await Gt({message:`${o} already exists. Overwrite?`,default:!1})){console.log("Init cancelled.");return}let n=z(e),i=n.projectId||Kt(e)||Xt(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(" + ")}.`)):G(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 qt({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"};U(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 Kt(t){let e=Yt(t,"package.json");if(!X(e))return null;try{return JSON.parse(zt(e,"utf8")).name??null}catch{return null}}import{chromium as te}from"playwright";import{existsSync as K,mkdirSync as Vt,statSync as Qt}from"node:fs";import{homedir as Zt}from"node:os";import{join as O,dirname as Ye}from"node:path";function V(){return O(Zt(),".document360-capture","auth-states")}function j(t,e){return O(V(),t,`${e}.json`)}function Q(t){let e=O(V(),t);K(e)||Vt(e,{recursive:!0})}function x(t,e){let o=j(t,e);if(!K(o))return{path:o,exists:!1};let r=Qt(o);return{path:o,exists:!0,ageMs:Date.now()-r.mtimeMs}}async function Z(t){let e=m(g()),o=$(e,t.profile);Q(e.projectId);let r=j(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 te.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 ee();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 ee(){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 Pe,readdirSync as ve}from"node:fs";import{isAbsolute as Pt,resolve as vt}from"node:path";import{writeFileSync as tt,mkdirSync as oe,existsSync as et}from"node:fs";import{createRequire as re}from"node:module";import{join as R,isAbsolute as ot,resolve as rt}from"node:path";var N=re(import.meta.url);function nt(t){let{cwd:e,profileName:o,captureDir:r,viewport:n,storageStatePath:i}=t,s=R(e,".d360-capture-cache");et(s)||oe(s,{recursive:!0});let a=R(s,`playwright.${o}.config.cjs`),l=ot(r)?r:rt(e,r),p=ot(i)?i:rt(e,i),f=N.resolve("@playwright/test"),c=`// Generated by document360-capture \u2014 do not edit by hand.
4
+ const { defineConfig } = require(${JSON.stringify(f)});
5
5
  module.exports = defineConfig({
6
- testDir: ${JSON.stringify(d)},
6
+ testDir: ${JSON.stringify(l)},
7
7
  testMatch: '**/*.spec.ts',
8
8
  fullyParallel: false,
9
9
  workers: 1,
@@ -18,6 +18,8 @@ module.exports = defineConfig({
18
18
  contextOptions: { reducedMotion: 'reduce' },
19
19
  },
20
20
  });
21
- `;V(a,l,"utf8");let c=A(s,`tsconfig.${e}.json`),g=b=>b.replace(/\\/g,"/"),u=I.resolve("document360-capture/helpers"),f=A(I.resolve("@playwright/test/package.json"),".."),m=A(f,"index.mjs"),C=K(m)?m:h,j={compilerOptions:{baseUrl:".",paths:{"document360-capture/helpers":[g(u)],"@playwright/test":[g(C)]}}};return V(c,JSON.stringify(j,null,2),"utf8"),{configPath:a,tsconfigPath:c}}import{spawn as no}from"node:child_process";import{existsSync as io}from"node:fs";import{createRequire as so}from"node:module";import{fileURLToPath as ao}from"node:url";import{delimiter as co,dirname as O,join as ot}from"node:path";var lo=so(import.meta.url);async function et(t){let{cwd:o,configPath:e,tsconfigPath:r,startUrl:n,authBoundaryUrlPattern:i,specId:s,captureDirAbs:a}=t,d=fo();if(!d)return{exitCode:127,stdout:"",stderr:"Cannot locate @playwright/test CLI. Try: npm install -g document360-capture (reinstall)"};let p=[d,"test","--config",e,"--tsconfig",r];return s&&p.push(po(`${s}.spec.ts`)),new Promise(h=>{let l="",c="",g=no(process.execPath,p,{cwd:o,env:{...process.env,CAPTURE_START_URL:n,CAPTURE_AUTH_BOUNDARY:i,NODE_PATH:uo()},shell:!1});g.stdout.on("data",u=>{let f=u.toString();l+=f,process.stdout.write(f)}),g.stderr.on("data",u=>{let f=u.toString();c+=f,process.stderr.write(f)}),g.on("error",u=>{c+=`
22
- [spawn error] ${u.message}`,h({exitCode:127,stdout:l,stderr:c})}),g.on("close",u=>{h({exitCode:u??1,stdout:l,stderr:c})})})}function po(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function uo(){let t=O(ao(import.meta.url));for(let e=0;e<4&&!io(ot(t,"package.json"));e++)t=O(t);let o=[ot(t,"node_modules"),O(t)];return process.env.NODE_PATH&&o.push(process.env.NODE_PATH),o.join(co)}function fo(){let t=["@playwright/test/cli","@playwright/test/cli.js","playwright/cli","playwright/cli.js"];for(let o of t)try{return lo.resolve(o)}catch{}return null}var go=[/login\.microsoftonline\.com/i,/login\.live\.com/i,/accounts\.google\.com/i,/\/oauth(2)?\/authorize/i,/\/sign[- ]?in/i,/\bauthn\b/i],ho=[/waitForURL.*timed?[- ]?out/i,/page\.goto.*redirected/i,/storageState.*invalid/i];function rt(t){return go.some(o=>o.test(t))?!0:ho.some(o=>o.test(t))}import{readdirSync as gt,readFileSync as wo,existsSync as dt,copyFileSync as yo}from"node:fs";import{join as ht}from"node:path";import N from"sharp";function it(t){return t.replace(/[<>&"']/g,o=>({"<":"&lt;",">":"&gt;","&":"&amp;",'"':"&quot;","'":"&apos;"})[o])}function st(t){let e=Math.max(0,t.x-4),r=Math.max(0,t.y-4),n=t.width+8,i=t.height+8;return`<rect x="${e}" y="${r}" width="${n}" height="${i}" rx="6" ry="6" fill="rgba(255,213,0,0.18)" stroke="#f5a623" stroke-width="2.5"/>`}function at(t){return`<rect x="${t.x}" y="${t.y}" width="${t.width}" height="${t.height}" fill="#1f2328"/>`}var ct="-apple-system,Segoe UI,sans-serif",nt=(t,o,e)=>Math.max(o,Math.min(e,t));function mo(t,o,e=4){return t.x<o.x+o.width+e&&t.x+t.width+e>o.x&&t.y<o.y+o.height+e&&t.y+t.height+e>o.y}function lt(t){let e=t.x+t.width+14+4,r=t.y+t.height/2;return{cx:e,cy:r,r:14,rect:{x:e-14,y:r-14,width:28,height:28}}}function pt(t,o){let{cx:e,cy:r,r:n,rect:i}=lt(o);return{svg:`<circle cx="${e}" cy="${r}" r="${n}" fill="#d1242f" stroke="#fff" stroke-width="2"/><text x="${e}" y="${r+5}" font-family="${ct}" font-size="14" font-weight="700" fill="#fff" text-anchor="middle">${it(t)}</text>`,rect:i}}function ut(t,o,e,r,n,i=[]){let{cx:s,cy:a,r:d}=lt(e),p=`${t}. ${o}`,h=8,l=13,g=p.length*7.2+h*2,u=l+h*2,f=s+d+6;f+g>r&&(f=e.x-g-d-6),f=nt(f,0,Math.max(0,r-g));let m=nt(a-u/2,0,Math.max(0,n-u)),C=u+6,j=P=>{if(P<0||P+u>n)return!1;let Ot={x:f,y:P,width:g,height:u};return!i.some(Nt=>mo(Ot,Nt))},b=m;if(!j(m))for(let P=1;P<=20;P++){if(j(m+P*C)){b=m+P*C;break}if(j(m-P*C)){b=m-P*C;break}}let It={x:f,y:b,width:g,height:u};return{svg:`<rect x="${f}" y="${b}" width="${g}" height="${u}" rx="4" ry="4" fill="#1f2328" stroke="#fff" stroke-width="1.5"/><text x="${f+h}" y="${b+h+l-2}" font-family="${ct}" font-size="${l}" fill="#fff">${it(p)}</text>`,rect:It}}async function Po(t){let o=JSON.parse(wo(t,"utf8"));if(!dt(o.rawPath))return{id:o.id,ok:!1,reason:`raw missing: ${o.rawPath}`};let e=await N(o.rawPath).metadata(),r=e.width??1440,n=e.height??900,i=[],s=[];for(let c of o.highlight)i.push(st(c.bbox)),s.push(c.bbox);let a=[];for(let c of o.redact)a.push(at(c.bbox)),s.push(c.bbox);let d=[];for(let c of o.annotations){let{svg:g,rect:u}=pt(c.label,c.bbox);d.push(g),s.push(u)}let p=[];for(let c of o.annotations){let{svg:g,rect:u}=ut(c.label,c.text,c.bbox,r,n,s);p.push(g),s.push(u)}let h=[...i,...a,...p,...d];if(h.length===0)return await N(o.rawPath).toFile(o.finalPath),{id:o.id,ok:!0,finalPath:o.finalPath};let l=`<svg xmlns="http://www.w3.org/2000/svg" width="${r}" height="${n}">${h.join("")}</svg>`;return await N(o.rawPath).composite([{input:Buffer.from(l),top:0,left:0}]).toFile(o.finalPath),{id:o.id,ok:!0,finalPath:o.finalPath}}async function ft(t){let o=gt(t).filter(r=>r.endsWith(".raw.png")),e=[];for(let r of o){let n=ht(t,r),i=n.replace(/\.raw\.png$/,".png");try{yo(n,i),e.push({id:r.replace(/\.raw\.png$/,""),ok:!0,finalPath:i})}catch(s){e.push({id:r,ok:!1,reason:String(s)})}}return e}async function mt(t,o){if(!dt(t))return[];if(!o)return ft(t);let e=gt(t).filter(n=>n.endsWith(".annotations.json"));if(e.length===0)return ft(t);let r=[];for(let n of e){let i=ht(t,n);try{r.push(await Po(i))}catch(s){r.push({id:n,ok:!1,reason:String(s)})}}return r}async function Pt(t,o){let e=process.cwd(),r=y(w(e)),n=v(r,o.profile),i=n.name,s=$(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=wt(n.captureDir)?n.captureDir:yt(e,n.captureDir);if(!xo(a))return console.error(`\u2717 Capture directory missing: ${a}`),console.error(" Generate at least one spec there before running capture."),2;let{configPath:d,tsconfigPath:p}=tt({cwd:e,profileName:i,captureDir:n.captureDir,viewport:n.viewport,storageStatePath:R(r.projectId,i)});console.log(""),n.production&&console.log(`\u26A0 Profile "${i}" is PRODUCTION (read-only capture).`),console.log(`\u25B6 Running specs against ${i} (${n.connection.startUrl})`),console.log(t?` Single spec: ${t}`:` All specs in ${n.captureDir}`),console.log("");let h=await et({cwd:e,configPath:d,tsconfigPath:p,startUrl:n.connection.startUrl,authBoundaryUrlPattern:n.connection.authBoundaryUrlPattern,specId:t,captureDirAbs:a});if(h.exitCode!==0){let m=h.stdout+`
23
- `+h.stderr;return console.error(""),console.error(`\u2717 Playwright exited with code ${h.exitCode}`),rt(m)&&(console.error(""),console.error(" The output mentions a login page \u2014 if specs that previously passed now"),console.error(" fail this way, your saved session may have expired. Re-auth with:"),console.error(` d360-capture auth --profile ${i}`),console.error(" Otherwise, inspect the failure above (selector timeouts are spec bugs,"),console.error(" not auth bugs) and the error-context file Playwright printed.")),h.exitCode}let l=!o.noAnnotate&&r.annotation.enabled,c=wt(n.outputDir)?n.outputDir:yt(e,n.outputDir);console.log(""),console.log(l?"\u25B6 Annotating screenshots...":"\u25B6 Skipping annotation (--no-annotate or config off)");let g=await mt(c,l),u=g.filter(m=>m.ok).length,f=g.filter(m=>!m.ok);if(console.log(""),console.log(`\u2713 ${u} screenshot${u===1?"":"s"} ready in ${n.outputDir}`),f.length>0){console.log(`\u2717 ${f.length} annotation failure${f.length===1?"":"s"}:`);for(let m of f)console.log(` - ${m.id}: ${m.reason}`)}return f.length===0?0:1}import{existsSync as xt,readdirSync as vo,statSync as $o}from"node:fs";import{isAbsolute as vt,join as bo,resolve as $t}from"node:path";async function bt(){let t=process.cwd(),o=y(w(t)),e=vt(o.captureDir)?o.captureDir:$t(t,o.captureDir),r=vt(o.outputDir)?o.outputDir:$t(t,o.outputDir);if(!xt(e)){console.log(`Capture dir does not exist: ${e}`);return}let n=vo(e).filter(i=>i.endsWith(".spec.ts")).sort();if(n.length===0){console.log(`No specs found in ${o.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=bo(r,`${s}.png`),d=xt(a)?new Date($o(a).mtimeMs).toISOString().slice(0,19).replace("T"," "):"(never)";console.log(` ${s.padEnd(47)} ${d}`)}console.log("")}import{existsSync as Dt,readFileSync as Ct}from"node:fs";import{isAbsolute as Do,resolve as Co}from"node:path";import{createRequire as jo}from"node:module";var jt=jo(import.meta.url);async function St(t){let o=process.cwd(),e=0,r=l=>console.log(` \u2713 ${l}`),n=l=>{console.log(` \u2717 ${l}`),e++};console.log(""),console.log("Checking document360-capture setup..."),console.log("");let i=w(o);if(!Dt(i))return n(`Project config missing at ${i} \u2014 run \`d360-capture init\``),2;r(`Project config: ${i}`);let s;try{s=y(i),r(`Config parses; projectId="${s.projectId}"`)}catch(l){return n(`Config invalid: ${l.message}`),2}let a;try{a=v(s,t.profile),r(`Profile "${a.name}" resolved (startUrl=${a.connection.startUrl})${a.production?" \u26A0 PRODUCTION":""}`)}catch(l){return n(`Profile resolve failed: ${l.message}`),1}a.authBoundaryDefaulted&&r(`Auth boundary: ${a.connection.authBoundaryUrlPattern} (auto-derived from start URL; set connection.authBoundaryUrlPattern to override)`);let d=Do(a.captureDir)?a.captureDir:Co(o,a.captureDir);Dt(d)?r(`Capture dir exists: ${a.captureDir}`):n(`Capture dir missing: ${a.captureDir}`);let p=$(s.projectId,a.name);if(p.exists){let l=p.ageMs!=null?Math.floor(p.ageMs/864e5):null;r(`Auth-state present (${l}d old): ${p.path}`);try{let c=JSON.parse(Ct(p.path,"utf8"));c&&typeof c=="object"?r("Auth-state parses as JSON"):n("Auth-state is not a valid object")}catch(c){n(`Auth-state unreadable: ${c.message}`)}}else n(`Auth-state missing for "${a.name}" \u2014 run \`d360-capture auth --profile ${a.name}\``);try{let l=jt.resolve("playwright/package.json"),c=JSON.parse(Ct(l,"utf8"));r(`playwright@${c.version} installed`)}catch{n("playwright not resolvable \u2014 reinstall document360-capture")}return["@playwright/test/cli","@playwright/test/cli.js"].some(l=>{try{return jt.resolve(l),!0}catch{return!1}})?r("@playwright/test CLI resolvable"):n("@playwright/test CLI not resolvable \u2014 reinstall document360-capture"),console.log(""),console.log(e===0?"All checks passed.":`${e} check${e===1?"":"s"} failed.`),console.log(""),e===0?0:1}async function Rt(){let t=y(w());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[o,e]of Object.entries(t.profiles)){let r=$(t.projectId,o),n=r.exists?`auth saved ${r.ageMs!=null?Math.floor(r.ageMs/864e5)+"d ago":""}`:"no auth-state",i=`${o===t.defaultProfile?" \u25CF":""}${e.production?" \u26A0 PRODUCTION":""}`;console.log(` ${o.padEnd(10)} ${e.connection.startUrl}${i}`),e.captureDir&&console.log(` captureDir: ${e.captureDir}`),console.log(` ${n}`)}console.log("")}function At(){let t=y(w());console.log("");for(let[o,e]of Object.entries(t.profiles)){let r=o===t.defaultProfile?"\u25CF ":" ",n=e.production?" \u26A0 PRODUCTION":"",s=$(t.projectId,o).exists?"auth saved":"no auth-state";console.log(`${r}${o} \u2192 ${e.connection.startUrl}${n} [${s}]`)}console.log(""),console.log("\u25CF = default. Switch with: d360-capture profile use <name>"),console.log("")}function kt(t){let o=w(),e=y(o);if(!e.profiles[t])return console.error(`\u2717 Unknown profile "${t}". Available: ${Object.keys(e.profiles).join(", ")}`),1;e.defaultProfile=t,k(o,e);let r=e.profiles[t].production?" \u26A0 PRODUCTION":"";return console.log(`\u2713 Default profile is now "${t}"${r}`),0}function Ut(t){let o=y(w()),e=v(o,t);return console.log(""),console.log(`Profile ${e.name}${e.production?" \u26A0 PRODUCTION":""}`),console.log(` startUrl: ${e.connection.startUrl}`),console.log(` authBoundary:${e.connection.authBoundaryUrlPattern}`),console.log(` captureDir: ${e.captureDir}`),console.log(` outputDir: ${e.outputDir}`),console.log(` viewport: ${e.viewport.width}x${e.viewport.height}`),console.log(""),0}var Ao=Ro(import.meta.url),ko=Ao("../package.json"),x=new So;function T(t){t.env&&(console.error("\u2717 --env was replaced by --profile (connection profiles). Use: --profile <name>"),process.exit(2))}x.name("d360-capture").description("Capture product screenshots from Playwright specs. Login once, capture many.").version(ko.version);x.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 z(t)});x.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(t),await X(t)});x.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,o)=>{T(o);let e=await Pt(t,o);e!==0&&process.exit(e)});x.command("list").description("List discovered specs and their last-capture times.").action(async()=>{await bt()});x.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(t);let o=await St(t);o!==0&&process.exit(o)});x.command("status").description("Print resolved config and auth-state freshness.").action(async()=>{await Rt()});var B=x.command("profile").description("Manage capture profiles for the current repo");B.command("list",{isDefault:!0}).description("List profiles (\u25CF = default)").action(()=>At());B.command("use <name>").description("Set the default profile for this repo").action(t=>process.exit(kt(t)));B.command("show [name]").description("Print the resolved profile").action(t=>process.exit(Ut(t)));x.parseAsync(process.argv).catch(t=>{console.error(""),console.error(`\u2717 ${t.message}`),process.exit(1)});
21
+ `;tt(a,c,"utf8");let u=R(s,`tsconfig.${o}.json`),y=I=>I.replace(/\\/g,"/"),w=N.resolve("document360-capture/helpers"),h=R(N.resolve("@playwright/test/package.json"),".."),P=R(h,"index.mjs"),D=et(P)?P:f,k={compilerOptions:{baseUrl:".",paths:{"document360-capture/helpers":[y(w)],"@playwright/test":[y(D)]}}};return tt(u,JSON.stringify(k,null,2),"utf8"),{configPath:a,tsconfigPath:u}}import{spawn as ne}from"node:child_process";import{existsSync as ie}from"node:fs";import{createRequire as se}from"node:module";import{fileURLToPath as ae}from"node:url";import{delimiter as ce,dirname as T,join as it}from"node:path";var le=se(import.meta.url);async function st(t){let{cwd:e,configPath:o,tsconfigPath:r,startUrl:n,authBoundaryUrlPattern:i,specId:s,captureDirAbs:a}=t,l=fe();if(!l)return{exitCode:127,stdout:"",stderr:"Cannot locate @playwright/test CLI. Try: npm install -g document360-capture (reinstall)"};let p=[l,"test","--config",o,"--tsconfig",r];return s&&p.push(pe(`${s}.spec.ts`)),new Promise(f=>{let c="",u="",y=ne(process.execPath,p,{cwd:e,env:{...process.env,CAPTURE_START_URL:n,CAPTURE_AUTH_BOUNDARY:i,NODE_PATH:ue()},shell:!1}),w=()=>{try{y.kill()}catch{}};t.signal&&(t.signal.aborted?w():t.signal.addEventListener("abort",w,{once:!0})),y.stdout.on("data",h=>{let P=h.toString();c+=P,process.stdout.write(P)}),y.stderr.on("data",h=>{let P=h.toString();u+=P,process.stderr.write(P)}),y.on("error",h=>{t.signal?.removeEventListener("abort",w),u+=`
22
+ [spawn error] ${h.message}`,f({exitCode:127,stdout:c,stderr:u})}),y.on("close",h=>{t.signal?.removeEventListener("abort",w),f({exitCode:h??1,stdout:c,stderr:u})})})}function pe(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function ue(){let t=T(ae(import.meta.url));for(let o=0;o<4&&!ie(it(t,"package.json"));o++)t=T(t);let e=[it(t,"node_modules"),T(t)];return process.env.NODE_PATH&&e.push(process.env.NODE_PATH),e.join(ce)}function fe(){let t=["@playwright/test/cli","@playwright/test/cli.js","playwright/cli","playwright/cli.js"];for(let e of t)try{return le.resolve(e)}catch{}return null}var ge=[/login\.microsoftonline\.com/i,/login\.live\.com/i,/accounts\.google\.com/i,/\/oauth(2)?\/authorize/i,/\/sign[- ]?in/i,/\bauthn\b/i],de=[/waitForURL.*timed?[- ]?out/i,/page\.goto.*redirected/i,/storageState.*invalid/i];function at(t){return ge.some(e=>e.test(t))?!0:de.some(e=>e.test(t))}import{readdirSync as mt,readFileSync as he,existsSync as ht,copyFileSync as we}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 lt(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 L="-apple-system,Segoe UI,sans-serif",ct=(t,e,o)=>Math.max(e,Math.min(o,t)),me=13;function ut(t,e,o,r){let n=me,i=ct(e.x,n,Math.max(n,o-n)),s=ct(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 ft(t,e){return t===0?0:e.pad*2+t*e.lineHeight}function gt(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 ye(t){let e=JSON.parse(he(t,"utf8"));if(!ht(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=ft(e.annotations.length,i),a=n+s,l=[];for(let c of e.highlight)l.push(lt(c.bbox));for(let c of e.redact)l.push(pt(c.bbox));for(let c of e.annotations)l.push(ut(c.label,c.bbox,r,n));if(s>0&&l.push(`<rect x="0" y="${n}" width="${r}" height="${s}" fill="#ffffff"/>`,gt(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 dt(t){let e=mt(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{we(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 yt(t,e){if(!ht(t))return[];if(!e)return dt(t);let o=mt(t).filter(n=>n.endsWith(".annotations.json"));if(o.length===0)return dt(t);let r=[];for(let n of o){let i=wt(t,n);try{r.push(await ye(i))}catch(s){r.push({id:n,ok:!1,reason:String(s)})}}return r}async function $t(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 a=Pt(n.captureDir)?n.captureDir:vt(o,n.captureDir);if(!Pe(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}=nt({cwd:o,profileName:i,captureDir:n.captureDir,viewport:n.viewport,storageStatePath:j(r.projectId,i)}),f=t?[t]:$e(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,y=xe(()=>{c?u?.abort():(c=!0,console.log(`
23
+ \u23F8 Stopping after the current spec\u2026 (press again to stop now)`))}),w=[],h=!1;try{for(let d of f){if(c)break;u=new AbortController,console.log(`
24
+ \u25B6 ${d}`);let A=await st({cwd:o,configPath:l,tsconfigPath:p,startUrl:n.connection.startUrl,authBoundaryUrlPattern:n.connection.authBoundaryUrlPattern,specId:d,captureDirAbs:a,signal:u.signal});if(w.push({id:d,ok:A.exitCode===0}),A.exitCode!==0&&!h&&at(`${A.stdout}
25
+ ${A.stderr}`)&&(h=!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{y(),u=null}let P=w.filter(d=>d.ok).length,D=w.filter(d=>!d.ok),k=f.length-w.length,I=!e.noAnnotate&&r.annotation.enabled,Nt=Pt(n.outputDir)?n.outputDir:vt(o,n.outputDir);console.log(""),console.log(I?"\u25B6 Annotating screenshots\u2026":"\u25B6 Skipping annotation (--no-annotate or config off)");let S=(await yt(Nt,I)).filter(d=>!d.ok);console.log(""),console.log(`\u2713 ${P} captured`+(D.length?` \xB7 \u2717 ${D.length} failed`:"")+(k?` \xB7 ${k} skipped (stopped)`:"")+` \u2192 ${n.outputDir}`);for(let d of D)console.log(` \u2717 ${d.id}`);if(S.length>0){console.log(`\u2717 ${S.length} annotation failure${S.length===1?"":"s"}:`);for(let d of S)console.log(` - ${d.id}: ${d.reason}`)}return D.length===0&&S.length===0?0:1}function $e(t){return ve(t).filter(e=>e.endsWith(".spec.ts")).map(e=>e.slice(0,-8)).sort()}function xe(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 xt,readdirSync as be,statSync as De}from"node:fs";import{isAbsolute as bt,join as Se,resolve as Dt}from"node:path";async function St(){let t=process.cwd(),e=m(g(t)),o=bt(e.captureDir)?e.captureDir:Dt(t,e.captureDir),r=bt(e.outputDir)?e.outputDir:Dt(t,e.outputDir);if(!xt(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$/,""),a=Se(r,`${s}.png`),l=xt(a)?new Date(De(a).mtimeMs).toISOString().slice(0,19).replace("T"," "):"(never)";console.log(` ${s.padEnd(47)} ${l}`)}console.log("")}import{existsSync as Ct,readFileSync as jt}from"node:fs";import{isAbsolute as Ce,resolve as je}from"node:path";import{createRequire as Re}from"node:module";var Rt=Re(import.meta.url);async function kt(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(!Ct(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=Ce(a.captureDir)?a.captureDir:je(e,a.captureDir);Ct(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(jt(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=Rt.resolve("playwright/package.json"),u=JSON.parse(jt(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 Rt.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(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 At(){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 Ut(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,U(e,o);let r=o.profiles[t].production?" \u26A0 PRODUCTION":"";return console.log(`\u2713 Default profile is now "${t}"${r}`),0}function Ot(t){let e=m(g()),o=$(e,t);return 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}`),console.log(""),0}var Ae=Ie(import.meta.url),Ue=Ae("../package.json"),v=new ke;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(Ue.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 Y(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 Z(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 $t(t,e);o!==0&&process.exit(o)});v.command("list").description("List discovered specs and their last-capture times.").action(async()=>{await St()});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 kt(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(()=>At());_.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(Ot(t)));v.parseAsync(process.argv).catch(t=>{console.error(""),console.error(`\u2717 ${t.message}`),process.exit(1)});
@@ -6,6 +6,8 @@ export type RunSpecsArgs = {
6
6
  authBoundaryUrlPattern: string;
7
7
  specId?: string;
8
8
  captureDirAbs: string;
9
+ /** Abort the in-flight Playwright run (hard interrupt) — kills the child process. */
10
+ signal?: AbortSignal;
9
11
  };
10
12
  export type RunSpecsResult = {
11
13
  exitCode: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document360-capture",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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": {