dsh-plugin-container 0.3.1 → 0.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-container",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "description": "DSH sidebar container panel for macOS Docker runtimes and Apple container",
6
6
  "type": "module",
@@ -41,8 +41,7 @@
41
41
  "prepack": "npm run build && npm test"
42
42
  },
43
43
  "devDependencies": {
44
- "esbuild": "0.25.0",
45
- "@deepseek-ai/dsh-tools": "^0.2.1-rc.5"
44
+ "esbuild": "0.25.0"
46
45
  },
47
46
  "license": "MIT",
48
47
  "keywords": [
@@ -68,5 +67,8 @@
68
67
  "bugs": {
69
68
  "url": "https://github.com/Zaynervewreck/dsh-plugin-container/issues"
70
69
  },
71
- "author": "Zaynervewreck"
70
+ "author": "Zaynervewreck",
71
+ "dependencies": {
72
+ "@deepseek-ai/dsh-tools": "^0.2.1-rc.5"
73
+ }
72
74
  }
@@ -0,0 +1,119 @@
1
+ /** Build a Docker/Apple image from a Dockerfile. */
2
+ import { useEffect, useState } from 'react'
3
+
4
+ import { Button, Modal } from './components.jsx'
5
+
6
+ const IMAGE_REF = /^[A-Za-z0-9][A-Za-z0-9_.:/@-]*$/u
7
+
8
+ export default function BuildImageDialog({ open, t, provider, cwd, onClose, onSubmit }) {
9
+ const [tag, setTag] = useState('')
10
+ const [contextPath, setContextPath] = useState('')
11
+ const [dockerfile, setDockerfile] = useState('Dockerfile')
12
+ const [noCache, setNoCache] = useState(false)
13
+ const [error, setError] = useState(null)
14
+ const [busy, setBusy] = useState(false)
15
+
16
+ useEffect(() => {
17
+ if (!open) return
18
+ setTag('')
19
+ setContextPath(cwd || '')
20
+ setDockerfile('Dockerfile')
21
+ setNoCache(false)
22
+ setError(null)
23
+ setBusy(false)
24
+ }, [open, cwd])
25
+
26
+ const close = () => {
27
+ if (busy) return
28
+ onClose()
29
+ }
30
+
31
+ const submit = async (event) => {
32
+ event?.preventDefault()
33
+ const imageTag = tag.trim()
34
+ const context = contextPath.trim()
35
+ if (!IMAGE_REF.test(imageTag)) {
36
+ setError(t('buildInvalidTag'))
37
+ return
38
+ }
39
+ if (!context.startsWith('/')) {
40
+ setError(t('buildInvalidContext'))
41
+ return
42
+ }
43
+ setBusy(true)
44
+ setError(null)
45
+ try {
46
+ await onSubmit({
47
+ kind: 'image',
48
+ action: 'build',
49
+ tag: imageTag,
50
+ contextPath: context,
51
+ dockerfile: dockerfile.trim() === '' ? undefined : dockerfile.trim(),
52
+ noCache,
53
+ })
54
+ setBusy(false)
55
+ onClose()
56
+ } catch (submitError) {
57
+ setError(submitError instanceof Error ? submitError.message : String(submitError))
58
+ setBusy(false)
59
+ }
60
+ }
61
+
62
+ return (
63
+ <Modal
64
+ open={open}
65
+ title={t('buildImageTitle')}
66
+ subtitle={provider === 'apple-container' ? t('providerApple') : t('providerDocker')}
67
+ onClose={close}
68
+ closeLabel={t('close')}
69
+ footer={
70
+ <>
71
+ <Button size="sm" variant="ghost" onClick={close} disabled={busy}>{t('modalCancel')}</Button>
72
+ <Button size="sm" variant="primary" onClick={submit} disabled={busy}>
73
+ {busy ? t('starting') : t('buildStart')}
74
+ </Button>
75
+ </>
76
+ }
77
+ >
78
+ <form className="dsh-docker-run-form" onSubmit={submit}>
79
+ <label className="dsh-docker-field">
80
+ <span>{t('buildTag')}</span>
81
+ <input
82
+ className="dsh-docker-input"
83
+ value={tag}
84
+ onChange={(event) => setTag(event.target.value)}
85
+ placeholder="my-app:latest"
86
+ spellCheck="false"
87
+ autoFocus
88
+ />
89
+ </label>
90
+ <label className="dsh-docker-field">
91
+ <span>{t('buildContext')}</span>
92
+ <input
93
+ className="dsh-docker-input"
94
+ value={contextPath}
95
+ onChange={(event) => setContextPath(event.target.value)}
96
+ placeholder="/Users/me/project"
97
+ spellCheck="false"
98
+ />
99
+ <small className="dsh-docker-muted">{t('buildContextHint')}</small>
100
+ </label>
101
+ <label className="dsh-docker-field">
102
+ <span>{t('buildDockerfile')}</span>
103
+ <input
104
+ className="dsh-docker-input"
105
+ value={dockerfile}
106
+ onChange={(event) => setDockerfile(event.target.value)}
107
+ placeholder="Dockerfile"
108
+ spellCheck="false"
109
+ />
110
+ </label>
111
+ <label className="dsh-docker-check">
112
+ <input type="checkbox" checked={noCache} onChange={(event) => setNoCache(event.target.checked)} />
113
+ {t('buildNoCache')}
114
+ </label>
115
+ {error !== null && <div className="dsh-docker-form-error">{error}</div>}
116
+ </form>
117
+ </Modal>
118
+ )
119
+ }
@@ -117,6 +117,17 @@ export const zh = {
117
117
  removeNetwork: '删除网络',
118
118
  pull: '拉取',
119
119
  pullPlaceholder: 'nginx:latest',
120
+ pushImage: '推送镜像',
121
+ buildImage: '构建镜像',
122
+ buildImageTitle: '构建镜像',
123
+ buildTag: '镜像标签',
124
+ buildContext: '构建上下文(绝对路径)',
125
+ buildContextHint: '例如 /Users/me/project,必须包含 Dockerfile。',
126
+ buildDockerfile: 'Dockerfile',
127
+ buildNoCache: '不使用构建缓存',
128
+ buildStart: '开始构建',
129
+ buildInvalidTag: '请输入合法的镜像标签,例如 my-app:latest',
130
+ buildInvalidContext: '构建上下文必须是绝对路径',
120
131
  pullAction: '拉取镜像',
121
132
  emptyContainers: '没有容器',
122
133
  emptyImages: '没有镜像',
@@ -363,6 +374,17 @@ export const en = {
363
374
  removeNetwork: 'Remove network',
364
375
  pull: 'Pull',
365
376
  pullPlaceholder: 'nginx:latest',
377
+ pushImage: 'Push image',
378
+ buildImage: 'Build image',
379
+ buildImageTitle: 'Build image',
380
+ buildTag: 'Image tag',
381
+ buildContext: 'Build context (absolute path)',
382
+ buildContextHint: 'For example /Users/me/project; it must contain the Dockerfile.',
383
+ buildDockerfile: 'Dockerfile',
384
+ buildNoCache: 'Disable build cache',
385
+ buildStart: 'Build',
386
+ buildInvalidTag: 'Enter a valid image tag, for example my-app:latest',
387
+ buildInvalidContext: 'Build context must be an absolute path',
366
388
  pullAction: 'Pull image',
367
389
  emptyContainers: 'No containers',
368
390
  emptyImages: 'No images',
@@ -214,6 +214,20 @@ export const IconExec = (props) => (
214
214
  </Svg>
215
215
  )
216
216
 
217
+ export const IconUpload = (props) => (
218
+ <Svg {...props}>
219
+ <path d="M12 16V4M7.5 8.5 12 4l4.5 4.5" />
220
+ <path d="M5 15v3.5A1.5 1.5 0 0 0 6.5 20h11a1.5 1.5 0 0 0 1.5-1.5V15" />
221
+ </Svg>
222
+ )
223
+
224
+ export const IconBuild = (props) => (
225
+ <Svg {...props}>
226
+ <path d="M14.5 5.5 18.5 9.5M4 20l4.2-1 10.9-10.9a2.1 2.1 0 0 0-3-3L5.2 16 4 20Z" />
227
+ <path d="m13.2 6.8 4 4" />
228
+ </Svg>
229
+ )
230
+
217
231
  export const IconAttach = (props) => (
218
232
  <Svg {...props}>
219
233
  <path d="M9.5 14.5 14.5 9.5" />
@@ -220,6 +220,7 @@ const CSS = `
220
220
  .dsh-docker-field > span { font-size: 11.5px; color: var(--dsw-alias-label-secondary, #60636c); }
221
221
  .dsh-docker-textarea { height: auto; min-height: 72px; padding: 8px 10px; resize: vertical; font-family: var(--dsw-font-markdown-code-font-family, ui-monospace, Menlo, monospace); line-height: 1.5; }
222
222
  .dsh-docker-form-error { color: var(--dsw-alias-label-error, #d33c3c); font-size: 12px; }
223
+ .dsh-docker-check { display: flex; align-items: center; gap: 7px; font-size: 12px; color: var(--dsw-alias-label-secondary, #60636c); }
223
224
 
224
225
 
225
226
  .dsh-docker-table-wrap { overflow: auto; border-radius: 0 0 11px 11px; }
@@ -11,15 +11,17 @@ import {
11
11
  displayValue,
12
12
  shortId,
13
13
  } from '../components.jsx'
14
+ import BuildImageDialog from '../build-image.jsx'
14
15
  import { useDockerList } from '../hooks.js'
15
- import { IconAlert, IconImage, IconInfo, IconPlay, IconTrash, IconDownload } from '../icons.jsx'
16
+ import { IconAlert, IconBuild, IconDownload, IconImage, IconInfo, IconPlay, IconTrash, IconUpload } from '../icons.jsx'
16
17
 
17
18
  const IMAGE_REF = /^[A-Za-z0-9][A-Za-z0-9_.:/@-]*$/u
18
19
 
19
- export default function ImagesTab({ t, api, provider, context, status, refreshToken, features = [], onAction, onRunContainer, onInspect, onNotify }) {
20
+ export default function ImagesTab({ t, api, provider, context, status, refreshToken, features = [], cwd, onAction, onRunContainer, onInspect, onNotify }) {
20
21
  const showArchitecture = features.includes('image-architecture')
21
22
  const [search, setSearch] = useState('')
22
23
  const [pull, setPull] = useState('')
24
+ const [buildOpen, setBuildOpen] = useState(false)
23
25
  const enabled = status?.daemon?.state === 'running'
24
26
  const list = useDockerList(api, provider, 'images', context, { refreshToken, enabled })
25
27
  const items = list.data?.items ?? []
@@ -61,6 +63,9 @@ export default function ImagesTab({ t, api, provider, context, status, refreshTo
61
63
  <div className="dsh-docker-toolbar-right">
62
64
  <span className="dsh-docker-muted">{filtered.length} / {items.length}</span>
63
65
  {!showArchitecture && <span className="dsh-docker-muted">{t('restartForArchitecture')}</span>}
66
+ {features.includes('image-build') && (
67
+ <Button size="sm" onClick={() => setBuildOpen(true)}><IconBuild size={13} /> {t('buildImage')}</Button>
68
+ )}
64
69
  <Button size="sm" onClick={list.reload} disabled={list.loading}>{t('refresh')}</Button>
65
70
  </div>
66
71
  </div>
@@ -106,6 +111,11 @@ export default function ImagesTab({ t, api, provider, context, status, refreshTo
106
111
  <IconPlay size={14} />
107
112
  </IconButton>
108
113
  )}
114
+ {features.includes('image-push') && item.name !== '<none>' && (
115
+ <IconButton size="sm" label={t('pushImage')} onClick={() => onAction({ kind: 'image', action: 'push', id: item.name }, { focusConsole: true })}>
116
+ <IconUpload size={14} />
117
+ </IconButton>
118
+ )}
109
119
  <IconButton size="sm" label={t('inspect')} onClick={() => onInspect('image', inspectId)}>
110
120
  <IconInfo size={14} />
111
121
  </IconButton>
@@ -122,6 +132,17 @@ export default function ImagesTab({ t, api, provider, context, status, refreshTo
122
132
  </div>
123
133
  </section>
124
134
  )}
135
+
136
+ <BuildImageDialog
137
+ open={buildOpen}
138
+ t={t}
139
+ provider={provider}
140
+ cwd={cwd}
141
+ onClose={() => setBuildOpen(false)}
142
+ onSubmit={async (input) => {
143
+ await onAction(input, { focusConsole: true })
144
+ }}
145
+ />
125
146
  </div>
126
147
  )
127
148
  }
package/src/host/apple.js CHANGED
@@ -397,6 +397,20 @@ export class AppleProvider {
397
397
  if (action === 'pull') {
398
398
  return this.run({ argv: ['image', 'pull', safeImageRef(payload.id ?? payload.reference)], cwd: payload.cwd, title: payload.title ?? 'container image pull' }, signal)
399
399
  }
400
+ if (action === 'push') {
401
+ return this.run({ argv: ['image', 'push', safeImageRef(payload.id ?? payload.reference)], cwd: payload.cwd, title: payload.title ?? 'container image push' }, signal)
402
+ }
403
+ if (action === 'build') {
404
+ const tag = safeImageRef(payload.tag)
405
+ const contextPath = resolveCwd(payload.contextPath === undefined ? this.home : requireString(payload.contextPath, 'contextPath'), { fallback: this.home, statSync })
406
+ const dockerfile = payload.dockerfile === undefined || payload.dockerfile === null ? undefined : requireString(payload.dockerfile, 'dockerfile')
407
+ if (dockerfile !== undefined && (dockerfile.includes('\0') || dockerfile.startsWith('-') || dockerfile.length > 4096)) throw new CommandError('Dockerfile 路径不合法')
408
+ const argv = ['build', '--progress', 'plain', '-t', tag]
409
+ if (dockerfile !== undefined && dockerfile !== '') argv.push('-f', dockerfile)
410
+ if (payload.noCache === true) argv.push('--no-cache')
411
+ argv.push(contextPath)
412
+ return this.run({ argv, cwd: contextPath, timeoutMs: payload.timeoutMs ?? 15 * 60_000, title: payload.title ?? `container build ${tag}` }, signal)
413
+ }
400
414
  if (action === 'delete') {
401
415
  return this.run({ argv: ['image', 'delete', safeId(payload.id)], confirm: payload.confirm, cwd: payload.cwd, title: payload.title ?? 'container image delete' }, signal)
402
416
  }
@@ -256,7 +256,7 @@ export class DockerService {
256
256
  }
257
257
  providers.push(await this.apple.info(signal))
258
258
  const defaultProvider = providers.find((provider) => provider.id === 'docker' && provider.available)?.id ?? providers.find((provider) => provider.available)?.id ?? 'docker'
259
- const features = ['attach', 'exec', 'run', 'image-architecture']
259
+ const features = ['attach', 'exec', 'run', 'image-architecture', 'image-push', 'image-build']
260
260
  if (this.toolsEnabled) features.push('ai-tools')
261
261
  return { providers, defaultProvider, features }
262
262
  }
@@ -736,6 +736,25 @@ export class DockerService {
736
736
  title: payload.title ?? `启动容器 ${image}`,
737
737
  }, signal)
738
738
  }
739
+ if (kind === 'image' && action === 'build') {
740
+ const tag = safeImageRef(payload.tag)
741
+ const contextPath = resolveCwd(optionalString(payload.contextPath, 'contextPath') ?? this.home, { fallback: this.home, statSync })
742
+ const dockerfile = optionalString(payload.dockerfile, 'dockerfile')
743
+ if (dockerfile !== undefined && (dockerfile.includes('\0') || dockerfile.startsWith('-') || dockerfile.length > 4096)) {
744
+ throw new CommandError('Dockerfile 路径不合法')
745
+ }
746
+ const argv = ['build', '--progress', 'plain', '-t', tag]
747
+ if (dockerfile !== undefined && dockerfile !== '') argv.push('-f', dockerfile)
748
+ if (payload.noCache === true) argv.push('--no-cache')
749
+ argv.push(contextPath)
750
+ return this.run({
751
+ argv,
752
+ context: payload.context,
753
+ cwd: contextPath,
754
+ timeoutMs: payload.timeoutMs ?? 15 * 60_000,
755
+ title: payload.title ?? `构建镜像 ${tag}`,
756
+ }, signal)
757
+ }
739
758
  const id = safeIdentifier(payload.id ?? payload.reference)
740
759
  let argv
741
760
  if (kind === 'container') {
@@ -746,6 +765,7 @@ export class DockerService {
746
765
  else if (action === 'logs') argv = ['logs', '--tail', payload.follow === true ? '100' : '500', ...(payload.follow === true ? ['-f'] : []), id]
747
766
  } else if (kind === 'image') {
748
767
  if (action === 'pull') argv = ['pull', id]
768
+ else if (action === 'push') argv = ['push', id]
749
769
  else if (action === 'delete') argv = ['rmi', id]
750
770
  } else if (kind === 'volume' && action === 'delete') {
751
771
  argv = ['volume', 'rm', id]