Skip to content

Commit 47f5fee

Browse files
fix(setup): launch the docker app the CLI is actually pointed at (#6253)
* fix(setup): detect OrbStack vs Docker Desktop before relaunching the daemon ensureDocker() always ran `open -a Docker` to relaunch a stopped daemon on macOS, which silently no-ops for OrbStack users (no Docker.app bundle exists), leading to a misleading "GUI license acceptance" timeout error. Now it checks the docker CLI's active context first (accurate regardless of install location) and falls back to checking for OrbStack.app, so the wizard launches and messages the app that's actually installed. * fix(setup): don't let an installed OrbStack override an explicit Docker Desktop context macDockerApp() fell through to the OrbStack.app existence check whenever docker context show returned anything other than "orbstack" — including a known, explicit context like "desktop-linux". With both apps installed but Docker Desktop active and stopped, this launched OrbStack while daemonUp() kept polling Docker Desktop's socket, timing out with OrbStack-flavored guidance for a Docker Desktop problem. The path fallback now only runs when the context command gives no answer at all (null); any resolved context is trusted outright. Flagged identically by Greptile and Cursor Bugbot on PR #6250. * fix(setup): fall back to the installed app when the context isn't OrbStack Context detection only fell back to the app bundle when `docker context show` failed outright, so an OrbStack-only Mac sitting on the `default` context still resolved to Docker Desktop — the same 90s hang this fix exists to remove. Treat an explicit OrbStack selection as the only positive context signal and otherwise pick whichever app is installed. Read `DOCKER_HOST` first: it overrides the active context, so the context name is not authoritative while it is set. * fix(setup): require OrbStack to be installed before selecting it A context or DOCKER_HOST left behind by an OrbStack uninstall selected an app that can never launch, turning a working Docker Desktop start into a guaranteed 90s timeout. Gate the OrbStack signal on the bundle being present and fall through to whichever app is. Look in ~/Applications as well as /Applications while here — Homebrew casks honour --appdir, so a user-local install is not unusual and a hardcoded /Applications check would misread it as "not installed". * fix(setup): resolve the docker app through LaunchServices, not fixed paths A Homebrew `--appdir` can put OrbStack anywhere, so enumerating install directories will always have a tail that reads a present app as missing and sends setup to the wrong one. Fall back to LaunchServices when the well-known directories miss: that is the same lookup `open -a` performs, so availability now agrees with what the launch will actually do. * fix(setup): settle the docker app with open(1) instead of probing for it `path to application` can raise a modal "Where is …?" picker when the name does not resolve, which in a terminal wizard reads as a hang. Drop it: the launch itself already answers the question, since `open` exits non-zero when macOS knows no such app, instantly and without UI. That inverts the design. Rather than predict which app is installed and then launch it, pick a provider, try to start it, and let the exit code correct a guess — so the directory probe no longer has to enumerate every possible install location to be right. An explicit OrbStack selection is now never redirected to Docker Desktop. The CLI is addressing OrbStack's socket, so `docker info` keeps failing no matter how well Docker Desktop starts; the earlier fallback only replaced a 90s timeout with a differently worded one. Say the context is stale and how to fix it instead. * fix(setup): honour `required` when the docker app fails to launch db.ts and redis.ts call ensureDocker(false) and branch on the boolean to offer an external Postgres or Redis instead. Throwing past that aborts the whole wizard when a working non-Docker path was on the table, so every post-confirm failure now warns and returns false unless Docker is required. That covers the 90s-timeout throw too, which ignored `required` before this branch existed — leaving it as the one path that still aborts would make the flag mean two different things in one function. Also name DOCKER_CONTEXT in the stale-selection hint. It overrides the config context, so `docker context use` alone leaves the CLI pointed at OrbStack and the next run fails identically. * improvement(setup): don't tell CLI-runtime users to install Docker Desktop Having the docker CLI but neither GUI app is exactly what a colima or Rancher Desktop user looks like, and the failure told them to install Docker Desktop — advice for a problem they don't have. Name the situation accurately and add starting an existing runtime as an option. --------- Co-authored-by: Bohdan Vilishchuk <iamtheflex@gmail.com>
1 parent 3d8b2ed commit 47f5fee

1 file changed

Lines changed: 112 additions & 10 deletions

File tree

scripts/setup/docker.ts

Lines changed: 112 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { spawnSync } from 'node:child_process'
2+
import { existsSync } from 'node:fs'
3+
import { homedir } from 'node:os'
4+
import { join } from 'node:path'
25
import { SetupError } from './errors.ts'
36
import { waitFor } from './probes.ts'
47
import * as p from './prompter.ts'
@@ -9,19 +12,105 @@ const INSTALL_HINTS = [
912
`or OrbStack (lighter on macOS): ${theme.command('brew install orbstack')}`,
1013
]
1114

15+
/**
16+
* Reaching this means the docker CLI exists but neither GUI app does, which is
17+
* also what a colima or Rancher Desktop user looks like — telling them to
18+
* install Docker Desktop would be advice for a problem they don't have.
19+
*/
20+
const NO_APP_HINTS = [
21+
...INSTALL_HINTS,
22+
`or start your existing runtime its own way, e.g. ${theme.command('colima start')}`,
23+
]
24+
25+
/** macOS GUI docker providers we know how to launch via `open -a`. */
26+
const ORBSTACK_APP = { name: 'OrbStack', bundle: 'OrbStack.app' } as const
27+
const DOCKER_DESKTOP_APP = { name: 'Docker', bundle: 'Docker.app' } as const
28+
29+
type DockerApp = typeof ORBSTACK_APP | typeof DOCKER_DESKTOP_APP
30+
31+
/** Homebrew casks honour `--appdir`, so a user-local install is not unusual. */
32+
const APP_DIRS = ['/Applications', join(homedir(), 'Applications')]
33+
1234
function daemonUp(): boolean {
1335
return spawnSync('docker', ['info'], { stdio: 'ignore' }).status === 0
1436
}
1537

38+
/** Uses `Bun.which` rather than `which`, which is not a standard Windows command. */
1639
function installed(): boolean {
17-
// Bun.which resolves PATH cross-platform (incl. PATHEXT on Windows); `which`
18-
// is not a standard Windows command.
1940
return Bun.which('docker') !== null
2041
}
2142

2243
/**
23-
* Returns whether the Docker daemon is available, offering to launch Docker
24-
* Desktop (macOS) when it's installed but stopped. Never installs anything.
44+
* Whether the docker CLI is currently pointed at OrbStack. `DOCKER_HOST` wins
45+
* over the active context when set, so it is the only signal worth reading in
46+
* that case; otherwise the active context is authoritative, since OrbStack
47+
* registers and selects a context named `orbstack`.
48+
*/
49+
function orbstackSelected(): boolean {
50+
const host = process.env.DOCKER_HOST
51+
if (host) return host.includes('.orbstack/')
52+
const result = spawnSync('docker', ['context', 'show'], { encoding: 'utf8' })
53+
return result.status === 0 && result.stdout.trim() === 'orbstack'
54+
}
55+
56+
function appInstalled(app: DockerApp): boolean {
57+
return APP_DIRS.some((dir) => existsSync(join(dir, app.bundle)))
58+
}
59+
60+
interface DockerChoice {
61+
app: DockerApp
62+
/** The CLI names this provider, so no other app can bring its daemon up. */
63+
explicit: boolean
64+
}
65+
66+
/**
67+
* Which app to offer to start. Both providers install a `docker` binary, so CLI
68+
* presence alone doesn't say which one to launch. An OrbStack selection is
69+
* explicit; anything else is a guess the launch is allowed to correct, which is
70+
* why the install probe here doesn't have to be exhaustive.
71+
*/
72+
function macDockerApp(): DockerChoice {
73+
if (orbstackSelected()) return { app: ORBSTACK_APP, explicit: true }
74+
const orbstackOnly = appInstalled(ORBSTACK_APP) && !appInstalled(DOCKER_DESKTOP_APP)
75+
return { app: orbstackOnly ? ORBSTACK_APP : DOCKER_DESKTOP_APP, explicit: false }
76+
}
77+
78+
/**
79+
* Starts a provider. `open` exits non-zero when macOS knows no such app, which
80+
* settles installation authoritatively and without a dialog — it resolves the
81+
* name the same way the launch does, so the two cannot disagree.
82+
*/
83+
function openApp(app: DockerApp): boolean {
84+
return spawnSync('open', ['-a', app.name], { stdio: 'ignore' }).status === 0
85+
}
86+
87+
/**
88+
* Starts the chosen provider, retrying with the other one when the choice was
89+
* only a guess. An explicit OrbStack selection is never redirected: `docker
90+
* info` would still be addressing OrbStack's socket, so Docker Desktop cannot
91+
* satisfy it however successfully it starts.
92+
*/
93+
function startDockerApp({ app, explicit }: DockerChoice): DockerApp | null {
94+
if (openApp(app)) return app
95+
if (explicit) return null
96+
const other = app === ORBSTACK_APP ? DOCKER_DESKTOP_APP : ORBSTACK_APP
97+
return openApp(other) ? other : null
98+
}
99+
100+
/**
101+
* A launch that failed after the user opted into it. Callers passing
102+
* required=false have a non-Docker path to offer, so the reason is worth
103+
* surfacing but must not abort the wizard.
104+
*/
105+
function launchFailed(required: boolean, message: string, hints: string[]): boolean {
106+
if (required) throw new SetupError(message, hints)
107+
p.log.warn([message, ...hints].join('\n'))
108+
return false
109+
}
110+
111+
/**
112+
* Returns whether the Docker daemon is available, offering to launch the
113+
* installed docker app (macOS) when it's stopped. Never installs anything.
25114
* With required=true, unavailability is a SetupError instead of false.
26115
*/
27116
export async function ensureDocker(required: boolean): Promise<boolean> {
@@ -41,27 +130,40 @@ export async function ensureDocker(required: boolean): Promise<boolean> {
41130
return false
42131
}
43132

133+
const choice = macDockerApp()
134+
44135
const launch = await p.confirm({
45-
message: 'Docker is installed but not running — start Docker Desktop now?',
136+
message: `Docker is installed but not running — start ${choice.app.name} now?`,
46137
initialValue: true,
47138
})
48139
if (!launch) {
49140
if (required) {
50141
throw new SetupError('Docker is required for this mode.', [
51-
'start Docker Desktop, then re-run the wizard',
142+
`start ${choice.app.name}, then re-run the wizard`,
52143
])
53144
}
54145
return false
55146
}
56147

57-
spawnSync('open', ['-a', 'Docker'], { stdio: 'ignore' })
148+
const app = startDockerApp(choice)
149+
if (!app) {
150+
return choice.explicit
151+
? launchFailed(required, 'The docker CLI is pointed at OrbStack, which is not installed.', [
152+
`reinstall it: ${theme.command('brew install orbstack')}`,
153+
`or point the CLI elsewhere: unset DOCKER_HOST and DOCKER_CONTEXT, then ${theme.command('docker context use <name>')}`,
154+
])
155+
: launchFailed(required, 'Found the docker CLI, but no app to start.', NO_APP_HINTS)
156+
}
157+
58158
const spin = p.spinner()
59-
spin.start('Waiting for the Docker daemon…')
159+
spin.start(`Waiting for the Docker daemon (${app.name})…`)
60160
const up = await waitFor(async () => daemonUp(), 90_000, 2000)
61161
spin.stop(up ? 'Docker is running' : `${glyph.fail} daemon did not come up`)
62162
if (!up) {
63-
throw new SetupError('Docker Desktop did not start within 90s.', [
64-
'first-ever launch needs a GUI license acceptance — open Docker Desktop manually once, then re-run',
163+
return launchFailed(required, `${app.name} did not start within 90s.`, [
164+
app === ORBSTACK_APP
165+
? 'open OrbStack manually once to finish its first-run setup, then re-run'
166+
: 'first-ever launch needs a GUI license acceptance — open Docker Desktop manually once, then re-run',
65167
])
66168
}
67169
return true

0 commit comments

Comments
 (0)