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,484 @@
1
+ /** Tolerant parsers for Docker, colima, and Apple container CLI output. */
2
+ import { formatBytes } from './util.js'
3
+
4
+ /** @param {unknown} value */
5
+ function textOf(value) {
6
+ return value === undefined || value === null ? '' : String(value)
7
+ }
8
+
9
+ /** @param {string} text @returns {any[]} */
10
+ export function parseNdjson(text) {
11
+ const items = []
12
+ for (const line of text.split(/\r?\n/u)) {
13
+ const trimmed = line.trim()
14
+ if (trimmed === '') continue
15
+ try {
16
+ const value = JSON.parse(trimmed)
17
+ if (Array.isArray(value)) items.push(...value)
18
+ else items.push(value)
19
+ } catch (error) {
20
+ throw new Error(`无法解析 JSON 行: ${error instanceof Error ? error.message : String(error)}`)
21
+ }
22
+ }
23
+ return items
24
+ }
25
+
26
+ /**
27
+ * Parse a whole JSON document, an NDJSON stream, or a JSON array.
28
+ * @param {string} text
29
+ * @returns {any}
30
+ */
31
+ export function parseJsonLoose(text) {
32
+ const trimmed = text.trim()
33
+ if (trimmed === '') return undefined
34
+ try {
35
+ return JSON.parse(trimmed)
36
+ } catch {
37
+ const rows = parseNdjson(trimmed)
38
+ return rows.length === 1 ? rows[0] : rows
39
+ }
40
+ }
41
+
42
+ /** @param {string} text */
43
+ export function firstLine(text) {
44
+ const line = text.split(/\r?\n/u).find((value) => value.trim() !== '')
45
+ return line === undefined ? '' : line.trim()
46
+ }
47
+
48
+ /** @param {string} text @param {number} [max] */
49
+ export function summarizeStderr(text, max = 2048) {
50
+ if (typeof text !== 'string' || text === '') return ''
51
+ if (text.length <= max) return text
52
+ return `…[truncated]\n${text.slice(-max)}`
53
+ }
54
+
55
+ /** @param {unknown} value */
56
+ export function parseLabels(value) {
57
+ if (typeof value !== 'string' || value.trim() === '') return {}
58
+ const labels = {}
59
+ for (const entry of value.split(',')) {
60
+ const separator = entry.indexOf('=')
61
+ if (separator <= 0) continue
62
+ labels[entry.slice(0, separator).trim()] = entry.slice(separator + 1).trim()
63
+ }
64
+ return labels
65
+ }
66
+
67
+ /** @param {unknown} value */
68
+ function portList(value) {
69
+ if (typeof value !== 'string' || value.trim() === '') return []
70
+ return value.split(',').map((port) => port.trim()).filter(Boolean)
71
+ }
72
+
73
+ /** @param {Record<string, any>} raw */
74
+ export function normalizeContainer(raw) {
75
+ const names = textOf(raw.Names).split(',').map((name) => name.trim()).filter(Boolean)
76
+ return {
77
+ id: textOf(raw.ID),
78
+ name: names[0] ?? textOf(raw.Names),
79
+ names,
80
+ image: textOf(raw.Image),
81
+ command: textOf(raw.Command),
82
+ createdAt: textOf(raw.CreatedAt),
83
+ runningFor: textOf(raw.RunningFor),
84
+ ports: portList(raw.Ports),
85
+ status: textOf(raw.Status),
86
+ state: textOf(raw.State).toLowerCase() || 'unknown',
87
+ size: textOf(raw.Size),
88
+ labels: parseLabels(raw.Labels),
89
+ mounts: textOf(raw.Mounts),
90
+ networks: textOf(raw.Networks),
91
+ raw,
92
+ }
93
+ }
94
+
95
+ /** @param {Record<string, any>} raw */
96
+ export function normalizeImage(raw) {
97
+ const repository = textOf(raw.Repository)
98
+ const tag = textOf(raw.Tag)
99
+ return {
100
+ id: textOf(raw.ID),
101
+ repository,
102
+ tag,
103
+ name: repository === '<none>' && tag === '<none>' ? '<none>' : `${repository}${tag ? `:${tag}` : ''}`,
104
+ createdAt: textOf(raw.CreatedAt),
105
+ createdSince: textOf(raw.CreatedSince),
106
+ size: textOf(raw.Size),
107
+ sharedSize: textOf(raw.SharedSize),
108
+ uniqueSize: textOf(raw.UniqueSize),
109
+ containers: textOf(raw.Containers),
110
+ raw,
111
+ }
112
+ }
113
+
114
+ /** @param {Record<string, any>} raw */
115
+ export function normalizeVolume(raw) {
116
+ return {
117
+ name: textOf(raw.Name),
118
+ driver: textOf(raw.Driver),
119
+ scope: textOf(raw.Scope),
120
+ mountpoint: textOf(raw.Mountpoint),
121
+ labels: parseLabels(raw.Labels),
122
+ raw,
123
+ }
124
+ }
125
+
126
+ /** @param {Record<string, any>} raw */
127
+ export function normalizeNetwork(raw) {
128
+ return {
129
+ id: textOf(raw.ID),
130
+ name: textOf(raw.Name),
131
+ driver: textOf(raw.Driver),
132
+ scope: textOf(raw.Scope),
133
+ ipv4: textOf(raw.IPv4),
134
+ ipv6: textOf(raw.IPv6),
135
+ internal: textOf(raw.Internal),
136
+ createdAt: textOf(raw.CreatedAt),
137
+ labels: parseLabels(raw.Labels),
138
+ raw,
139
+ }
140
+ }
141
+
142
+ /** @param {Record<string, any>} raw */
143
+ export function normalizeCompose(raw) {
144
+ return {
145
+ name: textOf(raw.Name ?? raw.name),
146
+ status: textOf(raw.Status ?? raw.status),
147
+ configFiles: textOf(raw.ConfigFiles ?? raw.configFiles),
148
+ raw,
149
+ }
150
+ }
151
+
152
+ /**
153
+ * @param {string} text
154
+ * @returns {{items: any[], format: 'json'|'text'}}
155
+ */
156
+ export function parseComposeList(text) {
157
+ try {
158
+ const value = JSON.parse(text.trim())
159
+ if (Array.isArray(value)) return { items: value.map(normalizeCompose), format: 'json' }
160
+ if (value !== null && typeof value === 'object') return { items: [normalizeCompose(value)], format: 'json' }
161
+ } catch {
162
+ // Older Compose releases have no --format json; fall through to the table.
163
+ }
164
+ const lines = text.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean)
165
+ if (lines.length === 0) return { items: [], format: 'text' }
166
+ const header = lines[0]
167
+ if (!/^NAME\b/iu.test(header)) return { items: [], format: 'text' }
168
+ const items = []
169
+ for (const line of lines.slice(1)) {
170
+ const columns = line.split(/\s{2,}/u)
171
+ if (columns.length < 2) continue
172
+ items.push(normalizeCompose({ Name: columns[0], Status: columns[1], ConfigFiles: columns.slice(2).join(' ') }))
173
+ }
174
+ return { items, format: 'text' }
175
+ }
176
+
177
+ /**
178
+ * @param {string} text
179
+ * @returns {{contexts: Array<{name:string,description:string,endpoint:string,current:boolean,error:string}>}}
180
+ */
181
+ export function parseContexts(text) {
182
+ const rows = parseNdjson(text)
183
+ return {
184
+ contexts: rows
185
+ .filter((row) => row !== null && typeof row === 'object')
186
+ .map((row) => ({
187
+ name: textOf(row.Name),
188
+ description: textOf(row.Description),
189
+ endpoint: textOf(row.DockerEndpoint),
190
+ current: row.Current === true,
191
+ error: textOf(row.Error),
192
+ }))
193
+ .filter((row) => row.name !== ''),
194
+ }
195
+ }
196
+
197
+ /** @param {Record<string, any>} raw */
198
+ export function mapDockerVersion(raw) {
199
+ const client = raw.Client !== null && typeof raw.Client === 'object' ? raw.Client : null
200
+ const server = raw.Server !== null && typeof raw.Server === 'object' ? raw.Server : null
201
+ return {
202
+ client:
203
+ client === null
204
+ ? null
205
+ : {
206
+ version: textOf(client.Version),
207
+ apiVersion: textOf(client.ApiVersion),
208
+ context: textOf(client.Context),
209
+ os: textOf(client.Os),
210
+ arch: textOf(client.Arch),
211
+ gitCommit: textOf(client.GitCommit),
212
+ goVersion: textOf(client.GoVersion),
213
+ },
214
+ server:
215
+ server === null
216
+ ? null
217
+ : {
218
+ version: textOf(server.Version),
219
+ apiVersion: textOf(server.ApiVersion),
220
+ os: textOf(server.Os),
221
+ arch: textOf(server.Arch),
222
+ kernelVersion: textOf(server.KernelVersion),
223
+ gitCommit: textOf(server.GitCommit),
224
+ goVersion: textOf(server.GoVersion),
225
+ },
226
+ }
227
+ }
228
+
229
+ /** @param {Record<string, any>} raw */
230
+ export function mapDockerInfo(raw) {
231
+ const client = raw.ClientInfo !== null && typeof raw.ClientInfo === 'object' ? raw.ClientInfo : null
232
+ return {
233
+ server: {
234
+ name: textOf(raw.Name),
235
+ version: textOf(raw.ServerVersion),
236
+ osType: textOf(raw.OSType),
237
+ operatingSystem: textOf(raw.OperatingSystem),
238
+ architecture: textOf(raw.Architecture),
239
+ kernelVersion: textOf(raw.KernelVersion),
240
+ driver: textOf(raw.Driver),
241
+ dockerRootDir: textOf(raw.DockerRootDir),
242
+ ncpu: Number(raw.NCPU) || 0,
243
+ memTotal: Number(raw.MemTotal) || 0,
244
+ containers: Number(raw.Containers) || 0,
245
+ containersRunning: Number(raw.ContainersRunning) || 0,
246
+ containersPaused: Number(raw.ContainersPaused) || 0,
247
+ containersStopped: Number(raw.ContainersStopped) || 0,
248
+ images: Number(raw.Images) || 0,
249
+ warnings: Array.isArray(raw.Warnings) ? raw.Warnings.map(String) : [],
250
+ },
251
+ client:
252
+ client === null
253
+ ? null
254
+ : {
255
+ version: textOf(client.Version),
256
+ apiVersion: textOf(client.DefaultAPIVersion),
257
+ os: textOf(client.Platform?.Name),
258
+ arch: textOf(client.Arch),
259
+ context: textOf(client.Context),
260
+ },
261
+ raw,
262
+ }
263
+ }
264
+
265
+ /** @param {string} text */
266
+ export function parseColimaList(text) {
267
+ const rows = parseNdjson(text)
268
+ return rows.map((row) => ({
269
+ name: textOf(row.name),
270
+ status: textOf(row.status),
271
+ arch: textOf(row.arch),
272
+ cpus: Number(row.cpus) || 0,
273
+ memory: Number(row.memory) || 0,
274
+ disk: Number(row.disk) || 0,
275
+ runtime: textOf(row.runtime),
276
+ raw: row,
277
+ }))
278
+ }
279
+
280
+ /** @param {string} text */
281
+ export function parseColimaStatus(text) {
282
+ const value = parseJsonLoose(text)
283
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return null
284
+ return {
285
+ displayName: textOf(value.display_name),
286
+ driver: textOf(value.driver),
287
+ arch: textOf(value.arch),
288
+ runtime: textOf(value.runtime),
289
+ mountType: textOf(value.mount_type),
290
+ ipAddress: textOf(value.ip_address),
291
+ dockerSocket: textOf(value.docker_socket),
292
+ kubernetes: value.kubernetes === true,
293
+ cpus: Number(value.cpu) || 0,
294
+ memory: Number(value.memory) || 0,
295
+ disk: Number(value.disk) || 0,
296
+ raw: value,
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Classify a Docker endpoint without deciding whether a local app exists.
302
+ * @param {string | null | undefined} endpoint
303
+ * @returns {{kind:'colima'|'orbstack'|'local-socket'|'remote'|'unknown', profile?:string}}
304
+ */
305
+ export function classifyEndpoint(endpoint) {
306
+ const value = textOf(endpoint)
307
+ if (value === '') return { kind: 'unknown' }
308
+ const colima = /\.colima\/([^/]+)\/docker\.sock/iu.exec(value)
309
+ if (colima !== null) return { kind: 'colima', profile: colima[1] }
310
+ if (/colima/iu.test(value)) return { kind: 'colima', profile: 'default' }
311
+ if (/orbstack/iu.test(value)) return { kind: 'orbstack' }
312
+ if (/^(?:tcp|ssh|http|https):\/\//iu.test(value)) return { kind: 'remote' }
313
+ if (/^(?:unix|npipe):/iu.test(value)) return { kind: 'local-socket' }
314
+ return { kind: 'unknown' }
315
+ }
316
+
317
+ /** @param {string} text */
318
+ export function parseSystemDf(text) {
319
+ return parseNdjson(text).map((row) => ({
320
+ type: textOf(row.Type),
321
+ totalCount: Number(row.TotalCount) || 0,
322
+ activeCount: Number(row.ActiveCount) || 0,
323
+ size: textOf(row.Size),
324
+ reclaimable: textOf(row.Reclaimable),
325
+ raw: row,
326
+ }))
327
+ }
328
+
329
+ /** @param {string} text */
330
+ export function parseAppleJson(text) {
331
+ const value = JSON.parse(text.trim())
332
+ return value
333
+ }
334
+
335
+ /** @param {Record<string, any>} raw */
336
+ function applePort(value) {
337
+ if (typeof value === 'string') return value
338
+ if (value === null || typeof value !== 'object') return ''
339
+ const host = textOf(value.hostAddress ?? value.address ?? value.host ?? '0.0.0.0')
340
+ const hostPort = textOf(value.hostPort ?? value.host_port ?? '')
341
+ const containerPort = textOf(value.containerPort ?? value.container_port ?? value.targetPort ?? '')
342
+ const protocol = textOf(value.protocol ?? 'tcp')
343
+ if (containerPort === '') return ''
344
+ return `${host}${hostPort === '' ? '' : `:${hostPort}`}->${containerPort}/${protocol}`
345
+ }
346
+
347
+ /** @param {Record<string, any>} raw */
348
+ export function normalizeAppleContainer(raw) {
349
+ const config = raw.configuration !== null && typeof raw.configuration === 'object' ? raw.configuration : {}
350
+ const status = raw.status !== null && typeof raw.status === 'object' ? raw.status : {}
351
+ const init = config.initProcess !== null && typeof config.initProcess === 'object' ? config.initProcess : {}
352
+ const args = Array.isArray(init.arguments) ? init.arguments.map(String) : []
353
+ const networks = Array.isArray(config.networks)
354
+ ? config.networks.map((entry) => textOf(entry?.network ?? entry?.name ?? entry)).filter(Boolean)
355
+ : []
356
+ const mounts = Array.isArray(config.mounts) ? config.mounts : []
357
+ return {
358
+ id: textOf(config.id ?? raw.id),
359
+ name: textOf(config.id ?? raw.id),
360
+ names: [textOf(config.id ?? raw.id)].filter(Boolean),
361
+ image: textOf(config.image?.reference ?? config.image?.descriptor?.digest),
362
+ command: [textOf(init.executable), ...args].filter(Boolean).join(' '),
363
+ createdAt: textOf(config.creationDate),
364
+ runningFor: textOf(status.startedDate),
365
+ ports: (Array.isArray(config.publishedPorts) ? config.publishedPorts : []).map(applePort).filter(Boolean),
366
+ status: textOf(status.state),
367
+ state: textOf(status.state).toLowerCase() || 'unknown',
368
+ size: '',
369
+ labels: config.labels !== null && typeof config.labels === 'object' ? config.labels : {},
370
+ mounts,
371
+ networks: networks.join(','),
372
+ raw,
373
+ }
374
+ }
375
+
376
+ /** @param {Record<string, any>} raw */
377
+ export function normalizeAppleImage(raw) {
378
+ const config = raw.configuration !== null && typeof raw.configuration === 'object' ? raw.configuration : {}
379
+ const reference = textOf(config.name ?? raw.id)
380
+ const lastSlash = reference.lastIndexOf('/')
381
+ const lastColon = reference.lastIndexOf(':')
382
+ const hasTag = lastColon > lastSlash
383
+ const repository = hasTag ? reference.slice(0, lastColon) : reference
384
+ const tag = hasTag ? reference.slice(lastColon + 1) : ''
385
+ const variants = Array.isArray(raw.variants) ? raw.variants : []
386
+ const size = variants.reduce((sum, variant) => sum + (Number(variant?.size) || 0), 0)
387
+ const architectures = []
388
+ for (const variant of variants) {
389
+ const platform = variant?.platform ?? {
390
+ os: variant?.config?.os,
391
+ architecture: variant?.config?.architecture,
392
+ variant: variant?.config?.variant,
393
+ }
394
+ const parts = [platform?.os, platform?.architecture, platform?.variant].filter(Boolean)
395
+ if (parts.length >= 2) architectures.push(parts.join('/'))
396
+ }
397
+ return {
398
+ id: textOf(raw.id ?? config.descriptor?.digest),
399
+ repository,
400
+ tag,
401
+ name: reference,
402
+ createdAt: textOf(config.creationDate),
403
+ createdSince: textOf(config.creationDate),
404
+ size: size > 0 ? formatBytes(size) : '',
405
+ sharedSize: '',
406
+ uniqueSize: '',
407
+ containers: '',
408
+ architecture: [...new Set(architectures)].join(', '),
409
+ raw,
410
+ }
411
+ }
412
+
413
+ /** @param {Record<string, any>} raw */
414
+ export function normalizeAppleVolume(raw) {
415
+ const config = raw.configuration !== null && typeof raw.configuration === 'object' ? raw.configuration : {}
416
+ return {
417
+ name: textOf(config.name ?? raw.id),
418
+ driver: textOf(config.driver),
419
+ scope: 'local',
420
+ mountpoint: textOf(config.source),
421
+ labels: config.labels !== null && typeof config.labels === 'object' ? config.labels : {},
422
+ createdAt: textOf(config.creationDate),
423
+ size: Number(config.sizeInBytes) || 0,
424
+ raw,
425
+ }
426
+ }
427
+
428
+ /** @param {Record<string, any>} raw */
429
+ export function normalizeAppleNetwork(raw) {
430
+ const config = raw.configuration !== null && typeof raw.configuration === 'object' ? raw.configuration : {}
431
+ const status = raw.status !== null && typeof raw.status === 'object' ? raw.status : {}
432
+ return {
433
+ id: textOf(config.name ?? raw.id),
434
+ name: textOf(config.name ?? raw.id),
435
+ driver: textOf(config.plugin ?? config.mode),
436
+ scope: 'local',
437
+ ipv4: textOf(status.ipv4Subnet),
438
+ ipv6: textOf(status.ipv6Subnet),
439
+ internal: textOf(config.mode),
440
+ createdAt: textOf(config.creationDate),
441
+ labels: config.labels !== null && typeof config.labels === 'object' ? config.labels : {},
442
+ raw,
443
+ }
444
+ }
445
+
446
+ /** @param {Record<string, any>} raw */
447
+ export function parseAppleSystemStatus(raw) {
448
+ return {
449
+ status: textOf(raw?.status),
450
+ apiServerVersion: textOf(raw?.apiServerVersion),
451
+ apiServerBuild: textOf(raw?.apiServerBuild),
452
+ apiServerCommit: textOf(raw?.apiServerCommit),
453
+ appRoot: textOf(raw?.appRoot),
454
+ installRoot: textOf(raw?.installRoot),
455
+ raw,
456
+ }
457
+ }
458
+
459
+ /** @param {any[]} raw */
460
+ export function parseAppleSystemVersion(raw) {
461
+ const rows = Array.isArray(raw) ? raw : raw === null || typeof raw !== 'object' ? [] : [raw]
462
+ const component = (name) => rows.find((row) => textOf(row?.appName) === name)
463
+ return {
464
+ containerVersion: textOf(component('container')?.version),
465
+ apiServerVersion: textOf(component('container-apiserver')?.version),
466
+ components: rows,
467
+ }
468
+ }
469
+
470
+ /** @param {Record<string, any>} raw */
471
+ export function parseAppleDf(raw) {
472
+ const item = (key, type) => {
473
+ const value = raw?.[key] !== null && typeof raw?.[key] === 'object' ? raw[key] : {}
474
+ return {
475
+ type,
476
+ totalCount: Number(value.total) || 0,
477
+ activeCount: Number(value.active) || 0,
478
+ size: formatBytes(Number(value.sizeInBytes) || 0),
479
+ reclaimable: formatBytes(Number(value.reclaimable) || 0),
480
+ raw: value,
481
+ }
482
+ }
483
+ return [item('images', 'Images'), item('containers', 'Containers'), item('volumes', 'Local Volumes')]
484
+ }