@guanzhu.me/pw-cli 0.0.15 → 0.0.17

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/bin/pw-cli.js CHANGED
@@ -305,6 +305,68 @@ Example:
305
305
  `;
306
306
  }
307
307
 
308
+ const REQUIRED_POSITIONAL_ARGS = new Map([
309
+ ['goto', { count: 1, usage: 'pw-cli goto <url>' }],
310
+ ['type', { count: 1, usage: 'pw-cli type <text>' }],
311
+ ['click', { count: 1, usage: 'pw-cli click <ref> [button]' }],
312
+ ['dblclick', { count: 1, usage: 'pw-cli dblclick <ref> [button]' }],
313
+ ['fill', { count: 2, usage: 'pw-cli fill <ref> <text>' }],
314
+ ['drag', { count: 2, usage: 'pw-cli drag <startRef> <endRef>' }],
315
+ ['hover', { count: 1, usage: 'pw-cli hover <ref>' }],
316
+ ['select', { count: 2, usage: 'pw-cli select <ref> <value>' }],
317
+ ['upload', { count: 1, usage: 'pw-cli upload <file>' }],
318
+ ['check', { count: 1, usage: 'pw-cli check <ref>' }],
319
+ ['uncheck', { count: 1, usage: 'pw-cli uncheck <ref>' }],
320
+ ['eval', { count: 1, usage: 'pw-cli eval <func> [ref]' }],
321
+ ['resize', { count: 2, usage: 'pw-cli resize <width> <height>' }],
322
+ ['press', { count: 1, usage: 'pw-cli press <key>' }],
323
+ ['keydown', { count: 1, usage: 'pw-cli keydown <key>' }],
324
+ ['keyup', { count: 1, usage: 'pw-cli keyup <key>' }],
325
+ ['mousemove', { count: 2, usage: 'pw-cli mousemove <x> <y>' }],
326
+ ['mousewheel', { count: 2, usage: 'pw-cli mousewheel <dx> <dy>' }],
327
+ ['tab-select', { count: 1, usage: 'pw-cli tab-select <index>' }],
328
+ ['cookie-get', { count: 1, usage: 'pw-cli cookie-get <name>' }],
329
+ ['cookie-set', { count: 2, usage: 'pw-cli cookie-set <name> <value>' }],
330
+ ['cookie-delete', { count: 1, usage: 'pw-cli cookie-delete <name>' }],
331
+ ['localstorage-get', { count: 1, usage: 'pw-cli localstorage-get <key>' }],
332
+ ['localstorage-set', { count: 2, usage: 'pw-cli localstorage-set <key> <value>' }],
333
+ ['localstorage-delete', { count: 1, usage: 'pw-cli localstorage-delete <key>' }],
334
+ ['sessionstorage-get', { count: 1, usage: 'pw-cli sessionstorage-get <key>' }],
335
+ ['sessionstorage-set', { count: 2, usage: 'pw-cli sessionstorage-set <key> <value>' }],
336
+ ['sessionstorage-delete', { count: 1, usage: 'pw-cli sessionstorage-delete <key>' }],
337
+ ['route', { count: 1, usage: 'pw-cli route <pattern>' }],
338
+ ]);
339
+
340
+ function getPositionalsAfterCommand(argv, command) {
341
+ const commandIdx = argv.indexOf(command);
342
+ if (commandIdx === -1) return [];
343
+
344
+ const positionals = [];
345
+ for (let i = commandIdx + 1; i < argv.length; i++) {
346
+ const arg = argv[i];
347
+ if (typeof arg !== 'string' || arg.length === 0) continue;
348
+ if (arg === '--') {
349
+ positionals.push(...argv.slice(i + 1).filter(Boolean));
350
+ break;
351
+ }
352
+ if (!arg.startsWith('-')) {
353
+ positionals.push(arg);
354
+ }
355
+ }
356
+ return positionals;
357
+ }
358
+
359
+ function validateRequiredArgs(argv, command) {
360
+ const rule = REQUIRED_POSITIONAL_ARGS.get(command);
361
+ if (!rule) return;
362
+
363
+ const positionals = getPositionalsAfterCommand(argv, command);
364
+ if (positionals.length >= rule.count) return;
365
+
366
+ process.stderr.write(`pw-cli: ${command} requires ${rule.count === 1 ? 'an argument' : `${rule.count} arguments`}\n\nUsage: ${rule.usage}\n`);
367
+ process.exit(1);
368
+ }
369
+
308
370
  // Management commands that don't need a running browser
309
371
  const MGMT_COMMANDS = new Set([
310
372
  'open', 'close', 'list', 'kill-all', 'close-all', 'delete-data',
@@ -753,6 +815,8 @@ async function main() {
753
815
  process.exit(1);
754
816
  }
755
817
 
818
+ validateRequiredArgs(rawArgv, command);
819
+
756
820
  // ── queue: batch actions and run them together ────────────────────────────
757
821
  if (command === 'queue') {
758
822
  await handleQueue(rawArgv);
@@ -771,6 +835,43 @@ async function main() {
771
835
  return;
772
836
  }
773
837
 
838
+ // ── Fast path: send command directly to playwright-cli daemon socket ────
839
+ // Skip heavy setup (npm root -g, require playwright, CDP probes) entirely.
840
+ // Only for commands that the daemon handles AND don't need local preprocessing.
841
+ if (command && !MGMT_COMMANDS.has(command)) {
842
+ // Build the args array the daemon expects: strip session flags, keep command + args
843
+ let fastArgs = [...rawArgv];
844
+ // Remove session flags (-s xxx / --session xxx / --session=xxx)
845
+ fastArgs = fastArgs.filter((a, i, arr) => {
846
+ if (a === '-s' || a === '--session') { arr[i + 1] = undefined; return false; }
847
+ if (a === undefined) return false;
848
+ if (a.startsWith('-s=') || a.startsWith('--session=')) return false;
849
+ return true;
850
+ }).filter(Boolean);
851
+
852
+ // Apply XPath conversion if needed
853
+ fastArgs = convertXPathCommand(fastArgs);
854
+
855
+ // Handle run-code wrapping for XPath-converted commands
856
+ if (fastArgs[0] === 'run-code' && fastArgs.length > 1) {
857
+ const code = fastArgs.slice(1).join(' ');
858
+ fastArgs = ['run-code', wrapCodeIfNeeded(code)];
859
+ }
860
+
861
+ const { sendCommand } = require('../src/fast-send');
862
+ const result = await sendCommand(fastArgs, session);
863
+ if (result !== null) {
864
+ // Daemon responded — use its result
865
+ if (result.isError) {
866
+ process.stderr.write(`${result.text}\n`);
867
+ process.exit(1);
868
+ }
869
+ if (result.text) process.stdout.write(result.text + '\n');
870
+ return;
871
+ }
872
+ // result === null means daemon not running — fall through to full path
873
+ }
874
+
774
875
  // ── From here on: delegate to playwright-cli (with enhancements) ─────────
775
876
  const cliPath = findPlaywrightCli();
776
877
  if (!cliPath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanzhu.me/pw-cli",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "Persistent Playwright browser CLI with headed defaults, profile support, queueing, and script execution",
5
5
  "bin": {
6
6
  "pw-cli": "./bin/pw-cli.js"
@@ -34,5 +34,13 @@
34
34
  "chromium",
35
35
  "testing"
36
36
  ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/wn0x00/pw-cli.git"
40
+ },
41
+ "homepage": "https://github.com/wn0x00/pw-cli#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/wn0x00/pw-cli/issues"
44
+ },
37
45
  "license": "MIT"
38
46
  }
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ const net = require('net');
4
+ const os = require('os');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const crypto = require('crypto');
8
+
9
+ const HOME_DIR = os.homedir();
10
+ const PW_CLI_DIR = path.join(HOME_DIR, '.pw-cli');
11
+ const WORKSPACE_HASH = crypto.createHash('sha1')
12
+ .update(PW_CLI_DIR)
13
+ .digest('hex')
14
+ .substring(0, 16);
15
+
16
+ function getSocketPath(sessionName = 'default') {
17
+ const socketName = `${sessionName}.sock`;
18
+ if (os.platform() === 'win32')
19
+ return `\\\\.\\pipe\\${WORKSPACE_HASH}-${socketName}`;
20
+ const socketsDir = process.env.PLAYWRIGHT_DAEMON_SOCKETS_DIR || path.join(os.tmpdir(), 'playwright-cli');
21
+ return path.join(socketsDir, WORKSPACE_HASH, socketName);
22
+ }
23
+
24
+ function getVersion() {
25
+ // Read version from session file (already written by playwright-cli daemon)
26
+ const sessionFile = path.join(
27
+ os.platform() === 'win32'
28
+ ? path.join(process.env.LOCALAPPDATA || path.join(HOME_DIR, 'AppData', 'Local'), 'ms-playwright', 'daemon')
29
+ : os.platform() === 'darwin'
30
+ ? path.join(HOME_DIR, 'Library', 'Caches', 'ms-playwright', 'daemon')
31
+ : path.join(process.env.XDG_CACHE_HOME || path.join(HOME_DIR, '.cache'), 'ms-playwright', 'daemon'),
32
+ WORKSPACE_HASH,
33
+ 'default.session'
34
+ );
35
+ try {
36
+ const session = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
37
+ return session.version;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Convert a string array to minimist-style object.
45
+ * The daemon expects { _: [cmd, arg1, ...], flagName: value, ... }.
46
+ */
47
+ function toMinimistArgs(argv) {
48
+ const result = { _: [] };
49
+ for (let i = 0; i < argv.length; i++) {
50
+ const arg = argv[i];
51
+ if (arg.startsWith('--')) {
52
+ const key = arg.slice(2);
53
+ const next = argv[i + 1];
54
+ if (next && !next.startsWith('-')) {
55
+ result[key] = next;
56
+ i++;
57
+ } else {
58
+ result[key] = true;
59
+ }
60
+ } else if (arg.startsWith('-') && arg.length === 2) {
61
+ const key = arg.slice(1);
62
+ const next = argv[i + 1];
63
+ if (next && !next.startsWith('-')) {
64
+ result[key] = next;
65
+ i++;
66
+ } else {
67
+ result[key] = true;
68
+ }
69
+ } else {
70
+ result._.push(arg);
71
+ }
72
+ }
73
+ return result;
74
+ }
75
+
76
+ /**
77
+ * Send a command directly to the playwright-cli daemon socket.
78
+ * Returns { text, isError } or null (daemon not running).
79
+ */
80
+ function sendCommand(args, sessionName = 'default') {
81
+ const socketPath = getSocketPath(sessionName);
82
+ const version = getVersion();
83
+ if (!version) return Promise.resolve(null);
84
+
85
+ const minimistArgs = toMinimistArgs(args);
86
+
87
+ return new Promise((resolve, reject) => {
88
+ const socket = net.createConnection(socketPath, () => {
89
+ const message = JSON.stringify({
90
+ id: 1,
91
+ method: 'run',
92
+ params: { args: minimistArgs, cwd: process.cwd() },
93
+ version,
94
+ }) + '\n';
95
+ socket.write(message);
96
+ });
97
+
98
+ let buf = '';
99
+ socket.on('data', chunk => {
100
+ buf += chunk.toString();
101
+ const nlIdx = buf.indexOf('\n');
102
+ if (nlIdx === -1) return;
103
+ const line = buf.slice(0, nlIdx);
104
+ clearTimeout(timer);
105
+ socket.destroy();
106
+ try {
107
+ const resp = JSON.parse(line);
108
+ if (resp.error) {
109
+ reject(new Error(resp.error));
110
+ } else {
111
+ resolve(resp.result);
112
+ }
113
+ } catch (e) {
114
+ reject(new Error('Invalid daemon response'));
115
+ }
116
+ });
117
+
118
+ socket.on('error', () => { clearTimeout(timer); resolve(null); }); // connection failed = daemon not running
119
+ const timer = setTimeout(() => { socket.destroy(); resolve(null); }, 3000);
120
+ });
121
+ }
122
+
123
+ module.exports = { sendCommand, getSocketPath, getVersion };