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.
- package/LICENSE +21 -0
- package/README.md +80 -2
- package/cordis.patch.yml +3 -0
- package/docs/ai-chat.png +0 -0
- package/docs/ai-new-session.png +0 -0
- package/docs/apple-container.png +0 -0
- package/docs/attach.png +0 -0
- package/docs/docker-console.png +0 -0
- package/docs/docker-overview.png +0 -0
- package/docs/docker-volumes.png +0 -0
- package/docs/image-architecture.png +0 -0
- package/docs/package-install.png +0 -0
- package/docs/provider-switch.png +0 -0
- package/docs/run-container.png +0 -0
- package/index.js +138 -0
- package/lib/client.js +3560 -0
- package/package.json +62 -4
- package/src/client/api.js +98 -0
- package/src/client/attach.jsx +134 -0
- package/src/client/components.jsx +320 -0
- package/src/client/hooks.js +161 -0
- package/src/client/i18n.js +502 -0
- package/src/client/icons.jsx +227 -0
- package/src/client/index.jsx +76 -0
- package/src/client/page.jsx +782 -0
- package/src/client/run-container.jsx +187 -0
- package/src/client/runs.js +182 -0
- package/src/client/store.js +91 -0
- package/src/client/styles.js +371 -0
- package/src/client/tabs/compose.jsx +94 -0
- package/src/client/tabs/console.jsx +211 -0
- package/src/client/tabs/containers.jsx +148 -0
- package/src/client/tabs/images.jsx +127 -0
- package/src/client/tabs/networks.jsx +91 -0
- package/src/client/tabs/overview.jsx +266 -0
- package/src/client/tabs/volumes.jsx +76 -0
- package/src/host/apple-cli.js +307 -0
- package/src/host/apple.js +471 -0
- package/src/host/attach.js +246 -0
- package/src/host/commands.js +235 -0
- package/src/host/docker-cli.js +405 -0
- package/src/host/jobs.js +245 -0
- package/src/host/parsers.js +484 -0
- package/src/host/runtime.js +285 -0
- package/src/host/service.js +836 -0
- package/src/host/tools.js +198 -0
- package/src/host/util.js +97 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple `container` provider.
|
|
3
|
+
*
|
|
4
|
+
* Read-only discovery uses `container system status/version` and the JSON list
|
|
5
|
+
* commands; mutations are normal jobs through the same JobManager used by the
|
|
6
|
+
* Docker provider. There is no Docker context, image store, or Compose here.
|
|
7
|
+
*/
|
|
8
|
+
import { cpus, homedir, totalmem } from 'node:os'
|
|
9
|
+
import { statSync } from 'node:fs'
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
CommandError,
|
|
13
|
+
describeCommand,
|
|
14
|
+
detectDestructive,
|
|
15
|
+
normalizeSubcommand,
|
|
16
|
+
parseArguments,
|
|
17
|
+
parseCommand,
|
|
18
|
+
resolveCwd,
|
|
19
|
+
} from './commands.js'
|
|
20
|
+
import { AppleCli } from './apple-cli.js'
|
|
21
|
+
import { JobManager } from './jobs.js'
|
|
22
|
+
import {
|
|
23
|
+
firstLine,
|
|
24
|
+
normalizeAppleContainer,
|
|
25
|
+
normalizeAppleImage,
|
|
26
|
+
normalizeAppleNetwork,
|
|
27
|
+
normalizeAppleVolume,
|
|
28
|
+
parseAppleDf,
|
|
29
|
+
parseAppleSystemStatus,
|
|
30
|
+
parseAppleSystemVersion,
|
|
31
|
+
summarizeStderr,
|
|
32
|
+
} from './parsers.js'
|
|
33
|
+
import { messageOf, optionalInteger, requireString } from './util.js'
|
|
34
|
+
|
|
35
|
+
const VALUE_CACHE_MS = 1500
|
|
36
|
+
|
|
37
|
+
/** @param {string} value */
|
|
38
|
+
function safeId(value) {
|
|
39
|
+
const id = requireString(value, 'id', { max: 256 })
|
|
40
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.:/@-]{0,255}$/u.test(id)) throw new CommandError('资源 ID 不合法')
|
|
41
|
+
return id
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @param {string} value */
|
|
45
|
+
function safeImageRef(value) {
|
|
46
|
+
const reference = requireString(value, 'reference', { max: 512 })
|
|
47
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.:/@-]*$/u.test(reference)) throw new CommandError('镜像名不合法')
|
|
48
|
+
return reference
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @param {unknown} value @param {string} field */
|
|
52
|
+
function stringList(value, field) {
|
|
53
|
+
if (value === undefined || value === null) return []
|
|
54
|
+
if (!Array.isArray(value)) throw new CommandError(`${field} 必须是字符串数组`)
|
|
55
|
+
if (value.length > 64) throw new CommandError(`${field} 数量过多`)
|
|
56
|
+
return value.map((entry, index) => requireString(entry, `${field}[${String(index)}]`, { max: 512 }))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @param {string} value */
|
|
60
|
+
function safePort(value) {
|
|
61
|
+
if (!/^(?:(?:\d{1,3}\.){3}\d{1,3}:)?\d{1,5}:\d{1,5}(?:\/(?:tcp|udp))?$/u.test(value) && !/^\d{1,5}(?:\/(?:tcp|udp))?$/u.test(value)) {
|
|
62
|
+
throw new CommandError(`端口格式不合法: ${value}(示例 8080:80)`)
|
|
63
|
+
}
|
|
64
|
+
return value
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @param {string} value */
|
|
68
|
+
function safeEnv(value) {
|
|
69
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*=[^\r\n]*$/u.test(value)) throw new CommandError(`环境变量格式不合法: ${value}(示例 KEY=VALUE)`)
|
|
70
|
+
return value
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class AppleProvider {
|
|
74
|
+
/**
|
|
75
|
+
* @param {{
|
|
76
|
+
* subprocess: any,
|
|
77
|
+
* config?: Record<string, any>,
|
|
78
|
+
* env?: NodeJS.ProcessEnv,
|
|
79
|
+
* home?: string,
|
|
80
|
+
* platform?: NodeJS.Platform,
|
|
81
|
+
* logger?: any,
|
|
82
|
+
* }} options
|
|
83
|
+
*/
|
|
84
|
+
constructor(options) {
|
|
85
|
+
this.config = options.config ?? {}
|
|
86
|
+
this.home = options.home ?? homedir()
|
|
87
|
+
this.env = options.env ?? process.env
|
|
88
|
+
this.platform = options.platform ?? process.platform
|
|
89
|
+
this.logger = options.logger
|
|
90
|
+
this.controlEnabled = this.platform === 'darwin' && this.config.enableRuntimeControl !== false
|
|
91
|
+
this.cli = new AppleCli({
|
|
92
|
+
subprocess: options.subprocess,
|
|
93
|
+
config: this.config,
|
|
94
|
+
env: this.env,
|
|
95
|
+
home: this.home,
|
|
96
|
+
platform: this.platform,
|
|
97
|
+
logger: this.logger,
|
|
98
|
+
})
|
|
99
|
+
this.jobs = new JobManager({
|
|
100
|
+
subprocess: options.subprocess,
|
|
101
|
+
cli: this.cli,
|
|
102
|
+
maxOutputBytes: Number(this.config.maxOutputBytes ?? 1024 * 1024),
|
|
103
|
+
maxRuns: Number(this.config.maxConcurrentRuns ?? 4),
|
|
104
|
+
idPrefix: 'apple',
|
|
105
|
+
})
|
|
106
|
+
this.cache = new Map()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @param {AbortSignal | undefined} signal */
|
|
110
|
+
async info(signal) {
|
|
111
|
+
let path = null
|
|
112
|
+
try {
|
|
113
|
+
path = await this.cli.ensureContainer({ signal, force: true })
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return {
|
|
116
|
+
id: 'apple-container',
|
|
117
|
+
label: 'Apple container',
|
|
118
|
+
available: false,
|
|
119
|
+
running: false,
|
|
120
|
+
version: null,
|
|
121
|
+
detail: messageOf(error),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const status = parseAppleSystemStatus(await this.cli.systemStatus(signal))
|
|
125
|
+
const probe = status.status === 'running' ? undefined : await this.cli.probe(signal)
|
|
126
|
+
const running = status.status === 'running' || probe !== undefined
|
|
127
|
+
const versions = parseAppleSystemVersion(await this.cli.systemVersion(signal))
|
|
128
|
+
return {
|
|
129
|
+
id: 'apple-container',
|
|
130
|
+
label: 'Apple container',
|
|
131
|
+
available: true,
|
|
132
|
+
running,
|
|
133
|
+
version: versions.containerVersion || null,
|
|
134
|
+
detail: path,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
invalidate() {
|
|
139
|
+
this.cache.clear()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** @param {string} key @param {() => Promise<any>} produce */
|
|
143
|
+
async cached(key, produce) {
|
|
144
|
+
const now = Date.now()
|
|
145
|
+
const hit = this.cache.get(key)
|
|
146
|
+
if (hit !== undefined && now - hit.at < VALUE_CACHE_MS) return hit.value
|
|
147
|
+
const value = await produce()
|
|
148
|
+
this.cache.set(key, { at: now, value })
|
|
149
|
+
return value
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {Record<string, any>} payload
|
|
154
|
+
* @param {AbortSignal | undefined} signal
|
|
155
|
+
*/
|
|
156
|
+
async status(payload, signal) {
|
|
157
|
+
let path = null
|
|
158
|
+
let cliError = null
|
|
159
|
+
try {
|
|
160
|
+
path = await this.cli.ensureContainer({ signal })
|
|
161
|
+
} catch (error) {
|
|
162
|
+
cliError = error
|
|
163
|
+
}
|
|
164
|
+
const system = path === null
|
|
165
|
+
? { status: 'unavailable', error: messageOf(cliError), raw: null }
|
|
166
|
+
: parseAppleSystemStatus(await this.cli.systemStatus(signal))
|
|
167
|
+
if (path !== null && system.status !== 'running') {
|
|
168
|
+
const probe = await this.cli.probe(signal)
|
|
169
|
+
if (probe !== undefined) {
|
|
170
|
+
system.status = 'running'
|
|
171
|
+
system.error = null
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const versions = path === null ? { containerVersion: '', apiServerVersion: '', components: [] } : parseAppleSystemVersion(await this.cli.systemVersion(signal))
|
|
175
|
+
let df = null
|
|
176
|
+
let dfError = null
|
|
177
|
+
if (path !== null && system.status === 'running') {
|
|
178
|
+
try {
|
|
179
|
+
df = await this.cli.df(signal)
|
|
180
|
+
} catch (error) {
|
|
181
|
+
dfError = messageOf(error)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const running = system.status === 'running'
|
|
185
|
+
const daemonState = running ? 'running' : system.status === 'unavailable' || system.status === 'error' ? 'unreachable' : 'stopped'
|
|
186
|
+
const server = {
|
|
187
|
+
name: 'container-apiserver',
|
|
188
|
+
version: versions.apiServerVersion || system.apiServerVersion || '',
|
|
189
|
+
ncpu: cpus().length,
|
|
190
|
+
memTotal: totalmem(),
|
|
191
|
+
containers: Number(df?.containers?.total) || 0,
|
|
192
|
+
containersRunning: Number(df?.containers?.active) || 0,
|
|
193
|
+
containersStopped: Math.max(0, (Number(df?.containers?.total) || 0) - (Number(df?.containers?.active) || 0)),
|
|
194
|
+
images: Number(df?.images?.total) || 0,
|
|
195
|
+
operatingSystem: 'Apple container',
|
|
196
|
+
architecture: this.platform === 'darwin' ? 'arm64' : this.platform,
|
|
197
|
+
}
|
|
198
|
+
const warnings = []
|
|
199
|
+
if (typeof system.error === 'string' && system.error !== '') warnings.push(firstLine(system.error))
|
|
200
|
+
if (dfError) warnings.push(firstLine(dfError))
|
|
201
|
+
const value = {
|
|
202
|
+
provider: 'apple-container',
|
|
203
|
+
fetchedAt: Date.now(),
|
|
204
|
+
platform: this.platform,
|
|
205
|
+
cli: {
|
|
206
|
+
installed: path !== null,
|
|
207
|
+
path,
|
|
208
|
+
version: versions.containerVersion || null,
|
|
209
|
+
error: cliError === null ? null : messageOf(cliError),
|
|
210
|
+
},
|
|
211
|
+
daemon: {
|
|
212
|
+
state: daemonState,
|
|
213
|
+
error: typeof system.error === 'string' && system.error !== '' ? summarizeStderr(system.error) : null,
|
|
214
|
+
},
|
|
215
|
+
context: {
|
|
216
|
+
selected: null,
|
|
217
|
+
current: null,
|
|
218
|
+
endpoint: '',
|
|
219
|
+
description: 'Apple container',
|
|
220
|
+
contexts: [],
|
|
221
|
+
},
|
|
222
|
+
client: {
|
|
223
|
+
version: versions.containerVersion || null,
|
|
224
|
+
context: null,
|
|
225
|
+
os: 'darwin',
|
|
226
|
+
arch: 'arm64',
|
|
227
|
+
},
|
|
228
|
+
server,
|
|
229
|
+
runtime: {
|
|
230
|
+
kind: 'apple-container',
|
|
231
|
+
label: 'Apple container',
|
|
232
|
+
profile: null,
|
|
233
|
+
installed: path !== null,
|
|
234
|
+
running,
|
|
235
|
+
vmRunning: running,
|
|
236
|
+
canStart: this.controlEnabled && path !== null && !running,
|
|
237
|
+
canStop: this.controlEnabled && path !== null && running,
|
|
238
|
+
canRestart: false,
|
|
239
|
+
endpoint: '',
|
|
240
|
+
details: {
|
|
241
|
+
system: system.raw ?? null,
|
|
242
|
+
status: system.status,
|
|
243
|
+
components: versions.components,
|
|
244
|
+
disk: df,
|
|
245
|
+
},
|
|
246
|
+
error: system.error ?? dfError ?? (cliError === null ? null : messageOf(cliError)),
|
|
247
|
+
},
|
|
248
|
+
env: {
|
|
249
|
+
dockerHost: false,
|
|
250
|
+
dockerContext: null,
|
|
251
|
+
},
|
|
252
|
+
capabilities: {
|
|
253
|
+
enableRuntimeControl: this.controlEnabled,
|
|
254
|
+
maxOutputBytes: Number(this.config.maxOutputBytes ?? 1024 * 1024),
|
|
255
|
+
defaultTimeoutMs: Number(this.config.defaultTimeoutMs ?? 120_000),
|
|
256
|
+
compose: false,
|
|
257
|
+
contexts: false,
|
|
258
|
+
},
|
|
259
|
+
warnings,
|
|
260
|
+
}
|
|
261
|
+
return value
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @param {Record<string, any>} payload
|
|
266
|
+
* @param {AbortSignal | undefined} signal
|
|
267
|
+
*/
|
|
268
|
+
async list(payload, signal) {
|
|
269
|
+
const resource = requireString(payload.resource, 'resource')
|
|
270
|
+
const all = payload.all !== false
|
|
271
|
+
const key = `${resource}:${all ? 'all' : 'running'}`
|
|
272
|
+
return this.cached(key, async () => {
|
|
273
|
+
let items
|
|
274
|
+
let format = 'json'
|
|
275
|
+
if (resource === 'containers') {
|
|
276
|
+
const raw = await this.cli.json(['list', ...(all ? ['-a'] : []), '--format', 'json'], { signal })
|
|
277
|
+
items = (Array.isArray(raw) ? raw : []).map(normalizeAppleContainer)
|
|
278
|
+
} else if (resource === 'images') {
|
|
279
|
+
const raw = await this.cli.json(['image', 'list', '--format', 'json'], { signal })
|
|
280
|
+
items = (Array.isArray(raw) ? raw : []).map(normalizeAppleImage)
|
|
281
|
+
} else if (resource === 'volumes') {
|
|
282
|
+
const raw = await this.cli.json(['volume', 'list', '--format', 'json'], { signal })
|
|
283
|
+
items = (Array.isArray(raw) ? raw : []).map(normalizeAppleVolume)
|
|
284
|
+
} else if (resource === 'networks') {
|
|
285
|
+
const raw = await this.cli.json(['network', 'list', '--format', 'json'], { signal })
|
|
286
|
+
items = (Array.isArray(raw) ? raw : []).map(normalizeAppleNetwork)
|
|
287
|
+
} else if (resource === 'disk') {
|
|
288
|
+
const raw = await this.cli.json(['system', 'df', '--format', 'json'], { signal })
|
|
289
|
+
items = parseAppleDf(raw)
|
|
290
|
+
} else if (resource === 'compose') {
|
|
291
|
+
throw new CommandError('Apple container 不支持 Compose;请切换到 Docker provider', 'container/unsupported')
|
|
292
|
+
} else {
|
|
293
|
+
throw new CommandError(`不支持的资源类型: ${resource}`)
|
|
294
|
+
}
|
|
295
|
+
return { fetchedAt: Date.now(), items, format, warnings: [] }
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {Record<string, any>} payload
|
|
301
|
+
* @param {AbortSignal | undefined} signal
|
|
302
|
+
*/
|
|
303
|
+
async inspect(payload, signal) {
|
|
304
|
+
const kind = requireString(payload.kind, 'kind')
|
|
305
|
+
const command = {
|
|
306
|
+
container: ['inspect', safeId(payload.id)],
|
|
307
|
+
image: ['image', 'inspect', safeId(payload.id)],
|
|
308
|
+
volume: ['volume', 'inspect', safeId(payload.id)],
|
|
309
|
+
network: ['network', 'inspect', safeId(payload.id)],
|
|
310
|
+
}[kind]
|
|
311
|
+
if (command === undefined) throw new CommandError('不支持的 inspect 类型')
|
|
312
|
+
const raw = await this.cli.json(command, { signal, timeoutMs: 20_000, maxBytes: 4 * 1024 * 1024 })
|
|
313
|
+
const value = Array.isArray(raw) ? raw[0] ?? null : raw
|
|
314
|
+
return { value, raw: JSON.stringify(value, null, 2), warnings: [] }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* @param {Record<string, any>} payload
|
|
319
|
+
* @param {AbortSignal | undefined} signal
|
|
320
|
+
*/
|
|
321
|
+
async run(payload, signal) {
|
|
322
|
+
const path = await this.cli.ensureContainer({ signal })
|
|
323
|
+
let subargv
|
|
324
|
+
if (payload.argv !== undefined) {
|
|
325
|
+
if (payload.command !== undefined) throw new CommandError('command 和 argv 只能提供一个')
|
|
326
|
+
subargv = normalizeSubcommand(payload.argv, { dockerPath: path, commandName: 'container' })
|
|
327
|
+
} else {
|
|
328
|
+
subargv = parseCommand(requireString(payload.command, 'command'), { dockerPath: path, commandName: 'container' })
|
|
329
|
+
}
|
|
330
|
+
const cwd = resolveCwd(payload.cwd, { fallback: this.home, statSync })
|
|
331
|
+
const displayCommand = describeCommand(subargv, null, 'container')
|
|
332
|
+
const destructive = detectDestructive(subargv)
|
|
333
|
+
if (destructive.dangerous && payload.confirm !== true) {
|
|
334
|
+
throw Object.assign(new Error(`危险操作需要确认: ${displayCommand}`), {
|
|
335
|
+
code: 'docker/confirmation-required',
|
|
336
|
+
details: { command: displayCommand, reason: destructive.reason },
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
const timeoutMs = optionalInteger(payload.timeoutMs, 'timeoutMs', { min: 5000, max: 30 * 60_000 }) ?? Number(this.config.defaultTimeoutMs ?? 120_000)
|
|
340
|
+
const run = this.jobs.start({
|
|
341
|
+
argv: [path, ...subargv],
|
|
342
|
+
displayCommand,
|
|
343
|
+
context: null,
|
|
344
|
+
cwd,
|
|
345
|
+
title: typeof payload.title === 'string' ? payload.title : '',
|
|
346
|
+
timeoutMs,
|
|
347
|
+
})
|
|
348
|
+
this.invalidate()
|
|
349
|
+
return { run }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Semantic quick action mapped to Apple CLI syntax.
|
|
354
|
+
* @param {Record<string, any>} payload
|
|
355
|
+
* @param {AbortSignal | undefined} signal
|
|
356
|
+
*/
|
|
357
|
+
async action(payload, signal) {
|
|
358
|
+
const kind = requireString(payload.kind, 'kind')
|
|
359
|
+
const action = requireString(payload.action, 'action')
|
|
360
|
+
if (kind === 'container' && action === 'run') {
|
|
361
|
+
const image = safeImageRef(payload.image)
|
|
362
|
+
const name = payload.name === undefined || payload.name === null ? undefined : safeId(payload.name)
|
|
363
|
+
const args = parseArguments(payload.command === undefined || payload.command === null ? '' : requireString(payload.command, 'command'))
|
|
364
|
+
const ports = stringList(payload.ports, 'ports').map(safePort)
|
|
365
|
+
const env = stringList(payload.env, 'env').map(safeEnv)
|
|
366
|
+
const argv = [
|
|
367
|
+
'run',
|
|
368
|
+
'-d',
|
|
369
|
+
...(name === undefined ? [] : ['--name', name]),
|
|
370
|
+
...ports.flatMap((port) => ['-p', port]),
|
|
371
|
+
...env.flatMap((entry) => ['-e', entry]),
|
|
372
|
+
image,
|
|
373
|
+
...args,
|
|
374
|
+
]
|
|
375
|
+
return this.run({
|
|
376
|
+
argv,
|
|
377
|
+
cwd: payload.cwd,
|
|
378
|
+
title: payload.title ?? `container run ${image}`,
|
|
379
|
+
}, signal)
|
|
380
|
+
}
|
|
381
|
+
if (kind === 'container') {
|
|
382
|
+
const id = safeId(payload.id)
|
|
383
|
+
if (action === 'restart') throw new CommandError('Apple container 不支持一键重启;请先停止再启动', 'container/unsupported')
|
|
384
|
+
const argv = action === 'start'
|
|
385
|
+
? ['start', id]
|
|
386
|
+
: action === 'stop'
|
|
387
|
+
? ['stop', id]
|
|
388
|
+
: action === 'delete'
|
|
389
|
+
? ['delete', id]
|
|
390
|
+
: action === 'logs'
|
|
391
|
+
? ['logs', ...(payload.follow === true ? ['-f', '-n', '100'] : ['-n', '500']), id]
|
|
392
|
+
: undefined
|
|
393
|
+
if (argv === undefined) throw new CommandError(`不支持的操作: ${action}`)
|
|
394
|
+
return this.run({ argv, confirm: payload.confirm, cwd: payload.cwd, title: payload.title ?? `container ${action} ${id}` }, signal)
|
|
395
|
+
}
|
|
396
|
+
if (kind === 'image') {
|
|
397
|
+
if (action === 'pull') {
|
|
398
|
+
return this.run({ argv: ['image', 'pull', safeImageRef(payload.id ?? payload.reference)], cwd: payload.cwd, title: payload.title ?? 'container image pull' }, signal)
|
|
399
|
+
}
|
|
400
|
+
if (action === 'delete') {
|
|
401
|
+
return this.run({ argv: ['image', 'delete', safeId(payload.id)], confirm: payload.confirm, cwd: payload.cwd, title: payload.title ?? 'container image delete' }, signal)
|
|
402
|
+
}
|
|
403
|
+
throw new CommandError(`不支持的镜像操作: ${action}`)
|
|
404
|
+
}
|
|
405
|
+
if (kind === 'volume') {
|
|
406
|
+
if (action !== 'delete') throw new CommandError(`不支持的卷操作: ${action}`)
|
|
407
|
+
return this.run({ argv: ['volume', 'delete', safeId(payload.id)], confirm: payload.confirm, cwd: payload.cwd, title: payload.title ?? 'container volume delete' }, signal)
|
|
408
|
+
}
|
|
409
|
+
if (kind === 'network') {
|
|
410
|
+
if (action !== 'delete') throw new CommandError(`不支持的网络操作: ${action}`)
|
|
411
|
+
return this.run({ argv: ['network', 'delete', safeId(payload.id)], confirm: payload.confirm, cwd: payload.cwd, title: payload.title ?? 'container network delete' }, signal)
|
|
412
|
+
}
|
|
413
|
+
throw new CommandError(`不支持的资源类型: ${kind}`)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* @param {Record<string, any>} payload
|
|
418
|
+
* @param {AbortSignal | undefined} signal
|
|
419
|
+
*/
|
|
420
|
+
async runtimeAction(payload, signal) {
|
|
421
|
+
const action = requireString(payload.action, 'action')
|
|
422
|
+
if (action !== 'start' && action !== 'stop') throw new CommandError('Apple container 只支持 system start/stop', 'container/unsupported')
|
|
423
|
+
if (action === 'stop' && payload.confirm !== true) {
|
|
424
|
+
throw Object.assign(new Error('停止 Apple container 系统服务会中断所有容器'), {
|
|
425
|
+
code: 'docker/confirmation-required',
|
|
426
|
+
details: { command: 'container system stop', reason: 'runtime-stop' },
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
const path = await this.cli.ensureContainer({ signal })
|
|
430
|
+
const argv = action === 'start'
|
|
431
|
+
? [path, 'system', 'start', '--disable-kernel-install', '--timeout', '120']
|
|
432
|
+
: [path, 'system', 'stop']
|
|
433
|
+
const displayCommand = action === 'start'
|
|
434
|
+
? 'container system start --disable-kernel-install --timeout 120'
|
|
435
|
+
: 'container system stop'
|
|
436
|
+
const run = this.jobs.start({
|
|
437
|
+
argv,
|
|
438
|
+
displayCommand,
|
|
439
|
+
context: null,
|
|
440
|
+
cwd: this.home,
|
|
441
|
+
title: action === 'start' ? 'container system start' : 'container system stop',
|
|
442
|
+
timeoutMs: 10 * 60_000,
|
|
443
|
+
})
|
|
444
|
+
this.invalidate()
|
|
445
|
+
return { run, dispatched: true, message: displayCommand }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** @param {Record<string, any>} payload */
|
|
449
|
+
poll(payload) {
|
|
450
|
+
const runId = requireString(payload.runId, 'runId')
|
|
451
|
+
return this.jobs.poll({
|
|
452
|
+
runId,
|
|
453
|
+
stdoutOffset: optionalInteger(payload.stdoutOffset, 'stdoutOffset', { min: 0 }),
|
|
454
|
+
stderrOffset: optionalInteger(payload.stderrOffset, 'stderrOffset', { min: 0 }),
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** @param {Record<string, any>} payload */
|
|
459
|
+
cancel(payload) {
|
|
460
|
+
return this.jobs.cancel(requireString(payload.runId, 'runId'))
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
dispose() {
|
|
464
|
+
this.jobs.dispose()
|
|
465
|
+
this.cache.clear()
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function createAppleProvider(options) {
|
|
470
|
+
return new AppleProvider(options)
|
|
471
|
+
}
|