@@ -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. */
2829function 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