shiplightai 0.1.63 → 0.1.64
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 +56 -56
- package/dist/cjs/debugger-server.cjs +2 -2
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/reporter.cjs +1 -1
- package/dist/cli.js +69 -69
- package/dist/debugger-manager.d.ts +18 -8
- package/dist/debugger-manager.js +12 -12
- package/dist/debugger-pw.d.ts +20 -2
- package/dist/debugger-pw.js +55 -55
- package/dist/debugger-server.d.ts +13 -0
- package/dist/debugger-server.js +2 -2
- package/dist/index.js +1 -1
- package/dist/reporter.js +1 -1
- package/dist/static/assets/{index-CKKo90Kh.js → index-BQYw0-8-.js} +88 -135
- package/dist/static/index.html +1 -1
- package/dist/static-embedded/assets/{index-CVIbsKcG.js → index-BRGTQwWu.js} +1502 -3481
- package/dist/static-embedded/assets/{index-BzKjxa3K.js → index-BuU8-IZQ.js} +2 -2
- package/dist/static-embedded/index.html +1 -1
- package/package.json +4 -4
|
@@ -19,7 +19,7 @@ import { Duplex } from 'stream';
|
|
|
19
19
|
* Design notes: see specs/004-testbox-debugger/research.md §1.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
type SessionStatus = "starting" | "running" | "ended";
|
|
22
|
+
type SessionStatus = "idle" | "starting" | "running" | "ended";
|
|
23
23
|
interface DebuggerSession {
|
|
24
24
|
/** URL-safe stable id for the lifetime of this session. */
|
|
25
25
|
sessionId: string;
|
|
@@ -124,15 +124,25 @@ declare class DebuggerManager {
|
|
|
124
124
|
private notifyStateChange;
|
|
125
125
|
/**
|
|
126
126
|
* Open a new debugger session for the given yaml path. If a session for
|
|
127
|
-
* the same absolute path already exists in
|
|
128
|
-
*
|
|
129
|
-
* allocates a free port, spawns the inner playwright process, waits up to
|
|
130
|
-
* 10s for its registration, and returns the ready session.
|
|
127
|
+
* the same absolute path already exists in a live status, returns the
|
|
128
|
+
* existing session (idempotent — satisfies FR-003).
|
|
131
129
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
130
|
+
* The session starts in `"idle"` status — no Playwright process is
|
|
131
|
+
* spawned. Call `startSandbox(sessionId)` to launch the inner process
|
|
132
|
+
* on demand (e.g. when the user clicks "Start" in the debugger bar).
|
|
134
133
|
*/
|
|
135
|
-
openSession(yamlPath: string):
|
|
134
|
+
openSession(yamlPath: string): DebuggerSession;
|
|
135
|
+
/**
|
|
136
|
+
* Spawn the inner Playwright process for an existing idle session.
|
|
137
|
+
* Idempotent — returns immediately if the sandbox is already running or
|
|
138
|
+
* starting. Throws on spawn failure; the session is left marked "ended".
|
|
139
|
+
*/
|
|
140
|
+
startSandbox(sessionId: string): Promise<void>;
|
|
141
|
+
/**
|
|
142
|
+
* Kill the sandbox (inner Playwright process) but keep the session in
|
|
143
|
+
* the map so the user can re-start. Status transitions to `"idle"`.
|
|
144
|
+
*/
|
|
145
|
+
stopSandbox(sessionId: string): Promise<void>;
|
|
136
146
|
/**
|
|
137
147
|
* Poll for inner-process liveness via its pid. When the pid is gone, flip
|
|
138
148
|
* the session to "ended" so subsequent requests return 410 and the UI can
|
package/dist/debugger-manager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as N from"http";import*as j from"net";import*as y from"path";import*as D from"fs";import{randomUUID as M}from"crypto";import*as w from"fs";import*as m from"path";import{spawn as A}from"child_process";import{parse as B}from"yaml";function O(d){let s=!1;return()=>{if(!s){s=!0;try{w.unlinkSync(d)}catch{}}}}function $(d){let s=["playwright.config.ts","playwright.config.js","playwright.config.mjs"],e=m.resolve(d);for(;;){for(let i of s){let t=m.join(e,i);if(w.existsSync(t))return t}let n=m.dirname(e);if(n===e)break;e=n}return null}function L(d,s,e,n){let i={...e},t=i.launchOptions?.args??[];return i.launchOptions={...i.launchOptions??{},args:[...t,"--remote-debugging-port=0"]},`// @generated by shiplightai \u2014 temporary debug test
|
|
2
2
|
import { test } from 'shiplightai/fixture';
|
|
3
3
|
${`
|
|
4
4
|
test.use(${JSON.stringify(i)});
|
|
@@ -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(d)},
|
|
14
|
+
port: ${s},
|
|
15
15
|
page,
|
|
16
16
|
agent,
|
|
17
|
-
projectRoot: ${JSON.stringify(
|
|
17
|
+
projectRoot: ${JSON.stringify(n??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 ${s}');
|
|
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 U(
|
|
25
|
+
`}async function U(d){let{createServer:s}=await import("net");for(let e=d;e<d+20;e++)if(await new Promise(i=>{let t=s();t.once("error",()=>i(!1)),t.once("listening",()=>{t.close(()=>i(!0))}),t.listen(e,"127.0.0.1")}))return e;throw new Error(`No available port found in range ${d}-${d+19}`)}async function I(d){let{yamlFilePath:s,configPath:e,tempSuffix:n="",headed:i}=d,t=m.dirname(e),o=await U(16174),r;if(!w.existsSync(s))throw new Error(`Please select a test file before starting the debug session. File not found: ${s}`);try{let l=B(w.readFileSync(s,"utf-8"));l?.use&&typeof l.use=="object"&&!Array.isArray(l.use)&&(r=l.use),l?.base_url&&!r?.baseURL&&(r={...r,baseURL:l.base_url}),l?.settings?.auto_dismiss_modal!==void 0&&(r={...r,autoDismissModal:!!l.settings.auto_dismiss_modal}),l?.settings?.browser_timezone!=null&&(r={...r,timezoneId:String(l.settings.browser_timezone)}),l?.settings?.browser_language!=null&&(r={...r,locale:String(l.settings.browser_language)}),l?.settings?.extra_http_headers!=null&&typeof l.settings.extra_http_headers=="object"&&(r={...r,extraHTTPHeaders:l.settings.extra_http_headers})}catch(l){console.error("[debugger] Could not parse YAML for `use` block:",l)}let u=m.dirname(m.resolve(s)),a=n?`-${n}`:"",g=m.join(u,`.__shiplight_debug__${a}.yaml.spec.ts`),p=L(s,o,r,t);w.writeFileSync(g,p);let h=O(g),v=["playwright","test",g,...i?["--headed"]:[]],c=A("npx",v,{stdio:["ignore","pipe","pipe"],shell:!0,cwd:t,env:{...process.env,PWDEBUG:"console",SHIPLIGHT_REGISTRY_URL:""}});c.stdout?.on("data",l=>{process.stderr.write(l)}),c.stderr?.on("data",l=>{process.stderr.write(l)});let f=()=>{c.killed||c.kill("SIGTERM")};process.on("SIGTERM",f),process.on("SIGINT",f),process.on("exit",f),c.on("close",l=>{process.removeListener("SIGTERM",f),process.removeListener("SIGINT",f),process.removeListener("exit",f),h(),l!==0&&l!==null&&console.error(`[debugger] Playwright process exited with code ${l}`)}),console.error("[debugger] Waiting for Playwright sandbox to start...");let x=["127.0.0.1","::1"];async function b(l){try{let P=l.includes(":")?`[${l}]`:l,_=await fetch(`http://${P}:${o}/api/test-flow`);if(_.ok){try{await _.text()}catch{}return!0}}catch{}return!1}let S=null;for(let l=0;l<180;l++){if(c.exitCode!==null)throw h(),new Error(`Playwright process exited with code ${c.exitCode} before sandbox was ready`);for(let P of x)if(await b(P)){S=P;break}if(S){console.error(`[debugger] Playwright sandbox ready on ${S}:${o}`);break}if(l===179)throw f(),h(),new Error("Timed out waiting for Playwright sandbox to start (180s)");await new Promise(P=>setTimeout(P,1e3))}if(!S)throw f(),h(),new Error("Sandbox poll finished without a reachable host");return{port:o,host:S,pid:c.pid??0,cleanup:async()=>{f(),h()}}}var R=1e4,E=class{sessions=new Map;byYamlPath=new Map;options;spawner;headed;constructor(s={}){this.options=s,this.spawner=s.spawner??I,this.headed=s.headed??!1}log(s){this.options.onLog?this.options.onLog(s):console.error(s)}notifyStateChange(s){try{this.options.onSessionStateChange?.({...s})}catch(e){this.log(`[manager] onSessionStateChange listener threw: ${e.message}`)}}openSession(s){let e=y.resolve(s),n=this.byYamlPath.get(e);if(n){let a=this.sessions.get(n);if(a&&a.session.status!=="ended")return{...a.session};this.byYamlPath.delete(e)}if(!D.existsSync(e))throw new Error(`YAML file not found: ${e}`);if(!$(y.dirname(e)))throw new Error(`No Playwright config found for ${e} (searched parents for playwright.config.{ts,js,mjs}).`);let t=`dbg-${M().slice(0,8)}`,o=new Date().toISOString(),r={sessionId:t,yamlPath:e,innerPort:0,innerHost:"127.0.0.1",pid:0,startedAt:o,status:"idle",exitInfo:null},u={session:r,cleanup:async()=>{},readyPromise:Promise.resolve()};return this.sessions.set(t,u),this.byYamlPath.set(e,t),this.notifyStateChange(r),this.log(`[manager] session ${t} created (idle) for ${y.basename(e)}`),{...r}}async startSandbox(s){let e=this.sessions.get(s);if(!e)throw new Error(`No session ${s} to start sandbox for`);if(e.session.status==="running"||e.session.status==="starting"){e.readyPromise&&await e.readyPromise;return}if(e.session.status==="ended")throw new Error(`Session ${s} has ended`);let n=e.session.yamlPath,i=$(y.dirname(n));if(!i)throw new Error(`No Playwright config found for ${n} (searched parents for playwright.config.{ts,js,mjs}).`);e.session.status="starting",this.notifyStateChange(e.session);let t=(async()=>{let o=R+18e4,r,u=new Promise((p,h)=>{r=setTimeout(()=>h(new Error(`Timed out after ${o/1e3}s waiting for inner playwright to register on a port.`)),o)}),a;try{a=await Promise.race([this.spawner({yamlFilePath:n,configPath:i,tempSuffix:s,headed:this.headed}),u])}finally{r&&clearTimeout(r)}e.session.innerPort=a.port,e.session.innerHost=a.host,e.session.pid=a.pid,e.session.status="running",e.cleanup=a.cleanup;let g=p=>{e.session.status!=="ended"&&(e.session.status="ended",e.session.exitInfo=p,this.notifyStateChange(e.session))};e.livenessTimer=this.startLivenessProbe(s,g),this.notifyStateChange(e.session),this.log(`[manager] session ${s} running on ${e.session.innerHost}:${e.session.innerPort} (yaml=${y.basename(n)})`)})();e.readyPromise=t;try{await t}catch(o){throw e.session.status="ended",e.session.exitInfo=o.message,this.notifyStateChange(e.session),o}}async stopSandbox(s){let e=this.sessions.get(s);if(e&&e.session.status!=="idle"&&e.session.status!=="ended"){this.log(`[manager] stopSandbox ${s}`),e.livenessTimer&&(clearInterval(e.livenessTimer),e.livenessTimer=void 0);try{await e.cleanup()}catch(n){this.log(`[manager] stopSandbox ${s} cleanup error: ${n.message}`)}e.session.innerPort=0,e.session.innerHost="127.0.0.1",e.session.pid=0,e.session.status="idle",e.session.exitInfo=null,e.cleanup=async()=>{},e.readyPromise=Promise.resolve(),this.notifyStateChange(e.session)}}startLivenessProbe(s,e){let t=0,o=setInterval(()=>{let r=this.sessions.get(s);if(!r||r.session.status==="ended"){clearInterval(o);return}let u=!1;try{process.kill(r.session.pid,0),u=!0}catch(a){a?.code!=="ESRCH"&&(u=!0)}u?t=0:(t+=1,t>=3&&(clearInterval(o),e("process-exited")))},3e3);return o.unref(),o}async closeSession(s){let e=this.sessions.get(s);if(e){this.log(`[manager] closeSession ${s} (status=${e.session.status})`),this.sessions.delete(s),this.byYamlPath.get(e.session.yamlPath)===s&&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(n){this.log(`[manager] closeSession ${s} cleanup error: ${n.message}`)}}}async restartInner(s){let e=this.sessions.get(s);if(!e)throw new Error(`No session ${s} to restart`);if(e.restartInProgress)throw new Error(`Restart already in progress for session ${s}`);let n=e.session.yamlPath,i=$(y.dirname(n));if(!i)throw new Error(`No Playwright config found for ${n} (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(a){this.log(`[manager] restartInner ${s} old cleanup error: ${a.message}`)}e.session.status="starting",e.session.innerPort=0,e.session.pid=0,this.notifyStateChange(e.session);let t=R+18e4,o,r=new Promise((a,g)=>{o=setTimeout(()=>g(new Error(`Timed out after ${t/1e3}s waiting for inner playwright to respawn.`)),t)}),u;try{u=await Promise.race([this.spawner({yamlFilePath:n,configPath:i,tempSuffix:s,headed:this.headed}),r])}catch(a){throw e.session.status="ended",e.session.exitInfo=a.message,this.notifyStateChange(e.session),a}finally{o&&clearTimeout(o)}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(s,a=>{e.session.status!=="ended"&&(e.session.status="ended",e.session.exitInfo=a,this.notifyStateChange(e.session))}),this.notifyStateChange(e.session),this.log(`[manager] session ${s} restarted on ${e.session.innerHost}:${e.session.innerPort} (yaml=${y.basename(n)})`)}finally{e.restartInProgress=!1}}listSessions(){return Array.from(this.sessions.values()).map(s=>({...s.session}))}getSession(s){let e=this.sessions.get(s);return e?{...e.session}:void 0}httpProxyFor(s,e={}){let{liveviewUrlBuilder:n}=e;return async(i,t,o)=>{let r=this.sessions.get(s);if(!r){t.status(404).json({status:"error",message:"Session not found"});return}if(r.session.status==="ended"){t.status(410).json({status:"error",message:"Session has ended",exitInfo:r.session.exitInfo});return}if(r.session.status==="idle"){t.status(503).json({status:"error",message:"Sandbox not started"});return}let u=`${i.method} ${i.path}`;if(u==="POST /api/int-runner/create-session"){let g=await H(i),p={};if(g.length)try{p=JSON.parse(g.toString("utf-8"))}catch{t.status(400).json({status:"error",message:"Invalid JSON body"});return}p.testFilePath=r.session.yamlPath,await C(i,t,r.session.innerHost,r.session.innerPort,Buffer.from(JSON.stringify(p),"utf-8"),"application/json");return}if(u==="POST /api/int-runner/terminate-session"){try{await this.stopSandbox(s),t.json({status:"success",details:"Sandbox stopped"})}catch(g){t.status(500).json({status:"error",message:g.message})}return}if(u==="POST /api/int-runner/liveview-url"){let g=n?.(i)??"";t.json({liveviewUrl:g,browserWsUrl:""});return}let a=await H(i);await C(i,t,r.session.innerHost,r.session.innerPort,a,i.headers["content-type"])}}wsUpgradeFor(s){return async(e,n,i,t)=>{let o=this.sessions.get(s);if(!o){T(n,`HTTP/1.1 404 Not Found\r
|
|
26
26
|
\r
|
|
27
|
-
`);return}if(
|
|
27
|
+
`);return}if(o.session.status==="ended"){T(n,`HTTP/1.1 410 Gone\r
|
|
28
28
|
\r
|
|
29
|
-
`);return}try{let
|
|
30
|
-
Host: ${
|
|
31
|
-
`;for(let[x,
|
|
29
|
+
`);return}try{let r=o.session.innerHost.includes(":")?`[${o.session.innerHost}]`:o.session.innerHost,u=await fetch(`http://${r}:${o.session.innerPort}/api/browser-cdp`);if(!u.ok)throw new Error(`Inner /api/browser-cdp returned ${u.status}`);let{cdpUrl:a}=await u.json(),g=new URL(a.replace(/^ws/,"http")),p=parseInt(g.port||"80",10),h=g.hostname,v=g.pathname;t&&t.startsWith("/page/")&&(v=`/devtools${t}`);let c=j.createConnection(p,h);c.on("connect",()=>{let f=`GET ${v} HTTP/1.1\r
|
|
30
|
+
Host: ${h}:${p}\r
|
|
31
|
+
`;for(let[x,b]of Object.entries(e.headers)){let S=x.toLowerCase();S!=="host"&&S!=="origin"&&(f+=`${x}: ${Array.isArray(b)?b.join(", "):b}\r
|
|
32
32
|
`)}f+=`\r
|
|
33
|
-
`,c.write(f),i.length&&c.write(i),c.pipe(
|
|
33
|
+
`,c.write(f),i.length&&c.write(i),c.pipe(n),n.pipe(c)}),c.on("error",()=>{(!("destroyed"in n)||!n.destroyed)&&n.destroy()}),n.on("error",()=>{c.destroyed||c.destroy()}),n.on("close",()=>{c.destroyed||c.destroy()})}catch(r){this.log(`[manager] WS upgrade for ${s} failed: ${r.message}`),T(n,`HTTP/1.1 502 Bad Gateway\r
|
|
34
34
|
\r
|
|
35
|
-
`)}}}async shutdown(){let
|
|
35
|
+
`)}}}async shutdown(){let s=Array.from(this.sessions.keys());await Promise.allSettled(s.map(e=>this.closeSession(e)))}};function H(d){return new Promise((s,e)=>{if(d.body&&typeof d.body=="object")try{s(Buffer.from(JSON.stringify(d.body),"utf-8"));return}catch{}if(d.readableEnded){s(Buffer.alloc(0));return}let n=[];d.on("data",i=>n.push(i)),d.on("end",()=>s(Buffer.concat(n))),d.on("error",e)})}function C(d,s,e,n,i,t){return new Promise(o=>{let r=Array.isArray(t)?t[0]:t,u=N.request({hostname:e,port:n,path:d.originalUrl,method:d.method,headers:{"content-type":r||"application/json","content-length":String(i.length)},timeout:3e5},a=>{s.writeHead(a.statusCode??502,a.headers),a.on("data",g=>s.write(g)),a.on("end",()=>{s.end(),o()}),a.on("error",()=>{s.writableEnded||s.end(),o()})});u.on("error",a=>{s.headersSent?s.writableEnded||s.end():s.status(502).json({status:"error",message:"Inner server unavailable: "+a.message}),o()}),u.end(i)})}function T(d,s){try{(!("destroyed"in d)||!d.destroyed)&&(d.write(s),d.destroy())}catch{}}export{E as DebuggerManager};
|
package/dist/debugger-pw.d.ts
CHANGED
|
@@ -60,10 +60,18 @@ interface SandboxService {
|
|
|
60
60
|
usePureVision?: boolean;
|
|
61
61
|
includeDebugInfo?: boolean;
|
|
62
62
|
}): Promise<any>;
|
|
63
|
-
takeScreenshot(sessionId: string): Promise<{
|
|
63
|
+
takeScreenshot(sessionId: string, stepId?: string): Promise<{
|
|
64
64
|
screenshot: string;
|
|
65
65
|
screenshotPath?: string;
|
|
66
66
|
}>;
|
|
67
|
+
/** Return accumulated artifacts (screenshots) from the current session. */
|
|
68
|
+
getSessionArtifacts?(sessionId: string): {
|
|
69
|
+
outputDir?: string;
|
|
70
|
+
screenshots: Array<{
|
|
71
|
+
stepId: string;
|
|
72
|
+
path: string;
|
|
73
|
+
}>;
|
|
74
|
+
};
|
|
67
75
|
startRecorder(sessionId: string, onEvent: (event: any) => void, testIdAttributeName?: string): Promise<void>;
|
|
68
76
|
stopRecorder(sessionId: string): Promise<void>;
|
|
69
77
|
}
|
|
@@ -90,6 +98,9 @@ declare class PlaywrightSandboxService implements SandboxService {
|
|
|
90
98
|
private abortController;
|
|
91
99
|
private recorderStopResolver?;
|
|
92
100
|
private recorderEnabled;
|
|
101
|
+
private screenshotHistory;
|
|
102
|
+
private yamlFilePath?;
|
|
103
|
+
private artifactsDir?;
|
|
93
104
|
constructor(page: Page, agent: WebAgent, fixturesDir?: string, projectRoot?: string);
|
|
94
105
|
private cdpEndpoint;
|
|
95
106
|
setCdpEndpoint(url: string): void;
|
|
@@ -133,10 +144,17 @@ declare class PlaywrightSandboxService implements SandboxService {
|
|
|
133
144
|
usePureVision?: boolean;
|
|
134
145
|
includeDebugInfo?: boolean;
|
|
135
146
|
}): Promise<ActionGenerationResponse>;
|
|
136
|
-
takeScreenshot(_sessionId: string): Promise<{
|
|
147
|
+
takeScreenshot(_sessionId: string, stepId?: string): Promise<{
|
|
137
148
|
screenshot: string;
|
|
138
149
|
screenshotPath?: string;
|
|
139
150
|
}>;
|
|
151
|
+
getSessionArtifacts(_sessionId: string): {
|
|
152
|
+
outputDir?: string;
|
|
153
|
+
screenshots: Array<{
|
|
154
|
+
stepId: string;
|
|
155
|
+
path: string;
|
|
156
|
+
}>;
|
|
157
|
+
};
|
|
140
158
|
startRecorder(_sessionId: string, onEvent: (event: any) => void, testIdAttributeName?: string): Promise<void>;
|
|
141
159
|
stopRecorder(_sessionId: string): Promise<void>;
|
|
142
160
|
terminateSession(_sessionId: string): Promise<void>;
|