opensteer 0.4.3 → 0.4.5
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/CHANGELOG.md +12 -0
- package/README.md +25 -0
- package/bin/opensteer.mjs +375 -106
- package/dist/cli/server.cjs +123 -48
- package/dist/cli/server.js +105 -30
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Breaking: CLI runtime routing now uses `--session`/`OPENSTEER_SESSION` instead
|
|
6
|
+
of `--name`/cwd/active-session fallback.
|
|
7
|
+
- Breaking: non-interactive CLI calls now require explicit runtime identity via
|
|
8
|
+
`--session`, `OPENSTEER_SESSION`, or `OPENSTEER_CLIENT_ID`.
|
|
9
|
+
- Added `OPENSTEER_CLIENT_ID` support for stable client-scoped default session
|
|
10
|
+
binding.
|
|
11
|
+
- CLI `--name` is now selector-cache namespace only and no longer controls
|
|
12
|
+
daemon/browser routing.
|
|
13
|
+
- Added per-session daemon startup locking + stale-lock recovery and ping-based
|
|
14
|
+
health checks to remove startup races across concurrent commands.
|
|
15
|
+
- Added strict in-daemon request serialization for session commands, while
|
|
16
|
+
keeping `ping` out of the queue for reliable liveness checks.
|
|
5
17
|
- Breaking: removed legacy `ai` config from `OpensteerConfig`; use top-level `model` instead.
|
|
6
18
|
- Breaking: `OPENSTEER_AI_MODEL` is no longer supported; use `OPENSTEER_MODEL`.
|
|
7
19
|
- Breaking: `OPENSTEER_RUNTIME` is no longer supported; use `OPENSTEER_MODE`.
|
package/README.md
CHANGED
|
@@ -20,6 +20,31 @@ npm install opensteer playwright
|
|
|
20
20
|
pnpm add opensteer playwright
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
## CLI Session Routing
|
|
24
|
+
|
|
25
|
+
OpenSteer CLI now separates runtime routing from selector caching:
|
|
26
|
+
|
|
27
|
+
- Runtime routing: `--session` or `OPENSTEER_SESSION`
|
|
28
|
+
- Selector cache namespace: `--name` or `OPENSTEER_NAME` (used on `open`)
|
|
29
|
+
|
|
30
|
+
If neither `--session` nor `OPENSTEER_SESSION` is set:
|
|
31
|
+
|
|
32
|
+
- In an interactive terminal, OpenSteer creates/reuses a terminal-scoped default session.
|
|
33
|
+
- In non-interactive environments (agents/CI), it fails fast unless you set
|
|
34
|
+
`OPENSTEER_SESSION` or `OPENSTEER_CLIENT_ID`.
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
export OPENSTEER_SESSION=agent-a
|
|
40
|
+
opensteer open https://example.com --name product-scraper
|
|
41
|
+
opensteer snapshot
|
|
42
|
+
opensteer click 3
|
|
43
|
+
opensteer status
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`opensteer status` reports `resolvedSession`, `sessionSource`, `resolvedName`, and `nameSource`.
|
|
47
|
+
|
|
23
48
|
## Quickstart
|
|
24
49
|
|
|
25
50
|
```ts
|
package/bin/opensteer.mjs
CHANGED
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
import { createHash } from 'crypto'
|
|
4
4
|
import { spawn } from 'child_process'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
closeSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
openSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
unlinkSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from 'fs'
|
|
6
14
|
import { connect } from 'net'
|
|
7
15
|
import { tmpdir } from 'os'
|
|
8
|
-
import {
|
|
16
|
+
import { dirname, join } from 'path'
|
|
9
17
|
import { fileURLToPath } from 'url'
|
|
10
18
|
|
|
11
19
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
@@ -14,10 +22,16 @@ const SERVER_SCRIPT = join(__dirname, '..', 'dist', 'cli', 'server.js')
|
|
|
14
22
|
const CONNECT_TIMEOUT = 15000
|
|
15
23
|
const POLL_INTERVAL = 100
|
|
16
24
|
const RESPONSE_TIMEOUT = 120000
|
|
25
|
+
const HEALTH_TIMEOUT = 1500
|
|
17
26
|
const RUNTIME_PREFIX = 'opensteer-'
|
|
18
27
|
const SOCKET_SUFFIX = '.sock'
|
|
19
28
|
const PID_SUFFIX = '.pid'
|
|
29
|
+
const LOCK_SUFFIX = '.lock'
|
|
30
|
+
const CLIENT_BINDING_PREFIX = `${RUNTIME_PREFIX}client-`
|
|
31
|
+
const CLIENT_BINDING_SUFFIX = '.session'
|
|
20
32
|
const CLOSE_ALL_REQUEST = { id: 1, command: 'close', args: {} }
|
|
33
|
+
const PING_REQUEST = { id: 1, command: 'ping', args: {} }
|
|
34
|
+
const SESSION_ID_PATTERN = /^[a-zA-Z0-9_-]+$/
|
|
21
35
|
|
|
22
36
|
function getVersion() {
|
|
23
37
|
try {
|
|
@@ -84,59 +98,162 @@ function sanitizeNamespace(value) {
|
|
|
84
98
|
return bounded || 'default'
|
|
85
99
|
}
|
|
86
100
|
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
return join(tmpdir(), `${RUNTIME_PREFIX}active-${hash}`)
|
|
101
|
+
function isValidSessionId(value) {
|
|
102
|
+
return SESSION_ID_PATTERN.test(value)
|
|
90
103
|
}
|
|
91
104
|
|
|
92
|
-
function
|
|
105
|
+
function validateSessionId(rawValue, label) {
|
|
106
|
+
const value = String(rawValue ?? '').trim()
|
|
107
|
+
if (!value) {
|
|
108
|
+
throw new Error(`${label} cannot be empty.`)
|
|
109
|
+
}
|
|
110
|
+
if (!isValidSessionId(value)) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`${label} "${value}" is invalid. Use only letters, numbers, underscores, and hyphens.`
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
return value
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveName(flags, session) {
|
|
119
|
+
if (flags.name !== undefined) {
|
|
120
|
+
if (flags.name === true) {
|
|
121
|
+
throw new Error('--name requires a namespace value.')
|
|
122
|
+
}
|
|
123
|
+
const raw = String(flags.name).trim()
|
|
124
|
+
if (raw.length > 0) {
|
|
125
|
+
return { name: sanitizeNamespace(raw), source: 'flag' }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (
|
|
130
|
+
typeof process.env.OPENSTEER_NAME === 'string' &&
|
|
131
|
+
process.env.OPENSTEER_NAME.trim().length > 0
|
|
132
|
+
) {
|
|
133
|
+
return {
|
|
134
|
+
name: sanitizeNamespace(process.env.OPENSTEER_NAME),
|
|
135
|
+
source: 'env',
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { name: sanitizeNamespace(session), source: 'session' }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function hashKey(value) {
|
|
143
|
+
return createHash('sha256').update(value).digest('hex')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getClientBindingPath(clientKey) {
|
|
147
|
+
return join(
|
|
148
|
+
tmpdir(),
|
|
149
|
+
`${CLIENT_BINDING_PREFIX}${hashKey(clientKey).slice(0, 24)}${CLIENT_BINDING_SUFFIX}`
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readClientBinding(clientKey) {
|
|
154
|
+
const bindingPath = getClientBindingPath(clientKey)
|
|
155
|
+
if (!existsSync(bindingPath)) {
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
|
|
93
159
|
try {
|
|
94
|
-
const
|
|
95
|
-
if (!
|
|
96
|
-
|
|
97
|
-
|
|
160
|
+
const rawSession = readFileSync(bindingPath, 'utf-8').trim()
|
|
161
|
+
if (!rawSession) {
|
|
162
|
+
unlinkSync(bindingPath)
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
if (!isValidSessionId(rawSession)) {
|
|
166
|
+
unlinkSync(bindingPath)
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
return rawSession
|
|
98
170
|
} catch {
|
|
99
171
|
return null
|
|
100
172
|
}
|
|
101
173
|
}
|
|
102
174
|
|
|
103
|
-
function
|
|
175
|
+
function writeClientBinding(clientKey, session) {
|
|
104
176
|
try {
|
|
105
|
-
writeFileSync(
|
|
177
|
+
writeFileSync(getClientBindingPath(clientKey), session)
|
|
106
178
|
} catch { /* best-effort */ }
|
|
107
179
|
}
|
|
108
180
|
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
181
|
+
function createDefaultSessionId(prefix, clientKey) {
|
|
182
|
+
return `${prefix}-${hashKey(clientKey).slice(0, 12)}`
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isInteractiveTerminal() {
|
|
186
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function resolveSession(flags) {
|
|
190
|
+
if (flags.session !== undefined) {
|
|
191
|
+
if (flags.session === true) {
|
|
192
|
+
throw new Error('--session requires a session id value.')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
session: validateSessionId(flags.session, 'Session id'),
|
|
197
|
+
source: 'flag',
|
|
198
|
+
}
|
|
112
199
|
}
|
|
113
200
|
|
|
114
201
|
if (
|
|
115
|
-
typeof process.env.
|
|
116
|
-
process.env.
|
|
202
|
+
typeof process.env.OPENSTEER_SESSION === 'string' &&
|
|
203
|
+
process.env.OPENSTEER_SESSION.trim().length > 0
|
|
117
204
|
) {
|
|
118
|
-
return {
|
|
205
|
+
return {
|
|
206
|
+
session: validateSessionId(
|
|
207
|
+
process.env.OPENSTEER_SESSION,
|
|
208
|
+
'OPENSTEER_SESSION'
|
|
209
|
+
),
|
|
210
|
+
source: 'env',
|
|
211
|
+
}
|
|
119
212
|
}
|
|
120
213
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
214
|
+
if (
|
|
215
|
+
typeof process.env.OPENSTEER_CLIENT_ID === 'string' &&
|
|
216
|
+
process.env.OPENSTEER_CLIENT_ID.trim().length > 0
|
|
217
|
+
) {
|
|
218
|
+
const clientId = process.env.OPENSTEER_CLIENT_ID.trim()
|
|
219
|
+
const clientKey = `client:${process.cwd()}:${clientId}`
|
|
220
|
+
const bound = readClientBinding(clientKey)
|
|
221
|
+
if (bound) {
|
|
222
|
+
return { session: bound, source: 'client_binding' }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const created = createDefaultSessionId('client', clientKey)
|
|
226
|
+
writeClientBinding(clientKey, created)
|
|
227
|
+
return { session: created, source: 'client_binding' }
|
|
124
228
|
}
|
|
125
229
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
230
|
+
if (isInteractiveTerminal()) {
|
|
231
|
+
const ttyKey = `tty:${process.cwd()}:${process.ppid}`
|
|
232
|
+
const bound = readClientBinding(ttyKey)
|
|
233
|
+
if (bound) {
|
|
234
|
+
return { session: bound, source: 'tty_default' }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const created = createDefaultSessionId('tty', ttyKey)
|
|
238
|
+
writeClientBinding(ttyKey, created)
|
|
239
|
+
return { session: created, source: 'tty_default' }
|
|
129
240
|
}
|
|
130
241
|
|
|
131
|
-
|
|
242
|
+
throw new Error(
|
|
243
|
+
'No session resolved for this non-interactive command. Set OPENSTEER_SESSION or OPENSTEER_CLIENT_ID, or pass --session <id>.'
|
|
244
|
+
)
|
|
132
245
|
}
|
|
133
246
|
|
|
134
|
-
function getSocketPath(
|
|
135
|
-
return join(tmpdir(), `${RUNTIME_PREFIX}${
|
|
247
|
+
function getSocketPath(session) {
|
|
248
|
+
return join(tmpdir(), `${RUNTIME_PREFIX}${session}${SOCKET_SUFFIX}`)
|
|
136
249
|
}
|
|
137
250
|
|
|
138
|
-
function getPidPath(
|
|
139
|
-
return join(tmpdir(), `${RUNTIME_PREFIX}${
|
|
251
|
+
function getPidPath(session) {
|
|
252
|
+
return join(tmpdir(), `${RUNTIME_PREFIX}${session}${PID_SUFFIX}`)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function getLockPath(session) {
|
|
256
|
+
return join(tmpdir(), `${RUNTIME_PREFIX}${session}${LOCK_SUFFIX}`)
|
|
140
257
|
}
|
|
141
258
|
|
|
142
259
|
function buildRequest(command, flags, positional) {
|
|
@@ -270,66 +387,36 @@ function isPidAlive(pid) {
|
|
|
270
387
|
}
|
|
271
388
|
}
|
|
272
389
|
|
|
273
|
-
function cleanStaleFiles(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
} catch { }
|
|
277
|
-
try {
|
|
278
|
-
unlinkSync(getPidPath(namespace))
|
|
279
|
-
} catch { }
|
|
280
|
-
}
|
|
390
|
+
function cleanStaleFiles(session, options = {}) {
|
|
391
|
+
const removeSocket = options.removeSocket !== false
|
|
392
|
+
const removePid = options.removePid !== false
|
|
281
393
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
cleanStaleFiles(namespace)
|
|
287
|
-
return false
|
|
394
|
+
if (removeSocket) {
|
|
395
|
+
try {
|
|
396
|
+
unlinkSync(getSocketPath(session))
|
|
397
|
+
} catch { }
|
|
288
398
|
}
|
|
289
399
|
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
|
|
400
|
+
if (removePid) {
|
|
401
|
+
try {
|
|
402
|
+
unlinkSync(getPidPath(session))
|
|
403
|
+
} catch { }
|
|
293
404
|
}
|
|
294
|
-
|
|
295
|
-
return existsSync(getSocketPath(namespace))
|
|
296
405
|
}
|
|
297
406
|
|
|
298
|
-
function startServer(
|
|
407
|
+
function startServer(session) {
|
|
299
408
|
const child = spawn('node', [SERVER_SCRIPT], {
|
|
300
409
|
detached: true,
|
|
301
410
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
302
411
|
env: {
|
|
303
412
|
...process.env,
|
|
304
|
-
|
|
413
|
+
OPENSTEER_SESSION: session,
|
|
305
414
|
},
|
|
306
415
|
})
|
|
307
416
|
child.unref()
|
|
308
417
|
}
|
|
309
418
|
|
|
310
|
-
function
|
|
311
|
-
return new Promise((resolve, reject) => {
|
|
312
|
-
const start = Date.now()
|
|
313
|
-
|
|
314
|
-
function poll() {
|
|
315
|
-
if (Date.now() - start > timeout) {
|
|
316
|
-
reject(new Error('Timed out waiting for server to start'))
|
|
317
|
-
return
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (existsSync(socketPath)) {
|
|
321
|
-
resolve()
|
|
322
|
-
return
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
setTimeout(poll, POLL_INTERVAL)
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
poll()
|
|
329
|
-
})
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function sendCommand(socketPath, request) {
|
|
419
|
+
function sendCommand(socketPath, request, timeoutMs = RESPONSE_TIMEOUT) {
|
|
333
420
|
return new Promise((resolve, reject) => {
|
|
334
421
|
const socket = connect(socketPath)
|
|
335
422
|
let buffer = ''
|
|
@@ -341,7 +428,7 @@ function sendCommand(socketPath, request) {
|
|
|
341
428
|
socket.destroy()
|
|
342
429
|
reject(new Error('Response timeout'))
|
|
343
430
|
}
|
|
344
|
-
},
|
|
431
|
+
}, timeoutMs)
|
|
345
432
|
|
|
346
433
|
socket.on('connect', () => {
|
|
347
434
|
socket.write(JSON.stringify(request) + '\n')
|
|
@@ -381,6 +468,169 @@ function sendCommand(socketPath, request) {
|
|
|
381
468
|
})
|
|
382
469
|
}
|
|
383
470
|
|
|
471
|
+
async function pingServer(session) {
|
|
472
|
+
const socketPath = getSocketPath(session)
|
|
473
|
+
if (!existsSync(socketPath)) return false
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
const response = await sendCommand(socketPath, PING_REQUEST, HEALTH_TIMEOUT)
|
|
477
|
+
return Boolean(response?.ok && response?.result?.pong)
|
|
478
|
+
} catch {
|
|
479
|
+
return false
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function isServerHealthy(session) {
|
|
484
|
+
const pid = readPid(getPidPath(session))
|
|
485
|
+
const pidAlive = pid ? isPidAlive(pid) : false
|
|
486
|
+
const socketExists = existsSync(getSocketPath(session))
|
|
487
|
+
|
|
488
|
+
if (pid && !pidAlive) {
|
|
489
|
+
cleanStaleFiles(session)
|
|
490
|
+
return false
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (!socketExists) {
|
|
494
|
+
return false
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const healthy = await pingServer(session)
|
|
498
|
+
if (healthy) {
|
|
499
|
+
return true
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// If there is no pid and ping fails, the socket file is stale.
|
|
503
|
+
if (!pid) {
|
|
504
|
+
cleanStaleFiles(session, { removeSocket: true, removePid: false })
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return false
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function acquireStartLock(session) {
|
|
511
|
+
const lockPath = getLockPath(session)
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
const fd = openSync(lockPath, 'wx')
|
|
515
|
+
writeFileSync(
|
|
516
|
+
fd,
|
|
517
|
+
JSON.stringify({
|
|
518
|
+
pid: process.pid,
|
|
519
|
+
createdAt: Date.now(),
|
|
520
|
+
})
|
|
521
|
+
)
|
|
522
|
+
closeSync(fd)
|
|
523
|
+
return true
|
|
524
|
+
} catch {
|
|
525
|
+
return false
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function releaseStartLock(session) {
|
|
530
|
+
try {
|
|
531
|
+
unlinkSync(getLockPath(session))
|
|
532
|
+
} catch { /* best-effort */ }
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function recoverStaleStartLock(session) {
|
|
536
|
+
const lockPath = getLockPath(session)
|
|
537
|
+
if (!existsSync(lockPath)) {
|
|
538
|
+
return false
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
const raw = readFileSync(lockPath, 'utf-8')
|
|
543
|
+
const parsed = JSON.parse(raw)
|
|
544
|
+
const pid =
|
|
545
|
+
parsed && Number.isInteger(parsed.pid) && parsed.pid > 0
|
|
546
|
+
? parsed.pid
|
|
547
|
+
: null
|
|
548
|
+
|
|
549
|
+
if (!pid || !isPidAlive(pid)) {
|
|
550
|
+
unlinkSync(lockPath)
|
|
551
|
+
return true
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return false
|
|
555
|
+
} catch {
|
|
556
|
+
try {
|
|
557
|
+
unlinkSync(lockPath)
|
|
558
|
+
return true
|
|
559
|
+
} catch {
|
|
560
|
+
return false
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function sleep(ms) {
|
|
566
|
+
await new Promise((resolve) => setTimeout(resolve, ms))
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function waitForServerReady(session, timeout) {
|
|
570
|
+
const start = Date.now()
|
|
571
|
+
|
|
572
|
+
while (Date.now() - start <= timeout) {
|
|
573
|
+
if (await isServerHealthy(session)) {
|
|
574
|
+
return
|
|
575
|
+
}
|
|
576
|
+
await sleep(POLL_INTERVAL)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
throw new Error(`Timed out waiting for server '${session}' to become healthy.`)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function ensureServer(session) {
|
|
583
|
+
if (await isServerHealthy(session)) {
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (!existsSync(SERVER_SCRIPT)) {
|
|
588
|
+
throw new Error(
|
|
589
|
+
`Server script not found: ${SERVER_SCRIPT}. Run the build script first.`
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const deadline = Date.now() + CONNECT_TIMEOUT
|
|
594
|
+
|
|
595
|
+
while (Date.now() < deadline) {
|
|
596
|
+
if (await isServerHealthy(session)) {
|
|
597
|
+
return
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const existingPid = readPid(getPidPath(session))
|
|
601
|
+
if (existingPid && isPidAlive(existingPid)) {
|
|
602
|
+
// A daemon process already owns this session. It may still be
|
|
603
|
+
// starting up or shutting down; avoid spawning a competing daemon.
|
|
604
|
+
await sleep(POLL_INTERVAL)
|
|
605
|
+
continue
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
recoverStaleStartLock(session)
|
|
609
|
+
|
|
610
|
+
if (acquireStartLock(session)) {
|
|
611
|
+
try {
|
|
612
|
+
if (!(await isServerHealthy(session))) {
|
|
613
|
+
startServer(session)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
await waitForServerReady(
|
|
617
|
+
session,
|
|
618
|
+
Math.max(500, deadline - Date.now())
|
|
619
|
+
)
|
|
620
|
+
return
|
|
621
|
+
} finally {
|
|
622
|
+
releaseStartLock(session)
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
await sleep(POLL_INTERVAL)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
throw new Error(
|
|
630
|
+
`Failed to start server for session '${session}'. Check that the build is complete.`
|
|
631
|
+
)
|
|
632
|
+
}
|
|
633
|
+
|
|
384
634
|
function listSessions() {
|
|
385
635
|
const sessions = []
|
|
386
636
|
const entries = readdirSync(tmpdir())
|
|
@@ -465,11 +715,11 @@ Navigation:
|
|
|
465
715
|
forward Go forward
|
|
466
716
|
reload Reload page
|
|
467
717
|
close Close browser and server
|
|
468
|
-
close --all Close all active
|
|
718
|
+
close --all Close all active session-scoped servers
|
|
469
719
|
|
|
470
720
|
Sessions:
|
|
471
|
-
sessions List active
|
|
472
|
-
status Show resolved
|
|
721
|
+
sessions List active session-scoped daemons
|
|
722
|
+
status Show resolved session/name and session state
|
|
473
723
|
|
|
474
724
|
Observation:
|
|
475
725
|
snapshot [--mode action] Get page snapshot
|
|
@@ -515,7 +765,8 @@ Utility:
|
|
|
515
765
|
extract <schema-json> Extract structured data
|
|
516
766
|
|
|
517
767
|
Global Flags:
|
|
518
|
-
--
|
|
768
|
+
--session <id> Runtime session id for daemon/browser routing
|
|
769
|
+
--name <namespace> Selector namespace for cache storage on 'open'
|
|
519
770
|
--headless Launch browser in headless mode
|
|
520
771
|
--connect-url <url> Connect to a running browser (e.g. http://localhost:9222)
|
|
521
772
|
--channel <browser> Use installed browser (chrome, chrome-beta, msedge)
|
|
@@ -527,7 +778,9 @@ Global Flags:
|
|
|
527
778
|
--version, -v Show version
|
|
528
779
|
|
|
529
780
|
Environment:
|
|
530
|
-
|
|
781
|
+
OPENSTEER_SESSION Runtime session id (equivalent to --session)
|
|
782
|
+
OPENSTEER_CLIENT_ID Stable client identity for default session binding
|
|
783
|
+
OPENSTEER_NAME Default selector namespace for 'open' when --name is omitted
|
|
531
784
|
OPENSTEER_MODE Runtime mode: "local" (default) or "remote"
|
|
532
785
|
OPENSTEER_API_KEY Required when remote mode is selected
|
|
533
786
|
OPENSTEER_BASE_URL Override remote control-plane base URL
|
|
@@ -536,26 +789,12 @@ Environment:
|
|
|
536
789
|
|
|
537
790
|
async function main() {
|
|
538
791
|
const { command, flags, positional } = parseArgs(process.argv)
|
|
539
|
-
const { namespace, source: namespaceSource } = resolveNamespace(flags)
|
|
540
|
-
const socketPath = getSocketPath(namespace)
|
|
541
792
|
|
|
542
793
|
if (command === 'sessions') {
|
|
543
794
|
output({ ok: true, sessions: listSessions() })
|
|
544
795
|
return
|
|
545
796
|
}
|
|
546
797
|
|
|
547
|
-
if (command === 'status') {
|
|
548
|
-
output({
|
|
549
|
-
ok: true,
|
|
550
|
-
namespace,
|
|
551
|
-
namespaceSource,
|
|
552
|
-
serverRunning: isServerRunning(namespace),
|
|
553
|
-
socketPath,
|
|
554
|
-
sessions: listSessions(),
|
|
555
|
-
})
|
|
556
|
-
return
|
|
557
|
-
}
|
|
558
|
-
|
|
559
798
|
if (command === 'close' && flags.all === true) {
|
|
560
799
|
try {
|
|
561
800
|
const closed = await closeAllSessions()
|
|
@@ -566,36 +805,66 @@ async function main() {
|
|
|
566
805
|
return
|
|
567
806
|
}
|
|
568
807
|
|
|
808
|
+
let resolvedSession
|
|
809
|
+
let resolvedName
|
|
810
|
+
try {
|
|
811
|
+
resolvedSession = resolveSession(flags)
|
|
812
|
+
resolvedName = resolveName(flags, resolvedSession.session)
|
|
813
|
+
} catch (err) {
|
|
814
|
+
error(err instanceof Error ? err.message : 'Failed to resolve session')
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const session = resolvedSession.session
|
|
818
|
+
const sessionSource = resolvedSession.source
|
|
819
|
+
const name = resolvedName.name
|
|
820
|
+
const nameSource = resolvedName.source
|
|
821
|
+
const socketPath = getSocketPath(session)
|
|
822
|
+
|
|
823
|
+
if (command === 'status') {
|
|
824
|
+
output({
|
|
825
|
+
ok: true,
|
|
826
|
+
resolvedSession: session,
|
|
827
|
+
sessionSource,
|
|
828
|
+
resolvedName: name,
|
|
829
|
+
nameSource,
|
|
830
|
+
serverRunning: await isServerHealthy(session),
|
|
831
|
+
socketPath,
|
|
832
|
+
sessions: listSessions(),
|
|
833
|
+
})
|
|
834
|
+
return
|
|
835
|
+
}
|
|
836
|
+
|
|
569
837
|
delete flags.name
|
|
838
|
+
delete flags.session
|
|
570
839
|
delete flags.all
|
|
840
|
+
|
|
571
841
|
const request = buildRequest(command, flags, positional)
|
|
842
|
+
if (command === 'open') {
|
|
843
|
+
request.args.name = name
|
|
844
|
+
}
|
|
572
845
|
|
|
573
|
-
if (!
|
|
846
|
+
if (!(await isServerHealthy(session))) {
|
|
574
847
|
if (command !== 'open') {
|
|
575
848
|
error(
|
|
576
|
-
`No server running for
|
|
849
|
+
`No server running for session '${session}' (resolved from ${sessionSource}). Run 'opensteer open' first or use 'opensteer sessions' to see active sessions.`
|
|
577
850
|
)
|
|
578
851
|
}
|
|
579
|
-
|
|
852
|
+
|
|
853
|
+
try {
|
|
854
|
+
await ensureServer(session)
|
|
855
|
+
} catch (err) {
|
|
580
856
|
error(
|
|
581
|
-
|
|
857
|
+
err instanceof Error
|
|
858
|
+
? err.message
|
|
859
|
+
: 'Failed to start server. Check that the build is complete.'
|
|
582
860
|
)
|
|
583
861
|
}
|
|
584
|
-
startServer(namespace)
|
|
585
|
-
try {
|
|
586
|
-
await waitForSocket(socketPath, CONNECT_TIMEOUT)
|
|
587
|
-
} catch {
|
|
588
|
-
error('Failed to start server. Check that the build is complete.')
|
|
589
|
-
}
|
|
590
862
|
}
|
|
591
863
|
|
|
592
864
|
try {
|
|
593
865
|
const response = await sendCommand(socketPath, request)
|
|
594
866
|
|
|
595
867
|
if (response.ok) {
|
|
596
|
-
if (command === 'open') {
|
|
597
|
-
writeActiveNamespace(namespace)
|
|
598
|
-
}
|
|
599
868
|
output({ ok: true, ...response.result })
|
|
600
869
|
} else {
|
|
601
870
|
process.stderr.write(
|
package/dist/cli/server.cjs
CHANGED
|
@@ -817,15 +817,15 @@ function normalizeNamespace(input) {
|
|
|
817
817
|
if (!segments.length) return DEFAULT_NAMESPACE;
|
|
818
818
|
return segments.join("/");
|
|
819
819
|
}
|
|
820
|
-
function resolveNamespaceDir(rootDir,
|
|
820
|
+
function resolveNamespaceDir(rootDir, namespace) {
|
|
821
821
|
const selectorsRoot = import_path2.default.resolve(rootDir, ".opensteer", "selectors");
|
|
822
|
-
const normalizedNamespace = normalizeNamespace(
|
|
822
|
+
const normalizedNamespace = normalizeNamespace(namespace);
|
|
823
823
|
const namespaceDir = import_path2.default.resolve(selectorsRoot, normalizedNamespace);
|
|
824
824
|
const relative = import_path2.default.relative(selectorsRoot, namespaceDir);
|
|
825
825
|
if (relative === "" || relative === ".") return namespaceDir;
|
|
826
826
|
if (relative.startsWith("..") || import_path2.default.isAbsolute(relative)) {
|
|
827
827
|
throw new Error(
|
|
828
|
-
`Namespace "${
|
|
828
|
+
`Namespace "${namespace}" resolves outside selectors root.`
|
|
829
829
|
);
|
|
830
830
|
}
|
|
831
831
|
return namespaceDir;
|
|
@@ -1275,9 +1275,9 @@ function createEmptyRegistry(name) {
|
|
|
1275
1275
|
var LocalSelectorStorage = class {
|
|
1276
1276
|
rootDir;
|
|
1277
1277
|
namespace;
|
|
1278
|
-
constructor(rootDir,
|
|
1278
|
+
constructor(rootDir, namespace) {
|
|
1279
1279
|
this.rootDir = rootDir;
|
|
1280
|
-
this.namespace = normalizeNamespace(
|
|
1280
|
+
this.namespace = normalizeNamespace(namespace);
|
|
1281
1281
|
}
|
|
1282
1282
|
getRootDir() {
|
|
1283
1283
|
return this.rootDir;
|
|
@@ -6973,7 +6973,7 @@ function withTokenQuery(wsUrl, token) {
|
|
|
6973
6973
|
var import_fs3 = __toESM(require("fs"), 1);
|
|
6974
6974
|
var import_path5 = __toESM(require("path"), 1);
|
|
6975
6975
|
function collectLocalSelectorCacheEntries(storage) {
|
|
6976
|
-
const
|
|
6976
|
+
const namespace = storage.getNamespace();
|
|
6977
6977
|
const namespaceDir = storage.getNamespaceDir();
|
|
6978
6978
|
if (!import_fs3.default.existsSync(namespaceDir)) return [];
|
|
6979
6979
|
const entries = [];
|
|
@@ -6994,7 +6994,7 @@ function collectLocalSelectorCacheEntries(storage) {
|
|
|
6994
6994
|
continue;
|
|
6995
6995
|
}
|
|
6996
6996
|
entries.push({
|
|
6997
|
-
namespace
|
|
6997
|
+
namespace,
|
|
6998
6998
|
siteOrigin,
|
|
6999
6999
|
method,
|
|
7000
7000
|
descriptionHash,
|
|
@@ -7430,20 +7430,20 @@ var Opensteer = class _Opensteer {
|
|
|
7430
7430
|
);
|
|
7431
7431
|
}
|
|
7432
7432
|
}
|
|
7433
|
-
const
|
|
7433
|
+
const session3 = await this.remote.sessionClient.create({
|
|
7434
7434
|
name: this.namespace,
|
|
7435
7435
|
model: this.config.model,
|
|
7436
7436
|
launchContext: options.context || void 0
|
|
7437
7437
|
});
|
|
7438
|
-
sessionId =
|
|
7438
|
+
sessionId = session3.sessionId;
|
|
7439
7439
|
actionClient = await ActionWsClient.connect({
|
|
7440
|
-
url:
|
|
7441
|
-
token:
|
|
7442
|
-
sessionId:
|
|
7440
|
+
url: session3.actionWsUrl,
|
|
7441
|
+
token: session3.actionToken,
|
|
7442
|
+
sessionId: session3.sessionId
|
|
7443
7443
|
});
|
|
7444
7444
|
const cdpConnection = await this.remote.cdpClient.connect({
|
|
7445
|
-
wsUrl:
|
|
7446
|
-
token:
|
|
7445
|
+
wsUrl: session3.cdpWsUrl,
|
|
7446
|
+
token: session3.cdpToken
|
|
7447
7447
|
});
|
|
7448
7448
|
browser = cdpConnection.browser;
|
|
7449
7449
|
this.browser = cdpConnection.browser;
|
|
@@ -7467,15 +7467,15 @@ var Opensteer = class _Opensteer {
|
|
|
7467
7467
|
throw error;
|
|
7468
7468
|
}
|
|
7469
7469
|
}
|
|
7470
|
-
const
|
|
7470
|
+
const session2 = await this.pool.launch({
|
|
7471
7471
|
...options,
|
|
7472
7472
|
connectUrl: options.connectUrl ?? this.config.browser?.connectUrl,
|
|
7473
7473
|
channel: options.channel ?? this.config.browser?.channel,
|
|
7474
7474
|
profileDir: options.profileDir ?? this.config.browser?.profileDir
|
|
7475
7475
|
});
|
|
7476
|
-
this.browser =
|
|
7477
|
-
this.contextRef =
|
|
7478
|
-
this.pageRef =
|
|
7476
|
+
this.browser = session2.browser;
|
|
7477
|
+
this.contextRef = session2.context;
|
|
7478
|
+
this.pageRef = session2.page;
|
|
7479
7479
|
this.ownsBrowser = true;
|
|
7480
7480
|
this.snapshotCache = null;
|
|
7481
7481
|
}
|
|
@@ -9501,14 +9501,14 @@ function getScrollDelta2(options) {
|
|
|
9501
9501
|
// src/cli/paths.ts
|
|
9502
9502
|
var import_os2 = require("os");
|
|
9503
9503
|
var import_path6 = require("path");
|
|
9504
|
-
function prefix(
|
|
9505
|
-
return `opensteer-${
|
|
9504
|
+
function prefix(session2) {
|
|
9505
|
+
return `opensteer-${session2}`;
|
|
9506
9506
|
}
|
|
9507
|
-
function getSocketPath(
|
|
9508
|
-
return (0, import_path6.join)((0, import_os2.tmpdir)(), `${prefix(
|
|
9507
|
+
function getSocketPath(session2) {
|
|
9508
|
+
return (0, import_path6.join)((0, import_os2.tmpdir)(), `${prefix(session2)}.sock`);
|
|
9509
9509
|
}
|
|
9510
|
-
function getPidPath(
|
|
9511
|
-
return (0, import_path6.join)((0, import_os2.tmpdir)(), `${prefix(
|
|
9510
|
+
function getPidPath(session2) {
|
|
9511
|
+
return (0, import_path6.join)((0, import_os2.tmpdir)(), `${prefix(session2)}.pid`);
|
|
9512
9512
|
}
|
|
9513
9513
|
|
|
9514
9514
|
// src/cli/commands.ts
|
|
@@ -9746,13 +9746,40 @@ function getCommandHandler(name) {
|
|
|
9746
9746
|
// src/cli/server.ts
|
|
9747
9747
|
var instance = null;
|
|
9748
9748
|
var launchPromise = null;
|
|
9749
|
-
var
|
|
9750
|
-
|
|
9751
|
-
|
|
9749
|
+
var selectorNamespace = null;
|
|
9750
|
+
var requestQueue = Promise.resolve();
|
|
9751
|
+
var shuttingDown = false;
|
|
9752
|
+
function sanitizeNamespace(value) {
|
|
9753
|
+
const trimmed = String(value || "").trim();
|
|
9754
|
+
if (!trimmed || trimmed === "." || trimmed === "..") {
|
|
9755
|
+
return "default";
|
|
9756
|
+
}
|
|
9757
|
+
const replaced = trimmed.replace(/[^a-zA-Z0-9_-]+/g, "_");
|
|
9758
|
+
const collapsed = replaced.replace(/_+/g, "_");
|
|
9759
|
+
const bounded = collapsed.replace(/^_+|_+$/g, "");
|
|
9760
|
+
return bounded || "default";
|
|
9761
|
+
}
|
|
9762
|
+
function invalidateInstance() {
|
|
9763
|
+
if (!instance) return;
|
|
9764
|
+
instance.close().catch(() => {
|
|
9765
|
+
});
|
|
9766
|
+
instance = null;
|
|
9767
|
+
}
|
|
9768
|
+
function attachLifecycleListeners(inst) {
|
|
9769
|
+
try {
|
|
9770
|
+
inst.page.on("close", invalidateInstance);
|
|
9771
|
+
inst.context.on("close", invalidateInstance);
|
|
9772
|
+
} catch {
|
|
9773
|
+
}
|
|
9774
|
+
}
|
|
9775
|
+
var sessionEnv = process.env.OPENSTEER_SESSION?.trim();
|
|
9776
|
+
if (!sessionEnv) {
|
|
9777
|
+
process.stderr.write("Missing OPENSTEER_SESSION environment variable.\n");
|
|
9752
9778
|
process.exit(1);
|
|
9753
9779
|
}
|
|
9754
|
-
var
|
|
9755
|
-
var
|
|
9780
|
+
var session = sessionEnv;
|
|
9781
|
+
var socketPath = getSocketPath(session);
|
|
9782
|
+
var pidPath = getPidPath(session);
|
|
9756
9783
|
function cleanup() {
|
|
9757
9784
|
try {
|
|
9758
9785
|
(0, import_fs4.unlinkSync)(socketPath);
|
|
@@ -9763,14 +9790,49 @@ function cleanup() {
|
|
|
9763
9790
|
} catch {
|
|
9764
9791
|
}
|
|
9765
9792
|
}
|
|
9793
|
+
function beginShutdown() {
|
|
9794
|
+
if (shuttingDown) return;
|
|
9795
|
+
shuttingDown = true;
|
|
9796
|
+
cleanup();
|
|
9797
|
+
server.close(() => {
|
|
9798
|
+
process.exit(0);
|
|
9799
|
+
});
|
|
9800
|
+
setTimeout(() => {
|
|
9801
|
+
process.exit(0);
|
|
9802
|
+
}, 250).unref();
|
|
9803
|
+
}
|
|
9766
9804
|
function sendResponse(socket, response) {
|
|
9767
9805
|
try {
|
|
9768
9806
|
socket.write(JSON.stringify(response) + "\n");
|
|
9769
9807
|
} catch {
|
|
9770
9808
|
}
|
|
9771
9809
|
}
|
|
9810
|
+
function enqueueRequest(request, socket) {
|
|
9811
|
+
if (request.command === "ping") {
|
|
9812
|
+
void handleRequest(request, socket);
|
|
9813
|
+
return;
|
|
9814
|
+
}
|
|
9815
|
+
requestQueue = requestQueue.then(() => handleRequest(request, socket)).catch(() => {
|
|
9816
|
+
});
|
|
9817
|
+
}
|
|
9772
9818
|
async function handleRequest(request, socket) {
|
|
9773
9819
|
const { id, command, args } = request;
|
|
9820
|
+
if (command === "ping" && shuttingDown) {
|
|
9821
|
+
sendResponse(socket, {
|
|
9822
|
+
id,
|
|
9823
|
+
ok: false,
|
|
9824
|
+
error: `Session '${session}' is shutting down.`
|
|
9825
|
+
});
|
|
9826
|
+
return;
|
|
9827
|
+
}
|
|
9828
|
+
if (shuttingDown) {
|
|
9829
|
+
sendResponse(socket, {
|
|
9830
|
+
id,
|
|
9831
|
+
ok: false,
|
|
9832
|
+
error: `Session '${session}' is shutting down. Retry your command.`
|
|
9833
|
+
});
|
|
9834
|
+
return;
|
|
9835
|
+
}
|
|
9774
9836
|
if (command === "open") {
|
|
9775
9837
|
try {
|
|
9776
9838
|
const url = args.url;
|
|
@@ -9778,23 +9840,31 @@ async function handleRequest(request, socket) {
|
|
|
9778
9840
|
const connectUrl = args["connect-url"];
|
|
9779
9841
|
const channel = args.channel;
|
|
9780
9842
|
const profileDir = args["profile-dir"];
|
|
9843
|
+
const requestedName = typeof args.name === "string" && args.name.trim().length > 0 ? sanitizeNamespace(args.name) : null;
|
|
9844
|
+
if (selectorNamespace && requestedName && requestedName !== selectorNamespace) {
|
|
9845
|
+
sendResponse(socket, {
|
|
9846
|
+
id,
|
|
9847
|
+
ok: false,
|
|
9848
|
+
error: `Session '${session}' is already bound to selector namespace '${selectorNamespace}'. Requested '${requestedName}' does not match. Use the same --name for this session or start a different --session.`
|
|
9849
|
+
});
|
|
9850
|
+
return;
|
|
9851
|
+
}
|
|
9852
|
+
if (!selectorNamespace) {
|
|
9853
|
+
selectorNamespace = requestedName ?? session;
|
|
9854
|
+
}
|
|
9855
|
+
const activeNamespace = selectorNamespace ?? session;
|
|
9781
9856
|
if (instance && !launchPromise) {
|
|
9782
9857
|
try {
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
await instance.close().catch(() => {
|
|
9786
|
-
});
|
|
9787
|
-
instance = null;
|
|
9858
|
+
if (instance.page.isClosed()) {
|
|
9859
|
+
invalidateInstance();
|
|
9788
9860
|
}
|
|
9789
9861
|
} catch {
|
|
9790
|
-
|
|
9791
|
-
});
|
|
9792
|
-
instance = null;
|
|
9862
|
+
invalidateInstance();
|
|
9793
9863
|
}
|
|
9794
9864
|
}
|
|
9795
9865
|
if (!instance) {
|
|
9796
9866
|
instance = new Opensteer({
|
|
9797
|
-
name:
|
|
9867
|
+
name: activeNamespace,
|
|
9798
9868
|
browser: {
|
|
9799
9869
|
headless: headless ?? false,
|
|
9800
9870
|
connectUrl,
|
|
@@ -9808,6 +9878,7 @@ async function handleRequest(request, socket) {
|
|
|
9808
9878
|
});
|
|
9809
9879
|
try {
|
|
9810
9880
|
await launchPromise;
|
|
9881
|
+
attachLifecycleListeners(instance);
|
|
9811
9882
|
} catch (err) {
|
|
9812
9883
|
instance = null;
|
|
9813
9884
|
throw err;
|
|
@@ -9825,8 +9896,9 @@ async function handleRequest(request, socket) {
|
|
|
9825
9896
|
ok: true,
|
|
9826
9897
|
result: {
|
|
9827
9898
|
url: instance.page.url(),
|
|
9828
|
-
|
|
9829
|
-
name:
|
|
9899
|
+
session,
|
|
9900
|
+
name: activeNamespace,
|
|
9901
|
+
remoteSessionId: instance.getRemoteSessionId() ?? void 0
|
|
9830
9902
|
}
|
|
9831
9903
|
});
|
|
9832
9904
|
} catch (err) {
|
|
@@ -9856,10 +9928,7 @@ async function handleRequest(request, socket) {
|
|
|
9856
9928
|
error: err instanceof Error ? err.message : String(err)
|
|
9857
9929
|
});
|
|
9858
9930
|
}
|
|
9859
|
-
|
|
9860
|
-
cleanup();
|
|
9861
|
-
process.exit(0);
|
|
9862
|
-
}, 100);
|
|
9931
|
+
beginShutdown();
|
|
9863
9932
|
return;
|
|
9864
9933
|
}
|
|
9865
9934
|
if (command === "ping") {
|
|
@@ -9870,7 +9939,7 @@ async function handleRequest(request, socket) {
|
|
|
9870
9939
|
sendResponse(socket, {
|
|
9871
9940
|
id,
|
|
9872
9941
|
ok: false,
|
|
9873
|
-
error: `No browser session in
|
|
9942
|
+
error: `No browser session in session '${session}'. Call 'opensteer open --session ${session}' first, or use 'opensteer sessions' to list active sessions.`
|
|
9874
9943
|
});
|
|
9875
9944
|
return;
|
|
9876
9945
|
}
|
|
@@ -9907,7 +9976,7 @@ var server = (0, import_net.createServer)((socket) => {
|
|
|
9907
9976
|
if (!line.trim()) continue;
|
|
9908
9977
|
try {
|
|
9909
9978
|
const request = JSON.parse(line);
|
|
9910
|
-
|
|
9979
|
+
enqueueRequest(request, socket);
|
|
9911
9980
|
} catch {
|
|
9912
9981
|
sendResponse(socket, {
|
|
9913
9982
|
id: 0,
|
|
@@ -9932,6 +10001,8 @@ server.on("error", (err) => {
|
|
|
9932
10001
|
process.exit(1);
|
|
9933
10002
|
});
|
|
9934
10003
|
async function shutdown() {
|
|
10004
|
+
if (shuttingDown) return;
|
|
10005
|
+
shuttingDown = true;
|
|
9935
10006
|
if (instance) {
|
|
9936
10007
|
try {
|
|
9937
10008
|
await instance.close();
|
|
@@ -9939,9 +10010,13 @@ async function shutdown() {
|
|
|
9939
10010
|
}
|
|
9940
10011
|
instance = null;
|
|
9941
10012
|
}
|
|
9942
|
-
server.close();
|
|
9943
10013
|
cleanup();
|
|
9944
|
-
|
|
10014
|
+
server.close(() => {
|
|
10015
|
+
process.exit(0);
|
|
10016
|
+
});
|
|
10017
|
+
setTimeout(() => {
|
|
10018
|
+
process.exit(0);
|
|
10019
|
+
}, 250).unref();
|
|
9945
10020
|
}
|
|
9946
10021
|
process.on("SIGTERM", shutdown);
|
|
9947
10022
|
process.on("SIGINT", shutdown);
|
package/dist/cli/server.js
CHANGED
|
@@ -10,14 +10,14 @@ import { writeFileSync, unlinkSync, existsSync } from "fs";
|
|
|
10
10
|
// src/cli/paths.ts
|
|
11
11
|
import { tmpdir } from "os";
|
|
12
12
|
import { join } from "path";
|
|
13
|
-
function prefix(
|
|
14
|
-
return `opensteer-${
|
|
13
|
+
function prefix(session2) {
|
|
14
|
+
return `opensteer-${session2}`;
|
|
15
15
|
}
|
|
16
|
-
function getSocketPath(
|
|
17
|
-
return join(tmpdir(), `${prefix(
|
|
16
|
+
function getSocketPath(session2) {
|
|
17
|
+
return join(tmpdir(), `${prefix(session2)}.sock`);
|
|
18
18
|
}
|
|
19
|
-
function getPidPath(
|
|
20
|
-
return join(tmpdir(), `${prefix(
|
|
19
|
+
function getPidPath(session2) {
|
|
20
|
+
return join(tmpdir(), `${prefix(session2)}.pid`);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// src/cli/commands.ts
|
|
@@ -255,13 +255,40 @@ function getCommandHandler(name) {
|
|
|
255
255
|
// src/cli/server.ts
|
|
256
256
|
var instance = null;
|
|
257
257
|
var launchPromise = null;
|
|
258
|
-
var
|
|
259
|
-
|
|
260
|
-
|
|
258
|
+
var selectorNamespace = null;
|
|
259
|
+
var requestQueue = Promise.resolve();
|
|
260
|
+
var shuttingDown = false;
|
|
261
|
+
function sanitizeNamespace(value) {
|
|
262
|
+
const trimmed = String(value || "").trim();
|
|
263
|
+
if (!trimmed || trimmed === "." || trimmed === "..") {
|
|
264
|
+
return "default";
|
|
265
|
+
}
|
|
266
|
+
const replaced = trimmed.replace(/[^a-zA-Z0-9_-]+/g, "_");
|
|
267
|
+
const collapsed = replaced.replace(/_+/g, "_");
|
|
268
|
+
const bounded = collapsed.replace(/^_+|_+$/g, "");
|
|
269
|
+
return bounded || "default";
|
|
270
|
+
}
|
|
271
|
+
function invalidateInstance() {
|
|
272
|
+
if (!instance) return;
|
|
273
|
+
instance.close().catch(() => {
|
|
274
|
+
});
|
|
275
|
+
instance = null;
|
|
276
|
+
}
|
|
277
|
+
function attachLifecycleListeners(inst) {
|
|
278
|
+
try {
|
|
279
|
+
inst.page.on("close", invalidateInstance);
|
|
280
|
+
inst.context.on("close", invalidateInstance);
|
|
281
|
+
} catch {
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
var sessionEnv = process.env.OPENSTEER_SESSION?.trim();
|
|
285
|
+
if (!sessionEnv) {
|
|
286
|
+
process.stderr.write("Missing OPENSTEER_SESSION environment variable.\n");
|
|
261
287
|
process.exit(1);
|
|
262
288
|
}
|
|
263
|
-
var
|
|
264
|
-
var
|
|
289
|
+
var session = sessionEnv;
|
|
290
|
+
var socketPath = getSocketPath(session);
|
|
291
|
+
var pidPath = getPidPath(session);
|
|
265
292
|
function cleanup() {
|
|
266
293
|
try {
|
|
267
294
|
unlinkSync(socketPath);
|
|
@@ -272,14 +299,49 @@ function cleanup() {
|
|
|
272
299
|
} catch {
|
|
273
300
|
}
|
|
274
301
|
}
|
|
302
|
+
function beginShutdown() {
|
|
303
|
+
if (shuttingDown) return;
|
|
304
|
+
shuttingDown = true;
|
|
305
|
+
cleanup();
|
|
306
|
+
server.close(() => {
|
|
307
|
+
process.exit(0);
|
|
308
|
+
});
|
|
309
|
+
setTimeout(() => {
|
|
310
|
+
process.exit(0);
|
|
311
|
+
}, 250).unref();
|
|
312
|
+
}
|
|
275
313
|
function sendResponse(socket, response) {
|
|
276
314
|
try {
|
|
277
315
|
socket.write(JSON.stringify(response) + "\n");
|
|
278
316
|
} catch {
|
|
279
317
|
}
|
|
280
318
|
}
|
|
319
|
+
function enqueueRequest(request, socket) {
|
|
320
|
+
if (request.command === "ping") {
|
|
321
|
+
void handleRequest(request, socket);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
requestQueue = requestQueue.then(() => handleRequest(request, socket)).catch(() => {
|
|
325
|
+
});
|
|
326
|
+
}
|
|
281
327
|
async function handleRequest(request, socket) {
|
|
282
328
|
const { id, command, args } = request;
|
|
329
|
+
if (command === "ping" && shuttingDown) {
|
|
330
|
+
sendResponse(socket, {
|
|
331
|
+
id,
|
|
332
|
+
ok: false,
|
|
333
|
+
error: `Session '${session}' is shutting down.`
|
|
334
|
+
});
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (shuttingDown) {
|
|
338
|
+
sendResponse(socket, {
|
|
339
|
+
id,
|
|
340
|
+
ok: false,
|
|
341
|
+
error: `Session '${session}' is shutting down. Retry your command.`
|
|
342
|
+
});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
283
345
|
if (command === "open") {
|
|
284
346
|
try {
|
|
285
347
|
const url = args.url;
|
|
@@ -287,23 +349,31 @@ async function handleRequest(request, socket) {
|
|
|
287
349
|
const connectUrl = args["connect-url"];
|
|
288
350
|
const channel = args.channel;
|
|
289
351
|
const profileDir = args["profile-dir"];
|
|
352
|
+
const requestedName = typeof args.name === "string" && args.name.trim().length > 0 ? sanitizeNamespace(args.name) : null;
|
|
353
|
+
if (selectorNamespace && requestedName && requestedName !== selectorNamespace) {
|
|
354
|
+
sendResponse(socket, {
|
|
355
|
+
id,
|
|
356
|
+
ok: false,
|
|
357
|
+
error: `Session '${session}' is already bound to selector namespace '${selectorNamespace}'. Requested '${requestedName}' does not match. Use the same --name for this session or start a different --session.`
|
|
358
|
+
});
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (!selectorNamespace) {
|
|
362
|
+
selectorNamespace = requestedName ?? session;
|
|
363
|
+
}
|
|
364
|
+
const activeNamespace = selectorNamespace ?? session;
|
|
290
365
|
if (instance && !launchPromise) {
|
|
291
366
|
try {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
await instance.close().catch(() => {
|
|
295
|
-
});
|
|
296
|
-
instance = null;
|
|
367
|
+
if (instance.page.isClosed()) {
|
|
368
|
+
invalidateInstance();
|
|
297
369
|
}
|
|
298
370
|
} catch {
|
|
299
|
-
|
|
300
|
-
});
|
|
301
|
-
instance = null;
|
|
371
|
+
invalidateInstance();
|
|
302
372
|
}
|
|
303
373
|
}
|
|
304
374
|
if (!instance) {
|
|
305
375
|
instance = new Opensteer({
|
|
306
|
-
name:
|
|
376
|
+
name: activeNamespace,
|
|
307
377
|
browser: {
|
|
308
378
|
headless: headless ?? false,
|
|
309
379
|
connectUrl,
|
|
@@ -317,6 +387,7 @@ async function handleRequest(request, socket) {
|
|
|
317
387
|
});
|
|
318
388
|
try {
|
|
319
389
|
await launchPromise;
|
|
390
|
+
attachLifecycleListeners(instance);
|
|
320
391
|
} catch (err) {
|
|
321
392
|
instance = null;
|
|
322
393
|
throw err;
|
|
@@ -334,8 +405,9 @@ async function handleRequest(request, socket) {
|
|
|
334
405
|
ok: true,
|
|
335
406
|
result: {
|
|
336
407
|
url: instance.page.url(),
|
|
337
|
-
|
|
338
|
-
name:
|
|
408
|
+
session,
|
|
409
|
+
name: activeNamespace,
|
|
410
|
+
remoteSessionId: instance.getRemoteSessionId() ?? void 0
|
|
339
411
|
}
|
|
340
412
|
});
|
|
341
413
|
} catch (err) {
|
|
@@ -365,10 +437,7 @@ async function handleRequest(request, socket) {
|
|
|
365
437
|
error: err instanceof Error ? err.message : String(err)
|
|
366
438
|
});
|
|
367
439
|
}
|
|
368
|
-
|
|
369
|
-
cleanup();
|
|
370
|
-
process.exit(0);
|
|
371
|
-
}, 100);
|
|
440
|
+
beginShutdown();
|
|
372
441
|
return;
|
|
373
442
|
}
|
|
374
443
|
if (command === "ping") {
|
|
@@ -379,7 +448,7 @@ async function handleRequest(request, socket) {
|
|
|
379
448
|
sendResponse(socket, {
|
|
380
449
|
id,
|
|
381
450
|
ok: false,
|
|
382
|
-
error: `No browser session in
|
|
451
|
+
error: `No browser session in session '${session}'. Call 'opensteer open --session ${session}' first, or use 'opensteer sessions' to list active sessions.`
|
|
383
452
|
});
|
|
384
453
|
return;
|
|
385
454
|
}
|
|
@@ -416,7 +485,7 @@ var server = createServer((socket) => {
|
|
|
416
485
|
if (!line.trim()) continue;
|
|
417
486
|
try {
|
|
418
487
|
const request = JSON.parse(line);
|
|
419
|
-
|
|
488
|
+
enqueueRequest(request, socket);
|
|
420
489
|
} catch {
|
|
421
490
|
sendResponse(socket, {
|
|
422
491
|
id: 0,
|
|
@@ -441,6 +510,8 @@ server.on("error", (err) => {
|
|
|
441
510
|
process.exit(1);
|
|
442
511
|
});
|
|
443
512
|
async function shutdown() {
|
|
513
|
+
if (shuttingDown) return;
|
|
514
|
+
shuttingDown = true;
|
|
444
515
|
if (instance) {
|
|
445
516
|
try {
|
|
446
517
|
await instance.close();
|
|
@@ -448,9 +519,13 @@ async function shutdown() {
|
|
|
448
519
|
}
|
|
449
520
|
instance = null;
|
|
450
521
|
}
|
|
451
|
-
server.close();
|
|
452
522
|
cleanup();
|
|
453
|
-
|
|
523
|
+
server.close(() => {
|
|
524
|
+
process.exit(0);
|
|
525
|
+
});
|
|
526
|
+
setTimeout(() => {
|
|
527
|
+
process.exit(0);
|
|
528
|
+
}, 250).unref();
|
|
454
529
|
}
|
|
455
530
|
process.on("SIGTERM", shutdown);
|
|
456
531
|
process.on("SIGINT", shutdown);
|