@jackwener/opencli 0.5.2 → 0.6.1
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/README.md +23 -7
- package/README.zh-CN.md +24 -8
- package/SKILL.md +6 -2
- package/dist/cli-manifest.json +80 -0
- package/dist/clis/coupang/add-to-cart.d.ts +1 -0
- package/dist/clis/coupang/add-to-cart.js +141 -0
- package/dist/clis/coupang/search.d.ts +1 -0
- package/dist/clis/coupang/search.js +453 -0
- package/dist/coupang.d.ts +24 -0
- package/dist/coupang.js +262 -0
- package/dist/coupang.test.d.ts +1 -0
- package/dist/coupang.test.js +62 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +226 -25
- package/dist/doctor.test.js +13 -6
- package/dist/main.js +7 -0
- package/dist/setup.d.ts +4 -0
- package/dist/setup.js +145 -0
- package/dist/tui.d.ts +22 -0
- package/dist/tui.js +139 -0
- package/package.json +1 -1
- package/src/clis/coupang/add-to-cart.ts +149 -0
- package/src/clis/coupang/search.ts +466 -0
- package/src/coupang.test.ts +78 -0
- package/src/coupang.ts +302 -0
- package/src/doctor.test.ts +15 -6
- package/src/doctor.ts +221 -25
- package/src/main.ts +8 -0
- package/src/setup.ts +169 -0
- package/src/tui.ts +171 -0
package/dist/doctor.js
CHANGED
|
@@ -1,23 +1,57 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as os from 'node:os';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
4
5
|
import { createInterface } from 'node:readline/promises';
|
|
5
6
|
import { stdin as input, stdout as output } from 'node:process';
|
|
7
|
+
import chalk from 'chalk';
|
|
6
8
|
import { getTokenFingerprint } from './browser.js';
|
|
7
9
|
const PLAYWRIGHT_SERVER_NAME = 'playwright';
|
|
8
|
-
const PLAYWRIGHT_TOKEN_ENV = 'PLAYWRIGHT_MCP_EXTENSION_TOKEN';
|
|
10
|
+
export const PLAYWRIGHT_TOKEN_ENV = 'PLAYWRIGHT_MCP_EXTENSION_TOKEN';
|
|
9
11
|
const PLAYWRIGHT_EXTENSION_ID = 'mmlmfjhmonkocbjadbfplnigmagldckm';
|
|
10
12
|
const TOKEN_LINE_RE = /^(\s*export\s+PLAYWRIGHT_MCP_EXTENSION_TOKEN=)(['"]?)([^'"\\\n]+)\2\s*$/m;
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
+
function colorLabel(status) {
|
|
14
|
+
switch (status) {
|
|
15
|
+
case 'OK': return chalk.green('[OK]');
|
|
16
|
+
case 'MISSING': return chalk.red('[MISSING]');
|
|
17
|
+
case 'MISMATCH': return chalk.yellow('[MISMATCH]');
|
|
18
|
+
case 'WARN': return chalk.yellow('[WARN]');
|
|
19
|
+
}
|
|
13
20
|
}
|
|
14
21
|
function statusLine(status, text) {
|
|
15
|
-
return `${
|
|
22
|
+
return `${colorLabel(status)} ${text}`;
|
|
16
23
|
}
|
|
17
24
|
function tokenSummary(token, fingerprint) {
|
|
18
25
|
if (!token)
|
|
19
|
-
return 'missing';
|
|
20
|
-
return `configured (${fingerprint})`;
|
|
26
|
+
return chalk.dim('missing');
|
|
27
|
+
return `configured ${chalk.dim(`(${fingerprint})`)}`;
|
|
28
|
+
}
|
|
29
|
+
export function shortenPath(p) {
|
|
30
|
+
const home = os.homedir();
|
|
31
|
+
return home && p.startsWith(home) ? '~' + p.slice(home.length) : p;
|
|
32
|
+
}
|
|
33
|
+
export function toolName(p) {
|
|
34
|
+
if (p.includes('.codex/'))
|
|
35
|
+
return 'Codex';
|
|
36
|
+
if (p.includes('.cursor/'))
|
|
37
|
+
return 'Cursor';
|
|
38
|
+
if (p.includes('.claude.json'))
|
|
39
|
+
return 'Claude Code';
|
|
40
|
+
if (p.includes('antigravity'))
|
|
41
|
+
return 'Antigravity';
|
|
42
|
+
if (p.includes('.gemini/settings'))
|
|
43
|
+
return 'Gemini CLI';
|
|
44
|
+
if (p.includes('opencode'))
|
|
45
|
+
return 'OpenCode';
|
|
46
|
+
if (p.includes('Claude/claude_desktop'))
|
|
47
|
+
return 'Claude Desktop';
|
|
48
|
+
if (p.includes('.vscode/'))
|
|
49
|
+
return 'VS Code';
|
|
50
|
+
if (p.includes('.mcp.json'))
|
|
51
|
+
return 'Project MCP';
|
|
52
|
+
if (p.includes('.zshrc') || p.includes('.bashrc') || p.includes('.profile'))
|
|
53
|
+
return 'Shell';
|
|
54
|
+
return '';
|
|
21
55
|
}
|
|
22
56
|
export function getDefaultShellRcPath() {
|
|
23
57
|
const shell = process.env.SHELL ?? '';
|
|
@@ -33,12 +67,16 @@ export function getDefaultMcpConfigPaths(cwd = process.cwd()) {
|
|
|
33
67
|
path.join(home, '.codex', 'config.toml'),
|
|
34
68
|
path.join(home, '.codex', 'mcp.json'),
|
|
35
69
|
path.join(home, '.cursor', 'mcp.json'),
|
|
70
|
+
path.join(home, '.claude.json'),
|
|
71
|
+
path.join(home, '.gemini', 'settings.json'),
|
|
72
|
+
path.join(home, '.gemini', 'antigravity', 'mcp_config.json'),
|
|
36
73
|
path.join(home, '.config', 'opencode', 'opencode.json'),
|
|
37
74
|
path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
|
|
38
75
|
path.join(home, '.config', 'Claude', 'claude_desktop_config.json'),
|
|
39
76
|
path.join(cwd, '.cursor', 'mcp.json'),
|
|
40
77
|
path.join(cwd, '.vscode', 'mcp.json'),
|
|
41
78
|
path.join(cwd, '.opencode', 'opencode.json'),
|
|
79
|
+
path.join(cwd, '.mcp.json'),
|
|
42
80
|
];
|
|
43
81
|
return [...new Set(candidates)];
|
|
44
82
|
}
|
|
@@ -119,7 +157,7 @@ export function upsertTomlConfigToken(content, token) {
|
|
|
119
157
|
const prefix = content.trim() ? `${content.replace(/\s*$/, '')}\n\n` : '';
|
|
120
158
|
return `${prefix}[mcp_servers.playwright]\ntype = "stdio"\ncommand = "npx"\nargs = ["-y", "@playwright/mcp@latest", "--extension"]\n\n[mcp_servers.playwright.env]\n${tokenLine}\n`;
|
|
121
159
|
}
|
|
122
|
-
function fileExists(filePath) {
|
|
160
|
+
export function fileExists(filePath) {
|
|
123
161
|
try {
|
|
124
162
|
return fs.existsSync(filePath);
|
|
125
163
|
}
|
|
@@ -169,6 +207,144 @@ function readConfigStatus(filePath) {
|
|
|
169
207
|
};
|
|
170
208
|
}
|
|
171
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Discover the auth token stored by the Playwright MCP Bridge extension
|
|
212
|
+
* by scanning Chrome's LevelDB localStorage files directly.
|
|
213
|
+
*
|
|
214
|
+
* Uses `strings` + `grep` for fast binary scanning on macOS/Linux,
|
|
215
|
+
* with a pure-Node fallback on Windows.
|
|
216
|
+
*/
|
|
217
|
+
export function discoverExtensionToken() {
|
|
218
|
+
const home = os.homedir();
|
|
219
|
+
const platform = os.platform();
|
|
220
|
+
const bases = [];
|
|
221
|
+
if (platform === 'darwin') {
|
|
222
|
+
bases.push(path.join(home, 'Library', 'Application Support', 'Google', 'Chrome'), path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Canary'), path.join(home, 'Library', 'Application Support', 'Chromium'), path.join(home, 'Library', 'Application Support', 'Microsoft Edge'));
|
|
223
|
+
}
|
|
224
|
+
else if (platform === 'linux') {
|
|
225
|
+
bases.push(path.join(home, '.config', 'google-chrome'), path.join(home, '.config', 'chromium'), path.join(home, '.config', 'microsoft-edge'));
|
|
226
|
+
}
|
|
227
|
+
else if (platform === 'win32') {
|
|
228
|
+
const appData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
229
|
+
bases.push(path.join(appData, 'Google', 'Chrome', 'User Data'), path.join(appData, 'Microsoft', 'Edge', 'User Data'));
|
|
230
|
+
}
|
|
231
|
+
const profiles = ['Default', 'Profile 1', 'Profile 2', 'Profile 3'];
|
|
232
|
+
// Token is 43 chars of base64url (from 32 random bytes)
|
|
233
|
+
const tokenRe = /([A-Za-z0-9_-]{40,50})/;
|
|
234
|
+
for (const base of bases) {
|
|
235
|
+
for (const profile of profiles) {
|
|
236
|
+
const dir = path.join(base, profile, 'Local Storage', 'leveldb');
|
|
237
|
+
if (!fileExists(dir))
|
|
238
|
+
continue;
|
|
239
|
+
// Fast path: use strings + grep to find candidate files and extract token
|
|
240
|
+
if (platform !== 'win32') {
|
|
241
|
+
const token = extractTokenViaStrings(dir, tokenRe);
|
|
242
|
+
if (token)
|
|
243
|
+
return token;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
// Slow path (Windows): read binary files directly
|
|
247
|
+
const token = extractTokenViaBinaryRead(dir, tokenRe);
|
|
248
|
+
if (token)
|
|
249
|
+
return token;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
function extractTokenViaStrings(dir, tokenRe) {
|
|
255
|
+
try {
|
|
256
|
+
// Single shell pipeline: for each LevelDB file, extract strings, find lines
|
|
257
|
+
// after the extension ID, and filter for base64url token pattern.
|
|
258
|
+
//
|
|
259
|
+
// LevelDB `strings` output for the extension's auth-token entry:
|
|
260
|
+
// auth-token ← key name
|
|
261
|
+
// 4,mmlmfjhmonkocbjadbfplnigmagldckm.7 ← LevelDB internal key
|
|
262
|
+
// hqI86ncsD1QpcVcj-k9CyzTF-ieCQd_4KreZ_wy1WHA ← token value
|
|
263
|
+
//
|
|
264
|
+
// We get the line immediately after any EXTENSION_ID mention and check
|
|
265
|
+
// if it looks like a base64url token (40-50 chars, [A-Za-z0-9_-]).
|
|
266
|
+
const shellDir = dir.replace(/'/g, "'\\''");
|
|
267
|
+
const cmd = `for f in '${shellDir}'/*.ldb '${shellDir}'/*.log; do ` +
|
|
268
|
+
`[ -f "$f" ] && strings "$f" 2>/dev/null | ` +
|
|
269
|
+
`grep -A1 '${PLAYWRIGHT_EXTENSION_ID}' | ` +
|
|
270
|
+
`grep -v '${PLAYWRIGHT_EXTENSION_ID}' | ` +
|
|
271
|
+
`grep -E '^[A-Za-z0-9_-]{40,50}$' | head -1; ` +
|
|
272
|
+
`done 2>/dev/null`;
|
|
273
|
+
const result = execSync(cmd, { encoding: 'utf-8', timeout: 10000 }).trim();
|
|
274
|
+
// Take the first non-empty line
|
|
275
|
+
for (const line of result.split('\n')) {
|
|
276
|
+
const token = line.trim();
|
|
277
|
+
if (token && validateBase64urlToken(token))
|
|
278
|
+
return token;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch { }
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
function extractTokenViaBinaryRead(dir, tokenRe) {
|
|
285
|
+
const extIdBuf = Buffer.from(PLAYWRIGHT_EXTENSION_ID);
|
|
286
|
+
const keyBuf = Buffer.from('auth-token');
|
|
287
|
+
let files;
|
|
288
|
+
try {
|
|
289
|
+
files = fs.readdirSync(dir)
|
|
290
|
+
.filter(f => f.endsWith('.ldb') || f.endsWith('.log'))
|
|
291
|
+
.map(f => path.join(dir, f));
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
// Sort by mtime descending
|
|
297
|
+
files.sort((a, b) => {
|
|
298
|
+
try {
|
|
299
|
+
return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs;
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
for (const file of files) {
|
|
306
|
+
let data;
|
|
307
|
+
try {
|
|
308
|
+
data = fs.readFileSync(file);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
// Quick check: does file contain both the extension ID and auth-token key?
|
|
314
|
+
const extPos = data.indexOf(extIdBuf);
|
|
315
|
+
if (extPos === -1)
|
|
316
|
+
continue;
|
|
317
|
+
const keyPos = data.indexOf(keyBuf, Math.max(0, extPos - 500));
|
|
318
|
+
if (keyPos === -1)
|
|
319
|
+
continue;
|
|
320
|
+
// Scan for token value after auth-token key
|
|
321
|
+
let idx = 0;
|
|
322
|
+
while (true) {
|
|
323
|
+
const kp = data.indexOf(keyBuf, idx);
|
|
324
|
+
if (kp === -1)
|
|
325
|
+
break;
|
|
326
|
+
const contextStart = Math.max(0, kp - 500);
|
|
327
|
+
if (data.indexOf(extIdBuf, contextStart) !== -1 && data.indexOf(extIdBuf, contextStart) < kp) {
|
|
328
|
+
const after = data.subarray(kp + keyBuf.length, kp + keyBuf.length + 200).toString('latin1');
|
|
329
|
+
const m = after.match(tokenRe);
|
|
330
|
+
if (m && validateBase64urlToken(m[1]))
|
|
331
|
+
return m[1];
|
|
332
|
+
}
|
|
333
|
+
idx = kp + 1;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
function validateBase64urlToken(token) {
|
|
339
|
+
try {
|
|
340
|
+
const b64 = token.replace(/-/g, '+').replace(/_/g, '/');
|
|
341
|
+
const decoded = Buffer.from(b64, 'base64');
|
|
342
|
+
return decoded.length >= 28 && decoded.length <= 36;
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
172
348
|
export async function runBrowserDoctor(opts = {}) {
|
|
173
349
|
const envToken = process.env[PLAYWRIGHT_TOKEN_ENV] ?? null;
|
|
174
350
|
const shellPath = opts.shellRc ?? getDefaultShellRcPath();
|
|
@@ -181,18 +357,23 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
181
357
|
});
|
|
182
358
|
const configPaths = opts.configPaths?.length ? opts.configPaths : getDefaultMcpConfigPaths();
|
|
183
359
|
const configs = configPaths.map(readConfigStatus);
|
|
360
|
+
// Try to discover the token directly from the Chrome extension's localStorage
|
|
361
|
+
const extensionToken = discoverExtensionToken();
|
|
184
362
|
const allTokens = [
|
|
185
363
|
opts.token ?? null,
|
|
364
|
+
extensionToken,
|
|
186
365
|
envToken,
|
|
187
366
|
...shellFiles.map(s => s.token),
|
|
188
367
|
...configs.map(c => c.token),
|
|
189
368
|
].filter((v) => !!v);
|
|
190
369
|
const uniqueTokens = [...new Set(allTokens)];
|
|
191
|
-
const recommendedToken = opts.token ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] : null) ?? null;
|
|
370
|
+
const recommendedToken = opts.token ?? extensionToken ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] : null) ?? null;
|
|
192
371
|
const report = {
|
|
193
372
|
cliVersion: opts.cliVersion,
|
|
194
373
|
envToken,
|
|
195
374
|
envFingerprint: getTokenFingerprint(envToken ?? undefined),
|
|
375
|
+
extensionToken,
|
|
376
|
+
extensionFingerprint: getTokenFingerprint(extensionToken ?? undefined),
|
|
196
377
|
shellFiles,
|
|
197
378
|
configs,
|
|
198
379
|
recommendedToken,
|
|
@@ -219,24 +400,29 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
219
400
|
}
|
|
220
401
|
export function renderBrowserDoctorReport(report) {
|
|
221
402
|
const tokenFingerprints = [
|
|
403
|
+
report.extensionFingerprint,
|
|
222
404
|
report.envFingerprint,
|
|
223
405
|
...report.shellFiles.map(shell => shell.fingerprint),
|
|
224
406
|
...report.configs.filter(config => config.exists).map(config => config.fingerprint),
|
|
225
407
|
].filter((value) => !!value);
|
|
226
408
|
const uniqueFingerprints = [...new Set(tokenFingerprints)];
|
|
227
409
|
const hasMismatch = uniqueFingerprints.length > 1;
|
|
228
|
-
const lines = [`opencli v${report.cliVersion ?? 'unknown'} doctor
|
|
410
|
+
const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), ''];
|
|
411
|
+
const extStatus = !report.extensionToken ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK';
|
|
412
|
+
lines.push(statusLine(extStatus, `Extension token (Chrome LevelDB): ${tokenSummary(report.extensionToken, report.extensionFingerprint)}`));
|
|
229
413
|
const envStatus = !report.envToken ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK';
|
|
230
414
|
lines.push(statusLine(envStatus, `Environment token: ${tokenSummary(report.envToken, report.envFingerprint)}`));
|
|
231
415
|
for (const shell of report.shellFiles) {
|
|
232
416
|
const shellStatus = !shell.token ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK';
|
|
233
|
-
|
|
417
|
+
const tool = toolName(shell.path);
|
|
418
|
+
const suffix = tool ? chalk.dim(` [${tool}]`) : '';
|
|
419
|
+
lines.push(statusLine(shellStatus, `${shortenPath(shell.path)}${suffix}: ${tokenSummary(shell.token, shell.fingerprint)}`));
|
|
234
420
|
}
|
|
235
421
|
const existingConfigs = report.configs.filter(config => config.exists);
|
|
236
422
|
const missingConfigCount = report.configs.length - existingConfigs.length;
|
|
237
423
|
if (existingConfigs.length > 0) {
|
|
238
424
|
for (const config of existingConfigs) {
|
|
239
|
-
const parseSuffix = config.parseError ? ` (parse error
|
|
425
|
+
const parseSuffix = config.parseError ? chalk.red(` (parse error)`) : '';
|
|
240
426
|
const configStatus = config.parseError
|
|
241
427
|
? 'WARN'
|
|
242
428
|
: !config.token
|
|
@@ -244,25 +430,27 @@ export function renderBrowserDoctorReport(report) {
|
|
|
244
430
|
: hasMismatch
|
|
245
431
|
? 'MISMATCH'
|
|
246
432
|
: 'OK';
|
|
247
|
-
|
|
433
|
+
const tool = toolName(config.path);
|
|
434
|
+
const suffix = tool ? chalk.dim(` [${tool}]`) : '';
|
|
435
|
+
lines.push(statusLine(configStatus, `${shortenPath(config.path)}${suffix}: ${tokenSummary(config.token, config.fingerprint)}${parseSuffix}`));
|
|
248
436
|
}
|
|
249
437
|
}
|
|
250
438
|
else {
|
|
251
|
-
lines.push(statusLine('MISSING', 'MCP config: no existing config files found
|
|
439
|
+
lines.push(statusLine('MISSING', 'MCP config: no existing config files found'));
|
|
252
440
|
}
|
|
253
441
|
if (missingConfigCount > 0)
|
|
254
|
-
lines.push(` Other scanned config locations not present: ${missingConfigCount}`);
|
|
442
|
+
lines.push(chalk.dim(` Other scanned config locations not present: ${missingConfigCount}`));
|
|
255
443
|
lines.push('');
|
|
256
444
|
lines.push(statusLine(hasMismatch ? 'MISMATCH' : report.recommendedToken ? 'OK' : 'WARN', `Recommended token fingerprint: ${report.recommendedFingerprint ?? 'unavailable'}`));
|
|
257
445
|
if (report.issues.length) {
|
|
258
|
-
lines.push('', 'Issues:');
|
|
446
|
+
lines.push('', chalk.yellow('Issues:'));
|
|
259
447
|
for (const issue of report.issues)
|
|
260
|
-
lines.push(
|
|
448
|
+
lines.push(chalk.dim(` • ${issue}`));
|
|
261
449
|
}
|
|
262
450
|
if (report.warnings.length) {
|
|
263
|
-
lines.push('', 'Warnings:');
|
|
451
|
+
lines.push('', chalk.yellow('Warnings:'));
|
|
264
452
|
for (const warning of report.warnings)
|
|
265
|
-
lines.push(
|
|
453
|
+
lines.push(chalk.dim(` • ${warning}`));
|
|
266
454
|
}
|
|
267
455
|
return lines.join('\n');
|
|
268
456
|
}
|
|
@@ -276,7 +464,7 @@ async function confirmPrompt(question) {
|
|
|
276
464
|
rl.close();
|
|
277
465
|
}
|
|
278
466
|
}
|
|
279
|
-
function writeFileWithMkdir(filePath, content) {
|
|
467
|
+
export function writeFileWithMkdir(filePath, content) {
|
|
280
468
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
281
469
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
282
470
|
}
|
|
@@ -284,25 +472,38 @@ export async function applyBrowserDoctorFix(report, opts = {}) {
|
|
|
284
472
|
const token = opts.token ?? report.recommendedToken;
|
|
285
473
|
if (!token)
|
|
286
474
|
throw new Error('No Playwright MCP token is available to write. Provide --token first.');
|
|
475
|
+
const fp = getTokenFingerprint(token);
|
|
287
476
|
const plannedWrites = [];
|
|
288
477
|
const shellPath = opts.shellRc ?? report.shellFiles[0]?.path ?? getDefaultShellRcPath();
|
|
289
|
-
|
|
478
|
+
const shellStatus = report.shellFiles.find(s => s.path === shellPath);
|
|
479
|
+
if (shellStatus?.fingerprint !== fp)
|
|
480
|
+
plannedWrites.push(shellPath);
|
|
290
481
|
for (const config of report.configs) {
|
|
291
482
|
if (!config.writable)
|
|
292
483
|
continue;
|
|
484
|
+
if (config.fingerprint === fp)
|
|
485
|
+
continue; // already correct
|
|
293
486
|
plannedWrites.push(config.path);
|
|
294
487
|
}
|
|
488
|
+
if (plannedWrites.length === 0) {
|
|
489
|
+
console.log(chalk.green('All config files are already up to date.'));
|
|
490
|
+
return [];
|
|
491
|
+
}
|
|
295
492
|
if (!opts.yes) {
|
|
296
|
-
const ok = await confirmPrompt(`Update ${plannedWrites.length} file(s) with Playwright MCP token fingerprint ${
|
|
493
|
+
const ok = await confirmPrompt(`Update ${plannedWrites.length} file(s) with Playwright MCP token fingerprint ${fp}?`);
|
|
297
494
|
if (!ok)
|
|
298
495
|
return [];
|
|
299
496
|
}
|
|
300
497
|
const written = [];
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
498
|
+
if (plannedWrites.includes(shellPath)) {
|
|
499
|
+
const shellBefore = fileExists(shellPath) ? fs.readFileSync(shellPath, 'utf-8') : '';
|
|
500
|
+
writeFileWithMkdir(shellPath, upsertShellToken(shellBefore, token));
|
|
501
|
+
written.push(shellPath);
|
|
502
|
+
}
|
|
304
503
|
for (const config of report.configs) {
|
|
305
|
-
if (!
|
|
504
|
+
if (!plannedWrites.includes(config.path))
|
|
505
|
+
continue;
|
|
506
|
+
if (config.parseError)
|
|
306
507
|
continue;
|
|
307
508
|
const before = fileExists(config.path) ? fs.readFileSync(config.path, 'utf-8') : '';
|
|
308
509
|
const next = config.format === 'toml' ? upsertTomlConfigToken(before, token) : upsertJsonConfigToken(before, token);
|
package/dist/doctor.test.js
CHANGED
|
@@ -70,33 +70,40 @@ describe('json token helpers', () => {
|
|
|
70
70
|
});
|
|
71
71
|
});
|
|
72
72
|
describe('doctor report rendering', () => {
|
|
73
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
73
74
|
it('renders OK-style report when tokens match', () => {
|
|
74
|
-
const text = renderBrowserDoctorReport({
|
|
75
|
+
const text = strip(renderBrowserDoctorReport({
|
|
75
76
|
envToken: 'abc123',
|
|
76
77
|
envFingerprint: 'fp1',
|
|
78
|
+
extensionToken: 'abc123',
|
|
79
|
+
extensionFingerprint: 'fp1',
|
|
77
80
|
shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'abc123', fingerprint: 'fp1' }],
|
|
78
81
|
configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
|
|
79
82
|
recommendedToken: 'abc123',
|
|
80
83
|
recommendedFingerprint: 'fp1',
|
|
81
84
|
warnings: [],
|
|
82
85
|
issues: [],
|
|
83
|
-
});
|
|
86
|
+
}));
|
|
84
87
|
expect(text).toContain('[OK] Environment token: configured (fp1)');
|
|
85
|
-
expect(text).toContain('[OK]
|
|
88
|
+
expect(text).toContain('[OK] /tmp/mcp.json');
|
|
89
|
+
expect(text).toContain('configured (fp1)');
|
|
86
90
|
});
|
|
87
91
|
it('renders MISMATCH-style report when fingerprints differ', () => {
|
|
88
|
-
const text = renderBrowserDoctorReport({
|
|
92
|
+
const text = strip(renderBrowserDoctorReport({
|
|
89
93
|
envToken: 'abc123',
|
|
90
94
|
envFingerprint: 'fp1',
|
|
95
|
+
extensionToken: null,
|
|
96
|
+
extensionFingerprint: null,
|
|
91
97
|
shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'def456', fingerprint: 'fp2' }],
|
|
92
98
|
configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
|
|
93
99
|
recommendedToken: 'abc123',
|
|
94
100
|
recommendedFingerprint: 'fp1',
|
|
95
101
|
warnings: [],
|
|
96
102
|
issues: ['Detected inconsistent Playwright MCP tokens across env/config files.'],
|
|
97
|
-
});
|
|
103
|
+
}));
|
|
98
104
|
expect(text).toContain('[MISMATCH] Environment token: configured (fp1)');
|
|
99
|
-
expect(text).toContain('[MISMATCH]
|
|
105
|
+
expect(text).toContain('[MISMATCH] /tmp/.zshrc');
|
|
106
|
+
expect(text).toContain('configured (fp2)');
|
|
100
107
|
expect(text).toContain('[MISMATCH] Recommended token fingerprint: fp1');
|
|
101
108
|
});
|
|
102
109
|
});
|
package/dist/main.js
CHANGED
|
@@ -114,6 +114,13 @@ program.command('doctor')
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
});
|
|
117
|
+
program.command('setup')
|
|
118
|
+
.description('Interactive setup: configure Playwright MCP token across all detected tools')
|
|
119
|
+
.option('--token <token>', 'Provide token directly instead of auto-detecting')
|
|
120
|
+
.action(async (opts) => {
|
|
121
|
+
const { runSetup } = await import('./setup.js');
|
|
122
|
+
await runSetup({ cliVersion: PKG_VERSION, token: opts.token });
|
|
123
|
+
});
|
|
117
124
|
// ── Dynamic site commands ──────────────────────────────────────────────────
|
|
118
125
|
const registry = getRegistry();
|
|
119
126
|
const siteGroups = new Map();
|
package/dist/setup.d.ts
ADDED
package/dist/setup.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* setup.ts — Interactive Playwright MCP token setup
|
|
3
|
+
*
|
|
4
|
+
* Discovers the extension token, shows an interactive checkbox
|
|
5
|
+
* for selecting which config files to update, and applies changes.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { createInterface } from 'node:readline/promises';
|
|
10
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
11
|
+
import { PLAYWRIGHT_TOKEN_ENV, discoverExtensionToken, fileExists, getDefaultShellRcPath, runBrowserDoctor, shortenPath, toolName, upsertJsonConfigToken, upsertShellToken, upsertTomlConfigToken, writeFileWithMkdir, } from './doctor.js';
|
|
12
|
+
import { getTokenFingerprint } from './browser.js';
|
|
13
|
+
import { checkboxPrompt } from './tui.js';
|
|
14
|
+
export async function runSetup(opts = {}) {
|
|
15
|
+
console.log();
|
|
16
|
+
console.log(chalk.bold(' opencli setup') + chalk.dim(' — Playwright MCP token configuration'));
|
|
17
|
+
console.log();
|
|
18
|
+
// Step 1: Discover token
|
|
19
|
+
let token = opts.token ?? null;
|
|
20
|
+
if (!token) {
|
|
21
|
+
const extensionToken = discoverExtensionToken();
|
|
22
|
+
const envToken = process.env[PLAYWRIGHT_TOKEN_ENV] ?? null;
|
|
23
|
+
if (extensionToken && envToken && extensionToken === envToken) {
|
|
24
|
+
token = extensionToken;
|
|
25
|
+
console.log(` ${chalk.green('✓')} Token auto-discovered from Chrome extension`);
|
|
26
|
+
console.log(` Fingerprint: ${chalk.bold(getTokenFingerprint(token) ?? 'unknown')}`);
|
|
27
|
+
}
|
|
28
|
+
else if (extensionToken) {
|
|
29
|
+
token = extensionToken;
|
|
30
|
+
console.log(` ${chalk.green('✓')} Token discovered from Chrome extension ` +
|
|
31
|
+
chalk.dim(`(${getTokenFingerprint(token)})`));
|
|
32
|
+
if (envToken && envToken !== extensionToken) {
|
|
33
|
+
console.log(` ${chalk.yellow('!')} Environment has different token ` +
|
|
34
|
+
chalk.dim(`(${getTokenFingerprint(envToken)})`));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (envToken) {
|
|
38
|
+
token = envToken;
|
|
39
|
+
console.log(` ${chalk.green('✓')} Token from environment variable ` +
|
|
40
|
+
chalk.dim(`(${getTokenFingerprint(token)})`));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.log(` ${chalk.green('✓')} Using provided token ` +
|
|
45
|
+
chalk.dim(`(${getTokenFingerprint(token)})`));
|
|
46
|
+
}
|
|
47
|
+
if (!token) {
|
|
48
|
+
console.log(` ${chalk.yellow('!')} No token found. Please enter it manually.`);
|
|
49
|
+
console.log(chalk.dim(' (Find it in the Playwright MCP Bridge extension → Status page)'));
|
|
50
|
+
console.log();
|
|
51
|
+
const rl = createInterface({ input, output });
|
|
52
|
+
const answer = await rl.question(' Token: ');
|
|
53
|
+
rl.close();
|
|
54
|
+
token = answer.trim();
|
|
55
|
+
if (!token) {
|
|
56
|
+
console.log(chalk.red('\n No token provided. Aborting.\n'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const fingerprint = getTokenFingerprint(token) ?? 'unknown';
|
|
61
|
+
console.log();
|
|
62
|
+
// Step 2: Scan all config locations
|
|
63
|
+
const report = await runBrowserDoctor({ token, cliVersion: opts.cliVersion });
|
|
64
|
+
// Step 3: Build checkbox items
|
|
65
|
+
const items = [];
|
|
66
|
+
// Shell file
|
|
67
|
+
const shellPath = report.shellFiles[0]?.path ?? getDefaultShellRcPath();
|
|
68
|
+
const shellStatus = report.shellFiles[0];
|
|
69
|
+
const shellFp = shellStatus?.fingerprint;
|
|
70
|
+
const shellOk = shellFp === fingerprint;
|
|
71
|
+
const shellTool = toolName(shellPath) || 'Shell';
|
|
72
|
+
items.push({
|
|
73
|
+
label: padRight(shortenPath(shellPath), 50) + chalk.dim(` [${shellTool}]`),
|
|
74
|
+
value: `shell:${shellPath}`,
|
|
75
|
+
checked: !shellOk,
|
|
76
|
+
status: shellOk ? `configured (${shellFp})` : shellFp ? `mismatch (${shellFp})` : 'missing',
|
|
77
|
+
statusColor: shellOk ? 'green' : shellFp ? 'yellow' : 'red',
|
|
78
|
+
});
|
|
79
|
+
// Config files
|
|
80
|
+
for (const config of report.configs) {
|
|
81
|
+
const fp = config.fingerprint;
|
|
82
|
+
const ok = fp === fingerprint;
|
|
83
|
+
const tool = toolName(config.path);
|
|
84
|
+
items.push({
|
|
85
|
+
label: padRight(shortenPath(config.path), 50) + chalk.dim(tool ? ` [${tool}]` : ''),
|
|
86
|
+
value: `config:${config.path}`,
|
|
87
|
+
checked: false, // let user explicitly select which tools to configure
|
|
88
|
+
status: ok ? `configured (${fp})` : !config.exists ? 'will create' : fp ? `mismatch (${fp})` : 'missing',
|
|
89
|
+
statusColor: ok ? 'green' : 'yellow',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// Step 4: Show interactive checkbox
|
|
93
|
+
console.clear();
|
|
94
|
+
const selected = await checkboxPrompt(items, {
|
|
95
|
+
title: ` ${chalk.bold('opencli setup')} — token ${chalk.cyan(fingerprint)}`,
|
|
96
|
+
});
|
|
97
|
+
if (selected.length === 0) {
|
|
98
|
+
console.log(chalk.dim(' No changes made.\n'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Step 5: Apply changes
|
|
102
|
+
const written = [];
|
|
103
|
+
let wroteShell = false;
|
|
104
|
+
for (const sel of selected) {
|
|
105
|
+
if (sel.startsWith('shell:')) {
|
|
106
|
+
const p = sel.slice('shell:'.length);
|
|
107
|
+
const before = fileExists(p) ? fs.readFileSync(p, 'utf-8') : '';
|
|
108
|
+
writeFileWithMkdir(p, upsertShellToken(before, token));
|
|
109
|
+
written.push(p);
|
|
110
|
+
wroteShell = true;
|
|
111
|
+
}
|
|
112
|
+
else if (sel.startsWith('config:')) {
|
|
113
|
+
const p = sel.slice('config:'.length);
|
|
114
|
+
const config = report.configs.find(c => c.path === p);
|
|
115
|
+
if (config && config.parseError)
|
|
116
|
+
continue;
|
|
117
|
+
const before = fileExists(p) ? fs.readFileSync(p, 'utf-8') : '';
|
|
118
|
+
const format = config?.format ?? (p.endsWith('.toml') ? 'toml' : 'json');
|
|
119
|
+
const next = format === 'toml' ? upsertTomlConfigToken(before, token) : upsertJsonConfigToken(before, token);
|
|
120
|
+
writeFileWithMkdir(p, next);
|
|
121
|
+
written.push(p);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
process.env[PLAYWRIGHT_TOKEN_ENV] = token;
|
|
125
|
+
// Step 6: Summary
|
|
126
|
+
if (written.length > 0) {
|
|
127
|
+
console.log(chalk.green.bold(` ✓ Updated ${written.length} file(s):`));
|
|
128
|
+
for (const p of written) {
|
|
129
|
+
const tool = toolName(p);
|
|
130
|
+
console.log(` ${chalk.dim('•')} ${shortenPath(p)}${tool ? chalk.dim(` [${tool}]`) : ''}`);
|
|
131
|
+
}
|
|
132
|
+
if (wroteShell) {
|
|
133
|
+
console.log();
|
|
134
|
+
console.log(chalk.cyan(` 💡 Run ${chalk.bold(`source ${shortenPath(shellPath)}`)} to apply token to current shell.`));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
console.log(chalk.yellow(' No files were changed.'));
|
|
139
|
+
}
|
|
140
|
+
console.log();
|
|
141
|
+
}
|
|
142
|
+
function padRight(s, n) {
|
|
143
|
+
const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
144
|
+
return visible.length >= n ? s : s + ' '.repeat(n - visible.length);
|
|
145
|
+
}
|
package/dist/tui.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface CheckboxItem {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string;
|
|
4
|
+
checked: boolean;
|
|
5
|
+
/** Optional status to display after the label */
|
|
6
|
+
status?: string;
|
|
7
|
+
statusColor?: 'green' | 'yellow' | 'red' | 'dim';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Interactive multi-select checkbox prompt.
|
|
11
|
+
*
|
|
12
|
+
* Controls:
|
|
13
|
+
* ↑/↓ or j/k — navigate
|
|
14
|
+
* Space — toggle selection
|
|
15
|
+
* a — toggle all
|
|
16
|
+
* Enter — confirm
|
|
17
|
+
* q/Esc — cancel (returns empty)
|
|
18
|
+
*/
|
|
19
|
+
export declare function checkboxPrompt(items: CheckboxItem[], opts?: {
|
|
20
|
+
title?: string;
|
|
21
|
+
hint?: string;
|
|
22
|
+
}): Promise<string[]>;
|