dsh-plugin-container 0.0.0-stage → 0.2.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 +341 -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 +3499 -0
- package/package.json +62 -4
- package/src/client/api.js +94 -0
- package/src/client/attach.jsx +133 -0
- package/src/client/components.jsx +301 -0
- package/src/client/hooks.js +161 -0
- package/src/client/i18n.js +496 -0
- package/src/client/icons.jsx +219 -0
- package/src/client/index.jsx +76 -0
- package/src/client/page.jsx +755 -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 +363 -0
- package/src/client/tabs/compose.jsx +94 -0
- package/src/client/tabs/console.jsx +211 -0
- package/src/client/tabs/containers.jsx +143 -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 +243 -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 +832 -0
- package/src/host/tools.js +198 -0
- package/src/host/util.js +97 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/** Start a new container from a local image or a manually entered reference. */
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Button, Modal } from './components.jsx'
|
|
5
|
+
import { useDockerList } from './hooks.js'
|
|
6
|
+
|
|
7
|
+
export default function RunContainerDialog({
|
|
8
|
+
open,
|
|
9
|
+
t,
|
|
10
|
+
api,
|
|
11
|
+
provider,
|
|
12
|
+
context,
|
|
13
|
+
status,
|
|
14
|
+
refreshToken = 0,
|
|
15
|
+
features = [],
|
|
16
|
+
initialImage = '',
|
|
17
|
+
onClose,
|
|
18
|
+
onSubmit,
|
|
19
|
+
}) {
|
|
20
|
+
const [image, setImage] = useState('')
|
|
21
|
+
const [manual, setManual] = useState(false)
|
|
22
|
+
const [customImage, setCustomImage] = useState('')
|
|
23
|
+
const [name, setName] = useState('')
|
|
24
|
+
const [command, setCommand] = useState('')
|
|
25
|
+
const [ports, setPorts] = useState('')
|
|
26
|
+
const [env, setEnv] = useState('')
|
|
27
|
+
const [error, setError] = useState(null)
|
|
28
|
+
const [busy, setBusy] = useState(false)
|
|
29
|
+
const images = useDockerList(api, provider, 'images', context, {
|
|
30
|
+
refreshToken,
|
|
31
|
+
enabled: open && features.includes('run') && status?.daemon?.state === 'running',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const options = useMemo(() => {
|
|
35
|
+
const names = []
|
|
36
|
+
for (const item of images.data?.items ?? []) {
|
|
37
|
+
if (typeof item.name === 'string' && item.name !== '' && item.name !== '<none>' && !names.includes(item.name)) names.push(item.name)
|
|
38
|
+
}
|
|
39
|
+
if (initialImage !== '' && !names.includes(initialImage)) names.unshift(initialImage)
|
|
40
|
+
return names
|
|
41
|
+
}, [images.data, initialImage])
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!open) return
|
|
45
|
+
setImage(initialImage)
|
|
46
|
+
setManual(false)
|
|
47
|
+
setCustomImage(initialImage)
|
|
48
|
+
setName('')
|
|
49
|
+
setCommand('')
|
|
50
|
+
setPorts('')
|
|
51
|
+
setEnv('')
|
|
52
|
+
setError(null)
|
|
53
|
+
setBusy(false)
|
|
54
|
+
}, [open, initialImage])
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!open || manual) return
|
|
58
|
+
if (initialImage !== '' && options.includes(initialImage)) {
|
|
59
|
+
setImage(initialImage)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
if (image === '' || !options.includes(image)) {
|
|
63
|
+
if (options.length > 0) setImage(options[0])
|
|
64
|
+
}
|
|
65
|
+
}, [open, options, image, manual, initialImage])
|
|
66
|
+
|
|
67
|
+
const reset = () => {
|
|
68
|
+
setImage('')
|
|
69
|
+
setManual(false)
|
|
70
|
+
setCustomImage('')
|
|
71
|
+
setName('')
|
|
72
|
+
setCommand('')
|
|
73
|
+
setPorts('')
|
|
74
|
+
setEnv('')
|
|
75
|
+
setError(null)
|
|
76
|
+
setBusy(false)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const close = () => {
|
|
80
|
+
reset()
|
|
81
|
+
onClose()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const submit = async (event) => {
|
|
85
|
+
event?.preventDefault()
|
|
86
|
+
const ref = (manual ? customImage : image).trim()
|
|
87
|
+
if (ref === '') {
|
|
88
|
+
setError(t('runImageRequired'))
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
setBusy(true)
|
|
92
|
+
setError(null)
|
|
93
|
+
try {
|
|
94
|
+
await onSubmit({
|
|
95
|
+
kind: 'container',
|
|
96
|
+
action: 'run',
|
|
97
|
+
image: ref,
|
|
98
|
+
name: name.trim() === '' ? undefined : name.trim(),
|
|
99
|
+
command: command.trim() === '' ? undefined : command.trim(),
|
|
100
|
+
ports: ports.split(/[\n,]+/u).map((entry) => entry.trim()).filter(Boolean),
|
|
101
|
+
env: env.split(/\n/u).map((entry) => entry.trim()).filter(Boolean),
|
|
102
|
+
})
|
|
103
|
+
reset()
|
|
104
|
+
onClose()
|
|
105
|
+
} catch (submitError) {
|
|
106
|
+
setError(submitError instanceof Error ? submitError.message : String(submitError))
|
|
107
|
+
setBusy(false)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Modal
|
|
113
|
+
open={open}
|
|
114
|
+
title={t('runContainerTitle')}
|
|
115
|
+
subtitle={provider === 'apple-container' ? t('providerApple') : t('providerDocker')}
|
|
116
|
+
onClose={close}
|
|
117
|
+
closeLabel={t('close')}
|
|
118
|
+
footer={
|
|
119
|
+
<>
|
|
120
|
+
<Button size="sm" variant="ghost" onClick={close} disabled={busy}>{t('modalCancel')}</Button>
|
|
121
|
+
<Button size="sm" variant="primary" onClick={submit} disabled={busy}>
|
|
122
|
+
{busy ? t('starting') : t('runStart')}
|
|
123
|
+
</Button>
|
|
124
|
+
</>
|
|
125
|
+
}
|
|
126
|
+
>
|
|
127
|
+
<form className="dsh-docker-run-form" onSubmit={submit}>
|
|
128
|
+
<label className="dsh-docker-field">
|
|
129
|
+
<span>{t('runImage')}</span>
|
|
130
|
+
<div className="dsh-docker-image-picker">
|
|
131
|
+
<select
|
|
132
|
+
className="dsh-docker-select"
|
|
133
|
+
value={manual ? '__custom__' : image}
|
|
134
|
+
onChange={(event) => {
|
|
135
|
+
if (event.target.value === '__custom__') {
|
|
136
|
+
setManual(true)
|
|
137
|
+
setCustomImage(image)
|
|
138
|
+
} else {
|
|
139
|
+
setManual(false)
|
|
140
|
+
setImage(event.target.value)
|
|
141
|
+
}
|
|
142
|
+
}}
|
|
143
|
+
autoFocus
|
|
144
|
+
>
|
|
145
|
+
<option value="">{t('runImageSelect')}</option>
|
|
146
|
+
{options.map((name) => <option key={name} value={name}>{name}</option>)}
|
|
147
|
+
<option value="__custom__">{t('runImageCustom')}</option>
|
|
148
|
+
</select>
|
|
149
|
+
<Button size="sm" variant="ghost" onClick={images.reload} disabled={images.loading}>{t('refresh')}</Button>
|
|
150
|
+
</div>
|
|
151
|
+
{manual && (
|
|
152
|
+
<input
|
|
153
|
+
className="dsh-docker-input"
|
|
154
|
+
value={customImage}
|
|
155
|
+
onChange={(event) => setCustomImage(event.target.value)}
|
|
156
|
+
placeholder={t('runImagePlaceholder')}
|
|
157
|
+
spellCheck="false"
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
160
|
+
{images.data !== null && options.length === 0 && !manual && (
|
|
161
|
+
<span className="dsh-docker-muted">{t('runImageListEmpty')}</span>
|
|
162
|
+
)}
|
|
163
|
+
</label>
|
|
164
|
+
<div className="dsh-docker-run-grid">
|
|
165
|
+
<label className="dsh-docker-field">
|
|
166
|
+
<span>{t('runName')}</span>
|
|
167
|
+
<input className="dsh-docker-input" value={name} onChange={(event) => setName(event.target.value)} placeholder={t('runNamePlaceholder')} spellCheck="false" />
|
|
168
|
+
</label>
|
|
169
|
+
<label className="dsh-docker-field">
|
|
170
|
+
<span>{t('runPorts')}</span>
|
|
171
|
+
<input className="dsh-docker-input" value={ports} onChange={(event) => setPorts(event.target.value)} placeholder={t('runPortsPlaceholder')} spellCheck="false" />
|
|
172
|
+
</label>
|
|
173
|
+
</div>
|
|
174
|
+
<label className="dsh-docker-field">
|
|
175
|
+
<span>{t('runCommand')}</span>
|
|
176
|
+
<input className="dsh-docker-input" value={command} onChange={(event) => setCommand(event.target.value)} placeholder={t('runCommandPlaceholder')} spellCheck="false" />
|
|
177
|
+
</label>
|
|
178
|
+
<label className="dsh-docker-field">
|
|
179
|
+
<span>{t('runEnv')}</span>
|
|
180
|
+
<textarea className="dsh-docker-input dsh-docker-textarea" value={env} onChange={(event) => setEnv(event.target.value)} placeholder={t('runEnvPlaceholder')} spellCheck="false" rows={3} />
|
|
181
|
+
</label>
|
|
182
|
+
<div className="dsh-docker-muted">{t('runDetachedHint')}</div>
|
|
183
|
+
{error !== null && <div className="dsh-docker-form-error">{error}</div>}
|
|
184
|
+
</form>
|
|
185
|
+
</Modal>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/** Single active command controller shared by the console and quick actions. */
|
|
2
|
+
|
|
3
|
+
import { DockerApiError, isAbortError } from './api.js'
|
|
4
|
+
|
|
5
|
+
const POLL_MS = 400
|
|
6
|
+
const UI_OUTPUT_LIMIT = 1_500_000
|
|
7
|
+
|
|
8
|
+
/** @param {number} ms */
|
|
9
|
+
function sleep(ms) {
|
|
10
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** @param {string} text */
|
|
14
|
+
function trimOutput(text) {
|
|
15
|
+
if (text.length <= UI_OUTPUT_LIMIT) return { text, truncated: false }
|
|
16
|
+
return { text: `…[前面的输出已省略]\n${text.slice(-UI_OUTPUT_LIMIT)}`, truncated: true }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class RunController {
|
|
20
|
+
/** @param {{call: Function}} api */
|
|
21
|
+
constructor(api) {
|
|
22
|
+
this.api = api
|
|
23
|
+
this.snapshot = {
|
|
24
|
+
run: null,
|
|
25
|
+
stdout: '',
|
|
26
|
+
stderr: '',
|
|
27
|
+
truncated: false,
|
|
28
|
+
error: null,
|
|
29
|
+
}
|
|
30
|
+
this.subscribers = new Set()
|
|
31
|
+
this.generation = 0
|
|
32
|
+
this.stdoutOffset = 0
|
|
33
|
+
this.stderrOffset = 0
|
|
34
|
+
this.polling = false
|
|
35
|
+
this.onFinished = undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
subscribe = (listener) => {
|
|
39
|
+
this.subscribers.add(listener)
|
|
40
|
+
return () => this.subscribers.delete(listener)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getSnapshot = () => this.snapshot
|
|
44
|
+
|
|
45
|
+
/** @param {Partial<typeof this.snapshot>} patch */
|
|
46
|
+
update(patch) {
|
|
47
|
+
this.snapshot = { ...this.snapshot, ...patch }
|
|
48
|
+
for (const listener of [...this.subscribers]) listener()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get active() {
|
|
52
|
+
const status = this.snapshot.run?.status
|
|
53
|
+
return status === 'running' || status === 'cancelling'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Start one docker command. Throws DockerApiError on validation/transport
|
|
58
|
+
* errors, including `docker/confirmation-required`.
|
|
59
|
+
*/
|
|
60
|
+
async start(input) {
|
|
61
|
+
if (this.active) throw new DockerApiError('docker/run-active', '已有命令正在运行,请先取消')
|
|
62
|
+
const generation = ++this.generation
|
|
63
|
+
this.polling = false
|
|
64
|
+
this.stdoutOffset = 0
|
|
65
|
+
this.stderrOffset = 0
|
|
66
|
+
this.update({ run: null, stdout: '', stderr: '', truncated: false, error: null })
|
|
67
|
+
const value = await this.api.call('run', input)
|
|
68
|
+
if (generation !== this.generation) return value?.run ?? null
|
|
69
|
+
this.update({ run: value?.run ?? null })
|
|
70
|
+
if (value?.run !== undefined && value.run !== null) this.beginPolling(value.run.id, generation)
|
|
71
|
+
return value?.run ?? null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Start a runtime lifecycle action (colima/Docker Desktop/OrbStack). */
|
|
75
|
+
async startRuntime(action, context, confirm = false, provider = 'docker') {
|
|
76
|
+
if (this.active) throw new DockerApiError('docker/run-active', '已有命令正在运行,请先取消')
|
|
77
|
+
const generation = ++this.generation
|
|
78
|
+
this.polling = false
|
|
79
|
+
this.stdoutOffset = 0
|
|
80
|
+
this.stderrOffset = 0
|
|
81
|
+
this.update({ run: null, stdout: '', stderr: '', truncated: false, error: null })
|
|
82
|
+
const value = await this.api.call('runtimeAction', { provider, action, context: context ?? undefined, confirm })
|
|
83
|
+
if (generation !== this.generation) return value
|
|
84
|
+
if (value?.run !== undefined && value.run !== null) {
|
|
85
|
+
this.update({ run: value.run })
|
|
86
|
+
this.beginPolling(value.run.id, generation)
|
|
87
|
+
}
|
|
88
|
+
return value
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Start one semantic quick action (provider-specific mapping lives on the host). */
|
|
92
|
+
async startAction(input) {
|
|
93
|
+
if (this.active) throw new DockerApiError('docker/run-active', '已有命令正在运行,请先取消')
|
|
94
|
+
const generation = ++this.generation
|
|
95
|
+
this.polling = false
|
|
96
|
+
this.stdoutOffset = 0
|
|
97
|
+
this.stderrOffset = 0
|
|
98
|
+
this.update({ run: null, stdout: '', stderr: '', truncated: false, error: null })
|
|
99
|
+
const value = await this.api.call('action', input)
|
|
100
|
+
if (generation !== this.generation) return value?.run ?? null
|
|
101
|
+
this.update({ run: value?.run ?? null })
|
|
102
|
+
if (value?.run !== undefined && value.run !== null) this.beginPolling(value.run.id, generation)
|
|
103
|
+
return value?.run ?? null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** @param {string} runId @param {number} generation */
|
|
107
|
+
beginPolling(runId, generation) {
|
|
108
|
+
this.polling = true
|
|
109
|
+
const loop = async () => {
|
|
110
|
+
while (this.polling && generation === this.generation) {
|
|
111
|
+
await sleep(POLL_MS)
|
|
112
|
+
if (!this.polling || generation !== this.generation) return
|
|
113
|
+
let value
|
|
114
|
+
try {
|
|
115
|
+
value = await this.api.call('poll', {
|
|
116
|
+
runId,
|
|
117
|
+
stdoutOffset: this.stdoutOffset,
|
|
118
|
+
stderrOffset: this.stderrOffset,
|
|
119
|
+
})
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (isAbortError(error)) return
|
|
122
|
+
this.update({ error: error instanceof Error ? error.message : String(error) })
|
|
123
|
+
this.polling = false
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
if (generation !== this.generation) return
|
|
127
|
+
const stdout = `${this.snapshot.stdout}${value.stdout?.text ?? ''}`
|
|
128
|
+
const stderr = `${this.snapshot.stderr}${value.stderr?.text ?? ''}`
|
|
129
|
+
this.stdoutOffset = value.stdout?.nextOffset ?? this.stdoutOffset
|
|
130
|
+
this.stderrOffset = value.stderr?.nextOffset ?? this.stderrOffset
|
|
131
|
+
const trimmed = trimOutput(stdout)
|
|
132
|
+
const trimmedErr = trimOutput(stderr)
|
|
133
|
+
const run = value.run ?? this.snapshot.run
|
|
134
|
+
this.update({
|
|
135
|
+
run,
|
|
136
|
+
stdout: trimmed.text,
|
|
137
|
+
stderr: trimmedErr.text,
|
|
138
|
+
truncated: this.snapshot.truncated || trimmed.truncated || trimmedErr.truncated || value.stdout?.lossy === true || value.stderr?.lossy === true,
|
|
139
|
+
})
|
|
140
|
+
const status = run?.status
|
|
141
|
+
if (status !== 'running' && status !== 'cancelling') {
|
|
142
|
+
this.polling = false
|
|
143
|
+
this.onFinished?.(run)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
loop().catch((error) => {
|
|
149
|
+
this.update({ error: error instanceof Error ? error.message : String(error) })
|
|
150
|
+
this.polling = false
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async cancel() {
|
|
155
|
+
const run = this.snapshot.run
|
|
156
|
+
if (run === null || (run.status !== 'running' && run.status !== 'cancelling')) return
|
|
157
|
+
try {
|
|
158
|
+
const value = await this.api.call('cancel', { runId: run.id })
|
|
159
|
+
this.update({ run: value?.run ?? run })
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (!isAbortError(error)) this.update({ error: error instanceof Error ? error.message : String(error) })
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
reset() {
|
|
166
|
+
this.generation += 1
|
|
167
|
+
this.polling = false
|
|
168
|
+
this.stdoutOffset = 0
|
|
169
|
+
this.stderrOffset = 0
|
|
170
|
+
this.update({ run: null, stdout: '', stderr: '', truncated: false, error: null })
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
dispose() {
|
|
174
|
+
this.generation += 1
|
|
175
|
+
this.polling = false
|
|
176
|
+
const run = this.snapshot.run
|
|
177
|
+
if (run !== null && (run.status === 'running' || run.status === 'cancelling')) {
|
|
178
|
+
this.api.call('cancel', { runId: run.id }).catch(() => {})
|
|
179
|
+
}
|
|
180
|
+
this.subscribers.clear()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** Browser-local view state: active tab, selected context, console cwd. */
|
|
2
|
+
import { defineStore } from '@deepseek-ai/dsh-client-store'
|
|
3
|
+
|
|
4
|
+
const TAB_KEY = 'dsh.docker.tab'
|
|
5
|
+
const CONTEXT_KEY = 'dsh.docker.context'
|
|
6
|
+
const CWD_KEY = 'dsh.docker.cwd'
|
|
7
|
+
const PROVIDER_KEY = 'dsh.docker.provider'
|
|
8
|
+
const HISTORY_KEY = 'dsh.docker.history'
|
|
9
|
+
const VALID_TABS = new Set(['overview', 'containers', 'images', 'volumes', 'networks', 'compose', 'console'])
|
|
10
|
+
const VALID_PROVIDERS = new Set(['docker', 'apple-container'])
|
|
11
|
+
|
|
12
|
+
/** @param {string} key */
|
|
13
|
+
function read(key) {
|
|
14
|
+
try {
|
|
15
|
+
return typeof localStorage === 'undefined' ? null : localStorage.getItem(key)
|
|
16
|
+
} catch {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @param {string} key @param {string | null} value */
|
|
22
|
+
function write(key, value) {
|
|
23
|
+
try {
|
|
24
|
+
if (typeof localStorage === 'undefined') return
|
|
25
|
+
if (value === null || value === '') localStorage.removeItem(key)
|
|
26
|
+
else localStorage.setItem(key, value)
|
|
27
|
+
} catch {
|
|
28
|
+
// Storage can be unavailable in private contexts; view state still works.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function readHistory() {
|
|
33
|
+
try {
|
|
34
|
+
const raw = read(HISTORY_KEY)
|
|
35
|
+
if (raw === null) return []
|
|
36
|
+
const parsed = JSON.parse(raw)
|
|
37
|
+
return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === 'string').slice(0, 50) : []
|
|
38
|
+
} catch {
|
|
39
|
+
return []
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @param {string[]} entries */
|
|
44
|
+
export function writeHistory(entries) {
|
|
45
|
+
try {
|
|
46
|
+
write(HISTORY_KEY, JSON.stringify(entries.slice(0, 50)))
|
|
47
|
+
} catch {
|
|
48
|
+
// ignore
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createDockerStore() {
|
|
53
|
+
return defineStore({
|
|
54
|
+
init: () => {
|
|
55
|
+
const storedTab = read(TAB_KEY)
|
|
56
|
+
const storedContext = read(CONTEXT_KEY)
|
|
57
|
+
const storedCwd = read(CWD_KEY)
|
|
58
|
+
const storedProvider = read(PROVIDER_KEY)
|
|
59
|
+
return {
|
|
60
|
+
provider: storedProvider !== null && VALID_PROVIDERS.has(storedProvider) ? storedProvider : 'docker',
|
|
61
|
+
tab: storedTab !== null && VALID_TABS.has(storedTab) ? storedTab : 'overview',
|
|
62
|
+
context: storedContext !== null && storedContext !== '' ? storedContext : null,
|
|
63
|
+
cwd: storedCwd ?? '',
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
actions: {
|
|
67
|
+
/** @param {{provider: string}} draft @param {string} provider */
|
|
68
|
+
setProvider(draft, provider) {
|
|
69
|
+
if (!VALID_PROVIDERS.has(provider)) return
|
|
70
|
+
draft.provider = provider
|
|
71
|
+
write(PROVIDER_KEY, provider)
|
|
72
|
+
},
|
|
73
|
+
/** @param {{tab: string}} draft @param {string} tab */
|
|
74
|
+
setTab(draft, tab) {
|
|
75
|
+
if (!VALID_TABS.has(tab)) return
|
|
76
|
+
draft.tab = tab
|
|
77
|
+
write(TAB_KEY, tab)
|
|
78
|
+
},
|
|
79
|
+
/** @param {{context: string | null}} draft @param {string | null} context */
|
|
80
|
+
setContext(draft, context) {
|
|
81
|
+
draft.context = context === '' ? null : context
|
|
82
|
+
write(CONTEXT_KEY, draft.context)
|
|
83
|
+
},
|
|
84
|
+
/** @param {{cwd: string}} draft @param {string} cwd */
|
|
85
|
+
setCwd(draft, cwd) {
|
|
86
|
+
draft.cwd = cwd
|
|
87
|
+
write(CWD_KEY, cwd)
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
}
|