openrod 0.1.1 → 0.1.2-rc.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/README.md +13 -9
- package/dist/assets/index-89TNohi_.css +1 -0
- package/dist/assets/index-DoKK2G9s.js +545 -0
- package/dist/assets/{terminal-view-Bqw3jQjI.js → terminal-view-DzqQnmh2.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/logos/SOURCES.md +0 -14
- package/package.json +1 -1
- package/server/api.js +57 -10
- package/server/cli.js +32 -11
- package/server/gateway-docker.js +499 -0
- package/server/gateway.js +8 -0
- package/server/image-templates.js +36 -12
- package/server/launch-token.js +23 -3
- package/server/openshell-cli.js +1 -1
- package/server/openshell-install.js +189 -0
- package/server/policy.js +3 -5
- package/server/remote-gateway.js +11 -4
- package/shared/openshell-release.js +13 -0
- package/src/lib/api.js +4 -1
- package/src/lib/gateway-docker-store.js +5 -0
- package/src/lib/gateway-docker.js +77 -0
- package/src/lib/sandbox-creations.js +3 -2
- package/src/lib/services.js +1 -12
- package/dist/assets/index-DbxTgFt9.css +0 -1
- package/dist/assets/index-Ddo8D1tE.js +0 -545
- package/dist/logos/anthropic.svg +0 -1
- package/dist/logos/aws-bedrock.svg +0 -1
- package/dist/logos/aws-s3.svg +0 -1
- package/dist/logos/aws.svg +0 -1
- package/dist/logos/deepinfra.svg +0 -1
- package/dist/logos/google-cloud.svg +0 -1
- package/dist/logos/google-vertex-ai.svg +0 -1
- package/dist/logos/nvidia.svg +0 -1
- package/dist/logos/pypi.svg +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{u as Jr,a as Zr,r as Te,d as Qr,s as as,b as en,j as U,p as hs,T as tn,L as sn,B as ls,R as rn,D as nn,c as on,C as an,e as hn,f as ln,t as cn}from"./index-
|
|
1
|
+
import{u as Jr,a as Zr,r as Te,d as Qr,s as as,b as en,j as U,p as hs,T as tn,L as sn,B as ls,R as rn,D as nn,c as on,C as an,e as hn,f as ln,t as cn}from"./index-DoKK2G9s.js";/**
|
|
2
2
|
* Copyright (c) 2014-2024 The xterm.js authors. All rights reserved.
|
|
3
3
|
* @license MIT
|
|
4
4
|
*
|
package/dist/index.html
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
<meta name="theme-color" content="#ffffff" />
|
|
8
8
|
<meta name="description" content="OpenShell control plane - sandboxes, approvals and live activity" />
|
|
9
9
|
<title>OpenRod</title>
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-DoKK2G9s.js"></script>
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-89TNohi_.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<div id="root"></div>
|
package/dist/logos/SOURCES.md
CHANGED
|
@@ -6,7 +6,6 @@ Every SVG in this directory is bundled locally so the console never contacts a l
|
|
|
6
6
|
| --- | --- | --- |
|
|
7
7
|
| Lobe Icons (`@lobehub/icons-static-svg` 1.95.1) | MIT, Copyright (c) 2023 LobeHub | https://github.com/lobehub/lobe-icons |
|
|
8
8
|
| GitHub Octicons (`@primer/octicons` 19.38.0) | MIT, Copyright (c) 2026 GitHub Inc. | https://github.com/primer/octicons |
|
|
9
|
-
| Simple Icons (`simple-icons`) | CC0-1.0 | https://github.com/simple-icons/simple-icons |
|
|
10
9
|
| Devicon (`devicon` 2.17.0) | MIT, Copyright (c) 2015 konpa | https://github.com/devicons/devicon |
|
|
11
10
|
| OpenRod authors | Apache-2.0 (this project) | - |
|
|
12
11
|
|
|
@@ -14,22 +13,13 @@ Every SVG in this directory is bundled locally so the console never contacts a l
|
|
|
14
13
|
|
|
15
14
|
| File | Source | Upstream icon |
|
|
16
15
|
| --- | --- | --- |
|
|
17
|
-
| `anthropic.svg` | Simple Icons | `anthropic` |
|
|
18
|
-
| `aws.svg` | Lobe Icons | `aws-color` |
|
|
19
|
-
| `aws-bedrock.svg` | Lobe Icons | `bedrock-color` |
|
|
20
|
-
| `aws-s3.svg` | OpenRod authors | neutral storage icon |
|
|
21
16
|
| `claude-code.svg` | Lobe Icons | `claudecode-color` |
|
|
22
17
|
| `codex.svg` | Lobe Icons | `codex-color` |
|
|
23
18
|
| `copilot.svg` | GitHub Octicons | `copilot-24` |
|
|
24
19
|
| `cursor.svg` | Lobe Icons | `cursor` |
|
|
25
|
-
| `deepinfra.svg` | Lobe Icons | `deepinfra-color` |
|
|
26
20
|
| `github.svg` | GitHub Octicons | `mark-github-24` |
|
|
27
|
-
| `google-cloud.svg` | Simple Icons | `googlecloud` |
|
|
28
|
-
| `google-vertex-ai.svg` | Lobe Icons | `vertexai-color` |
|
|
29
|
-
| `nvidia.svg` | Lobe Icons | `nvidia-color` |
|
|
30
21
|
| `openai.svg` | Lobe Icons | `openai` |
|
|
31
22
|
| `openrouter.svg` | Lobe Icons | `openrouter-color` |
|
|
32
|
-
| `pypi.svg` | Simple Icons | `pypi` |
|
|
33
23
|
| `vscode.svg` | Devicon | `vscode` |
|
|
34
24
|
| `agents/aider.svg` | OpenRod authors | neutral terminal icon |
|
|
35
25
|
| `agents/antigravity.svg` | Lobe Icons | `antigravity-color` |
|
|
@@ -127,7 +117,3 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
|
127
117
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
128
118
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
129
119
|
```
|
|
130
|
-
|
|
131
|
-
### Simple Icons
|
|
132
|
-
|
|
133
|
-
Simple Icons are released under CC0-1.0 (https://creativecommons.org/publicdomain/zero/1.0/).
|
package/package.json
CHANGED
package/server/api.js
CHANGED
|
@@ -6,7 +6,7 @@ import { setupTargetsFor, validateSetupTargets } from '../shared/setup-targets.j
|
|
|
6
6
|
import { localTransfer, importTransfer, exportTransfer } from './cloud-transfer.js'
|
|
7
7
|
import path from 'node:path'
|
|
8
8
|
import { Readable } from 'node:stream'
|
|
9
|
-
import { scopedStateDirectory } from './paths.js'
|
|
9
|
+
import { scopedStateDirectory, stateDirectory } from './paths.js'
|
|
10
10
|
import { createActivityStore } from './activity-store.js'
|
|
11
11
|
import { createActivityDelivery } from './activity-delivery.js'
|
|
12
12
|
import { exportEvent } from '../src/lib/activity-export.js'
|
|
@@ -15,14 +15,16 @@ import { randomUUID } from 'node:crypto'
|
|
|
15
15
|
import { imageTemplateLabels, nameSandboxImages } from '../src/lib/sandbox-images.js'
|
|
16
16
|
import { PROJECT_LABEL, templateSession, isSession, sessionLaunch, persistentTerminalPolicy, persistentGateway, PERSISTENT_TERMINAL_LABEL } from '../src/lib/sandbox-session.js'
|
|
17
17
|
import { sandboxIdentityLabels } from './sandbox-identity.js'
|
|
18
|
-
import { consoleContext, contextConfigured, contextKey, contextSelection, gateway, iso, logView, policyView, providerView, runWithContext, sandboxView, selectConsoleContext } from './gateway.js'
|
|
18
|
+
import { consoleContext, contextConfigured, contextKey, contextSelection, forgetGatewayConnection, gateway, gatewayWorkspaces, iso, listGateways, logView, policyView, providerView, runWithContext, sandboxView, selectConsoleContext, workspaceScope } from './gateway.js'
|
|
19
19
|
import { createRemoteConnections } from './remote-gateway.js'
|
|
20
20
|
import { createSshHostStore } from './ssh-hosts.js'
|
|
21
21
|
import { createLocationInventory } from './location-inventory.js'
|
|
22
22
|
import { policyRoute } from './policy.js'
|
|
23
23
|
import { orgRoute, createInGroups, enforcePolicyOnly, startOrgSweeper, assignGroup } from './org.js'
|
|
24
24
|
import { expose, ingressRoute, startSweeper } from './ingress.js'
|
|
25
|
-
import { imageTemplateRoute, imageTemplateForLaunch, listImageTemplates } from './image-templates.js'
|
|
25
|
+
import { imageTemplateRoute, imageTemplateForLaunch, listImageTemplates, localEngine, runDocker, savingTemplate, setGatewayDocker } from './image-templates.js'
|
|
26
|
+
import { createGatewayDocker } from './gateway-docker.js'
|
|
27
|
+
import { runCli } from './openshell-cli.js'
|
|
26
28
|
import { editorRoute } from './editor.js'
|
|
27
29
|
import { terminalRoute, terminalUpgrade } from './terminal.js'
|
|
28
30
|
import { assertPackagesPrepared } from '../shared/setup-launch.js'
|
|
@@ -135,10 +137,11 @@ async function sandboxDetail(name) {
|
|
|
135
137
|
// ---- writes -----------------------------------------------------------------
|
|
136
138
|
|
|
137
139
|
export async function createSandbox(input, { sessionOverride = false } = {}) {
|
|
138
|
-
const { client, target, workspace } = await gateway()
|
|
139
140
|
// An image template is an OpenShell sandbox template: the gateway supplies
|
|
140
141
|
// its image and environment; the console adds how the sandbox starts.
|
|
141
142
|
const saved = input.imageTemplate ? await imageTemplateForLaunch(String(input.imageTemplate)) : null
|
|
143
|
+
// After the template check, which can restart the local gateway.
|
|
144
|
+
const { client, target, workspace } = await gateway()
|
|
142
145
|
const imageLabels = imageTemplateLabels(saved)
|
|
143
146
|
// A built template's agents get their sign-in and model destinations from
|
|
144
147
|
// the reviewed table in shared/agent-access.js, never from the recipe.
|
|
@@ -347,7 +350,9 @@ export function openshellApi(security = createSecurity(releaseConfig()), token =
|
|
|
347
350
|
|
|
348
351
|
// Both Vite and the installed CLI use this exact HTTP/WebSocket lifecycle.
|
|
349
352
|
export function createOpenShellApi({ httpServer, logger = console, security = createSecurity(releaseConfig()), token = createLaunchToken() } = {}) {
|
|
350
|
-
const gate = security.config.mode === 'local' ? createTokenGate(token
|
|
353
|
+
const gate = security.config.mode === 'local' ? createTokenGate(token, {
|
|
354
|
+
onReuse: () => logger.warn('The link OpenRod opened in your browser was used a second time. If that wasn’t you, another program may have opened the console first: stop OpenRod with Ctrl+C and start it again.'),
|
|
355
|
+
}) : null
|
|
351
356
|
const runtimes = new Map(), streams = new Set(), sockets = new Set(), pending = new Set(), responses = new Set()
|
|
352
357
|
const localCloud = security.config.mode === 'local' ? createLocalCloud({ native: createLocalCloudNative() }) : null
|
|
353
358
|
const cloudOff = security.config.mode === 'local' && !cloudOrigin()
|
|
@@ -384,6 +389,8 @@ export function createOpenShellApi({ httpServer, logger = console, security = cr
|
|
|
384
389
|
active = next
|
|
385
390
|
if (httpServer?.listening) runWithContext(next.context, () => next.hub.start())
|
|
386
391
|
}
|
|
392
|
+
// Read-only warm-up; edits wait for a build or launch that needs them.
|
|
393
|
+
void gatewayDocker?.status({ fresh: true }).catch(() => {})
|
|
387
394
|
return next
|
|
388
395
|
}
|
|
389
396
|
const sshHosts = security.config.mode === 'local' ? createSshHostStore() : null
|
|
@@ -394,12 +401,43 @@ export function createOpenShellApi({ httpServer, logger = console, security = cr
|
|
|
394
401
|
},
|
|
395
402
|
logger,
|
|
396
403
|
}) : null
|
|
404
|
+
// The local gateway's VM driver must use the Docker that builds templates.
|
|
405
|
+
const gatewayDocker = security.config.mode === 'local' ? createGatewayDocker({
|
|
406
|
+
target: async () => {
|
|
407
|
+
const local = (await remoteConnections.locationSnapshot()).returnContext
|
|
408
|
+
return local ? listGateways().find((entry) => entry.name === local.gateway) ?? null : null
|
|
409
|
+
},
|
|
410
|
+
client: async (name) => (await gateway({ gateway: name, workspace: 'default' })).client,
|
|
411
|
+
forget: forgetGatewayConnection,
|
|
412
|
+
sandboxes: async (name) => {
|
|
413
|
+
const { client } = await gateway({ gateway: name, workspace: 'default' })
|
|
414
|
+
const all = []
|
|
415
|
+
for (const { name: workspace } of await gatewayWorkspaces(name)) {
|
|
416
|
+
let pageToken = ''
|
|
417
|
+
do {
|
|
418
|
+
const page = await client.raw.listSandboxes({ workspaceScope: workspaceScope(workspace), pageSize: 1000, pageToken })
|
|
419
|
+
all.push(...page.sandboxes.map(sandboxView).map((s) => ({ name: s.name, workspace: s.workspace || workspace, phase: s.phase, image: s.image })))
|
|
420
|
+
pageToken = page.nextPageToken
|
|
421
|
+
} while (pageToken)
|
|
422
|
+
}
|
|
423
|
+
return all
|
|
424
|
+
},
|
|
425
|
+
localEngine: () => localEngine(), docker: runDocker, run: runCli,
|
|
426
|
+
busy: () => remoteConnections.changing() ? 'connection' : savingTemplate() ? 'saving' : null,
|
|
427
|
+
stateFile: path.join(stateDirectory(), 'gateway-docker.json'),
|
|
428
|
+
onJob: (job) => { pending.add(job); job.then(() => pending.delete(job), () => pending.delete(job)) },
|
|
429
|
+
logger,
|
|
430
|
+
}) : null
|
|
431
|
+
setGatewayDocker(gatewayDocker)
|
|
397
432
|
const inventory = createLocationInventory({
|
|
398
433
|
connections: remoteConnections, listSandboxes, listTemplates: listImageTemplates, logger,
|
|
399
434
|
defaultLabel: security.config.mode === 'local' ? 'Local' : 'Cloud',
|
|
400
435
|
})
|
|
401
|
-
|
|
402
|
-
|
|
436
|
+
let ready = Promise.resolve()
|
|
437
|
+
const start = () => { ready = begin(); return ready }
|
|
438
|
+
const begin = async () => {
|
|
439
|
+
if (closed) return
|
|
440
|
+
if (security.config.mode === 'local' && !contextConfigured()) { await remoteConnections?.autoSelect(); return }
|
|
403
441
|
if (remoteConnections && /^console-ssh-[a-f0-9]{24}$/.test(initialContext.gateway)) {
|
|
404
442
|
const remote = (await remoteConnections.locationSnapshot()).remote
|
|
405
443
|
if (remote?.gateway !== initialContext.gateway || remote.status !== 'connected') return
|
|
@@ -467,7 +505,7 @@ export function createOpenShellApi({ httpServer, logger = console, security = cr
|
|
|
467
505
|
const parts = url.pathname.slice('/api/os'.length).split('/').filter(Boolean)
|
|
468
506
|
assertCloudOperation(parts)
|
|
469
507
|
if (cloudOff && ['cloud-export', 'cloud-import', 'cloud-transfer'].includes(parts[0])) return send(res, 409, { error: CLOUD_SOON })
|
|
470
|
-
if (security.config.mode !== 'local' && (parts[0] === 'connections' || (parts[0] === 'context' && req.method !== 'GET') || (parts[0] === 'sandboxes' && ['ssh', 'ssh-open', 'ssh-config'].includes(parts[2])))) throw fail('Host-local actions are unavailable in OpenRod Cloud.', 403)
|
|
508
|
+
if (security.config.mode !== 'local' && (parts[0] === 'connections' || parts[0] === 'gateway-docker' || (parts[0] === 'context' && req.method !== 'GET') || (parts[0] === 'sandboxes' && ['ssh', 'ssh-open', 'ssh-config'].includes(parts[2])))) throw fail('Host-local actions are unavailable in OpenRod Cloud.', 403)
|
|
471
509
|
const requestedContext = req.headers['x-openshell-context'] ?? url.searchParams.get('context')
|
|
472
510
|
let owner = contextSelection()
|
|
473
511
|
const explicitLocation = req.headers['x-openshell-location'] === '1' || url.searchParams.get('location') === '1'
|
|
@@ -510,6 +548,15 @@ export function createOpenShellApi({ httpServer, logger = console, security = cr
|
|
|
510
548
|
}
|
|
511
549
|
return send(res, 404, { error: 'Not found' })
|
|
512
550
|
}
|
|
551
|
+
if (parts[0] === 'gateway-docker') {
|
|
552
|
+
if (!gatewayDocker) return send(res, 404, { error: 'Not found' })
|
|
553
|
+
if (req.method === 'GET' && parts.length === 1) return send(res, 200, await gatewayDocker.status({ fresh: url.searchParams.get('fresh') === '1' }))
|
|
554
|
+
if (req.method !== 'POST' || parts.length !== 2 || !['connect', 'undo'].includes(parts[1])) return send(res, 404, { error: 'Not found' })
|
|
555
|
+
if (!isMutation(req, security)) return send(res, 403, { error: 'Request rejected' })
|
|
556
|
+
const input = await body(req)
|
|
557
|
+
const seen = Array.isArray(input.seen) ? input.seen.slice(0, 500).map(String) : []
|
|
558
|
+
return send(res, 200, await gatewayDocker[parts[1]]({ confirm: input.confirm === true, seen }))
|
|
559
|
+
}
|
|
513
560
|
if (req.method === 'GET' && parts.length === 1) {
|
|
514
561
|
if (parts[0] === 'context') {
|
|
515
562
|
const remote = remoteConnections ? (await remoteConnections.locationSnapshot()).remote : null
|
|
@@ -631,7 +678,7 @@ export function createOpenShellApi({ httpServer, logger = console, security = cr
|
|
|
631
678
|
} catch (error) {
|
|
632
679
|
// Gateway errors carry a readable message; nothing here includes credentials.
|
|
633
680
|
if (res.headersSent) res.destroy(error)
|
|
634
|
-
else send(res, error.status ?? 502, { error: error.rawMessage ?? error.message ?? 'Gateway request failed', ...(error.code === 'TEMPLATE_IN_USE' ? { code: error.code, sandboxes: error.sandboxes } : {}) })
|
|
681
|
+
else send(res, error.status ?? 502, { error: error.rawMessage ?? error.message ?? 'Gateway request failed', ...(error.code === 'TEMPLATE_IN_USE' || error.code === 'GATEWAY_DOCKER_MISMATCH' ? { code: error.code, sandboxes: error.sandboxes, fix: error.fix } : {}) })
|
|
635
682
|
}
|
|
636
683
|
})
|
|
637
684
|
pending.add(operation)
|
|
@@ -644,5 +691,5 @@ export function createOpenShellApi({ httpServer, logger = console, security = cr
|
|
|
644
691
|
const authenticate = () => security.middleware(req, res, () => route(req, res, next))
|
|
645
692
|
return localCloud ? localCloud.middleware(req, res, authenticate) : authenticate()
|
|
646
693
|
}
|
|
647
|
-
return { middleware, close, token }
|
|
694
|
+
return { middleware, close, token, launchCode: gate?.launchCode, ready: () => ready }
|
|
648
695
|
}
|
package/server/cli.js
CHANGED
|
@@ -9,21 +9,25 @@ import { parseArgs } from 'node:util'
|
|
|
9
9
|
|
|
10
10
|
const HELP = `Usage: openrod [--host 127.0.0.1] [--port 4600] [--open | --no-open]
|
|
11
11
|
|
|
12
|
-
Serve OpenRod locally for local sandboxes or configured SSH hosts
|
|
13
|
-
|
|
12
|
+
Serve OpenRod locally for local sandboxes or configured SSH hosts and
|
|
13
|
+
open it in your default browser. The link carries a secret that changes
|
|
14
|
+
on every start. Over SSH, in CI, without a display, or when the output is
|
|
15
|
+
not a terminal, the link is only printed.
|
|
14
16
|
|
|
15
17
|
--host <host> Loopback only: 127.0.0.1, localhost, or ::1
|
|
16
18
|
--port <port> HTTP port from 1 to 65535 (default: 4600)
|
|
17
|
-
--open
|
|
18
|
-
--no-open
|
|
19
|
+
--open Always open the console in your default browser
|
|
20
|
+
--no-open Only print the link
|
|
19
21
|
--help, -h Show this help
|
|
20
22
|
--version, -v Show the installed version
|
|
21
23
|
|
|
22
24
|
Mutable data: OPENSHELL_CONSOLE_DATA_DIR, or
|
|
23
25
|
$XDG_STATE_HOME/openshell-console (~/.local/state/openshell-console).
|
|
24
26
|
Requires Node.js >=22.13.0. SSH hosts need local OpenSSH and OpenSSL,
|
|
25
|
-
plus a running Docker Engine on the remote Linux host.
|
|
26
|
-
|
|
27
|
+
plus a running Docker Engine on the remote Linux host.
|
|
28
|
+
If OpenShell is missing, openrod offers to install the pinned 0.1.2
|
|
29
|
+
release (macOS needs Homebrew). Connecting an SSH host downloads a missing
|
|
30
|
+
openshell-gateway executable, checksum-verified, on supported platforms.
|
|
27
31
|
`
|
|
28
32
|
|
|
29
33
|
export function nodeSupported(version = process.versions.node) {
|
|
@@ -51,7 +55,15 @@ export function parseOptions(args = process.argv.slice(2)) {
|
|
|
51
55
|
if (!['127.0.0.1', 'localhost', '::1'].includes(values.host)) throw new Error('--host must be a loopback address: 127.0.0.1, localhost, or ::1.')
|
|
52
56
|
if (!/^\d+$/.test(values.port) || Number(values.port) < 1 || Number(values.port) > 65535) throw new Error('--port must be an integer from 1 to 65535.')
|
|
53
57
|
if (values.open && values['no-open']) throw new Error('Choose either --open or --no-open, not both.')
|
|
54
|
-
return { host: values.host, port: Number(values.port), open:
|
|
58
|
+
return { host: values.host, port: Number(values.port), open: values.open ? true : values['no-open'] ? false : undefined, help: Boolean(values.help), version: Boolean(values.version) }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Opening the page hands over the launch link without copying it, so it is the
|
|
62
|
+
// default. A session with no screen to open on only prints the link.
|
|
63
|
+
export function autoOpen({ env = process.env, platform = process.platform, tty = process.stdout.isTTY } = {}) {
|
|
64
|
+
const set = (name) => Boolean(env[name]) && !['0', 'false'].includes(env[name].toLowerCase())
|
|
65
|
+
if (!tty || set('CI') || env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY) return false
|
|
66
|
+
return ['darwin', 'win32'].includes(platform) || Boolean(env.DISPLAY || env.WAYLAND_DISPLAY)
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
const TYPES = {
|
|
@@ -104,8 +116,8 @@ function openBrowser(url, logger) {
|
|
|
104
116
|
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'rundll32' : 'xdg-open'
|
|
105
117
|
const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url]
|
|
106
118
|
const child = spawn(command, args, { stdio: 'ignore', detached: true })
|
|
107
|
-
child.once('error', (error) => logger.warn(`
|
|
108
|
-
child.once('exit', (code) => { if (code) logger.warn(`
|
|
119
|
+
child.once('error', (error) => logger.warn(`Couldn’t open a browser (${error.message}). Open the link above.`))
|
|
120
|
+
child.once('exit', (code) => { if (code) logger.warn(`Couldn’t open a browser (exit ${code}). Open the link above.`) })
|
|
109
121
|
child.unref()
|
|
110
122
|
}
|
|
111
123
|
|
|
@@ -159,7 +171,11 @@ export async function startConsole(options = parseOptions([]), logger = console)
|
|
|
159
171
|
// The link carries this launch's secret; it changes on every restart.
|
|
160
172
|
const url = tokenUrl(host, port, token)
|
|
161
173
|
logger.info(`OpenRod console (open this link): ${url}`)
|
|
162
|
-
if (options.open)
|
|
174
|
+
if (options.open) {
|
|
175
|
+
// A fresh console first connects to its local gateway, so the page opens on it.
|
|
176
|
+
await Promise.race([api.ready?.(), new Promise((resolve) => setTimeout(resolve, 5000).unref())])
|
|
177
|
+
openBrowser(tokenUrl(host, port, api.launchCode?.() ?? token), logger)
|
|
178
|
+
}
|
|
163
179
|
return { server, url, close }
|
|
164
180
|
}
|
|
165
181
|
|
|
@@ -169,7 +185,12 @@ async function main() {
|
|
|
169
185
|
if (options.version) { console.log(JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url), 'utf8')).version); return }
|
|
170
186
|
if (!nodeSupported()) throw new Error(`OpenRod needs Node.js 22.13 or newer, and this is ${process.versions.node}. Install a current release from https://nodejs.org, then run it again.`)
|
|
171
187
|
quietSqliteWarning()
|
|
172
|
-
|
|
188
|
+
// Before the console starts, so the browser opens on a usable local gateway.
|
|
189
|
+
try {
|
|
190
|
+
const { offerOpenShellInstall } = await import('./openshell-install.js')
|
|
191
|
+
if (await offerOpenShellInstall() === 'cancelled') { process.exitCode = 130; return }
|
|
192
|
+
} catch (error) { console.warn(`Could not check for OpenShell: ${error.message}`) }
|
|
193
|
+
const runtime = await startConsole({ ...options, open: options.open ?? autoOpen() })
|
|
173
194
|
let stopping = false
|
|
174
195
|
const shutdown = () => {
|
|
175
196
|
if (stopping) return
|