@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.
- package/lib/mcp/browser/browserContextFactory.js +11 -4
- package/lib/mcp/browser/browserServerBackend.js +2 -4
- package/lib/mcp/browser/config.js +71 -47
- package/lib/mcp/browser/context.js +65 -4
- package/lib/mcp/browser/logFile.js +96 -0
- package/lib/mcp/browser/response.js +107 -104
- package/lib/mcp/browser/sessionLog.js +1 -1
- package/lib/mcp/browser/tab.js +73 -18
- package/lib/mcp/browser/tools/config.js +41 -0
- package/lib/mcp/browser/tools/console.js +6 -2
- package/lib/mcp/browser/tools/cookies.js +152 -0
- package/lib/mcp/browser/tools/install.js +1 -0
- package/lib/mcp/browser/tools/network.js +25 -11
- package/lib/mcp/browser/tools/pdf.js +3 -4
- package/lib/mcp/browser/tools/route.js +140 -0
- package/lib/mcp/browser/tools/runCode.js +0 -2
- package/lib/mcp/browser/tools/screenshot.js +6 -7
- package/lib/mcp/browser/tools/storage.js +3 -4
- package/lib/mcp/browser/tools/tracing.js +10 -9
- package/lib/mcp/browser/tools/utils.js +0 -6
- package/lib/mcp/browser/tools/video.js +31 -13
- package/lib/mcp/browser/tools/webstorage.js +223 -0
- package/lib/mcp/browser/tools.js +11 -3
- package/lib/mcp/extension/cdpRelay.js +7 -7
- package/lib/mcp/extension/extensionContextFactory.js +4 -2
- package/lib/mcp/program.js +19 -12
- package/lib/mcp/terminal/cli.js +23 -2
- package/lib/mcp/terminal/command.js +34 -30
- package/lib/mcp/terminal/commands.js +310 -38
- package/lib/mcp/terminal/daemon.js +23 -38
- package/lib/mcp/terminal/helpGenerator.js +8 -6
- package/lib/mcp/terminal/program.js +482 -199
- package/lib/mcp/terminal/socketConnection.js +17 -2
- 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.
|
|
47
|
-
this.
|
|
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
|
-
|
|
70
|
+
if (!quiet)
|
|
71
|
+
console.log(`Browser '${this.name}' is not open.`);
|
|
55
72
|
return;
|
|
56
73
|
}
|
|
57
|
-
await this.
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
103
|
+
async deleteData() {
|
|
97
104
|
await this.stop();
|
|
98
|
-
const dataDirs = await import_fs.default.promises.readdir(daemonProfilesDir).catch(() => []);
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
101
|
-
console.log(`No user data found for
|
|
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
|
|
105
|
-
const userDataDir = import_path.default.resolve(daemonProfilesDir,
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
159
|
+
this._connection = new import_socketConnection.SocketConnection(socket, this._config.version);
|
|
152
160
|
this._connection.onmessage = (message) => this._onMessage(message);
|
|
153
|
-
this._connection.
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
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
|
|
189
|
+
const args = [
|
|
191
190
|
cliPath,
|
|
192
191
|
"run-mcp-server",
|
|
193
|
-
`--
|
|
194
|
-
|
|
195
|
-
|
|
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",
|
|
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
|
-
|
|
208
|
-
if (
|
|
209
|
-
console.log(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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(
|
|
273
|
+
constructor(clientInfo, sessions) {
|
|
274
|
+
this.clientInfo = clientInfo;
|
|
246
275
|
this.sessions = sessions;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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(
|
|
291
|
+
return new SessionManager(clientInfo, sessions);
|
|
265
292
|
}
|
|
266
|
-
async
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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.
|
|
315
|
+
session.disconnect();
|
|
276
316
|
}
|
|
277
|
-
async
|
|
278
|
-
sessionName = this._resolveSessionName(
|
|
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(`
|
|
321
|
+
console.log(`Browser '${sessionName}' is not open.`);
|
|
282
322
|
return;
|
|
283
323
|
}
|
|
284
324
|
await session.stop();
|
|
285
325
|
}
|
|
286
|
-
async
|
|
287
|
-
sessionName = this._resolveSessionName(
|
|
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
|
|
330
|
+
console.log(`No user data found for browser '${sessionName}'.`);
|
|
291
331
|
return;
|
|
292
332
|
}
|
|
293
|
-
await session.
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
345
|
-
process.exit(1);
|
|
368
|
+
return void 0;
|
|
346
369
|
}
|
|
347
|
-
const
|
|
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"
|
|
382
|
+
return import_path.default.join(localCacheDir, "ms-playwright", "daemon");
|
|
365
383
|
})();
|
|
366
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (
|
|
374
|
-
|
|
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(
|
|
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(
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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 (
|
|
408
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
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 = {
|