11import { spawnSync } from 'node:child_process'
2+ import { existsSync } from 'node:fs'
23import { SetupError } from './errors.ts'
34import { waitFor } from './probes.ts'
45import * as p from './prompter.ts'
@@ -9,6 +10,10 @@ const INSTALL_HINTS = [
910 `or OrbStack (lighter on macOS): ${ theme . command ( 'brew install orbstack' ) } ` ,
1011]
1112
13+ /** macOS GUI docker providers we know how to launch via `open -a`. */
14+ const ORBSTACK_APP = { name : 'OrbStack' , path : '/Applications/OrbStack.app' } as const
15+ const DOCKER_DESKTOP_APP = { name : 'Docker' , path : '/Applications/Docker.app' } as const
16+
1217function daemonUp ( ) : boolean {
1318 return spawnSync ( 'docker' , [ 'info' ] , { stdio : 'ignore' } ) . status === 0
1419}
@@ -19,6 +24,24 @@ function installed(): boolean {
1924 return Bun . which ( 'docker' ) !== null
2025}
2126
27+ function currentDockerContext ( ) : string | null {
28+ const result = spawnSync ( 'docker' , [ 'context' , 'show' ] , { encoding : 'utf8' } )
29+ return result . status === 0 ? result . stdout . trim ( ) : null
30+ }
31+
32+ /**
33+ * Which GUI app owns the `docker` CLI on this Mac. Docker Desktop and OrbStack
34+ * both install a `docker` binary, so presence of the CLI alone doesn't tell us
35+ * which app to relaunch. Prefer the docker CLI's own active context — it's
36+ * accurate regardless of where the app bundle lives — and fall back to
37+ * checking the well-known `.app` install paths when the context doesn't say.
38+ */
39+ function macDockerApp ( ) : typeof ORBSTACK_APP | typeof DOCKER_DESKTOP_APP {
40+ if ( currentDockerContext ( ) === 'orbstack' ) return ORBSTACK_APP
41+ if ( existsSync ( ORBSTACK_APP . path ) ) return ORBSTACK_APP
42+ return DOCKER_DESKTOP_APP
43+ }
44+
2245/**
2346 * Returns whether the Docker daemon is available, offering to launch Docker
2447 * Desktop (macOS) when it's installed but stopped. Never installs anything.
@@ -41,27 +64,31 @@ export async function ensureDocker(required: boolean): Promise<boolean> {
4164 return false
4265 }
4366
67+ const app = macDockerApp ( )
68+
4469 const launch = await p . confirm ( {
45- message : ' Docker is installed but not running — start Docker Desktop now?' ,
70+ message : ` Docker is installed but not running — start ${ app . name } now?` ,
4671 initialValue : true ,
4772 } )
4873 if ( ! launch ) {
4974 if ( required ) {
5075 throw new SetupError ( 'Docker is required for this mode.' , [
51- ' start Docker Desktop , then re-run the wizard' ,
76+ ` start ${ app . name } , then re-run the wizard` ,
5277 ] )
5378 }
5479 return false
5580 }
5681
57- spawnSync ( 'open' , [ '-a' , 'Docker' ] , { stdio : 'ignore' } )
82+ spawnSync ( 'open' , [ '-a' , app . name ] , { stdio : 'ignore' } )
5883 const spin = p . spinner ( )
59- spin . start ( ' Waiting for the Docker daemon…' )
84+ spin . start ( ` Waiting for the Docker daemon ( ${ app . name } )…` )
6085 const up = await waitFor ( async ( ) => daemonUp ( ) , 90_000 , 2000 )
6186 spin . stop ( up ? 'Docker is running' : `${ glyph . fail } daemon did not come up` )
6287 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' ,
88+ throw new SetupError ( `${ app . name } did not start within 90s.` , [
89+ app === ORBSTACK_APP
90+ ? 'open OrbStack manually once to finish its first-run setup, then re-run'
91+ : 'first-ever launch needs a GUI license acceptance — open Docker Desktop manually once, then re-run' ,
6592 ] )
6693 }
6794 return true
0 commit comments