-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathpatch-opencli.js
More file actions
250 lines (235 loc) · 10.9 KB
/
Copy pathpatch-opencli.js
File metadata and controls
250 lines (235 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
#!/usr/bin/env node
/**
* Post-install patch for @jackwener/opencli (CommonJS, works on Node 18+).
*
* Usage:
* node patch-opencli.js # patch the default global install
* node patch-opencli.js /opt/my-dir # patch a specific npm prefix dir
*
* Adds two env-var hooks that the published package lacks:
*
* OPENCLI_DAEMON_LISTEN (daemon.js)
* Default: 127.0.0.1 — set to 0.0.0.0 so the API container can reach
* the daemon running inside the chrome-N container.
*
* OPENCLI_DAEMON_HOST (daemon-client.js + browser/bridge.js)
* Default: 127.0.0.1 — set to chrome-1 (or chrome-N) so the CLI in the
* API container contacts the remote daemon instead of spawning one locally.
*
* v1.7.0 migration: mcp.js renamed to browser/bridge.js
*/
'use strict';
const fs = require('fs');
const path = require('path');
function resolvePackageDir(prefixDir) {
if (prefixDir) {
// Explicit prefix supplied (e.g. /opt/opencli-bridge)
const candidate = path.join(prefixDir, 'lib', 'node_modules', '@jackwener', 'opencli');
if (fs.existsSync(candidate)) return candidate;
// Some npm versions omit the 'lib/' level
const candidate2 = path.join(prefixDir, 'node_modules', '@jackwener', 'opencli');
if (fs.existsSync(candidate2)) return candidate2;
throw new Error('Could not find @jackwener/opencli under prefix: ' + prefixDir);
}
try {
return path.dirname(require.resolve('@jackwener/opencli/package.json'));
} catch (_) {
// Fallback: ask npm where its global root is (works with NodeSource installs)
const { execSync } = require('child_process');
const npmRoot = execSync('npm root -g').toString().trim();
return path.join(npmRoot, '@jackwener', 'opencli');
}
}
function patch(filePath, search, replace, label) {
if (!fs.existsSync(filePath)) {
console.log(' [skip] ' + label + ': file not found ' + filePath);
return;
}
let content = fs.readFileSync(filePath, 'utf8');
if (label === 'execution.js: managed web-adapter CDP routing' &&
content.includes('OPENCLI_ADMIN_MANAGED_CDP_ROUTING_V2')) {
console.log(' [skip] ' + label + ' superseded by V2');
return;
}
if (content.includes(replace.slice(0, 40))) {
console.log(' [skip] ' + label + ' already patched');
return;
}
if (!content.includes(search)) {
console.error(' [warn] ' + label + ': search string not found in ' + filePath);
return;
}
content = content.replace(search, replace);
fs.writeFileSync(filePath, content);
console.log(' [ok] ' + label);
}
const prefixDir = process.argv[2] || null;
const pkgDir = resolvePackageDir(prefixDir);
console.log('Patching opencli at ' + pkgDir + ' ...');
// ── 1. daemon.js: honour OPENCLI_DAEMON_LISTEN ───────────────────────────────
patch(
path.join(pkgDir, 'dist', 'src', 'daemon.js'),
"httpServer.listen(PORT, '127.0.0.1', () => {",
"const DAEMON_LISTEN = process.env.OPENCLI_DAEMON_LISTEN ?? '127.0.0.1';\nhttpServer.listen(PORT, DAEMON_LISTEN, () => {",
'daemon.js: OPENCLI_DAEMON_LISTEN'
);
// ── 2. daemon-client.js: honour OPENCLI_DAEMON_HOST ─────────────────────────
patch(
path.join(pkgDir, 'dist', 'src', 'browser', 'daemon-transport.js'),
'const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;',
"const DAEMON_HOST = process.env.OPENCLI_DAEMON_HOST ?? '127.0.0.1';\nconst DAEMON_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}`;",
'daemon-client.js: OPENCLI_DAEMON_HOST'
);
// ── 3. browser/bridge.js: skip local auto-spawn when daemon is remote ────────
// v1.7.0+: _ensureDaemon moved from mcp.js to browser/bridge.js.
// When OPENCLI_DAEMON_HOST is set to a remote address, probe that daemon
// instead of spawning a process inside the caller's container.
patch(
path.join(pkgDir, 'dist', 'src', 'browser', 'bridge.js'),
"import { ensureBrowserBridgeReady } from './daemon-lifecycle.js';",
"// OPENCLI_ADMIN_REMOTE_DAEMON_IMPORT_V1\nimport { ensureBrowserBridgeReady } from './daemon-lifecycle.js';\nimport { fetchDaemonStatus } from './daemon-transport.js';",
'browser/bridge.js: import remote daemon status probe'
);
patch(
path.join(pkgDir, 'dist', 'src', 'browser', 'bridge.js'),
` async _ensureDaemon(timeoutSeconds, contextId, preferredContextId) {
await ensureBrowserBridgeReady({
timeoutSeconds: timeoutSeconds ?? Math.ceil(DAEMON_SPAWN_TIMEOUT / 1000),
contextId,
preferredContextId,
});
}`,
` // OPENCLI_ADMIN_REMOTE_DAEMON_ROUTE_V2
async _ensureDaemon(timeoutSeconds, contextId, preferredContextId) {
const daemonHost = process.env.OPENCLI_DAEMON_HOST;
if (daemonHost && daemonHost !== '127.0.0.1' && daemonHost !== 'localhost') {
const remoteStatus = await fetchDaemonStatus({
timeout: (timeoutSeconds ?? Math.ceil(DAEMON_SPAWN_TIMEOUT / 1000)) * 1000,
contextId,
preferredContextId,
});
if (!remoteStatus) {
throw new Error('Remote Browser Bridge daemon at ' + daemonHost + ' is not reachable. Ensure BROWSER_BRIDGE_ENABLED=true on the chrome container.');
}
return;
}
await ensureBrowserBridgeReady({
timeoutSeconds: timeoutSeconds ?? Math.ceil(DAEMON_SPAWN_TIMEOUT / 1000),
contextId,
preferredContextId,
});
}`,
'browser/bridge.js: route remote daemon health checks'
);
// ── 4. execution.js: honour explicit CDP endpoint for web adapters ──────────
// OpenCLI 1.8.7 only reads OPENCLI_CDP_ENDPOINT inside the Electron branch.
// A normal web adapter therefore silently falls back to Browser Bridge and can
// escape Admin's selected profile. Managed acquisition must fail closed at the
// requested endpoint instead.
patch(
path.join(pkgDir, 'dist', 'src', 'execution.js'),
` let cdpEndpoint;
if (electron) {
// Electron apps: respect manual endpoint override, then try auto-detect
const manualEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
if (manualEndpoint) {
const port = Number(new URL(manualEndpoint).port);
if (!await probeCDP(port)) {
throw new CommandExecutionError(\`CDP not reachable at \${manualEndpoint}\`, 'Check that the app is running with --remote-debugging-port and the endpoint is correct.');
}
cdpEndpoint = manualEndpoint;
}
else {
cdpEndpoint = await resolveElectronEndpoint(cmd.site);
}
}`,
` // OPENCLI_ADMIN_MANAGED_CDP_ROUTING_V1
const manualEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
let cdpEndpoint;
if (manualEndpoint) {
const port = Number(new URL(manualEndpoint).port);
if (!await probeCDP(port)) {
throw new CommandExecutionError(\`CDP not reachable at \${manualEndpoint}\`, 'Check that the managed browser profile is running and the endpoint is correct.');
}
cdpEndpoint = manualEndpoint;
}
else if (electron) {
cdpEndpoint = await resolveElectronEndpoint(cmd.site);
}`,
'execution.js: managed web-adapter CDP routing'
);
// Upgrade the first managed-routing patch so remote/container endpoints are
// probed at their real host. OpenCLI's probeCDP(port) is intentionally local
// to Electron discovery and always calls 127.0.0.1.
patch(
path.join(pkgDir, 'dist', 'src', 'execution.js'),
` // OPENCLI_ADMIN_MANAGED_CDP_ROUTING_V1
const manualEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
let cdpEndpoint;
if (manualEndpoint) {
const port = Number(new URL(manualEndpoint).port);
if (!await probeCDP(port)) {
throw new CommandExecutionError(\`CDP not reachable at \${manualEndpoint}\`, 'Check that the managed browser profile is running and the endpoint is correct.');
}
cdpEndpoint = manualEndpoint;
}
else if (electron) {
cdpEndpoint = await resolveElectronEndpoint(cmd.site);
}`,
`// OPENCLI_ADMIN_MANAGED_CDP_ROUTING_V2
const manualEndpoint = process.env.OPENCLI_CDP_ENDPOINT?.trim();
let cdpEndpoint;
if (manualEndpoint) {
let reachable = true;
try {
const probeUrl = new URL(manualEndpoint);
if (probeUrl.protocol === 'http:' || probeUrl.protocol === 'https:') {
probeUrl.pathname = probeUrl.pathname.replace(/\\/$/, '') + '/json';
const response = await fetch(probeUrl, { signal: AbortSignal.timeout(3000) });
reachable = response.ok;
}
}
catch {
reachable = false;
}
if (!reachable) {
throw new CommandExecutionError(\`CDP not reachable at \${manualEndpoint}\`, 'Check that the managed browser profile is running and the endpoint is correct.');
}
cdpEndpoint = manualEndpoint;
}
else if (electron) {
cdpEndpoint = await resolveElectronEndpoint(cmd.site);
}`,
'execution.js: probe the actual managed CDP host'
);
patch(
path.join(pkgDir, 'dist', 'src', 'execution.js'),
' const BrowserFactory = getBrowserFactory(cmd.site);',
' // OPENCLI_ADMIN_FACTORY_SELECTION_V1\n const BrowserFactory = getBrowserFactory(cmd.site, { cdpEndpoint });',
'execution.js: select CDP factory for managed endpoint'
);
patch(
path.join(pkgDir, 'dist', 'src', 'runtime.js'),
`export function getBrowserFactory(site) {
if (site && isElectronApp(site))
return CDPBridge;
return BrowserBridge;
}`,
`// OPENCLI_ADMIN_RUNTIME_FACTORY_V1
export function getBrowserFactory(site, opts = {}) {
if (opts.cdpEndpoint || (site && isElectronApp(site)))
return CDPBridge;
return BrowserBridge;
}`,
'runtime.js: explicit endpoint selects CDPBridge'
);
const executionSource = fs.readFileSync(path.join(pkgDir, 'dist', 'src', 'execution.js'), 'utf8');
const runtimeSource = fs.readFileSync(path.join(pkgDir, 'dist', 'src', 'runtime.js'), 'utf8');
const bridgeSource = fs.readFileSync(path.join(pkgDir, 'dist', 'src', 'browser', 'bridge.js'), 'utf8');
if (!executionSource.includes('OPENCLI_ADMIN_MANAGED_CDP_ROUTING_V2') ||
!executionSource.includes('OPENCLI_ADMIN_FACTORY_SELECTION_V1') ||
!runtimeSource.includes('OPENCLI_ADMIN_RUNTIME_FACTORY_V1') ||
!bridgeSource.includes('OPENCLI_ADMIN_REMOTE_DAEMON_ROUTE_V2')) {
throw new Error('Managed CDP routing patch verification failed');
}
console.log('Done.');