shiplightai 0.1.61 → 0.1.63
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/cjs/debugger-manager.cjs +12 -12
- package/dist/cjs/debugger-pw.cjs +48 -48
- package/dist/cjs/fixture.cjs +81 -81
- package/dist/cjs/index.cjs +56 -56
- package/dist/cjs/reporter.cjs +2 -2
- package/dist/cli.js +100 -102
- package/dist/debugger-manager.d.ts +13 -0
- package/dist/debugger-manager.js +14 -14
- package/dist/debugger-pw.js +48 -48
- package/dist/fixture.js +80 -80
- package/dist/index.js +54 -54
- package/dist/reporter.js +2 -2
- package/dist/static/assets/index-CKKo90Kh.js +8027 -0
- package/dist/static/index.html +9 -5
- package/dist/{static/assets/index-BXd8kn7I.js → static-embedded/assets/index-BzKjxa3K.js} +2 -2
- package/dist/{static/assets/index-Bi1S98MP.js → static-embedded/assets/index-CVIbsKcG.js} +3707 -528
- package/dist/static-embedded/index.html +17 -0
- package/package.json +3 -3
- /package/dist/{static → static-embedded}/assets/index-B7_79VfZ.css +0 -0
|
@@ -157,6 +157,19 @@ declare class DebuggerManager {
|
|
|
157
157
|
* contracts/session-api.md for the full lifecycle diagram.
|
|
158
158
|
*/
|
|
159
159
|
closeSession(sessionId: string): Promise<void>;
|
|
160
|
+
/**
|
|
161
|
+
* Kill the current inner Playwright process and spawn a fresh one for the
|
|
162
|
+
* same session. The sessionId and the session-map entry are preserved, so
|
|
163
|
+
* the consumer's `/debugger/<sid>/` URL and webview base href keep working.
|
|
164
|
+
*
|
|
165
|
+
* Used by the SPA's "Reset" button (via POST /api/int-runner/terminate-session)
|
|
166
|
+
* to get a clean browser without forcing the consumer to re-create the
|
|
167
|
+
* session and reload its UI.
|
|
168
|
+
*
|
|
169
|
+
* Throws on respawn failure; the session is left marked "ended" so the
|
|
170
|
+
* consumer can react.
|
|
171
|
+
*/
|
|
172
|
+
restartInner(sessionId: string): Promise<void>;
|
|
160
173
|
/** Snapshot of all current sessions across all states. */
|
|
161
174
|
listSessions(): DebuggerSession[];
|
|
162
175
|
getSession(sessionId: string): DebuggerSession | undefined;
|
package/dist/debugger-manager.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import*as D from"http";import*as
|
|
1
|
+
import*as D from"http";import*as N from"net";import*as S from"path";import*as j from"fs";import{randomUUID as M}from"crypto";import*as y from"fs";import*as m from"path";import{spawn as A}from"child_process";import{parse as B}from"yaml";function O(l){let t=!1;return()=>{if(!t){t=!0;try{y.unlinkSync(l)}catch{}}}}function T(l){let t=["playwright.config.ts","playwright.config.js","playwright.config.mjs"],e=m.resolve(l);for(;;){for(let i of t){let n=m.join(e,i);if(y.existsSync(n))return n}let r=m.dirname(e);if(r===e)break;e=r}return null}function L(l,t,e,r){let i={...e},n=i.launchOptions?.args??[];return i.launchOptions={...i.launchOptions??{},args:[...n,"--remote-debugging-port=0"]},`// @generated by shiplightai \u2014 temporary debug test
|
|
2
2
|
import { test } from 'shiplightai/fixture';
|
|
3
3
|
${`
|
|
4
|
-
test.use(${JSON.stringify(
|
|
4
|
+
test.use(${JSON.stringify(i)});
|
|
5
5
|
`}
|
|
6
6
|
test('__shiplight_debug__', async ({ page, agent }) => {
|
|
7
7
|
// Disable timeout \u2014 debugger runs until user stops it
|
|
@@ -10,26 +10,26 @@ test('__shiplight_debug__', async ({ page, agent }) => {
|
|
|
10
10
|
const { startPlaywrightDebugServer } = await import('shiplightai/debugger-pw');
|
|
11
11
|
|
|
12
12
|
const server = await startPlaywrightDebugServer({
|
|
13
|
-
yamlFilePath: ${JSON.stringify(
|
|
14
|
-
port: ${
|
|
13
|
+
yamlFilePath: ${JSON.stringify(l)},
|
|
14
|
+
port: ${t},
|
|
15
15
|
page,
|
|
16
16
|
agent,
|
|
17
|
-
projectRoot: ${JSON.stringify(
|
|
17
|
+
projectRoot: ${JSON.stringify(r??null)},
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
console.error('[debugger] Playwright sandbox ready on internal port ${
|
|
20
|
+
console.error('[debugger] Playwright sandbox ready on internal port ${t}');
|
|
21
21
|
|
|
22
22
|
// Keep alive until the server is closed externally (Ctrl+C kills the process)
|
|
23
23
|
await new Promise(() => {});
|
|
24
24
|
});
|
|
25
|
-
`}async function
|
|
25
|
+
`}async function U(l){let{createServer:t}=await import("net");for(let e=l;e<l+20;e++)if(await new Promise(i=>{let n=t();n.once("error",()=>i(!1)),n.once("listening",()=>{n.close(()=>i(!0))}),n.listen(e,"127.0.0.1")}))return e;throw new Error(`No available port found in range ${l}-${l+19}`)}async function E(l){let{yamlFilePath:t,configPath:e,tempSuffix:r="",headed:i}=l,n=m.dirname(e),d=await U(16174),s;if(!y.existsSync(t))throw new Error(`Please select a test file before starting the debug session. File not found: ${t}`);try{let a=B(y.readFileSync(t,"utf-8"));a?.use&&typeof a.use=="object"&&!Array.isArray(a.use)&&(s=a.use),a?.base_url&&!s?.baseURL&&(s={...s,baseURL:a.base_url}),a?.settings?.auto_dismiss_modal!==void 0&&(s={...s,autoDismissModal:!!a.settings.auto_dismiss_modal}),a?.settings?.browser_timezone!=null&&(s={...s,timezoneId:String(a.settings.browser_timezone)}),a?.settings?.browser_language!=null&&(s={...s,locale:String(a.settings.browser_language)}),a?.settings?.extra_http_headers!=null&&typeof a.settings.extra_http_headers=="object"&&(s={...s,extraHTTPHeaders:a.settings.extra_http_headers})}catch(a){console.error("[debugger] Could not parse YAML for `use` block:",a)}let u=m.dirname(m.resolve(t)),o=r?`-${r}`:"",g=m.join(u,`.__shiplight_debug__${o}.yaml.spec.ts`),h=L(t,d,s,n);y.writeFileSync(g,h);let p=O(g),P=["playwright","test",g,...i?["--headed"]:[]],c=A("npx",P,{stdio:["ignore","pipe","pipe"],shell:!0,cwd:n,env:{...process.env,PWDEBUG:"console",SHIPLIGHT_REGISTRY_URL:""}});c.stdout?.on("data",a=>{process.stderr.write(a)}),c.stderr?.on("data",a=>{process.stderr.write(a)});let f=()=>{c.killed||c.kill("SIGTERM")};process.on("SIGTERM",f),process.on("SIGINT",f),process.on("exit",f),c.on("close",a=>{process.removeListener("SIGTERM",f),process.removeListener("SIGINT",f),process.removeListener("exit",f),p(),a!==0&&a!==null&&console.error(`[debugger] Playwright process exited with code ${a}`)}),console.error("[debugger] Waiting for Playwright sandbox to start...");let x=["127.0.0.1","::1"];async function v(a){try{let b=a.includes(":")?`[${a}]`:a,_=await fetch(`http://${b}:${d}/api/test-flow`);if(_.ok){try{await _.text()}catch{}return!0}}catch{}return!1}let w=null;for(let a=0;a<180;a++){if(c.exitCode!==null)throw p(),new Error(`Playwright process exited with code ${c.exitCode} before sandbox was ready`);for(let b of x)if(await v(b)){w=b;break}if(w){console.error(`[debugger] Playwright sandbox ready on ${w}:${d}`);break}if(a===179)throw f(),p(),new Error("Timed out waiting for Playwright sandbox to start (180s)");await new Promise(b=>setTimeout(b,1e3))}if(!w)throw f(),p(),new Error("Sandbox poll finished without a reachable host");return{port:d,host:w,pid:c.pid??0,cleanup:async()=>{f(),p()}}}var R=1e4,I=class{sessions=new Map;byYamlPath=new Map;options;spawner;headed;constructor(t={}){this.options=t,this.spawner=t.spawner??E,this.headed=t.headed??!1}log(t){this.options.onLog?this.options.onLog(t):console.error(t)}notifyStateChange(t){try{this.options.onSessionStateChange?.({...t})}catch(e){this.log(`[manager] onSessionStateChange listener threw: ${e.message}`)}}async openSession(t){let e=S.resolve(t),r=this.byYamlPath.get(e);if(r){let o=this.sessions.get(r);if(o&&o.session.status!=="ended")return{...o.session};this.byYamlPath.delete(e)}if(!j.existsSync(e))throw new Error(`YAML file not found: ${e}`);let i=T(S.dirname(e));if(!i)throw new Error(`No Playwright config found for ${e} (searched parents for playwright.config.{ts,js,mjs}).`);let n=`dbg-${M().slice(0,8)}`,d=new Date().toISOString(),s={sessionId:n,yamlPath:e,innerPort:0,innerHost:"127.0.0.1",pid:0,startedAt:d,status:"starting",exitInfo:null},u={session:s,cleanup:async()=>{},readyPromise:Promise.resolve()};this.sessions.set(n,u),this.byYamlPath.set(e,n),this.notifyStateChange(s);try{let o=R+18e4,g,h=new Promise((c,f)=>{g=setTimeout(()=>f(new Error(`Timed out after ${o/1e3}s waiting for inner playwright to register on a port.`)),o)}),p;try{p=await Promise.race([this.spawner({yamlFilePath:e,configPath:i,tempSuffix:n,headed:this.headed}),h])}finally{g&&clearTimeout(g)}s.innerPort=p.port,s.innerHost=p.host,s.pid=p.pid,s.status="running",u.cleanup=p.cleanup;let P=c=>{s.status!=="ended"&&(s.status="ended",s.exitInfo=c,this.notifyStateChange(s))};return u.livenessTimer=this.startLivenessProbe(n,P),this.notifyStateChange(s),this.log(`[manager] session ${n} running on ${s.innerHost}:${s.innerPort} (yaml=${S.basename(e)})`),{...s}}catch(o){throw this.sessions.delete(n),this.byYamlPath.get(e)===n&&this.byYamlPath.delete(e),s.status="ended",s.exitInfo=o.message,this.notifyStateChange(s),o}}startLivenessProbe(t,e){let n=0,d=setInterval(()=>{let s=this.sessions.get(t);if(!s||s.session.status==="ended"){clearInterval(d);return}let u=!1;try{process.kill(s.session.pid,0),u=!0}catch(o){o?.code!=="ESRCH"&&(u=!0)}u?n=0:(n+=1,n>=3&&(clearInterval(d),e("process-exited")))},3e3);return d.unref(),d}async closeSession(t){let e=this.sessions.get(t);if(e){this.sessions.delete(t),this.byYamlPath.get(e.session.yamlPath)===t&&this.byYamlPath.delete(e.session.yamlPath),e.livenessTimer&&(clearInterval(e.livenessTimer),e.livenessTimer=void 0),e.session.status!=="ended"&&(e.session.status="ended",e.session.exitInfo="SIGTERM",this.notifyStateChange(e.session));try{await e.cleanup()}catch(r){this.log(`[manager] closeSession ${t} cleanup error: ${r.message}`)}}}async restartInner(t){let e=this.sessions.get(t);if(!e)throw new Error(`No session ${t} to restart`);if(e.restartInProgress)throw new Error(`Restart already in progress for session ${t}`);let r=e.session.yamlPath,i=T(S.dirname(r));if(!i)throw new Error(`No Playwright config found for ${r} (searched parents for playwright.config.{ts,js,mjs}).`);e.restartInProgress=!0;try{e.livenessTimer&&(clearInterval(e.livenessTimer),e.livenessTimer=void 0);try{await e.cleanup()}catch(o){this.log(`[manager] restartInner ${t} old cleanup error: ${o.message}`)}e.session.status="starting",e.session.innerPort=0,e.session.pid=0,this.notifyStateChange(e.session);let n=R+18e4,d,s=new Promise((o,g)=>{d=setTimeout(()=>g(new Error(`Timed out after ${n/1e3}s waiting for inner playwright to respawn.`)),n)}),u;try{u=await Promise.race([this.spawner({yamlFilePath:r,configPath:i,tempSuffix:t,headed:this.headed}),s])}catch(o){throw e.session.status="ended",e.session.exitInfo=o.message,this.notifyStateChange(e.session),o}finally{d&&clearTimeout(d)}e.session.innerPort=u.port,e.session.innerHost=u.host,e.session.pid=u.pid,e.session.status="running",e.cleanup=u.cleanup,e.livenessTimer=this.startLivenessProbe(t,o=>{e.session.status!=="ended"&&(e.session.status="ended",e.session.exitInfo=o,this.notifyStateChange(e.session))}),this.notifyStateChange(e.session),this.log(`[manager] session ${t} restarted on ${e.session.innerHost}:${e.session.innerPort} (yaml=${S.basename(r)})`)}finally{e.restartInProgress=!1}}listSessions(){return Array.from(this.sessions.values()).map(t=>({...t.session}))}getSession(t){let e=this.sessions.get(t);return e?{...e.session}:void 0}httpProxyFor(t,e={}){let{liveviewUrlBuilder:r}=e;return async(i,n,d)=>{let s=this.sessions.get(t);if(!s){n.status(404).json({status:"error",message:"Session not found"});return}if(s.session.status==="ended"){n.status(410).json({status:"error",message:"Session has ended",exitInfo:s.session.exitInfo});return}let u=`${i.method} ${i.path}`;if(u==="POST /api/int-runner/create-session"){let g=await H(i),h={};if(g.length)try{h=JSON.parse(g.toString("utf-8"))}catch{n.status(400).json({status:"error",message:"Invalid JSON body"});return}h.testFilePath=s.session.yamlPath,await C(i,n,s.session.innerHost,s.session.innerPort,Buffer.from(JSON.stringify(h),"utf-8"),"application/json");return}if(u==="POST /api/int-runner/terminate-session"){try{await this.restartInner(t),n.json({status:"success",details:"Session restarted"})}catch(g){n.status(500).json({status:"error",message:g.message})}return}if(u==="POST /api/int-runner/liveview-url"){let g=r?.(i)??"";n.json({liveviewUrl:g,browserWsUrl:""});return}let o=await H(i);await C(i,n,s.session.innerHost,s.session.innerPort,o,i.headers["content-type"])}}wsUpgradeFor(t){return async(e,r,i,n)=>{let d=this.sessions.get(t);if(!d){$(r,`HTTP/1.1 404 Not Found\r
|
|
26
26
|
\r
|
|
27
|
-
`);return}if(d.session.status==="ended"){
|
|
27
|
+
`);return}if(d.session.status==="ended"){$(r,`HTTP/1.1 410 Gone\r
|
|
28
28
|
\r
|
|
29
|
-
`);return}try{let s=d.session.innerHost.includes(":")?`[${d.session.innerHost}]`:d.session.innerHost,
|
|
30
|
-
Host: ${
|
|
31
|
-
`;for(let[x,v]of Object.entries(
|
|
32
|
-
`)}
|
|
33
|
-
`,
|
|
29
|
+
`);return}try{let s=d.session.innerHost.includes(":")?`[${d.session.innerHost}]`:d.session.innerHost,u=await fetch(`http://${s}:${d.session.innerPort}/api/browser-cdp`);if(!u.ok)throw new Error(`Inner /api/browser-cdp returned ${u.status}`);let{cdpUrl:o}=await u.json(),g=new URL(o.replace(/^ws/,"http")),h=parseInt(g.port||"80",10),p=g.hostname,P=g.pathname;n&&n.startsWith("/page/")&&(P=`/devtools${n}`);let c=N.createConnection(h,p);c.on("connect",()=>{let f=`GET ${P} HTTP/1.1\r
|
|
30
|
+
Host: ${p}:${h}\r
|
|
31
|
+
`;for(let[x,v]of Object.entries(e.headers)){let w=x.toLowerCase();w!=="host"&&w!=="origin"&&(f+=`${x}: ${Array.isArray(v)?v.join(", "):v}\r
|
|
32
|
+
`)}f+=`\r
|
|
33
|
+
`,c.write(f),i.length&&c.write(i),c.pipe(r),r.pipe(c)}),c.on("error",()=>{(!("destroyed"in r)||!r.destroyed)&&r.destroy()}),r.on("error",()=>{c.destroyed||c.destroy()}),r.on("close",()=>{c.destroyed||c.destroy()})}catch(s){this.log(`[manager] WS upgrade for ${t} failed: ${s.message}`),$(r,`HTTP/1.1 502 Bad Gateway\r
|
|
34
34
|
\r
|
|
35
|
-
`)}}}async shutdown(){let
|
|
35
|
+
`)}}}async shutdown(){let t=Array.from(this.sessions.keys());await Promise.allSettled(t.map(e=>this.closeSession(e)))}};function H(l){return new Promise((t,e)=>{if(l.body&&typeof l.body=="object")try{t(Buffer.from(JSON.stringify(l.body),"utf-8"));return}catch{}if(l.readableEnded){t(Buffer.alloc(0));return}let r=[];l.on("data",i=>r.push(i)),l.on("end",()=>t(Buffer.concat(r))),l.on("error",e)})}function C(l,t,e,r,i,n){return new Promise(d=>{let s=Array.isArray(n)?n[0]:n,u=D.request({hostname:e,port:r,path:l.originalUrl,method:l.method,headers:{"content-type":s||"application/json","content-length":String(i.length)},timeout:3e5},o=>{t.writeHead(o.statusCode??502,o.headers),o.on("data",g=>t.write(g)),o.on("end",()=>{t.end(),d()}),o.on("error",()=>{t.writableEnded||t.end(),d()})});u.on("error",o=>{t.headersSent?t.writableEnded||t.end():t.status(502).json({status:"error",message:"Inner server unavailable: "+o.message}),d()}),u.end(i)})}function $(l,t){try{(!("destroyed"in l)||!l.destroyed)&&(l.write(t),l.destroy())}catch{}}export{I as DebuggerManager};
|