playwriter 0.0.15 → 0.0.20

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 (47) hide show
  1. package/dist/cdp-session.d.ts +21 -0
  2. package/dist/cdp-session.d.ts.map +1 -0
  3. package/dist/cdp-session.js +114 -0
  4. package/dist/cdp-session.js.map +1 -0
  5. package/dist/cdp-types.d.ts +15 -0
  6. package/dist/cdp-types.d.ts.map +1 -1
  7. package/dist/cdp-types.js.map +1 -1
  8. package/dist/create-logger.d.ts +9 -0
  9. package/dist/create-logger.d.ts.map +1 -0
  10. package/dist/create-logger.js +43 -0
  11. package/dist/create-logger.js.map +1 -0
  12. package/dist/extension/cdp-relay.d.ts +7 -3
  13. package/dist/extension/cdp-relay.d.ts.map +1 -1
  14. package/dist/extension/cdp-relay.js +46 -13
  15. package/dist/extension/cdp-relay.js.map +1 -1
  16. package/dist/mcp.js +52 -27
  17. package/dist/mcp.js.map +1 -1
  18. package/dist/mcp.test.d.ts.map +1 -1
  19. package/dist/mcp.test.js +625 -185
  20. package/dist/mcp.test.js.map +1 -1
  21. package/dist/prompt.md +36 -8
  22. package/dist/selector-generator.js +331 -0
  23. package/dist/start-relay-server.d.ts +1 -3
  24. package/dist/start-relay-server.d.ts.map +1 -1
  25. package/dist/start-relay-server.js +3 -16
  26. package/dist/start-relay-server.js.map +1 -1
  27. package/dist/utils.d.ts +3 -0
  28. package/dist/utils.d.ts.map +1 -1
  29. package/dist/utils.js +34 -0
  30. package/dist/utils.js.map +1 -1
  31. package/dist/wait-for-page-load.d.ts +16 -0
  32. package/dist/wait-for-page-load.d.ts.map +1 -0
  33. package/dist/wait-for-page-load.js +114 -0
  34. package/dist/wait-for-page-load.js.map +1 -0
  35. package/package.json +4 -2
  36. package/src/cdp-session.ts +142 -0
  37. package/src/cdp-types.ts +6 -0
  38. package/src/create-logger.ts +56 -0
  39. package/src/debugger.md +453 -0
  40. package/src/extension/cdp-relay.ts +57 -15
  41. package/src/mcp.test.ts +743 -191
  42. package/src/mcp.ts +63 -29
  43. package/src/prompt.md +36 -8
  44. package/src/snapshots/shadcn-ui-accessibility.md +94 -91
  45. package/src/start-relay-server.ts +3 -20
  46. package/src/utils.ts +43 -0
  47. package/src/wait-for-page-load.ts +162 -0
package/src/mcp.ts CHANGED
@@ -4,13 +4,16 @@ import { z } from 'zod'
4
4
  import { Page, Browser, BrowserContext, chromium } from 'playwright-core'
5
5
  import fs from 'node:fs'
6
6
  import path from 'node:path'
7
+ import os from 'node:os'
7
8
  import { spawn } from 'node:child_process'
8
9
  import { createRequire } from 'node:module'
9
10
  import { fileURLToPath } from 'node:url'
10
11
  import vm from 'node:vm'
11
12
  import dedent from 'string-dedent'
12
13
  import { createPatch } from 'diff'
13
- import { getCdpUrl } from './utils.js'
14
+ import { getCdpUrl, LOG_FILE_PATH } from './utils.js'
15
+ import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js'
16
+ import { getCDPSessionForPage, CDPSession } from './cdp-session.js'
14
17
 
15
18
  const require = createRequire(import.meta.url)
16
19
 
@@ -59,6 +62,8 @@ interface VMContext {
59
62
  resetPlaywright: () => Promise<{ page: Page; context: BrowserContext }>
60
63
  getLatestLogs: (options?: { page?: Page; count?: number; search?: string | RegExp }) => Promise<string[]>
61
64
  clearAllLogs: () => void
65
+ waitForPageLoad: (options: WaitForPageLoadOptions) => Promise<WaitForPageLoadResult>
66
+ getCDPSession: (options: { page: Page }) => Promise<CDPSession>
62
67
  require: NodeRequire
63
68
  import: (specifier: string) => Promise<any>
64
69
  }
@@ -83,9 +88,31 @@ const MAX_LOGS_PER_PAGE = 5000
83
88
  // Store last accessibility snapshot per page for diff feature
84
89
  const lastSnapshots: WeakMap<Page, string> = new WeakMap()
85
90
 
91
+ // Cache CDP sessions per page
92
+ const cdpSessionCache: WeakMap<Page, CDPSession> = new WeakMap()
93
+
86
94
  const RELAY_PORT = 19988
87
95
  const NO_TABS_ERROR = `No browser tabs are connected. Please install and enable the Playwriter extension on at least one tab: https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe`
88
96
 
97
+
98
+
99
+ async function setDeviceScaleFactorForMacOS(context: BrowserContext): Promise<void> {
100
+ if (os.platform() !== 'darwin') {
101
+ return
102
+ }
103
+ const options = (context as any)._options
104
+ if (!options || options.deviceScaleFactor === 2) {
105
+ return
106
+ }
107
+ options.deviceScaleFactor = 2
108
+ for (const page of context.pages()) {
109
+ const delegate = (page as any)._delegate
110
+ if (delegate?.updateEmulatedViewportSize) {
111
+ await delegate.updateEmulatedViewportSize().catch(() => {})
112
+ }
113
+ }
114
+ }
115
+
89
116
  function isRegExp(value: any): value is RegExp {
90
117
  return typeof value === 'object' && value !== null && typeof value.test === 'function' && typeof value.exec === 'function'
91
118
  }
@@ -152,7 +179,7 @@ async function ensureRelayServer(): Promise<void> {
152
179
  }
153
180
  }
154
181
 
155
- throw new Error('Failed to start CDP relay server after 5 seconds')
182
+ throw new Error(`Failed to start CDP relay server after 5 seconds. Check logs at: ${LOG_FILE_PATH}`)
156
183
  }
157
184
 
158
185
  async function ensureConnection(): Promise<{ browser: Browser; page: Page }> {
@@ -182,6 +209,8 @@ async function ensureConnection(): Promise<{ browser: Browser; page: Page }> {
182
209
  // Set up console listener for all existing pages
183
210
  context.pages().forEach((p) => setupPageConsoleListener(p))
184
211
 
212
+ await setDeviceScaleFactorForMacOS(context)
213
+
185
214
  state.browser = browser
186
215
  state.page = page
187
216
  state.context = context
@@ -195,22 +224,12 @@ async function getPageTargetId(page: Page): Promise<string> {
195
224
  throw new Error('Page is null or undefined')
196
225
  }
197
226
 
198
- // Always use internal _guid for consistency and speed
199
227
  const guid = (page as any)._guid
200
228
  if (guid) {
201
229
  return guid
202
230
  }
203
231
 
204
- try {
205
- // Fallback to CDP if _guid is not available
206
- const client = await page.context().newCDPSession(page)
207
- const { targetInfo } = await client.send('Target.getTargetInfo')
208
- await client.detach()
209
-
210
- return targetInfo.targetId
211
- } catch (e) {
212
- throw new Error(`Could not get page identifier: ${e}`)
213
- }
232
+ throw new Error('Could not get page identifier: _guid not available')
214
233
  }
215
234
 
216
235
  function setupPageConsoleListener(page: Page) {
@@ -315,6 +334,8 @@ async function resetConnection(): Promise<{ browser: Browser; page: Page; contex
315
334
  // Set up console listener for all existing pages
316
335
  context.pages().forEach((p) => setupPageConsoleListener(p))
317
336
 
337
+ await setDeviceScaleFactorForMacOS(context)
338
+
318
339
  state.browser = browser
319
340
  state.page = page
320
341
  state.context = context
@@ -329,7 +350,7 @@ const server = new McpServer({
329
350
  version: '1.0.0',
330
351
  })
331
352
 
332
- const promptContent = fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'prompt.md'), 'utf-8')
353
+ const promptContent = fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'prompt.md'), 'utf-8') + `\n\nfor debugging errors, check relay server logs at: ${LOG_FILE_PATH}`
333
354
 
334
355
  server.tool(
335
356
  'execute',
@@ -464,20 +485,19 @@ server.tool(
464
485
  throw new Error('getLocatorStringForElement: argument must be a Playwright Locator or ElementHandle')
465
486
  }
466
487
 
467
- return await element.evaluate(async (el: any) => {
468
- const WIN = globalThis as any
469
- if (!WIN.__selectorGenerator) {
470
- const module: SelectorGenerator = await import(
471
- // @ts-ignore
472
- 'https://unpkg.com/@mizchi/selector-generator@1.50.0-next/dist/index.js'
473
- )
474
- WIN.__selectorGenerator = {
475
- createSelectorGenerator: module.createSelectorGenerator,
476
- toLocator: module.toLocator,
477
- }
478
- }
479
- const { createSelectorGenerator, toLocator } = WIN.__selectorGenerator as SelectorGenerator
480
- const generator = createSelectorGenerator(WIN)
488
+ const elementPage = element.page ? element.page() : page
489
+ const hasGenerator = await elementPage.evaluate(() => !!(globalThis as any).__selectorGenerator)
490
+
491
+ if (!hasGenerator) {
492
+ const currentDir = path.dirname(fileURLToPath(import.meta.url))
493
+ const scriptPath = path.join(currentDir, '..', 'dist', 'selector-generator.js')
494
+ const scriptContent = fs.readFileSync(scriptPath, 'utf-8')
495
+ await elementPage.addScriptTag({ content: scriptContent })
496
+ }
497
+
498
+ return await element.evaluate((el: any) => {
499
+ const { createSelectorGenerator, toLocator } = (globalThis as any).__selectorGenerator
500
+ const generator = createSelectorGenerator(globalThis)
481
501
  const result = generator(el)
482
502
  return toLocator(result.selector, 'javascript')
483
503
  })
@@ -516,6 +536,17 @@ server.tool(
516
536
  browserLogs.clear()
517
537
  }
518
538
 
539
+ const getCDPSession = async (options: { page: Page }) => {
540
+ const cached = cdpSessionCache.get(options.page)
541
+ if (cached) {
542
+ return cached
543
+ }
544
+ const wsUrl = getCdpUrl({ port: RELAY_PORT })
545
+ const session = await getCDPSessionForPage({ page: options.page, wsUrl })
546
+ cdpSessionCache.set(options.page, session)
547
+ return session
548
+ }
549
+
519
550
  let vmContextObj: VMContextWithGlobals = {
520
551
  page,
521
552
  context,
@@ -525,6 +556,8 @@ server.tool(
525
556
  getLocatorStringForElement,
526
557
  getLatestLogs,
527
558
  clearAllLogs,
559
+ waitForPageLoad,
560
+ getCDPSession,
528
561
  resetPlaywright: async () => {
529
562
  const { page: newPage, context: newContext } = await resetConnection()
530
563
 
@@ -537,6 +570,8 @@ server.tool(
537
570
  getLocatorStringForElement,
538
571
  getLatestLogs,
539
572
  clearAllLogs,
573
+ waitForPageLoad,
574
+ getCDPSession,
540
575
  resetPlaywright: vmContextObj.resetPlaywright,
541
576
  require,
542
577
  // TODO --experimental-vm-modules is needed to make import work in vm
@@ -668,4 +703,3 @@ async function main() {
668
703
  }
669
704
 
670
705
  main().catch(console.error)
671
-
package/src/prompt.md CHANGED
@@ -52,12 +52,13 @@ IMPORTANT! never call bringToFront unless specifically asked by the user. It is
52
52
  - only call `page.close()` if the user asks you so or if you previously created this page yourself with `newPage`. do not close user created pages unless asked
53
53
  - try to never sleep or run `page.waitForTimeout` unless you have to. there are better ways to wait for an element
54
54
  - never close browser or context. NEVER call `browser.close()`
55
+ - NEVER use `page.context().newCDPSession()` or `browser.newCDPSession()` - these do not work through the playwriter relay. If you need to send raw CDP commands, use the `getCDPSession` utility function instead.
55
56
 
56
57
  ## always check the current page state after an action
57
58
 
58
59
  after you click a button or submit a form you ALWAYS have to then check what is the current state of the page. you cannot assume what happened after doing an action. instead run the following code to know what happened after the action:
59
60
 
60
- `console.log('url:', page.url()); console.log(await accessibilitySnapshot({ page }).then(x => x.slice(0, 1000)));`
61
+ `console.log('url:', page.url()); console.log(await accessibilitySnapshot({ page }).then(x => x.split('\n').slice(0, 30).join('\n')));`
61
62
 
62
63
  if nothing happened you may need to wait before the action completes, using something like `page.waitForNavigation({timeout: 3000})` or `await page.waitForLoadState('networkidle', {timeout: 3000})`
63
64
 
@@ -81,8 +82,25 @@ you have access to some functions in addition to playwright methods:
81
82
  - `page`: (optional) filter logs by a specific page instance. Only returns logs from that page
82
83
  - `count`: (optional) limit number of logs to return. If not specified, returns all available logs
83
84
  - `search`: (optional) string or regex to filter logs. Only returns logs that match
84
-
85
- To bring a tab to front and focus it, use the standard Playwright method `await page.bringToFront()`
85
+ - `waitForPageLoad({ page, timeout, pollInterval, minWait })`: smart network-aware page load detection. Playwright's `networkidle` waits for ALL requests to finish, which often times out on sites with analytics/ads. This function ignores those and returns when meaningful content is loaded.
86
+ - `page`: the page object to wait on
87
+ - `timeout`: (optional) max wait time in ms (default: 30000)
88
+ - `pollInterval`: (optional) how often to check in ms (default: 100)
89
+ - `minWait`: (optional) minimum wait before checking in ms (default: 500)
90
+ - Returns: `{ success, readyState, pendingRequests, waitTimeMs, timedOut }`
91
+ - Filters out: ad networks (doubleclick, googlesyndication), analytics (google-analytics, mixpanel, segment), social (facebook.net, twitter), support widgets (intercom, zendesk), and slow fonts/images
92
+ - `getCDPSession({ page })`: creates a CDP session to send raw Chrome DevTools Protocol commands. Use this instead of `page.context().newCDPSession()` which does not work through the playwriter relay. Sessions are cached per page.
93
+ - `page`: the page object to create the session for
94
+ - Returns: `{ send(method, params?), on(event, callback), off(event, callback) }`
95
+ - Example: `const cdp = await getCDPSession({ page }); const metrics = await cdp.send('Page.getLayoutMetrics');`
96
+ - Example listening for events:
97
+ ```js
98
+ const cdp = await getCDPSession({ page });
99
+ await cdp.send('Debugger.enable');
100
+ const pausedEvent = await new Promise((resolve) => { cdp.on('Debugger.paused', resolve); });
101
+ console.log('Paused at:', pausedEvent.callFrames[0].location);
102
+ await cdp.send('Debugger.resume');
103
+ ```
86
104
 
87
105
  example:
88
106
 
@@ -108,14 +126,24 @@ Then you can use `page.locator(`aria-ref=${ref}`)` to get an element with a spec
108
126
 
109
127
  IMPORTANT: notice that we do not add any quotes in `aria-ref`! it MUST be called without quotes
110
128
 
111
- ## getting selector for a locator identified by snapshot aria-ref
129
+ ## getting a stable selector for an element (getLocatorStringForElement)
130
+
131
+ The `aria-ref` values from accessibility snapshots are ephemeral - they change on page reload and when components remount. Use `getLocatorStringForElement(element)` to get a stable Playwright locator string that you can reuse programmatically.
112
132
 
113
- in some cases you want to get a selector for a locator you just identified using `const element = page.locator('aria-ref=${ref}')`. To do so you can use `await getLocatorStringForElement(element)`. This is useful if you need to find other elements of the same type in a list for example. If you know the selector you can usually change a bit the selector to find the other elements of the same type in the list or table
133
+ This is useful for:
134
+ - Getting a selector you can store and reuse across page reloads
135
+ - Finding similar elements in a list (modify the selector pattern)
136
+ - Debugging which selector Playwright would use for an element
114
137
 
115
138
  ```js
116
- const loc = page.locator('aria-ref=123');
117
- console.log(await getLocatorStringForElement(loc));
118
- // => "getByRole('button', { name: 'Save' })" or similar
139
+ const loc = page.locator('aria-ref=e14');
140
+ const selector = await getLocatorStringForElement(loc);
141
+ console.log(selector);
142
+ // => "getByRole('button', { name: 'Save' })"
143
+
144
+ // use the selector programmatically with eval:
145
+ const stableLocator = page.getByRole('button', { name: 'Save' })
146
+ await stableLocator.click();
119
147
  ```
120
148
 
121
149
  ## finding specific elements with snapshot
@@ -18,124 +18,127 @@ Return value:
18
18
  - /url: /charts/area
19
19
  - link "Directory" [ref=e13] [cursor=pointer]:
20
20
  - /url: /docs/directory
21
- - link "Themes" [ref=e14] [cursor=pointer]:
22
- - /url: /themes
23
- - link "Colors" [ref=e15] [cursor=pointer]:
24
- - /url: /colors
25
- - generic [ref=e16]:
26
- - button "Search documentation... ⌘ K" [ref=e18]:
27
- - generic [ref=e19]: Search documentation...
28
- - generic [ref=e21]:
29
- - generic: ⌘
30
- - generic: K
31
- - link "100.5k" [ref=e22] [cursor=pointer]:
21
+ - link "Create" [ref=e14] [cursor=pointer]:
22
+ - /url: /create
23
+ - generic [ref=e15]:
24
+ - button "Search documentation... ⌘K" [ref=e17]:
25
+ - generic [ref=e18]: Search documentation...
26
+ - generic [ref=e19]:
27
+ - generic: ⌘K
28
+ - link "103k" [ref=e20] [cursor=pointer]:
32
29
  - /url: https://github.com/shadcn-ui/ui
33
30
  - img
34
- - generic [ref=e23]: 100.5k
35
- - button "Toggle theme" [ref=e24]:
31
+ - generic [ref=e21]: 103k
32
+ - button "Toggle theme" [ref=e22]:
36
33
  - img
37
- - generic [ref=e25]: Toggle theme
38
- - main [ref=e26]:
39
- - generic [ref=e27]:
40
- - generic [ref=e30]:
41
- - 'link "New New Components: Field, Input Group, Item and more" [ref=e31] [cursor=pointer]':
34
+ - generic [ref=e23]: Toggle theme
35
+ - link "New Project" [ref=e24] [cursor=pointer]:
36
+ - /url: /create
37
+ - img
38
+ - text: New Project
39
+ - main [ref=e25]:
40
+ - generic [ref=e26]:
41
+ - generic [ref=e29]:
42
+ - link "New npx shadcn create" [ref=e30] [cursor=pointer]:
42
43
  - /url: /docs/changelog
43
- - generic "New" [ref=e32]
44
- - text: "New Components: Field, Input Group, Item and more"
44
+ - generic "New" [ref=e31]
45
+ - text: npx shadcn create
45
46
  - img
46
- - heading "The Foundation for your Design System" [level=1] [ref=e33]
47
- - paragraph [ref=e34]: A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code.
48
- - generic [ref=e35]:
49
- - link "Get Started" [ref=e36] [cursor=pointer]:
50
- - /url: /docs/installation
51
- - link "View Components" [ref=e37] [cursor=pointer]:
47
+ - heading "The Foundation for your Design System" [level=1] [ref=e32]
48
+ - paragraph [ref=e33]: A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code.
49
+ - generic [ref=e34]:
50
+ - link "New Project" [ref=e35] [cursor=pointer]:
51
+ - /url: /create
52
+ - img
53
+ - text: New Project
54
+ - link "View Components" [ref=e36] [cursor=pointer]:
52
55
  - /url: /docs/components
53
- - generic [ref=e39]:
54
- - generic [ref=e44]:
55
- - link "Examples" [ref=e45] [cursor=pointer]:
56
+ - generic [ref=e38]:
57
+ - generic [ref=e43]:
58
+ - link "Examples" [ref=e44] [cursor=pointer]:
56
59
  - /url: /
57
- - link "Dashboard" [ref=e46] [cursor=pointer]:
60
+ - link "Dashboard" [ref=e45] [cursor=pointer]:
58
61
  - /url: /examples/dashboard
59
- - link "Tasks" [ref=e47] [cursor=pointer]:
62
+ - link "Tasks" [ref=e46] [cursor=pointer]:
60
63
  - /url: /examples/tasks
61
- - link "Playground" [ref=e48] [cursor=pointer]:
64
+ - link "Playground" [ref=e47] [cursor=pointer]:
62
65
  - /url: /examples/playground
63
- - link "Authentication" [ref=e49] [cursor=pointer]:
66
+ - link "Authentication" [ref=e48] [cursor=pointer]:
64
67
  - /url: /examples/authentication
65
- - generic [ref=e50]:
66
- - generic [ref=e51]: Theme
67
- - combobox "Theme" [ref=e52]:
68
- - generic [ref=e53]: "Theme:"
68
+ - generic [ref=e49]:
69
+ - generic [ref=e50]: Theme
70
+ - combobox "Theme" [ref=e51]:
71
+ - generic [ref=e52]: "Theme:"
69
72
  - generic: Neutral
70
73
  - img
71
- - button "Copy Code" [ref=e54]:
74
+ - button "Copy Code" [ref=e53]:
72
75
  - img
73
- - generic [ref=e55]: Copy Code
74
- - generic [ref=e59]:
75
- - generic [ref=e63]:
76
- - group "Payment Method" [ref=e64]:
77
- - generic [ref=e65]: Payment Method
78
- - paragraph [ref=e66]: All transactions are secure and encrypted
79
- - generic [ref=e67]:
80
- - group [ref=e68]:
81
- - generic [ref=e69]: Name on Card
82
- - textbox "Name on Card" [ref=e70]:
76
+ - generic [ref=e54]: Copy Code
77
+ - generic [ref=e58]:
78
+ - generic [ref=e62]:
79
+ - group "Payment Method" [ref=e63]:
80
+ - generic [ref=e64]: Payment Method
81
+ - paragraph [ref=e65]: All transactions are secure and encrypted
82
+ - generic [ref=e66]:
83
+ - group [ref=e67]:
84
+ - generic [ref=e68]: Name on Card
85
+ - textbox "Name on Card" [ref=e69]:
83
86
  - /placeholder: John Doe
84
- - generic [ref=e71]:
85
- - group [ref=e72]:
86
- - generic [ref=e73]: Card Number
87
- - textbox "Card Number" [ref=e74]:
87
+ - generic [ref=e70]:
88
+ - group [ref=e71]:
89
+ - generic [ref=e72]: Card Number
90
+ - textbox "Card Number" [ref=e73]:
88
91
  - /placeholder: 1234 5678 9012 3456
89
- - paragraph [ref=e75]: Enter your 16-digit number.
90
- - group [ref=e76]:
91
- - generic [ref=e77]: CVV
92
- - textbox "CVV" [ref=e78]:
92
+ - paragraph [ref=e74]: Enter your 16-digit number.
93
+ - group [ref=e75]:
94
+ - generic [ref=e76]: CVV
95
+ - textbox "CVV" [ref=e77]:
93
96
  - /placeholder: "123"
94
- - generic [ref=e79]:
95
- - group [ref=e80]:
96
- - generic [ref=e81]: Month
97
- - combobox "Month" [ref=e82]:
97
+ - generic [ref=e78]:
98
+ - group [ref=e79]:
99
+ - generic [ref=e80]: Month
100
+ - combobox "Month" [ref=e81]:
98
101
  - generic: MM
99
102
  - img
100
- - combobox [ref=e83]
101
- - group [ref=e84]:
102
- - generic [ref=e85]: Year
103
- - combobox "Year" [ref=e86]:
103
+ - combobox [ref=e82]
104
+ - group [ref=e83]:
105
+ - generic [ref=e84]: Year
106
+ - combobox "Year" [ref=e85]:
104
107
  - generic: YYYY
105
108
  - img
106
- - combobox [ref=e87]
107
- - group "Billing Address" [ref=e89]:
108
- - generic [ref=e90]: Billing Address
109
- - paragraph [ref=e91]: The billing address associated with your payment method
110
- - group [ref=e93]:
111
- - checkbox "Same as shipping address" [checked] [ref=e94]:
109
+ - combobox [ref=e86]
110
+ - group "Billing Address" [ref=e88]:
111
+ - generic [ref=e89]: Billing Address
112
+ - paragraph [ref=e90]: The billing address associated with your payment method
113
+ - group [ref=e92]:
114
+ - checkbox "Same as shipping address" [checked] [ref=e93]:
112
115
  - generic:
113
116
  - img
114
117
  - checkbox [checked]
115
- - generic [ref=e95]: Same as shipping address
116
- - group [ref=e97]:
117
- - group [ref=e99]:
118
- - generic [ref=e100]: Comments
119
- - textbox "Comments" [ref=e101]:
118
+ - generic [ref=e94]: Same as shipping address
119
+ - group [ref=e96]:
120
+ - group [ref=e98]:
121
+ - generic [ref=e99]: Comments
122
+ - textbox "Comments" [ref=e100]:
120
123
  - /placeholder: Add any additional comments
121
- - group [ref=e102]:
122
- - button "Submit" [ref=e103]
123
- - button "Cancel" [ref=e104]
124
- - generic [ref=e105]:
125
- - generic [ref=e106]:
126
- - generic [ref=e107]:
127
- - generic [ref=e109]:
128
- - img "@shadcn" [ref=e111]
129
- - img "@maxleiter" [ref=e113]
130
- - img "@evilrabbit" [ref=e115]
131
- - generic [ref=e116]: No Team Members
132
- - generic [ref=e117]: Invite your team to collaborate on this project.
133
- - button "Invite Members" [ref=e119]:
124
+ - group [ref=e101]:
125
+ - button "Submit" [ref=e102]
126
+ - button "Cancel" [ref=e103]
127
+ - generic [ref=e104]:
128
+ - generic [ref=e105]:
129
+ - generic [ref=e106]:
130
+ - generic [ref=e108]:
131
+ - img "@shadcn" [ref=e110]
132
+ - img "@maxleiter" [ref=e112]
133
+ - img "@evilrabbit" [ref=e114]
134
+ - generic [ref=e115]: No Team Members
135
+ - generic [ref=e116]: Invite your team to collaborate on this project.
136
+ - button "Invite Members" [ref=e118]:
134
137
  - img
135
138
  - text: Invite Members
136
- - generic [ref=e120]:
137
- - generic [ref=e121]:
139
+ - generic [ref=e119]:
140
+ - generic [ref=e120]:
138
141
  - status "Loading"
139
-
142
+
140
143
 
141
144
  [Truncated to 6000 characters. Better manage your logs or paginate them to read the full logs]
@@ -1,26 +1,9 @@
1
1
  import { startPlayWriterCDPRelayServer } from './extension/cdp-relay.js'
2
- import fs from 'node:fs'
3
- import path from 'node:path'
4
- import { fileURLToPath } from 'node:url'
5
- import util from 'node:util'
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
- const logFilePath = path.join(__dirname, '..', 'relay-server.log')
2
+ import { createFileLogger } from './create-logger.js'
9
3
 
10
4
  process.title = 'playwriter-ws-server'
11
- fs.writeFileSync(logFilePath, '')
12
-
13
- const log = (...args: any[]) => {
14
- const message = args.map(arg =>
15
- typeof arg === 'string' ? arg : util.inspect(arg, { depth: null, colors: false })
16
- ).join(' ')
17
- return fs.promises.appendFile(logFilePath, message + '\n')
18
- }
19
5
 
20
- const logger = {
21
- log,
22
- error: log
23
- }
6
+ const logger = createFileLogger()
24
7
 
25
8
  process.on('uncaughtException', async (err) => {
26
9
  await logger.error('Uncaught Exception:', err);
@@ -41,7 +24,7 @@ export async function startServer({ port = 19988 }: { port?: number } = {}) {
41
24
  const server = await startPlayWriterCDPRelayServer({ port, logger })
42
25
 
43
26
  console.log('CDP Relay Server running. Press Ctrl+C to stop.')
44
- console.log('Logs are being written to:', logFilePath)
27
+ console.log('Logs are being written to:', logger.logFilePath)
45
28
 
46
29
  process.on('SIGINT', () => {
47
30
  console.log('\nShutting down...')
package/src/utils.ts CHANGED
@@ -1,4 +1,47 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { xdgData } from 'xdg-basedir'
4
+
1
5
  export function getCdpUrl({ port = 19988, host = '127.0.0.1' }: { port?: number; host?: string } = {}) {
2
6
  const id = `${Math.random().toString(36).substring(2, 15)}_${Date.now()}`
3
7
  return `ws://${host}:${port}/cdp/${id}`
4
8
  }
9
+
10
+ export function getDataDir(): string {
11
+ return path.join(xdgData!, 'playwriter')
12
+ }
13
+
14
+ export function ensureDataDir(): string {
15
+ const dataDir = getDataDir()
16
+ if (!fs.existsSync(dataDir)) {
17
+ fs.mkdirSync(dataDir, { recursive: true })
18
+ }
19
+ return dataDir
20
+ }
21
+
22
+ function getLogsDir(): string {
23
+ return path.join(getDataDir(), 'logs')
24
+ }
25
+
26
+ function getLogFilePath(): string {
27
+ if (process.env.PLAYWRITER_LOG_PATH) {
28
+ return process.env.PLAYWRITER_LOG_PATH
29
+ }
30
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
31
+ return path.join(getLogsDir(), `relay-server-${timestamp}.log`)
32
+ }
33
+
34
+ export const LOG_FILE_PATH = getLogFilePath()
35
+
36
+ // export function getDidPromptReviewPath(): string {
37
+ // return path.join(getDataDir(), 'did-prompt-review')
38
+ // }
39
+
40
+ // export function hasReviewedPrompt(): boolean {
41
+ // return fs.existsSync(getDidPromptReviewPath())
42
+ // }
43
+
44
+ // export function markPromptReviewed(): void {
45
+ // ensureDataDir()
46
+ // fs.writeFileSync(getDidPromptReviewPath(), new Date().toISOString())
47
+ // }