playwriter 0.0.62 → 0.0.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bippy.js +1 -1
- package/dist/cdp-relay.d.ts.map +1 -1
- package/dist/cdp-relay.js +58 -0
- package/dist/cdp-relay.js.map +1 -1
- package/dist/cli.js +26 -6
- package/dist/cli.js.map +1 -1
- package/dist/create-logger.js +1 -1
- package/dist/create-logger.js.map +1 -1
- package/dist/executor.d.ts +2 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +22 -7
- package/dist/executor.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +6 -0
- package/dist/mcp.js.map +1 -1
- package/dist/prompt.md +208 -12
- package/dist/readability.js +1 -1
- package/dist/relay-client.d.ts +15 -0
- package/dist/relay-client.d.ts.map +1 -1
- package/dist/relay-client.js +16 -1
- package/dist/relay-client.js.map +1 -1
- package/dist/selector-generator.js +1 -1
- package/package.json +1 -1
- package/src/aria-snapshots/hackernews-interactive.txt +241 -237
- package/src/aria-snapshots/hackernews-raw.txt +269 -264
- package/src/assets/aria-labels-github.png +0 -0
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/cdp-relay.ts +67 -0
- package/src/cli.ts +31 -6
- package/src/create-logger.ts +1 -1
- package/src/executor.ts +27 -11
- package/src/mcp.ts +8 -0
- package/src/relay-client.ts +19 -3
- package/src/skill.md +208 -12
|
Binary file
|
|
Binary file
|
package/src/cdp-relay.ts
CHANGED
|
@@ -17,6 +17,13 @@ import type {
|
|
|
17
17
|
IsRecordingParams,
|
|
18
18
|
} from './protocol.js'
|
|
19
19
|
import pc from 'picocolors'
|
|
20
|
+
import util from 'node:util'
|
|
21
|
+
|
|
22
|
+
// Prevent Buffers from dumping hex bytes in util.inspect output.
|
|
23
|
+
Buffer.prototype[util.inspect.custom] = function () {
|
|
24
|
+
return `<Buffer ${this.length} bytes>`
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
import { EventEmitter } from 'node:events'
|
|
21
28
|
import { VERSION, EXTENSION_IDS } from './utils.js'
|
|
22
29
|
import { createCdpLogger, type CdpLogEntry, type CdpLogger } from './cdp-log.js'
|
|
@@ -71,6 +78,8 @@ type ExtensionInfo = {
|
|
|
71
78
|
browser?: string
|
|
72
79
|
email?: string
|
|
73
80
|
id?: string
|
|
81
|
+
/** playwriter package version the extension was built with (sent as ?v= query param) */
|
|
82
|
+
version?: string
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
type ExtensionConnection = {
|
|
@@ -675,6 +684,7 @@ export async function startPlayWriterCDPRelayServer({
|
|
|
675
684
|
activeTargets,
|
|
676
685
|
browser: info?.browser || null,
|
|
677
686
|
profile: info ? { email: info.email || '', id: info.id || '' } : null,
|
|
687
|
+
playwriterVersion: info?.version || null,
|
|
678
688
|
})
|
|
679
689
|
})
|
|
680
690
|
|
|
@@ -686,6 +696,7 @@ export async function startPlayWriterCDPRelayServer({
|
|
|
686
696
|
browser: extension.info.browser || null,
|
|
687
697
|
profile: extension.info ? { email: extension.info.email || '', id: extension.info.id || '' } : null,
|
|
688
698
|
activeTargets: extension.connectedTargets.size,
|
|
699
|
+
playwriterVersion: extension.info?.version || null,
|
|
689
700
|
}
|
|
690
701
|
})
|
|
691
702
|
return c.json({ extensions })
|
|
@@ -1002,10 +1013,12 @@ export async function startPlayWriterCDPRelayServer({
|
|
|
1002
1013
|
const browser = c.req.query('browser')
|
|
1003
1014
|
const email = c.req.query('email')
|
|
1004
1015
|
const id = c.req.query('id')
|
|
1016
|
+
const version = c.req.query('v')
|
|
1005
1017
|
return {
|
|
1006
1018
|
browser: browser || undefined,
|
|
1007
1019
|
email: email || undefined,
|
|
1008
1020
|
id: id || undefined,
|
|
1021
|
+
version: version || undefined,
|
|
1009
1022
|
}
|
|
1010
1023
|
}
|
|
1011
1024
|
|
|
@@ -1428,6 +1441,60 @@ export async function startPlayWriterCDPRelayServer({
|
|
|
1428
1441
|
return executorManager
|
|
1429
1442
|
}
|
|
1430
1443
|
|
|
1444
|
+
// ============================================================================
|
|
1445
|
+
// Security middleware for privileged HTTP routes (/cli/*, /recording/*)
|
|
1446
|
+
//
|
|
1447
|
+
// CORS alone does NOT prevent cross-origin POST attacks. Browsers skip the
|
|
1448
|
+
// preflight for "simple" requests (POST + Content-Type: text/plain), so a
|
|
1449
|
+
// malicious website can fire-and-forget a POST to localhost:19988/cli/execute
|
|
1450
|
+
// and the code executes before CORS even enters the picture.
|
|
1451
|
+
//
|
|
1452
|
+
// Two layers of defense:
|
|
1453
|
+
// 1. Sec-Fetch-Site: browsers set this forbidden header on every request.
|
|
1454
|
+
// If present and not "same-origin"/"none", it's a cross-origin browser
|
|
1455
|
+
// request → reject. Node.js clients don't send it → unaffected.
|
|
1456
|
+
// 2. Content-Type must be application/json on POST. This forces a CORS
|
|
1457
|
+
// preflight as a fallback, which our CORS policy already blocks.
|
|
1458
|
+
// 3. When token mode is enabled (remote access), require the token.
|
|
1459
|
+
// ============================================================================
|
|
1460
|
+
const privilegedRouteMiddleware = async (c: Parameters<Parameters<typeof app.use>[1]>[0], next: () => Promise<void>) => {
|
|
1461
|
+
// Block cross-origin browser requests via Sec-Fetch-Site header.
|
|
1462
|
+
// Browsers always set this forbidden header; it cannot be spoofed.
|
|
1463
|
+
// Non-browser clients (Node.js, curl, MCP) don't send it.
|
|
1464
|
+
const secFetchSite = c.req.header('sec-fetch-site')
|
|
1465
|
+
if (secFetchSite && secFetchSite !== 'same-origin' && secFetchSite !== 'none') {
|
|
1466
|
+
logger?.log(pc.red(`Rejecting ${c.req.path}: cross-origin browser request (Sec-Fetch-Site: ${secFetchSite})`))
|
|
1467
|
+
return c.text('Forbidden - Cross-origin requests not allowed', 403)
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// Require application/json on POST to force CORS preflight as backup defense.
|
|
1471
|
+
// A text/plain POST is a "simple request" that skips preflight entirely.
|
|
1472
|
+
if (c.req.method === 'POST') {
|
|
1473
|
+
const contentType = c.req.header('content-type') || ''
|
|
1474
|
+
if (!contentType.includes('application/json')) {
|
|
1475
|
+
logger?.log(pc.red(`Rejecting ${c.req.path}: Content-Type must be application/json, got: ${contentType}`))
|
|
1476
|
+
return c.text('Content-Type must be application/json', 415)
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// When token mode is enabled (remote/serve mode), require authentication.
|
|
1481
|
+
if (token) {
|
|
1482
|
+
const authHeader = c.req.header('authorization') || ''
|
|
1483
|
+
const bearerToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null
|
|
1484
|
+
const url = new URL(c.req.url, 'http://localhost')
|
|
1485
|
+
const queryToken = url.searchParams.get('token')
|
|
1486
|
+
if (bearerToken !== token && queryToken !== token) {
|
|
1487
|
+
logger?.log(pc.red(`Rejecting ${c.req.path}: invalid or missing token`))
|
|
1488
|
+
return c.text('Unauthorized', 401)
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
return next()
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
app.use('/cli/*', privilegedRouteMiddleware)
|
|
1496
|
+
app.use('/recording/*', privilegedRouteMiddleware)
|
|
1497
|
+
|
|
1431
1498
|
app.post('/cli/execute', async (c) => {
|
|
1432
1499
|
try {
|
|
1433
1500
|
const body = await c.req.json() as { sessionId: string | number; code: string; timeout?: number }
|
package/src/cli.ts
CHANGED
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'node:fs'
|
|
4
4
|
import path from 'node:path'
|
|
5
|
+
import util from 'node:util'
|
|
5
6
|
import { fileURLToPath } from 'node:url'
|
|
6
7
|
import { cac } from '@xmorse/cac'
|
|
8
|
+
|
|
9
|
+
// Prevent Buffers from dumping hex bytes in util.inspect output.
|
|
10
|
+
Buffer.prototype[util.inspect.custom] = function () {
|
|
11
|
+
return `<Buffer ${this.length} bytes>`
|
|
12
|
+
}
|
|
7
13
|
import { killPortProcess } from './kill-port.js'
|
|
8
14
|
import { VERSION, LOG_FILE_PATH, LOG_CDP_FILE_PATH, parseRelayHost } from './utils.js'
|
|
9
|
-
import { ensureRelayServer, RELAY_PORT, waitForExtension } from './relay-client.js'
|
|
15
|
+
import { ensureRelayServer, RELAY_PORT, waitForExtension, getExtensionOutdatedWarning, getExtensionStatus } from './relay-client.js'
|
|
10
16
|
|
|
11
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
12
18
|
|
|
@@ -20,6 +26,7 @@ type ExtensionStatus = {
|
|
|
20
26
|
browser: string | null
|
|
21
27
|
profile: { email: string; id: string } | null
|
|
22
28
|
activeTargets: number
|
|
29
|
+
playwriterVersion?: string | null
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
cli
|
|
@@ -74,22 +81,24 @@ async function fetchExtensionsStatus(host?: string): Promise<ExtensionStatus[]>
|
|
|
74
81
|
activeTargets: number
|
|
75
82
|
browser: string | null
|
|
76
83
|
profile: { email: string; id: string } | null
|
|
84
|
+
playwriterVersion?: string | null
|
|
77
85
|
}
|
|
78
|
-
if (!fallbackData
|
|
86
|
+
if (!fallbackData?.connected) {
|
|
79
87
|
return []
|
|
80
88
|
}
|
|
81
89
|
return [{
|
|
82
90
|
extensionId: 'default',
|
|
83
91
|
stableKey: undefined,
|
|
84
|
-
browser: fallbackData
|
|
85
|
-
profile: fallbackData
|
|
86
|
-
activeTargets: fallbackData
|
|
92
|
+
browser: fallbackData?.browser,
|
|
93
|
+
profile: fallbackData?.profile,
|
|
94
|
+
activeTargets: fallbackData?.activeTargets,
|
|
95
|
+
playwriterVersion: fallbackData?.playwriterVersion || null,
|
|
87
96
|
}]
|
|
88
97
|
}
|
|
89
98
|
const data = await response.json() as {
|
|
90
99
|
extensions: ExtensionStatus[]
|
|
91
100
|
}
|
|
92
|
-
return data
|
|
101
|
+
return data?.extensions || []
|
|
93
102
|
} catch {
|
|
94
103
|
return []
|
|
95
104
|
}
|
|
@@ -126,6 +135,13 @@ async function executeCode(options: {
|
|
|
126
135
|
}
|
|
127
136
|
}
|
|
128
137
|
|
|
138
|
+
// Warn once if extension is outdated
|
|
139
|
+
const extensionStatus = await getExtensionStatus()
|
|
140
|
+
const outdatedWarning = getExtensionOutdatedWarning(extensionStatus?.playwriterVersion)
|
|
141
|
+
if (outdatedWarning) {
|
|
142
|
+
console.error(outdatedWarning)
|
|
143
|
+
}
|
|
144
|
+
|
|
129
145
|
// Build request URL with token if provided
|
|
130
146
|
const executeUrl = `${serverUrl}/cli/execute`
|
|
131
147
|
|
|
@@ -197,6 +213,15 @@ cli
|
|
|
197
213
|
process.exit(1)
|
|
198
214
|
}
|
|
199
215
|
|
|
216
|
+
// Warn if any connected extension was built with an older playwriter version
|
|
217
|
+
for (const ext of extensions) {
|
|
218
|
+
const warning = getExtensionOutdatedWarning(ext.playwriterVersion)
|
|
219
|
+
if (warning) {
|
|
220
|
+
console.error(warning)
|
|
221
|
+
break
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
200
225
|
let selectedExtension: ExtensionStatus | null = null
|
|
201
226
|
|
|
202
227
|
if (extensions.length === 1) {
|
package/src/create-logger.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function createFileLogger({ logFilePath }: { logFilePath?: string } = {})
|
|
|
22
22
|
|
|
23
23
|
const log = (...args: unknown[]): Promise<void> => {
|
|
24
24
|
const message = args.map(arg =>
|
|
25
|
-
typeof arg === 'string' ? arg : util.inspect(arg, { depth: null, colors: false })
|
|
25
|
+
typeof arg === 'string' ? arg : util.inspect(arg, { depth: null, colors: false, maxStringLength: 1000 })
|
|
26
26
|
).join(' ')
|
|
27
27
|
queue = queue.then(() => fs.promises.appendFile(resolvedLogFilePath, stripAnsi(message) + '\n'))
|
|
28
28
|
return queue
|
package/src/executor.ts
CHANGED
|
@@ -15,6 +15,7 @@ import vm from 'node:vm'
|
|
|
15
15
|
import * as acorn from 'acorn'
|
|
16
16
|
import { createSmartDiff } from './diff-utils.js'
|
|
17
17
|
import { getCdpUrl, parseRelayHost } from './utils.js'
|
|
18
|
+
import { getExtensionOutdatedWarning } from './relay-client.js'
|
|
18
19
|
import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js'
|
|
19
20
|
import { ICDPSession, getCDPSessionForPage } from './cdp-session.js'
|
|
20
21
|
import { Debugger } from './debugger.js'
|
|
@@ -225,6 +226,7 @@ export class PlaywrightExecutor {
|
|
|
225
226
|
private cdpConfig: CdpConfig
|
|
226
227
|
private logger: ExecutorLogger
|
|
227
228
|
private sessionMetadata: SessionMetadata
|
|
229
|
+
private hasWarnedExtensionOutdated = false
|
|
228
230
|
|
|
229
231
|
constructor(options: ExecutorOptions) {
|
|
230
232
|
this.cdpConfig = options.cdpConfig
|
|
@@ -292,6 +294,17 @@ export class PlaywrightExecutor {
|
|
|
292
294
|
this.context = null
|
|
293
295
|
}
|
|
294
296
|
|
|
297
|
+
private warnIfExtensionOutdated(playwriterVersion: string | null) {
|
|
298
|
+
if (this.hasWarnedExtensionOutdated) {
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
const warning = getExtensionOutdatedWarning(playwriterVersion)
|
|
302
|
+
if (warning) {
|
|
303
|
+
this.logger.log(warning)
|
|
304
|
+
this.hasWarnedExtensionOutdated = true
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
295
308
|
private setupPageConsoleListener(page: Page) {
|
|
296
309
|
// Use targetId() if available, fallback to internal _guid for CDP connections
|
|
297
310
|
const targetId = page.targetId() || (page as any)._guid as string | undefined
|
|
@@ -330,9 +343,10 @@ export class PlaywrightExecutor {
|
|
|
330
343
|
})
|
|
331
344
|
}
|
|
332
345
|
|
|
333
|
-
private async checkExtensionStatus(): Promise<{ connected: boolean; activeTargets: number }> {
|
|
346
|
+
private async checkExtensionStatus(): Promise<{ connected: boolean; activeTargets: number; playwriterVersion: string | null }> {
|
|
334
347
|
const { host = '127.0.0.1', port = 19988, extensionId } = this.cdpConfig
|
|
335
348
|
const { httpBaseUrl } = parseRelayHost(host, port)
|
|
349
|
+
const notConnected = { connected: false, activeTargets: 0, playwriterVersion: null }
|
|
336
350
|
try {
|
|
337
351
|
if (extensionId) {
|
|
338
352
|
const response = await fetch(`${httpBaseUrl}/extensions/status`, {
|
|
@@ -343,31 +357,31 @@ export class PlaywrightExecutor {
|
|
|
343
357
|
signal: AbortSignal.timeout(2000),
|
|
344
358
|
})
|
|
345
359
|
if (!fallback.ok) {
|
|
346
|
-
return
|
|
360
|
+
return notConnected
|
|
347
361
|
}
|
|
348
|
-
return (await fallback.json()) as { connected: boolean; activeTargets: number }
|
|
362
|
+
return (await fallback.json()) as { connected: boolean; activeTargets: number; playwriterVersion: string | null }
|
|
349
363
|
}
|
|
350
364
|
const data = await response.json() as {
|
|
351
|
-
extensions: Array<{ extensionId: string; stableKey?: string; activeTargets: number }>
|
|
365
|
+
extensions: Array<{ extensionId: string; stableKey?: string; activeTargets: number; playwriterVersion?: string | null }>
|
|
352
366
|
}
|
|
353
367
|
const extension = data.extensions.find((item) => {
|
|
354
368
|
return item.extensionId === extensionId || item.stableKey === extensionId
|
|
355
369
|
})
|
|
356
370
|
if (!extension) {
|
|
357
|
-
return
|
|
371
|
+
return notConnected
|
|
358
372
|
}
|
|
359
|
-
return { connected: true, activeTargets: extension.activeTargets }
|
|
373
|
+
return { connected: true, activeTargets: extension.activeTargets, playwriterVersion: extension?.playwriterVersion || null }
|
|
360
374
|
}
|
|
361
375
|
|
|
362
376
|
const response = await fetch(`${httpBaseUrl}/extension/status`, {
|
|
363
377
|
signal: AbortSignal.timeout(2000),
|
|
364
378
|
})
|
|
365
379
|
if (!response.ok) {
|
|
366
|
-
return
|
|
380
|
+
return notConnected
|
|
367
381
|
}
|
|
368
|
-
return (await response.json()) as { connected: boolean; activeTargets: number }
|
|
382
|
+
return (await response.json()) as { connected: boolean; activeTargets: number; playwriterVersion: string | null }
|
|
369
383
|
} catch {
|
|
370
|
-
return
|
|
384
|
+
return notConnected
|
|
371
385
|
}
|
|
372
386
|
}
|
|
373
387
|
|
|
@@ -381,6 +395,7 @@ export class PlaywrightExecutor {
|
|
|
381
395
|
if (!extensionStatus.connected) {
|
|
382
396
|
throw new Error(EXTENSION_NOT_CONNECTED_ERROR)
|
|
383
397
|
}
|
|
398
|
+
this.warnIfExtensionOutdated(extensionStatus.playwriterVersion)
|
|
384
399
|
|
|
385
400
|
// Generate a fresh unique URL for each Playwright connection
|
|
386
401
|
const cdpUrl = getCdpUrl(this.cdpConfig)
|
|
@@ -455,6 +470,7 @@ export class PlaywrightExecutor {
|
|
|
455
470
|
if (!extensionStatus.connected) {
|
|
456
471
|
throw new Error(EXTENSION_NOT_CONNECTED_ERROR)
|
|
457
472
|
}
|
|
473
|
+
this.warnIfExtensionOutdated(extensionStatus.playwriterVersion)
|
|
458
474
|
|
|
459
475
|
// Generate a fresh unique URL for each Playwright connection
|
|
460
476
|
const cdpUrl = getCdpUrl(this.cdpConfig)
|
|
@@ -498,7 +514,7 @@ export class PlaywrightExecutor {
|
|
|
498
514
|
const formattedArgs = args
|
|
499
515
|
.map((arg) => {
|
|
500
516
|
if (typeof arg === 'string') return arg
|
|
501
|
-
return util.inspect(arg, { depth: 4, colors: false, maxArrayLength: 100, breakLength: 80 })
|
|
517
|
+
return util.inspect(arg, { depth: 4, colors: false, maxArrayLength: 100, maxStringLength: 1000, breakLength: 80 })
|
|
502
518
|
})
|
|
503
519
|
.join(' ')
|
|
504
520
|
text += `[${method}] ${formattedArgs}\n`
|
|
@@ -837,7 +853,7 @@ export class PlaywrightExecutor {
|
|
|
837
853
|
const formatted =
|
|
838
854
|
typeof resolvedResult === 'string'
|
|
839
855
|
? resolvedResult
|
|
840
|
-
: util.inspect(resolvedResult, { depth: 4, colors: false, maxArrayLength: 100, breakLength: 80 })
|
|
856
|
+
: util.inspect(resolvedResult, { depth: 4, colors: false, maxArrayLength: 100, maxStringLength: 1000, breakLength: 80 })
|
|
841
857
|
if (formatted.trim()) {
|
|
842
858
|
responseText += `[return value] ${formatted}\n`
|
|
843
859
|
}
|
package/src/mcp.ts
CHANGED
|
@@ -3,8 +3,16 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
import fs from 'node:fs'
|
|
5
5
|
import path from 'node:path'
|
|
6
|
+
import util from 'node:util'
|
|
6
7
|
import { fileURLToPath } from 'node:url'
|
|
7
8
|
import { createRequire } from 'node:module'
|
|
9
|
+
|
|
10
|
+
// Prevent Buffers from dumping hex bytes in util.inspect output.
|
|
11
|
+
// Without this, returning a screenshot Buffer would log ~400+ chars of useless hex.
|
|
12
|
+
Buffer.prototype[util.inspect.custom] = function () {
|
|
13
|
+
return `<Buffer ${this.length} bytes>`
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
import dedent from 'string-dedent'
|
|
9
17
|
import { LOG_FILE_PATH, VERSION, parseRelayHost } from './utils.js'
|
|
10
18
|
import { ensureRelayServer, RELAY_PORT } from './relay-client.js'
|
package/src/relay-client.ts
CHANGED
|
@@ -30,7 +30,7 @@ export async function getRelayServerVersion(port: number = RELAY_PORT): Promise<
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export async function getExtensionStatus(port: number = RELAY_PORT): Promise<{ connected: boolean; activeTargets: number } | null> {
|
|
33
|
+
export async function getExtensionStatus(port: number = RELAY_PORT): Promise<{ connected: boolean; activeTargets: number; playwriterVersion: string | null } | null> {
|
|
34
34
|
try {
|
|
35
35
|
const response = await fetch(`http://127.0.0.1:${port}/extension/status`, {
|
|
36
36
|
signal: AbortSignal.timeout(500),
|
|
@@ -38,7 +38,7 @@ export async function getExtensionStatus(port: number = RELAY_PORT): Promise<{ c
|
|
|
38
38
|
if (!response.ok) {
|
|
39
39
|
return null
|
|
40
40
|
}
|
|
41
|
-
return await response.json() as { connected: boolean; activeTargets: number }
|
|
41
|
+
return await response.json() as { connected: boolean; activeTargets: number; playwriterVersion: string | null }
|
|
42
42
|
} catch {
|
|
43
43
|
return null
|
|
44
44
|
}
|
|
@@ -96,7 +96,7 @@ async function killRelayServer(options: { port: number; waitForFreeMs?: number }
|
|
|
96
96
|
* - 0 if v1 === v2
|
|
97
97
|
* - positive if v1 > v2
|
|
98
98
|
*/
|
|
99
|
-
function compareVersions(v1: string, v2: string): number {
|
|
99
|
+
export function compareVersions(v1: string, v2: string): number {
|
|
100
100
|
const parts1 = v1.split('.').map(Number)
|
|
101
101
|
const parts2 = v2.split('.').map(Number)
|
|
102
102
|
const len = Math.max(parts1.length, parts2.length)
|
|
@@ -111,6 +111,22 @@ function compareVersions(v1: string, v2: string): number {
|
|
|
111
111
|
return 0
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Check if the running playwriter package is older than the version the extension was built with.
|
|
116
|
+
* The extension bundles the playwriter version at build time. If the extension reports a newer
|
|
117
|
+
* version, it means the user's CLI/MCP needs updating.
|
|
118
|
+
* Returns a warning message if outdated, null otherwise.
|
|
119
|
+
*/
|
|
120
|
+
export function getExtensionOutdatedWarning(extensionPlaywriterVersion: string | null | undefined): string | null {
|
|
121
|
+
if (!extensionPlaywriterVersion) {
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
if (compareVersions(extensionPlaywriterVersion, VERSION) > 0) {
|
|
125
|
+
return `Playwriter ${VERSION} is outdated (extension requires ${extensionPlaywriterVersion}). Run \`npm install -g playwriter@latest\` or update the playwriter package in your project.`
|
|
126
|
+
}
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
|
|
114
130
|
export interface EnsureRelayServerOptions {
|
|
115
131
|
logger?: { log: (...args: any[]) => void }
|
|
116
132
|
/** If true, will kill and restart server on version mismatch. Default: true */
|