opensteer 0.4.4 → 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 +106 -40
- package/dist/cli/server.js +88 -22
- 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,6 +9746,19 @@ function getCommandHandler(name) {
|
|
|
9746
9746
|
// src/cli/server.ts
|
|
9747
9747
|
var instance = null;
|
|
9748
9748
|
var launchPromise = null;
|
|
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
|
+
}
|
|
9749
9762
|
function invalidateInstance() {
|
|
9750
9763
|
if (!instance) return;
|
|
9751
9764
|
instance.close().catch(() => {
|
|
@@ -9759,13 +9772,14 @@ function attachLifecycleListeners(inst) {
|
|
|
9759
9772
|
} catch {
|
|
9760
9773
|
}
|
|
9761
9774
|
}
|
|
9762
|
-
var
|
|
9763
|
-
if (!
|
|
9764
|
-
process.stderr.write("Missing
|
|
9775
|
+
var sessionEnv = process.env.OPENSTEER_SESSION?.trim();
|
|
9776
|
+
if (!sessionEnv) {
|
|
9777
|
+
process.stderr.write("Missing OPENSTEER_SESSION environment variable.\n");
|
|
9765
9778
|
process.exit(1);
|
|
9766
9779
|
}
|
|
9767
|
-
var
|
|
9768
|
-
var
|
|
9780
|
+
var session = sessionEnv;
|
|
9781
|
+
var socketPath = getSocketPath(session);
|
|
9782
|
+
var pidPath = getPidPath(session);
|
|
9769
9783
|
function cleanup() {
|
|
9770
9784
|
try {
|
|
9771
9785
|
(0, import_fs4.unlinkSync)(socketPath);
|
|
@@ -9776,14 +9790,49 @@ function cleanup() {
|
|
|
9776
9790
|
} catch {
|
|
9777
9791
|
}
|
|
9778
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
|
+
}
|
|
9779
9804
|
function sendResponse(socket, response) {
|
|
9780
9805
|
try {
|
|
9781
9806
|
socket.write(JSON.stringify(response) + "\n");
|
|
9782
9807
|
} catch {
|
|
9783
9808
|
}
|
|
9784
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
|
+
}
|
|
9785
9818
|
async function handleRequest(request, socket) {
|
|
9786
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
|
+
}
|
|
9787
9836
|
if (command === "open") {
|
|
9788
9837
|
try {
|
|
9789
9838
|
const url = args.url;
|
|
@@ -9791,6 +9840,19 @@ async function handleRequest(request, socket) {
|
|
|
9791
9840
|
const connectUrl = args["connect-url"];
|
|
9792
9841
|
const channel = args.channel;
|
|
9793
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;
|
|
9794
9856
|
if (instance && !launchPromise) {
|
|
9795
9857
|
try {
|
|
9796
9858
|
if (instance.page.isClosed()) {
|
|
@@ -9802,7 +9864,7 @@ async function handleRequest(request, socket) {
|
|
|
9802
9864
|
}
|
|
9803
9865
|
if (!instance) {
|
|
9804
9866
|
instance = new Opensteer({
|
|
9805
|
-
name:
|
|
9867
|
+
name: activeNamespace,
|
|
9806
9868
|
browser: {
|
|
9807
9869
|
headless: headless ?? false,
|
|
9808
9870
|
connectUrl,
|
|
@@ -9834,8 +9896,9 @@ async function handleRequest(request, socket) {
|
|
|
9834
9896
|
ok: true,
|
|
9835
9897
|
result: {
|
|
9836
9898
|
url: instance.page.url(),
|
|
9837
|
-
|
|
9838
|
-
name:
|
|
9899
|
+
session,
|
|
9900
|
+
name: activeNamespace,
|
|
9901
|
+
remoteSessionId: instance.getRemoteSessionId() ?? void 0
|
|
9839
9902
|
}
|
|
9840
9903
|
});
|
|
9841
9904
|
} catch (err) {
|
|
@@ -9865,10 +9928,7 @@ async function handleRequest(request, socket) {
|
|
|
9865
9928
|
error: err instanceof Error ? err.message : String(err)
|
|
9866
9929
|
});
|
|
9867
9930
|
}
|
|
9868
|
-
|
|
9869
|
-
cleanup();
|
|
9870
|
-
process.exit(0);
|
|
9871
|
-
}, 100);
|
|
9931
|
+
beginShutdown();
|
|
9872
9932
|
return;
|
|
9873
9933
|
}
|
|
9874
9934
|
if (command === "ping") {
|
|
@@ -9879,7 +9939,7 @@ async function handleRequest(request, socket) {
|
|
|
9879
9939
|
sendResponse(socket, {
|
|
9880
9940
|
id,
|
|
9881
9941
|
ok: false,
|
|
9882
|
-
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.`
|
|
9883
9943
|
});
|
|
9884
9944
|
return;
|
|
9885
9945
|
}
|
|
@@ -9916,7 +9976,7 @@ var server = (0, import_net.createServer)((socket) => {
|
|
|
9916
9976
|
if (!line.trim()) continue;
|
|
9917
9977
|
try {
|
|
9918
9978
|
const request = JSON.parse(line);
|
|
9919
|
-
|
|
9979
|
+
enqueueRequest(request, socket);
|
|
9920
9980
|
} catch {
|
|
9921
9981
|
sendResponse(socket, {
|
|
9922
9982
|
id: 0,
|
|
@@ -9941,6 +10001,8 @@ server.on("error", (err) => {
|
|
|
9941
10001
|
process.exit(1);
|
|
9942
10002
|
});
|
|
9943
10003
|
async function shutdown() {
|
|
10004
|
+
if (shuttingDown) return;
|
|
10005
|
+
shuttingDown = true;
|
|
9944
10006
|
if (instance) {
|
|
9945
10007
|
try {
|
|
9946
10008
|
await instance.close();
|
|
@@ -9948,9 +10010,13 @@ async function shutdown() {
|
|
|
9948
10010
|
}
|
|
9949
10011
|
instance = null;
|
|
9950
10012
|
}
|
|
9951
|
-
server.close();
|
|
9952
10013
|
cleanup();
|
|
9953
|
-
|
|
10014
|
+
server.close(() => {
|
|
10015
|
+
process.exit(0);
|
|
10016
|
+
});
|
|
10017
|
+
setTimeout(() => {
|
|
10018
|
+
process.exit(0);
|
|
10019
|
+
}, 250).unref();
|
|
9954
10020
|
}
|
|
9955
10021
|
process.on("SIGTERM", shutdown);
|
|
9956
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,6 +255,19 @@ function getCommandHandler(name) {
|
|
|
255
255
|
// src/cli/server.ts
|
|
256
256
|
var instance = null;
|
|
257
257
|
var launchPromise = null;
|
|
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
|
+
}
|
|
258
271
|
function invalidateInstance() {
|
|
259
272
|
if (!instance) return;
|
|
260
273
|
instance.close().catch(() => {
|
|
@@ -268,13 +281,14 @@ function attachLifecycleListeners(inst) {
|
|
|
268
281
|
} catch {
|
|
269
282
|
}
|
|
270
283
|
}
|
|
271
|
-
var
|
|
272
|
-
if (!
|
|
273
|
-
process.stderr.write("Missing
|
|
284
|
+
var sessionEnv = process.env.OPENSTEER_SESSION?.trim();
|
|
285
|
+
if (!sessionEnv) {
|
|
286
|
+
process.stderr.write("Missing OPENSTEER_SESSION environment variable.\n");
|
|
274
287
|
process.exit(1);
|
|
275
288
|
}
|
|
276
|
-
var
|
|
277
|
-
var
|
|
289
|
+
var session = sessionEnv;
|
|
290
|
+
var socketPath = getSocketPath(session);
|
|
291
|
+
var pidPath = getPidPath(session);
|
|
278
292
|
function cleanup() {
|
|
279
293
|
try {
|
|
280
294
|
unlinkSync(socketPath);
|
|
@@ -285,14 +299,49 @@ function cleanup() {
|
|
|
285
299
|
} catch {
|
|
286
300
|
}
|
|
287
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
|
+
}
|
|
288
313
|
function sendResponse(socket, response) {
|
|
289
314
|
try {
|
|
290
315
|
socket.write(JSON.stringify(response) + "\n");
|
|
291
316
|
} catch {
|
|
292
317
|
}
|
|
293
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
|
+
}
|
|
294
327
|
async function handleRequest(request, socket) {
|
|
295
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
|
+
}
|
|
296
345
|
if (command === "open") {
|
|
297
346
|
try {
|
|
298
347
|
const url = args.url;
|
|
@@ -300,6 +349,19 @@ async function handleRequest(request, socket) {
|
|
|
300
349
|
const connectUrl = args["connect-url"];
|
|
301
350
|
const channel = args.channel;
|
|
302
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;
|
|
303
365
|
if (instance && !launchPromise) {
|
|
304
366
|
try {
|
|
305
367
|
if (instance.page.isClosed()) {
|
|
@@ -311,7 +373,7 @@ async function handleRequest(request, socket) {
|
|
|
311
373
|
}
|
|
312
374
|
if (!instance) {
|
|
313
375
|
instance = new Opensteer({
|
|
314
|
-
name:
|
|
376
|
+
name: activeNamespace,
|
|
315
377
|
browser: {
|
|
316
378
|
headless: headless ?? false,
|
|
317
379
|
connectUrl,
|
|
@@ -343,8 +405,9 @@ async function handleRequest(request, socket) {
|
|
|
343
405
|
ok: true,
|
|
344
406
|
result: {
|
|
345
407
|
url: instance.page.url(),
|
|
346
|
-
|
|
347
|
-
name:
|
|
408
|
+
session,
|
|
409
|
+
name: activeNamespace,
|
|
410
|
+
remoteSessionId: instance.getRemoteSessionId() ?? void 0
|
|
348
411
|
}
|
|
349
412
|
});
|
|
350
413
|
} catch (err) {
|
|
@@ -374,10 +437,7 @@ async function handleRequest(request, socket) {
|
|
|
374
437
|
error: err instanceof Error ? err.message : String(err)
|
|
375
438
|
});
|
|
376
439
|
}
|
|
377
|
-
|
|
378
|
-
cleanup();
|
|
379
|
-
process.exit(0);
|
|
380
|
-
}, 100);
|
|
440
|
+
beginShutdown();
|
|
381
441
|
return;
|
|
382
442
|
}
|
|
383
443
|
if (command === "ping") {
|
|
@@ -388,7 +448,7 @@ async function handleRequest(request, socket) {
|
|
|
388
448
|
sendResponse(socket, {
|
|
389
449
|
id,
|
|
390
450
|
ok: false,
|
|
391
|
-
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.`
|
|
392
452
|
});
|
|
393
453
|
return;
|
|
394
454
|
}
|
|
@@ -425,7 +485,7 @@ var server = createServer((socket) => {
|
|
|
425
485
|
if (!line.trim()) continue;
|
|
426
486
|
try {
|
|
427
487
|
const request = JSON.parse(line);
|
|
428
|
-
|
|
488
|
+
enqueueRequest(request, socket);
|
|
429
489
|
} catch {
|
|
430
490
|
sendResponse(socket, {
|
|
431
491
|
id: 0,
|
|
@@ -450,6 +510,8 @@ server.on("error", (err) => {
|
|
|
450
510
|
process.exit(1);
|
|
451
511
|
});
|
|
452
512
|
async function shutdown() {
|
|
513
|
+
if (shuttingDown) return;
|
|
514
|
+
shuttingDown = true;
|
|
453
515
|
if (instance) {
|
|
454
516
|
try {
|
|
455
517
|
await instance.close();
|
|
@@ -457,9 +519,13 @@ async function shutdown() {
|
|
|
457
519
|
}
|
|
458
520
|
instance = null;
|
|
459
521
|
}
|
|
460
|
-
server.close();
|
|
461
522
|
cleanup();
|
|
462
|
-
|
|
523
|
+
server.close(() => {
|
|
524
|
+
process.exit(0);
|
|
525
|
+
});
|
|
526
|
+
setTimeout(() => {
|
|
527
|
+
process.exit(0);
|
|
528
|
+
}, 250).unref();
|
|
463
529
|
}
|
|
464
530
|
process.on("SIGTERM", shutdown);
|
|
465
531
|
process.on("SIGINT", shutdown);
|