document360-capture 0.2.16 → 0.2.18
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 +21 -16
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as
|
|
3
|
-
`,"utf8")}function
|
|
4
|
-
node "${t}" install chromium`);console.log("\u2713 Chromium ready.")}async function
|
|
5
|
-
const { defineConfig } = require(${JSON.stringify(
|
|
2
|
+
import{Command as po}from"commander";import{createRequire as uo}from"node:module";import{input as ae,confirm as nt}from"@inquirer/prompts";import{existsSync as it,readFileSync as ce}from"node:fs";import{basename as le,join as pe}from"node:path";import{readFileSync as Q,writeFileSync as Xt,existsSync as V,mkdirSync as Qt}from"node:fs";import{basename as Vt,dirname as Zt,join as L}from"node:path";var te=L(".d360-writer","capture.json"),k={captureDir:".d360-writer/user-docs/_capture",outputDir:".d360-writer/user-docs/_screenshots",viewport:{width:1440,height:900},loginTimeoutMs:0,annotation:{enabled:!0}},D=class extends Error{};function m(t=process.cwd()){return L(t,te)}function h(t){if(!V(t))throw new Error(`No project config at ${t}. Run \`d360-capture init\` to create one.`);let e=JSON.parse(Q(t,"utf8").replace(/^/,""));return oe(e,t)}function O(t,e){Qt(Zt(t),{recursive:!0}),Xt(t,JSON.stringify(e,null,2)+`
|
|
3
|
+
`,"utf8")}function ee(t){try{return"^"+new URL(t).origin.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}catch{return"."}}function oe(t,e,o=process.cwd()){re(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||ne(o)||Vt(o),captureDir:t.captureDir??k.captureDir,outputDir:t.outputDir??k.outputDir,viewport:t.viewport??{...k.viewport},loginTimeoutMs:t.loginTimeoutMs??k.loginTimeoutMs,annotation:t.annotation??{...k.annotation},profiles:t.profiles,defaultProfile:n}}function C(t,e){let o=e??t.defaultProfile,r=t.profiles[o];if(!r){let i=Object.keys(t.profiles).join(", ");throw new D(`Unknown profile "${o}". Available: ${i}`)}let n=r.connection.authBoundaryUrlPattern;return{name:o,connection:{startUrl:r.connection.startUrl,authBoundaryUrlPattern:n||ee(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 re(t,e){if(t.environments&&!t.profiles)throw new D(`Config at ${e} uses the old "environments" shape, which is no longer supported. It is now "profiles" (each with a "connection" { startUrl }) + "defaultProfile". Re-run \`d360-capture init\`, then \`d360-capture auth --profile <name>\`.`);if(!t.profiles||Object.keys(t.profiles).length===0)throw new D(`Invalid config at ${e}: needs at least one profile under "profiles".`);for(let[o,r]of Object.entries(t.profiles))if(!r?.connection?.startUrl)throw new D(`Invalid config at ${e}: profile "${o}" is missing connection.startUrl.`);if(t.defaultProfile&&!t.profiles[t.defaultProfile])throw new D(`Invalid config at ${e}: defaultProfile "${t.defaultProfile}" is not in profiles.`)}function ne(t){let e=L(t,"package.json");if(!V(e))return null;try{return JSON.parse(Q(e,"utf8")).name??null}catch{return null}}import{existsSync as Z,readFileSync as ie}from"node:fs";import{join as tt}from"node:path";var se=tt(".d360-writer","config.json");function et(t=process.cwd()){return tt(t,se)}function ot(t=process.cwd()){return Z(et(t))}function rt(t=process.cwd()){let e=et(t);if(!Z(e))return{};try{let o=JSON.parse(ie(e,"utf8").replace(/^/,"")),r=o.docsDir?.replace(/\/+$/,"");return{projectId:o.projectId,captureDir:o.captureDir??(r?`${r}/_capture`:void 0),outputDir:o.outputDir??(r?`${r}/_screenshots`:void 0),appUrl:typeof o.appUrl=="string"&&o.appUrl.startsWith("http")?o.appUrl:void 0}}catch{return{}}}async function st(t={}){let e=process.cwd(),o=m(e),r=t.yes===!0;if(it(o)&&!r&&!await nt({message:`${o} already exists. Overwrite?`,default:!1})){console.log("Init cancelled.");return}let n=rt(e),i=n.projectId||ue(e)||le(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(" + ")}.`)):ot(e)&&(console.log(""),console.log("Found .d360-writer/config.json (could not read defaults from it \u2014 using built-in defaults)."));let a=r?n.appUrl??"https://example.com/":await ae({message:"Live app URL (where captures start):",default:n.appUrl??"https://example.com/",validate:c=>c.startsWith("http")?!0:"Must be a full URL"}),p=r?!1:await nt({message:"Is this URL your production environment?",default:!1}),u=p?"production":"staging",f={projectId:i,...n.captureDir?{captureDir:n.captureDir}:{},...n.outputDir?{outputDir:n.outputDir}:{},profiles:{[u]:{connection:{startUrl:a},...p?{production:!0}:{}}},defaultProfile:u};O(o,f),console.log(""),console.log(`\u2713 Wrote ${o}`),r&&a==="https://example.com/"&&console.log(` \u26A0 Set the ${u} profile's connection.startUrl to your real app URL.`),console.log(""),console.log("Next steps:"),console.log(" 1. d360-capture auth # log in once, no timeout"),console.log(" 2. d360-capture capture # run all specs in the capture dir"),console.log(""),console.log("Add more profiles or tune the auth boundary later by editing .d360-writer/capture.json."),console.log("")}function ue(t){let e=pe(t,"package.json");if(!it(e))return null;try{return JSON.parse(ce(e,"utf8")).name??null}catch{return null}}import{chromium as ve}from"playwright";import{existsSync as at,mkdirSync as fe,statSync as de}from"node:fs";import{homedir as ge}from"node:os";import{join as M,dirname as Ao}from"node:path";function ct(){return M(ge(),".document360-capture","auth-states")}function A(t,e){return M(ct(),t,`${e}.json`)}function lt(t){let e=M(ct(),t);at(e)||fe(e,{recursive:!0})}function j(t,e){let o=A(t,e);if(!at(o))return{path:o,exists:!1};let r=de(o);return{path:o,exists:!0,ageMs:Date.now()-r.mtimeMs}}import{existsSync as me}from"node:fs";import{spawn as he}from"node:child_process";import{createRequire as we}from"node:module";import{chromium as ye}from"playwright";var Pe=we(import.meta.url);function _(){try{return ye.executablePath()}catch{return null}}function T(){let t=_();return t!=null&&me(t)}async function N(){if(T())return;console.log(""),console.log("\u25B6 Chromium is not downloaded on this machine yet \u2014 fetching it now (one-time)\u2026");let t=Pe.resolve("playwright/cli.js"),e=await new Promise((o,r)=>{let n=he(process.execPath,[t,"install","chromium"],{stdio:"inherit"});n.on("error",r),n.on("exit",i=>o(i??1))});if(e!==0||!T())throw new Error(`Chromium download failed (exit code ${e}). Check your network/proxy and retry, or run:
|
|
4
|
+
node "${t}" install chromium`);console.log("\u2713 Chromium ready.")}async function pt(t){let e=h(m()),o=C(e,t.profile);lt(e.projectId);let r=A(e.projectId,o.name);await N(),console.log(""),o.production&&console.log(`\u26A0 Profile "${o.name}" is PRODUCTION.`),console.log(`Opening Chromium against ${o.connection.startUrl}`),console.log(`Auth-state will save to: ${r}`),console.log("");let n=await ve.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"),t.auto?(console.log(" Log in fully (MFA included) at your own pace."),console.log(" The session saves automatically when you reach the app \u2014"),console.log(" no keypress needed.")):(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),p=setInterval(()=>{a.test(s.url())&&i.storageState({path:r}).catch(()=>{})},3e4);try{if(t.auto){let u=Date.now()+6e5;for(;;){if(s.isClosed())throw new Error("Browser window was closed before login completed. Run d360-capture auth again.");if(a.test(s.url()))break;if(Date.now()>u)throw new Error("Timed out waiting for login (10 minutes). Run d360-capture auth again.");await s.waitForTimeout(1e3)}await s.waitForTimeout(2e3)}else for(;;){await $e();let u=s.url();if(a.test(u))break;console.log(""),console.log("\u2717 Login doesn't look complete yet \u2014 the browser is still at:"),console.log(` ${u}`),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(p),await n.close().catch(()=>{})}}function $e(){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 Fe,readdirSync as Je,readFileSync as He}from"node:fs";import{isAbsolute as Ut,join as We,resolve as It}from"node:path";import{writeFileSync as ut,mkdirSync as xe,existsSync as ft}from"node:fs";import{createRequire as be}from"node:module";import{join as U,isAbsolute as dt,resolve as gt}from"node:path";var B=be(import.meta.url);function mt(t){let{cwd:e,profileName:o,captureDir:r,viewport:n,storageStatePath:i}=t,s;try{t.startUrl&&(s=new URL(t.startUrl).origin)}catch{}let a=U(e,".d360-writer","cache");ft(a)||xe(a,{recursive:!0});let p=U(a,`playwright.${o}.config.cjs`),u=dt(r)?r:gt(e,r),f=dt(i)?i:gt(e,i),c=B.resolve("@playwright/test"),d=`// Generated by document360-capture \u2014 do not edit by hand.
|
|
5
|
+
const { defineConfig } = require(${JSON.stringify(c)});
|
|
6
6
|
module.exports = defineConfig({
|
|
7
|
-
testDir: ${JSON.stringify(
|
|
7
|
+
testDir: ${JSON.stringify(u)},
|
|
8
8
|
testMatch: '**/*.spec.ts',
|
|
9
9
|
fullyParallel: false,
|
|
10
10
|
workers: 1,
|
|
11
11
|
retries: 0,
|
|
12
|
+
// Playwright's default per-test timeout is 30s \u2014 too tight for a capture spec that rides SSO
|
|
13
|
+
// redirects, enters a project, navigates a second route, and settles before shooting (dogfood
|
|
14
|
+
// 2026-07-26: bulk failures were "Test timeout of 30000ms exceeded" mid-navigation).
|
|
15
|
+
timeout: 120000,
|
|
12
16
|
reporter: [['list']],
|
|
13
17
|
use: {
|
|
14
|
-
headless: false
|
|
18
|
+
headless: false,${s?`
|
|
19
|
+
baseURL: ${JSON.stringify(s)},`:""}
|
|
15
20
|
viewport: ${JSON.stringify(n)},
|
|
16
|
-
storageState: ${JSON.stringify(
|
|
21
|
+
storageState: ${JSON.stringify(f)},
|
|
17
22
|
actionTimeout: 15000,
|
|
18
|
-
navigationTimeout:
|
|
23
|
+
navigationTimeout: 60000,
|
|
19
24
|
contextOptions: { reducedMotion: 'reduce' },
|
|
20
25
|
},
|
|
21
26
|
});
|
|
22
|
-
`;
|
|
23
|
-
[spawn error] ${
|
|
24
|
-
\u23F8 Stopping after the current spec\u2026 (press again to stop now)`))}),
|
|
25
|
-
\u25B6 ${
|
|
26
|
-
${
|
|
27
|
-
`)}catch{}}function
|
|
28
|
-
\u2B06 d360-capture ${t} \u2192 ${e.latest} available \u2014 run: npm i -g ${
|
|
29
|
-
`),e?.checkedAt&&Date.now()-new Date(e.checkedAt).getTime()<
|
|
27
|
+
`;ut(p,d,"utf8");let w=U(a,`tsconfig.${o}.json`),$=I=>I.replace(/\\/g,"/"),P=B.resolve("document360-capture/helpers"),v=U(B.resolve("@playwright/test/package.json"),".."),y=U(v,"index.mjs"),x=ft(y)?y:c,S={compilerOptions:{baseUrl:".",paths:{"document360-capture/helpers":[$(P)],"@playwright/test":[$(x)]}}};return ut(w,JSON.stringify(S,null,2),"utf8"),{configPath:p,tsconfigPath:w}}import{spawn as Se}from"node:child_process";import{existsSync as ht,readFileSync as Ce,unlinkSync as je}from"node:fs";import{createRequire as De}from"node:module";import{fileURLToPath as Re}from"node:url";import{delimiter as ke,dirname as E,join as F}from"node:path";function J(t){if(Array.isArray(t)){for(let e of t){let o=J(e);if(o)return o}return null}if(t&&typeof t=="object"){let e=t;if((e.type==="skip"||e.type==="fixme")&&typeof e.description=="string"&&e.description)return e.description;for(let o of Object.values(e)){let r=J(o);if(r)return r}}return null}function Ae(t,e){return t!==0?"failed":/\b\d+\s+skipped\b/.test(e)&&!/\b\d+\s+passed\b/.test(e)?"skipped":"passed"}var Ue=De(import.meta.url);async function wt(t){let{cwd:e,configPath:o,tsconfigPath:r,startUrl:n,authBoundaryUrlPattern:i,specId:s,captureDirAbs:a}=t,p=Te();if(!p)return{exitCode:127,stdout:"",stderr:"Cannot locate @playwright/test CLI. Try: npm install -g document360-capture (reinstall)",outcome:"failed"};let u=F(E(o),`pw-report-${s??"all"}-${process.pid}.json`),f=[p,"test","--config",o,"--tsconfig",r,"--reporter=line,json"];return s&&f.push(Ie(`${s}.spec.ts`)),new Promise(c=>{let d="",w="",$=Se(process.execPath,f,{cwd:e,env:{...process.env,CAPTURE_START_URL:n,CAPTURE_AUTH_BOUNDARY:i,CAPTURE_SCOPE:JSON.stringify(t.scope??{}),PLAYWRIGHT_JSON_OUTPUT_NAME:u,NODE_PATH:Oe()},shell:!1}),P=()=>{try{$.kill()}catch{}};t.signal&&(t.signal.aborted?P():t.signal.addEventListener("abort",P,{once:!0})),$.stdout.on("data",v=>{let y=v.toString();d+=y,process.stdout.write(y)}),$.stderr.on("data",v=>{let y=v.toString();w+=y,process.stderr.write(y)}),$.on("error",v=>{t.signal?.removeEventListener("abort",P),w+=`
|
|
28
|
+
[spawn error] ${v.message}`,c({exitCode:127,stdout:d,stderr:w,outcome:"failed"})}),$.on("close",v=>{t.signal?.removeEventListener("abort",P);let y=v??1,x=Ae(y,d),S;try{ht(u)&&(x==="skipped"&&(S=J(JSON.parse(Ce(u,"utf8")))??void 0),je(u))}catch{}c({exitCode:y,stdout:d,stderr:w,outcome:x,skipReason:S})})})}function Ie(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function Oe(){let t=E(Re(import.meta.url));for(let o=0;o<4&&!ht(F(t,"package.json"));o++)t=E(t);let e=[F(t,"node_modules"),E(t)];return process.env.NODE_PATH&&e.push(process.env.NODE_PATH),e.join(ke)}function Te(){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 Ne=[/login\.microsoftonline\.com/i,/login\.live\.com/i,/accounts\.google\.com/i,/\/oauth(2)?\/authorize/i,/\/sign[- ]?in/i,/\bauthn\b/i],Ee=[/waitForURL.*timed?[- ]?out/i,/page\.goto.*redirected/i,/storageState.*invalid/i];function yt(t){return Ne.some(e=>e.test(t))?!0:Ee.some(e=>e.test(t))}import{readdirSync as jt,readFileSync as Me,existsSync as Dt,copyFileSync as _e}from"node:fs";import{join as Rt}from"node:path";import q from"sharp";function H(t){return t.replace(/[<>&"']/g,e=>({"<":"<",">":">","&":"&",'"':""","'":"'"})[e])}function vt(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 $t(t){return`<rect x="${t.x}" y="${t.y}" width="${t.width}" height="${t.height}" fill="#1f2328"/>`}var W="-apple-system,Segoe UI,sans-serif",Pt=(t,e,o)=>Math.max(e,Math.min(o,t)),Le=13;function xt(t,e,o,r){let n=Le,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="${W}" font-size="13" font-weight="700" fill="#fff" text-anchor="middle">${H(t)}</text>`}function bt(t,e){return t===0?0:e.pad*2+t*e.lineHeight}function St(t,e){let{top:o,width:r,pad:n,lineHeight:i}=e,s=11,a=20+s,p=[`<line x1="0" y1="${o}" x2="${r}" y2="${o}" stroke="#d0d7de" stroke-width="1"/>`];return t.forEach((u,f)=>{let c=o+n+f*i+i/2;p.push(`<circle cx="${a}" cy="${c}" r="${s}" fill="#d1242f" stroke="#fff" stroke-width="1.5"/>`,`<text x="${a}" y="${c+4}" font-family="${W}" font-size="12" font-weight="700" fill="#fff" text-anchor="middle">${H(u.label)}</text>`,`<text x="${a+s+12}" y="${c+5}" font-family="${W}" font-size="14" fill="#1f2328">${H(u.text)}</text>`)}),p.join("")}async function Be(t){let e=JSON.parse(Me(t,"utf8"));if(!Dt(e.rawPath))return{id:e.id,ok:!1,reason:`raw missing: ${e.rawPath}`};let o=await q(e.rawPath).metadata(),r=o.width??1440,n=o.height??900,i={pad:16,lineHeight:30},s=bt(e.annotations.length,i),a=n+s,p=[];for(let c of e.highlight)p.push(vt(c.bbox));for(let c of e.redact)p.push($t(c.bbox));for(let c of e.annotations)p.push(xt(c.label,c.bbox,r,n));if(s>0&&p.push(`<rect x="0" y="${n}" width="${r}" height="${s}" fill="#ffffff"/>`,St(e.annotations.map(c=>({label:c.label,text:c.text})),{top:n,width:r,...i})),p.length===0)return await q(e.rawPath).toFile(e.finalPath),{id:e.id,ok:!0,finalPath:e.finalPath};let u=`<svg xmlns="http://www.w3.org/2000/svg" width="${r}" height="${a}">${p.join("")}</svg>`,f=q(e.rawPath);return s>0&&(f=f.extend({bottom:s,background:"#ffffff"})),await f.composite([{input:Buffer.from(u),top:0,left:0}]).toFile(e.finalPath),{id:e.id,ok:!0,finalPath:e.finalPath}}async function Ct(t){let e=jt(t).filter(r=>r.endsWith(".raw.png")),o=[];for(let r of e){let n=Rt(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 kt(t,e){if(!Dt(t))return[];if(!e)return Ct(t);let o=jt(t).filter(n=>n.endsWith(".annotations.json"));if(o.length===0)return Ct(t);let r=[];for(let n of o){let i=Rt(t,n);try{r.push(await Be(i))}catch(s){r.push({id:n,ok:!1,reason:String(s)})}}return r}function At(t){let e=/^\s*\/\/\s*CAPTURE-SAFETY:\s*(readonly|mutates)\b[\s—:–-]*(.*)$/im.exec(t);if(e)return{mutates:e[1].toLowerCase()==="mutates",declared:!0,what:e[2]?.trim()||void 0};let o=/setInputFiles/.test(t),r=/\{\s*name:\s*['"][^'"]*\b(create|import|save|apply|confirm|delete|submit|send|mint)\b[^'"]*['"][^}]*\}\s*\)\s*\.click\(/i.test(t),n=/\.fill\(/.test(t);return{mutates:o||n&&r,declared:!1}}async function Ot(t,e){let o=process.cwd(),r=h(m(o)),n=C(r,e.profile),i=n.name,s=j(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=Ut(n.captureDir)?n.captureDir:It(o,n.captureDir);if(!Fe(a))return console.error(`\u2717 Capture directory missing: ${a}`),console.error(" Generate at least one spec there before running capture."),2;await N();let{configPath:p,tsconfigPath:u}=mt({cwd:o,profileName:i,captureDir:n.captureDir,viewport:n.viewport,storageStatePath:A(r.projectId,i),startUrl:n.connection.startUrl}),f=t?[t]:qe(a);if(f.length===0)return console.error(`\u2717 No .spec.ts files in ${n.captureDir}.`),2;console.log("");let c=l=>{try{return At(He(We(a,`${l}.spec.ts`),"utf8"))}catch{return{mutates:!1}}};if(!t&&!e.allowMutations){let l=f.filter(g=>c(g).mutates);if(l.length>0){f=f.filter(g=>!c(g).mutates),console.log(`\u26A0 ${l.length} spec${l.length===1?"":"s"} perform${l.length===1?"s":""} a REAL write in your app \u2014 excluded from this bulk run:`);for(let g of l){let X=c(g);console.log(` \u26A0 ${g}${X.what?` \u2014 ${X.what}`:""} (run alone: d360-capture capture ${g})`)}if(f.length===0)return 0}}else if(t&&c(t).mutates){let l=c(t);console.log(`\u26A0 This capture performs a REAL write in your app${l.what?`: ${l.what}`:""}. Running because you named it explicitly.`)}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 d=!1,w=null,$=Ge(()=>{d?w?.abort():(d=!0,console.log(`
|
|
29
|
+
\u23F8 Stopping after the current spec\u2026 (press again to stop now)`))}),P=[],v=!1;try{for(let l of f){if(d)break;w=new AbortController,console.log(`
|
|
30
|
+
\u25B6 ${l}`);let g=await wt({cwd:o,configPath:p,tsconfigPath:u,startUrl:n.connection.startUrl,authBoundaryUrlPattern:n.connection.authBoundaryUrlPattern,specId:l,captureDirAbs:a,signal:w.signal,scope:n.scope});if(P.push({id:l,outcome:g.outcome,skipReason:g.skipReason}),g.outcome==="skipped"&&g.skipReason&&console.log(` \u2298 skipped \u2014 ${g.skipReason}`),g.exitCode!==0&&!v&&yt(`${g.stdout}
|
|
31
|
+
${g.stderr}`)&&(v=!0,console.error(""),console.error(" Output mentions a login page \u2014 if specs that used to pass now fail this way, your"),console.error(` saved session may have expired. Re-auth: d360-capture auth --profile ${i}`),console.error(" (Selector/route timeouts are spec bugs, not auth bugs \u2014 inspect the failure above.)")),w.signal.aborted)break}}finally{$(),w=null}let y=P.filter(l=>l.outcome==="passed").length,x=P.filter(l=>l.outcome==="failed"),S=P.filter(l=>l.outcome==="skipped"),I=f.length-P.length,K=!e.noAnnotate&&r.annotation.enabled,Kt=Ut(n.outputDir)?n.outputDir:It(o,n.outputDir);console.log(""),console.log(K?"\u25B6 Annotating screenshots\u2026":"\u25B6 Skipping annotation (--no-annotate or config off)");let R=(await kt(Kt,K)).filter(l=>!l.ok);console.log(""),console.log(`\u2713 ${y} captured`+(S.length?` \xB7 \u2298 ${S.length} skipped`:"")+(x.length?` \xB7 \u2717 ${x.length} failed`:"")+(I?` \xB7 ${I} not run (stopped)`:"")+` \u2192 ${n.outputDir}`);for(let l of S)console.log(` \u2298 ${l.id}${l.skipReason?` \u2014 ${l.skipReason}`:""}`);for(let l of x)console.log(` \u2717 ${l.id}`);if(R.length>0){console.log(`\u2717 ${R.length} annotation failure${R.length===1?"":"s"}:`);for(let l of R)console.log(` - ${l.id}: ${l.reason}`)}return x.length===0&&R.length===0?0:1}function qe(t){return Je(t).filter(e=>e.endsWith(".spec.ts")).map(e=>e.slice(0,-8)).sort()}function Ge(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 Tt,readdirSync as ze,statSync as Ye}from"node:fs";import{isAbsolute as Nt,join as Ke,resolve as Et}from"node:path";async function Lt(){let t=process.cwd(),e=h(m(t)),o=Nt(e.captureDir)?e.captureDir:Et(t,e.captureDir),r=Nt(e.outputDir)?e.outputDir:Et(t,e.outputDir);if(!Tt(o)){console.log(`Capture dir does not exist: ${o}`);return}let n=ze(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`),p=Tt(a)?new Date(Ye(a).mtimeMs).toISOString().slice(0,19).replace("T"," "):"(never)";console.log(` ${s.padEnd(47)} ${p}`)}console.log("")}import{existsSync as Mt,readFileSync as _t}from"node:fs";import{isAbsolute as Xe,resolve as Qe}from"node:path";import{createRequire as Ve}from"node:module";var Bt=Ve(import.meta.url);async function Ft(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=m(e);if(!Mt(i))return n(`Project config missing at ${i} \u2014 run \`d360-capture init\``),2;r(`Project config: ${i}`);let s;try{s=h(i),r(`Config parses; projectId="${s.projectId}"`)}catch(c){return n(`Config invalid: ${c.message}`),2}let a;try{a=C(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 p=Xe(a.captureDir)?a.captureDir:Qe(e,a.captureDir);Mt(p)?r(`Capture dir exists: ${a.captureDir}`):r(`Capture dir not created yet: ${a.captureDir} (created with your first spec \u2014 author specs in d360-writer via /screenshot)`);let u=j(s.projectId,a.name);if(u.exists){let c=u.ageMs!=null?Math.floor(u.ageMs/864e5):null;r(`Auth-state present (${c}d old): ${u.path}`);try{let d=JSON.parse(_t(u.path,"utf8"));d&&typeof d=="object"?r("Auth-state parses as JSON"):n("Auth-state is not a valid object")}catch(d){n(`Auth-state unreadable: ${d.message}`)}}else n(`Auth-state missing for "${a.name}" \u2014 run \`d360-capture auth --profile ${a.name}\``);try{let c=Bt.resolve("playwright/package.json"),d=JSON.parse(_t(c,"utf8"));r(`playwright@${d.version} installed`)}catch{n("playwright not resolvable \u2014 reinstall document360-capture")}return T()?r(`Chromium browser downloaded: ${_()}`):n("Chromium browser not downloaded \u2014 it auto-downloads on the next `d360-capture auth` or `capture` run"),["@playwright/test/cli","@playwright/test/cli.js"].some(c=>{try{return Bt.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 Jt(){let t=h(m());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=j(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 Ht(){let t=h(m());console.log("");for(let[e,o]of Object.entries(t.profiles)){let r=e===t.defaultProfile?"\u25CF ":" ",n=o.production?" \u26A0 PRODUCTION":"",s=j(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 Wt(t){let e=m(),o=h(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 qt(t){let e=h(m()),o=C(e,t);console.log(""),console.log(`Profile ${o.name}${o.production?" \u26A0 PRODUCTION":""}`),console.log(` startUrl: ${o.connection.startUrl}`),console.log(` authBoundary:${o.connection.authBoundaryUrlPattern}`),console.log(` captureDir: ${o.captureDir}`),console.log(` outputDir: ${o.outputDir}`),console.log(` viewport: ${o.viewport.width}x${o.viewport.height}`);let r=Object.keys(o.scope);return r.length>0&&console.log(` scope: ${r.map(n=>`${n}=${o.scope[n]}`).join(", ")}`),console.log(""),0}import{homedir as Ze}from"node:os";import{join as to,dirname as eo}from"node:path";import{mkdirSync as oo,readFileSync as ro,writeFileSync as no}from"node:fs";var Gt="document360-capture",G=to(Ze(),".document360-capture","update-cache.json"),io=10800*1e3;function so(t,e){let o=i=>{let[s="",a=""]=i.trim().replace(/^v/,"").split("-",2),p=s.split(".").map(u=>parseInt(u,10)||0);return{n:[p[0]??0,p[1]??0,p[2]??0],pre:a}},r=o(t),n=o(e);for(let i=0;i<3;i++)if(r.n[i]!==n.n[i])return r.n[i]<n.n[i]?-1:1;return r.pre===n.pre?0:r.pre?n.pre?r.pre<n.pre?-1:1:-1:1}function ao(){try{return JSON.parse(ro(G,"utf8"))}catch{return null}}function co(){let t=(process.argv[1]??"").replace(/\\/g,"/");return/\/_npx\//.test(t)||/\/packages\/capture\//.test(t)?!1:/\/node_modules\/document360-capture\//.test(t)||!/\/packages\//.test(t)}async function lo(){try{let t=new AbortController,e=setTimeout(()=>t.abort(),2500),o=await fetch(`https://registry.npmjs.org/${Gt}/latest`,{signal:t.signal,headers:{accept:"application/json"}});if(clearTimeout(e),!o.ok)return;let r=await o.json(),n=typeof r.version=="string"?r.version:null;if(!n)return;oo(eo(G),{recursive:!0}),no(G,`${JSON.stringify({checkedAt:new Date().toISOString(),latest:n})}
|
|
32
|
+
`)}catch{}}function zt(t){try{if(!co())return;let e=ao();e?.latest&&so(t,e.latest)<0&&console.error(`
|
|
33
|
+
\u2B06 d360-capture ${t} \u2192 ${e.latest} available \u2014 run: npm i -g ${Gt}@latest
|
|
34
|
+
`),e?.checkedAt&&Date.now()-new Date(e.checkedAt).getTime()<io||lo()}catch{}}var fo=uo(import.meta.url),Yt=fo("../package.json");zt(Yt.version);var b=new po;function z(t){t.env&&(console.error("\u2717 --env was replaced by --profile (connection profiles). Use: --profile <name>"),process.exit(2))}b.name("d360-capture").description("Capture product screenshots from Playwright specs. Login once, capture many.").version(Yt.version);b.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 st(t)});b.command("auth").description("Open Chromium for interactive login. Press ENTER when done to save the session (or --auto to save on login).").option("-p, --profile <name>","Profile name (default: defaultProfile from config)").option("--auto","Save automatically once login completes \u2014 no ENTER (used by d360-writer)").option("-e, --env <name>",!1).action(async t=>{z(t),await pt(t)});b.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").option("--allow-mutations","Let a BULK run execute specs that perform real writes (default: they are excluded; run them singly)").action(async(t,e)=>{z(e);let o=await Ot(t,e);o!==0&&process.exit(o)});b.command("list").description("List discovered specs and their last-capture times.").action(async()=>{await Lt()});b.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=>{z(t);let e=await Ft(t);e!==0&&process.exit(e)});b.command("status").description("Print resolved config and auth-state freshness.").action(async()=>{await Jt()});var Y=b.command("profile").description("Manage capture profiles for the current repo");Y.command("list",{isDefault:!0}).description("List profiles (\u25CF = default)").action(()=>Ht());Y.command("use <name>").description("Set the default profile for this repo").action(t=>process.exit(Wt(t)));Y.command("show [name]").description("Print the resolved profile").action(t=>process.exit(qt(t)));b.parseAsync(process.argv).catch(t=>{console.error(""),console.error(`\u2717 ${t.message}`),process.exit(1)});
|
package/package.json
CHANGED