document360-capture 0.2.20 → 0.2.22
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 +11 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 ut(t){let e=h(m()),o=C(e,t.profile);pt(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
|
|
2
|
+
import{Command as go}from"commander";import{createRequire as mo}from"node:module";import{input as le,confirm as it}from"@inquirer/prompts";import{existsSync as st,readFileSync as pe}from"node:fs";import{basename as ue,join as fe}from"node:path";import{readFileSync as Z,writeFileSync as Zt,existsSync as Q,mkdirSync as Qt}from"node:fs";import{basename as te,dirname as ee,join as L}from"node:path";var oe=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,oe)}function h(t){if(!Q(t))throw new Error(`No project config at ${t}. Run \`d360-capture init\` to create one.`);let e=JSON.parse(Z(t,"utf8").replace(/^/,""));return ne(e,t)}function I(t,e){Qt(ee(t),{recursive:!0}),Zt(t,JSON.stringify(e,null,2)+`
|
|
3
|
+
`,"utf8")}function re(t){try{return"^"+new URL(t).origin.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}catch{return"."}}function ne(t,e,o=process.cwd()){ie(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||se(o)||te(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||re(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 ie(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 se(t){let e=L(t,"package.json");if(!Q(e))return null;try{return JSON.parse(Z(e,"utf8")).name??null}catch{return null}}import{existsSync as tt,readFileSync as ae}from"node:fs";import{join as et}from"node:path";var ce=et(".d360-writer","config.json");function ot(t=process.cwd()){return et(t,ce)}function rt(t=process.cwd()){return tt(ot(t))}function nt(t=process.cwd()){let e=ot(t);if(!tt(e))return{};try{let o=JSON.parse(ae(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 at(t={}){let e=process.cwd(),o=m(e),r=t.yes===!0;if(st(o)&&!r&&!await it({message:`${o} already exists. Overwrite?`,default:!1})){console.log("Init cancelled.");return}let n=nt(e),i=n.projectId||de(e)||ue(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(" + ")}.`)):rt(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 le({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 it({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};I(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 de(t){let e=fe(t,"package.json");if(!st(e))return null;try{return JSON.parse(pe(e,"utf8")).name??null}catch{return null}}import{chromium as xe}from"playwright";import{existsSync as ct,mkdirSync as ge,statSync as me}from"node:fs";import{homedir as he}from"node:os";import{join as M,dirname as Io}from"node:path";function lt(){return M(he(),".document360-capture","auth-states")}function A(t,e){return M(lt(),t,`${e}.json`)}function pt(t){let e=M(lt(),t);ct(e)||ge(e,{recursive:!0})}function j(t,e){let o=A(t,e);if(!ct(o))return{path:o,exists:!1};let r=me(o);return{path:o,exists:!0,ageMs:Date.now()-r.mtimeMs}}import{existsSync as we}from"node:fs";import{spawn as ye}from"node:child_process";import{createRequire as Pe}from"node:module";import{chromium as $e}from"playwright";var ve=Pe(import.meta.url);function F(){try{return $e.executablePath()}catch{return null}}function T(){let t=F();return t!=null&&we(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=ve.resolve("playwright/cli.js"),e=await new Promise((o,r)=>{let n=ye(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 ut(t){let e=h(m()),o=C(e,t.profile);pt(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 xe.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 be();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 be(){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 We,readdirSync as qe,readFileSync as Ge}from"node:fs";import{isAbsolute as Ot,join as ze,resolve as It}from"node:path";import{writeFileSync as ft,mkdirSync as Se,existsSync as dt}from"node:fs";import{createRequire as Ce}from"node:module";import{join as U,isAbsolute as gt,resolve as mt}from"node:path";var _=Ce(import.meta.url);function ht(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");dt(a)||Se(a,{recursive:!0});let p=U(a,`playwright.${o}.config.cjs`),u=gt(r)?r:mt(e,r),f=gt(i)?i:mt(e,i),c=_.resolve("@playwright/test"),d=`// Generated by document360-capture \u2014 do not edit by hand.
|
|
5
5
|
const { defineConfig } = require(${JSON.stringify(c)});
|
|
6
6
|
module.exports = defineConfig({
|
|
7
7
|
testDir: ${JSON.stringify(u)},
|
|
@@ -24,11 +24,11 @@ module.exports = defineConfig({
|
|
|
24
24
|
contextOptions: { reducedMotion: 'reduce' },
|
|
25
25
|
},
|
|
26
26
|
});
|
|
27
|
-
`;ft(p,d,"utf8");let w=U(a,`tsconfig.${o}.json`)
|
|
28
|
-
[spawn error] ${
|
|
29
|
-
\u23F8 Stopping after the current spec\u2026 (press again to stop now)`))}),y=[]
|
|
30
|
-
\u25B6 ${l}`);let g=await yt({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(y.push({id:l,outcome:g.outcome,skipReason:g.skipReason}),g.outcome==="skipped"&&g.skipReason&&console.log(` \u2298 skipped \u2014 ${g.skipReason}`),g.exitCode!==0
|
|
31
|
-
${g.stderr}`)&&(
|
|
32
|
-
`)}catch{}}function
|
|
33
|
-
\u2B06 d360-capture ${t} \u2192 ${e.latest} available \u2014 run: npm i -g ${
|
|
34
|
-
`),e?.checkedAt&&Date.now()-new Date(e.checkedAt).getTime()<
|
|
27
|
+
`;ft(p,d,"utf8");let w=U(a,`tsconfig.${o}.json`),v=O=>O.replace(/\\/g,"/"),y=_.resolve("document360-capture/helpers"),$=U(_.resolve("@playwright/test/package.json"),".."),P=U($,"index.mjs"),x=dt(P)?P:c,S={compilerOptions:{baseUrl:".",paths:{"document360-capture/helpers":[v(y)],"@playwright/test":[v(x)]}}};return ft(w,JSON.stringify(S,null,2),"utf8"),{configPath:p,tsconfigPath:w}}import{spawn as je}from"node:child_process";import{existsSync as wt,readFileSync as De,unlinkSync as Re}from"node:fs";import{createRequire as ke}from"node:module";import{fileURLToPath as Ae}from"node:url";import{delimiter as Ue,dirname as E,join as B}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 Oe(t,e){return t!==0?"failed":/\b\d+\s+skipped\b/.test(e)&&!/\b\d+\s+passed\b/.test(e)?"skipped":"passed"}var Ie=ke(import.meta.url);async function yt(t){let{cwd:e,configPath:o,tsconfigPath:r,startUrl:n,authBoundaryUrlPattern:i,specId:s,captureDirAbs:a}=t,p=Ee();if(!p)return{exitCode:127,stdout:"",stderr:"Cannot locate @playwright/test CLI. Try: npm install -g document360-capture (reinstall)",outcome:"failed"};let u=B(E(o),`pw-report-${s??"all"}-${process.pid}.json`),f=[p,"test","--config",o,"--tsconfig",r,"--reporter=line,json"];return s&&f.push(Te(`${s}.spec.ts`)),new Promise(c=>{let d="",w="",v=je(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:Ne()},shell:!1}),y=()=>{try{v.kill()}catch{}};t.signal&&(t.signal.aborted?y():t.signal.addEventListener("abort",y,{once:!0})),v.stdout.on("data",$=>{let P=$.toString();d+=P,process.stdout.write(P)}),v.stderr.on("data",$=>{let P=$.toString();w+=P,process.stderr.write(P)}),v.on("error",$=>{t.signal?.removeEventListener("abort",y),w+=`
|
|
28
|
+
[spawn error] ${$.message}`,c({exitCode:127,stdout:d,stderr:w,outcome:"failed"})}),v.on("close",$=>{t.signal?.removeEventListener("abort",y);let P=$??1,x=Oe(P,d),S;try{wt(u)&&(x==="skipped"&&(S=J(JSON.parse(De(u,"utf8")))??void 0),Re(u))}catch{}c({exitCode:P,stdout:d,stderr:w,outcome:x,skipReason:S})})})}function Te(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function Ne(){let t=E(Ae(import.meta.url));for(let o=0;o<4&&!wt(B(t,"package.json"));o++)t=E(t);let e=[B(t,"node_modules"),E(t)];return process.env.NODE_PATH&&e.push(process.env.NODE_PATH),e.join(Ue)}function Ee(){let t=["@playwright/test/cli","@playwright/test/cli.js","playwright/cli","playwright/cli.js"];for(let e of t)try{return Ie.resolve(e)}catch{}return null}var Le=[/login\.microsoftonline\.com/i,/login\.live\.com/i,/accounts\.google\.com/i,/\/oauth(2)?\/authorize/i,/\/sign[- ]?in/i,/\bauthn\b/i],Me=[/waitForURL.*timed?[- ]?out/i,/page\.goto.*redirected/i,/storageState.*invalid/i];function Pt(t){return Le.some(e=>e.test(t))?!0:Me.some(e=>e.test(t))}import{readdirSync as Dt,readFileSync as _e,existsSync as Rt,copyFileSync as Be}from"node:fs";import{join as kt}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 xt(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",$t=(t,e,o)=>Math.max(e,Math.min(o,t)),Fe=13;function bt(t,e,o,r){let n=Fe,i=$t(e.x,n,Math.max(n,o-n)),s=$t(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 St(t,e){return t===0?0:e.pad*2+t*e.lineHeight}function Ct(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 Je(t){let e=JSON.parse(_e(t,"utf8"));if(!Rt(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=St(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(xt(c.bbox));for(let c of e.annotations)p.push(bt(c.label,c.bbox,r,n));if(s>0&&p.push(`<rect x="0" y="${n}" width="${r}" height="${s}" fill="#ffffff"/>`,Ct(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 jt(t,e){let o=Dt(t).filter(n=>n.endsWith(".raw.png")).filter(n=>!e||e.has(n.slice(0,-8))),r=[];for(let n of o){let i=kt(t,n),s=i.replace(/\.raw\.png$/,".png");try{Be(i,s),r.push({id:n.replace(/\.raw\.png$/,""),ok:!0,finalPath:s})}catch(a){r.push({id:n,ok:!1,reason:String(a)})}}return r}async function At(t,e,o){if(!Rt(t))return[];if(o?.size===0)return[];if(!e)return jt(t,o);let r=Dt(t).filter(i=>i.endsWith(".annotations.json")).filter(i=>!o||o.has(i.slice(0,-17)));if(r.length===0)return jt(t,o);let n=[];for(let i of r){let s=kt(t,i);try{n.push(await Je(s))}catch(a){n.push({id:i,ok:!1,reason:String(a)})}}return n}function He(t,e,o){let r=[o.trim()];for(let n=e+1;n<t.length;n++){let i=/^\s*\/\/\s?(.*)$/.exec(t[n]);if(!i)break;let s=i[1].trim();if(!s||/^(CAPTURE-[A-Z]+:|TODO\b|NOTE\b|Grounding\b|\d+\.\s)/.test(s)||(r.push(s),r.join(" ").length>400))break}return r.join(" ")}function Ut(t){let e=t.split(/\r?\n/),o=e.findIndex(a=>/^\s*\/\/\s*CAPTURE-SAFETY:\s*(readonly|mutates)\b/i.test(a)),r=o>=0?/^\s*\/\/\s*CAPTURE-SAFETY:\s*(readonly|mutates)\b[\s—:–-]*(.*)$/i.exec(e[o]):null;if(r){let a=He(e,o,r[2]??"");return{mutates:r[1].toLowerCase()==="mutates",declared:!0,what:a||void 0}}let n=/setInputFiles/.test(t),i=/\{\s*name:\s*['"][^'"]*\b(create|import|save|apply|confirm|delete|submit|send|mint)\b[^'"]*['"][^}]*\}\s*\)\s*\.click\(/i.test(t),s=/\.fill\(/.test(t);return{mutates:n||s&&i,declared:!1}}var Tt=t=>t.trim().replace(/[.\s]+$/,"");async function Nt(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=Ot(n.captureDir)?n.captureDir:It(o,n.captureDir);if(!We(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}=ht({cwd:o,profileName:i,captureDir:n.captureDir,viewport:n.viewport,storageStatePath:A(r.projectId,i),startUrl:n.connection.startUrl}),f=t?[t]:Ye(a);if(f.length===0)return console.error(`\u2717 No .spec.ts files in ${n.captureDir}.`),2;console.log("");let c=l=>{try{return Ut(Ge(ze(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 V=c(g);console.log(` \u26A0 ${g}${V.what?` \u2014 ${Tt(V.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?`: ${Tt(l.what)}`:""}. Running because you named it explicitly.`)}if(n.production){let l=!!t&&c(t).mutates;console.log(l?`\u26A0 Profile "${i}" is PRODUCTION \u2014 the write above lands in the LIVE tenant (${n.connection.startUrl}). Verify the spec's cleanup ran.`:`\u26A0 Profile "${i}" is PRODUCTION. Nothing in this run writes: bulk runs exclude specs that do, and only a spec you name explicitly can.`)}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,v=Ke(()=>{d?w?.abort():(d=!0,console.log(`
|
|
29
|
+
\u23F8 Stopping after the current spec\u2026 (press again to stop now)`))}),y=[],$=!1;try{for(let l of f){if(d)break;w=new AbortController,console.log(`
|
|
30
|
+
\u25B6 ${l}`);let g=await yt({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(y.push({id:l,outcome:g.outcome,skipReason:g.skipReason}),g.outcome==="skipped"&&g.skipReason&&console.log(` \u2298 skipped \u2014 ${g.skipReason}`),g.exitCode!==0&&!$&&Pt(`${g.stdout}
|
|
31
|
+
${g.stderr}`)&&($=!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{v(),w=null}let P=y.filter(l=>l.outcome==="passed").length,x=y.filter(l=>l.outcome==="failed"),S=y.filter(l=>l.outcome==="skipped"),O=f.length-y.length,K=!e.noAnnotate&&r.annotation.enabled,Vt=Ot(n.outputDir)?n.outputDir:It(o,n.outputDir),X=new Set(y.filter(l=>l.outcome==="passed").map(l=>l.id));X.size>0&&(console.log(""),console.log(K?"\u25B6 Annotating screenshots\u2026":"\u25B6 Skipping annotation (--no-annotate or config off)"));let R=(await At(Vt,K,X)).filter(l=>!l.ok);console.log(""),console.log(`\u2713 ${P} captured`+(S.length?` \xB7 \u2298 ${S.length} skipped`:"")+(x.length?` \xB7 \u2717 ${x.length} failed`:"")+(O?` \xB7 ${O} 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 Ye(t){return qe(t).filter(e=>e.endsWith(".spec.ts")).map(e=>e.slice(0,-8)).sort()}function Ke(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 Et,readdirSync as Xe,statSync as Ve}from"node:fs";import{isAbsolute as Lt,join as Ze,resolve as Mt}from"node:path";async function Ft(){let t=process.cwd(),e=h(m(t)),o=Lt(e.captureDir)?e.captureDir:Mt(t,e.captureDir),r=Lt(e.outputDir)?e.outputDir:Mt(t,e.outputDir);if(!Et(o)){console.log(`Capture dir does not exist: ${o}`);return}let n=Xe(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=Ze(r,`${s}.png`),p=Et(a)?new Date(Ve(a).mtimeMs).toISOString().slice(0,19).replace("T"," "):"(never)";console.log(` ${s.padEnd(47)} ${p}`)}console.log("")}import{existsSync as _t,readFileSync as Bt}from"node:fs";import{isAbsolute as Qe,resolve as to}from"node:path";import{createRequire as eo}from"node:module";var Jt=eo(import.meta.url);async function Ht(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(!_t(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=Qe(a.captureDir)?a.captureDir:to(e,a.captureDir);_t(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(Bt(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=Jt.resolve("playwright/package.json"),d=JSON.parse(Bt(c,"utf8"));r(`playwright@${d.version} installed`)}catch{n("playwright not resolvable \u2014 reinstall document360-capture")}return T()?r(`Chromium browser downloaded: ${F()}`):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 Jt.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 Wt(){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 qt(){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 Gt(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,I(e,o);let r=o.profiles[t].production?" \u26A0 PRODUCTION":"";return console.log(`\u2713 Default profile is now "${t}"${r}`),0}function zt(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 oo}from"node:os";import{join as ro,dirname as no}from"node:path";import{mkdirSync as io,readFileSync as so,writeFileSync as ao}from"node:fs";var Yt="document360-capture",G=ro(oo(),".document360-capture","update-cache.json"),co=10800*1e3;function lo(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 po(){try{return JSON.parse(so(G,"utf8"))}catch{return null}}function uo(){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 fo(){try{let t=new AbortController,e=setTimeout(()=>t.abort(),2500),o=await fetch(`https://registry.npmjs.org/${Yt}/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;io(no(G),{recursive:!0}),ao(G,`${JSON.stringify({checkedAt:new Date().toISOString(),latest:n})}
|
|
32
|
+
`)}catch{}}function Kt(t){try{if(!uo())return;let e=po();e?.latest&&lo(t,e.latest)<0&&console.error(`
|
|
33
|
+
\u2B06 d360-capture ${t} \u2192 ${e.latest} available \u2014 run: npm i -g ${Yt}@latest
|
|
34
|
+
`),e?.checkedAt&&Date.now()-new Date(e.checkedAt).getTime()<co||fo()}catch{}}var ho=mo(import.meta.url),Xt=ho("../package.json");Kt(Xt.version);var b=new go;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(Xt.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 at(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 ut(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 Nt(t,e);o!==0&&process.exit(o)});b.command("list").description("List discovered specs and their last-capture times.").action(async()=>{await Ft()});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 Ht(t);e!==0&&process.exit(e)});b.command("status").description("Print resolved config and auth-state freshness.").action(async()=>{await Wt()});var Y=b.command("profile").description("Manage capture profiles for the current repo");Y.command("list",{isDefault:!0}).description("List profiles (\u25CF = default)").action(()=>qt());Y.command("use <name>").description("Set the default profile for this repo").action(t=>process.exit(Gt(t)));Y.command("show [name]").description("Print the resolved profile").action(t=>process.exit(zt(t)));b.parseAsync(process.argv).catch(t=>{console.error(""),console.error(`\u2717 ${t.message}`),process.exit(1)});
|
package/package.json
CHANGED