@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/src/doctor.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
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 type { IPage } from './types.js';
|
|
7
9
|
import { PlaywrightMCP, getTokenFingerprint } from './browser.js';
|
|
8
10
|
import { browserSession } from './runtime.js';
|
|
9
11
|
|
|
10
12
|
const PLAYWRIGHT_SERVER_NAME = 'playwright';
|
|
11
|
-
const PLAYWRIGHT_TOKEN_ENV = 'PLAYWRIGHT_MCP_EXTENSION_TOKEN';
|
|
13
|
+
export const PLAYWRIGHT_TOKEN_ENV = 'PLAYWRIGHT_MCP_EXTENSION_TOKEN';
|
|
12
14
|
const PLAYWRIGHT_EXTENSION_ID = 'mmlmfjhmonkocbjadbfplnigmagldckm';
|
|
13
15
|
const TOKEN_LINE_RE = /^(\s*export\s+PLAYWRIGHT_MCP_EXTENSION_TOKEN=)(['"]?)([^'"\\\n]+)\2\s*$/m;
|
|
14
16
|
export type DoctorOptions = {
|
|
@@ -43,6 +45,8 @@ export type DoctorReport = {
|
|
|
43
45
|
cliVersion?: string;
|
|
44
46
|
envToken: string | null;
|
|
45
47
|
envFingerprint: string | null;
|
|
48
|
+
extensionToken: string | null;
|
|
49
|
+
extensionFingerprint: string | null;
|
|
46
50
|
shellFiles: ShellFileStatus[];
|
|
47
51
|
configs: McpConfigStatus[];
|
|
48
52
|
recommendedToken: string | null;
|
|
@@ -53,17 +57,41 @@ export type DoctorReport = {
|
|
|
53
57
|
|
|
54
58
|
type ReportStatus = 'OK' | 'MISSING' | 'MISMATCH' | 'WARN';
|
|
55
59
|
|
|
56
|
-
function
|
|
57
|
-
|
|
60
|
+
function colorLabel(status: ReportStatus): string {
|
|
61
|
+
switch (status) {
|
|
62
|
+
case 'OK': return chalk.green('[OK]');
|
|
63
|
+
case 'MISSING': return chalk.red('[MISSING]');
|
|
64
|
+
case 'MISMATCH': return chalk.yellow('[MISMATCH]');
|
|
65
|
+
case 'WARN': return chalk.yellow('[WARN]');
|
|
66
|
+
}
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
function statusLine(status: ReportStatus, text: string): string {
|
|
61
|
-
return `${
|
|
70
|
+
return `${colorLabel(status)} ${text}`;
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
function tokenSummary(token: string | null, fingerprint: string | null): string {
|
|
65
|
-
if (!token) return 'missing';
|
|
66
|
-
return `configured (${fingerprint})`;
|
|
74
|
+
if (!token) return chalk.dim('missing');
|
|
75
|
+
return `configured ${chalk.dim(`(${fingerprint})`)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function shortenPath(p: string): string {
|
|
79
|
+
const home = os.homedir();
|
|
80
|
+
return home && p.startsWith(home) ? '~' + p.slice(home.length) : p;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function toolName(p: string): string {
|
|
84
|
+
if (p.includes('.codex/')) return 'Codex';
|
|
85
|
+
if (p.includes('.cursor/')) return 'Cursor';
|
|
86
|
+
if (p.includes('.claude.json')) return 'Claude Code';
|
|
87
|
+
if (p.includes('antigravity')) return 'Antigravity';
|
|
88
|
+
if (p.includes('.gemini/settings')) return 'Gemini CLI';
|
|
89
|
+
if (p.includes('opencode')) return 'OpenCode';
|
|
90
|
+
if (p.includes('Claude/claude_desktop')) return 'Claude Desktop';
|
|
91
|
+
if (p.includes('.vscode/')) return 'VS Code';
|
|
92
|
+
if (p.includes('.mcp.json')) return 'Project MCP';
|
|
93
|
+
if (p.includes('.zshrc') || p.includes('.bashrc') || p.includes('.profile')) return 'Shell';
|
|
94
|
+
return '';
|
|
67
95
|
}
|
|
68
96
|
|
|
69
97
|
export function getDefaultShellRcPath(): string {
|
|
@@ -79,12 +107,16 @@ export function getDefaultMcpConfigPaths(cwd: string = process.cwd()): string[]
|
|
|
79
107
|
path.join(home, '.codex', 'config.toml'),
|
|
80
108
|
path.join(home, '.codex', 'mcp.json'),
|
|
81
109
|
path.join(home, '.cursor', 'mcp.json'),
|
|
110
|
+
path.join(home, '.claude.json'),
|
|
111
|
+
path.join(home, '.gemini', 'settings.json'),
|
|
112
|
+
path.join(home, '.gemini', 'antigravity', 'mcp_config.json'),
|
|
82
113
|
path.join(home, '.config', 'opencode', 'opencode.json'),
|
|
83
114
|
path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
|
|
84
115
|
path.join(home, '.config', 'Claude', 'claude_desktop_config.json'),
|
|
85
116
|
path.join(cwd, '.cursor', 'mcp.json'),
|
|
86
117
|
path.join(cwd, '.vscode', 'mcp.json'),
|
|
87
118
|
path.join(cwd, '.opencode', 'opencode.json'),
|
|
119
|
+
path.join(cwd, '.mcp.json'),
|
|
88
120
|
];
|
|
89
121
|
return [...new Set(candidates)];
|
|
90
122
|
}
|
|
@@ -170,7 +202,7 @@ export function upsertTomlConfigToken(content: string, token: string): string {
|
|
|
170
202
|
return `${prefix}[mcp_servers.playwright]\ntype = "stdio"\ncommand = "npx"\nargs = ["-y", "@playwright/mcp@latest", "--extension"]\n\n[mcp_servers.playwright.env]\n${tokenLine}\n`;
|
|
171
203
|
}
|
|
172
204
|
|
|
173
|
-
function fileExists(filePath: string): boolean {
|
|
205
|
+
export function fileExists(filePath: string): boolean {
|
|
174
206
|
try {
|
|
175
207
|
return fs.existsSync(filePath);
|
|
176
208
|
} catch {
|
|
@@ -220,6 +252,145 @@ function readConfigStatus(filePath: string): McpConfigStatus {
|
|
|
220
252
|
}
|
|
221
253
|
}
|
|
222
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Discover the auth token stored by the Playwright MCP Bridge extension
|
|
257
|
+
* by scanning Chrome's LevelDB localStorage files directly.
|
|
258
|
+
*
|
|
259
|
+
* Uses `strings` + `grep` for fast binary scanning on macOS/Linux,
|
|
260
|
+
* with a pure-Node fallback on Windows.
|
|
261
|
+
*/
|
|
262
|
+
export function discoverExtensionToken(): string | null {
|
|
263
|
+
const home = os.homedir();
|
|
264
|
+
const platform = os.platform();
|
|
265
|
+
const bases: string[] = [];
|
|
266
|
+
|
|
267
|
+
if (platform === 'darwin') {
|
|
268
|
+
bases.push(
|
|
269
|
+
path.join(home, 'Library', 'Application Support', 'Google', 'Chrome'),
|
|
270
|
+
path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Canary'),
|
|
271
|
+
path.join(home, 'Library', 'Application Support', 'Chromium'),
|
|
272
|
+
path.join(home, 'Library', 'Application Support', 'Microsoft Edge'),
|
|
273
|
+
);
|
|
274
|
+
} else if (platform === 'linux') {
|
|
275
|
+
bases.push(
|
|
276
|
+
path.join(home, '.config', 'google-chrome'),
|
|
277
|
+
path.join(home, '.config', 'chromium'),
|
|
278
|
+
path.join(home, '.config', 'microsoft-edge'),
|
|
279
|
+
);
|
|
280
|
+
} else if (platform === 'win32') {
|
|
281
|
+
const appData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
282
|
+
bases.push(
|
|
283
|
+
path.join(appData, 'Google', 'Chrome', 'User Data'),
|
|
284
|
+
path.join(appData, 'Microsoft', 'Edge', 'User Data'),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const profiles = ['Default', 'Profile 1', 'Profile 2', 'Profile 3'];
|
|
289
|
+
// Token is 43 chars of base64url (from 32 random bytes)
|
|
290
|
+
const tokenRe = /([A-Za-z0-9_-]{40,50})/;
|
|
291
|
+
|
|
292
|
+
for (const base of bases) {
|
|
293
|
+
for (const profile of profiles) {
|
|
294
|
+
const dir = path.join(base, profile, 'Local Storage', 'leveldb');
|
|
295
|
+
if (!fileExists(dir)) continue;
|
|
296
|
+
|
|
297
|
+
// Fast path: use strings + grep to find candidate files and extract token
|
|
298
|
+
if (platform !== 'win32') {
|
|
299
|
+
const token = extractTokenViaStrings(dir, tokenRe);
|
|
300
|
+
if (token) return token;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Slow path (Windows): read binary files directly
|
|
305
|
+
const token = extractTokenViaBinaryRead(dir, tokenRe);
|
|
306
|
+
if (token) return token;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function extractTokenViaStrings(dir: string, tokenRe: RegExp): string | null {
|
|
314
|
+
try {
|
|
315
|
+
// Single shell pipeline: for each LevelDB file, extract strings, find lines
|
|
316
|
+
// after the extension ID, and filter for base64url token pattern.
|
|
317
|
+
//
|
|
318
|
+
// LevelDB `strings` output for the extension's auth-token entry:
|
|
319
|
+
// auth-token ← key name
|
|
320
|
+
// 4,mmlmfjhmonkocbjadbfplnigmagldckm.7 ← LevelDB internal key
|
|
321
|
+
// hqI86ncsD1QpcVcj-k9CyzTF-ieCQd_4KreZ_wy1WHA ← token value
|
|
322
|
+
//
|
|
323
|
+
// We get the line immediately after any EXTENSION_ID mention and check
|
|
324
|
+
// if it looks like a base64url token (40-50 chars, [A-Za-z0-9_-]).
|
|
325
|
+
const shellDir = dir.replace(/'/g, "'\\''");
|
|
326
|
+
const cmd = `for f in '${shellDir}'/*.ldb '${shellDir}'/*.log; do ` +
|
|
327
|
+
`[ -f "$f" ] && strings "$f" 2>/dev/null | ` +
|
|
328
|
+
`grep -A1 '${PLAYWRIGHT_EXTENSION_ID}' | ` +
|
|
329
|
+
`grep -v '${PLAYWRIGHT_EXTENSION_ID}' | ` +
|
|
330
|
+
`grep -E '^[A-Za-z0-9_-]{40,50}$' | head -1; ` +
|
|
331
|
+
`done 2>/dev/null`;
|
|
332
|
+
const result = execSync(cmd, { encoding: 'utf-8', timeout: 10000 }).trim();
|
|
333
|
+
|
|
334
|
+
// Take the first non-empty line
|
|
335
|
+
for (const line of result.split('\n')) {
|
|
336
|
+
const token = line.trim();
|
|
337
|
+
if (token && validateBase64urlToken(token)) return token;
|
|
338
|
+
}
|
|
339
|
+
} catch {}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function extractTokenViaBinaryRead(dir: string, tokenRe: RegExp): string | null {
|
|
344
|
+
const extIdBuf = Buffer.from(PLAYWRIGHT_EXTENSION_ID);
|
|
345
|
+
const keyBuf = Buffer.from('auth-token');
|
|
346
|
+
|
|
347
|
+
let files: string[];
|
|
348
|
+
try {
|
|
349
|
+
files = fs.readdirSync(dir)
|
|
350
|
+
.filter(f => f.endsWith('.ldb') || f.endsWith('.log'))
|
|
351
|
+
.map(f => path.join(dir, f));
|
|
352
|
+
} catch { return null; }
|
|
353
|
+
|
|
354
|
+
// Sort by mtime descending
|
|
355
|
+
files.sort((a, b) => {
|
|
356
|
+
try { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; } catch { return 0; }
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
for (const file of files) {
|
|
360
|
+
let data: Buffer;
|
|
361
|
+
try { data = fs.readFileSync(file); } catch { continue; }
|
|
362
|
+
|
|
363
|
+
// Quick check: does file contain both the extension ID and auth-token key?
|
|
364
|
+
const extPos = data.indexOf(extIdBuf);
|
|
365
|
+
if (extPos === -1) continue;
|
|
366
|
+
const keyPos = data.indexOf(keyBuf, Math.max(0, extPos - 500));
|
|
367
|
+
if (keyPos === -1) continue;
|
|
368
|
+
|
|
369
|
+
// Scan for token value after auth-token key
|
|
370
|
+
let idx = 0;
|
|
371
|
+
while (true) {
|
|
372
|
+
const kp = data.indexOf(keyBuf, idx);
|
|
373
|
+
if (kp === -1) break;
|
|
374
|
+
|
|
375
|
+
const contextStart = Math.max(0, kp - 500);
|
|
376
|
+
if (data.indexOf(extIdBuf, contextStart) !== -1 && data.indexOf(extIdBuf, contextStart) < kp) {
|
|
377
|
+
const after = data.subarray(kp + keyBuf.length, kp + keyBuf.length + 200).toString('latin1');
|
|
378
|
+
const m = after.match(tokenRe);
|
|
379
|
+
if (m && validateBase64urlToken(m[1])) return m[1];
|
|
380
|
+
}
|
|
381
|
+
idx = kp + 1;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function validateBase64urlToken(token: string): boolean {
|
|
388
|
+
try {
|
|
389
|
+
const b64 = token.replace(/-/g, '+').replace(/_/g, '/');
|
|
390
|
+
const decoded = Buffer.from(b64, 'base64');
|
|
391
|
+
return decoded.length >= 28 && decoded.length <= 36;
|
|
392
|
+
} catch { return false; }
|
|
393
|
+
}
|
|
223
394
|
|
|
224
395
|
|
|
225
396
|
export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<DoctorReport> {
|
|
@@ -234,19 +405,25 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
234
405
|
const configPaths = opts.configPaths?.length ? opts.configPaths : getDefaultMcpConfigPaths();
|
|
235
406
|
const configs = configPaths.map(readConfigStatus);
|
|
236
407
|
|
|
408
|
+
// Try to discover the token directly from the Chrome extension's localStorage
|
|
409
|
+
const extensionToken = discoverExtensionToken();
|
|
410
|
+
|
|
237
411
|
const allTokens = [
|
|
238
412
|
opts.token ?? null,
|
|
413
|
+
extensionToken,
|
|
239
414
|
envToken,
|
|
240
415
|
...shellFiles.map(s => s.token),
|
|
241
416
|
...configs.map(c => c.token),
|
|
242
417
|
].filter((v): v is string => !!v);
|
|
243
418
|
const uniqueTokens = [...new Set(allTokens)];
|
|
244
|
-
const recommendedToken = opts.token ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] : null) ?? null;
|
|
419
|
+
const recommendedToken = opts.token ?? extensionToken ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] : null) ?? null;
|
|
245
420
|
|
|
246
421
|
const report: DoctorReport = {
|
|
247
422
|
cliVersion: opts.cliVersion,
|
|
248
423
|
envToken,
|
|
249
424
|
envFingerprint: getTokenFingerprint(envToken ?? undefined),
|
|
425
|
+
extensionToken,
|
|
426
|
+
extensionFingerprint: getTokenFingerprint(extensionToken ?? undefined),
|
|
250
427
|
shellFiles,
|
|
251
428
|
configs,
|
|
252
429
|
recommendedToken,
|
|
@@ -270,26 +447,32 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
270
447
|
|
|
271
448
|
export function renderBrowserDoctorReport(report: DoctorReport): string {
|
|
272
449
|
const tokenFingerprints = [
|
|
450
|
+
report.extensionFingerprint,
|
|
273
451
|
report.envFingerprint,
|
|
274
452
|
...report.shellFiles.map(shell => shell.fingerprint),
|
|
275
453
|
...report.configs.filter(config => config.exists).map(config => config.fingerprint),
|
|
276
454
|
].filter((value): value is string => !!value);
|
|
277
455
|
const uniqueFingerprints = [...new Set(tokenFingerprints)];
|
|
278
456
|
const hasMismatch = uniqueFingerprints.length > 1;
|
|
279
|
-
const lines = [`opencli v${report.cliVersion ?? 'unknown'} doctor
|
|
457
|
+
const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), ''];
|
|
458
|
+
|
|
459
|
+
const extStatus: ReportStatus = !report.extensionToken ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK';
|
|
460
|
+
lines.push(statusLine(extStatus, `Extension token (Chrome LevelDB): ${tokenSummary(report.extensionToken, report.extensionFingerprint)}`));
|
|
280
461
|
|
|
281
462
|
const envStatus: ReportStatus = !report.envToken ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK';
|
|
282
463
|
lines.push(statusLine(envStatus, `Environment token: ${tokenSummary(report.envToken, report.envFingerprint)}`));
|
|
283
464
|
|
|
284
465
|
for (const shell of report.shellFiles) {
|
|
285
466
|
const shellStatus: ReportStatus = !shell.token ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK';
|
|
286
|
-
|
|
467
|
+
const tool = toolName(shell.path);
|
|
468
|
+
const suffix = tool ? chalk.dim(` [${tool}]`) : '';
|
|
469
|
+
lines.push(statusLine(shellStatus, `${shortenPath(shell.path)}${suffix}: ${tokenSummary(shell.token, shell.fingerprint)}`));
|
|
287
470
|
}
|
|
288
471
|
const existingConfigs = report.configs.filter(config => config.exists);
|
|
289
472
|
const missingConfigCount = report.configs.length - existingConfigs.length;
|
|
290
473
|
if (existingConfigs.length > 0) {
|
|
291
474
|
for (const config of existingConfigs) {
|
|
292
|
-
const parseSuffix = config.parseError ? ` (parse error
|
|
475
|
+
const parseSuffix = config.parseError ? chalk.red(` (parse error)`) : '';
|
|
293
476
|
const configStatus: ReportStatus = config.parseError
|
|
294
477
|
? 'WARN'
|
|
295
478
|
: !config.token
|
|
@@ -297,24 +480,26 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
|
|
|
297
480
|
: hasMismatch
|
|
298
481
|
? 'MISMATCH'
|
|
299
482
|
: 'OK';
|
|
300
|
-
|
|
483
|
+
const tool = toolName(config.path);
|
|
484
|
+
const suffix = tool ? chalk.dim(` [${tool}]`) : '';
|
|
485
|
+
lines.push(statusLine(configStatus, `${shortenPath(config.path)}${suffix}: ${tokenSummary(config.token, config.fingerprint)}${parseSuffix}`));
|
|
301
486
|
}
|
|
302
487
|
} else {
|
|
303
|
-
lines.push(statusLine('MISSING', 'MCP config: no existing config files found
|
|
488
|
+
lines.push(statusLine('MISSING', 'MCP config: no existing config files found'));
|
|
304
489
|
}
|
|
305
|
-
if (missingConfigCount > 0) lines.push(` Other scanned config locations not present: ${missingConfigCount}`);
|
|
490
|
+
if (missingConfigCount > 0) lines.push(chalk.dim(` Other scanned config locations not present: ${missingConfigCount}`));
|
|
306
491
|
lines.push('');
|
|
307
492
|
lines.push(statusLine(
|
|
308
493
|
hasMismatch ? 'MISMATCH' : report.recommendedToken ? 'OK' : 'WARN',
|
|
309
494
|
`Recommended token fingerprint: ${report.recommendedFingerprint ?? 'unavailable'}`,
|
|
310
495
|
));
|
|
311
496
|
if (report.issues.length) {
|
|
312
|
-
lines.push('', 'Issues:');
|
|
313
|
-
for (const issue of report.issues) lines.push(
|
|
497
|
+
lines.push('', chalk.yellow('Issues:'));
|
|
498
|
+
for (const issue of report.issues) lines.push(chalk.dim(` • ${issue}`));
|
|
314
499
|
}
|
|
315
500
|
if (report.warnings.length) {
|
|
316
|
-
lines.push('', 'Warnings:');
|
|
317
|
-
for (const warning of report.warnings) lines.push(
|
|
501
|
+
lines.push('', chalk.yellow('Warnings:'));
|
|
502
|
+
for (const warning of report.warnings) lines.push(chalk.dim(` • ${warning}`));
|
|
318
503
|
}
|
|
319
504
|
return lines.join('\n');
|
|
320
505
|
}
|
|
@@ -329,7 +514,7 @@ async function confirmPrompt(question: string): Promise<boolean> {
|
|
|
329
514
|
}
|
|
330
515
|
}
|
|
331
516
|
|
|
332
|
-
function writeFileWithMkdir(filePath: string, content: string): void {
|
|
517
|
+
export function writeFileWithMkdir(filePath: string, content: string): void {
|
|
333
518
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
334
519
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
335
520
|
}
|
|
@@ -337,27 +522,38 @@ function writeFileWithMkdir(filePath: string, content: string): void {
|
|
|
337
522
|
export async function applyBrowserDoctorFix(report: DoctorReport, opts: DoctorOptions = {}): Promise<string[]> {
|
|
338
523
|
const token = opts.token ?? report.recommendedToken;
|
|
339
524
|
if (!token) throw new Error('No Playwright MCP token is available to write. Provide --token first.');
|
|
525
|
+
const fp = getTokenFingerprint(token);
|
|
340
526
|
|
|
341
527
|
const plannedWrites: string[] = [];
|
|
342
528
|
const shellPath = opts.shellRc ?? report.shellFiles[0]?.path ?? getDefaultShellRcPath();
|
|
343
|
-
|
|
529
|
+
const shellStatus = report.shellFiles.find(s => s.path === shellPath);
|
|
530
|
+
if (shellStatus?.fingerprint !== fp) plannedWrites.push(shellPath);
|
|
344
531
|
for (const config of report.configs) {
|
|
345
532
|
if (!config.writable) continue;
|
|
533
|
+
if (config.fingerprint === fp) continue; // already correct
|
|
346
534
|
plannedWrites.push(config.path);
|
|
347
535
|
}
|
|
348
536
|
|
|
537
|
+
if (plannedWrites.length === 0) {
|
|
538
|
+
console.log(chalk.green('All config files are already up to date.'));
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
|
|
349
542
|
if (!opts.yes) {
|
|
350
|
-
const ok = await confirmPrompt(`Update ${plannedWrites.length} file(s) with Playwright MCP token fingerprint ${
|
|
543
|
+
const ok = await confirmPrompt(`Update ${plannedWrites.length} file(s) with Playwright MCP token fingerprint ${fp}?`);
|
|
351
544
|
if (!ok) return [];
|
|
352
545
|
}
|
|
353
546
|
|
|
354
547
|
const written: string[] = [];
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
548
|
+
if (plannedWrites.includes(shellPath)) {
|
|
549
|
+
const shellBefore = fileExists(shellPath) ? fs.readFileSync(shellPath, 'utf-8') : '';
|
|
550
|
+
writeFileWithMkdir(shellPath, upsertShellToken(shellBefore, token));
|
|
551
|
+
written.push(shellPath);
|
|
552
|
+
}
|
|
358
553
|
|
|
359
554
|
for (const config of report.configs) {
|
|
360
|
-
if (!
|
|
555
|
+
if (!plannedWrites.includes(config.path)) continue;
|
|
556
|
+
if (config.parseError) continue;
|
|
361
557
|
const before = fileExists(config.path) ? fs.readFileSync(config.path, 'utf-8') : '';
|
|
362
558
|
const next = config.format === 'toml' ? upsertTomlConfigToken(before, token) : upsertJsonConfigToken(before, token);
|
|
363
559
|
writeFileWithMkdir(config.path, next);
|
package/src/main.ts
CHANGED
|
@@ -111,6 +111,14 @@ program.command('doctor')
|
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
program.command('setup')
|
|
115
|
+
.description('Interactive setup: configure Playwright MCP token across all detected tools')
|
|
116
|
+
.option('--token <token>', 'Provide token directly instead of auto-detecting')
|
|
117
|
+
.action(async (opts) => {
|
|
118
|
+
const { runSetup } = await import('./setup.js');
|
|
119
|
+
await runSetup({ cliVersion: PKG_VERSION, token: opts.token });
|
|
120
|
+
});
|
|
121
|
+
|
|
114
122
|
// ── Dynamic site commands ──────────────────────────────────────────────────
|
|
115
123
|
|
|
116
124
|
const registry = getRegistry();
|
package/src/setup.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
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 {
|
|
12
|
+
type DoctorReport,
|
|
13
|
+
PLAYWRIGHT_TOKEN_ENV,
|
|
14
|
+
discoverExtensionToken,
|
|
15
|
+
fileExists,
|
|
16
|
+
getDefaultShellRcPath,
|
|
17
|
+
runBrowserDoctor,
|
|
18
|
+
shortenPath,
|
|
19
|
+
toolName,
|
|
20
|
+
upsertJsonConfigToken,
|
|
21
|
+
upsertShellToken,
|
|
22
|
+
upsertTomlConfigToken,
|
|
23
|
+
writeFileWithMkdir,
|
|
24
|
+
} from './doctor.js';
|
|
25
|
+
import { getTokenFingerprint } from './browser.js';
|
|
26
|
+
import { type CheckboxItem, checkboxPrompt } from './tui.js';
|
|
27
|
+
|
|
28
|
+
export async function runSetup(opts: { cliVersion?: string; token?: string } = {}) {
|
|
29
|
+
console.log();
|
|
30
|
+
console.log(chalk.bold(' opencli setup') + chalk.dim(' — Playwright MCP token configuration'));
|
|
31
|
+
console.log();
|
|
32
|
+
|
|
33
|
+
// Step 1: Discover token
|
|
34
|
+
let token = opts.token ?? null;
|
|
35
|
+
|
|
36
|
+
if (!token) {
|
|
37
|
+
const extensionToken = discoverExtensionToken();
|
|
38
|
+
const envToken = process.env[PLAYWRIGHT_TOKEN_ENV] ?? null;
|
|
39
|
+
|
|
40
|
+
if (extensionToken && envToken && extensionToken === envToken) {
|
|
41
|
+
token = extensionToken;
|
|
42
|
+
console.log(` ${chalk.green('✓')} Token auto-discovered from Chrome extension`);
|
|
43
|
+
console.log(` Fingerprint: ${chalk.bold(getTokenFingerprint(token) ?? 'unknown')}`);
|
|
44
|
+
} else if (extensionToken) {
|
|
45
|
+
token = extensionToken;
|
|
46
|
+
console.log(` ${chalk.green('✓')} Token discovered from Chrome extension ` +
|
|
47
|
+
chalk.dim(`(${getTokenFingerprint(token)})`));
|
|
48
|
+
if (envToken && envToken !== extensionToken) {
|
|
49
|
+
console.log(` ${chalk.yellow('!')} Environment has different token ` +
|
|
50
|
+
chalk.dim(`(${getTokenFingerprint(envToken)})`));
|
|
51
|
+
}
|
|
52
|
+
} else if (envToken) {
|
|
53
|
+
token = envToken;
|
|
54
|
+
console.log(` ${chalk.green('✓')} Token from environment variable ` +
|
|
55
|
+
chalk.dim(`(${getTokenFingerprint(token)})`));
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
console.log(` ${chalk.green('✓')} Using provided token ` +
|
|
59
|
+
chalk.dim(`(${getTokenFingerprint(token)})`));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!token) {
|
|
63
|
+
console.log(` ${chalk.yellow('!')} No token found. Please enter it manually.`);
|
|
64
|
+
console.log(chalk.dim(' (Find it in the Playwright MCP Bridge extension → Status page)'));
|
|
65
|
+
console.log();
|
|
66
|
+
const rl = createInterface({ input, output });
|
|
67
|
+
const answer = await rl.question(' Token: ');
|
|
68
|
+
rl.close();
|
|
69
|
+
token = answer.trim();
|
|
70
|
+
if (!token) {
|
|
71
|
+
console.log(chalk.red('\n No token provided. Aborting.\n'));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fingerprint = getTokenFingerprint(token) ?? 'unknown';
|
|
77
|
+
console.log();
|
|
78
|
+
|
|
79
|
+
// Step 2: Scan all config locations
|
|
80
|
+
const report = await runBrowserDoctor({ token, cliVersion: opts.cliVersion });
|
|
81
|
+
|
|
82
|
+
// Step 3: Build checkbox items
|
|
83
|
+
const items: CheckboxItem[] = [];
|
|
84
|
+
|
|
85
|
+
// Shell file
|
|
86
|
+
const shellPath = report.shellFiles[0]?.path ?? getDefaultShellRcPath();
|
|
87
|
+
const shellStatus = report.shellFiles[0];
|
|
88
|
+
const shellFp = shellStatus?.fingerprint;
|
|
89
|
+
const shellOk = shellFp === fingerprint;
|
|
90
|
+
const shellTool = toolName(shellPath) || 'Shell';
|
|
91
|
+
items.push({
|
|
92
|
+
label: padRight(shortenPath(shellPath), 50) + chalk.dim(` [${shellTool}]`),
|
|
93
|
+
value: `shell:${shellPath}`,
|
|
94
|
+
checked: !shellOk,
|
|
95
|
+
status: shellOk ? `configured (${shellFp})` : shellFp ? `mismatch (${shellFp})` : 'missing',
|
|
96
|
+
statusColor: shellOk ? 'green' : shellFp ? 'yellow' : 'red',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Config files
|
|
100
|
+
for (const config of report.configs) {
|
|
101
|
+
const fp = config.fingerprint;
|
|
102
|
+
const ok = fp === fingerprint;
|
|
103
|
+
const tool = toolName(config.path);
|
|
104
|
+
items.push({
|
|
105
|
+
label: padRight(shortenPath(config.path), 50) + chalk.dim(tool ? ` [${tool}]` : ''),
|
|
106
|
+
value: `config:${config.path}`,
|
|
107
|
+
checked: false, // let user explicitly select which tools to configure
|
|
108
|
+
status: ok ? `configured (${fp})` : !config.exists ? 'will create' : fp ? `mismatch (${fp})` : 'missing',
|
|
109
|
+
statusColor: ok ? 'green' : 'yellow',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Step 4: Show interactive checkbox
|
|
114
|
+
console.clear();
|
|
115
|
+
const selected = await checkboxPrompt(items, {
|
|
116
|
+
title: ` ${chalk.bold('opencli setup')} — token ${chalk.cyan(fingerprint)}`,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (selected.length === 0) {
|
|
120
|
+
console.log(chalk.dim(' No changes made.\n'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Step 5: Apply changes
|
|
125
|
+
const written: string[] = [];
|
|
126
|
+
let wroteShell = false;
|
|
127
|
+
|
|
128
|
+
for (const sel of selected) {
|
|
129
|
+
if (sel.startsWith('shell:')) {
|
|
130
|
+
const p = sel.slice('shell:'.length);
|
|
131
|
+
const before = fileExists(p) ? fs.readFileSync(p, 'utf-8') : '';
|
|
132
|
+
writeFileWithMkdir(p, upsertShellToken(before, token));
|
|
133
|
+
written.push(p);
|
|
134
|
+
wroteShell = true;
|
|
135
|
+
} else if (sel.startsWith('config:')) {
|
|
136
|
+
const p = sel.slice('config:'.length);
|
|
137
|
+
const config = report.configs.find(c => c.path === p);
|
|
138
|
+
if (config && config.parseError) continue;
|
|
139
|
+
const before = fileExists(p) ? fs.readFileSync(p, 'utf-8') : '';
|
|
140
|
+
const format = config?.format ?? (p.endsWith('.toml') ? 'toml' : 'json');
|
|
141
|
+
const next = format === 'toml' ? upsertTomlConfigToken(before, token) : upsertJsonConfigToken(before, token);
|
|
142
|
+
writeFileWithMkdir(p, next);
|
|
143
|
+
written.push(p);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
process.env[PLAYWRIGHT_TOKEN_ENV] = token;
|
|
148
|
+
|
|
149
|
+
// Step 6: Summary
|
|
150
|
+
if (written.length > 0) {
|
|
151
|
+
console.log(chalk.green.bold(` ✓ Updated ${written.length} file(s):`));
|
|
152
|
+
for (const p of written) {
|
|
153
|
+
const tool = toolName(p);
|
|
154
|
+
console.log(` ${chalk.dim('•')} ${shortenPath(p)}${tool ? chalk.dim(` [${tool}]`) : ''}`);
|
|
155
|
+
}
|
|
156
|
+
if (wroteShell) {
|
|
157
|
+
console.log();
|
|
158
|
+
console.log(chalk.cyan(` 💡 Run ${chalk.bold(`source ${shortenPath(shellPath)}`)} to apply token to current shell.`));
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
console.log(chalk.yellow(' No files were changed.'));
|
|
162
|
+
}
|
|
163
|
+
console.log();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function padRight(s: string, n: number): string {
|
|
167
|
+
const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
168
|
+
return visible.length >= n ? s : s + ' '.repeat(n - visible.length);
|
|
169
|
+
}
|