playwriter 0.0.25 → 0.0.29

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 (105) hide show
  1. package/bin.js +1 -1
  2. package/dist/bippy.js +966 -0
  3. package/dist/{extension/cdp-relay.d.ts → cdp-relay.d.ts} +3 -2
  4. package/dist/cdp-relay.d.ts.map +1 -0
  5. package/dist/{extension/cdp-relay.js → cdp-relay.js} +101 -3
  6. package/dist/cdp-relay.js.map +1 -0
  7. package/dist/cdp-session.d.ts +1 -1
  8. package/dist/cdp-session.d.ts.map +1 -1
  9. package/dist/cdp-session.js +4 -4
  10. package/dist/cdp-session.js.map +1 -1
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +71 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/create-logger.d.ts.map +1 -1
  16. package/dist/create-logger.js +2 -1
  17. package/dist/create-logger.js.map +1 -1
  18. package/dist/debugger-examples-types.d.ts +18 -0
  19. package/dist/debugger-examples-types.d.ts.map +1 -0
  20. package/dist/debugger-examples-types.js +2 -0
  21. package/dist/debugger-examples-types.js.map +1 -0
  22. package/dist/debugger-examples.d.ts +6 -0
  23. package/dist/debugger-examples.d.ts.map +1 -0
  24. package/dist/debugger-examples.js +53 -0
  25. package/dist/debugger-examples.js.map +1 -0
  26. package/dist/debugger-examples.ts +66 -0
  27. package/dist/debugger.d.ts +380 -0
  28. package/dist/debugger.d.ts.map +1 -0
  29. package/dist/debugger.js +631 -0
  30. package/dist/debugger.js.map +1 -0
  31. package/dist/editor-examples.d.ts +11 -0
  32. package/dist/editor-examples.d.ts.map +1 -0
  33. package/dist/editor-examples.js +124 -0
  34. package/dist/editor-examples.js.map +1 -0
  35. package/dist/editor.d.ts +203 -0
  36. package/dist/editor.d.ts.map +1 -0
  37. package/dist/editor.js +335 -0
  38. package/dist/editor.js.map +1 -0
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/mcp-client.d.ts +5 -1
  44. package/dist/mcp-client.d.ts.map +1 -1
  45. package/dist/mcp-client.js +13 -9
  46. package/dist/mcp-client.js.map +1 -1
  47. package/dist/mcp.d.ts +4 -1
  48. package/dist/mcp.d.ts.map +1 -1
  49. package/dist/mcp.js +170 -27
  50. package/dist/mcp.js.map +1 -1
  51. package/dist/mcp.test.d.ts.map +1 -1
  52. package/dist/mcp.test.js +886 -182
  53. package/dist/mcp.test.js.map +1 -1
  54. package/dist/prompt.md +86 -6
  55. package/dist/{extension/protocol.d.ts → protocol.d.ts} +1 -1
  56. package/dist/protocol.d.ts.map +1 -0
  57. package/dist/protocol.js.map +1 -0
  58. package/dist/react-source.d.ts +13 -0
  59. package/dist/react-source.d.ts.map +1 -0
  60. package/dist/react-source.js +66 -0
  61. package/dist/react-source.js.map +1 -0
  62. package/dist/selector-generator.js +7065 -18
  63. package/dist/start-relay-server.d.ts +4 -2
  64. package/dist/start-relay-server.d.ts.map +1 -1
  65. package/dist/start-relay-server.js +3 -3
  66. package/dist/start-relay-server.js.map +1 -1
  67. package/dist/styles.d.ts +27 -0
  68. package/dist/styles.d.ts.map +1 -0
  69. package/dist/styles.js +232 -0
  70. package/dist/styles.js.map +1 -0
  71. package/dist/utils.d.ts +3 -1
  72. package/dist/utils.d.ts.map +1 -1
  73. package/dist/utils.js +7 -3
  74. package/dist/utils.js.map +1 -1
  75. package/dist/wait-for-page-load.d.ts.map +1 -1
  76. package/dist/wait-for-page-load.js +3 -2
  77. package/dist/wait-for-page-load.js.map +1 -1
  78. package/package.json +5 -2
  79. package/src/{extension/cdp-relay.ts → cdp-relay.ts} +109 -5
  80. package/src/cdp-session.ts +4 -4
  81. package/src/cdp-timing.md +128 -0
  82. package/src/cli.ts +85 -0
  83. package/src/create-logger.ts +2 -1
  84. package/src/debugger-examples-types.ts +10 -0
  85. package/src/debugger-examples.ts +66 -0
  86. package/src/debugger.ts +711 -0
  87. package/src/editor-examples.ts +148 -0
  88. package/src/editor.ts +389 -0
  89. package/src/index.ts +1 -1
  90. package/src/mcp-client.ts +14 -9
  91. package/src/mcp.test.ts +1053 -196
  92. package/src/mcp.ts +195 -30
  93. package/src/prompt.md +86 -6
  94. package/src/{extension/protocol.ts → protocol.ts} +1 -1
  95. package/src/react-source.ts +92 -0
  96. package/src/snapshots/shadcn-ui-accessibility.md +57 -57
  97. package/src/start-relay-server.ts +3 -3
  98. package/src/styles.ts +343 -0
  99. package/src/utils.ts +8 -3
  100. package/src/wait-for-page-load.ts +3 -2
  101. package/dist/extension/cdp-relay.d.ts.map +0 -1
  102. package/dist/extension/cdp-relay.js.map +0 -1
  103. package/dist/extension/protocol.d.ts.map +0 -1
  104. package/dist/extension/protocol.js.map +0 -1
  105. /package/dist/{extension/protocol.js → protocol.js} +0 -0
@@ -0,0 +1,148 @@
1
+ import { page, getCDPSession, createEditor, console } from './debugger-examples-types.js'
2
+
3
+ // Example: List available scripts
4
+ async function listScripts() {
5
+ const cdp = await getCDPSession({ page })
6
+ const editor = createEditor({ cdp })
7
+ await editor.enable()
8
+
9
+ const scripts = editor.list({ pattern: /app/ })
10
+ console.log(scripts)
11
+ }
12
+
13
+ // Example: Read a script with line numbers
14
+ async function readScript() {
15
+ const cdp = await getCDPSession({ page })
16
+ const editor = createEditor({ cdp })
17
+ await editor.enable()
18
+
19
+ const { content, totalLines } = await editor.read({
20
+ url: 'https://example.com/app.js',
21
+ })
22
+ console.log('Total lines:', totalLines)
23
+ console.log(content)
24
+
25
+ const { content: partial } = await editor.read({
26
+ url: 'https://example.com/app.js',
27
+ offset: 100,
28
+ limit: 50,
29
+ })
30
+ console.log(partial)
31
+ }
32
+
33
+ // Example: Edit a script (exact string replacement)
34
+ async function editScript() {
35
+ const cdp = await getCDPSession({ page })
36
+ const editor = createEditor({ cdp })
37
+ await editor.enable()
38
+
39
+ await editor.edit({
40
+ url: 'https://example.com/app.js',
41
+ oldString: 'const DEBUG = false',
42
+ newString: 'const DEBUG = true',
43
+ })
44
+
45
+ const dryRunResult = await editor.edit({
46
+ url: 'https://example.com/app.js',
47
+ oldString: 'old code',
48
+ newString: 'new code',
49
+ dryRun: true,
50
+ })
51
+ console.log('Dry run result:', dryRunResult)
52
+ }
53
+
54
+ // Example: Search across all scripts
55
+ async function searchScripts() {
56
+ const cdp = await getCDPSession({ page })
57
+ const editor = createEditor({ cdp })
58
+ await editor.enable()
59
+
60
+ const matches = await editor.grep({ regex: /console\.log/ })
61
+ console.log(matches)
62
+
63
+ const todoMatches = await editor.grep({
64
+ regex: /TODO|FIXME/i,
65
+ pattern: /app/,
66
+ })
67
+ console.log(todoMatches)
68
+ }
69
+
70
+ // Example: Write entire script content
71
+ async function writeScript() {
72
+ const cdp = await getCDPSession({ page })
73
+ const editor = createEditor({ cdp })
74
+ await editor.enable()
75
+
76
+ const { content } = await editor.read({ url: 'https://example.com/app.js' })
77
+ const newContent = content.replace(/console\.log/g, 'console.debug')
78
+
79
+ await editor.write({
80
+ url: 'https://example.com/app.js',
81
+ content: newContent,
82
+ })
83
+ }
84
+
85
+ // Example: Edit an inline script (scripts without URL get inline://{id} URLs)
86
+ async function editInlineScript() {
87
+ const cdp = await getCDPSession({ page })
88
+ const editor = createEditor({ cdp })
89
+ await editor.enable()
90
+
91
+ const matches = await editor.grep({ regex: /myFunction/ })
92
+ if (matches.length > 0) {
93
+ const { url } = matches[0]
94
+ console.log('Found in:', url)
95
+
96
+ await editor.edit({
97
+ url,
98
+ oldString: 'return false',
99
+ newString: 'return true',
100
+ })
101
+ }
102
+ }
103
+
104
+ // Example: List and read CSS stylesheets
105
+ async function readStylesheet() {
106
+ const cdp = await getCDPSession({ page })
107
+ const editor = createEditor({ cdp })
108
+ await editor.enable()
109
+
110
+ const stylesheets = await editor.list({ pattern: /\.css/ })
111
+ console.log('Stylesheets:', stylesheets)
112
+
113
+ if (stylesheets.length > 0) {
114
+ const { content, totalLines } = await editor.read({
115
+ url: stylesheets[0],
116
+ })
117
+ console.log('Total lines:', totalLines)
118
+ console.log(content)
119
+ }
120
+ }
121
+
122
+ // Example: Edit a CSS stylesheet
123
+ async function editStylesheet() {
124
+ const cdp = await getCDPSession({ page })
125
+ const editor = createEditor({ cdp })
126
+ await editor.enable()
127
+
128
+ await editor.edit({
129
+ url: 'https://example.com/styles.css',
130
+ oldString: 'color: red',
131
+ newString: 'color: blue',
132
+ })
133
+ }
134
+
135
+ // Example: Search CSS for specific properties
136
+ async function searchStyles() {
137
+ const cdp = await getCDPSession({ page })
138
+ const editor = createEditor({ cdp })
139
+ await editor.enable()
140
+
141
+ const matches = await editor.grep({
142
+ regex: /background-color/,
143
+ pattern: /\.css/,
144
+ })
145
+ console.log(matches)
146
+ }
147
+
148
+ export { listScripts, readScript, editScript, searchScripts, writeScript, editInlineScript, readStylesheet, editStylesheet, searchStyles }
package/src/editor.ts ADDED
@@ -0,0 +1,389 @@
1
+ import type { CDPSession } from './cdp-session.js'
2
+
3
+ export interface ReadResult {
4
+ content: string
5
+ totalLines: number
6
+ startLine: number
7
+ endLine: number
8
+ }
9
+
10
+ export interface SearchMatch {
11
+ url: string
12
+ lineNumber: number
13
+ lineContent: string
14
+ }
15
+
16
+ export interface EditResult {
17
+ success: boolean
18
+ stackChanged?: boolean
19
+ }
20
+
21
+ /**
22
+ * A class for viewing and editing web page scripts via Chrome DevTools Protocol.
23
+ * Provides a Claude Code-like interface: list, read, edit, grep.
24
+ *
25
+ * Edits are in-memory only and persist until page reload. They modify the running
26
+ * V8 instance but are not saved to disk or server.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const cdp = await getCDPSession({ page })
31
+ * const editor = new Editor({ cdp })
32
+ * await editor.enable()
33
+ *
34
+ * // List available scripts
35
+ * const scripts = editor.list({ search: 'app' })
36
+ *
37
+ * // Read a script
38
+ * const { content } = await editor.read({ url: 'https://example.com/app.js' })
39
+ *
40
+ * // Edit a script
41
+ * await editor.edit({
42
+ * url: 'https://example.com/app.js',
43
+ * oldString: 'console.log("old")',
44
+ * newString: 'console.log("new")'
45
+ * })
46
+ * ```
47
+ */
48
+ export class Editor {
49
+ private cdp: CDPSession
50
+ private enabled = false
51
+ private scripts = new Map<string, string>()
52
+ private stylesheets = new Map<string, string>()
53
+ private sourceCache = new Map<string, string>()
54
+
55
+ constructor({ cdp }: { cdp: CDPSession }) {
56
+ this.cdp = cdp
57
+ this.setupEventListeners()
58
+ }
59
+
60
+ private setupEventListeners() {
61
+ this.cdp.on('Debugger.scriptParsed', (params) => {
62
+ if (!params.url.startsWith('chrome') && !params.url.startsWith('devtools')) {
63
+ const url = params.url || `inline://${params.scriptId}`
64
+ this.scripts.set(url, params.scriptId)
65
+ this.sourceCache.delete(params.scriptId)
66
+ }
67
+ })
68
+
69
+ this.cdp.on('CSS.styleSheetAdded', (params) => {
70
+ const header = params.header
71
+ if (header.sourceURL?.startsWith('chrome') || header.sourceURL?.startsWith('devtools')) {
72
+ return
73
+ }
74
+ const url = header.sourceURL || `inline-css://${header.styleSheetId}`
75
+ this.stylesheets.set(url, header.styleSheetId)
76
+ this.sourceCache.delete(header.styleSheetId)
77
+ })
78
+ }
79
+
80
+ /**
81
+ * Enables the editor. Must be called before other methods.
82
+ * Scripts are collected from Debugger.scriptParsed events.
83
+ * Reload the page after enabling to capture all scripts.
84
+ */
85
+ async enable(): Promise<void> {
86
+ if (this.enabled) {
87
+ return
88
+ }
89
+ await this.cdp.send('Debugger.disable')
90
+ await this.cdp.send('CSS.disable')
91
+ this.scripts.clear()
92
+ this.stylesheets.clear()
93
+ this.sourceCache.clear()
94
+ const resourcesReady = new Promise<void>((resolve) => {
95
+ let timeout: ReturnType<typeof setTimeout>
96
+ const listener = () => {
97
+ clearTimeout(timeout)
98
+ timeout = setTimeout(() => {
99
+ this.cdp.off('Debugger.scriptParsed', listener)
100
+ this.cdp.off('CSS.styleSheetAdded', listener)
101
+ resolve()
102
+ }, 100)
103
+ }
104
+ this.cdp.on('Debugger.scriptParsed', listener)
105
+ this.cdp.on('CSS.styleSheetAdded', listener)
106
+ timeout = setTimeout(() => {
107
+ this.cdp.off('Debugger.scriptParsed', listener)
108
+ this.cdp.off('CSS.styleSheetAdded', listener)
109
+ resolve()
110
+ }, 100)
111
+ })
112
+ await this.cdp.send('Debugger.enable')
113
+ await this.cdp.send('DOM.enable')
114
+ await this.cdp.send('CSS.enable')
115
+ await resourcesReady
116
+ this.enabled = true
117
+ }
118
+
119
+ private getIdByUrl(url: string): { scriptId: string } | { styleSheetId: string } {
120
+ const scriptId = this.scripts.get(url)
121
+ if (scriptId) {
122
+ return { scriptId }
123
+ }
124
+ const styleSheetId = this.stylesheets.get(url)
125
+ if (styleSheetId) {
126
+ return { styleSheetId }
127
+ }
128
+ const allUrls = [...Array.from(this.scripts.keys()), ...Array.from(this.stylesheets.keys())]
129
+ const available = allUrls.slice(0, 5)
130
+ throw new Error(`Resource not found: ${url}\nAvailable: ${available.join(', ')}${allUrls.length > 5 ? '...' : ''}`)
131
+ }
132
+
133
+ /**
134
+ * Lists available script and stylesheet URLs. Use pattern to filter by regex.
135
+ * Automatically enables the editor if not already enabled.
136
+ *
137
+ * @param options - Options
138
+ * @param options.pattern - Optional regex to filter URLs
139
+ * @returns Array of URLs
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * // List all scripts and stylesheets
144
+ * const urls = await editor.list()
145
+ *
146
+ * // List only JS files
147
+ * const jsFiles = await editor.list({ pattern: /\.js/ })
148
+ *
149
+ * // List only CSS files
150
+ * const cssFiles = await editor.list({ pattern: /\.css/ })
151
+ *
152
+ * // Search for specific scripts
153
+ * const appScripts = await editor.list({ pattern: /app/ })
154
+ * ```
155
+ */
156
+ async list({ pattern }: { pattern?: RegExp } = {}): Promise<string[]> {
157
+ await this.enable()
158
+ const urls = [...Array.from(this.scripts.keys()), ...Array.from(this.stylesheets.keys())]
159
+
160
+ if (!pattern) {
161
+ return urls
162
+ }
163
+ return urls.filter((url) => {
164
+ const matches = pattern.test(url)
165
+ pattern.lastIndex = 0
166
+ return matches
167
+ })
168
+ }
169
+
170
+ /**
171
+ * Reads a script or stylesheet's source code by URL.
172
+ * Returns line-numbered content like Claude Code's Read tool.
173
+ * For inline scripts, use the `inline://` URL from list() or grep().
174
+ *
175
+ * @param options - Options
176
+ * @param options.url - Script or stylesheet URL (inline scripts have `inline://{id}` URLs)
177
+ * @param options.offset - Line number to start from (0-based, default 0)
178
+ * @param options.limit - Number of lines to return (default 2000)
179
+ * @returns Content with line numbers, total lines, and range info
180
+ *
181
+ * @example
182
+ * ```ts
183
+ * // Read by URL
184
+ * const { content, totalLines } = await editor.read({
185
+ * url: 'https://example.com/app.js'
186
+ * })
187
+ *
188
+ * // Read a CSS file
189
+ * const { content } = await editor.read({ url: 'https://example.com/styles.css' })
190
+ *
191
+ * // Read lines 100-200
192
+ * const { content } = await editor.read({
193
+ * url: 'https://example.com/app.js',
194
+ * offset: 100,
195
+ * limit: 100
196
+ * })
197
+ * ```
198
+ */
199
+ async read({ url, offset = 0, limit = 2000 }: { url: string; offset?: number; limit?: number }): Promise<ReadResult> {
200
+ await this.enable()
201
+ const id = this.getIdByUrl(url)
202
+ const source = await this.getSource(id)
203
+
204
+ const lines = source.split('\n')
205
+ const totalLines = lines.length
206
+ const startLine = Math.min(offset, totalLines)
207
+ const endLine = Math.min(offset + limit, totalLines)
208
+ const selectedLines = lines.slice(startLine, endLine)
209
+
210
+ const content = selectedLines.map((line, i) => `${String(startLine + i + 1).padStart(5)}| ${line}`).join('\n')
211
+
212
+ return {
213
+ content,
214
+ totalLines,
215
+ startLine: startLine + 1,
216
+ endLine,
217
+ }
218
+ }
219
+
220
+ private async getSource(id: { scriptId: string } | { styleSheetId: string }): Promise<string> {
221
+ if ('styleSheetId' in id) {
222
+ const cached = this.sourceCache.get(id.styleSheetId)
223
+ if (cached) {
224
+ return cached
225
+ }
226
+ const response = await this.cdp.send('CSS.getStyleSheetText', { styleSheetId: id.styleSheetId })
227
+ this.sourceCache.set(id.styleSheetId, response.text)
228
+ return response.text
229
+ }
230
+ const cached = this.sourceCache.get(id.scriptId)
231
+ if (cached) {
232
+ return cached
233
+ }
234
+ const response = await this.cdp.send('Debugger.getScriptSource', { scriptId: id.scriptId })
235
+ this.sourceCache.set(id.scriptId, response.scriptSource)
236
+ return response.scriptSource
237
+ }
238
+
239
+ /**
240
+ * Edits a script or stylesheet by replacing oldString with newString.
241
+ * Like Claude Code's Edit tool - performs exact string replacement.
242
+ *
243
+ * @param options - Options
244
+ * @param options.url - Script or stylesheet URL (inline scripts have `inline://{id}` URLs)
245
+ * @param options.oldString - Exact string to find and replace
246
+ * @param options.newString - Replacement string
247
+ * @param options.dryRun - If true, validate without applying (default false)
248
+ * @returns Result with success status
249
+ *
250
+ * @example
251
+ * ```ts
252
+ * // Replace a string in JS
253
+ * await editor.edit({
254
+ * url: 'https://example.com/app.js',
255
+ * oldString: 'const DEBUG = false',
256
+ * newString: 'const DEBUG = true'
257
+ * })
258
+ *
259
+ * // Edit CSS
260
+ * await editor.edit({
261
+ * url: 'https://example.com/styles.css',
262
+ * oldString: 'color: red',
263
+ * newString: 'color: blue'
264
+ * })
265
+ * ```
266
+ */
267
+ async edit({
268
+ url,
269
+ oldString,
270
+ newString,
271
+ dryRun = false,
272
+ }: {
273
+ url: string
274
+ oldString: string
275
+ newString: string
276
+ dryRun?: boolean
277
+ }): Promise<EditResult> {
278
+ await this.enable()
279
+ const id = this.getIdByUrl(url)
280
+ const source = await this.getSource(id)
281
+
282
+ const matchCount = source.split(oldString).length - 1
283
+ if (matchCount === 0) {
284
+ throw new Error(`oldString not found in ${url}`)
285
+ }
286
+ if (matchCount > 1) {
287
+ throw new Error(`oldString found ${matchCount} times in ${url}. Provide more context to make it unique.`)
288
+ }
289
+
290
+ const newSource = source.replace(oldString, newString)
291
+ return this.setSource(id, newSource, dryRun)
292
+ }
293
+
294
+ private async setSource(
295
+ id: { scriptId: string } | { styleSheetId: string },
296
+ content: string,
297
+ dryRun = false
298
+ ): Promise<EditResult> {
299
+ if ('styleSheetId' in id) {
300
+ await this.cdp.send('CSS.setStyleSheetText', { styleSheetId: id.styleSheetId, text: content })
301
+ if (!dryRun) {
302
+ this.sourceCache.set(id.styleSheetId, content)
303
+ }
304
+ return { success: true }
305
+ }
306
+ const response = await this.cdp.send('Debugger.setScriptSource', {
307
+ scriptId: id.scriptId,
308
+ scriptSource: content,
309
+ dryRun,
310
+ })
311
+ if (!dryRun) {
312
+ this.sourceCache.set(id.scriptId, content)
313
+ }
314
+ return { success: true, stackChanged: response.stackChanged }
315
+ }
316
+
317
+ /**
318
+ * Searches for a regex across all scripts and stylesheets.
319
+ * Like Claude Code's Grep tool - returns matching lines with context.
320
+ *
321
+ * @param options - Options
322
+ * @param options.regex - Regular expression to search for in file contents
323
+ * @param options.pattern - Optional regex to filter which URLs to search
324
+ * @returns Array of matches with url, line number, and line content
325
+ *
326
+ * @example
327
+ * ```ts
328
+ * // Search all scripts and stylesheets for "color"
329
+ * const matches = await editor.grep({ regex: /color/ })
330
+ *
331
+ * // Search only CSS files
332
+ * const matches = await editor.grep({
333
+ * regex: /background-color/,
334
+ * pattern: /\.css/
335
+ * })
336
+ *
337
+ * // Regex search for console methods in JS
338
+ * const matches = await editor.grep({
339
+ * regex: /console\.(log|error|warn)/,
340
+ * pattern: /\.js/
341
+ * })
342
+ * ```
343
+ */
344
+ async grep({ regex, pattern }: { regex: RegExp; pattern?: RegExp }): Promise<SearchMatch[]> {
345
+ await this.enable()
346
+
347
+ const matches: SearchMatch[] = []
348
+ const urls = await this.list({ pattern })
349
+
350
+ for (const url of urls) {
351
+ let source: string
352
+ try {
353
+ const id = this.getIdByUrl(url)
354
+ source = await this.getSource(id)
355
+ } catch {
356
+ continue
357
+ }
358
+
359
+ const lines = source.split('\n')
360
+ for (let i = 0; i < lines.length; i++) {
361
+ if (regex.test(lines[i])) {
362
+ matches.push({
363
+ url,
364
+ lineNumber: i + 1,
365
+ lineContent: lines[i].trim().slice(0, 200),
366
+ })
367
+ regex.lastIndex = 0
368
+ }
369
+ }
370
+ }
371
+
372
+ return matches
373
+ }
374
+
375
+ /**
376
+ * Writes entire content to a script or stylesheet, replacing all existing code.
377
+ * Use with caution - prefer edit() for targeted changes.
378
+ *
379
+ * @param options - Options
380
+ * @param options.url - Script or stylesheet URL (inline scripts have `inline://{id}` URLs)
381
+ * @param options.content - New content
382
+ * @param options.dryRun - If true, validate without applying (default false, only works for JS)
383
+ */
384
+ async write({ url, content, dryRun = false }: { url: string; content: string; dryRun?: boolean }): Promise<EditResult> {
385
+ await this.enable()
386
+ const id = this.getIdByUrl(url)
387
+ return this.setSource(id, content, dryRun)
388
+ }
389
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export * from './extension/cdp-relay.js'
1
+ export * from './cdp-relay.js'
2
2
  export * from './utils.js'
package/src/mcp-client.ts CHANGED
@@ -9,23 +9,28 @@ const __filename = url.fileURLToPath(import.meta.url)
9
9
 
10
10
  export interface CreateTransportOptions {
11
11
  clientName?: string
12
+ port?: number
12
13
  }
13
14
 
14
- export async function createTransport(args: string[]): Promise<{
15
+ export async function createTransport({ args = [], port }: { args?: string[]; port?: number } = {}): Promise<{
15
16
  transport: Transport
16
17
  stderr: Stream | null
17
18
  }> {
19
+ const env: Record<string, string> = {
20
+ ...process.env as Record<string, string>,
21
+ DEBUG: 'playwriter:mcp:test',
22
+ DEBUG_COLORS: '0',
23
+ DEBUG_HIDE_DATE: '1',
24
+ }
25
+ if (port) {
26
+ env.PLAYWRITER_PORT = String(port)
27
+ }
18
28
  const transport = new StdioClientTransport({
19
29
  command: 'pnpm',
20
- args: ['vite-node', path.join(path.dirname(__filename), 'mcp.ts'), ...args],
30
+ args: ['vite-node', path.join(path.dirname(__filename), 'cli.ts'), ...args],
21
31
  cwd: path.join(path.dirname(__filename), '..'),
22
32
  stderr: 'pipe',
23
- env: {
24
- ...process.env,
25
- DEBUG: 'playwriter:mcp:test',
26
- DEBUG_COLORS: '0',
27
- DEBUG_HIDE_DATE: '1',
28
- },
33
+ env,
29
34
  })
30
35
 
31
36
  return {
@@ -44,7 +49,7 @@ export async function createMCPClient(options?: CreateTransportOptions): Promise
44
49
  version: '1.0.0',
45
50
  })
46
51
 
47
- const { transport, stderr } = await createTransport([])
52
+ const { transport, stderr } = await createTransport({ port: options?.port })
48
53
 
49
54
  let stderrBuffer = ''
50
55
  stderr?.on('data', (data) => {