@cotestdev/mcp_playwright 0.0.50 → 0.0.52

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 (34) hide show
  1. package/lib/mcp/browser/browserContextFactory.js +11 -4
  2. package/lib/mcp/browser/browserServerBackend.js +2 -4
  3. package/lib/mcp/browser/config.js +71 -47
  4. package/lib/mcp/browser/context.js +65 -4
  5. package/lib/mcp/browser/logFile.js +96 -0
  6. package/lib/mcp/browser/response.js +107 -104
  7. package/lib/mcp/browser/sessionLog.js +1 -1
  8. package/lib/mcp/browser/tab.js +73 -18
  9. package/lib/mcp/browser/tools/config.js +41 -0
  10. package/lib/mcp/browser/tools/console.js +6 -2
  11. package/lib/mcp/browser/tools/cookies.js +152 -0
  12. package/lib/mcp/browser/tools/install.js +1 -0
  13. package/lib/mcp/browser/tools/network.js +25 -11
  14. package/lib/mcp/browser/tools/pdf.js +3 -4
  15. package/lib/mcp/browser/tools/route.js +140 -0
  16. package/lib/mcp/browser/tools/runCode.js +0 -2
  17. package/lib/mcp/browser/tools/screenshot.js +6 -7
  18. package/lib/mcp/browser/tools/storage.js +3 -4
  19. package/lib/mcp/browser/tools/tracing.js +10 -9
  20. package/lib/mcp/browser/tools/utils.js +0 -6
  21. package/lib/mcp/browser/tools/video.js +31 -13
  22. package/lib/mcp/browser/tools/webstorage.js +223 -0
  23. package/lib/mcp/browser/tools.js +11 -3
  24. package/lib/mcp/extension/cdpRelay.js +7 -7
  25. package/lib/mcp/extension/extensionContextFactory.js +4 -2
  26. package/lib/mcp/program.js +19 -12
  27. package/lib/mcp/terminal/cli.js +23 -2
  28. package/lib/mcp/terminal/command.js +34 -30
  29. package/lib/mcp/terminal/commands.js +310 -38
  30. package/lib/mcp/terminal/daemon.js +23 -38
  31. package/lib/mcp/terminal/helpGenerator.js +8 -6
  32. package/lib/mcp/terminal/program.js +482 -199
  33. package/lib/mcp/terminal/socketConnection.js +17 -2
  34. package/package.json +2 -2
@@ -39,35 +39,42 @@ var import_os = __toESM(require("os"));
39
39
  var import_path = __toESM(require("path"));
40
40
  var import_socketConnection = require("./socketConnection");
41
41
  class Session {
42
- constructor(name, options) {
42
+ constructor(clientInfo, name, options) {
43
43
  this._nextMessageId = 1;
44
44
  this._callbacks = /* @__PURE__ */ new Map();
45
45
  this.name = name;
46
- this._socketPath = this._daemonSocketPath();
47
- this._options = options;
46
+ this._clientInfo = clientInfo;
47
+ this._config = options;
48
+ }
49
+ config() {
50
+ return this._config;
51
+ }
52
+ isCompatible() {
53
+ return this._clientInfo.version === this._config.version;
54
+ }
55
+ checkCompatible() {
56
+ if (!this.isCompatible()) {
57
+ throw new Error(`Client is v${this._clientInfo.version}, session '${this.name}' is v${this._config.version}. Run
58
+
59
+ playwright-cli session-restart${this.name !== "default" ? ` ${this.name}` : ""}
60
+
61
+ to restart the session daemon.`);
62
+ }
48
63
  }
49
64
  async run(args) {
65
+ this.checkCompatible();
50
66
  return await this._send("run", { args, cwd: process.cwd() });
51
67
  }
52
- async stop() {
68
+ async stop(quiet = false) {
53
69
  if (!await this.canConnect()) {
54
- console.log(`Session '${this.name}' is not running.`);
70
+ if (!quiet)
71
+ console.log(`Browser '${this.name}' is not open.`);
55
72
  return;
56
73
  }
57
- await this._send("stop").catch((e) => {
58
- if (e.message !== "Session closed")
59
- throw e;
60
- });
61
- this.close();
62
- if (import_os.default.platform() !== "win32")
63
- await import_fs.default.promises.unlink(this._socketPath).catch(() => {
64
- });
65
- console.log(`Session '${this.name}' stopped.`);
66
- }
67
- async restart(options) {
68
- await this.stop();
69
- this._options = options;
70
- await this._startDaemonIfNeeded();
74
+ await this._stopDaemon();
75
+ if (!quiet)
76
+ console.log(`Browser '${this.name}' closed
77
+ `);
71
78
  }
72
79
  async _send(method, params = {}) {
73
80
  const connection = await this._startDaemonIfNeeded();
@@ -76,7 +83,7 @@ class Session {
76
83
  id: messageId,
77
84
  method,
78
85
  params,
79
- version: this._options.daemonVersion
86
+ version: this._config.version
80
87
  };
81
88
  const responsePromise = new Promise((resolve, reject) => {
82
89
  this._callbacks.set(messageId, { resolve, reject, method, params });
@@ -84,7 +91,7 @@ class Session {
84
91
  const [result] = await Promise.all([responsePromise, connection.send(message)]);
85
92
  return result;
86
93
  }
87
- close() {
94
+ disconnect() {
88
95
  if (!this._connection)
89
96
  return;
90
97
  for (const callback of this._callbacks.values())
@@ -93,24 +100,25 @@ class Session {
93
100
  this._connection.close();
94
101
  this._connection = void 0;
95
102
  }
96
- async delete() {
103
+ async deleteData() {
97
104
  await this.stop();
98
- const dataDirs = await import_fs.default.promises.readdir(daemonProfilesDir).catch(() => []);
99
- const matchingDirs = dataDirs.filter((dir) => dir.startsWith(`ud-${this.name}-`));
100
- if (matchingDirs.length === 0) {
101
- console.log(`No user data found for session '${this.name}'.`);
105
+ const dataDirs = await import_fs.default.promises.readdir(this._clientInfo.daemonProfilesDir).catch(() => []);
106
+ const matchingEntries = dataDirs.filter((file) => file === `${this.name}.session` || file.startsWith(`ud-${this.name}-`));
107
+ if (matchingEntries.length === 0) {
108
+ console.log(`No user data found for browser '${this.name}'.`);
102
109
  return;
103
110
  }
104
- for (const dir of matchingDirs) {
105
- const userDataDir = import_path.default.resolve(daemonProfilesDir, dir);
111
+ for (const entry of matchingEntries) {
112
+ const userDataDir = import_path.default.resolve(this._clientInfo.daemonProfilesDir, entry);
106
113
  for (let i = 0; i < 5; i++) {
107
114
  try {
108
115
  await import_fs.default.promises.rm(userDataDir, { recursive: true });
109
- console.log(`Deleted user data for session '${this.name}'.`);
116
+ if (entry.startsWith("ud-"))
117
+ console.log(`Deleted user data for browser '${this.name}'.`);
110
118
  break;
111
119
  } catch (e) {
112
120
  if (e.code === "ENOENT") {
113
- console.log(`No user data found for session '${this.name}'.`);
121
+ console.log(`No user data found for browser '${this.name}'.`);
114
122
  break;
115
123
  }
116
124
  await new Promise((resolve) => setTimeout(resolve, 1e3));
@@ -122,12 +130,12 @@ class Session {
122
130
  }
123
131
  async _connect() {
124
132
  return await new Promise((resolve) => {
125
- const socket = import_net.default.createConnection(this._socketPath, () => {
133
+ const socket = import_net.default.createConnection(this._config.socketPath, () => {
126
134
  resolve({ socket });
127
135
  });
128
136
  socket.on("error", (error) => {
129
137
  if (import_os.default.platform() !== "win32")
130
- void import_fs.default.promises.unlink(this._socketPath).catch(() => {
138
+ void import_fs.default.promises.unlink(this._config.socketPath).catch(() => {
131
139
  }).then(() => resolve({ error }));
132
140
  else
133
141
  resolve({ error });
@@ -148,17 +156,9 @@ class Session {
148
156
  let { socket } = await this._connect();
149
157
  if (!socket)
150
158
  socket = await this._startDaemon();
151
- this._connection = new import_socketConnection.SocketConnection(socket, this._options.daemonVersion);
159
+ this._connection = new import_socketConnection.SocketConnection(socket, this._config.version);
152
160
  this._connection.onmessage = (message) => this._onMessage(message);
153
- this._connection.onversionerror = (id, e) => {
154
- if (e.received && e.received !== "undefined-for-test") {
155
- return false;
156
- }
157
- console.error(`Daemon is older than client, killing it.`);
158
- this.stop().then(() => process.exit(1)).catch(() => process.exit(1));
159
- return true;
160
- };
161
- this._connection.onclose = () => this.close();
161
+ this._connection.onclose = () => this.disconnect();
162
162
  return this._connection;
163
163
  }
164
164
  _onMessage(object) {
@@ -175,134 +175,164 @@ class Session {
175
175
  throw new Error(`Unexpected message without id: ${JSON.stringify(object)}`);
176
176
  }
177
177
  }
178
+ _sessionFile(suffix) {
179
+ return import_path.default.resolve(this._clientInfo.daemonProfilesDir, `${this.name}${suffix}`);
180
+ }
178
181
  async _startDaemon() {
179
- await import_fs.default.promises.mkdir(daemonProfilesDir, { recursive: true });
180
- const userDataDir = import_path.default.resolve(daemonProfilesDir, `ud-${this.name}`);
182
+ await import_fs.default.promises.mkdir(this._clientInfo.daemonProfilesDir, { recursive: true });
181
183
  const cliPath = import_path.default.join(__dirname, "../../../cli.js");
182
- const configFile = resolveConfigFile(this._options.config);
183
- const configArg = configFile !== void 0 ? [`--config=${configFile}`] : [];
184
- const headedArg = this._options.headed ? [`--daemon-headed`] : [];
185
- const extensionArg = this._options.extension ? [`--extension`] : [];
186
- const outLog = import_path.default.join(daemonProfilesDir, "out.log");
187
- const errLog = import_path.default.join(daemonProfilesDir, "err.log");
188
- const out = import_fs.default.openSync(outLog, "w");
184
+ const sessionConfigFile = this._sessionFile(".session");
185
+ this._config.version = this._clientInfo.version;
186
+ await import_fs.default.promises.writeFile(sessionConfigFile, JSON.stringify(this._config, null, 2));
187
+ const errLog = this._sessionFile(".err");
189
188
  const err = import_fs.default.openSync(errLog, "w");
190
- const child = (0, import_child_process.spawn)(process.execPath, [
189
+ const args = [
191
190
  cliPath,
192
191
  "run-mcp-server",
193
- `--output-dir=${outputDir}`,
194
- `--daemon=${this._socketPath}`,
195
- `--daemon-data-dir=${userDataDir}`,
196
- `--daemon-version=${this._options.daemonVersion}`,
197
- ...configArg,
198
- ...headedArg,
199
- ...extensionArg
200
- ], {
192
+ `--daemon-session=${sessionConfigFile}`
193
+ ];
194
+ const child = (0, import_child_process.spawn)(process.execPath, args, {
201
195
  detached: true,
202
- stdio: ["ignore", out, err],
196
+ stdio: ["ignore", "pipe", err],
203
197
  cwd: process.cwd()
204
198
  // Will be used as root.
205
199
  });
200
+ let signalled = false;
201
+ const sigintHandler = () => {
202
+ signalled = true;
203
+ child.kill("SIGINT");
204
+ };
205
+ const sigtermHandler = () => {
206
+ signalled = true;
207
+ child.kill("SIGTERM");
208
+ };
209
+ process.on("SIGINT", sigintHandler);
210
+ process.on("SIGTERM", sigtermHandler);
211
+ let outLog = "";
212
+ await new Promise((resolve, reject) => {
213
+ child.stdout.on("data", (data) => {
214
+ outLog += data.toString();
215
+ if (!outLog.includes("<EOF>"))
216
+ return;
217
+ const errorMatch = outLog.match(/### Error\n([\s\S]*)<EOF>/);
218
+ const error = errorMatch ? errorMatch[1].trim() : void 0;
219
+ if (error) {
220
+ const errLogContent = import_fs.default.readFileSync(errLog, "utf-8");
221
+ const message = error + (errLogContent ? "\n" + errLogContent : "");
222
+ reject(new Error(message));
223
+ }
224
+ const successMatch = outLog.match(/### Success\nDaemon listening on (.*)\n<EOF>/);
225
+ if (successMatch)
226
+ resolve();
227
+ });
228
+ child.on("close", (code) => {
229
+ if (!signalled)
230
+ reject(new Error(`Daemon process exited with code ${code}`));
231
+ });
232
+ });
233
+ process.off("SIGINT", sigintHandler);
234
+ process.off("SIGTERM", sigtermHandler);
235
+ child.stdout.destroy();
206
236
  child.unref();
207
- console.log(`<!-- Daemon for \`${this.name}\` session started with pid ${child.pid}.`);
208
- if (configFile)
209
- console.log(`- Using config file at \`${import_path.default.relative(process.cwd(), configFile)}\`.`);
210
- const sessionSuffix = this.name !== "default" ? ` "${this.name}"` : "";
211
- console.log(`- You can stop the session daemon with \`playwright-cli session-stop${sessionSuffix}\` when done.`);
212
- console.log(`- You can delete the session data with \`playwright-cli session-delete${sessionSuffix}\` when done.`);
213
- console.log("-->");
214
- const maxRetries = 50;
215
- const retryDelay = 100;
216
- for (let i = 0; i < maxRetries; i++) {
217
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
218
- try {
219
- const { socket } = await this._connect();
220
- if (socket)
221
- return socket;
222
- } catch (e) {
223
- if (e.code !== "ENOENT" && e.code !== "ECONNREFUSED")
224
- throw e;
237
+ const { socket } = await this._connect();
238
+ if (socket) {
239
+ console.log(`### Browser \`${this.name}\` opened with pid ${child.pid}.`);
240
+ const resolvedConfig = await parseResolvedConfig(outLog);
241
+ if (resolvedConfig) {
242
+ this._config.resolvedConfig = resolvedConfig;
243
+ console.log(`- ${this.name}:`);
244
+ console.log(renderResolvedConfig(resolvedConfig).join("\n"));
225
245
  }
246
+ console.log(`---`);
247
+ await import_fs.default.promises.writeFile(sessionConfigFile, JSON.stringify(this._config, null, 2));
248
+ return socket;
226
249
  }
227
- const outData = await import_fs.default.promises.readFile(outLog, "utf-8").catch(() => "");
228
- const errData = await import_fs.default.promises.readFile(errLog, "utf-8").catch(() => "");
229
- console.error(`Failed to connect to daemon at ${this._socketPath} after ${maxRetries * retryDelay}ms`);
230
- if (outData.length)
231
- console.log(outData);
232
- if (errData.length)
233
- console.error(errData);
250
+ console.error(`Failed to connect to daemon at ${this._config.socketPath}`);
234
251
  process.exit(1);
235
252
  }
236
- _daemonSocketPath() {
237
- const socketName = `${this.name}.sock`;
238
- if (import_os.default.platform() === "win32")
239
- return `\\\\.\\pipe\\${installationDirHash}-${socketName}`;
240
- const socketsDir = process.env.PLAYWRIGHT_DAEMON_SOCKETS_DIR || import_path.default.join(import_os.default.tmpdir(), "playwright-cli");
241
- return import_path.default.join(socketsDir, installationDirHash, socketName);
253
+ async _stopDaemon() {
254
+ let error;
255
+ await this._send("stop").catch((e) => {
256
+ error = e;
257
+ });
258
+ if (import_os.default.platform() !== "win32")
259
+ await import_fs.default.promises.unlink(this._config.socketPath).catch(() => {
260
+ });
261
+ this.disconnect();
262
+ if (!this._config.cli.persistent)
263
+ await this.deleteSessionConfig();
264
+ if (error && !error?.message?.includes("Session closed"))
265
+ throw error;
266
+ }
267
+ async deleteSessionConfig() {
268
+ await import_fs.default.promises.rm(this._sessionFile(".session")).catch(() => {
269
+ });
242
270
  }
243
271
  }
244
272
  class SessionManager {
245
- constructor(sessions, options) {
273
+ constructor(clientInfo, sessions) {
274
+ this.clientInfo = clientInfo;
246
275
  this.sessions = sessions;
247
- this.options = options;
248
- }
249
- static async create(options) {
250
- const dir = daemonProfilesDir;
251
- const sessions = /* @__PURE__ */ new Map([
252
- ["default", new Session("default", options)]
253
- ]);
254
- try {
255
- const files = await import_fs.default.promises.readdir(dir);
256
- for (const file of files) {
257
- if (file.startsWith("ud-")) {
258
- const sessionName = file.split("-")[1];
259
- sessions.set(sessionName, new Session(sessionName, options));
260
- }
276
+ }
277
+ static async create(clientInfo) {
278
+ const dir = clientInfo.daemonProfilesDir;
279
+ const sessions = /* @__PURE__ */ new Map();
280
+ const files = await import_fs.default.promises.readdir(dir).catch(() => []);
281
+ for (const file of files) {
282
+ if (!file.endsWith(".session"))
283
+ continue;
284
+ try {
285
+ const sessionName = import_path.default.basename(file, ".session");
286
+ const sessionConfig = await import_fs.default.promises.readFile(import_path.default.join(dir, file), "utf-8").then((data) => JSON.parse(data));
287
+ sessions.set(sessionName, new Session(clientInfo, sessionName, sessionConfig));
288
+ } catch {
261
289
  }
262
- } catch {
263
290
  }
264
- return new SessionManager(sessions, options);
291
+ return new SessionManager(clientInfo, sessions);
265
292
  }
266
- async run(args) {
293
+ async open(args) {
267
294
  const sessionName = this._resolveSessionName(args.session);
268
295
  let session = this.sessions.get(sessionName);
296
+ if (session)
297
+ await session.stop(true);
298
+ session = new Session(this.clientInfo, sessionName, sessionConfigFromArgs(this.clientInfo, sessionName, args));
299
+ this.sessions.set(sessionName, session);
300
+ await this.run(args);
301
+ }
302
+ async run(args) {
303
+ const sessionName = this._resolveSessionName(args.session);
304
+ const session = this.sessions.get(sessionName);
269
305
  if (!session) {
270
- session = new Session(sessionName, { ...this.options, ...args });
271
- this.sessions.set(sessionName, session);
306
+ console.log(`The browser '${sessionName}' is not open, please run open first`);
307
+ console.log("");
308
+ console.log(` playwright-cli${sessionName !== "default" ? ` -s=${sessionName}` : ""} open [params]`);
309
+ process.exit(1);
272
310
  }
273
- const result = await session.run({ ...args, outputDir });
311
+ for (const globalOption of globalOptions)
312
+ delete args[globalOption];
313
+ const result = await session.run(args);
274
314
  console.log(result.text);
275
- session.close();
315
+ session.disconnect();
276
316
  }
277
- async stop(sessionName) {
278
- sessionName = this._resolveSessionName(sessionName);
317
+ async close(options) {
318
+ const sessionName = this._resolveSessionName(options.session);
279
319
  const session = this.sessions.get(sessionName);
280
320
  if (!session || !await session.canConnect()) {
281
- console.log(`Session '${sessionName}' is not running.`);
321
+ console.log(`Browser '${sessionName}' is not open.`);
282
322
  return;
283
323
  }
284
324
  await session.stop();
285
325
  }
286
- async delete(sessionName) {
287
- sessionName = this._resolveSessionName(sessionName);
326
+ async deleteData(options) {
327
+ const sessionName = this._resolveSessionName(options.session);
288
328
  const session = this.sessions.get(sessionName);
289
329
  if (!session) {
290
- console.log(`No user data found for session '${sessionName}'.`);
330
+ console.log(`No user data found for browser '${sessionName}'.`);
291
331
  return;
292
332
  }
293
- await session.delete();
333
+ await session.deleteData();
294
334
  this.sessions.delete(sessionName);
295
335
  }
296
- async configure(args) {
297
- const sessionName = this._resolveSessionName(args.session);
298
- let session = this.sessions.get(sessionName);
299
- if (!session) {
300
- session = new Session(sessionName, this.options);
301
- this.sessions.set(sessionName, session);
302
- }
303
- await session.restart({ ...this.options, ...args, config: args._[1] });
304
- session.close();
305
- }
306
336
  _resolveSessionName(sessionName) {
307
337
  if (sessionName)
308
338
  return sessionName;
@@ -311,45 +341,33 @@ class SessionManager {
311
341
  return "default";
312
342
  }
313
343
  }
314
- async function handleSessionCommand(sessionManager, subcommand, args) {
315
- if (subcommand === "list") {
316
- const sessions = sessionManager.sessions;
317
- console.log("Sessions:");
318
- for (const session of sessions.values()) {
319
- const liveMarker = await session.canConnect() ? " (live)" : "";
320
- console.log(` ${session.name}${liveMarker}`);
321
- }
322
- if (sessions.size === 0)
323
- console.log(" (no sessions)");
324
- return;
325
- }
326
- if (subcommand === "stop") {
327
- await sessionManager.stop(args._[1]);
328
- return;
329
- }
330
- if (subcommand === "stop-all") {
331
- const sessions = sessionManager.sessions;
332
- for (const session of sessions.values())
333
- await session.stop();
334
- return;
335
- }
336
- if (subcommand === "delete") {
337
- await sessionManager.delete(args._[1]);
338
- return;
339
- }
340
- if (subcommand === "config") {
341
- await sessionManager.configure(args);
342
- return;
344
+ function createClientInfo(packageLocation) {
345
+ const packageJSON = require(packageLocation);
346
+ const workspaceDir = findWorkspaceDir(process.cwd());
347
+ const version = process.env.PLAYWRIGHT_CLI_VERSION_FOR_TEST || packageJSON.version;
348
+ const hash = import_crypto.default.createHash("sha1");
349
+ hash.update(workspaceDir || packageLocation);
350
+ const workspaceDirHash = hash.digest("hex").substring(0, 16);
351
+ return {
352
+ version,
353
+ workspaceDir,
354
+ workspaceDirHash,
355
+ daemonProfilesDir: daemonProfilesDir(workspaceDirHash)
356
+ };
357
+ }
358
+ function findWorkspaceDir(startDir) {
359
+ let dir = startDir;
360
+ for (let i = 0; i < 10; i++) {
361
+ if (import_fs.default.existsSync(import_path.default.join(dir, ".playwright")))
362
+ return dir;
363
+ const parentDir = import_path.default.dirname(dir);
364
+ if (parentDir === dir)
365
+ break;
366
+ dir = parentDir;
343
367
  }
344
- console.error(`Unknown session subcommand: ${subcommand}`);
345
- process.exit(1);
368
+ return void 0;
346
369
  }
347
- const installationDirHash = (() => {
348
- const hash = import_crypto.default.createHash("sha1");
349
- hash.update(process.env.PLAYWRIGHT_DAEMON_INSTALL_DIR || require.resolve("../../../package.json"));
350
- return hash.digest("hex").substring(0, 16);
351
- })();
352
- const daemonProfilesDir = (() => {
370
+ const baseDaemonDir = (() => {
353
371
  if (process.env.PLAYWRIGHT_DAEMON_SESSION_DIR)
354
372
  return process.env.PLAYWRIGHT_DAEMON_SESSION_DIR;
355
373
  let localCacheDir;
@@ -361,24 +379,49 @@ const daemonProfilesDir = (() => {
361
379
  localCacheDir = process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local");
362
380
  if (!localCacheDir)
363
381
  throw new Error("Unsupported platform: " + process.platform);
364
- return import_path.default.join(localCacheDir, "ms-playwright", "daemon", installationDirHash);
382
+ return import_path.default.join(localCacheDir, "ms-playwright", "daemon");
365
383
  })();
366
- async function program(options) {
384
+ const daemonProfilesDir = (workspaceDirHash) => {
385
+ return import_path.default.join(baseDaemonDir, workspaceDirHash);
386
+ };
387
+ const globalOptions = [
388
+ "browser",
389
+ "config",
390
+ "extension",
391
+ "headed",
392
+ "help",
393
+ "persistent",
394
+ "profile",
395
+ "session",
396
+ "version"
397
+ ];
398
+ const booleanOptions = [
399
+ "all",
400
+ "help",
401
+ "version",
402
+ "extension",
403
+ "headed",
404
+ "persistent"
405
+ ];
406
+ async function program(packageLocation) {
407
+ const clientInfo = createClientInfo(packageLocation);
367
408
  const argv = process.argv.slice(2);
368
- const args = require("minimist")(argv, {
369
- boolean: ["help", "version", "headed", "extension"]
370
- });
371
- if (!argv.includes("--headed") && !argv.includes("--no-headed"))
372
- delete args.headed;
373
- if (!argv.includes("--extension"))
374
- delete args.extension;
409
+ const args = require("minimist")(argv, { boolean: booleanOptions });
410
+ for (const option of booleanOptions) {
411
+ if (!argv.includes(`--${option}`) && !argv.includes(`--no-${option}`))
412
+ delete args[option];
413
+ }
414
+ if (args.s) {
415
+ args.session = args.s;
416
+ delete args.s;
417
+ }
375
418
  const help = require("./help.json");
376
- const commandName = args._[0];
419
+ const commandName = args._?.[0];
377
420
  if (args.version || args.v) {
378
- console.log(options.version);
421
+ console.log(clientInfo.version);
379
422
  process.exit(0);
380
423
  }
381
- const command = help.commands[commandName];
424
+ const command = commandName && help.commands[commandName];
382
425
  if (args.help || args.h) {
383
426
  if (command) {
384
427
  console.log(command);
@@ -394,33 +437,273 @@ async function program(options) {
394
437
  console.log(help.global);
395
438
  process.exit(1);
396
439
  }
397
- const sessionManager = await SessionManager.create({ daemonVersion: options.version, ...args });
398
- if (commandName.startsWith("session")) {
399
- const subcommand = args._[0].split("-").slice(1).join("-");
400
- await handleSessionCommand(sessionManager, subcommand, args);
401
- return;
440
+ const sessionManager = await SessionManager.create(clientInfo);
441
+ switch (commandName) {
442
+ case "list": {
443
+ if (args.all)
444
+ await listAllSessions(clientInfo);
445
+ else
446
+ await listSessions(sessionManager);
447
+ return;
448
+ }
449
+ case "close-all": {
450
+ const sessions = sessionManager.sessions;
451
+ for (const session of sessions.values())
452
+ await session.stop(true);
453
+ return;
454
+ }
455
+ case "delete-data":
456
+ await sessionManager.deleteData(args);
457
+ return;
458
+ case "kill-all":
459
+ await killAllDaemons();
460
+ return;
461
+ case "open":
462
+ await sessionManager.open(args);
463
+ return;
464
+ case "close":
465
+ await sessionManager.close(args);
466
+ return;
467
+ case "install":
468
+ await install(args);
469
+ return;
470
+ default:
471
+ await sessionManager.run(args);
402
472
  }
403
- if (commandName === "config") {
404
- await handleSessionCommand(sessionManager, "config", args);
405
- return;
473
+ }
474
+ async function install(args) {
475
+ const cwd = process.cwd();
476
+ const playwrightDir = import_path.default.join(cwd, ".playwright");
477
+ await import_fs.default.promises.mkdir(playwrightDir, { recursive: true });
478
+ console.log(`Workspace initialized at ${cwd}`);
479
+ if (args.skills) {
480
+ const skillSourceDir = import_path.default.join(__dirname, "../../skill");
481
+ const skillDestDir = import_path.default.join(cwd, ".claude", "skills", "playwright-cli");
482
+ if (!import_fs.default.existsSync(skillSourceDir)) {
483
+ console.error("Skills source directory not found:", skillSourceDir);
484
+ process.exit(1);
485
+ }
486
+ await import_fs.default.promises.cp(skillSourceDir, skillDestDir, { recursive: true });
487
+ console.log(`Skills installed to ${import_path.default.relative(cwd, skillDestDir)}`);
406
488
  }
407
- if (commandName === "close") {
408
- await handleSessionCommand(sessionManager, "stop", args);
489
+ if (!args.config)
490
+ await ensureConfiguredBrowserInstalled();
491
+ }
492
+ async function ensureConfiguredBrowserInstalled() {
493
+ if (import_fs.default.existsSync(defaultConfigFile())) {
494
+ const { registry } = await import("playwright-core/lib/server/registry/index");
495
+ const config = JSON.parse(await import_fs.default.promises.readFile(defaultConfigFile(), "utf-8"));
496
+ const browserName = config.browser?.browserName ?? "chromium";
497
+ const channel = config.browser?.launchOptions?.channel;
498
+ if (!channel || channel.startsWith("chromium")) {
499
+ const executable = registry.findExecutable(channel ?? browserName);
500
+ if (executable && !import_fs.default.existsSync(executable?.executablePath()))
501
+ await registry.install([executable]);
502
+ }
503
+ } else {
504
+ const channel = await findOrInstallDefaultBrowser();
505
+ if (channel !== "chrome")
506
+ await createDefaultConfig(channel);
507
+ }
508
+ }
509
+ async function createDefaultConfig(channel) {
510
+ const config = {
511
+ browser: {
512
+ browserName: "chromium",
513
+ launchOptions: {
514
+ channel
515
+ }
516
+ }
517
+ };
518
+ await import_fs.default.promises.writeFile(defaultConfigFile(), JSON.stringify(config, null, 2));
519
+ console.log(`Created default config for ${channel} at ${import_path.default.relative(process.cwd(), defaultConfigFile())}.`);
520
+ }
521
+ async function findOrInstallDefaultBrowser() {
522
+ const { registry } = await import("playwright-core/lib/server/registry/index");
523
+ const channels = ["chrome", "msedge"];
524
+ for (const channel of channels) {
525
+ const executable = registry.findExecutable(channel);
526
+ if (!executable?.executablePath())
527
+ continue;
528
+ console.log(`Found ${channel}, will use it as the default browser.`);
529
+ return channel;
530
+ }
531
+ const chromiumExecutable = registry.findExecutable("chromium");
532
+ if (!import_fs.default.existsSync(chromiumExecutable?.executablePath()))
533
+ await registry.install([chromiumExecutable]);
534
+ return "chromium";
535
+ }
536
+ function daemonSocketPath(clientInfo, sessionName) {
537
+ const socketName = `${sessionName}.sock`;
538
+ if (import_os.default.platform() === "win32")
539
+ return `\\\\.\\pipe\\${clientInfo.workspaceDirHash}-${socketName}`;
540
+ const socketsDir = process.env.PLAYWRIGHT_DAEMON_SOCKETS_DIR || import_path.default.join(import_os.default.tmpdir(), "playwright-cli");
541
+ return import_path.default.join(socketsDir, clientInfo.workspaceDirHash, socketName);
542
+ }
543
+ function defaultConfigFile() {
544
+ return import_path.default.resolve(".playwright", "cli.config.json");
545
+ }
546
+ function sessionConfigFromArgs(clientInfo, sessionName, args) {
547
+ let config = args.config ? import_path.default.resolve(args.config) : void 0;
548
+ try {
549
+ if (!config && import_fs.default.existsSync(defaultConfigFile()))
550
+ config = defaultConfigFile();
551
+ } catch {
552
+ }
553
+ if (!args.persistent && args.profile)
554
+ args.persistent = true;
555
+ return {
556
+ version: clientInfo.version,
557
+ socketPath: daemonSocketPath(clientInfo, sessionName),
558
+ cli: {
559
+ headed: args.headed,
560
+ extension: args.extension,
561
+ browser: args.browser,
562
+ persistent: args.persistent,
563
+ profile: args.profile,
564
+ config
565
+ },
566
+ userDataDirPrefix: import_path.default.resolve(clientInfo.daemonProfilesDir, `ud-${sessionName}`),
567
+ workspaceDir: clientInfo.workspaceDir
568
+ };
569
+ }
570
+ async function killAllDaemons() {
571
+ const platform = import_os.default.platform();
572
+ let killed = 0;
573
+ try {
574
+ if (platform === "win32") {
575
+ const result = (0, import_child_process.execSync)(
576
+ `powershell -NoProfile -NonInteractive -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*run-mcp-server*' -and $_.CommandLine -like '*--daemon-session*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue; $_.ProcessId }"`,
577
+ { encoding: "utf-8" }
578
+ );
579
+ const pids = result.split("\n").map((line) => line.trim()).filter((line) => /^\d+$/.test(line));
580
+ for (const pid of pids)
581
+ console.log(`Killed daemon process ${pid}`);
582
+ killed = pids.length;
583
+ } else {
584
+ const result = (0, import_child_process.execSync)("ps aux", { encoding: "utf-8" });
585
+ const lines = result.split("\n");
586
+ for (const line of lines) {
587
+ if (line.includes("run-mcp-server") && line.includes("--daemon-session")) {
588
+ const parts = line.trim().split(/\s+/);
589
+ const pid = parts[1];
590
+ if (pid && /^\d+$/.test(pid)) {
591
+ try {
592
+ process.kill(parseInt(pid, 10), "SIGKILL");
593
+ console.log(`Killed daemon process ${pid}`);
594
+ killed++;
595
+ } catch {
596
+ }
597
+ }
598
+ }
599
+ }
600
+ }
601
+ } catch (e) {
602
+ }
603
+ if (killed === 0)
604
+ console.log("No daemon processes found.");
605
+ else if (killed > 0)
606
+ console.log(`Killed ${killed} daemon process${killed === 1 ? "" : "es"}.`);
607
+ }
608
+ async function listSessions(sessionManager) {
609
+ const sessions = sessionManager.sessions;
610
+ console.log("### Browsers");
611
+ await gcAndPrintSessions([...sessions.values()]);
612
+ }
613
+ async function listAllSessions(clientInfo) {
614
+ const hashes = await import_fs.default.promises.readdir(baseDaemonDir).catch(() => []);
615
+ const sessionsByWorkspace = /* @__PURE__ */ new Map();
616
+ for (const hash of hashes) {
617
+ const hashDir = import_path.default.join(baseDaemonDir, hash);
618
+ const stat = await import_fs.default.promises.stat(hashDir).catch(() => null);
619
+ if (!stat?.isDirectory())
620
+ continue;
621
+ const files = await import_fs.default.promises.readdir(hashDir).catch(() => []);
622
+ for (const file of files) {
623
+ if (!file.endsWith(".session"))
624
+ continue;
625
+ try {
626
+ const sessionName = import_path.default.basename(file, ".session");
627
+ const sessionConfig = await import_fs.default.promises.readFile(import_path.default.join(hashDir, file), "utf-8").then((data) => JSON.parse(data));
628
+ const session = new Session(clientInfo, sessionName, sessionConfig);
629
+ const workspaceKey = sessionConfig.workspaceDir || "<global>";
630
+ if (!sessionsByWorkspace.has(workspaceKey))
631
+ sessionsByWorkspace.set(workspaceKey, []);
632
+ sessionsByWorkspace.get(workspaceKey).push(session);
633
+ } catch {
634
+ }
635
+ }
636
+ }
637
+ if (sessionsByWorkspace.size === 0) {
638
+ console.log("No browsers found.");
409
639
  return;
410
640
  }
411
- await sessionManager.run(args);
641
+ const sortedWorkspaces = [...sessionsByWorkspace.keys()].sort();
642
+ for (const workspace of sortedWorkspaces) {
643
+ const sessions = sessionsByWorkspace.get(workspace);
644
+ console.log(`${workspace}:`);
645
+ await gcAndPrintSessions(sessions);
646
+ }
412
647
  }
413
- const outputDir = import_path.default.join(process.cwd(), ".playwright-cli");
414
- function resolveConfigFile(configParam) {
415
- const configFile = configParam || process.env.PLAYWRIGHT_CLI_CONFIG;
416
- if (configFile)
417
- return import_path.default.resolve(process.cwd(), configFile);
648
+ async function gcAndPrintSessions(sessions) {
649
+ const running = [];
650
+ const stopped = [];
651
+ for (const session of sessions.values()) {
652
+ const canConnect = await session.canConnect();
653
+ if (canConnect) {
654
+ running.push(session);
655
+ } else {
656
+ if (session.config().cli.persistent)
657
+ stopped.push(session);
658
+ else
659
+ await session.deleteSessionConfig();
660
+ }
661
+ }
662
+ for (const session of running)
663
+ console.log(await renderSessionStatus(session));
664
+ for (const session of stopped)
665
+ console.log(await renderSessionStatus(session));
666
+ if (running.length === 0 && stopped.length === 0)
667
+ console.log(" (no browsers)");
668
+ }
669
+ async function renderSessionStatus(session) {
670
+ const text = [];
671
+ const config = session.config();
672
+ const canConnect = await session.canConnect();
673
+ const restartMarker = canConnect && !session.isCompatible() ? ` - v${config.version}, please reopen` : "";
674
+ text.push(`- ${session.name}:`);
675
+ text.push(` - status: ${canConnect ? "open" : "closed"}${restartMarker}`);
676
+ if (config.resolvedConfig)
677
+ text.push(...renderResolvedConfig(config.resolvedConfig));
678
+ return text.join("\n");
679
+ }
680
+ function renderResolvedConfig(resolvedConfig) {
681
+ const channel = resolvedConfig.browser.launchOptions.channel ?? resolvedConfig.browser.browserName;
682
+ const lines = [];
683
+ if (channel)
684
+ lines.push(` - browser-type: ${channel}`);
685
+ if (resolvedConfig.browser.isolated)
686
+ lines.push(` - user-data-dir: <in-memory>`);
687
+ else
688
+ lines.push(` - user-data-dir: ${resolvedConfig.browser.userDataDir}`);
689
+ lines.push(` - headed: ${!resolvedConfig.browser.launchOptions.headless}`);
690
+ return lines;
691
+ }
692
+ async function parseResolvedConfig(errLog) {
693
+ const marker = "### Config\n```json\n";
694
+ const markerIndex = errLog.indexOf(marker);
695
+ if (markerIndex === -1)
696
+ return null;
697
+ const jsonStart = markerIndex + marker.length;
698
+ const jsonEnd = errLog.indexOf("\n```", jsonStart);
699
+ if (jsonEnd === -1)
700
+ throw null;
701
+ const jsonString = errLog.substring(jsonStart, jsonEnd).trim();
418
702
  try {
419
- if (import_fs.default.existsSync(import_path.default.resolve(process.cwd(), "playwright-cli.json")))
420
- return import_path.default.resolve(process.cwd(), "playwright-cli.json");
703
+ return JSON.parse(jsonString);
421
704
  } catch {
705
+ return null;
422
706
  }
423
- return void 0;
424
707
  }
425
708
  // Annotate the CommonJS export names for ESM import in node:
426
709
  0 && (module.exports = {