dsh-plugin-container 0.3.0 → 0.4.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/README.md +72 -45
- package/README.zh-CN.md +108 -0
- package/lib/client.js +481 -310
- package/package.json +13 -5
- package/src/client/build-image.jsx +119 -0
- package/src/client/i18n.js +22 -0
- package/src/client/icons.jsx +14 -0
- package/src/client/styles.js +1 -0
- package/src/client/tabs/images.jsx +23 -2
- package/src/host/apple.js +14 -0
- package/src/host/service.js +21 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-container",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "DSH sidebar container panel for macOS Docker runtimes and Apple container",
|
|
6
6
|
"type": "module",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"src/**/*.jsx",
|
|
18
18
|
"cordis.patch.yml",
|
|
19
19
|
"README.md",
|
|
20
|
+
"README.zh-CN.md",
|
|
20
21
|
"docs/*.png"
|
|
21
22
|
],
|
|
22
23
|
"dsh": {
|
|
@@ -40,9 +41,7 @@
|
|
|
40
41
|
"prepack": "npm run build && npm test"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
|
-
"esbuild": "0.25.0"
|
|
44
|
-
},
|
|
45
|
-
"dependencies": {
|
|
44
|
+
"esbuild": "0.25.0",
|
|
46
45
|
"@deepseek-ai/dsh-tools": "^0.2.1-rc.5"
|
|
47
46
|
},
|
|
48
47
|
"license": "MIT",
|
|
@@ -60,5 +59,14 @@
|
|
|
60
59
|
},
|
|
61
60
|
"publishConfig": {
|
|
62
61
|
"access": "public"
|
|
63
|
-
}
|
|
62
|
+
},
|
|
63
|
+
"repository": {
|
|
64
|
+
"type": "git",
|
|
65
|
+
"url": "git+https://github.com/Zaynervewreck/dsh-plugin-container.git"
|
|
66
|
+
},
|
|
67
|
+
"homepage": "https://github.com/Zaynervewreck/dsh-plugin-container#readme",
|
|
68
|
+
"bugs": {
|
|
69
|
+
"url": "https://github.com/Zaynervewreck/dsh-plugin-container/issues"
|
|
70
|
+
},
|
|
71
|
+
"author": "Zaynervewreck"
|
|
64
72
|
}
|
|
@@ -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
|
+
}
|
package/src/client/i18n.js
CHANGED
|
@@ -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',
|
package/src/client/icons.jsx
CHANGED
|
@@ -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" />
|
package/src/client/styles.js
CHANGED
|
@@ -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,
|
|
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
|
}
|
package/src/host/service.js
CHANGED
|
@@ -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]
|