openrod 0.1.2-rc.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openrod",
3
- "version": "0.1.2-rc.1",
3
+ "version": "0.1.2-rc.2",
4
4
  "description": "A local console for OpenShell gateways, sandboxes, and activity.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/OpenRod/OpenRod#readme",
package/server/api.js CHANGED
@@ -433,8 +433,11 @@ export function createOpenShellApi({ httpServer, logger = console, security = cr
433
433
  connections: remoteConnections, listSandboxes, listTemplates: listImageTemplates, logger,
434
434
  defaultLabel: security.config.mode === 'local' ? 'Local' : 'Cloud',
435
435
  })
436
- const start = async () => {
437
- if (closed || (security.config.mode === 'local' && !contextConfigured())) return
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 }
438
441
  if (remoteConnections && /^console-ssh-[a-f0-9]{24}$/.test(initialContext.gateway)) {
439
442
  const remote = (await remoteConnections.locationSnapshot()).remote
440
443
  if (remote?.gateway !== initialContext.gateway || remote.status !== 'connected') return
@@ -688,5 +691,5 @@ export function createOpenShellApi({ httpServer, logger = console, security = cr
688
691
  const authenticate = () => security.middleware(req, res, () => route(req, res, next))
689
692
  return localCloud ? localCloud.middleware(req, res, authenticate) : authenticate()
690
693
  }
691
- return { middleware, close, token, launchCode: gate?.launchCode }
694
+ return { middleware, close, token, launchCode: gate?.launchCode, ready: () => ready }
692
695
  }
package/server/cli.js CHANGED
@@ -171,7 +171,11 @@ export async function startConsole(options = parseOptions([]), logger = console)
171
171
  // The link carries this launch's secret; it changes on every restart.
172
172
  const url = tokenUrl(host, port, token)
173
173
  logger.info(`OpenRod console (open this link): ${url}`)
174
- if (options.open) openBrowser(tokenUrl(host, port, api.launchCode?.() ?? token), logger)
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
+ }
175
179
  return { server, url, close }
176
180
  }
177
181
 
package/server/gateway.js CHANGED
@@ -32,6 +32,9 @@ let selected = {
32
32
  let configured = Boolean(process.env.OPENSHELL_GATEWAY || saved.gateway)
33
33
  let selectionSource = process.env.OPENSHELL_GATEWAY ? 'environment' : saved.gateway ? 'saved' : cliGateway ? 'cli' : 'default'
34
34
  export const contextConfigured = () => configured
35
+ // A console nobody has pointed anywhere yet: no saved or cleared choice and no
36
+ // OPENSHELL_GATEWAY. Its candidate is the gateway the OpenShell CLI is set to.
37
+ export const unchosenCliGateway = () => !configured && !process.env.OPENSHELL_GATEWAY && cliGateway && !fs.existsSync(CONTEXT_FILE) ? cliGateway : null
35
38
 
36
39
  const requestContext = new AsyncLocalStorage()
37
40
  export const contextSelection = () => ({ ...(requestContext.getStore() ?? selected) })
@@ -146,7 +146,9 @@ export async function offerOpenShellInstall({
146
146
  const needs = platform === 'darwin' ? 'OpenRod needs it to build sandbox images.' : "OpenShell's gateway won't start without it."
147
147
  if (docker === 'missing') log.info(`Docker isn't installed. ${needs}`)
148
148
  else if (!docker) log.info(`Docker isn't running. ${needs}`)
149
- const answer = await ask(`Install OpenShell ${OPENSHELL_VERSION} now? ${platform === 'darwin' ? 'It uses Homebrew and runs sandboxes in VMs.' : 'It uses sudo and may ask for your password.'} [Y/n] `)
149
+ let answer = await ask(`Install OpenShell ${OPENSHELL_VERSION} now? ${platform === 'darwin' ? 'It uses Homebrew and runs sandboxes in VMs.' : 'It uses sudo and may ask for your password.'} [Y/n] `)
150
+ // Anything but yes or no (a key from another keyboard layout, say) asks again.
151
+ for (let tries = 0; answer !== null && !['', 'y', 'yes', 'n', 'no'].includes(answer.trim().toLowerCase()) && tries < 3; tries++) answer = await ask('Please answer y or n. [Y/n] ')
150
152
  if (answer === null) return 'cancelled'
151
153
  if (!['', 'y', 'yes'].includes(answer.trim().toLowerCase())) { manual('Skipped. To install it later, run:'); return 'skipped' }
152
154
 
@@ -8,7 +8,7 @@ import { randomUUID } from 'node:crypto'
8
8
  import { Transform } from 'node:stream'
9
9
  import { pipeline } from 'node:stream/promises'
10
10
  import { setTimeout as delay } from 'node:timers/promises'
11
- import { CONFIG_DIR, clearConsoleContext, contextConfigured, contextSelection, defaultContextSelection, localGateways, gatewayWorkspaces, selectConsoleContext } from './gateway.js'
11
+ import { CONFIG_DIR, clearConsoleContext, contextConfigured, contextSelection, defaultContextSelection, localGateways, gatewayWorkspaces, selectConsoleContext, unchosenCliGateway } from './gateway.js'
12
12
  import { stateDirectory } from './paths.js'
13
13
  import { fail, findExecutable, openshellBinary, runCli, sshBinary } from './openshell-cli.js'
14
14
  import { listSshHosts, probeHost, installDocker as installHostDocker, installRuntime, sshArgs } from './remote-hosts.js'
@@ -322,6 +322,13 @@ export function createRemoteConnections({ onSelected = () => {}, onDeselected =
322
322
  return { hosts: await listSshHosts(), locals: localGateways().map(({ name, endpoint }) => ({ name, endpoint })), active: view(connection), job: view(job), tools: { ssh: Boolean(sshBinary()), gateway: Boolean(findExecutable('openshell-gateway')), openshell: Boolean(openshellBinary()), installCommand: installCommand(process.platform) } }
323
323
  },
324
324
  changing: () => disconnecting || job?.status === 'working',
325
+ // A fresh console connects to the CLI's local gateway the way "Use this
326
+ // computer" does, but only once it answers; otherwise setup stays up.
327
+ async autoSelect() {
328
+ const name = unchosenCliGateway()
329
+ if (!name || job?.status === 'working' || !localGateways().some(target => target.name === name)) return false
330
+ try { await select(name); return true } catch { return false }
331
+ },
325
332
  architecture: () => active?.status === 'connected' ? active.architecture : null,
326
333
  begin,
327
334
  job: id => view(getJob(id)),