Skip to content

Commit ce25e1d

Browse files
committed
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.
1 parent 2010e3f commit ce25e1d

1 file changed

Lines changed: 51 additions & 26 deletions

File tree

scripts/setup/docker.ts

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@ function daemonUp(): boolean {
2525
return spawnSync('docker', ['info'], { stdio: 'ignore' }).status === 0
2626
}
2727

28+
/** Uses `Bun.which` rather than `which`, which is not a standard Windows command. */
2829
function installed(): boolean {
29-
// Bun.which resolves PATH cross-platform (incl. PATHEXT on Windows); `which`
30-
// is not a standard Windows command.
3130
return Bun.which('docker') !== null
3231
}
3332

@@ -44,32 +43,48 @@ function orbstackSelected(): boolean {
4443
return result.status === 0 && result.stdout.trim() === 'orbstack'
4544
}
4645

46+
function appInstalled(app: DockerApp): boolean {
47+
return APP_DIRS.some((dir) => existsSync(join(dir, app.bundle)))
48+
}
49+
50+
interface DockerChoice {
51+
app: DockerApp
52+
/** The CLI names this provider, so no other app can bring its daemon up. */
53+
explicit: boolean
54+
}
55+
4756
/**
48-
* Whether macOS can launch this app. The well-known directories cover every
49-
* normal install without spawning anything; LaunchServices is the authority
50-
* for the rest, since a Homebrew `--appdir` can put the bundle anywhere and
51-
* `open -a` would still find it there.
57+
* Which app to offer to start. Both providers install a `docker` binary, so CLI
58+
* presence alone doesn't say which one to launch. An OrbStack selection is
59+
* explicit; anything else is a guess the launch is allowed to correct, which is
60+
* why the install probe here doesn't have to be exhaustive.
5261
*/
53-
function appInstalled(app: DockerApp): boolean {
54-
if (APP_DIRS.some((dir) => existsSync(join(dir, app.bundle)))) return true
55-
const lookup = spawnSync('osascript', ['-e', `path to application "${app.name}"`], {
56-
stdio: 'ignore',
57-
})
58-
return lookup.status === 0
62+
function macDockerApp(): DockerChoice {
63+
if (orbstackSelected()) return { app: ORBSTACK_APP, explicit: true }
64+
const orbstackOnly = appInstalled(ORBSTACK_APP) && !appInstalled(DOCKER_DESKTOP_APP)
65+
return { app: orbstackOnly ? ORBSTACK_APP : DOCKER_DESKTOP_APP, explicit: false }
5966
}
6067

6168
/**
62-
* Which GUI app owns the `docker` CLI on this Mac. Both apps install a `docker`
63-
* binary, so CLI presence alone doesn't say which one to launch. An explicit
64-
* OrbStack selection wins, but only when OrbStack is still installed — a
65-
* context or `DOCKER_HOST` left behind by an uninstall would otherwise pick an
66-
* app that can never come up. Otherwise fall back to whichever app is present,
67-
* which also covers CLIs too old for `docker context show`.
69+
* Starts a provider. `open` exits non-zero when macOS knows no such app, which
70+
* settles installation authoritatively and without a dialog — it resolves the
71+
* name the same way the launch does, so the two cannot disagree.
6872
*/
69-
function macDockerApp(): DockerApp {
70-
if (orbstackSelected() && appInstalled(ORBSTACK_APP)) return ORBSTACK_APP
71-
if (appInstalled(DOCKER_DESKTOP_APP)) return DOCKER_DESKTOP_APP
72-
return appInstalled(ORBSTACK_APP) ? ORBSTACK_APP : DOCKER_DESKTOP_APP
73+
function openApp(app: DockerApp): boolean {
74+
return spawnSync('open', ['-a', app.name], { stdio: 'ignore' }).status === 0
75+
}
76+
77+
/**
78+
* Starts the chosen provider, retrying with the other one when the choice was
79+
* only a guess. An explicit OrbStack selection is never redirected: `docker
80+
* info` would still be addressing OrbStack's socket, so Docker Desktop cannot
81+
* satisfy it however successfully it starts.
82+
*/
83+
function startDockerApp({ app, explicit }: DockerChoice): DockerApp | null {
84+
if (openApp(app)) return app
85+
if (explicit) return null
86+
const other = app === ORBSTACK_APP ? DOCKER_DESKTOP_APP : ORBSTACK_APP
87+
return openApp(other) ? other : null
7388
}
7489

7590
/**
@@ -94,22 +109,32 @@ export async function ensureDocker(required: boolean): Promise<boolean> {
94109
return false
95110
}
96111

97-
const app = macDockerApp()
112+
const choice = macDockerApp()
98113

99114
const launch = await p.confirm({
100-
message: `Docker is installed but not running — start ${app.name} now?`,
115+
message: `Docker is installed but not running — start ${choice.app.name} now?`,
101116
initialValue: true,
102117
})
103118
if (!launch) {
104119
if (required) {
105120
throw new SetupError('Docker is required for this mode.', [
106-
`start ${app.name}, then re-run the wizard`,
121+
`start ${choice.app.name}, then re-run the wizard`,
107122
])
108123
}
109124
return false
110125
}
111126

112-
spawnSync('open', ['-a', app.name], { stdio: 'ignore' })
127+
const app = startDockerApp(choice)
128+
if (!app) {
129+
if (choice.explicit) {
130+
throw new SetupError('The docker CLI is pointed at OrbStack, which is not installed.', [
131+
`reinstall it: ${theme.command('brew install orbstack')}`,
132+
`or point the CLI elsewhere: unset DOCKER_HOST / ${theme.command('docker context use <name>')}`,
133+
])
134+
}
135+
throw new SetupError('No docker app is installed.', INSTALL_HINTS)
136+
}
137+
113138
const spin = p.spinner()
114139
spin.start(`Waiting for the Docker daemon (${app.name})…`)
115140
const up = await waitFor(async () => daemonUp(), 90_000, 2000)

0 commit comments

Comments
 (0)