opensteer 0.4.4 → 0.4.6
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 +372 -106
- package/dist/{chunk-2NKR4JZ6.js → chunk-MGZ3QEYT.js} +663 -99
- package/dist/cli/server.cjs +769 -139
- package/dist/cli/server.js +89 -23
- package/dist/index.cjs +663 -99
- package/dist/index.d.cts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +1 -1
- 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,166 @@ 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 (!pid) {
|
|
503
|
+
cleanStaleFiles(session, { removeSocket: true, removePid: false })
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return false
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function acquireStartLock(session) {
|
|
510
|
+
const lockPath = getLockPath(session)
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
const fd = openSync(lockPath, 'wx')
|
|
514
|
+
writeFileSync(
|
|
515
|
+
fd,
|
|
516
|
+
JSON.stringify({
|
|
517
|
+
pid: process.pid,
|
|
518
|
+
createdAt: Date.now(),
|
|
519
|
+
})
|
|
520
|
+
)
|
|
521
|
+
closeSync(fd)
|
|
522
|
+
return true
|
|
523
|
+
} catch {
|
|
524
|
+
return false
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function releaseStartLock(session) {
|
|
529
|
+
try {
|
|
530
|
+
unlinkSync(getLockPath(session))
|
|
531
|
+
} catch { /* best-effort */ }
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function recoverStaleStartLock(session) {
|
|
535
|
+
const lockPath = getLockPath(session)
|
|
536
|
+
if (!existsSync(lockPath)) {
|
|
537
|
+
return false
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
const raw = readFileSync(lockPath, 'utf-8')
|
|
542
|
+
const parsed = JSON.parse(raw)
|
|
543
|
+
const pid =
|
|
544
|
+
parsed && Number.isInteger(parsed.pid) && parsed.pid > 0
|
|
545
|
+
? parsed.pid
|
|
546
|
+
: null
|
|
547
|
+
|
|
548
|
+
if (!pid || !isPidAlive(pid)) {
|
|
549
|
+
unlinkSync(lockPath)
|
|
550
|
+
return true
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return false
|
|
554
|
+
} catch {
|
|
555
|
+
try {
|
|
556
|
+
unlinkSync(lockPath)
|
|
557
|
+
return true
|
|
558
|
+
} catch {
|
|
559
|
+
return false
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function sleep(ms) {
|
|
565
|
+
await new Promise((resolve) => setTimeout(resolve, ms))
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function waitForServerReady(session, timeout) {
|
|
569
|
+
const start = Date.now()
|
|
570
|
+
|
|
571
|
+
while (Date.now() - start <= timeout) {
|
|
572
|
+
if (await isServerHealthy(session)) {
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
await sleep(POLL_INTERVAL)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
throw new Error(`Timed out waiting for server '${session}' to become healthy.`)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function ensureServer(session) {
|
|
582
|
+
if (await isServerHealthy(session)) {
|
|
583
|
+
return
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (!existsSync(SERVER_SCRIPT)) {
|
|
587
|
+
throw new Error(
|
|
588
|
+
`Server script not found: ${SERVER_SCRIPT}. Run the build script first.`
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const deadline = Date.now() + CONNECT_TIMEOUT
|
|
593
|
+
|
|
594
|
+
while (Date.now() < deadline) {
|
|
595
|
+
if (await isServerHealthy(session)) {
|
|
596
|
+
return
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const existingPid = readPid(getPidPath(session))
|
|
600
|
+
if (existingPid && isPidAlive(existingPid)) {
|
|
601
|
+
await sleep(POLL_INTERVAL)
|
|
602
|
+
continue
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
recoverStaleStartLock(session)
|
|
606
|
+
|
|
607
|
+
if (acquireStartLock(session)) {
|
|
608
|
+
try {
|
|
609
|
+
if (!(await isServerHealthy(session))) {
|
|
610
|
+
startServer(session)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
await waitForServerReady(
|
|
614
|
+
session,
|
|
615
|
+
Math.max(500, deadline - Date.now())
|
|
616
|
+
)
|
|
617
|
+
return
|
|
618
|
+
} finally {
|
|
619
|
+
releaseStartLock(session)
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
await sleep(POLL_INTERVAL)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
throw new Error(
|
|
627
|
+
`Failed to start server for session '${session}'. Check that the build is complete.`
|
|
628
|
+
)
|
|
629
|
+
}
|
|
630
|
+
|
|
384
631
|
function listSessions() {
|
|
385
632
|
const sessions = []
|
|
386
633
|
const entries = readdirSync(tmpdir())
|
|
@@ -465,11 +712,11 @@ Navigation:
|
|
|
465
712
|
forward Go forward
|
|
466
713
|
reload Reload page
|
|
467
714
|
close Close browser and server
|
|
468
|
-
close --all Close all active
|
|
715
|
+
close --all Close all active session-scoped servers
|
|
469
716
|
|
|
470
717
|
Sessions:
|
|
471
|
-
sessions List active
|
|
472
|
-
status Show resolved
|
|
718
|
+
sessions List active session-scoped daemons
|
|
719
|
+
status Show resolved session/name and session state
|
|
473
720
|
|
|
474
721
|
Observation:
|
|
475
722
|
snapshot [--mode action] Get page snapshot
|
|
@@ -515,7 +762,8 @@ Utility:
|
|
|
515
762
|
extract <schema-json> Extract structured data
|
|
516
763
|
|
|
517
764
|
Global Flags:
|
|
518
|
-
--
|
|
765
|
+
--session <id> Runtime session id for daemon/browser routing
|
|
766
|
+
--name <namespace> Selector namespace for cache storage on 'open'
|
|
519
767
|
--headless Launch browser in headless mode
|
|
520
768
|
--connect-url <url> Connect to a running browser (e.g. http://localhost:9222)
|
|
521
769
|
--channel <browser> Use installed browser (chrome, chrome-beta, msedge)
|
|
@@ -527,7 +775,9 @@ Global Flags:
|
|
|
527
775
|
--version, -v Show version
|
|
528
776
|
|
|
529
777
|
Environment:
|
|
530
|
-
|
|
778
|
+
OPENSTEER_SESSION Runtime session id (equivalent to --session)
|
|
779
|
+
OPENSTEER_CLIENT_ID Stable client identity for default session binding
|
|
780
|
+
OPENSTEER_NAME Default selector namespace for 'open' when --name is omitted
|
|
531
781
|
OPENSTEER_MODE Runtime mode: "local" (default) or "remote"
|
|
532
782
|
OPENSTEER_API_KEY Required when remote mode is selected
|
|
533
783
|
OPENSTEER_BASE_URL Override remote control-plane base URL
|
|
@@ -536,26 +786,12 @@ Environment:
|
|
|
536
786
|
|
|
537
787
|
async function main() {
|
|
538
788
|
const { command, flags, positional } = parseArgs(process.argv)
|
|
539
|
-
const { namespace, source: namespaceSource } = resolveNamespace(flags)
|
|
540
|
-
const socketPath = getSocketPath(namespace)
|
|
541
789
|
|
|
542
790
|
if (command === 'sessions') {
|
|
543
791
|
output({ ok: true, sessions: listSessions() })
|
|
544
792
|
return
|
|
545
793
|
}
|
|
546
794
|
|
|
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
795
|
if (command === 'close' && flags.all === true) {
|
|
560
796
|
try {
|
|
561
797
|
const closed = await closeAllSessions()
|
|
@@ -566,36 +802,66 @@ async function main() {
|
|
|
566
802
|
return
|
|
567
803
|
}
|
|
568
804
|
|
|
805
|
+
let resolvedSession
|
|
806
|
+
let resolvedName
|
|
807
|
+
try {
|
|
808
|
+
resolvedSession = resolveSession(flags)
|
|
809
|
+
resolvedName = resolveName(flags, resolvedSession.session)
|
|
810
|
+
} catch (err) {
|
|
811
|
+
error(err instanceof Error ? err.message : 'Failed to resolve session')
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const session = resolvedSession.session
|
|
815
|
+
const sessionSource = resolvedSession.source
|
|
816
|
+
const name = resolvedName.name
|
|
817
|
+
const nameSource = resolvedName.source
|
|
818
|
+
const socketPath = getSocketPath(session)
|
|
819
|
+
|
|
820
|
+
if (command === 'status') {
|
|
821
|
+
output({
|
|
822
|
+
ok: true,
|
|
823
|
+
resolvedSession: session,
|
|
824
|
+
sessionSource,
|
|
825
|
+
resolvedName: name,
|
|
826
|
+
nameSource,
|
|
827
|
+
serverRunning: await isServerHealthy(session),
|
|
828
|
+
socketPath,
|
|
829
|
+
sessions: listSessions(),
|
|
830
|
+
})
|
|
831
|
+
return
|
|
832
|
+
}
|
|
833
|
+
|
|
569
834
|
delete flags.name
|
|
835
|
+
delete flags.session
|
|
570
836
|
delete flags.all
|
|
837
|
+
|
|
571
838
|
const request = buildRequest(command, flags, positional)
|
|
839
|
+
if (command === 'open') {
|
|
840
|
+
request.args.name = name
|
|
841
|
+
}
|
|
572
842
|
|
|
573
|
-
if (!
|
|
843
|
+
if (!(await isServerHealthy(session))) {
|
|
574
844
|
if (command !== 'open') {
|
|
575
845
|
error(
|
|
576
|
-
`No server running for
|
|
846
|
+
`No server running for session '${session}' (resolved from ${sessionSource}). Run 'opensteer open' first or use 'opensteer sessions' to see active sessions.`
|
|
577
847
|
)
|
|
578
848
|
}
|
|
579
|
-
|
|
849
|
+
|
|
850
|
+
try {
|
|
851
|
+
await ensureServer(session)
|
|
852
|
+
} catch (err) {
|
|
580
853
|
error(
|
|
581
|
-
|
|
854
|
+
err instanceof Error
|
|
855
|
+
? err.message
|
|
856
|
+
: 'Failed to start server. Check that the build is complete.'
|
|
582
857
|
)
|
|
583
858
|
}
|
|
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
859
|
}
|
|
591
860
|
|
|
592
861
|
try {
|
|
593
862
|
const response = await sendCommand(socketPath, request)
|
|
594
863
|
|
|
595
864
|
if (response.ok) {
|
|
596
|
-
if (command === 'open') {
|
|
597
|
-
writeActiveNamespace(namespace)
|
|
598
|
-
}
|
|
599
865
|
output({ ok: true, ...response.result })
|
|
600
866
|
} else {
|
|
601
867
|
process.stderr.write(
|