opensteer 0.2.0 → 0.4.0
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/bin/opensteer.mjs +251 -28
- package/dist/cli/server.cjs +58 -46
- package/dist/cli/server.js +27 -18
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/package.json +3 -2
- package/dist/chunk-6L24FEKD.js +0 -9233
package/bin/opensteer.mjs
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { createHash } from 'crypto'
|
|
4
4
|
import { spawn } from 'child_process'
|
|
5
|
-
import { existsSync, readFileSync, unlinkSync,
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'fs'
|
|
6
|
+
import { connect } from 'net'
|
|
7
|
+
import { tmpdir } from 'os'
|
|
8
|
+
import { basename, dirname, join } from 'path'
|
|
8
9
|
import { fileURLToPath } from 'url'
|
|
9
10
|
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
11
|
-
|
|
12
|
-
const RUNTIME_DIR = join(homedir(), '.opensteer')
|
|
13
|
-
const SOCKET_PATH = join(RUNTIME_DIR, 'opensteer.sock')
|
|
14
|
-
const PID_PATH = join(RUNTIME_DIR, 'opensteer.pid')
|
|
15
12
|
const SERVER_SCRIPT = join(__dirname, '..', 'dist', 'cli', 'server.js')
|
|
16
13
|
|
|
17
14
|
const CONNECT_TIMEOUT = 15000
|
|
18
15
|
const POLL_INTERVAL = 100
|
|
19
16
|
const RESPONSE_TIMEOUT = 120000
|
|
17
|
+
const RUNTIME_PREFIX = 'opensteer-'
|
|
18
|
+
const SOCKET_SUFFIX = '.sock'
|
|
19
|
+
const PID_SUFFIX = '.pid'
|
|
20
|
+
const CLOSE_ALL_REQUEST = { id: 1, command: 'close', args: {} }
|
|
21
|
+
|
|
22
|
+
function getVersion() {
|
|
23
|
+
try {
|
|
24
|
+
const pkgPath = join(__dirname, '..', 'package.json')
|
|
25
|
+
return JSON.parse(readFileSync(pkgPath, 'utf-8')).version
|
|
26
|
+
} catch {
|
|
27
|
+
return 'unknown'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
20
30
|
|
|
21
31
|
function parseArgs(argv) {
|
|
22
32
|
const args = argv.slice(2)
|
|
@@ -25,6 +35,11 @@ function parseArgs(argv) {
|
|
|
25
35
|
process.exit(0)
|
|
26
36
|
}
|
|
27
37
|
|
|
38
|
+
if (args[0] === '--version' || args[0] === '-v') {
|
|
39
|
+
console.log(getVersion())
|
|
40
|
+
process.exit(0)
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
const command = args[0]
|
|
29
44
|
const flags = {}
|
|
30
45
|
const positional = []
|
|
@@ -56,10 +71,78 @@ function parseValue(str) {
|
|
|
56
71
|
return str
|
|
57
72
|
}
|
|
58
73
|
|
|
74
|
+
function sanitizeNamespace(value) {
|
|
75
|
+
const trimmed = String(value || '').trim()
|
|
76
|
+
if (!trimmed || trimmed === '.' || trimmed === '..') {
|
|
77
|
+
return 'default'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const replaced = trimmed.replace(/[^a-zA-Z0-9_-]+/g, '_')
|
|
81
|
+
const collapsed = replaced.replace(/_+/g, '_')
|
|
82
|
+
const bounded = collapsed.replace(/^_+|_+$/g, '')
|
|
83
|
+
|
|
84
|
+
return bounded || 'default'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getActiveNamespacePath() {
|
|
88
|
+
const hash = createHash('md5').update(process.cwd()).digest('hex').slice(0, 16)
|
|
89
|
+
return join(tmpdir(), `${RUNTIME_PREFIX}active-${hash}`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readActiveNamespace() {
|
|
93
|
+
try {
|
|
94
|
+
const filePath = getActiveNamespacePath()
|
|
95
|
+
if (!existsSync(filePath)) return null
|
|
96
|
+
const ns = readFileSync(filePath, 'utf-8').trim()
|
|
97
|
+
return ns || null
|
|
98
|
+
} catch {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function writeActiveNamespace(namespace) {
|
|
104
|
+
try {
|
|
105
|
+
writeFileSync(getActiveNamespacePath(), namespace)
|
|
106
|
+
} catch { /* best-effort */ }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveNamespace(flags) {
|
|
110
|
+
if (flags.name !== undefined && String(flags.name).trim().length > 0) {
|
|
111
|
+
return { namespace: sanitizeNamespace(String(flags.name)), source: 'flag' }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (
|
|
115
|
+
typeof process.env.OPENSTEER_NAME === 'string' &&
|
|
116
|
+
process.env.OPENSTEER_NAME.trim().length > 0
|
|
117
|
+
) {
|
|
118
|
+
return { namespace: sanitizeNamespace(process.env.OPENSTEER_NAME), source: 'env' }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const active = readActiveNamespace()
|
|
122
|
+
if (active && isServerRunning(active)) {
|
|
123
|
+
return { namespace: active, source: 'active' }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const cwdBase = basename(process.cwd())
|
|
127
|
+
if (cwdBase && cwdBase !== '.' && cwdBase !== '/') {
|
|
128
|
+
return { namespace: sanitizeNamespace(cwdBase), source: 'cwd' }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { namespace: 'default', source: 'default' }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getSocketPath(namespace) {
|
|
135
|
+
return join(tmpdir(), `${RUNTIME_PREFIX}${namespace}${SOCKET_SUFFIX}`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getPidPath(namespace) {
|
|
139
|
+
return join(tmpdir(), `${RUNTIME_PREFIX}${namespace}${PID_SUFFIX}`)
|
|
140
|
+
}
|
|
141
|
+
|
|
59
142
|
function buildRequest(command, flags, positional) {
|
|
60
143
|
const id = 1
|
|
61
144
|
const globalFlags = {}
|
|
62
|
-
for (const key of ['
|
|
145
|
+
for (const key of ['headless', 'json', 'connect-url', 'channel', 'profile-dir']) {
|
|
63
146
|
if (key in flags) {
|
|
64
147
|
globalFlags[key] = flags[key]
|
|
65
148
|
delete flags[key]
|
|
@@ -165,38 +248,66 @@ function buildRequest(command, flags, positional) {
|
|
|
165
248
|
return { id, command, args }
|
|
166
249
|
}
|
|
167
250
|
|
|
168
|
-
function
|
|
169
|
-
if (!existsSync(
|
|
251
|
+
function readPid(pidPath) {
|
|
252
|
+
if (!existsSync(pidPath)) {
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const parsed = Number.parseInt(readFileSync(pidPath, 'utf-8').trim(), 10)
|
|
257
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
258
|
+
return null
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return parsed
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isPidAlive(pid) {
|
|
170
265
|
try {
|
|
171
|
-
const pid = parseInt(readFileSync(PID_PATH, 'utf-8').trim(), 10)
|
|
172
266
|
process.kill(pid, 0)
|
|
173
267
|
return true
|
|
174
268
|
} catch {
|
|
175
|
-
cleanStaleFiles()
|
|
176
269
|
return false
|
|
177
270
|
}
|
|
178
271
|
}
|
|
179
272
|
|
|
180
|
-
function cleanStaleFiles() {
|
|
273
|
+
function cleanStaleFiles(namespace) {
|
|
181
274
|
try {
|
|
182
|
-
unlinkSync(
|
|
275
|
+
unlinkSync(getSocketPath(namespace))
|
|
183
276
|
} catch { }
|
|
184
277
|
try {
|
|
185
|
-
unlinkSync(
|
|
278
|
+
unlinkSync(getPidPath(namespace))
|
|
186
279
|
} catch { }
|
|
187
280
|
}
|
|
188
281
|
|
|
189
|
-
function
|
|
190
|
-
|
|
282
|
+
function isServerRunning(namespace) {
|
|
283
|
+
const pidPath = getPidPath(namespace)
|
|
284
|
+
const pid = readPid(pidPath)
|
|
285
|
+
if (!pid) {
|
|
286
|
+
cleanStaleFiles(namespace)
|
|
287
|
+
return false
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!isPidAlive(pid)) {
|
|
291
|
+
cleanStaleFiles(namespace)
|
|
292
|
+
return false
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return existsSync(getSocketPath(namespace))
|
|
296
|
+
}
|
|
191
297
|
|
|
298
|
+
function startServer(namespace) {
|
|
192
299
|
const child = spawn('node', [SERVER_SCRIPT], {
|
|
193
300
|
detached: true,
|
|
194
301
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
302
|
+
env: {
|
|
303
|
+
...process.env,
|
|
304
|
+
OPENSTEER_NAME: namespace,
|
|
305
|
+
},
|
|
195
306
|
})
|
|
196
307
|
child.unref()
|
|
197
308
|
}
|
|
198
309
|
|
|
199
|
-
function waitForSocket(timeout) {
|
|
310
|
+
function waitForSocket(socketPath, timeout) {
|
|
200
311
|
return new Promise((resolve, reject) => {
|
|
201
312
|
const start = Date.now()
|
|
202
313
|
|
|
@@ -206,7 +317,7 @@ function waitForSocket(timeout) {
|
|
|
206
317
|
return
|
|
207
318
|
}
|
|
208
319
|
|
|
209
|
-
if (existsSync(
|
|
320
|
+
if (existsSync(socketPath)) {
|
|
210
321
|
resolve()
|
|
211
322
|
return
|
|
212
323
|
}
|
|
@@ -218,9 +329,9 @@ function waitForSocket(timeout) {
|
|
|
218
329
|
})
|
|
219
330
|
}
|
|
220
331
|
|
|
221
|
-
function sendCommand(request) {
|
|
332
|
+
function sendCommand(socketPath, request) {
|
|
222
333
|
return new Promise((resolve, reject) => {
|
|
223
|
-
const socket = connect(
|
|
334
|
+
const socket = connect(socketPath)
|
|
224
335
|
let buffer = ''
|
|
225
336
|
let settled = false
|
|
226
337
|
|
|
@@ -270,6 +381,71 @@ function sendCommand(request) {
|
|
|
270
381
|
})
|
|
271
382
|
}
|
|
272
383
|
|
|
384
|
+
function listSessions() {
|
|
385
|
+
const sessions = []
|
|
386
|
+
const entries = readdirSync(tmpdir())
|
|
387
|
+
|
|
388
|
+
for (const entry of entries) {
|
|
389
|
+
if (!entry.startsWith(RUNTIME_PREFIX) || !entry.endsWith(PID_SUFFIX)) {
|
|
390
|
+
continue
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const name = entry.slice(
|
|
394
|
+
RUNTIME_PREFIX.length,
|
|
395
|
+
entry.length - PID_SUFFIX.length
|
|
396
|
+
)
|
|
397
|
+
if (!name) {
|
|
398
|
+
continue
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const pid = readPid(join(tmpdir(), entry))
|
|
402
|
+
if (!pid || !isPidAlive(pid)) {
|
|
403
|
+
cleanStaleFiles(name)
|
|
404
|
+
continue
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
sessions.push({ name, pid })
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
sessions.sort((a, b) => a.name.localeCompare(b.name))
|
|
411
|
+
return sessions
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function closeAllSessions() {
|
|
415
|
+
const sessions = listSessions()
|
|
416
|
+
const closed = []
|
|
417
|
+
const failures = []
|
|
418
|
+
|
|
419
|
+
for (const session of sessions) {
|
|
420
|
+
const socketPath = getSocketPath(session.name)
|
|
421
|
+
if (!existsSync(socketPath)) {
|
|
422
|
+
cleanStaleFiles(session.name)
|
|
423
|
+
continue
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const response = await sendCommand(socketPath, CLOSE_ALL_REQUEST)
|
|
428
|
+
if (response && response.ok === true) {
|
|
429
|
+
closed.push(session)
|
|
430
|
+
} else {
|
|
431
|
+
failures.push(
|
|
432
|
+
`${session.name}: ${response?.error || 'unknown close error'}`
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
} catch (err) {
|
|
436
|
+
failures.push(
|
|
437
|
+
`${session.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (failures.length > 0) {
|
|
443
|
+
throw new Error(`Failed to close sessions: ${failures.join('; ')}`)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return closed
|
|
447
|
+
}
|
|
448
|
+
|
|
273
449
|
function output(data) {
|
|
274
450
|
process.stdout.write(JSON.stringify(data) + '\n')
|
|
275
451
|
}
|
|
@@ -289,6 +465,11 @@ Navigation:
|
|
|
289
465
|
forward Go forward
|
|
290
466
|
reload Reload page
|
|
291
467
|
close Close browser and server
|
|
468
|
+
close --all Close all active namespace-scoped servers
|
|
469
|
+
|
|
470
|
+
Sessions:
|
|
471
|
+
sessions List active namespace-scoped sessions
|
|
472
|
+
status Show resolved namespace and session state
|
|
292
473
|
|
|
293
474
|
Observation:
|
|
294
475
|
snapshot [--mode action] Get page snapshot
|
|
@@ -334,7 +515,7 @@ Utility:
|
|
|
334
515
|
extract <schema-json> Extract structured data
|
|
335
516
|
|
|
336
517
|
Global Flags:
|
|
337
|
-
--name <namespace>
|
|
518
|
+
--name <namespace> Session namespace (default: CWD basename or OPENSTEER_NAME)
|
|
338
519
|
--headless Launch browser in headless mode
|
|
339
520
|
--connect-url <url> Connect to a running browser (e.g. http://localhost:9222)
|
|
340
521
|
--channel <browser> Use installed browser (chrome, chrome-beta, msedge)
|
|
@@ -343,8 +524,10 @@ Global Flags:
|
|
|
343
524
|
--selector <css> Target element by CSS selector
|
|
344
525
|
--description <text> Description for selector persistence
|
|
345
526
|
--help Show this help
|
|
527
|
+
--version, -v Show version
|
|
346
528
|
|
|
347
529
|
Environment:
|
|
530
|
+
OPENSTEER_NAME Default session namespace when --name is omitted
|
|
348
531
|
OPENSTEER_MODE Runtime mode: "local" (default) or "remote"
|
|
349
532
|
OPENSTEER_API_KEY Required when remote mode is selected
|
|
350
533
|
OPENSTEER_BASE_URL Override remote control-plane base URL
|
|
@@ -353,26 +536,66 @@ Environment:
|
|
|
353
536
|
|
|
354
537
|
async function main() {
|
|
355
538
|
const { command, flags, positional } = parseArgs(process.argv)
|
|
539
|
+
const { namespace, source: namespaceSource } = resolveNamespace(flags)
|
|
540
|
+
const socketPath = getSocketPath(namespace)
|
|
541
|
+
|
|
542
|
+
if (command === 'sessions') {
|
|
543
|
+
output({ ok: true, sessions: listSessions() })
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
|
|
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
|
+
if (command === 'close' && flags.all === true) {
|
|
560
|
+
try {
|
|
561
|
+
const closed = await closeAllSessions()
|
|
562
|
+
output({ ok: true, closed })
|
|
563
|
+
} catch (err) {
|
|
564
|
+
error(err instanceof Error ? err.message : 'Failed to close sessions')
|
|
565
|
+
}
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
delete flags.name
|
|
570
|
+
delete flags.all
|
|
356
571
|
const request = buildRequest(command, flags, positional)
|
|
357
572
|
|
|
358
|
-
if (!isServerRunning()) {
|
|
573
|
+
if (!isServerRunning(namespace)) {
|
|
574
|
+
if (command !== 'open') {
|
|
575
|
+
error(
|
|
576
|
+
`No server running for namespace '${namespace}' (resolved from ${namespaceSource}). Run 'opensteer open' first or use 'opensteer sessions' to see active sessions.`
|
|
577
|
+
)
|
|
578
|
+
}
|
|
359
579
|
if (!existsSync(SERVER_SCRIPT)) {
|
|
360
580
|
error(
|
|
361
581
|
`Server script not found: ${SERVER_SCRIPT}. Run the build script first.`
|
|
362
582
|
)
|
|
363
583
|
}
|
|
364
|
-
startServer()
|
|
584
|
+
startServer(namespace)
|
|
365
585
|
try {
|
|
366
|
-
await waitForSocket(CONNECT_TIMEOUT)
|
|
586
|
+
await waitForSocket(socketPath, CONNECT_TIMEOUT)
|
|
367
587
|
} catch {
|
|
368
588
|
error('Failed to start server. Check that the build is complete.')
|
|
369
589
|
}
|
|
370
590
|
}
|
|
371
591
|
|
|
372
592
|
try {
|
|
373
|
-
const response = await sendCommand(request)
|
|
593
|
+
const response = await sendCommand(socketPath, request)
|
|
374
594
|
|
|
375
595
|
if (response.ok) {
|
|
596
|
+
if (command === 'open') {
|
|
597
|
+
writeActiveNamespace(namespace)
|
|
598
|
+
}
|
|
376
599
|
output({ ok: true, ...response.result })
|
|
377
600
|
} else {
|
|
378
601
|
process.stderr.write(
|
|
@@ -381,7 +604,7 @@ async function main() {
|
|
|
381
604
|
process.exit(1)
|
|
382
605
|
}
|
|
383
606
|
} catch (err) {
|
|
384
|
-
error(err.message
|
|
607
|
+
error(err instanceof Error ? err.message : 'Connection failed')
|
|
385
608
|
}
|
|
386
609
|
}
|
|
387
610
|
|