-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Expand file tree
/
Copy pathdocker.ts
More file actions
170 lines (149 loc) · 6.29 KB
/
Copy pathdocker.ts
File metadata and controls
170 lines (149 loc) · 6.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
import { spawnSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { SetupError } from './errors.ts'
import { waitFor } from './probes.ts'
import * as p from './prompter.ts'
import { glyph, theme } from './theme.ts'
const INSTALL_HINTS = [
'install Docker Desktop: https://docker.com/products/docker-desktop',
`or OrbStack (lighter on macOS): ${theme.command('brew install orbstack')}`,
]
/**
* Reaching this means the docker CLI exists but neither GUI app does, which is
* also what a colima or Rancher Desktop user looks like — telling them to
* install Docker Desktop would be advice for a problem they don't have.
*/
const NO_APP_HINTS = [
...INSTALL_HINTS,
`or start your existing runtime its own way, e.g. ${theme.command('colima start')}`,
]
/** macOS GUI docker providers we know how to launch via `open -a`. */
const ORBSTACK_APP = { name: 'OrbStack', bundle: 'OrbStack.app' } as const
const DOCKER_DESKTOP_APP = { name: 'Docker', bundle: 'Docker.app' } as const
type DockerApp = typeof ORBSTACK_APP | typeof DOCKER_DESKTOP_APP
/** Homebrew casks honour `--appdir`, so a user-local install is not unusual. */
const APP_DIRS = ['/Applications', join(homedir(), 'Applications')]
function daemonUp(): boolean {
return spawnSync('docker', ['info'], { stdio: 'ignore' }).status === 0
}
/** Uses `Bun.which` rather than `which`, which is not a standard Windows command. */
function installed(): boolean {
return Bun.which('docker') !== null
}
/**
* Whether the docker CLI is currently pointed at OrbStack. `DOCKER_HOST` wins
* over the active context when set, so it is the only signal worth reading in
* that case; otherwise the active context is authoritative, since OrbStack
* registers and selects a context named `orbstack`.
*/
function orbstackSelected(): boolean {
const host = process.env.DOCKER_HOST
if (host) return host.includes('.orbstack/')
const result = spawnSync('docker', ['context', 'show'], { encoding: 'utf8' })
return result.status === 0 && result.stdout.trim() === 'orbstack'
}
function appInstalled(app: DockerApp): boolean {
return APP_DIRS.some((dir) => existsSync(join(dir, app.bundle)))
}
interface DockerChoice {
app: DockerApp
/** The CLI names this provider, so no other app can bring its daemon up. */
explicit: boolean
}
/**
* Which app to offer to start. Both providers install a `docker` binary, so CLI
* presence alone doesn't say which one to launch. An OrbStack selection is
* explicit; anything else is a guess the launch is allowed to correct, which is
* why the install probe here doesn't have to be exhaustive.
*/
function macDockerApp(): DockerChoice {
if (orbstackSelected()) return { app: ORBSTACK_APP, explicit: true }
const orbstackOnly = appInstalled(ORBSTACK_APP) && !appInstalled(DOCKER_DESKTOP_APP)
return { app: orbstackOnly ? ORBSTACK_APP : DOCKER_DESKTOP_APP, explicit: false }
}
/**
* Starts a provider. `open` exits non-zero when macOS knows no such app, which
* settles installation authoritatively and without a dialog — it resolves the
* name the same way the launch does, so the two cannot disagree.
*/
function openApp(app: DockerApp): boolean {
return spawnSync('open', ['-a', app.name], { stdio: 'ignore' }).status === 0
}
/**
* Starts the chosen provider, retrying with the other one when the choice was
* only a guess. An explicit OrbStack selection is never redirected: `docker
* info` would still be addressing OrbStack's socket, so Docker Desktop cannot
* satisfy it however successfully it starts.
*/
function startDockerApp({ app, explicit }: DockerChoice): DockerApp | null {
if (openApp(app)) return app
if (explicit) return null
const other = app === ORBSTACK_APP ? DOCKER_DESKTOP_APP : ORBSTACK_APP
return openApp(other) ? other : null
}
/**
* A launch that failed after the user opted into it. Callers passing
* required=false have a non-Docker path to offer, so the reason is worth
* surfacing but must not abort the wizard.
*/
function launchFailed(required: boolean, message: string, hints: string[]): boolean {
if (required) throw new SetupError(message, hints)
p.log.warn([message, ...hints].join('\n'))
return false
}
/**
* Returns whether the Docker daemon is available, offering to launch the
* installed docker app (macOS) when it's stopped. Never installs anything.
* With required=true, unavailability is a SetupError instead of false.
*/
export async function ensureDocker(required: boolean): Promise<boolean> {
if (daemonUp()) return true
if (!installed()) {
if (required) throw new SetupError('Docker is not installed.', INSTALL_HINTS)
return false
}
if (process.platform !== 'darwin') {
if (required) {
throw new SetupError('Docker is installed but the daemon is not running.', [
`start it: ${theme.command('sudo systemctl start docker')} (Linux)`,
])
}
return false
}
const choice = macDockerApp()
const launch = await p.confirm({
message: `Docker is installed but not running — start ${choice.app.name} now?`,
initialValue: true,
})
if (!launch) {
if (required) {
throw new SetupError('Docker is required for this mode.', [
`start ${choice.app.name}, then re-run the wizard`,
])
}
return false
}
const app = startDockerApp(choice)
if (!app) {
return choice.explicit
? launchFailed(required, 'The docker CLI is pointed at OrbStack, which is not installed.', [
`reinstall it: ${theme.command('brew install orbstack')}`,
`or point the CLI elsewhere: unset DOCKER_HOST and DOCKER_CONTEXT, then ${theme.command('docker context use <name>')}`,
])
: launchFailed(required, 'Found the docker CLI, but no app to start.', NO_APP_HINTS)
}
const spin = p.spinner()
spin.start(`Waiting for the Docker daemon (${app.name})…`)
const up = await waitFor(async () => daemonUp(), 90_000, 2000)
spin.stop(up ? 'Docker is running' : `${glyph.fail} daemon did not come up`)
if (!up) {
return launchFailed(required, `${app.name} did not start within 90s.`, [
app === ORBSTACK_APP
? 'open OrbStack manually once to finish its first-run setup, then re-run'
: 'first-ever launch needs a GUI license acceptance — open Docker Desktop manually once, then re-run',
])
}
return true
}