dsh-plugin-container 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -2
  3. package/cordis.patch.yml +3 -0
  4. package/docs/ai-chat.png +0 -0
  5. package/docs/ai-new-session.png +0 -0
  6. package/docs/apple-container.png +0 -0
  7. package/docs/attach.png +0 -0
  8. package/docs/docker-console.png +0 -0
  9. package/docs/docker-overview.png +0 -0
  10. package/docs/docker-volumes.png +0 -0
  11. package/docs/image-architecture.png +0 -0
  12. package/docs/package-install.png +0 -0
  13. package/docs/provider-switch.png +0 -0
  14. package/docs/run-container.png +0 -0
  15. package/index.js +138 -0
  16. package/lib/client.js +3560 -0
  17. package/package.json +62 -4
  18. package/src/client/api.js +98 -0
  19. package/src/client/attach.jsx +134 -0
  20. package/src/client/components.jsx +320 -0
  21. package/src/client/hooks.js +161 -0
  22. package/src/client/i18n.js +502 -0
  23. package/src/client/icons.jsx +227 -0
  24. package/src/client/index.jsx +76 -0
  25. package/src/client/page.jsx +782 -0
  26. package/src/client/run-container.jsx +187 -0
  27. package/src/client/runs.js +182 -0
  28. package/src/client/store.js +91 -0
  29. package/src/client/styles.js +371 -0
  30. package/src/client/tabs/compose.jsx +94 -0
  31. package/src/client/tabs/console.jsx +211 -0
  32. package/src/client/tabs/containers.jsx +148 -0
  33. package/src/client/tabs/images.jsx +127 -0
  34. package/src/client/tabs/networks.jsx +91 -0
  35. package/src/client/tabs/overview.jsx +266 -0
  36. package/src/client/tabs/volumes.jsx +76 -0
  37. package/src/host/apple-cli.js +307 -0
  38. package/src/host/apple.js +471 -0
  39. package/src/host/attach.js +246 -0
  40. package/src/host/commands.js +235 -0
  41. package/src/host/docker-cli.js +405 -0
  42. package/src/host/jobs.js +245 -0
  43. package/src/host/parsers.js +484 -0
  44. package/src/host/runtime.js +285 -0
  45. package/src/host/service.js +836 -0
  46. package/src/host/tools.js +198 -0
  47. package/src/host/util.js +97 -0
@@ -0,0 +1,285 @@
1
+ /**
2
+ * macOS Docker runtime discovery and lifecycle actions.
3
+ *
4
+ * The runtime is inferred from the selected Docker context endpoint plus the
5
+ * local application/CLI inventory. Nothing here mutates Docker configuration:
6
+ * commands use `--context`, and colima/Docker Desktop actions are explicit
7
+ * user choices from the panel.
8
+ */
9
+ import { existsSync } from 'node:fs'
10
+ import { homedir } from 'node:os'
11
+ import { join } from 'node:path'
12
+
13
+ import { classifyEndpoint, parseColimaList, parseColimaStatus } from './parsers.js'
14
+ import { messageOf } from './util.js'
15
+
16
+ const DOCKER_APP_CANDIDATES = ['/Applications/Docker.app', join(homedir(), 'Applications/Docker.app')]
17
+ const ORB_APP_CANDIDATES = ['/Applications/OrbStack.app', join(homedir(), 'Applications/OrbStack.app')]
18
+
19
+ /** @param {string[]} paths @param {(path: string) => boolean} exists */
20
+ function firstExisting(paths, exists) {
21
+ return paths.find((path) => exists(path)) ?? null
22
+ }
23
+
24
+ /**
25
+ * @param {{
26
+ * cli: import('./docker-cli.js').DockerCli,
27
+ * platform?: NodeJS.Platform,
28
+ * env?: NodeJS.ProcessEnv,
29
+ * fs?: { existsSync: (path: string) => boolean },
30
+ * config?: Record<string, any>,
31
+ * }} options
32
+ */
33
+ export function createRuntimeResolver(options) {
34
+ const cli = options.cli
35
+ const platform = options.platform ?? process.platform
36
+ const env = options.env ?? process.env
37
+ const fs = options.fs ?? { existsSync }
38
+ const config = options.config ?? {}
39
+ const controlEnabled = platform === 'darwin' && config.enableRuntimeControl !== false
40
+
41
+ /** @param {AbortSignal | undefined} signal */
42
+ async function inventory(signal) {
43
+ const dockerDesktop = firstExisting(DOCKER_APP_CANDIDATES, fs.existsSync)
44
+ const orbStack = firstExisting(ORB_APP_CANDIDATES, fs.existsSync)
45
+ const colimaPath = platform === 'darwin' ? await cli.ensureColima({ signal }) : null
46
+ let colimaInstances = []
47
+ let colimaError
48
+ if (colimaPath !== null) {
49
+ try {
50
+ const result = await cli.program(colimaPath, ['list', '--json'], { signal, timeoutMs: 15_000, maxBytes: 128 * 1024 })
51
+ colimaInstances = parseColimaList(result.stdout)
52
+ } catch (error) {
53
+ colimaError = messageOf(error)
54
+ }
55
+ }
56
+ return { dockerDesktop, orbStack, colimaPath, colimaInstances, colimaError }
57
+ }
58
+
59
+ /**
60
+ * @param {Array<{name:string,description:string,endpoint:string,current:boolean,error:string}>} contexts
61
+ * @param {string | null | undefined} requested
62
+ */
63
+ function selectContext(contexts, requested) {
64
+ const current = contexts.find((context) => context.current) ?? null
65
+ const selected = (requested ? contexts.find((context) => context.name === requested) : undefined) ?? current ?? contexts[0] ?? null
66
+ return { current, selected, requested: requested ?? null }
67
+ }
68
+
69
+ /**
70
+ * @param {Awaited<ReturnType<typeof inventory>>} found
71
+ * @param {{name:string, endpoint:string} | null} context
72
+ * @param {{running?: boolean, error?: string}} daemon
73
+ */
74
+ function describe(found, context, daemon = {}) {
75
+ const endpointClass = classifyEndpoint(context?.endpoint)
76
+ const daemonRunning = daemon.running === true
77
+ if (endpointClass.kind === 'colima') {
78
+ const profile = endpointClass.profile ?? 'default'
79
+ const instance =
80
+ found.colimaInstances.find((item) => item.name === profile) ??
81
+ found.colimaInstances.find((item) => (context?.name ?? '').endsWith(profile)) ??
82
+ null
83
+ const vmRunning = instance !== null ? /running/iu.test(instance.status) : daemonRunning
84
+ return {
85
+ kind: 'colima',
86
+ label: 'colima',
87
+ profile,
88
+ installed: found.colimaPath !== null,
89
+ running: daemonRunning,
90
+ vmRunning,
91
+ canStart: controlEnabled && found.colimaPath !== null && !daemonRunning,
92
+ canStop: controlEnabled && found.colimaPath !== null && (daemonRunning || vmRunning),
93
+ canRestart: controlEnabled && found.colimaPath !== null && (daemonRunning || vmRunning) && vmRunning,
94
+ endpoint: context?.endpoint ?? '',
95
+ details: instance,
96
+ error: found.colimaError ?? daemon.error ?? null,
97
+ }
98
+ }
99
+ if (endpointClass.kind === 'orbstack') {
100
+ return {
101
+ kind: 'orbstack',
102
+ label: 'OrbStack',
103
+ profile: null,
104
+ installed: found.orbStack !== null,
105
+ running: daemonRunning,
106
+ vmRunning: daemonRunning,
107
+ canStart: controlEnabled && found.orbStack !== null && !daemonRunning,
108
+ canStop: controlEnabled && found.orbStack !== null && daemonRunning,
109
+ canRestart: false,
110
+ endpoint: context?.endpoint ?? '',
111
+ details: found.orbStack !== null ? { appPath: found.orbStack } : null,
112
+ error: daemon.error ?? null,
113
+ }
114
+ }
115
+ if (endpointClass.kind === 'local-socket') {
116
+ if (found.dockerDesktop !== null) {
117
+ return {
118
+ kind: 'docker-desktop',
119
+ label: 'Docker Desktop',
120
+ profile: null,
121
+ installed: true,
122
+ running: daemonRunning,
123
+ vmRunning: daemonRunning,
124
+ canStart: controlEnabled && !daemonRunning,
125
+ canStop: controlEnabled && daemonRunning,
126
+ canRestart: false,
127
+ endpoint: context?.endpoint ?? '',
128
+ details: { appPath: found.dockerDesktop },
129
+ error: daemon.error ?? null,
130
+ }
131
+ }
132
+ return {
133
+ kind: 'unknown',
134
+ label: 'Docker',
135
+ profile: null,
136
+ installed: null,
137
+ running: daemonRunning,
138
+ vmRunning: daemonRunning,
139
+ canStart: false,
140
+ canStop: false,
141
+ canRestart: false,
142
+ endpoint: context?.endpoint ?? '',
143
+ details: null,
144
+ error: daemon.error ?? null,
145
+ }
146
+ }
147
+ if (endpointClass.kind === 'remote') {
148
+ return {
149
+ kind: 'remote',
150
+ label: '远程 Docker',
151
+ profile: null,
152
+ installed: null,
153
+ running: daemonRunning,
154
+ vmRunning: daemonRunning,
155
+ canStart: false,
156
+ canStop: false,
157
+ canRestart: false,
158
+ endpoint: context?.endpoint ?? '',
159
+ details: null,
160
+ error: daemon.error ?? null,
161
+ }
162
+ }
163
+ return {
164
+ kind: 'unknown',
165
+ label: 'Docker',
166
+ profile: null,
167
+ installed: null,
168
+ running: daemonRunning,
169
+ vmRunning: daemonRunning,
170
+ canStart: false,
171
+ canStop: false,
172
+ canRestart: false,
173
+ endpoint: context?.endpoint ?? '',
174
+ details: null,
175
+ error: daemon.error ?? null,
176
+ }
177
+ }
178
+
179
+ /**
180
+ * @param {Array<{name:string,description:string,endpoint:string,current:boolean,error:string}>} contexts
181
+ * @param {string | null | undefined} requested
182
+ * @param {{running?: boolean, error?: string}} daemon
183
+ * @param {AbortSignal | undefined} signal
184
+ */
185
+ async function resolve(contexts, requested, daemon, signal) {
186
+ const found = await inventory(signal)
187
+ const selected = selectContext(contexts, requested)
188
+ const runtime = describe(found, selected.selected, daemon)
189
+ if (runtime.kind === 'colima' && runtime.profile !== null && runtime.installed) {
190
+ try {
191
+ const result = await cli.program(found.colimaPath, ['status', '--json', '--profile', runtime.profile], {
192
+ signal,
193
+ timeoutMs: 15_000,
194
+ maxBytes: 256 * 1024,
195
+ })
196
+ const status = parseColimaStatus(result.stdout)
197
+ if (status !== null) {
198
+ runtime.details = { ...(runtime.details ?? {}), ...status }
199
+ runtime.vmRunning = true
200
+ runtime.canStop = controlEnabled
201
+ runtime.canRestart = controlEnabled
202
+ runtime.cpus = status.cpus
203
+ runtime.memory = status.memory
204
+ runtime.disk = status.disk
205
+ runtime.driver = status.driver
206
+ runtime.arch = status.arch
207
+ runtime.mountType = status.mountType
208
+ runtime.dockerSocket = status.dockerSocket
209
+ runtime.kubernetes = status.kubernetes
210
+ }
211
+ } catch (error) {
212
+ // Stopped colima instances cannot report status; the list already
213
+ // carries the state, so this is not an error worth surfacing.
214
+ if (runtime.vmRunning) runtime.error = runtime.error ?? messageOf(error)
215
+ }
216
+ }
217
+ return { ...selected, runtime }
218
+ }
219
+
220
+ /**
221
+ * Build one explicit runtime action. The caller decides confirmation and
222
+ * whether to run it as a job.
223
+ * @param {string} action
224
+ * @param {ReturnType<typeof describe>} runtime
225
+ */
226
+ function actionSpec(action, runtime) {
227
+ if (!controlEnabled) throw Object.assign(new Error('当前平台或配置不允许控制 Docker 运行时'), { code: 'docker/unsupported' })
228
+ if (action !== 'start' && action !== 'stop' && action !== 'restart') {
229
+ throw Object.assign(new Error(`不支持的运行时操作: ${action}`), { code: 'docker/unsupported' })
230
+ }
231
+ if (runtime.kind === 'colima') {
232
+ if (!runtime.installed) throw Object.assign(new Error('未找到 colima CLI'), { code: 'docker/cli-missing' })
233
+ if (action === 'restart' && !runtime.canRestart) {
234
+ throw Object.assign(new Error('当前 colima 未运行,无需重启'), { code: 'docker/unsupported' })
235
+ }
236
+ const colimaPath = cli.colimaPath
237
+ if (colimaPath === undefined || colimaPath === null) throw Object.assign(new Error('未找到 colima CLI'), { code: 'docker/cli-missing' })
238
+ const profile = runtime.profile ?? 'default'
239
+ return {
240
+ argv: [colimaPath, action, '--profile', profile],
241
+ title: `colima ${action}`,
242
+ displayCommand: `colima ${action} --profile ${profile}`,
243
+ timeoutMs: 10 * 60_000,
244
+ }
245
+ }
246
+ if (runtime.kind === 'docker-desktop') {
247
+ if (action === 'restart') throw Object.assign(new Error('Docker Desktop 暂不支持一键重启,请先停止再启动'), { code: 'docker/unsupported' })
248
+ if (action === 'start') {
249
+ const app = runtime.details?.appPath ?? 'Docker'
250
+ return {
251
+ argv: ['/usr/bin/open', '-a', app],
252
+ title: '启动 Docker Desktop',
253
+ displayCommand: `open -a ${app}`,
254
+ timeoutMs: 30_000,
255
+ }
256
+ }
257
+ return {
258
+ argv: ['/usr/bin/osascript', '-e', 'tell application id "com.docker.docker" to quit'],
259
+ title: '停止 Docker Desktop',
260
+ displayCommand: 'osascript -e \'tell application id "com.docker.docker" to quit\'',
261
+ timeoutMs: 120_000,
262
+ }
263
+ }
264
+ if (runtime.kind === 'orbstack') {
265
+ if (action === 'restart') throw Object.assign(new Error('OrbStack 暂不支持一键重启'), { code: 'docker/unsupported' })
266
+ const app = runtime.details?.appPath ?? 'OrbStack'
267
+ return action === 'start'
268
+ ? {
269
+ argv: ['/usr/bin/open', '-a', app],
270
+ title: '启动 OrbStack',
271
+ displayCommand: `open -a ${app}`,
272
+ timeoutMs: 30_000,
273
+ }
274
+ : {
275
+ argv: ['/usr/bin/osascript', '-e', 'tell application "OrbStack" to quit'],
276
+ title: '停止 OrbStack',
277
+ displayCommand: 'osascript -e \'tell application "OrbStack" to quit\'',
278
+ timeoutMs: 120_000,
279
+ }
280
+ }
281
+ throw Object.assign(new Error('当前 Docker 上下文没有可本机控制的运行时'), { code: 'docker/unsupported' })
282
+ }
283
+
284
+ return { inventory, selectContext, resolve, actionSpec, controlEnabled, platform, env }
285
+ }