@jackwener/opencli 1.2.6 → 1.3.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/.github/workflows/release.yml +7 -0
- package/README.md +3 -64
- package/README.zh-CN.md +2 -48
- package/SKILL.md +1 -3
- package/TESTING.md +87 -69
- package/dist/browser/cdp.js +5 -4
- package/dist/browser/daemon-client.js +9 -3
- package/dist/browser/discover.js +3 -1
- package/dist/browser/dom-helpers.d.ts +8 -0
- package/dist/browser/dom-helpers.js +33 -0
- package/dist/browser/page.js +9 -5
- package/dist/cli.js +2 -9
- package/dist/daemon.d.ts +8 -0
- package/dist/daemon.js +53 -6
- package/dist/doctor.js +14 -2
- package/dist/doctor.test.js +15 -19
- package/docs/developer/testing.md +87 -69
- package/docs/guide/browser-bridge.md +0 -1
- package/docs/guide/getting-started.md +1 -1
- package/docs/guide/troubleshooting.md +1 -1
- package/docs/index.md +1 -1
- package/docs/zh/guide/browser-bridge.md +0 -1
- package/package.json +1 -1
- package/src/browser/cdp.ts +5 -3
- package/src/browser/daemon-client.ts +9 -3
- package/src/browser/discover.ts +3 -1
- package/src/browser/dom-helpers.ts +34 -0
- package/src/browser/page.ts +9 -4
- package/src/cli.ts +2 -10
- package/src/daemon.ts +57 -7
- package/src/doctor.test.ts +17 -25
- package/src/doctor.ts +14 -2
- package/.github/workflows/release-please.yml +0 -25
- package/dist/setup.d.ts +0 -10
- package/dist/setup.js +0 -66
- package/src/setup.ts +0 -69
|
@@ -145,3 +145,37 @@ export function networkRequestsJs(includeStatic: boolean): string {
|
|
|
145
145
|
})()
|
|
146
146
|
`;
|
|
147
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Generate JS to wait until the DOM stabilizes (no mutations for `quietMs`),
|
|
151
|
+
* with a hard cap at `maxMs`. Uses MutationObserver in the browser.
|
|
152
|
+
*
|
|
153
|
+
* Returns as soon as the page stops changing, avoiding unnecessary fixed waits.
|
|
154
|
+
* If document.body is not available, falls back to a fixed sleep of maxMs.
|
|
155
|
+
*/
|
|
156
|
+
export function waitForDomStableJs(maxMs: number, quietMs: number): string {
|
|
157
|
+
return `
|
|
158
|
+
new Promise(resolve => {
|
|
159
|
+
if (!document.body) {
|
|
160
|
+
setTimeout(() => resolve('nobody'), ${maxMs});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
let timer = null;
|
|
164
|
+
let cap = null;
|
|
165
|
+
const done = (reason) => {
|
|
166
|
+
clearTimeout(timer);
|
|
167
|
+
clearTimeout(cap);
|
|
168
|
+
obs.disconnect();
|
|
169
|
+
resolve(reason);
|
|
170
|
+
};
|
|
171
|
+
const resetQuiet = () => {
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
timer = setTimeout(() => done('quiet'), ${quietMs});
|
|
174
|
+
};
|
|
175
|
+
const obs = new MutationObserver(resetQuiet);
|
|
176
|
+
obs.observe(document.body, { childList: true, subtree: true, attributes: true });
|
|
177
|
+
resetQuiet();
|
|
178
|
+
cap = setTimeout(() => done('capped'), ${maxMs});
|
|
179
|
+
})
|
|
180
|
+
`;
|
|
181
|
+
}
|
package/src/browser/page.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
scrollJs,
|
|
24
24
|
autoScrollJs,
|
|
25
25
|
networkRequestsJs,
|
|
26
|
+
waitForDomStableJs,
|
|
26
27
|
} from './dom-helpers.js';
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -53,11 +54,15 @@ export class Page implements IPage {
|
|
|
53
54
|
if (result?.tabId) {
|
|
54
55
|
this._tabId = result.tabId;
|
|
55
56
|
}
|
|
56
|
-
//
|
|
57
|
-
//
|
|
57
|
+
// Smart settle: use DOM stability detection instead of fixed sleep.
|
|
58
|
+
// settleMs is now a timeout cap (default 1000ms), not a fixed wait.
|
|
58
59
|
if (options?.waitUntil !== 'none') {
|
|
59
|
-
const
|
|
60
|
-
await
|
|
60
|
+
const maxMs = options?.settleMs ?? 1000;
|
|
61
|
+
await sendCommand('exec', {
|
|
62
|
+
code: waitForDomStableJs(maxMs, Math.min(500, maxMs)),
|
|
63
|
+
...this._workspaceOpt(),
|
|
64
|
+
...this._tabOpt(),
|
|
65
|
+
});
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
|
package/src/cli.ts
CHANGED
|
@@ -202,12 +202,12 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
202
202
|
console.log(renderCascadeResult(result));
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
-
// ── Built-in: doctor /
|
|
205
|
+
// ── Built-in: doctor / completion ──────────────────────────────────────────
|
|
206
206
|
|
|
207
207
|
program
|
|
208
208
|
.command('doctor')
|
|
209
209
|
.description('Diagnose opencli browser bridge connectivity')
|
|
210
|
-
.option('--live', '
|
|
210
|
+
.option('--no-live', 'Skip live browser connectivity test')
|
|
211
211
|
.option('--sessions', 'Show active automation sessions', false)
|
|
212
212
|
.action(async (opts) => {
|
|
213
213
|
const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
|
|
@@ -215,14 +215,6 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
215
215
|
console.log(renderBrowserDoctorReport(report));
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
-
program
|
|
219
|
-
.command('setup')
|
|
220
|
-
.description('Interactive setup: verify browser bridge connectivity')
|
|
221
|
-
.action(async () => {
|
|
222
|
-
const { runSetup } = await import('./setup.js');
|
|
223
|
-
await runSetup({ cliVersion: PKG_VERSION });
|
|
224
|
-
});
|
|
225
|
-
|
|
226
218
|
program
|
|
227
219
|
.command('completion')
|
|
228
220
|
.description('Output shell completion script')
|
package/src/daemon.ts
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* CLI → HTTP POST /command → daemon → WebSocket → Extension
|
|
6
6
|
* Extension → WebSocket result → daemon → HTTP response → CLI
|
|
7
7
|
*
|
|
8
|
+
* Security (defense-in-depth against browser-based CSRF):
|
|
9
|
+
* 1. Origin check — reject HTTP/WS from non chrome-extension:// origins
|
|
10
|
+
* 2. Custom header — require X-OpenCLI header (browsers can't send it
|
|
11
|
+
* without CORS preflight, which we deny)
|
|
12
|
+
* 3. No CORS headers — responses never include Access-Control-Allow-Origin
|
|
13
|
+
* 4. Body size limit — 1 MB max to prevent OOM
|
|
14
|
+
* 5. WebSocket verifyClient — reject upgrade before connection is established
|
|
15
|
+
*
|
|
8
16
|
* Lifecycle:
|
|
9
17
|
* - Auto-spawned by opencli on first browser command
|
|
10
18
|
* - Auto-exits after 5 minutes of idle
|
|
@@ -49,25 +57,56 @@ function resetIdleTimer(): void {
|
|
|
49
57
|
|
|
50
58
|
// ─── HTTP Server ─────────────────────────────────────────────────────
|
|
51
59
|
|
|
60
|
+
const MAX_BODY = 1024 * 1024; // 1 MB — commands are tiny; this prevents OOM
|
|
61
|
+
|
|
52
62
|
function readBody(req: IncomingMessage): Promise<string> {
|
|
53
63
|
return new Promise((resolve, reject) => {
|
|
54
64
|
const chunks: Buffer[] = [];
|
|
55
|
-
|
|
65
|
+
let size = 0;
|
|
66
|
+
req.on('data', (c: Buffer) => {
|
|
67
|
+
size += c.length;
|
|
68
|
+
if (size > MAX_BODY) { req.destroy(); reject(new Error('Body too large')); return; }
|
|
69
|
+
chunks.push(c);
|
|
70
|
+
});
|
|
56
71
|
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
57
72
|
req.on('error', reject);
|
|
58
73
|
});
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
function jsonResponse(res: ServerResponse, status: number, data: unknown): void {
|
|
62
|
-
res.writeHead(status, { 'Content-Type': 'application/json'
|
|
77
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
63
78
|
res.end(JSON.stringify(data));
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
// ─── Security: Origin & custom-header check ──────────────────────
|
|
83
|
+
// Block browser-based CSRF: browsers always send an Origin header on
|
|
84
|
+
// cross-origin requests. Node.js CLI fetch does NOT send Origin, so
|
|
85
|
+
// legitimate CLI requests pass through. Chrome Extension connects via
|
|
86
|
+
// WebSocket (which bypasses this HTTP handler entirely).
|
|
87
|
+
const origin = req.headers['origin'] as string | undefined;
|
|
88
|
+
if (origin && !origin.startsWith('chrome-extension://')) {
|
|
89
|
+
jsonResponse(res, 403, { ok: false, error: 'Forbidden: cross-origin request blocked' });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// CORS: do NOT send Access-Control-Allow-Origin for normal requests.
|
|
94
|
+
// Only handle preflight so browsers get a definitive "no" answer.
|
|
95
|
+
if (req.method === 'OPTIONS') {
|
|
96
|
+
// No ACAO header → browser will block the actual request.
|
|
97
|
+
res.writeHead(204);
|
|
98
|
+
res.end();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Require custom header on all HTTP requests. Browsers cannot attach
|
|
103
|
+
// custom headers in "simple" requests, and our preflight returns no
|
|
104
|
+
// Access-Control-Allow-Headers, so scripted fetch() from web pages is
|
|
105
|
+
// blocked even if Origin check is somehow bypassed.
|
|
106
|
+
if (!req.headers['x-opencli']) {
|
|
107
|
+
jsonResponse(res, 403, { ok: false, error: 'Forbidden: missing X-OpenCLI header' });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
71
110
|
|
|
72
111
|
const url = req.url ?? '/';
|
|
73
112
|
const pathname = url.split('?')[0];
|
|
@@ -136,7 +175,18 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
|
|
|
136
175
|
// ─── WebSocket for Extension ─────────────────────────────────────────
|
|
137
176
|
|
|
138
177
|
const httpServer = createServer((req, res) => { handleRequest(req, res).catch(() => { res.writeHead(500); res.end(); }); });
|
|
139
|
-
const wss = new WebSocketServer({
|
|
178
|
+
const wss = new WebSocketServer({
|
|
179
|
+
server: httpServer,
|
|
180
|
+
path: '/ext',
|
|
181
|
+
verifyClient: ({ req }: { req: IncomingMessage }) => {
|
|
182
|
+
// Block browser-originated WebSocket connections. Browsers don't
|
|
183
|
+
// enforce CORS on WebSocket, so a malicious webpage could connect to
|
|
184
|
+
// ws://localhost:19825/ext and impersonate the Extension. Real Chrome
|
|
185
|
+
// Extensions send origin chrome-extension://<id>.
|
|
186
|
+
const origin = req.headers['origin'] as string | undefined;
|
|
187
|
+
return !origin || origin.startsWith('chrome-extension://');
|
|
188
|
+
},
|
|
189
|
+
});
|
|
140
190
|
|
|
141
191
|
wss.on('connection', (ws: WebSocket) => {
|
|
142
192
|
console.error('[daemon] Extension connected');
|
package/src/doctor.test.ts
CHANGED
|
@@ -84,36 +84,28 @@ describe('doctor report rendering', () => {
|
|
|
84
84
|
issues: [],
|
|
85
85
|
}));
|
|
86
86
|
|
|
87
|
-
expect(text).toContain('[SKIP] Connectivity:
|
|
87
|
+
expect(text).toContain('[SKIP] Connectivity: skipped (--no-live)');
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
it('reports consistent status when live check auto-starts the daemon', async () => {
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
mockCheckDaemonStatus.mockResolvedValueOnce({ running:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
const report = await runBrowserDoctor({ live:
|
|
100
|
-
|
|
101
|
-
// Status reflects
|
|
102
|
-
expect(report.daemonRunning).toBe(
|
|
91
|
+
// checkDaemonStatus is called twice: once for auto-start check, once for final status.
|
|
92
|
+
// First call: daemon not running (triggers auto-start attempt)
|
|
93
|
+
mockCheckDaemonStatus.mockResolvedValueOnce({ running: false, extensionConnected: false });
|
|
94
|
+
// Auto-start attempt via BrowserBridge.connect fails
|
|
95
|
+
mockConnect.mockRejectedValueOnce(new Error('Could not start daemon'));
|
|
96
|
+
// Second call: daemon still not running after failed auto-start
|
|
97
|
+
mockCheckDaemonStatus.mockResolvedValueOnce({ running: false, extensionConnected: false });
|
|
98
|
+
|
|
99
|
+
const report = await runBrowserDoctor({ live: false });
|
|
100
|
+
|
|
101
|
+
// Status reflects daemon not running
|
|
102
|
+
expect(report.daemonRunning).toBe(false);
|
|
103
103
|
expect(report.extensionConnected).toBe(false);
|
|
104
|
-
// checkDaemonStatus
|
|
105
|
-
expect(mockCheckDaemonStatus).toHaveBeenCalledTimes(
|
|
106
|
-
// Should
|
|
107
|
-
expect(report.issues).not.toContain(
|
|
108
|
-
expect.stringContaining('Daemon is not running'),
|
|
109
|
-
);
|
|
110
|
-
// Should report extension not connected
|
|
111
|
-
expect(report.issues).toEqual(expect.arrayContaining([
|
|
112
|
-
expect.stringContaining('Chrome extension is not connected'),
|
|
113
|
-
]));
|
|
114
|
-
// Should report connectivity failure
|
|
104
|
+
// checkDaemonStatus called twice (initial + final)
|
|
105
|
+
expect(mockCheckDaemonStatus).toHaveBeenCalledTimes(2);
|
|
106
|
+
// Should report daemon not running
|
|
115
107
|
expect(report.issues).toEqual(expect.arrayContaining([
|
|
116
|
-
expect.stringContaining('
|
|
108
|
+
expect.stringContaining('Daemon is not running'),
|
|
117
109
|
]));
|
|
118
110
|
});
|
|
119
111
|
});
|
package/src/doctor.ts
CHANGED
|
@@ -51,7 +51,19 @@ export async function checkConnectivity(opts?: { timeout?: number }): Promise<Co
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<DoctorReport> {
|
|
54
|
-
//
|
|
54
|
+
// Try to auto-start daemon if it's not running, so we show accurate status.
|
|
55
|
+
let initialStatus = await checkDaemonStatus();
|
|
56
|
+
if (!initialStatus.running) {
|
|
57
|
+
try {
|
|
58
|
+
const mcp = new BrowserBridge();
|
|
59
|
+
await mcp.connect({ timeout: 5 });
|
|
60
|
+
await mcp.close();
|
|
61
|
+
} catch {
|
|
62
|
+
// Auto-start failed; we'll report it below.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Run the live connectivity check — it may also auto-start the daemon as a
|
|
55
67
|
// side-effect, so we read daemon status only *after* all side-effects settle.
|
|
56
68
|
let connectivity: ConnectivityResult | undefined;
|
|
57
69
|
if (opts.live) {
|
|
@@ -109,7 +121,7 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
|
|
|
109
121
|
: `failed (${report.connectivity.error ?? 'unknown'})`;
|
|
110
122
|
lines.push(`${connIcon} Connectivity: ${detail}`);
|
|
111
123
|
} else {
|
|
112
|
-
lines.push(`${chalk.dim('[SKIP]')} Connectivity:
|
|
124
|
+
lines.push(`${chalk.dim('[SKIP]')} Connectivity: skipped (--no-live)`);
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
if (report.sessions) {
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
name: Release Please
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
|
|
7
|
-
permissions:
|
|
8
|
-
contents: write
|
|
9
|
-
pull-requests: write
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
release-please:
|
|
13
|
-
runs-on: ubuntu-latest
|
|
14
|
-
steps:
|
|
15
|
-
- name: Ensure release-please token is configured
|
|
16
|
-
run: |
|
|
17
|
-
if [ -z "${{ secrets.RELEASE_PLEASE_TOKEN }}" ]; then
|
|
18
|
-
echo "RELEASE_PLEASE_TOKEN secret is required so release PRs can trigger downstream CI workflows." >&2
|
|
19
|
-
exit 1
|
|
20
|
-
fi
|
|
21
|
-
|
|
22
|
-
- uses: googleapis/release-please-action@v4
|
|
23
|
-
with:
|
|
24
|
-
release-type: node
|
|
25
|
-
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
package/dist/setup.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* setup.ts — Interactive browser setup for opencli
|
|
3
|
-
*
|
|
4
|
-
* Simplified for daemon-based architecture. No more token management.
|
|
5
|
-
* Just verifies daemon + extension connectivity.
|
|
6
|
-
*/
|
|
7
|
-
export declare function runSetup(opts?: {
|
|
8
|
-
cliVersion?: string;
|
|
9
|
-
token?: string;
|
|
10
|
-
}): Promise<void>;
|
package/dist/setup.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* setup.ts — Interactive browser setup for opencli
|
|
3
|
-
*
|
|
4
|
-
* Simplified for daemon-based architecture. No more token management.
|
|
5
|
-
* Just verifies daemon + extension connectivity.
|
|
6
|
-
*/
|
|
7
|
-
import chalk from 'chalk';
|
|
8
|
-
import { checkDaemonStatus } from './browser/discover.js';
|
|
9
|
-
import { checkConnectivity } from './doctor.js';
|
|
10
|
-
import { BrowserBridge } from './browser/index.js';
|
|
11
|
-
export async function runSetup(opts = {}) {
|
|
12
|
-
console.log();
|
|
13
|
-
console.log(chalk.bold(' opencli setup') + chalk.dim(' — browser bridge configuration'));
|
|
14
|
-
console.log();
|
|
15
|
-
// Step 1: Check daemon
|
|
16
|
-
console.log(chalk.dim(' Checking daemon status...'));
|
|
17
|
-
const status = await checkDaemonStatus();
|
|
18
|
-
if (status.running) {
|
|
19
|
-
console.log(` ${chalk.green('✓')} Daemon is running on port 19825`);
|
|
20
|
-
}
|
|
21
|
-
else {
|
|
22
|
-
console.log(` ${chalk.yellow('!')} Daemon is not running`);
|
|
23
|
-
console.log(chalk.dim(' The daemon starts automatically when you run a browser command.'));
|
|
24
|
-
console.log(chalk.dim(' Starting daemon now...'));
|
|
25
|
-
// Try to spawn daemon
|
|
26
|
-
const mcp = new BrowserBridge();
|
|
27
|
-
try {
|
|
28
|
-
await mcp.connect({ timeout: 5 });
|
|
29
|
-
await mcp.close();
|
|
30
|
-
console.log(` ${chalk.green('✓')} Daemon started successfully`);
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
console.log(` ${chalk.yellow('!')} Could not start daemon automatically`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
// Step 2: Check extension
|
|
37
|
-
const statusAfter = await checkDaemonStatus();
|
|
38
|
-
if (statusAfter.extensionConnected) {
|
|
39
|
-
console.log(` ${chalk.green('✓')} Chrome extension connected`);
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
console.log(` ${chalk.red('✗')} Chrome extension not connected`);
|
|
43
|
-
console.log();
|
|
44
|
-
console.log(chalk.dim(' To install the opencli Browser Bridge extension:'));
|
|
45
|
-
console.log(chalk.dim(' 1. Download from GitHub Releases'));
|
|
46
|
-
console.log(chalk.dim(' 2. Open chrome://extensions/ → Enable Developer Mode'));
|
|
47
|
-
console.log(chalk.dim(' 3. Click "Load unpacked" → select the extension folder'));
|
|
48
|
-
console.log(chalk.dim(' 4. Make sure Chrome is running'));
|
|
49
|
-
console.log();
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
// Step 3: Test connectivity
|
|
53
|
-
console.log();
|
|
54
|
-
console.log(chalk.dim(' Testing browser connectivity...'));
|
|
55
|
-
const conn = await checkConnectivity({ timeout: 5 });
|
|
56
|
-
if (conn.ok) {
|
|
57
|
-
console.log(` ${chalk.green('✓')} Browser connected in ${(conn.durationMs / 1000).toFixed(1)}s`);
|
|
58
|
-
console.log();
|
|
59
|
-
console.log(chalk.green.bold(' ✓ Setup complete! You can now use opencli browser commands.'));
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
console.log(` ${chalk.yellow('!')} Connectivity test failed: ${conn.error ?? 'unknown'}`);
|
|
63
|
-
console.log(chalk.dim(` Run ${chalk.bold('opencli doctor --live')} to diagnose.`));
|
|
64
|
-
}
|
|
65
|
-
console.log();
|
|
66
|
-
}
|
package/src/setup.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* setup.ts — Interactive browser setup for opencli
|
|
3
|
-
*
|
|
4
|
-
* Simplified for daemon-based architecture. No more token management.
|
|
5
|
-
* Just verifies daemon + extension connectivity.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import chalk from 'chalk';
|
|
9
|
-
import { checkDaemonStatus } from './browser/discover.js';
|
|
10
|
-
import { checkConnectivity } from './doctor.js';
|
|
11
|
-
import { BrowserBridge } from './browser/index.js';
|
|
12
|
-
|
|
13
|
-
export async function runSetup(opts: { cliVersion?: string; token?: string } = {}) {
|
|
14
|
-
console.log();
|
|
15
|
-
console.log(chalk.bold(' opencli setup') + chalk.dim(' — browser bridge configuration'));
|
|
16
|
-
console.log();
|
|
17
|
-
|
|
18
|
-
// Step 1: Check daemon
|
|
19
|
-
console.log(chalk.dim(' Checking daemon status...'));
|
|
20
|
-
const status = await checkDaemonStatus();
|
|
21
|
-
|
|
22
|
-
if (status.running) {
|
|
23
|
-
console.log(` ${chalk.green('✓')} Daemon is running on port 19825`);
|
|
24
|
-
} else {
|
|
25
|
-
console.log(` ${chalk.yellow('!')} Daemon is not running`);
|
|
26
|
-
console.log(chalk.dim(' The daemon starts automatically when you run a browser command.'));
|
|
27
|
-
console.log(chalk.dim(' Starting daemon now...'));
|
|
28
|
-
|
|
29
|
-
// Try to spawn daemon
|
|
30
|
-
const mcp = new BrowserBridge();
|
|
31
|
-
try {
|
|
32
|
-
await mcp.connect({ timeout: 5 });
|
|
33
|
-
await mcp.close();
|
|
34
|
-
console.log(` ${chalk.green('✓')} Daemon started successfully`);
|
|
35
|
-
} catch {
|
|
36
|
-
console.log(` ${chalk.yellow('!')} Could not start daemon automatically`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Step 2: Check extension
|
|
41
|
-
const statusAfter = await checkDaemonStatus();
|
|
42
|
-
if (statusAfter.extensionConnected) {
|
|
43
|
-
console.log(` ${chalk.green('✓')} Chrome extension connected`);
|
|
44
|
-
} else {
|
|
45
|
-
console.log(` ${chalk.red('✗')} Chrome extension not connected`);
|
|
46
|
-
console.log();
|
|
47
|
-
console.log(chalk.dim(' To install the opencli Browser Bridge extension:'));
|
|
48
|
-
console.log(chalk.dim(' 1. Download from GitHub Releases'));
|
|
49
|
-
console.log(chalk.dim(' 2. Open chrome://extensions/ → Enable Developer Mode'));
|
|
50
|
-
console.log(chalk.dim(' 3. Click "Load unpacked" → select the extension folder'));
|
|
51
|
-
console.log(chalk.dim(' 4. Make sure Chrome is running'));
|
|
52
|
-
console.log();
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Step 3: Test connectivity
|
|
57
|
-
console.log();
|
|
58
|
-
console.log(chalk.dim(' Testing browser connectivity...'));
|
|
59
|
-
const conn = await checkConnectivity({ timeout: 5 });
|
|
60
|
-
if (conn.ok) {
|
|
61
|
-
console.log(` ${chalk.green('✓')} Browser connected in ${(conn.durationMs / 1000).toFixed(1)}s`);
|
|
62
|
-
console.log();
|
|
63
|
-
console.log(chalk.green.bold(' ✓ Setup complete! You can now use opencli browser commands.'));
|
|
64
|
-
} else {
|
|
65
|
-
console.log(` ${chalk.yellow('!')} Connectivity test failed: ${conn.error ?? 'unknown'}`);
|
|
66
|
-
console.log(chalk.dim(` Run ${chalk.bold('opencli doctor --live')} to diagnose.`));
|
|
67
|
-
}
|
|
68
|
-
console.log();
|
|
69
|
-
}
|