@jackwener/opencli 0.7.6 → 0.7.9
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/.agents/skills/cross-project-adapter-migration/SKILL.md +249 -0
- package/.agents/workflows/cross-project-adapter-migration.md +54 -0
- package/dist/_debug.d.ts +1 -0
- package/dist/_debug.js +7 -0
- package/dist/browser/discover.d.ts +8 -0
- package/dist/browser/discover.js +83 -0
- package/dist/browser/errors.d.ts +21 -0
- package/dist/browser/errors.js +54 -0
- package/dist/browser/index.d.ts +22 -0
- package/dist/browser/index.js +22 -0
- package/dist/browser/mcp.d.ts +33 -0
- package/dist/browser/mcp.js +304 -0
- package/dist/browser/page.d.ts +41 -0
- package/dist/browser/page.js +142 -0
- package/dist/browser/tabs.d.ts +13 -0
- package/dist/browser/tabs.js +70 -0
- package/dist/browser-tab.d.ts +2 -0
- package/dist/browser-tab.js +30 -0
- package/dist/browser.test.js +1 -1
- package/dist/cli-manifest.json +70 -3
- package/dist/clis/github/search.d.ts +1 -0
- package/dist/clis/github/search.js +20 -0
- package/dist/clis/index.d.ts +27 -0
- package/dist/clis/index.js +41 -0
- package/dist/clis/twitter/timeline.js +174 -35
- package/dist/clis/xiaohongshu/me.d.ts +1 -0
- package/dist/clis/xiaohongshu/me.js +86 -0
- package/dist/completion.js +2 -2
- package/dist/doctor.js +7 -7
- package/dist/engine.js +6 -4
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +42 -0
- package/dist/logger.d.ts +22 -0
- package/dist/logger.js +47 -0
- package/dist/main.js +8 -2
- package/dist/pipeline/_debug.d.ts +1 -0
- package/dist/pipeline/_debug.js +7 -0
- package/dist/pipeline/executor.js +8 -8
- package/dist/pipeline/steps/browser.d.ts +7 -7
- package/dist/pipeline/steps/intercept.d.ts +1 -1
- package/dist/pipeline/steps/tap.d.ts +1 -1
- package/dist/promote.d.ts +1 -0
- package/dist/promote.js +3 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +2 -0
- package/dist/scaffold.d.ts +2 -0
- package/dist/scaffold.js +2 -0
- package/dist/setup.js +9 -3
- package/dist/smoke.d.ts +2 -0
- package/dist/smoke.js +2 -0
- package/package.json +3 -3
- package/scripts/clean-yaml.cjs +19 -0
- package/scripts/copy-yaml.cjs +21 -0
- package/scripts/postinstall.js +30 -9
- package/src/bilibili.ts +1 -1
- package/src/browser/discover.ts +90 -0
- package/src/browser/errors.ts +89 -0
- package/src/browser/index.ts +26 -0
- package/src/browser/mcp.ts +305 -0
- package/src/browser/page.ts +152 -0
- package/src/browser/tabs.ts +76 -0
- package/src/browser.test.ts +1 -1
- package/src/clis/twitter/timeline.ts +204 -36
- package/src/completion.ts +2 -2
- package/src/doctor.ts +13 -1
- package/src/engine.ts +9 -4
- package/src/errors.ts +48 -0
- package/src/logger.ts +57 -0
- package/src/main.ts +10 -3
- package/src/pipeline/executor.ts +8 -7
- package/src/pipeline/steps/browser.ts +18 -18
- package/src/pipeline/steps/intercept.ts +8 -8
- package/src/pipeline/steps/tap.ts +2 -2
- package/src/setup.ts +9 -3
- package/tsconfig.json +1 -2
- package/src/browser.ts +0 -698
package/dist/errors.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified error types for opencli.
|
|
3
|
+
*
|
|
4
|
+
* All errors thrown by the framework should extend CliError so that
|
|
5
|
+
* the top-level handler in main.ts can render consistent, helpful output.
|
|
6
|
+
*/
|
|
7
|
+
export class CliError extends Error {
|
|
8
|
+
/** Machine-readable error code (e.g. 'BROWSER_CONNECT', 'ADAPTER_LOAD') */
|
|
9
|
+
code;
|
|
10
|
+
/** Human-readable hint on how to fix the problem */
|
|
11
|
+
hint;
|
|
12
|
+
constructor(code, message, hint) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'CliError';
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.hint = hint;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class BrowserConnectError extends CliError {
|
|
20
|
+
constructor(message, hint) {
|
|
21
|
+
super('BROWSER_CONNECT', message, hint);
|
|
22
|
+
this.name = 'BrowserConnectError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class AdapterLoadError extends CliError {
|
|
26
|
+
constructor(message, hint) {
|
|
27
|
+
super('ADAPTER_LOAD', message, hint);
|
|
28
|
+
this.name = 'AdapterLoadError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class CommandExecutionError extends CliError {
|
|
32
|
+
constructor(message, hint) {
|
|
33
|
+
super('COMMAND_EXEC', message, hint);
|
|
34
|
+
this.name = 'CommandExecutionError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class ConfigError extends CliError {
|
|
38
|
+
constructor(message, hint) {
|
|
39
|
+
super('CONFIG', message, hint);
|
|
40
|
+
this.name = 'ConfigError';
|
|
41
|
+
}
|
|
42
|
+
}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified logging for opencli.
|
|
3
|
+
*
|
|
4
|
+
* All framework output (warnings, debug info, errors) should go through
|
|
5
|
+
* this module so that verbosity levels are respected consistently.
|
|
6
|
+
*/
|
|
7
|
+
export declare const log: {
|
|
8
|
+
/** Informational message (always shown) */
|
|
9
|
+
info(msg: string): void;
|
|
10
|
+
/** Warning (always shown) */
|
|
11
|
+
warn(msg: string): void;
|
|
12
|
+
/** Error (always shown) */
|
|
13
|
+
error(msg: string): void;
|
|
14
|
+
/** Verbose output (only when OPENCLI_VERBOSE is set or -v flag) */
|
|
15
|
+
verbose(msg: string): void;
|
|
16
|
+
/** Debug output (only when DEBUG includes 'opencli') */
|
|
17
|
+
debug(msg: string): void;
|
|
18
|
+
/** Step-style debug (for pipeline steps, etc.) */
|
|
19
|
+
step(stepNum: number, total: number, op: string, preview?: string): void;
|
|
20
|
+
/** Step result summary */
|
|
21
|
+
stepResult(summary: string): void;
|
|
22
|
+
};
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified logging for opencli.
|
|
3
|
+
*
|
|
4
|
+
* All framework output (warnings, debug info, errors) should go through
|
|
5
|
+
* this module so that verbosity levels are respected consistently.
|
|
6
|
+
*/
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
function isVerbose() {
|
|
9
|
+
return !!process.env.OPENCLI_VERBOSE;
|
|
10
|
+
}
|
|
11
|
+
function isDebug() {
|
|
12
|
+
return !!process.env.DEBUG?.includes('opencli');
|
|
13
|
+
}
|
|
14
|
+
export const log = {
|
|
15
|
+
/** Informational message (always shown) */
|
|
16
|
+
info(msg) {
|
|
17
|
+
process.stderr.write(`${chalk.blue('ℹ')} ${msg}\n`);
|
|
18
|
+
},
|
|
19
|
+
/** Warning (always shown) */
|
|
20
|
+
warn(msg) {
|
|
21
|
+
process.stderr.write(`${chalk.yellow('⚠')} ${msg}\n`);
|
|
22
|
+
},
|
|
23
|
+
/** Error (always shown) */
|
|
24
|
+
error(msg) {
|
|
25
|
+
process.stderr.write(`${chalk.red('✖')} ${msg}\n`);
|
|
26
|
+
},
|
|
27
|
+
/** Verbose output (only when OPENCLI_VERBOSE is set or -v flag) */
|
|
28
|
+
verbose(msg) {
|
|
29
|
+
if (isVerbose()) {
|
|
30
|
+
process.stderr.write(`${chalk.dim('[verbose]')} ${msg}\n`);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
/** Debug output (only when DEBUG includes 'opencli') */
|
|
34
|
+
debug(msg) {
|
|
35
|
+
if (isDebug()) {
|
|
36
|
+
process.stderr.write(`${chalk.dim('[debug]')} ${msg}\n`);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
/** Step-style debug (for pipeline steps, etc.) */
|
|
40
|
+
step(stepNum, total, op, preview = '') {
|
|
41
|
+
process.stderr.write(` ${chalk.dim(`[${stepNum}/${total}]`)} ${chalk.bold.cyan(op)}${preview}\n`);
|
|
42
|
+
},
|
|
43
|
+
/** Step result summary */
|
|
44
|
+
stepResult(summary) {
|
|
45
|
+
process.stderr.write(` ${chalk.dim(`→ ${summary}`)}\n`);
|
|
46
|
+
},
|
|
47
|
+
};
|
package/dist/main.js
CHANGED
|
@@ -10,10 +10,11 @@ import chalk from 'chalk';
|
|
|
10
10
|
import { discoverClis, executeCommand } from './engine.js';
|
|
11
11
|
import { fullName, getRegistry, strategyLabel } from './registry.js';
|
|
12
12
|
import { render as renderOutput } from './output.js';
|
|
13
|
-
import { PlaywrightMCP } from './browser.js';
|
|
13
|
+
import { PlaywrightMCP } from './browser/index.js';
|
|
14
14
|
import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
|
|
15
15
|
import { PKG_VERSION } from './version.js';
|
|
16
16
|
import { getCompletions, printCompletionScript } from './completion.js';
|
|
17
|
+
import { CliError } from './errors.js';
|
|
17
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
19
|
const __dirname = path.dirname(__filename);
|
|
19
20
|
const BUILTIN_CLIS = path.resolve(__dirname, 'clis');
|
|
@@ -228,7 +229,12 @@ for (const [, cmd] of registry) {
|
|
|
228
229
|
renderOutput(result, { fmt: actionOpts.format, columns: cmd.columns, title: `${cmd.site}/${cmd.name}`, elapsed: (Date.now() - startTime) / 1000, source: fullName(cmd) });
|
|
229
230
|
}
|
|
230
231
|
catch (err) {
|
|
231
|
-
if (
|
|
232
|
+
if (err instanceof CliError) {
|
|
233
|
+
console.error(chalk.red(`Error [${err.code}]: ${err.message}`));
|
|
234
|
+
if (err.hint)
|
|
235
|
+
console.error(chalk.yellow(`Hint: ${err.hint}`));
|
|
236
|
+
}
|
|
237
|
+
else if (actionOpts.verbose && err.stack) {
|
|
232
238
|
console.error(chalk.red(err.stack));
|
|
233
239
|
}
|
|
234
240
|
else {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { render, evalExpr } from './pipeline/template.js';
|
|
2
|
+
const ctx = { item: { first: 'X', second: 'Y' } };
|
|
3
|
+
console.log('evalExpr item.first:', JSON.stringify(evalExpr('item.first', ctx)));
|
|
4
|
+
console.log('evalExpr item.second:', JSON.stringify(evalExpr('item.second', ctx)));
|
|
5
|
+
const template = '$' + '{{ item.first }}-$' + '{{ item.second }}';
|
|
6
|
+
console.log('template:', JSON.stringify(template));
|
|
7
|
+
console.log('render result:', JSON.stringify(render(template, ctx)));
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pipeline executor: runs YAML pipeline steps sequentially.
|
|
3
3
|
*/
|
|
4
|
-
import chalk from 'chalk';
|
|
5
4
|
import { stepNavigate, stepClick, stepType, stepWait, stepPress, stepSnapshot, stepEvaluate } from './steps/browser.js';
|
|
6
5
|
import { stepFetch } from './steps/fetch.js';
|
|
7
6
|
import { stepSelect, stepMap, stepFilter, stepSort, stepLimit } from './steps/transform.js';
|
|
8
7
|
import { stepIntercept } from './steps/intercept.js';
|
|
9
8
|
import { stepTap } from './steps/tap.js';
|
|
9
|
+
import { log } from '../logger.js';
|
|
10
10
|
/** Registry of all available step handlers */
|
|
11
11
|
const STEP_HANDLERS = {
|
|
12
12
|
navigate: stepNavigate,
|
|
@@ -43,7 +43,7 @@ export async function executePipeline(page, pipeline, ctx = {}) {
|
|
|
43
43
|
}
|
|
44
44
|
else {
|
|
45
45
|
if (debug)
|
|
46
|
-
|
|
46
|
+
log.warn(`Unknown step: ${op}`);
|
|
47
47
|
}
|
|
48
48
|
if (debug)
|
|
49
49
|
debugStepResult(op, data);
|
|
@@ -59,24 +59,24 @@ function debugStepStart(stepNum, total, op, params) {
|
|
|
59
59
|
else if (params && typeof params === 'object' && !Array.isArray(params)) {
|
|
60
60
|
preview = ` (${Object.keys(params).join(', ')})`;
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
log.step(stepNum, total, op, preview);
|
|
63
63
|
}
|
|
64
64
|
function debugStepResult(op, data) {
|
|
65
65
|
if (data === null || data === undefined) {
|
|
66
|
-
|
|
66
|
+
log.stepResult('(no data)');
|
|
67
67
|
}
|
|
68
68
|
else if (Array.isArray(data)) {
|
|
69
|
-
|
|
69
|
+
log.stepResult(`${data.length} items`);
|
|
70
70
|
}
|
|
71
71
|
else if (typeof data === 'object') {
|
|
72
72
|
const keys = Object.keys(data).slice(0, 5);
|
|
73
|
-
|
|
73
|
+
log.stepResult(`dict (${keys.join(', ')}${Object.keys(data).length > 5 ? '...' : ''})`);
|
|
74
74
|
}
|
|
75
75
|
else if (typeof data === 'string') {
|
|
76
76
|
const p = data.slice(0, 60).replace(/\n/g, '\\n');
|
|
77
|
-
|
|
77
|
+
log.stepResult(`"${p}${data.length > 60 ? '...' : ''}"`);
|
|
78
78
|
}
|
|
79
79
|
else {
|
|
80
|
-
|
|
80
|
+
log.stepResult(`${typeof data}`);
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Browser interaction primitives.
|
|
4
4
|
*/
|
|
5
5
|
import type { IPage } from '../../types.js';
|
|
6
|
-
export declare function stepNavigate(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
7
|
-
export declare function stepClick(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
8
|
-
export declare function stepType(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
9
|
-
export declare function stepWait(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
10
|
-
export declare function stepPress(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
11
|
-
export declare function stepSnapshot(page: IPage, params: any, _data: any, _args: Record<string, any>): Promise<any>;
|
|
12
|
-
export declare function stepEvaluate(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
6
|
+
export declare function stepNavigate(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
7
|
+
export declare function stepClick(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
8
|
+
export declare function stepType(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
9
|
+
export declare function stepWait(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
10
|
+
export declare function stepPress(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
11
|
+
export declare function stepSnapshot(page: IPage | null, params: any, _data: any, _args: Record<string, any>): Promise<any>;
|
|
12
|
+
export declare function stepEvaluate(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Pipeline step: intercept — declarative XHR interception.
|
|
3
3
|
*/
|
|
4
4
|
import type { IPage } from '../../types.js';
|
|
5
|
-
export declare function stepIntercept(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
5
|
+
export declare function stepIntercept(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
* 5. Returns the captured data (optionally sub-selected)
|
|
10
10
|
*/
|
|
11
11
|
import type { IPage } from '../../types.js';
|
|
12
|
-
export declare function stepTap(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
12
|
+
export declare function stepTap(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function promoteCandidate(opts: any): any;
|
package/dist/promote.js
ADDED
package/dist/register.js
ADDED
package/dist/scaffold.js
ADDED
package/dist/setup.js
CHANGED
|
@@ -9,7 +9,7 @@ import chalk from 'chalk';
|
|
|
9
9
|
import { createInterface } from 'node:readline/promises';
|
|
10
10
|
import { stdin as input, stdout as output } from 'node:process';
|
|
11
11
|
import { PLAYWRIGHT_TOKEN_ENV, checkExtensionInstalled, checkTokenConnectivity, discoverExtensionToken, fileExists, getDefaultShellRcPath, runBrowserDoctor, shortenPath, toolName, upsertJsonConfigToken, upsertShellToken, upsertTomlConfigToken, writeFileWithMkdir, } from './doctor.js';
|
|
12
|
-
import { getTokenFingerprint } from './browser.js';
|
|
12
|
+
import { getTokenFingerprint } from './browser/index.js';
|
|
13
13
|
import { checkboxPrompt } from './tui.js';
|
|
14
14
|
export async function runSetup(opts = {}) {
|
|
15
15
|
console.log();
|
|
@@ -159,12 +159,18 @@ export async function runSetup(opts = {}) {
|
|
|
159
159
|
console.log(` ${chalk.green('✓')} Browser connected in ${(result.durationMs / 1000).toFixed(1)}s`);
|
|
160
160
|
}
|
|
161
161
|
else {
|
|
162
|
+
console.log(` ${chalk.green('✓')} Token saved successfully.`);
|
|
162
163
|
console.log(` ${chalk.yellow('!')} Browser connectivity test failed: ${result.error ?? 'unknown'}`);
|
|
163
|
-
console.log(chalk.dim('
|
|
164
|
+
console.log(chalk.dim(' Token configuration is complete. To use opencli, make sure Chrome'));
|
|
165
|
+
console.log(chalk.dim(' is running with the Playwright MCP Bridge extension enabled.'));
|
|
166
|
+
console.log(chalk.dim(` Run ${chalk.bold('opencli doctor --live')} to re-test connectivity.`));
|
|
164
167
|
}
|
|
165
168
|
}
|
|
166
169
|
catch {
|
|
167
|
-
console.log(` ${chalk.
|
|
170
|
+
console.log(` ${chalk.green('✓')} Token saved successfully.`);
|
|
171
|
+
console.log(` ${chalk.yellow('!')} Browser connectivity test skipped (Chrome may not be running).`);
|
|
172
|
+
console.log(chalk.dim(' Token configuration is complete. Start Chrome to begin using opencli.'));
|
|
173
|
+
console.log(chalk.dim(` Run ${chalk.bold('opencli doctor --live')} to re-test connectivity.`));
|
|
168
174
|
}
|
|
169
175
|
console.log();
|
|
170
176
|
}
|
package/dist/smoke.d.ts
ADDED
package/dist/smoke.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jackwener/opencli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"dev": "tsx src/main.ts",
|
|
18
18
|
"build": "tsc && npm run clean-yaml && npm run copy-yaml && npm run build-manifest",
|
|
19
19
|
"build-manifest": "node dist/build-manifest.js",
|
|
20
|
-
"clean-yaml": "node -
|
|
21
|
-
"copy-yaml": "node -
|
|
20
|
+
"clean-yaml": "node scripts/clean-yaml.cjs",
|
|
21
|
+
"copy-yaml": "node scripts/copy-yaml.cjs",
|
|
22
22
|
"start": "node dist/main.js",
|
|
23
23
|
"postinstall": "node scripts/postinstall.js || true",
|
|
24
24
|
"typecheck": "tsc --noEmit",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clean YAML files from dist/clis/ before copying fresh ones.
|
|
3
|
+
*/
|
|
4
|
+
const { readdirSync, rmSync, existsSync, statSync } = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
function walk(dir) {
|
|
8
|
+
if (!existsSync(dir)) return;
|
|
9
|
+
for (const f of readdirSync(dir)) {
|
|
10
|
+
const fp = path.join(dir, f);
|
|
11
|
+
if (statSync(fp).isDirectory()) {
|
|
12
|
+
walk(fp);
|
|
13
|
+
} else if (/\.ya?ml$/.test(f)) {
|
|
14
|
+
rmSync(fp);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
walk('dist/clis');
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy YAML files from src/clis/ to dist/clis/.
|
|
3
|
+
*/
|
|
4
|
+
const { readdirSync, copyFileSync, mkdirSync, existsSync, statSync } = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
function walk(src, dst) {
|
|
8
|
+
if (!existsSync(src)) return;
|
|
9
|
+
for (const f of readdirSync(src)) {
|
|
10
|
+
const sp = path.join(src, f);
|
|
11
|
+
const dp = path.join(dst, f);
|
|
12
|
+
if (statSync(sp).isDirectory()) {
|
|
13
|
+
walk(sp, dp);
|
|
14
|
+
} else if (/\.ya?ml$/.test(f)) {
|
|
15
|
+
mkdirSync(path.dirname(dp), { recursive: true });
|
|
16
|
+
copyFileSync(sp, dp);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
walk('src/clis', 'dist/clis');
|
package/scripts/postinstall.js
CHANGED
|
@@ -70,30 +70,51 @@ function ensureDir(dir) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* Ensure fpath contains the custom completions directory in .zshrc
|
|
73
|
+
* Ensure fpath contains the custom completions directory in .zshrc.
|
|
74
|
+
*
|
|
75
|
+
* Key detail: the fpath line MUST appear BEFORE the first `compinit` call,
|
|
76
|
+
* otherwise compinit won't scan our completions directory. This is critical
|
|
77
|
+
* for oh-my-zsh users (source $ZSH/oh-my-zsh.sh calls compinit internally).
|
|
74
78
|
*/
|
|
75
79
|
function ensureZshFpath(completionsDir, zshrcPath) {
|
|
76
80
|
const fpathLine = `fpath=(${completionsDir} $fpath)`;
|
|
77
81
|
const autoloadLine = `autoload -Uz compinit && compinit`;
|
|
82
|
+
const marker = '# opencli completion';
|
|
78
83
|
|
|
79
84
|
if (!existsSync(zshrcPath)) {
|
|
80
|
-
writeFileSync(zshrcPath, `${fpathLine}\n${autoloadLine}\n`, 'utf8');
|
|
85
|
+
writeFileSync(zshrcPath, `${marker}\n${fpathLine}\n${autoloadLine}\n`, 'utf8');
|
|
81
86
|
return;
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
const content = readFileSync(zshrcPath, 'utf8');
|
|
85
90
|
|
|
86
|
-
//
|
|
91
|
+
// Already configured — nothing to do
|
|
87
92
|
if (content.includes(completionsDir)) {
|
|
88
|
-
return;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Find the first line that triggers compinit (direct call or oh-my-zsh source)
|
|
97
|
+
const lines = content.split('\n');
|
|
98
|
+
let insertIdx = -1;
|
|
99
|
+
for (let i = 0; i < lines.length; i++) {
|
|
100
|
+
const trimmed = lines[i].trim();
|
|
101
|
+
// Skip comment-only lines
|
|
102
|
+
if (trimmed.startsWith('#')) continue;
|
|
103
|
+
if (/compinit/.test(trimmed) || /source\s+.*oh-my-zsh\.sh/.test(trimmed)) {
|
|
104
|
+
insertIdx = i;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
89
107
|
}
|
|
90
108
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
if (insertIdx !== -1) {
|
|
110
|
+
// Insert fpath BEFORE the compinit / oh-my-zsh source line
|
|
111
|
+
lines.splice(insertIdx, 0, marker, fpathLine);
|
|
112
|
+
writeFileSync(zshrcPath, lines.join('\n'), 'utf8');
|
|
113
|
+
} else {
|
|
114
|
+
// No compinit found — append fpath + compinit at the end
|
|
115
|
+
let addition = `\n${marker}\n${fpathLine}\n${autoloadLine}\n`;
|
|
116
|
+
appendFileSync(zshrcPath, addition, 'utf8');
|
|
95
117
|
}
|
|
96
|
-
appendFileSync(zshrcPath, addition, 'utf8');
|
|
97
118
|
}
|
|
98
119
|
|
|
99
120
|
// ── Main ───────────────────────────────────────────────────────────────────
|
package/src/bilibili.ts
CHANGED
|
@@ -56,7 +56,7 @@ export async function wbiSign(
|
|
|
56
56
|
const mixinKey = getMixinKey(imgKey, subKey);
|
|
57
57
|
const wts = Math.floor(Date.now() / 1000);
|
|
58
58
|
const sorted: Record<string, string> = {};
|
|
59
|
-
const allParams = { ...params, wts: String(wts) };
|
|
59
|
+
const allParams: Record<string, any> = { ...params, wts: String(wts) };
|
|
60
60
|
for (const key of Object.keys(allParams).sort()) {
|
|
61
61
|
sorted[key] = String(allParams[key]).replace(/[!'()*]/g, '');
|
|
62
62
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server path discovery and argument building.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
|
|
11
|
+
let _cachedMcpServerPath: string | null | undefined;
|
|
12
|
+
|
|
13
|
+
export function findMcpServerPath(): string | null {
|
|
14
|
+
if (_cachedMcpServerPath !== undefined) return _cachedMcpServerPath;
|
|
15
|
+
|
|
16
|
+
const envMcp = process.env.OPENCLI_MCP_SERVER_PATH;
|
|
17
|
+
if (envMcp && fs.existsSync(envMcp)) {
|
|
18
|
+
_cachedMcpServerPath = envMcp;
|
|
19
|
+
return _cachedMcpServerPath;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Check local node_modules first (@playwright/mcp is the modern package)
|
|
23
|
+
const localMcp = path.resolve('node_modules', '@playwright', 'mcp', 'cli.js');
|
|
24
|
+
if (fs.existsSync(localMcp)) {
|
|
25
|
+
_cachedMcpServerPath = localMcp;
|
|
26
|
+
return _cachedMcpServerPath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check project-relative path
|
|
30
|
+
const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const projectMcp = path.resolve(__dirname2, '..', '..', 'node_modules', '@playwright', 'mcp', 'cli.js');
|
|
32
|
+
if (fs.existsSync(projectMcp)) {
|
|
33
|
+
_cachedMcpServerPath = projectMcp;
|
|
34
|
+
return _cachedMcpServerPath;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check common locations
|
|
38
|
+
const candidates = [
|
|
39
|
+
path.join(os.homedir(), '.npm', '_npx'),
|
|
40
|
+
path.join(os.homedir(), 'node_modules', '.bin'),
|
|
41
|
+
'/usr/local/lib/node_modules',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Try npx resolution (legacy package name)
|
|
45
|
+
try {
|
|
46
|
+
const result = execSync('npx -y --package=@playwright/mcp which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 10000 }).trim();
|
|
47
|
+
if (result && fs.existsSync(result)) {
|
|
48
|
+
_cachedMcpServerPath = result;
|
|
49
|
+
return _cachedMcpServerPath;
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
|
|
53
|
+
// Try which
|
|
54
|
+
try {
|
|
55
|
+
const result = execSync('which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
56
|
+
if (result && fs.existsSync(result)) {
|
|
57
|
+
_cachedMcpServerPath = result;
|
|
58
|
+
return _cachedMcpServerPath;
|
|
59
|
+
}
|
|
60
|
+
} catch {}
|
|
61
|
+
|
|
62
|
+
// Search in common npx cache
|
|
63
|
+
for (const base of candidates) {
|
|
64
|
+
if (!fs.existsSync(base)) continue;
|
|
65
|
+
try {
|
|
66
|
+
const found = execSync(`find "${base}" -name "cli.js" -path "*playwright*mcp*" 2>/dev/null | head -1`, { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
67
|
+
if (found) {
|
|
68
|
+
_cachedMcpServerPath = found;
|
|
69
|
+
return _cachedMcpServerPath;
|
|
70
|
+
}
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_cachedMcpServerPath = null;
|
|
75
|
+
return _cachedMcpServerPath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function buildMcpArgs(input: { mcpPath: string; executablePath?: string | null }): string[] {
|
|
79
|
+
const args = [input.mcpPath];
|
|
80
|
+
if (!process.env.CI) {
|
|
81
|
+
// Local: always connect to user's running Chrome via MCP Bridge extension
|
|
82
|
+
args.push('--extension');
|
|
83
|
+
}
|
|
84
|
+
// CI: standalone mode — @playwright/mcp launches its own browser (headed by default).
|
|
85
|
+
// xvfb provides a virtual display for headed mode in GitHub Actions.
|
|
86
|
+
if (input.executablePath) {
|
|
87
|
+
args.push('--executable-path', input.executablePath);
|
|
88
|
+
}
|
|
89
|
+
return args;
|
|
90
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser connection error classification and formatting.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
export type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'unknown';
|
|
8
|
+
|
|
9
|
+
export type ConnectFailureInput = {
|
|
10
|
+
kind: ConnectFailureKind;
|
|
11
|
+
timeout: number;
|
|
12
|
+
hasExtensionToken: boolean;
|
|
13
|
+
tokenFingerprint?: string | null;
|
|
14
|
+
stderr?: string;
|
|
15
|
+
exitCode?: number | null;
|
|
16
|
+
rawMessage?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function getTokenFingerprint(token: string | undefined): string | null {
|
|
20
|
+
if (!token) return null;
|
|
21
|
+
return createHash('sha256').update(token).digest('hex').slice(0, 8);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatBrowserConnectError(input: ConnectFailureInput): Error {
|
|
25
|
+
const stderr = input.stderr?.trim();
|
|
26
|
+
const suffix = stderr ? `\n\nMCP stderr:\n${stderr}` : '';
|
|
27
|
+
const tokenHint = input.tokenFingerprint ? ` Token fingerprint: ${input.tokenFingerprint}.` : '';
|
|
28
|
+
|
|
29
|
+
if (input.kind === 'missing-token') {
|
|
30
|
+
return new Error(
|
|
31
|
+
'Failed to connect to Playwright MCP Bridge: PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set.\n\n' +
|
|
32
|
+
'Without this token, Chrome will show a manual approval dialog for every new MCP connection. ' +
|
|
33
|
+
'Copy the token from the Playwright MCP Bridge extension and set it in BOTH your shell environment and MCP client config.' +
|
|
34
|
+
suffix,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (input.kind === 'extension-not-installed') {
|
|
39
|
+
return new Error(
|
|
40
|
+
'Failed to connect to Playwright MCP Bridge: the browser extension did not attach.\n\n' +
|
|
41
|
+
'Make sure Chrome is running and the "Playwright MCP Bridge" extension is installed and enabled. ' +
|
|
42
|
+
'If Chrome shows an approval dialog, click Allow.' +
|
|
43
|
+
suffix,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (input.kind === 'extension-timeout') {
|
|
48
|
+
const likelyCause = input.hasExtensionToken
|
|
49
|
+
? `The most likely cause is that PLAYWRIGHT_MCP_EXTENSION_TOKEN does not match the token currently shown by the browser extension.${tokenHint} Re-copy the token from the extension and update BOTH your shell environment and MCP client config.`
|
|
50
|
+
: 'PLAYWRIGHT_MCP_EXTENSION_TOKEN is not configured, so the extension may be waiting for manual approval.';
|
|
51
|
+
return new Error(
|
|
52
|
+
`Timed out connecting to Playwright MCP Bridge (${input.timeout}s).\n\n` +
|
|
53
|
+
`${likelyCause} If a browser prompt is visible, click Allow.` +
|
|
54
|
+
suffix,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (input.kind === 'mcp-init') {
|
|
59
|
+
return new Error(`Failed to initialize Playwright MCP: ${input.rawMessage ?? 'unknown error'}${suffix}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (input.kind === 'process-exit') {
|
|
63
|
+
return new Error(
|
|
64
|
+
`Playwright MCP process exited before the browser connection was established${input.exitCode == null ? '' : ` (code ${input.exitCode})`}.` +
|
|
65
|
+
suffix,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return new Error(input.rawMessage ?? 'Failed to connect to browser');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function inferConnectFailureKind(args: {
|
|
73
|
+
hasExtensionToken: boolean;
|
|
74
|
+
stderr: string;
|
|
75
|
+
rawMessage?: string;
|
|
76
|
+
exited?: boolean;
|
|
77
|
+
}): ConnectFailureKind {
|
|
78
|
+
const haystack = `${args.rawMessage ?? ''}\n${args.stderr}`.toLowerCase();
|
|
79
|
+
|
|
80
|
+
if (!args.hasExtensionToken)
|
|
81
|
+
return 'missing-token';
|
|
82
|
+
if (haystack.includes('extension connection timeout') || haystack.includes('playwright mcp bridge'))
|
|
83
|
+
return 'extension-not-installed';
|
|
84
|
+
if (args.rawMessage?.startsWith('MCP init failed:'))
|
|
85
|
+
return 'mcp-init';
|
|
86
|
+
if (args.exited)
|
|
87
|
+
return 'process-exit';
|
|
88
|
+
return 'extension-timeout';
|
|
89
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser module — public API re-exports.
|
|
3
|
+
*
|
|
4
|
+
* This barrel replaces the former monolithic browser.ts.
|
|
5
|
+
* External code should import from './browser/index.js' (or './browser.js' via Node resolution).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { Page } from './page.js';
|
|
9
|
+
export { PlaywrightMCP } from './mcp.js';
|
|
10
|
+
export { getTokenFingerprint, formatBrowserConnectError } from './errors.js';
|
|
11
|
+
export type { ConnectFailureKind, ConnectFailureInput } from './errors.js';
|
|
12
|
+
|
|
13
|
+
// Test-only helpers — exposed for unit tests
|
|
14
|
+
import { createJsonRpcRequest } from './mcp.js';
|
|
15
|
+
import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
|
|
16
|
+
import { buildMcpArgs } from './discover.js';
|
|
17
|
+
import { withTimeoutMs } from '../runtime.js';
|
|
18
|
+
|
|
19
|
+
export const __test__ = {
|
|
20
|
+
createJsonRpcRequest,
|
|
21
|
+
extractTabEntries,
|
|
22
|
+
diffTabIndexes,
|
|
23
|
+
appendLimited,
|
|
24
|
+
buildMcpArgs,
|
|
25
|
+
withTimeoutMs,
|
|
26
|
+
};
|