dsh-builtin-browser 0.1.5 → 0.1.7
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 +9 -1
- package/lib/browser/runtime.d.ts +24 -8
- package/lib/browser/runtime.js +35 -7
- package/lib/browser/types.d.ts +122 -7
- package/lib/browser/types.js +1 -1
- package/lib/browser-electron/host-main.js +85 -0
- package/lib/browser-electron/provider.d.ts +49 -8
- package/lib/browser-electron/provider.js +406 -37
- package/lib/browser-electron/remote-host.js +35 -2
- package/lib/tool-browser/index.d.ts +13 -7
- package/lib/tool-browser/index.js +509 -34
- package/package.json +4 -2
- package/src/browser/runtime.ts +260 -0
- package/src/browser/types.ts +338 -0
- package/src/browser-electron/entry.ts +58 -0
- package/src/browser-electron/host-main.ts +229 -0
- package/src/browser-electron/provider.ts +922 -0
- package/src/browser-electron/remote-host.ts +421 -0
- package/src/index.ts +33 -0
- package/src/tool-browser/index.ts +862 -0
- package/src/types/electron-shim.d.ts +82 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-hosted Electron browser host (parent side): an
|
|
3
|
+
* {@link ElectronBrowserViewHost} implementation that spawns the plugin's own
|
|
4
|
+
* Electron child process (host-main.js) and drives it over line-delimited
|
|
5
|
+
* JSON-RPC on stdio. This is what makes the plugin work on surfaces without a
|
|
6
|
+
* desktop shell's electronViewHost (plain dsh web): installing the plugin is
|
|
7
|
+
* enough — the browser window appears on first use.
|
|
8
|
+
*
|
|
9
|
+
* Protocol (one JSON object per line, both directions):
|
|
10
|
+
* -> { id, op: 'createView' } | { id, op: 'destroyView', viewId } |
|
|
11
|
+
* { id, op: 'showView', viewId } | { id, op: 'command', viewId, method, params }
|
|
12
|
+
* <- { id, ok: true, result? } | { id, ok: false, err }
|
|
13
|
+
*
|
|
14
|
+
* The child is Electron's main process; host-main.js owns the BrowserWindow,
|
|
15
|
+
* WebContentsViews, and webContents.debugger (CDP).
|
|
16
|
+
* @module dsh-browser/browser-electron/remote-host
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { spawn, type ChildProcessByStdio } from 'node:child_process'
|
|
20
|
+
import { createRequire } from 'node:module'
|
|
21
|
+
import { existsSync, readdirSync, writeFileSync } from 'node:fs'
|
|
22
|
+
import { join } from 'node:path'
|
|
23
|
+
import { createServer, type Server, type Socket } from 'node:net'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
import type { ElectronBrowserViewHost, ElectronViewHandle } from './provider.ts'
|
|
26
|
+
import type { ExportedCookie } from '../browser/types.ts'
|
|
27
|
+
|
|
28
|
+
/** How long to wait for the child to signal readiness before failing. */
|
|
29
|
+
const READY_TIMEOUT_MS = 20_000
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Locate the Electron binary, trying, in order:
|
|
33
|
+
* 1. require('electron') from this module (optional peer dependency),
|
|
34
|
+
* 2. require.resolve('electron') against DSH install anchors (global dsh
|
|
35
|
+
* package, npm global root) — desktop-shell environments satisfy this,
|
|
36
|
+
* 3. a pnpm virtual store under common workspace layouts,
|
|
37
|
+
* 4. ELECTRON_PATH (explicit override).
|
|
38
|
+
* @returns the path to the Electron executable.
|
|
39
|
+
*/
|
|
40
|
+
function resolveElectronPath(): string {
|
|
41
|
+
const require = createRequire(import.meta.url)
|
|
42
|
+
// 1. Optional peer dependency.
|
|
43
|
+
try {
|
|
44
|
+
const resolved: unknown = require('electron')
|
|
45
|
+
if (typeof resolved === 'string' && resolved.length > 0) return resolved
|
|
46
|
+
} catch {
|
|
47
|
+
// continue probing
|
|
48
|
+
}
|
|
49
|
+
// 2. DSH install anchors: the global dsh package and the npm global root
|
|
50
|
+
// both carry electron junctions under pnpm layouts.
|
|
51
|
+
const anchors: string[] = []
|
|
52
|
+
const globalPrefix = process.env.npm_config_prefix ?? process.env.PREFIX
|
|
53
|
+
if (globalPrefix !== undefined) {
|
|
54
|
+
anchors.push(join(globalPrefix, 'node_modules'))
|
|
55
|
+
anchors.push(join(globalPrefix, 'node_modules', '@deepseek-ai', 'dsh', 'node_modules'))
|
|
56
|
+
}
|
|
57
|
+
if (process.env.DSH_HOME !== undefined) {
|
|
58
|
+
anchors.push(join(process.env.DSH_HOME, 'profiles', 'node_modules'))
|
|
59
|
+
}
|
|
60
|
+
for (const anchor of anchors) {
|
|
61
|
+
try {
|
|
62
|
+
const resolved: unknown = require.resolve('electron', { paths: [anchor] })
|
|
63
|
+
if (typeof resolved === 'string' && resolved.length > 0) {
|
|
64
|
+
// electron's index.js is not the binary; find the dist executable
|
|
65
|
+
// next to the package root.
|
|
66
|
+
const exe = electronExeBeside(resolved)
|
|
67
|
+
if (exe !== undefined) return exe
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// continue probing
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// 3. pnpm virtual store, walking up from several candidate roots: this
|
|
74
|
+
// module (its real path may differ from where the profile resolves it when
|
|
75
|
+
// installed via a junction/link), the process CWD, and the Node executable
|
|
76
|
+
// dir (DSH CLI typically runs from the workspace root).
|
|
77
|
+
const roots = new Set<string>([
|
|
78
|
+
fileURLToPath(new URL('.', import.meta.url)),
|
|
79
|
+
process.cwd(),
|
|
80
|
+
dirname(process.execPath),
|
|
81
|
+
])
|
|
82
|
+
for (const root of roots) {
|
|
83
|
+
let dir = root
|
|
84
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
85
|
+
const store = join(dir, 'node_modules', '.pnpm')
|
|
86
|
+
if (existsSync(store)) {
|
|
87
|
+
const exe = findElectronExe(store)
|
|
88
|
+
if (exe !== undefined) return exe
|
|
89
|
+
}
|
|
90
|
+
const parent = join(dir, '..')
|
|
91
|
+
if (parent === dir) break
|
|
92
|
+
dir = parent
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// 4. Explicit override.
|
|
96
|
+
const override = process.env.ELECTRON_PATH
|
|
97
|
+
if (typeof override === 'string' && override.length > 0 && existsSync(override)) return override
|
|
98
|
+
throw new Error(
|
|
99
|
+
'dsh-builtin-browser: cannot locate the Electron binary. Install electron in the host profile ' +
|
|
100
|
+
'(e.g. `dsh plugin --profile web add electron`) or set ELECTRON_PATH to the electron executable.',
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** From an electron package entry file, find the dist executable beside it. */
|
|
105
|
+
function electronExeBeside(entry: string): string | undefined {
|
|
106
|
+
const candidates = [
|
|
107
|
+
join(dirname(entry), 'dist', 'electron.exe'),
|
|
108
|
+
join(dirname(entry), 'dist', 'electron'),
|
|
109
|
+
join(dirname(entry), '..', 'dist', 'electron.exe'),
|
|
110
|
+
join(dirname(entry), '..', 'dist', 'electron'),
|
|
111
|
+
]
|
|
112
|
+
for (const candidate of candidates) {
|
|
113
|
+
if (existsSync(candidate)) return candidate
|
|
114
|
+
}
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** dirname without importing node:path's dirname separately. */
|
|
119
|
+
function dirname(p: string): string {
|
|
120
|
+
const i = p.lastIndexOf('/')
|
|
121
|
+
const j = p.lastIndexOf('\\')
|
|
122
|
+
const k = Math.max(i, j)
|
|
123
|
+
return k < 0 ? p : p.slice(0, k)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Search a pnpm virtual store for an Electron dist binary. */
|
|
127
|
+
function findElectronExe(store: string): string | undefined {
|
|
128
|
+
const entries = existsSync(store) ? readdirSync(store) : []
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
if (!entry.startsWith('electron@')) continue
|
|
131
|
+
const candidates = [
|
|
132
|
+
join(store, entry, 'node_modules', 'electron', 'dist', 'electron.exe'),
|
|
133
|
+
join(store, entry, 'node_modules', 'electron', 'dist', 'electron'),
|
|
134
|
+
]
|
|
135
|
+
for (const candidate of candidates) {
|
|
136
|
+
if (existsSync(candidate)) return candidate
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return undefined
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** One RPC round-trip with the child. */
|
|
143
|
+
interface Pending {
|
|
144
|
+
resolve(result: unknown): void
|
|
145
|
+
reject(err: Error): void
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Line-delimited JSON-RPC client over a local TCP socket. Electron's main
|
|
150
|
+
* process on Windows does not receive piped stdin, so the parent listens on a
|
|
151
|
+
* loopback port and passes it to the child via `--rpc-port`; the child
|
|
152
|
+
* connects back and speaks the same one-JSON-per-line protocol.
|
|
153
|
+
*/
|
|
154
|
+
class ElectronChildClient {
|
|
155
|
+
private readonly child: ChildProcessByStdio<null, import('node:stream').Readable, import('node:stream').Readable>
|
|
156
|
+
private readonly pending = new Map<number, Pending>()
|
|
157
|
+
private nextId = 1
|
|
158
|
+
private buffer = ''
|
|
159
|
+
private socket: import('node:net').Socket | undefined
|
|
160
|
+
private connected = false
|
|
161
|
+
private outbox: string[] = []
|
|
162
|
+
|
|
163
|
+
constructor(
|
|
164
|
+
private readonly hostMainPath: string,
|
|
165
|
+
private readonly port: number,
|
|
166
|
+
) {
|
|
167
|
+
const electron = resolveElectronPath()
|
|
168
|
+
// ELECTRON_RUN_AS_NODE (even an empty string) makes Electron run as plain
|
|
169
|
+
// Node, breaking require('electron'); NODE_OPTIONS can inject flags that
|
|
170
|
+
// break the child. Rebuild the env without either.
|
|
171
|
+
const env: Record<string, string | undefined> = { ...process.env }
|
|
172
|
+
delete env.ELECTRON_RUN_AS_NODE
|
|
173
|
+
delete env.NODE_OPTIONS
|
|
174
|
+
this.child = spawn(electron, [hostMainPath, '--rpc-port', String(port)], {
|
|
175
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
176
|
+
windowsHide: false,
|
|
177
|
+
env,
|
|
178
|
+
})
|
|
179
|
+
this.child.stderr.setEncoding('utf8')
|
|
180
|
+
this.child.stderr.on('data', chunk => {
|
|
181
|
+
// Diagnostics only; never parse stderr as protocol.
|
|
182
|
+
process.stderr.write(`[dsh-browser host] ${String(chunk)}`)
|
|
183
|
+
})
|
|
184
|
+
this.child.on('exit', (code, signal) => {
|
|
185
|
+
const err = new Error(`dsh-builtin-browser: browser host exited (code=${String(code)} signal=${String(signal)})`)
|
|
186
|
+
for (const pending of this.pending.values()) pending.reject(err)
|
|
187
|
+
this.pending.clear()
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Accept the child's connection (called by the server). */
|
|
192
|
+
attach(socket: import('node:net').Socket): void {
|
|
193
|
+
this.socket = socket
|
|
194
|
+
this.connected = true
|
|
195
|
+
socket.setEncoding('utf8')
|
|
196
|
+
socket.on('data', chunk => this.onData(chunk))
|
|
197
|
+
socket.on('close', () => {
|
|
198
|
+
this.connected = false
|
|
199
|
+
const err = new Error('dsh-builtin-browser: browser host connection closed')
|
|
200
|
+
for (const pending of this.pending.values()) pending.reject(err)
|
|
201
|
+
this.pending.clear()
|
|
202
|
+
})
|
|
203
|
+
// Flush anything queued while disconnected.
|
|
204
|
+
if (this.outbox.length > 0) {
|
|
205
|
+
for (const line of this.outbox) socket.write(line + '\n')
|
|
206
|
+
this.outbox = []
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private onData(chunk: string | Buffer): void {
|
|
211
|
+
this.buffer += typeof chunk === 'string' ? chunk : chunk.toString('utf8')
|
|
212
|
+
let nl: number
|
|
213
|
+
while ((nl = this.buffer.indexOf('\n')) >= 0) {
|
|
214
|
+
const line = this.buffer.slice(0, nl).trim()
|
|
215
|
+
this.buffer = this.buffer.slice(nl + 1)
|
|
216
|
+
if (line === '') continue
|
|
217
|
+
let msg: { id?: number; ok?: boolean; result?: unknown; err?: string }
|
|
218
|
+
try {
|
|
219
|
+
msg = JSON.parse(line) as typeof msg
|
|
220
|
+
} catch {
|
|
221
|
+
// Non-protocol line; ignore.
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
if (typeof msg.id !== 'number') continue
|
|
225
|
+
const pending = this.pending.get(msg.id)
|
|
226
|
+
if (pending === undefined) continue
|
|
227
|
+
this.pending.delete(msg.id)
|
|
228
|
+
if (msg.ok === true) pending.resolve(msg.result)
|
|
229
|
+
else pending.reject(new Error(msg.err ?? 'browser host command failed'))
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Send one command and await the reply. */
|
|
234
|
+
call<T = unknown>(op: string, payload: Record<string, unknown> = {}): Promise<T> {
|
|
235
|
+
const id = this.nextId++
|
|
236
|
+
const line = JSON.stringify({ id, op, ...payload })
|
|
237
|
+
return new Promise<T>((resolve, reject) => {
|
|
238
|
+
this.pending.set(id, { resolve: resolve as (v: unknown) => void, reject })
|
|
239
|
+
if (this.connected && this.socket !== undefined) {
|
|
240
|
+
this.socket.write(line + '\n')
|
|
241
|
+
} else {
|
|
242
|
+
// Not connected yet: queue; attach() flushes on the child's arrival.
|
|
243
|
+
this.outbox.push(line)
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Terminate the child. */
|
|
249
|
+
kill(): void {
|
|
250
|
+
try { this.socket?.destroy() } catch { /* already closed */ }
|
|
251
|
+
this.child.kill()
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** One view in the child: its id, used for every command. */
|
|
256
|
+
class RemoteView implements ElectronViewHandle {
|
|
257
|
+
constructor(readonly id: string, private readonly client: ElectronChildClient) {}
|
|
258
|
+
|
|
259
|
+
sendCommand(method: string, params?: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
260
|
+
return this.client.call<Record<string, unknown>>('command', {
|
|
261
|
+
viewId: this.id,
|
|
262
|
+
method,
|
|
263
|
+
params: params ?? {},
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Ask the child to download a URL to a local file (keeps cookies/login). */
|
|
268
|
+
async download(url: string, savePath: string): Promise<void> {
|
|
269
|
+
const result = await this.client.call<{ base64: string }>('download', { viewId: this.id, url, savePath })
|
|
270
|
+
writeFileSync(savePath, Buffer.from(result.base64, 'base64'))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Export the session's cookies (login state). */
|
|
274
|
+
flushAuth(): Promise<ExportedCookie[]> {
|
|
275
|
+
return this.client.call<{ cookies: ExportedCookie[] }>('flushAuth', { viewId: this.id }).then(r => r.cookies)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Import cookies into the session (restore login state). */
|
|
279
|
+
restoreAuth(cookies: ExportedCookie[]): Promise<number> {
|
|
280
|
+
return this.client.call<{ restored: number }>('restoreAuth', { viewId: this.id, cookies }).then(r => r.restored)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Self-hosted view host: spawns the plugin's Electron child on first use and
|
|
286
|
+
* keeps it alive until dispose(). Fallback when no desktop shell provides
|
|
287
|
+
* ctx.electronViewHost.
|
|
288
|
+
*/
|
|
289
|
+
export class RemoteElectronViewHost implements ElectronBrowserViewHost {
|
|
290
|
+
private client: ElectronChildClient | undefined
|
|
291
|
+
private server: Server | undefined
|
|
292
|
+
private pendingSocket: Socket | undefined
|
|
293
|
+
private readonly views = new Map<string, ElectronViewHandle>()
|
|
294
|
+
private readyPromise: Promise<void> | undefined
|
|
295
|
+
|
|
296
|
+
constructor(private readonly hostMainPath: string) {}
|
|
297
|
+
|
|
298
|
+
/** Ensure the child is up and ready (lazy on first use). */
|
|
299
|
+
private ready(): Promise<void> {
|
|
300
|
+
if (this.readyPromise !== undefined) return this.readyPromise
|
|
301
|
+
this.readyPromise = this.start()
|
|
302
|
+
return this.readyPromise
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private async start(): Promise<void> {
|
|
306
|
+
// Listen on an ephemeral loopback port; the child connects back.
|
|
307
|
+
const server = createServer(socket => {
|
|
308
|
+
if (this.client !== undefined) this.client.attach(socket)
|
|
309
|
+
else this.pendingSocket = socket
|
|
310
|
+
})
|
|
311
|
+
await new Promise<void>((resolve, reject) => {
|
|
312
|
+
server.once('error', reject)
|
|
313
|
+
server.listen(0, '127.0.0.1', () => resolve())
|
|
314
|
+
})
|
|
315
|
+
const address = server.address()
|
|
316
|
+
const port = typeof address === 'object' && address !== null ? address.port : 0
|
|
317
|
+
this.server = server
|
|
318
|
+
this.client = new ElectronChildClient(this.hostMainPath, port)
|
|
319
|
+
if (this.pendingSocket !== undefined) {
|
|
320
|
+
this.client.attach(this.pendingSocket)
|
|
321
|
+
this.pendingSocket = undefined
|
|
322
|
+
}
|
|
323
|
+
// Wait for the child's connection + readiness ping.
|
|
324
|
+
await withTimeout(this.client.call('ping'), READY_TIMEOUT_MS, 'browser host did not become ready')
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
createView(): ElectronViewHandle {
|
|
328
|
+
// The seam is synchronous; the provider uses the handle immediately, so
|
|
329
|
+
// commands are deferred until the child is up and the view materialized.
|
|
330
|
+
const id = `view:${Math.random().toString(36).slice(2, 10)}`
|
|
331
|
+
const view = new DeferredRemoteView(id, () => this.ensureView(id))
|
|
332
|
+
this.views.set(id, view)
|
|
333
|
+
return view
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private async ensureView(id: string): Promise<RemoteView> {
|
|
337
|
+
await this.ready()
|
|
338
|
+
const client = this.client
|
|
339
|
+
if (client === undefined) throw new Error('browser host unavailable')
|
|
340
|
+
await client.call('createView', { viewId: id })
|
|
341
|
+
const view = new RemoteView(id, client)
|
|
342
|
+
this.views.set(id, view)
|
|
343
|
+
return view
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
showView(handle: ElectronViewHandle): void {
|
|
347
|
+
void this.ready().then(() => this.client?.call('showView', { viewId: handle.id }))
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
destroyView(handle: ElectronViewHandle): void {
|
|
351
|
+
const view = this.views.get(handle.id)
|
|
352
|
+
if (view === undefined) return
|
|
353
|
+
this.views.delete(handle.id)
|
|
354
|
+
void this.ready()
|
|
355
|
+
.then(() => this.client?.call('destroyView', { viewId: handle.id }))
|
|
356
|
+
.catch(() => { /* child already gone */ })
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Shut the child and the RPC server down. */
|
|
360
|
+
dispose(): void {
|
|
361
|
+
this.client?.kill()
|
|
362
|
+
this.client = undefined
|
|
363
|
+
this.server?.close()
|
|
364
|
+
this.server = undefined
|
|
365
|
+
this.readyPromise = undefined
|
|
366
|
+
this.views.clear()
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** A view handle that waits for child readiness before issuing commands. */
|
|
371
|
+
class DeferredRemoteView implements ElectronViewHandle {
|
|
372
|
+
private materialized: Promise<RemoteView> | undefined
|
|
373
|
+
|
|
374
|
+
constructor(
|
|
375
|
+
readonly id: string,
|
|
376
|
+
private readonly materialize: () => Promise<RemoteView>,
|
|
377
|
+
) {}
|
|
378
|
+
|
|
379
|
+
async sendCommand(method: string, params?: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
380
|
+
// Materialize once and cache: every sendCommand on the same handle must
|
|
381
|
+
// target the SAME child view. Re-materializing would re-run createView
|
|
382
|
+
// (duplicate window) and drop the established view.
|
|
383
|
+
this.materialized ??= this.materialize()
|
|
384
|
+
const view = await this.materialized
|
|
385
|
+
return view.sendCommand(method, params)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async download(url: string, savePath: string): Promise<void> {
|
|
389
|
+
this.materialized ??= this.materialize()
|
|
390
|
+
const view = await this.materialized
|
|
391
|
+
return view.download(url, savePath)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async flushAuth(): Promise<ExportedCookie[]> {
|
|
395
|
+
this.materialized ??= this.materialize()
|
|
396
|
+
const view = await this.materialized
|
|
397
|
+
return view.flushAuth()
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async restoreAuth(cookies: ExportedCookie[]): Promise<number> {
|
|
401
|
+
this.materialized ??= this.materialize()
|
|
402
|
+
const view = await this.materialized
|
|
403
|
+
return view.restoreAuth(cookies)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Reject a promise if it does not settle within the budget. */
|
|
408
|
+
function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
|
|
409
|
+
return new Promise<T>((resolve, reject) => {
|
|
410
|
+
const timer = setTimeout(() => reject(new Error(`${message} (${ms}ms)`)), ms)
|
|
411
|
+
promise.then(
|
|
412
|
+
value => { clearTimeout(timer); resolve(value) },
|
|
413
|
+
error => { clearTimeout(timer); reject(error) },
|
|
414
|
+
)
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Default host-main path relative to this module's build output. */
|
|
419
|
+
export function defaultHostMainPath(): string {
|
|
420
|
+
return fileURLToPath(new URL('./host-main.js', import.meta.url))
|
|
421
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-browser plugin entry: aggregates the shared-browser capability pieces.
|
|
3
|
+
* The cordis.patch.yml rows reference subpath exports:
|
|
4
|
+
* - `dsh-browser/browser` -> the ctx.browser seam (Service Definition)
|
|
5
|
+
* - `dsh-browser/browser-electron` -> the Electron CDP provider
|
|
6
|
+
* - `dsh-browser/tool-browser` -> the model-facing browser_* tools
|
|
7
|
+
* This root entry only re-exports for programmatic use; the loader rows are
|
|
8
|
+
* the composition surface.
|
|
9
|
+
* @module dsh-browser
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { BrowserError } from './browser/types.ts'
|
|
13
|
+
export type {
|
|
14
|
+
BrowserClickRequest,
|
|
15
|
+
BrowserContentFormat,
|
|
16
|
+
BrowserContentRequest,
|
|
17
|
+
BrowserContentResult,
|
|
18
|
+
BrowserExecuteRequest,
|
|
19
|
+
BrowserExecuteResult,
|
|
20
|
+
BrowserNavigateRequest,
|
|
21
|
+
BrowserOpenRequest,
|
|
22
|
+
BrowserProvider,
|
|
23
|
+
BrowserScreenshotRequest,
|
|
24
|
+
BrowserScreenshotResult,
|
|
25
|
+
BrowserSessionId,
|
|
26
|
+
BrowserSnapshotElement,
|
|
27
|
+
BrowserSnapshotResult,
|
|
28
|
+
BrowserTab,
|
|
29
|
+
BrowserTypeRequest,
|
|
30
|
+
} from './browser/types.ts'
|
|
31
|
+
export { BrowserRuntime } from './browser/runtime.ts'
|
|
32
|
+
export { ElectronBrowserProvider } from './browser-electron/provider.ts'
|
|
33
|
+
export type { ElectronBrowserViewHost, ElectronViewHandle } from './browser-electron/provider.ts'
|