playwriter 0.0.63 → 0.0.89

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.
Files changed (223) hide show
  1. package/dist/a11y-client.js +18 -8
  2. package/dist/aria-snapshot.d.ts +41 -3
  3. package/dist/aria-snapshot.d.ts.map +1 -1
  4. package/dist/aria-snapshot.js +134 -55
  5. package/dist/aria-snapshot.js.map +1 -1
  6. package/dist/aria-snapshot.test.js +5 -2
  7. package/dist/aria-snapshot.test.js.map +1 -1
  8. package/dist/aria-snapshot.unit.test.js +83 -41
  9. package/dist/aria-snapshot.unit.test.js.map +1 -1
  10. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +5 -0
  11. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +1 -0
  12. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +5 -0
  13. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +1 -0
  14. package/dist/bippy.js +1 -1
  15. package/dist/cdp-log.d.ts +1 -1
  16. package/dist/cdp-log.d.ts.map +1 -1
  17. package/dist/cdp-log.js +1 -1
  18. package/dist/cdp-log.js.map +1 -1
  19. package/dist/cdp-relay.d.ts.map +1 -1
  20. package/dist/cdp-relay.js +492 -298
  21. package/dist/cdp-relay.js.map +1 -1
  22. package/dist/cdp-session.d.ts.map +1 -1
  23. package/dist/cdp-session.js.map +1 -1
  24. package/dist/cdp-types.d.ts.map +1 -1
  25. package/dist/cdp-types.js +7 -7
  26. package/dist/cdp-types.js.map +1 -1
  27. package/dist/clean-html.d.ts.map +1 -1
  28. package/dist/clean-html.js +4 -5
  29. package/dist/clean-html.js.map +1 -1
  30. package/dist/cli.js +45 -27
  31. package/dist/cli.js.map +1 -1
  32. package/dist/create-logger.d.ts.map +1 -1
  33. package/dist/create-logger.js +3 -1
  34. package/dist/create-logger.js.map +1 -1
  35. package/dist/debugger-examples-types.d.ts.map +1 -1
  36. package/dist/debugger.d.ts.map +1 -1
  37. package/dist/debugger.js +1 -3
  38. package/dist/debugger.js.map +1 -1
  39. package/dist/diff-utils.d.ts.map +1 -1
  40. package/dist/diff-utils.js +1 -4
  41. package/dist/diff-utils.js.map +1 -1
  42. package/dist/editor-api.md +12 -2
  43. package/dist/editor-examples.d.ts +1 -1
  44. package/dist/editor-examples.d.ts.map +1 -1
  45. package/dist/editor-examples.js +1 -1
  46. package/dist/editor-examples.js.map +1 -1
  47. package/dist/editor.d.ts +1 -1
  48. package/dist/editor.d.ts.map +1 -1
  49. package/dist/editor.js +1 -1
  50. package/dist/editor.js.map +1 -1
  51. package/dist/executor.d.ts +26 -3
  52. package/dist/executor.d.ts.map +1 -1
  53. package/dist/executor.js +297 -64
  54. package/dist/executor.js.map +1 -1
  55. package/dist/executor.unit.test.js +38 -1
  56. package/dist/executor.unit.test.js.map +1 -1
  57. package/dist/extension-connection.test.js +139 -36
  58. package/dist/extension-connection.test.js.map +1 -1
  59. package/dist/ffmpeg.d.ts +148 -0
  60. package/dist/ffmpeg.d.ts.map +1 -0
  61. package/dist/ffmpeg.js +523 -0
  62. package/dist/ffmpeg.js.map +1 -0
  63. package/dist/ghost-browser.d.ts.map +1 -1
  64. package/dist/ghost-browser.js.map +1 -1
  65. package/dist/ghost-cursor-client.js +287 -0
  66. package/dist/ghost-cursor.d.ts +27 -0
  67. package/dist/ghost-cursor.d.ts.map +1 -0
  68. package/dist/ghost-cursor.js +63 -0
  69. package/dist/ghost-cursor.js.map +1 -0
  70. package/dist/htmlrewrite.d.ts.map +1 -1
  71. package/dist/htmlrewrite.js +17 -55
  72. package/dist/htmlrewrite.js.map +1 -1
  73. package/dist/htmlrewrite.test.js.map +1 -1
  74. package/dist/kill-port.d.ts.map +1 -1
  75. package/dist/kill-port.js +1 -3
  76. package/dist/kill-port.js.map +1 -1
  77. package/dist/locator-selector.test.d.ts +2 -0
  78. package/dist/locator-selector.test.d.ts.map +1 -0
  79. package/dist/locator-selector.test.js +96 -0
  80. package/dist/locator-selector.test.js.map +1 -0
  81. package/dist/mcp-client.js.map +1 -1
  82. package/dist/mcp.d.ts.map +1 -1
  83. package/dist/mcp.js +8 -3
  84. package/dist/mcp.js.map +1 -1
  85. package/dist/on-mouse-action.test.d.ts +2 -0
  86. package/dist/on-mouse-action.test.d.ts.map +1 -0
  87. package/dist/on-mouse-action.test.js +155 -0
  88. package/dist/on-mouse-action.test.js.map +1 -0
  89. package/dist/page-markdown.js +4 -4
  90. package/dist/page-markdown.js.map +1 -1
  91. package/dist/prompt.md +450 -377
  92. package/dist/protocol.d.ts +4 -0
  93. package/dist/protocol.d.ts.map +1 -1
  94. package/dist/readability.js +16 -2
  95. package/dist/recording-ghost-cursor.d.ts +41 -0
  96. package/dist/recording-ghost-cursor.d.ts.map +1 -0
  97. package/dist/recording-ghost-cursor.js +79 -0
  98. package/dist/recording-ghost-cursor.js.map +1 -0
  99. package/dist/recording-relay.d.ts.map +1 -1
  100. package/dist/recording-relay.js +8 -8
  101. package/dist/recording-relay.js.map +1 -1
  102. package/dist/relay-client.d.ts +17 -4
  103. package/dist/relay-client.d.ts.map +1 -1
  104. package/dist/relay-client.js +45 -11
  105. package/dist/relay-client.js.map +1 -1
  106. package/dist/relay-core.test.d.ts.map +1 -1
  107. package/dist/relay-core.test.js +515 -26
  108. package/dist/relay-core.test.js.map +1 -1
  109. package/dist/relay-navigation.test.d.ts.map +1 -1
  110. package/dist/relay-navigation.test.js +169 -31
  111. package/dist/relay-navigation.test.js.map +1 -1
  112. package/dist/relay-session.test.d.ts.map +1 -1
  113. package/dist/relay-session.test.js +113 -65
  114. package/dist/relay-session.test.js.map +1 -1
  115. package/dist/relay-state.d.ts +158 -0
  116. package/dist/relay-state.d.ts.map +1 -0
  117. package/dist/relay-state.js +306 -0
  118. package/dist/relay-state.js.map +1 -0
  119. package/dist/relay-state.test.d.ts +2 -0
  120. package/dist/relay-state.test.d.ts.map +1 -0
  121. package/dist/relay-state.test.js +472 -0
  122. package/dist/relay-state.test.js.map +1 -0
  123. package/dist/scoped-fs.d.ts.map +1 -1
  124. package/dist/scoped-fs.js.map +1 -1
  125. package/dist/screen-recording.d.ts +66 -4
  126. package/dist/screen-recording.d.ts.map +1 -1
  127. package/dist/screen-recording.js +150 -13
  128. package/dist/screen-recording.js.map +1 -1
  129. package/dist/screen-recording.test.d.ts +2 -0
  130. package/dist/screen-recording.test.d.ts.map +1 -0
  131. package/dist/screen-recording.test.js +102 -0
  132. package/dist/screen-recording.test.js.map +1 -0
  133. package/dist/selector-generator.js +1 -1
  134. package/dist/snapshot-tools.test.js +71 -28
  135. package/dist/snapshot-tools.test.js.map +1 -1
  136. package/dist/start-relay-server.d.ts +1 -1
  137. package/dist/start-relay-server.d.ts.map +1 -1
  138. package/dist/start-relay-server.js +1 -1
  139. package/dist/start-relay-server.js.map +1 -1
  140. package/dist/styles-api.md +8 -1
  141. package/dist/styles-examples.d.ts +1 -1
  142. package/dist/styles-examples.d.ts.map +1 -1
  143. package/dist/styles-examples.js +1 -1
  144. package/dist/styles-examples.js.map +1 -1
  145. package/dist/styles.d.ts.map +1 -1
  146. package/dist/styles.js +1 -3
  147. package/dist/styles.js.map +1 -1
  148. package/dist/test-declarations.d.ts.map +1 -1
  149. package/dist/test-utils.d.ts +1 -1
  150. package/dist/test-utils.d.ts.map +1 -1
  151. package/dist/test-utils.js +7 -5
  152. package/dist/test-utils.js.map +1 -1
  153. package/dist/utils.d.ts.map +1 -1
  154. package/dist/utils.js.map +1 -1
  155. package/dist/wait-for-page-load.d.ts.map +1 -1
  156. package/dist/wait-for-page-load.js +1 -1
  157. package/dist/wait-for-page-load.js.map +1 -1
  158. package/package.json +4 -3
  159. package/src/a11y-client.ts +5 -4
  160. package/src/aria-snapshot.test.ts +5 -2
  161. package/src/aria-snapshot.ts +306 -117
  162. package/src/aria-snapshot.unit.test.ts +199 -141
  163. package/src/aria-snapshots/github-interactive.txt +2 -0
  164. package/src/aria-snapshots/github-raw.txt +5 -1
  165. package/src/aria-snapshots/hackernews-interactive.txt +238 -241
  166. package/src/aria-snapshots/hackernews-raw.txt +265 -269
  167. package/src/assets/aria-labels-example.png +0 -0
  168. package/src/assets/aria-labels-github.png +0 -0
  169. package/src/assets/aria-labels-hacker-news.png +0 -0
  170. package/src/assets/aria-labels-old-reddit.png +0 -0
  171. package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +5 -0
  172. package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +18 -0
  173. package/src/cdp-log.ts +4 -1
  174. package/src/cdp-relay.ts +1059 -737
  175. package/src/cdp-session.ts +12 -3
  176. package/src/cdp-types.ts +51 -51
  177. package/src/clean-html.ts +4 -5
  178. package/src/cli.ts +82 -55
  179. package/src/create-logger.ts +5 -3
  180. package/src/debugger-examples-types.ts +4 -1
  181. package/src/debugger.ts +1 -5
  182. package/src/diff-utils.ts +2 -5
  183. package/src/editor-examples.ts +11 -1
  184. package/src/editor.ts +10 -2
  185. package/src/executor.ts +374 -73
  186. package/src/executor.unit.test.ts +48 -1
  187. package/src/extension-connection.test.ts +612 -488
  188. package/src/ffmpeg.ts +769 -0
  189. package/src/ghost-browser.ts +4 -6
  190. package/src/ghost-cursor-client.ts +369 -0
  191. package/src/ghost-cursor.ts +110 -0
  192. package/src/htmlrewrite.test.ts +6 -2
  193. package/src/htmlrewrite.ts +348 -386
  194. package/src/kill-port.ts +1 -3
  195. package/src/locator-selector.test.ts +115 -0
  196. package/src/mcp-client.ts +1 -1
  197. package/src/mcp.ts +21 -15
  198. package/src/on-mouse-action.test.ts +196 -0
  199. package/src/page-markdown.ts +7 -7
  200. package/src/protocol.ts +73 -57
  201. package/src/recording-ghost-cursor.ts +113 -0
  202. package/src/recording-relay.ts +20 -12
  203. package/src/relay-client.ts +85 -18
  204. package/src/relay-core.test.ts +1117 -578
  205. package/src/relay-navigation.test.ts +648 -483
  206. package/src/relay-session.test.ts +984 -929
  207. package/src/relay-state.test.ts +570 -0
  208. package/src/relay-state.ts +497 -0
  209. package/src/resource.md +21 -49
  210. package/src/scoped-fs.ts +9 -3
  211. package/src/screen-recording.test.ts +111 -0
  212. package/src/screen-recording.ts +256 -31
  213. package/src/skill.md +476 -396
  214. package/src/snapshot-tools.test.ts +580 -528
  215. package/src/snapshots/shadcn-ui-accessibility-full.md +8 -8
  216. package/src/snapshots/shadcn-ui-accessibility-interactive.md +8 -8
  217. package/src/start-relay-server.ts +14 -11
  218. package/src/styles-examples.ts +8 -1
  219. package/src/styles.ts +20 -21
  220. package/src/test-declarations.ts +6 -6
  221. package/src/test-utils.ts +104 -91
  222. package/src/utils.ts +2 -1
  223. package/src/wait-for-page-load.ts +6 -1
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Encapsulates ghost cursor lifecycle for recording sessions.
3
+ * Keeps onMouseAction chaining/restoration isolated from executor logic.
4
+ */
5
+
6
+ import type { BrowserContext, Page } from '@xmorse/playwright-core'
7
+ import {
8
+ applyGhostCursorMouseAction,
9
+ disableGhostCursor,
10
+ enableGhostCursor,
11
+ type GhostCursorClientOptions,
12
+ } from './ghost-cursor.js'
13
+
14
+ interface RecordingGhostCursorLogger {
15
+ error: (...args: unknown[]) => void
16
+ }
17
+
18
+ interface RecordingTargetOptions {
19
+ page?: Page
20
+ sessionId?: string
21
+ }
22
+
23
+ export class RecordingGhostCursorController {
24
+ private readonly previousMouseActionByPage = new WeakMap<Page, Page['onMouseAction']>()
25
+ private readonly cursorApplyQueueByPage = new WeakMap<Page, Promise<void>>()
26
+ private readonly logger: RecordingGhostCursorLogger
27
+
28
+ constructor(options: { logger: RecordingGhostCursorLogger }) {
29
+ this.logger = options.logger
30
+ }
31
+
32
+ resolveRecordingTargetPage(options: {
33
+ context: BrowserContext
34
+ defaultPage: Page
35
+ target?: RecordingTargetOptions
36
+ }): Page {
37
+ const { context, defaultPage, target } = options
38
+
39
+ if (target?.page) {
40
+ return target.page
41
+ }
42
+
43
+ if (target?.sessionId) {
44
+ const pageForSession = context.pages().find((candidatePage) => {
45
+ return candidatePage.sessionId() === target.sessionId
46
+ })
47
+
48
+ if (pageForSession) {
49
+ return pageForSession
50
+ }
51
+ }
52
+
53
+ return defaultPage
54
+ }
55
+
56
+ async enableForRecording(options: { page: Page }): Promise<void> {
57
+ const { page } = options
58
+
59
+ try {
60
+ await enableGhostCursor({ page })
61
+
62
+ if (!this.previousMouseActionByPage.has(page)) {
63
+ this.previousMouseActionByPage.set(page, page.onMouseAction)
64
+ }
65
+
66
+ const previousMouseAction = this.previousMouseActionByPage.get(page)
67
+ page.onMouseAction = async (event) => {
68
+ const pendingCursorApply = this.cursorApplyQueueByPage.get(page) || Promise.resolve()
69
+ const nextCursorApply = pendingCursorApply
70
+ .then(async () => {
71
+ await applyGhostCursorMouseAction({ page, event })
72
+ })
73
+ .catch((error) => {
74
+ this.logger.error('[playwriter] Failed to apply ghost cursor action', error)
75
+ })
76
+ this.cursorApplyQueueByPage.set(page, nextCursorApply)
77
+
78
+ if (!previousMouseAction) {
79
+ return
80
+ }
81
+
82
+ await previousMouseAction(event)
83
+ }
84
+ } catch (error) {
85
+ page.onMouseAction = this.previousMouseActionByPage.get(page) ?? null
86
+ this.previousMouseActionByPage.delete(page)
87
+ this.logger.error('[playwriter] Failed to enable ghost cursor', error)
88
+ }
89
+ }
90
+
91
+ async disableForRecording(options: { page: Page }): Promise<void> {
92
+ const { page } = options
93
+ page.onMouseAction = this.previousMouseActionByPage.get(page) ?? null
94
+ this.previousMouseActionByPage.delete(page)
95
+ this.cursorApplyQueueByPage.delete(page)
96
+
97
+ try {
98
+ await disableGhostCursor({ page })
99
+ } catch (error) {
100
+ this.logger.error('[playwriter] Failed to disable ghost cursor', error)
101
+ }
102
+ }
103
+
104
+ async show(options: { page: Page; cursorOptions?: GhostCursorClientOptions }): Promise<void> {
105
+ const { page, cursorOptions } = options
106
+ await enableGhostCursor({ page, cursorOptions })
107
+ }
108
+
109
+ async hide(options: { page: Page }): Promise<void> {
110
+ const { page } = options
111
+ await disableGhostCursor({ page })
112
+ }
113
+ }
@@ -22,7 +22,7 @@ import type {
22
22
  // Recording state - tracks active recordings and their accumulated chunks
23
23
  export interface ActiveRecording {
24
24
  tabId: number
25
- sessionId?: string // The sessionId used to start this recording, for lookup when stopping
25
+ sessionId?: string // The sessionId used to start this recording, for lookup when stopping
26
26
  outputPath: string
27
27
  chunks: Buffer[]
28
28
  startedAt: number
@@ -40,7 +40,7 @@ export class RecordingRelay {
40
40
  constructor(
41
41
  sendToExtension: (params: { method: string; params?: unknown; timeout?: number }) => Promise<unknown>,
42
42
  isExtensionConnected: () => boolean,
43
- logger?: { log(...args: unknown[]): void; error(...args: unknown[]): void }
43
+ logger?: { log(...args: unknown[]): void; error(...args: unknown[]): void },
44
44
  ) {
45
45
  this.sendToExtension = sendToExtension
46
46
  this.isExtensionConnected = isExtensionConnected
@@ -58,7 +58,11 @@ export class RecordingRelay {
58
58
  const recording = this.activeRecordings.get(tabId)
59
59
  if (recording) {
60
60
  recording.chunks.push(buffer)
61
- this.logger?.log(pc.blue(`Received recording chunk for tab ${tabId}: ${buffer.length} bytes (total chunks: ${recording.chunks.length})`))
61
+ this.logger?.log(
62
+ pc.blue(
63
+ `Received recording chunk for tab ${tabId}: ${buffer.length} bytes (total chunks: ${recording.chunks.length})`,
64
+ ),
65
+ )
62
66
  } else {
63
67
  this.logger?.log(pc.yellow(`Received recording chunk for unknown tab ${tabId}, ignoring`))
64
68
  }
@@ -140,11 +144,11 @@ export class RecordingRelay {
140
144
  }
141
145
 
142
146
  try {
143
- const result = await this.sendToExtension({
147
+ const result = (await this.sendToExtension({
144
148
  method: 'startRecording',
145
149
  params: recordingParams,
146
150
  timeout: 10000,
147
- }) as StartRecordingResult
151
+ })) as StartRecordingResult
148
152
 
149
153
  if (!result) {
150
154
  return { success: false, error: 'Extension returned empty result' }
@@ -158,7 +162,11 @@ export class RecordingRelay {
158
162
  chunks: [],
159
163
  startedAt: result.startedAt,
160
164
  })
161
- this.logger?.log(pc.green(`Recording started for tab ${result.tabId} (sessionId: ${recordingParams.sessionId || 'none'}), output: ${outputPath}`))
165
+ this.logger?.log(
166
+ pc.green(
167
+ `Recording started for tab ${result.tabId} (sessionId: ${recordingParams.sessionId || 'none'}), output: ${outputPath}`,
168
+ ),
169
+ )
162
170
  }
163
171
 
164
172
  return result
@@ -211,11 +219,11 @@ export class RecordingRelay {
211
219
  })
212
220
 
213
221
  try {
214
- const result = await this.sendToExtension({
222
+ const result = (await this.sendToExtension({
215
223
  method: 'stopRecording',
216
224
  params,
217
225
  timeout: 10000,
218
- }) as StopRecordingResult
226
+ })) as StopRecordingResult
219
227
 
220
228
  if (!result.success) {
221
229
  recording.resolveStop = undefined
@@ -237,11 +245,11 @@ export class RecordingRelay {
237
245
  }
238
246
 
239
247
  try {
240
- return await this.sendToExtension({
248
+ return (await this.sendToExtension({
241
249
  method: 'isRecording',
242
250
  params,
243
251
  timeout: 5000,
244
- }) as IsRecordingResult
252
+ })) as IsRecordingResult
245
253
  } catch {
246
254
  return { isRecording: false }
247
255
  }
@@ -253,11 +261,11 @@ export class RecordingRelay {
253
261
  }
254
262
 
255
263
  try {
256
- return await this.sendToExtension({
264
+ return (await this.sendToExtension({
257
265
  method: 'cancelRecording',
258
266
  params,
259
267
  timeout: 5000,
260
- }) as CancelRecordingResult
268
+ })) as CancelRecordingResult
261
269
  } catch (error: unknown) {
262
270
  const errorMessage = error instanceof Error ? error.message : String(error)
263
271
  this.logger?.error('Cancel recording error:', error)
@@ -15,10 +15,19 @@ const __dirname = path.dirname(__filename)
15
15
 
16
16
  export const RELAY_PORT = Number(process.env.PLAYWRITER_PORT) || 19988
17
17
 
18
+ export type ExtensionStatus = {
19
+ extensionId: string
20
+ stableKey?: string
21
+ browser: string | null
22
+ profile: { email: string; id: string } | null
23
+ activeTargets: number
24
+ playwriterVersion: string | null
25
+ }
26
+
18
27
  export async function getRelayServerVersion(port: number = RELAY_PORT): Promise<string | null> {
19
28
  try {
20
29
  const response = await fetch(`http://127.0.0.1:${port}/version`, {
21
- signal: AbortSignal.timeout(500),
30
+ signal: AbortSignal.timeout(2000),
22
31
  })
23
32
  if (!response.ok) {
24
33
  return null
@@ -30,7 +39,9 @@ export async function getRelayServerVersion(port: number = RELAY_PORT): Promise<
30
39
  }
31
40
  }
32
41
 
33
- export async function getExtensionStatus(port: number = RELAY_PORT): Promise<{ connected: boolean; activeTargets: number; playwriterVersion: string | null } | null> {
42
+ export async function getExtensionStatus(
43
+ port: number = RELAY_PORT,
44
+ ): Promise<{ connected: boolean; activeTargets: number; playwriterVersion: string | null } | null> {
34
45
  try {
35
46
  const response = await fetch(`http://127.0.0.1:${port}/extension/status`, {
36
47
  signal: AbortSignal.timeout(500),
@@ -38,37 +49,87 @@ export async function getExtensionStatus(port: number = RELAY_PORT): Promise<{ c
38
49
  if (!response.ok) {
39
50
  return null
40
51
  }
41
- return await response.json() as { connected: boolean; activeTargets: number; playwriterVersion: string | null }
52
+ return (await response.json()) as { connected: boolean; activeTargets: number; playwriterVersion: string | null }
42
53
  } catch {
43
54
  return null
44
55
  }
45
56
  }
46
57
 
58
+ export async function getExtensionsStatus(port: number = RELAY_PORT): Promise<ExtensionStatus[]> {
59
+ try {
60
+ const response = await fetch(`http://127.0.0.1:${port}/extensions/status`, {
61
+ signal: AbortSignal.timeout(2000),
62
+ })
63
+ if (!response.ok) {
64
+ const fallback = await fetch(`http://127.0.0.1:${port}/extension/status`, {
65
+ signal: AbortSignal.timeout(2000),
66
+ })
67
+ if (!fallback.ok) {
68
+ return []
69
+ }
70
+
71
+ const fallbackData = (await fallback.json()) as {
72
+ connected: boolean
73
+ activeTargets: number
74
+ browser: string | null
75
+ profile: { email: string; id: string } | null
76
+ playwriterVersion?: string | null
77
+ }
78
+
79
+ if (!fallbackData?.connected) {
80
+ return []
81
+ }
82
+
83
+ return [
84
+ {
85
+ extensionId: 'default',
86
+ stableKey: undefined,
87
+ browser: fallbackData.browser,
88
+ profile: fallbackData.profile,
89
+ activeTargets: fallbackData.activeTargets,
90
+ playwriterVersion: fallbackData.playwriterVersion || null,
91
+ },
92
+ ]
93
+ }
94
+
95
+ const data = (await response.json()) as {
96
+ extensions: ExtensionStatus[]
97
+ }
98
+
99
+ return data.extensions || []
100
+ } catch {
101
+ return []
102
+ }
103
+ }
104
+
47
105
  /**
48
- * Wait for the extension to connect to the relay server.
49
- * Returns true if connected within timeout, false otherwise.
106
+ * Wait for at least one extension to appear in extensions status.
107
+ * Returns connected extension entries, or [] on timeout.
50
108
  */
51
- export async function waitForExtension(options: {
52
- port?: number
53
- timeoutMs?: number
54
- logger?: { log: (...args: any[]) => void }
55
- } = {}): Promise<boolean> {
56
- const { port = RELAY_PORT, timeoutMs = 5000, logger } = options
109
+ export async function waitForConnectedExtensions(
110
+ options: {
111
+ port?: number
112
+ timeoutMs?: number
113
+ pollIntervalMs?: number
114
+ logger?: { log: (...args: any[]) => void }
115
+ } = {},
116
+ ): Promise<ExtensionStatus[]> {
117
+ const { port = RELAY_PORT, timeoutMs = 5000, pollIntervalMs = 200, logger } = options
57
118
  const startTime = Date.now()
58
119
 
59
120
  logger?.log(pc.dim('Waiting for extension to connect...'))
60
121
 
61
122
  while (Date.now() - startTime < timeoutMs) {
62
- const status = await getExtensionStatus(port)
63
- if (status?.connected) {
123
+ const extensions = await getExtensionsStatus(port)
124
+ if (extensions.length > 0) {
64
125
  logger?.log(pc.green('Extension connected'))
65
- return true
126
+ return extensions
66
127
  }
67
- await sleep(200)
128
+ await sleep(pollIntervalMs)
68
129
  }
69
130
 
70
131
  logger?.log(pc.yellow('Extension did not connect within timeout'))
71
- return false
132
+ return []
72
133
  }
73
134
 
74
135
  async function killRelayServer(options: { port: number; waitForFreeMs?: number }): Promise<void> {
@@ -155,7 +216,9 @@ export async function ensureRelayServer(options: EnsureRelayServerOptions = {}):
155
216
 
156
217
  if (serverVersion !== null) {
157
218
  if (restartOnVersionMismatch) {
158
- logger?.log(pc.yellow(`CDP relay server version mismatch (server: ${serverVersion}, client: ${VERSION}), restarting...`))
219
+ logger?.log(
220
+ pc.yellow(`CDP relay server version mismatch (server: ${serverVersion}, client: ${VERSION}), restarting...`),
221
+ )
159
222
  await killRelayServer({ port: RELAY_PORT })
160
223
  } else {
161
224
  // Server is running but different version, just use it
@@ -164,7 +227,11 @@ export async function ensureRelayServer(options: EnsureRelayServerOptions = {}):
164
227
  } else {
165
228
  const listeningPids = await getListeningPidsForPort({ port: RELAY_PORT }).catch(() => [])
166
229
  if (listeningPids.length > 0) {
167
- logger?.log(pc.yellow(`Port ${RELAY_PORT} is already in use (pid(s): ${listeningPids.join(', ')}). Attempting to stop the existing process...`))
230
+ logger?.log(
231
+ pc.yellow(
232
+ `Port ${RELAY_PORT} is already in use (pid(s): ${listeningPids.join(', ')}). Attempting to stop the existing process...`,
233
+ ),
234
+ )
168
235
  await killRelayServer({ port: RELAY_PORT })
169
236
  }
170
237