opensteer 0.4.0 → 0.4.2
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 +28 -251
- package/dist/chunk-6L24FEKD.js +9233 -0
- package/dist/chunk-OXFW5I3Y.js +7679 -0
- package/dist/cli/server.cjs +46 -58
- package/dist/cli/server.js +18 -27
- package/dist/index.cjs +0 -3
- package/dist/index.d.cts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -1
- package/package.json +2 -3
package/bin/opensteer.mjs
CHANGED
|
@@ -1,32 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { createHash } from 'crypto'
|
|
4
|
-
import { spawn } from 'child_process'
|
|
5
|
-
import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'fs'
|
|
6
3
|
import { connect } from 'net'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
4
|
+
import { spawn } from 'child_process'
|
|
5
|
+
import { existsSync, readFileSync, unlinkSync, mkdirSync } from 'fs'
|
|
6
|
+
import { join, dirname } from 'path'
|
|
7
|
+
import { homedir } from 'os'
|
|
9
8
|
import { fileURLToPath } from 'url'
|
|
10
9
|
|
|
11
10
|
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')
|
|
12
15
|
const SERVER_SCRIPT = join(__dirname, '..', 'dist', 'cli', 'server.js')
|
|
13
16
|
|
|
14
17
|
const CONNECT_TIMEOUT = 15000
|
|
15
18
|
const POLL_INTERVAL = 100
|
|
16
19
|
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
|
-
}
|
|
30
20
|
|
|
31
21
|
function parseArgs(argv) {
|
|
32
22
|
const args = argv.slice(2)
|
|
@@ -35,11 +25,6 @@ function parseArgs(argv) {
|
|
|
35
25
|
process.exit(0)
|
|
36
26
|
}
|
|
37
27
|
|
|
38
|
-
if (args[0] === '--version' || args[0] === '-v') {
|
|
39
|
-
console.log(getVersion())
|
|
40
|
-
process.exit(0)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
28
|
const command = args[0]
|
|
44
29
|
const flags = {}
|
|
45
30
|
const positional = []
|
|
@@ -71,78 +56,10 @@ function parseValue(str) {
|
|
|
71
56
|
return str
|
|
72
57
|
}
|
|
73
58
|
|
|
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
|
-
|
|
142
59
|
function buildRequest(command, flags, positional) {
|
|
143
60
|
const id = 1
|
|
144
61
|
const globalFlags = {}
|
|
145
|
-
for (const key of ['headless', 'json', 'connect-url', 'channel', 'profile-dir']) {
|
|
62
|
+
for (const key of ['name', 'headless', 'json', 'connect-url', 'channel', 'profile-dir']) {
|
|
146
63
|
if (key in flags) {
|
|
147
64
|
globalFlags[key] = flags[key]
|
|
148
65
|
delete flags[key]
|
|
@@ -248,66 +165,38 @@ function buildRequest(command, flags, positional) {
|
|
|
248
165
|
return { id, command, args }
|
|
249
166
|
}
|
|
250
167
|
|
|
251
|
-
function
|
|
252
|
-
if (!existsSync(
|
|
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) {
|
|
168
|
+
function isServerRunning() {
|
|
169
|
+
if (!existsSync(PID_PATH)) return false
|
|
265
170
|
try {
|
|
171
|
+
const pid = parseInt(readFileSync(PID_PATH, 'utf-8').trim(), 10)
|
|
266
172
|
process.kill(pid, 0)
|
|
267
173
|
return true
|
|
268
174
|
} catch {
|
|
175
|
+
cleanStaleFiles()
|
|
269
176
|
return false
|
|
270
177
|
}
|
|
271
178
|
}
|
|
272
179
|
|
|
273
|
-
function cleanStaleFiles(
|
|
180
|
+
function cleanStaleFiles() {
|
|
274
181
|
try {
|
|
275
|
-
unlinkSync(
|
|
182
|
+
unlinkSync(SOCKET_PATH)
|
|
276
183
|
} catch { }
|
|
277
184
|
try {
|
|
278
|
-
unlinkSync(
|
|
185
|
+
unlinkSync(PID_PATH)
|
|
279
186
|
} catch { }
|
|
280
187
|
}
|
|
281
188
|
|
|
282
|
-
function
|
|
283
|
-
|
|
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
|
-
}
|
|
189
|
+
function startServer() {
|
|
190
|
+
mkdirSync(RUNTIME_DIR, { recursive: true })
|
|
297
191
|
|
|
298
|
-
function startServer(namespace) {
|
|
299
192
|
const child = spawn('node', [SERVER_SCRIPT], {
|
|
300
193
|
detached: true,
|
|
301
194
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
302
|
-
env: {
|
|
303
|
-
...process.env,
|
|
304
|
-
OPENSTEER_NAME: namespace,
|
|
305
|
-
},
|
|
306
195
|
})
|
|
307
196
|
child.unref()
|
|
308
197
|
}
|
|
309
198
|
|
|
310
|
-
function waitForSocket(
|
|
199
|
+
function waitForSocket(timeout) {
|
|
311
200
|
return new Promise((resolve, reject) => {
|
|
312
201
|
const start = Date.now()
|
|
313
202
|
|
|
@@ -317,7 +206,7 @@ function waitForSocket(socketPath, timeout) {
|
|
|
317
206
|
return
|
|
318
207
|
}
|
|
319
208
|
|
|
320
|
-
if (existsSync(
|
|
209
|
+
if (existsSync(SOCKET_PATH)) {
|
|
321
210
|
resolve()
|
|
322
211
|
return
|
|
323
212
|
}
|
|
@@ -329,9 +218,9 @@ function waitForSocket(socketPath, timeout) {
|
|
|
329
218
|
})
|
|
330
219
|
}
|
|
331
220
|
|
|
332
|
-
function sendCommand(
|
|
221
|
+
function sendCommand(request) {
|
|
333
222
|
return new Promise((resolve, reject) => {
|
|
334
|
-
const socket = connect(
|
|
223
|
+
const socket = connect(SOCKET_PATH)
|
|
335
224
|
let buffer = ''
|
|
336
225
|
let settled = false
|
|
337
226
|
|
|
@@ -381,71 +270,6 @@ function sendCommand(socketPath, request) {
|
|
|
381
270
|
})
|
|
382
271
|
}
|
|
383
272
|
|
|
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
|
-
|
|
449
273
|
function output(data) {
|
|
450
274
|
process.stdout.write(JSON.stringify(data) + '\n')
|
|
451
275
|
}
|
|
@@ -465,11 +289,6 @@ Navigation:
|
|
|
465
289
|
forward Go forward
|
|
466
290
|
reload Reload page
|
|
467
291
|
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
|
|
473
292
|
|
|
474
293
|
Observation:
|
|
475
294
|
snapshot [--mode action] Get page snapshot
|
|
@@ -515,7 +334,7 @@ Utility:
|
|
|
515
334
|
extract <schema-json> Extract structured data
|
|
516
335
|
|
|
517
336
|
Global Flags:
|
|
518
|
-
--name <namespace>
|
|
337
|
+
--name <namespace> Storage namespace (default: "cli")
|
|
519
338
|
--headless Launch browser in headless mode
|
|
520
339
|
--connect-url <url> Connect to a running browser (e.g. http://localhost:9222)
|
|
521
340
|
--channel <browser> Use installed browser (chrome, chrome-beta, msedge)
|
|
@@ -524,10 +343,8 @@ Global Flags:
|
|
|
524
343
|
--selector <css> Target element by CSS selector
|
|
525
344
|
--description <text> Description for selector persistence
|
|
526
345
|
--help Show this help
|
|
527
|
-
--version, -v Show version
|
|
528
346
|
|
|
529
347
|
Environment:
|
|
530
|
-
OPENSTEER_NAME Default session namespace when --name is omitted
|
|
531
348
|
OPENSTEER_MODE Runtime mode: "local" (default) or "remote"
|
|
532
349
|
OPENSTEER_API_KEY Required when remote mode is selected
|
|
533
350
|
OPENSTEER_BASE_URL Override remote control-plane base URL
|
|
@@ -536,66 +353,26 @@ Environment:
|
|
|
536
353
|
|
|
537
354
|
async function main() {
|
|
538
355
|
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
|
|
571
356
|
const request = buildRequest(command, flags, positional)
|
|
572
357
|
|
|
573
|
-
if (!isServerRunning(
|
|
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
|
-
}
|
|
358
|
+
if (!isServerRunning()) {
|
|
579
359
|
if (!existsSync(SERVER_SCRIPT)) {
|
|
580
360
|
error(
|
|
581
361
|
`Server script not found: ${SERVER_SCRIPT}. Run the build script first.`
|
|
582
362
|
)
|
|
583
363
|
}
|
|
584
|
-
startServer(
|
|
364
|
+
startServer()
|
|
585
365
|
try {
|
|
586
|
-
await waitForSocket(
|
|
366
|
+
await waitForSocket(CONNECT_TIMEOUT)
|
|
587
367
|
} catch {
|
|
588
368
|
error('Failed to start server. Check that the build is complete.')
|
|
589
369
|
}
|
|
590
370
|
}
|
|
591
371
|
|
|
592
372
|
try {
|
|
593
|
-
const response = await sendCommand(
|
|
373
|
+
const response = await sendCommand(request)
|
|
594
374
|
|
|
595
375
|
if (response.ok) {
|
|
596
|
-
if (command === 'open') {
|
|
597
|
-
writeActiveNamespace(namespace)
|
|
598
|
-
}
|
|
599
376
|
output({ ok: true, ...response.result })
|
|
600
377
|
} else {
|
|
601
378
|
process.stderr.write(
|
|
@@ -604,7 +381,7 @@ async function main() {
|
|
|
604
381
|
process.exit(1)
|
|
605
382
|
}
|
|
606
383
|
} catch (err) {
|
|
607
|
-
error(err
|
|
384
|
+
error(err.message || 'Connection failed')
|
|
608
385
|
}
|
|
609
386
|
}
|
|
610
387
|
|