dsh-plugin-container 0.2.0 → 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/README.md +49 -310
- package/lib/client.js +84 -23
- package/package.json +1 -1
- package/src/client/api.js +5 -1
- package/src/client/attach.jsx +5 -4
- package/src/client/components.jsx +29 -10
- package/src/client/i18n.js +6 -0
- package/src/client/icons.jsx +8 -0
- package/src/client/page.jsx +34 -7
- package/src/client/styles.js +8 -0
- package/src/client/tabs/containers.jsx +6 -1
- package/src/host/attach.js +3 -0
- package/src/host/service.js +9 -5
package/README.md
CHANGED
|
@@ -1,342 +1,81 @@
|
|
|
1
|
-
# DSH
|
|
1
|
+
# DSH 容器
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DSH 左栏容器面板:管理 macOS 上的 Docker(colima / Docker Desktop / OrbStack / remote context)和 Apple `container`。
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
- **Apple container**:macOS 26 / Apple silicon 原生 `container` CLI(Containerization 框架)。它不是 Docker:没有 Docker daemon/socket/context,每个容器一个轻量 VM,镜像/卷/网络独立,不支持 Compose。
|
|
7
|
-
|
|
8
|
-

|
|
9
|
-
|
|
10
|
-

|
|
11
|
-
|
|
12
|
-

|
|
13
|
-
|
|
14
|
-
当前机器验证环境:
|
|
15
|
-
|
|
16
|
-
- macOS 26.5.1,arm64
|
|
17
|
-
- Docker CLI 28.2.2,colima context 正常
|
|
18
|
-
- Apple `container` CLI 1.0.0(`/opt/homebrew/bin/container`)
|
|
19
|
-
- Docker / Apple 两个 provider 均已通过 Playwright UI 冒烟
|
|
20
|
-
- Apple 系统服务是否运行以面板状态为准;未启动时点击“启动运行时”
|
|
5
|
+

|
|
21
6
|
|
|
22
7
|
## 功能
|
|
23
8
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
- 内置命令终端:选择 context、工作目录,实时输出,取消/超时/输出截断提示,危险命令确认。
|
|
34
|
-
|
|
35
|
-
### Apple container provider
|
|
36
|
-
|
|
37
|
-
- `container system status/version` 状态;`container system start/stop` 生命周期。
|
|
38
|
-
- 容器、镜像、卷、网络、磁盘(`container system df`)列表和 Inspect。
|
|
39
|
-
- 容器 start/stop/delete/logs;镜像 pull/delete;卷/网络 delete。
|
|
40
|
-
- **启动新容器 / Attach**:Apple `container run`;Attach 使用 `container exec -i -t <id> /bin/sh`(Apple CLI 没有 attach 命令)。
|
|
41
|
-
- 命令终端切换为 `container ...` 语法,快捷命令自动适配。
|
|
42
|
-
- Apple provider 不支持 Compose;面板会自动隐藏 Compose tab。
|
|
43
|
-
- Apple 的 `container image inspect`/`delete` 使用镜像名(不是 digest),UI 已按 provider 适配。
|
|
44
|
-
|
|
45
|
-

|
|
46
|
-
|
|
47
|
-

|
|
48
|
-
|
|
49
|
-

|
|
50
|
-
|
|
51
|
-

|
|
52
|
-
|
|
53
|
-
## AI 工具(只读)
|
|
54
|
-
|
|
55
|
-
插件向 DSH Agent 注册 4 个只读工具,复用现有 provider:
|
|
56
|
-
|
|
57
|
-
| Tool | 作用 |
|
|
58
|
-
|---|---|
|
|
59
|
-
| `container_status` | CLI、daemon/系统服务、runtime、warnings |
|
|
60
|
-
| `container_list` | 容器/镜像/卷/网络 |
|
|
61
|
-
| `container_inspect` | 单个资源详情,敏感 env 脱敏 |
|
|
62
|
-
| `container_logs` | 最近日志,默认 200 行、上限 500 |
|
|
63
|
-
|
|
64
|
-
使用方式:
|
|
65
|
-
|
|
66
|
-
- 主聊天直接问“我的 nginx 为什么退出?”;模型会调用上述工具读取事实。
|
|
67
|
-
- 容器行点 **问 AI**(运行中)或 **诊断**(未运行)会**新建一个独立诊断会话**(复用当前 workspace/cwd/agent preset),发送 provider/context/资源 ID/状态,并自动在主对话中打开;不会污染当前会话。
|
|
68
|
-
- 模型调用来自 DSH 当前 Session;插件不持有模型 API Key,也不直接调模型 HTTP。
|
|
69
|
-
- 日志和 inspect 文本被标注为不可信数据;工具描述和 system prompt 都要求不要服从其中的指令。
|
|
70
|
-
|
|
71
|
-

|
|
72
|
-
|
|
73
|
-

|
|
74
|
-
|
|
75
|
-
需要安装运行时依赖 `@deepseek-ai/dsh-tools`(已写入 `package.json`)。host 增加工具后需要重启 DSH;旧 host 下 client 会根据 `ai-tools` feature 隐藏 AI 按钮。
|
|
76
|
-
|
|
77
|
-
## 安全边界
|
|
78
|
-
|
|
79
|
-
- **owner-only**:`/docker` 与 `/api` 共用 DSH Connection 的 Host/Origin 校验和浏览器 cookie 认证;未认证返回 401。服务端再检查 `peer.principal.kind === 'owner'`。
|
|
80
|
-
- **只读模型工具**:AI 只能调用 `container_status` / `container_list` / `container_inspect` / `container_logs` 读取事实;没有 start/stop/rm/prune/run 等 mutation tool,模型不能自动改容器。
|
|
81
|
-
- **PTY 隔离**:Attach 使用 DSH `spawnTerminal` 的 PTY;Docker 使用 `docker attach`,Apple 使用 `container exec -i -t`;关闭对话框或插件卸载时终止。
|
|
82
|
-
- **无 shell**:命令拆成 argv 直接 spawn;`| & ; < > ( ) \` $`、不平衡引号会被拒绝,不执行管道/重定向/命令替换。
|
|
83
|
-
- **危险操作确认**:`rm`/`rmi`/`delete`/`stop`/`kill`/`prune`/`compose down` 等未带 `confirm: true` 时 Host 直接返回 `docker/confirmation-required`,不会 spawn;UI 显示实际命令。
|
|
84
|
-
- **上下文锁定**:Docker 命令自动加 `--context`;客户端传入 `--context`/`--host`/`-H`/`--config`/`--tls*` 会被拒绝。Apple provider 没有 context。
|
|
85
|
-
- **输出与历史**:输出只在 host 内存中保留(上限 1 MiB/stream),不写日志;命令历史存 localStorage,敏感命令(login/password/token)不记录。
|
|
86
|
-
- **权限**:命令以 DSH app 当前用户权限执行,等价于本机终端。`container system start` 可能触发管理员权限/launchd/kernel 安装;插件不会静默 sudo。
|
|
9
|
+
- Docker / Apple container 双 provider,运行时状态、上下文切换
|
|
10
|
+
- 容器、镜像、卷、网络、Compose、命令
|
|
11
|
+
- 容器启动/停止/重启/删除,镜像 pull/inspect/delete
|
|
12
|
+
- 查看日志、inspect、实时 follow
|
|
13
|
+
- **Attach**:Docker `docker attach`;Apple `container exec`
|
|
14
|
+
- **Exec**:进入容器内部 shell(`docker exec -it <id> /bin/sh`)
|
|
15
|
+
- 镜像架构列(arm64/amd64)
|
|
16
|
+
- 只读 AI 工具:`container_status`、`container_list`、`container_inspect`、`container_logs`
|
|
17
|
+
- AI 诊断会新建独立会话,不污染当前对话
|
|
87
18
|
|
|
88
|
-
##
|
|
19
|
+
## 要求
|
|
89
20
|
|
|
90
|
-
|
|
21
|
+
- macOS
|
|
22
|
+
- DSH 0.2.x
|
|
23
|
+
- Docker CLI;或安装 [Apple container](https://github.com/apple/container)
|
|
24
|
+
- 网络代理按本机环境配置
|
|
91
25
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
dsh plugin --profile desktop add /path/to/dsh-plugin-container-0.2.0.tgz
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
发布到 npm 后:
|
|
26
|
+
## 安装
|
|
99
27
|
|
|
100
28
|
```bash
|
|
101
29
|
dsh plugin --profile desktop add dsh-plugin-container
|
|
102
30
|
```
|
|
103
31
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
```json
|
|
107
|
-
"dependencies": {
|
|
108
|
-
"dsh-plugin-container": "file:/path/to/dsh-plugin-container-0.2.0.tgz"
|
|
109
|
-
},
|
|
110
|
-
"dsh": {
|
|
111
|
-
"profile": {
|
|
112
|
-
"bundles": [
|
|
113
|
-
"@deepseek-ai/dsh-base",
|
|
114
|
-
"@deepseek-ai/dsh-web-app",
|
|
115
|
-
"dsh-plugin-container"
|
|
116
|
-
]
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
包内的 `cordis.patch.yml` 会被 DSH 自动应用:
|
|
122
|
-
|
|
123
|
-
```yaml
|
|
124
|
-
- insert:
|
|
125
|
-
- id: docker-sidebar
|
|
126
|
-
name: dsh-plugin-container
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
本机安装后的效果:
|
|
130
|
-
|
|
131
|
-

|
|
32
|
+
重启 DSH。安装器会自动写入 `dsh.profile.bundles`,**不需要手改 `cordis.patch.yml`**。
|
|
132
33
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
如果右上角只看到 Docker / Apple container 但没有“启动容器”和 Attach 按钮,说明当前 host 还是旧进程:client 会显示“重启 DSH 以启用启动容器 / Attach”,退出并重开 DSH 即可。
|
|
136
|
-
|
|
137
|
-
### 源码开发安装
|
|
138
|
-
|
|
139
|
-
仍可用绝对路径方式开发:
|
|
34
|
+
本地开发版:
|
|
140
35
|
|
|
141
36
|
```bash
|
|
142
37
|
node scripts/install-profile.mjs --home "$HOME/.dsh" --profile desktop
|
|
143
38
|
```
|
|
144
39
|
|
|
145
|
-
|
|
40
|
+
源码路径模式与 npm 包模式不要同时启用。
|
|
146
41
|
|
|
147
|
-
|
|
42
|
+
## 使用
|
|
148
43
|
|
|
149
|
-
|
|
44
|
+
1. 左栏打开 **容器**
|
|
45
|
+
2. 顶部切换 Docker / Apple container
|
|
46
|
+
3. 容器行操作:
|
|
47
|
+
- 启动 / 停止 / 重启 / 删除
|
|
48
|
+
- 日志 / Follow / Inspect
|
|
49
|
+
- Attach:接管容器主进程
|
|
50
|
+
- Exec:在容器内开一个新 shell
|
|
51
|
+
- 问 AI / 诊断:新建独立 DSH 会话,调用只读容器工具
|
|
52
|
+
4. 镜像 tab 可查看架构,或直接用镜像启动容器
|
|
150
53
|
|
|
151
|
-
|
|
152
|
-
# >>> dsh-plugin-container >>>
|
|
153
|
-
- insert:
|
|
154
|
-
- id: docker-sidebar
|
|
155
|
-
name: '/Users/zaynfang/Codes/dsh/docker/index.js'
|
|
156
|
-
# <<< dsh-plugin-container <<<
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### 卸载
|
|
54
|
+
## 安全
|
|
160
55
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
```bash
|
|
168
|
-
node scripts/install-profile.mjs --remove --home "$HOME/.dsh" --profile desktop
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
## 配置
|
|
172
|
-
|
|
173
|
-
Loader row 可带可选 `config`:
|
|
174
|
-
|
|
175
|
-
```yaml
|
|
176
|
-
- insert:
|
|
177
|
-
- id: docker-sidebar
|
|
178
|
-
name: '/Users/zaynfang/Codes/dsh/docker/index.js'
|
|
179
|
-
config:
|
|
180
|
-
dockerPath: /opt/homebrew/bin/docker # 显式配置后不回退自动探测
|
|
181
|
-
colimaPath: /opt/homebrew/bin/colima
|
|
182
|
-
containerPath: /opt/homebrew/bin/container # Apple container CLI
|
|
183
|
-
defaultTimeoutMs: 120000
|
|
184
|
-
maxOutputBytes: 1048576
|
|
185
|
-
maxConcurrentRuns: 4
|
|
186
|
-
enableRuntimeControl: true
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
不配置时:
|
|
190
|
-
|
|
191
|
-
- `docker` 依次探测 `/opt/homebrew/bin/docker`、`/usr/local/bin/docker`、`~/.docker/bin/docker`、Docker.app 内置路径,再走 PATH。
|
|
192
|
-
- `colima` 依次探测 `/opt/homebrew/bin/colima`、`/usr/local/bin/colima`、`~/go/bin/colima`,再走 PATH。
|
|
193
|
-
- `container` 依次探测 `/usr/local/bin/container`、`/opt/homebrew/bin/container`、`~/.local/bin/container`,再走 PATH。
|
|
194
|
-
- 运行时启停只在 macOS 且 `enableRuntimeControl !== false` 时可用。
|
|
195
|
-
|
|
196
|
-
## macOS 运行时说明
|
|
197
|
-
|
|
198
|
-
Docker 兼容运行时包括 Docker Desktop、Colima、OrbStack、Rancher Desktop、Podman machine,以及 Lima/UTM/Parallels/VMware 里自建的 Docker;它们通常通过 Docker CLI/context 工作,因此由 **Docker provider** 统一管理(具体 context 由各运行时提供)。
|
|
199
|
-
|
|
200
|
-
Apple `container` 是独立平台:
|
|
201
|
-
|
|
202
|
-
- 仅 macOS 26 + Apple silicon。
|
|
203
|
-
- 每个容器一个轻量 VM,没有共享 Docker daemon。
|
|
204
|
-
- 安装:从 [apple/container releases](https://github.com/apple/container/releases) 安装,或使用 Homebrew;然后 `container system start`。
|
|
205
|
-
- `container system status` 在部分安装上会显示 `unregistered`,但 apiserver 实际可达;插件会用 `container ls` 做健康探测兜底。
|
|
206
|
-
- 系统服务启动可能需要管理员权限/launchd/kernel;插件使用 `--disable-kernel-install` 避免交互式挂起,失败时提示在终端执行 `container system start`。
|
|
207
|
-
- 不支持 Compose;镜像/卷/网络与 Docker 不共享;不支持一键 restart(需先 stop 再 start)。
|
|
208
|
-
|
|
209
|
-
## 工作原理
|
|
210
|
-
|
|
211
|
-
```text
|
|
212
|
-
左栏 sidebar.panellist + main keyed panel "docker"
|
|
213
|
-
│
|
|
214
|
-
▼
|
|
215
|
-
Browser ctx.connection.rpc.call('/docker', method, { provider, ... })
|
|
216
|
-
│
|
|
217
|
-
▼
|
|
218
|
-
Host ctx.webServer prefix route /docker
|
|
219
|
-
ctx.connection.admit(req)
|
|
220
|
-
DockerService (router)
|
|
221
|
-
├─ Docker provider → docker CLI + context + JobManager(idPrefix=docker)
|
|
222
|
-
└─ Apple provider → container CLI + JobManager(idPrefix=apple)
|
|
223
|
-
│
|
|
224
|
-
▼
|
|
225
|
-
ctx.subprocess.spawn(argv) ← 无 shell,provider 决定 CLI 语法
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
- `providers` 返回可用 provider 和默认 provider。
|
|
229
|
-
- `status/list/inspect/run/action/runtimeAction` 都接受 `provider`;不传默认 Docker。
|
|
230
|
-
- `poll/cancel` 按 run id 前缀(`docker_` / `apple_`)路由,切换 provider 时也能取消旧 run。
|
|
231
|
-
- 语义化 `action`(container start/stop/delete/logs、image pull/delete、volume/network delete)由 host 映射成对应 CLI,UI 不拼 Docker/Apple 语法。
|
|
232
|
-
|
|
233
|
-
### RPC 方法
|
|
234
|
-
|
|
235
|
-
| method | 说明 |
|
|
236
|
-
|---|---|
|
|
237
|
-
| `providers` | 可用 provider、版本、运行状态 |
|
|
238
|
-
| `status` | CLI、daemon/system、context/runtime、资源计数 |
|
|
239
|
-
| `list` | containers / images / volumes / networks / compose(Docker) / disk |
|
|
240
|
-
| `inspect` | container / image / volume / network |
|
|
241
|
-
| `action` | 语义化快捷操作(含启动新容器) |
|
|
242
|
-
| `attach` | 启动 Docker attach / Apple exec PTY 会话 |
|
|
243
|
-
| `attachPoll` / `attachInput` / `attachResize` / `attachClose` | Attach 输出、输入、尺寸、关闭 |
|
|
244
|
-
| `run` | 启动自由命令作业 |
|
|
245
|
-
| `poll` / `cancel` | 增量输出 / 取消 |
|
|
246
|
-
| `runtimeAction` | Docker 运行时或 Apple system start/stop |
|
|
247
|
-
|
|
248
|
-
## 发布到 npm
|
|
249
|
-
|
|
250
|
-
```bash
|
|
251
|
-
npm login
|
|
252
|
-
npm publish --access public
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
发布后安装:
|
|
256
|
-
|
|
257
|
-
```bash
|
|
258
|
-
dsh plugin --profile desktop add dsh-plugin-container
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
当前包声明了 `dsh.bundle.patch`,安装器会自动写入 `dependencies` 和 `dsh.profile.bundles`,不需要手改 `cordis.patch.yml`。
|
|
56
|
+
- 所有 Docker/Apple 命令以当前用户权限执行
|
|
57
|
+
- 无 shell 拼接;命令拆成 argv 直接 spawn
|
|
58
|
+
- 删除、stop、prune 等危险操作需要确认
|
|
59
|
+
- AI 工具只读,不提供 start/stop/rm/prune tool
|
|
60
|
+
- 日志和 inspect 被标记为不可信数据,不能作为指令执行
|
|
262
61
|
|
|
263
62
|
## 开发
|
|
264
63
|
|
|
265
|
-
构建期依赖只有 esbuild;`lib/client.js` 已提交,运行不需要 `npm install`。
|
|
266
|
-
|
|
267
|
-
本机网络走 SOCKS5 8119(8119 不是 HTTP proxy):
|
|
268
|
-
|
|
269
64
|
```bash
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
NO_PROXY=127.0.0.1,localhost,::1 \
|
|
275
|
-
npm install --no-audit --no-fund
|
|
65
|
+
npm install
|
|
66
|
+
npm run build # src/client -> lib/client.js
|
|
67
|
+
npm test # 46 tests
|
|
68
|
+
npm pack # 发布前检查
|
|
276
69
|
```
|
|
277
70
|
|
|
278
|
-
|
|
279
|
-
npm run build # src/client/index.jsx -> lib/client.js
|
|
280
|
-
node --check lib/client.js
|
|
281
|
-
npm test # 45 个 node:test 单测
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
修改 client 后重新 build;`client-hmr` 会热替换。修改 `index.js` 或 `src/host/*` 后重启 DSH。
|
|
285
|
-
|
|
286
|
-
### UI 冒烟
|
|
287
|
-
|
|
288
|
-
```bash
|
|
289
|
-
PLAYWRIGHT_CHROMIUM_EXECUTABLE="/Users/zaynfang/Library/Caches/ms-playwright/chromium-1208/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing" \
|
|
290
|
-
node scripts/ui-smoke.mjs "http://127.0.0.1:<port>/?token=..."
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
冒烟覆盖:
|
|
294
|
-
|
|
295
|
-
- Docker provider:状态、容器/镜像/卷/网络/Compose、`docker ps -a`、`docker events` 取消、危险命令确认、Inspect。
|
|
296
|
-
- 切换到 Apple provider:provider 选择器、Apple 运行状态、Compose 隐藏、`container ls -a`。
|
|
297
|
-
- 不删除资源、不停止运行时;截图写入 `docs/`。
|
|
298
|
-
|
|
299
|
-
## 文件结构
|
|
71
|
+
主要结构:
|
|
300
72
|
|
|
301
73
|
```text
|
|
302
|
-
index.js
|
|
303
|
-
cordis.patch.yml
|
|
304
|
-
|
|
305
|
-
src/host/
|
|
306
|
-
src/host/
|
|
307
|
-
src/host/
|
|
308
|
-
src/
|
|
309
|
-
src/host/tools.js 只读 Agent Tools
|
|
310
|
-
src/host/docker-cli.js Docker CLI
|
|
311
|
-
src/host/runtime.js colima/Docker Desktop/OrbStack/remote
|
|
312
|
-
src/host/jobs.js 命令作业(docker_/apple_ 前缀)
|
|
313
|
-
src/host/parsers.js Docker / Apple JSON 归一化
|
|
314
|
-
src/client/ provider 切换、页面、tabs、i18n、styles
|
|
315
|
-
src/client/attach.jsx Attach 终端模态
|
|
316
|
-
src/client/run-container.jsx 启动新容器表单
|
|
317
|
-
scripts/build-client.mjs
|
|
318
|
-
scripts/install-profile.mjs
|
|
319
|
-
scripts/ui-smoke.mjs
|
|
320
|
-
test/*.test.mjs
|
|
321
|
-
docs/*.png
|
|
74
|
+
index.js Host 入口
|
|
75
|
+
cordis.patch.yml bundle 安装 patch
|
|
76
|
+
src/host/service.js provider 路由 + Docker
|
|
77
|
+
src/host/apple.js Apple container
|
|
78
|
+
src/host/attach.js Attach/Exec PTY
|
|
79
|
+
src/host/tools.js 只读 Agent Tools
|
|
80
|
+
src/client/ 页面、tabs、i18n、styles
|
|
322
81
|
```
|
|
323
|
-
|
|
324
|
-
## 限制
|
|
325
|
-
|
|
326
|
-
- Docker `attach` 需要容器有 TTY;“启动容器”表单默认使用 `-dit`,已有容器若创建时没有 TTY,`docker attach` 可能无法交互,请改用日志或重新创建容器。
|
|
327
|
-
- Apple container 没有 `attach` 命令;面板使用 `container exec -i -t <id> /bin/sh`,镜像内必须存在 `/bin/sh`。
|
|
328
|
-
- Attach 依赖 DSH 的 PTY(`spawnTerminal`);若部署环境禁用 PTY,会返回明确的 `attach-unsupported` 错误。
|
|
329
|
-
- Attach 是单终端文本视图,不包含完整 xterm 能力(鼠标、复杂 TUI、图片协议可能不完整)。
|
|
330
|
-
- 启动新容器固定后台运行(`-d`);交互式前台运行请使用命令终端。
|
|
331
|
-
- Apple provider 仍然不支持 Compose。
|
|
332
|
-
- host 新增 Attach/启动新容器后需要再重启 DSH 一次;旧 host 下 client 会显示“重启 DSH 以启用启动容器 / Attach”,不会出现无效按钮。
|
|
333
|
-
|
|
334
|
-
## 故障排查
|
|
335
|
-
|
|
336
|
-
- **左栏没有“容器”**:确认 patch 块存在;刷新 GUI;host 代码更新后退出并重开 DSH。
|
|
337
|
-
- **只有 Docker,没有 Apple container 选项**:确认 `container` CLI 可执行(`container --version`);刷新;host 需要新版本。
|
|
338
|
-
- **Apple 服务显示未启动**:点击“启动运行时”,或终端执行 `container system start`。首次可能需要管理员权限/kernel。
|
|
339
|
-
- **Apple 启动卡住/失败**:查看命令终端里的 `container system start` 输出;`--disable-kernel-install` 下缺 kernel 会失败,需先在终端启动一次。
|
|
340
|
-
- **Apple 镜像 Inspect 失败**:请使用镜像名(UI 已自动这样做),不要用 digest。
|
|
341
|
-
- **Docker Desktop 停止失败**:macOS Automation 权限可能拦截 `osascript`;从菜单栏退出或授予权限。
|
|
342
|
-
- **patch 损坏**:用 `cordis.patch.yml.docker.bak` 恢复,或 `install-profile.mjs --remove` 后重试。
|
package/lib/client.js
CHANGED
|
@@ -48,7 +48,7 @@ var DockerApiError = class extends Error {
|
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
50
|
function isAbortError(error) {
|
|
51
|
-
return error instanceof DOMException && error.name === "AbortError" || error?.name === "AbortError";
|
|
51
|
+
return error instanceof DOMException && error.name === "AbortError" || error?.name === "AbortError" || error?.code === "docker/cancelled";
|
|
52
52
|
}
|
|
53
53
|
function isResult(value) {
|
|
54
54
|
return typeof value === "object" && value !== null && typeof value.ok === "boolean";
|
|
@@ -218,6 +218,11 @@ var IconSparkles = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Svg,
|
|
|
218
218
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M18 14l.7 1.8 1.8.7-1.8.7L18 19l-.7-1.8-1.8-.7 1.8-.7L18 14Z" }),
|
|
219
219
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M6 15l.6 1.4L8 17l-1.4.6L6 19l-.6-1.4L4 17l1.4-.6L6 15Z" })
|
|
220
220
|
] });
|
|
221
|
+
var IconExec = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Svg, { ...props, children: [
|
|
222
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "3.5", y: "4.5", width: "17", height: "15", rx: "2" }),
|
|
223
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "m7.2 9.2 3 2.8-3 2.8M12.5 15h4" }),
|
|
224
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M17 9.2h.01" })
|
|
225
|
+
] });
|
|
221
226
|
var IconAttach = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Svg, { ...props, children: [
|
|
222
227
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M9.5 14.5 14.5 9.5" }),
|
|
223
228
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M11 7.5 12.6 5.9a3.2 3.2 0 0 1 4.5 4.5L15.4 12" }),
|
|
@@ -311,6 +316,8 @@ var zh = {
|
|
|
311
316
|
restartFeatures: "\u91CD\u542F DSH \u4EE5\u542F\u7528\u542F\u52A8\u5BB9\u5668 / Attach",
|
|
312
317
|
restartForArchitecture: "\u91CD\u542F DSH \u4EE5\u663E\u793A\u955C\u50CF\u67B6\u6784",
|
|
313
318
|
attach: "Attach",
|
|
319
|
+
exec: "Exec",
|
|
320
|
+
execTitle: "Exec \u7EC8\u7AEF",
|
|
314
321
|
askAi: "\u95EE AI",
|
|
315
322
|
diagnose: "\u8BCA\u65AD",
|
|
316
323
|
askAiSent: "\u5DF2\u53D1\u9001\u5230 AI \u4F1A\u8BDD",
|
|
@@ -325,6 +332,7 @@ var zh = {
|
|
|
325
332
|
attachHint: "\u5DF2\u8FDE\u63A5\u3002\u70B9\u51FB\u7EC8\u7AEF\u540E\u8F93\u5165\uFF1BCtrl+C \u4E2D\u65AD\uFF0C\u5173\u95ED\u7A97\u53E3\u7ED3\u675F\u4F1A\u8BDD\u3002",
|
|
326
333
|
attachInputHint: "\u70B9\u51FB\u7EC8\u7AEF\u8F93\u5165 \xB7 Ctrl+C \u4E2D\u65AD",
|
|
327
334
|
attachClose: "\u7ED3\u675F Attach",
|
|
335
|
+
execClose: "\u7ED3\u675F Exec",
|
|
328
336
|
attachNotRunning: "\u5BB9\u5668\u672A\u8FD0\u884C\uFF0C\u65E0\u6CD5 attach",
|
|
329
337
|
followLogs: "\u6301\u7EED\u65E5\u5FD7",
|
|
330
338
|
inspect: "\u68C0\u67E5",
|
|
@@ -553,6 +561,8 @@ var en = {
|
|
|
553
561
|
restartFeatures: "Restart DSH to enable Start container / Attach",
|
|
554
562
|
restartForArchitecture: "Restart DSH to show image architecture",
|
|
555
563
|
attach: "Attach",
|
|
564
|
+
exec: "Exec",
|
|
565
|
+
execTitle: "Exec terminal",
|
|
556
566
|
askAi: "Ask AI",
|
|
557
567
|
diagnose: "Diagnose",
|
|
558
568
|
askAiSent: "Sent to the AI conversation",
|
|
@@ -567,6 +577,7 @@ var en = {
|
|
|
567
577
|
attachHint: "Connected. Click the terminal and type; Ctrl+C interrupts. Closing the dialog ends the session.",
|
|
568
578
|
attachInputHint: "Click terminal to type \xB7 Ctrl+C interrupts",
|
|
569
579
|
attachClose: "End attach",
|
|
580
|
+
execClose: "End exec",
|
|
570
581
|
attachNotRunning: "Container is not running; attach is unavailable",
|
|
571
582
|
followLogs: "Follow logs",
|
|
572
583
|
inspect: "Inspect",
|
|
@@ -748,17 +759,36 @@ function Button({ variant = "ghost", size = "md", className, type = "button", ch
|
|
|
748
759
|
);
|
|
749
760
|
}
|
|
750
761
|
function IconButton({ label, size = "md", className, children, ...rest }) {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
+
const [tip, setTip] = (0, import_react2.useState)(null);
|
|
763
|
+
const ref = (0, import_react2.useRef)(null);
|
|
764
|
+
const showTip = () => {
|
|
765
|
+
const rect = ref.current?.getBoundingClientRect();
|
|
766
|
+
if (rect === void 0) return;
|
|
767
|
+
const left = Math.min(Math.max(rect.left + rect.width / 2, 16), window.innerWidth - 16);
|
|
768
|
+
setTip({ left, top: Math.max(rect.top - 7, 8) });
|
|
769
|
+
};
|
|
770
|
+
const hideTip = () => setTip(null);
|
|
771
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
772
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
773
|
+
"button",
|
|
774
|
+
{
|
|
775
|
+
type: "button",
|
|
776
|
+
ref,
|
|
777
|
+
"aria-label": label,
|
|
778
|
+
className: cx("dsh-docker-icon-btn", size === "sm" && "dsh-docker-sm", className),
|
|
779
|
+
onMouseEnter: showTip,
|
|
780
|
+
onMouseLeave: hideTip,
|
|
781
|
+
onFocus: showTip,
|
|
782
|
+
onBlur: hideTip,
|
|
783
|
+
...rest,
|
|
784
|
+
children
|
|
785
|
+
}
|
|
786
|
+
),
|
|
787
|
+
tip !== null && (0, import_react_dom.createPortal)(
|
|
788
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-docker-tooltip", style: { left: tip.left, top: tip.top }, role: "tooltip", children: label }),
|
|
789
|
+
document.body
|
|
790
|
+
)
|
|
791
|
+
] });
|
|
762
792
|
}
|
|
763
793
|
function Spinner({ size = 16, className }) {
|
|
764
794
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IconSpinner, { size, className });
|
|
@@ -994,7 +1024,7 @@ function keyBytes(event) {
|
|
|
994
1024
|
if (event.key.length === 1) return event.key;
|
|
995
1025
|
return null;
|
|
996
1026
|
}
|
|
997
|
-
function AttachDialog({ open, t, session, output, loading, error, onInput, onClose }) {
|
|
1027
|
+
function AttachDialog({ open, t, mode = "attach", session, output, loading, error, onInput, onClose }) {
|
|
998
1028
|
const screenRef = (0, import_react3.useRef)(null);
|
|
999
1029
|
const preRef = (0, import_react3.useRef)(null);
|
|
1000
1030
|
const [stick, setStick] = (0, import_react3.useState)(true);
|
|
@@ -1010,12 +1040,13 @@ function AttachDialog({ open, t, session, output, loading, error, onInput, onClo
|
|
|
1010
1040
|
if (element !== null) element.scrollTop = element.scrollHeight;
|
|
1011
1041
|
}, [output, stick]);
|
|
1012
1042
|
const running = session?.status === "running";
|
|
1043
|
+
const title = (session?.mode ?? mode) === "exec" ? t("execTitle") : t("attachTitle");
|
|
1013
1044
|
const shown = cleanTerminal(output);
|
|
1014
1045
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1015
1046
|
Modal,
|
|
1016
1047
|
{
|
|
1017
1048
|
open,
|
|
1018
|
-
title: `${
|
|
1049
|
+
title: `${title} \xB7 ${session?.containerName ?? ""}`,
|
|
1019
1050
|
subtitle: session?.command ?? void 0,
|
|
1020
1051
|
wide: true,
|
|
1021
1052
|
onClose,
|
|
@@ -1029,7 +1060,7 @@ function AttachDialog({ open, t, session, output, loading, error, onInput, onClo
|
|
|
1029
1060
|
session.exitCode
|
|
1030
1061
|
] }),
|
|
1031
1062
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CopyButton, { text: shown, label: t("copy"), copiedLabel: t("copied") }),
|
|
1032
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Button, { size: "sm", onClick: onClose, children: t("attachClose") })
|
|
1063
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Button, { size: "sm", onClick: onClose, children: t((session?.mode ?? mode) === "exec" ? "execClose" : "attachClose") })
|
|
1033
1064
|
] }),
|
|
1034
1065
|
children: [
|
|
1035
1066
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dsh-docker-attach-status", children: [
|
|
@@ -1050,7 +1081,7 @@ function AttachDialog({ open, t, session, output, loading, error, onInput, onClo
|
|
|
1050
1081
|
className: "dsh-docker-attach-screen",
|
|
1051
1082
|
tabIndex: 0,
|
|
1052
1083
|
role: "textbox",
|
|
1053
|
-
"aria-label":
|
|
1084
|
+
"aria-label": title,
|
|
1054
1085
|
onKeyDown: (event) => {
|
|
1055
1086
|
if (!running || loading) return;
|
|
1056
1087
|
const data = keyBytes(event);
|
|
@@ -1817,6 +1848,7 @@ function ContainersTab({ t, api, provider, context, status, refreshToken, featur
|
|
|
1817
1848
|
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconSparkles, { size: 14 })
|
|
1818
1849
|
}
|
|
1819
1850
|
),
|
|
1851
|
+
features.includes("exec") && item.state === "running" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconButton, { size: "sm", label: t("exec"), onClick: () => onAttach(item, "exec"), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconExec, { size: 14 }) }),
|
|
1820
1852
|
features.includes("attach") && item.state === "running" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconButton, { size: "sm", label: t("attach"), onClick: () => onAttach(item), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconAttach, { size: 14 }) }),
|
|
1821
1853
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconButton, { size: "sm", label: t("logs"), onClick: () => act("logs", item, true), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconTerminal, { size: 14 }) }),
|
|
1822
1854
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconButton, { size: "sm", label: t("followLogs"), onClick: () => act("logs", item, true, true), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconTerminal, { size: 14, style: { transform: "scaleX(-1)" } }) }),
|
|
@@ -2304,7 +2336,7 @@ function DockerPage(props) {
|
|
|
2304
2336
|
const [runtimeBusy, setRuntimeBusy] = (0, import_react13.useState)(null);
|
|
2305
2337
|
const [inspect, setInspect] = (0, import_react13.useState)({ open: false, loading: false, kind: null, id: null, value: null, raw: "", error: null });
|
|
2306
2338
|
const [confirm, setConfirm] = (0, import_react13.useState)(null);
|
|
2307
|
-
const [attach, setAttach] = (0, import_react13.useState)({ open: false, loading: false, session: null, output: "", error: null });
|
|
2339
|
+
const [attach, setAttach] = (0, import_react13.useState)({ open: false, loading: false, mode: "attach", session: null, output: "", error: null });
|
|
2308
2340
|
const [runDialog, setRunDialog] = (0, import_react13.useState)({ open: false, image: "" });
|
|
2309
2341
|
const attachRef = (0, import_react13.useRef)(null);
|
|
2310
2342
|
const attachRequest = (0, import_react13.useRef)(0);
|
|
@@ -2383,6 +2415,19 @@ function DockerPage(props) {
|
|
|
2383
2415
|
setRefreshToken((value) => value + 1);
|
|
2384
2416
|
statusState.refresh();
|
|
2385
2417
|
}, [statusState.refresh]);
|
|
2418
|
+
const lastRun = (0, import_react13.useRef)(null);
|
|
2419
|
+
(0, import_react13.useEffect)(() => {
|
|
2420
|
+
const run = runState.run;
|
|
2421
|
+
if (run === null || run === void 0) {
|
|
2422
|
+
lastRun.current = null;
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
if (run.status === "running" || run.status === "cancelling") return;
|
|
2426
|
+
const signature = `${run.id ?? ""}:${run.status}`;
|
|
2427
|
+
if (lastRun.current === signature) return;
|
|
2428
|
+
lastRun.current = signature;
|
|
2429
|
+
refreshAll();
|
|
2430
|
+
}, [refreshAll, runState.run?.id, runState.run?.status]);
|
|
2386
2431
|
const confirmThen = (0, import_react13.useCallback)((config, execute) => {
|
|
2387
2432
|
setConfirm({
|
|
2388
2433
|
...config,
|
|
@@ -2481,11 +2526,11 @@ function DockerPage(props) {
|
|
|
2481
2526
|
attachRequest.current += 1;
|
|
2482
2527
|
const session = attachRef.current;
|
|
2483
2528
|
attachRef.current = null;
|
|
2484
|
-
setAttach({ open: false, loading: false, session: null, output: "", error: null });
|
|
2529
|
+
setAttach({ open: false, loading: false, mode: "attach", session: null, output: "", error: null });
|
|
2485
2530
|
if (session !== null && session?.id !== void 0) api.call("attachClose", { attachId: session.id }).catch(() => {
|
|
2486
2531
|
});
|
|
2487
2532
|
}, [api]);
|
|
2488
|
-
const openAttach = (0, import_react13.useCallback)(async (container) => {
|
|
2533
|
+
const openAttach = (0, import_react13.useCallback)(async (container, mode = "attach") => {
|
|
2489
2534
|
if (container?.state !== "running") {
|
|
2490
2535
|
notify("info", t("attachNotRunning"));
|
|
2491
2536
|
return;
|
|
@@ -2497,13 +2542,14 @@ function DockerPage(props) {
|
|
|
2497
2542
|
api.call("attachClose", { attachId: previous.id }).catch(() => {
|
|
2498
2543
|
});
|
|
2499
2544
|
}
|
|
2500
|
-
setAttach({ open: true, loading: true, session: null, output: "", error: null });
|
|
2545
|
+
setAttach({ open: true, loading: true, mode, session: null, output: "", error: null });
|
|
2501
2546
|
try {
|
|
2502
2547
|
const value = await api.call("attach", {
|
|
2503
2548
|
provider,
|
|
2504
2549
|
context: selectedContext ?? void 0,
|
|
2505
2550
|
id: container.id,
|
|
2506
2551
|
name: container.name || container.id,
|
|
2552
|
+
mode,
|
|
2507
2553
|
cols: 120,
|
|
2508
2554
|
rows: 30
|
|
2509
2555
|
});
|
|
@@ -2513,10 +2559,10 @@ function DockerPage(props) {
|
|
|
2513
2559
|
return;
|
|
2514
2560
|
}
|
|
2515
2561
|
attachRef.current = value.session;
|
|
2516
|
-
setAttach({ open: true, loading: false, session: value.session, output: "", error: null });
|
|
2562
|
+
setAttach({ open: true, loading: false, mode, session: value.session, output: "", error: null });
|
|
2517
2563
|
} catch (error) {
|
|
2518
2564
|
if (request !== attachRequest.current) return;
|
|
2519
|
-
setAttach({ open: true, loading: false, session: null, output: "", error });
|
|
2565
|
+
setAttach({ open: true, loading: false, mode, session: null, output: "", error });
|
|
2520
2566
|
}
|
|
2521
2567
|
}, [api, notify, provider, selectedContext, t]);
|
|
2522
2568
|
const attachInput = (0, import_react13.useCallback)(async (data) => {
|
|
@@ -2594,6 +2640,10 @@ function DockerPage(props) {
|
|
|
2594
2640
|
}, [canAskAI, notify, provider, selectedContext, startDiagnosis, t]);
|
|
2595
2641
|
const openRun = (0, import_react13.useCallback)((image = "") => setRunDialog({ open: true, image }), []);
|
|
2596
2642
|
const closeRun = (0, import_react13.useCallback)(() => setRunDialog({ open: false, image: "" }), []);
|
|
2643
|
+
const scheduleListRefresh = (0, import_react13.useCallback)(() => {
|
|
2644
|
+
window.setTimeout(() => refreshAll(), 900);
|
|
2645
|
+
window.setTimeout(() => refreshAll(), 2600);
|
|
2646
|
+
}, [refreshAll]);
|
|
2597
2647
|
const runAction = (0, import_react13.useCallback)(async (input, { focusConsole = false } = {}) => {
|
|
2598
2648
|
if (runs.active) {
|
|
2599
2649
|
notify("info", t("commandRunning"));
|
|
@@ -2604,6 +2654,7 @@ function DockerPage(props) {
|
|
|
2604
2654
|
try {
|
|
2605
2655
|
const run = await runs.startAction(payload);
|
|
2606
2656
|
if (run !== null && input.kind === "container" && input.action === "run") notify("success", t("runStarted"));
|
|
2657
|
+
if (run !== null) scheduleListRefresh();
|
|
2607
2658
|
return run;
|
|
2608
2659
|
} catch (error) {
|
|
2609
2660
|
if (error instanceof DockerApiError && error.code === "docker/confirmation-required") {
|
|
@@ -2617,6 +2668,7 @@ function DockerPage(props) {
|
|
|
2617
2668
|
},
|
|
2618
2669
|
async () => {
|
|
2619
2670
|
await runs.startAction({ ...payload, confirm: true });
|
|
2671
|
+
scheduleListRefresh();
|
|
2620
2672
|
}
|
|
2621
2673
|
);
|
|
2622
2674
|
return null;
|
|
@@ -2624,7 +2676,7 @@ function DockerPage(props) {
|
|
|
2624
2676
|
notify("error", errorMessage(error));
|
|
2625
2677
|
return null;
|
|
2626
2678
|
}
|
|
2627
|
-
}, [actions, confirmThen, notify, provider, runs, t]);
|
|
2679
|
+
}, [actions, confirmThen, notify, provider, runs, scheduleListRefresh, t]);
|
|
2628
2680
|
const openTab = (0, import_react13.useCallback)((next) => actions.setTab(next), [actions]);
|
|
2629
2681
|
const tabProps = {
|
|
2630
2682
|
t,
|
|
@@ -2798,6 +2850,7 @@ ${t("statusErrorHint")}`,
|
|
|
2798
2850
|
AttachDialog,
|
|
2799
2851
|
{
|
|
2800
2852
|
open: attach.open,
|
|
2853
|
+
mode: attach.mode,
|
|
2801
2854
|
t,
|
|
2802
2855
|
session: attach.session,
|
|
2803
2856
|
output: attach.output,
|
|
@@ -3160,6 +3213,7 @@ var CSS = `
|
|
|
3160
3213
|
.dsh-docker-btn.dsh-docker-sm { height: 26px; padding: 0 9px; font-size: 12px; border-radius: 6px; }
|
|
3161
3214
|
.dsh-docker-btn.dsh-docker-lg { height: 34px; padding: 0 14px; font-size: 13px; }
|
|
3162
3215
|
.dsh-docker-icon-btn {
|
|
3216
|
+
position: relative;
|
|
3163
3217
|
appearance: none; border: .5px solid transparent; background: transparent; cursor: pointer;
|
|
3164
3218
|
width: 30px; height: 30px; border-radius: 7px; padding: 0;
|
|
3165
3219
|
display: inline-grid; place-items: center;
|
|
@@ -3170,6 +3224,13 @@ var CSS = `
|
|
|
3170
3224
|
.dsh-docker-icon-btn[disabled] { opacity: .4; cursor: not-allowed; }
|
|
3171
3225
|
.dsh-docker-icon-btn.dsh-docker-sm { width: 26px; height: 26px; }
|
|
3172
3226
|
|
|
3227
|
+
.dsh-docker-tooltip {
|
|
3228
|
+
position: fixed; z-index: 10000; transform: translate(-50%, -100%);
|
|
3229
|
+
padding: 5px 8px; border-radius: 6px; pointer-events: none; white-space: nowrap;
|
|
3230
|
+
color: var(--dsw-alias-label-primary, #fff); background: var(--dsw-alias-bg-inverse, #23252b);
|
|
3231
|
+
font-size: 11px; line-height: 1.2; box-shadow: 0 4px 14px rgba(0,0,0,.18);
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3173
3234
|
.dsh-docker-select, .dsh-docker-input {
|
|
3174
3235
|
appearance: none; -webkit-appearance: none;
|
|
3175
3236
|
height: 30px; min-width: 0; padding: 0 9px; border-radius: 7px;
|
package/package.json
CHANGED
package/src/client/api.js
CHANGED
|
@@ -20,7 +20,11 @@ export class DockerApiError extends Error {
|
|
|
20
20
|
|
|
21
21
|
/** @param {unknown} error */
|
|
22
22
|
export function isAbortError(error) {
|
|
23
|
-
return
|
|
23
|
+
return (
|
|
24
|
+
(error instanceof DOMException && error.name === 'AbortError') ||
|
|
25
|
+
error?.name === 'AbortError' ||
|
|
26
|
+
error?.code === 'docker/cancelled'
|
|
27
|
+
)
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/** @param {unknown} value */
|
package/src/client/attach.jsx
CHANGED
|
@@ -45,7 +45,7 @@ function keyBytes(event) {
|
|
|
45
45
|
return null
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export default function AttachDialog({ open, t, session, output, loading, error, onInput, onClose }) {
|
|
48
|
+
export default function AttachDialog({ open, t, mode = 'attach', session, output, loading, error, onInput, onClose }) {
|
|
49
49
|
const screenRef = useRef(null)
|
|
50
50
|
const preRef = useRef(null)
|
|
51
51
|
const [stick, setStick] = useState(true)
|
|
@@ -64,11 +64,12 @@ export default function AttachDialog({ open, t, session, output, loading, error,
|
|
|
64
64
|
}, [output, stick])
|
|
65
65
|
|
|
66
66
|
const running = session?.status === 'running'
|
|
67
|
+
const title = (session?.mode ?? mode) === 'exec' ? t('execTitle') : t('attachTitle')
|
|
67
68
|
const shown = cleanTerminal(output)
|
|
68
69
|
return (
|
|
69
70
|
<Modal
|
|
70
71
|
open={open}
|
|
71
|
-
title={`${
|
|
72
|
+
title={`${title} · ${session?.containerName ?? ''}`}
|
|
72
73
|
subtitle={session?.command ?? undefined}
|
|
73
74
|
wide
|
|
74
75
|
onClose={onClose}
|
|
@@ -79,7 +80,7 @@ export default function AttachDialog({ open, t, session, output, loading, error,
|
|
|
79
80
|
<span style={{ flex: 1 }} />
|
|
80
81
|
{session?.exitCode !== null && session?.exitCode !== undefined && <span className="dsh-docker-muted">{t('exitCode')}: {session.exitCode}</span>}
|
|
81
82
|
<CopyButton text={shown} label={t('copy')} copiedLabel={t('copied')} />
|
|
82
|
-
<Button size="sm" onClick={onClose}>{t('attachClose')}</Button>
|
|
83
|
+
<Button size="sm" onClick={onClose}>{t((session?.mode ?? mode) === 'exec' ? 'execClose' : 'attachClose')}</Button>
|
|
83
84
|
</>
|
|
84
85
|
}
|
|
85
86
|
>
|
|
@@ -95,7 +96,7 @@ export default function AttachDialog({ open, t, session, output, loading, error,
|
|
|
95
96
|
className="dsh-docker-attach-screen"
|
|
96
97
|
tabIndex={0}
|
|
97
98
|
role="textbox"
|
|
98
|
-
aria-label={
|
|
99
|
+
aria-label={title}
|
|
99
100
|
onKeyDown={(event) => {
|
|
100
101
|
if (!running || loading) return
|
|
101
102
|
const data = keyBytes(event)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Small presentational building blocks shared by the Docker tabs. */
|
|
2
|
-
import { useEffect, useState } from 'react'
|
|
2
|
+
import { useEffect, useRef, useState } from 'react'
|
|
3
3
|
import { createPortal } from 'react-dom'
|
|
4
4
|
|
|
5
5
|
import { IconAlert, IconCheck, IconCopy, IconSearch, IconSpinner, IconX } from './icons.jsx'
|
|
@@ -30,16 +30,35 @@ export function Button({ variant = 'ghost', size = 'md', className, type = 'butt
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function IconButton({ label, size = 'md', className, children, ...rest }) {
|
|
33
|
+
const [tip, setTip] = useState(null)
|
|
34
|
+
const ref = useRef(null)
|
|
35
|
+
const showTip = () => {
|
|
36
|
+
const rect = ref.current?.getBoundingClientRect()
|
|
37
|
+
if (rect === undefined) return
|
|
38
|
+
const left = Math.min(Math.max(rect.left + rect.width / 2, 16), window.innerWidth - 16)
|
|
39
|
+
setTip({ left, top: Math.max(rect.top - 7, 8) })
|
|
40
|
+
}
|
|
41
|
+
const hideTip = () => setTip(null)
|
|
33
42
|
return (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
<>
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
ref={ref}
|
|
47
|
+
aria-label={label}
|
|
48
|
+
className={cx('dsh-docker-icon-btn', size === 'sm' && 'dsh-docker-sm', className)}
|
|
49
|
+
onMouseEnter={showTip}
|
|
50
|
+
onMouseLeave={hideTip}
|
|
51
|
+
onFocus={showTip}
|
|
52
|
+
onBlur={hideTip}
|
|
53
|
+
{...rest}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</button>
|
|
57
|
+
{tip !== null && createPortal(
|
|
58
|
+
<span className="dsh-docker-tooltip" style={{ left: tip.left, top: tip.top }} role="tooltip">{label}</span>,
|
|
59
|
+
document.body,
|
|
60
|
+
)}
|
|
61
|
+
</>
|
|
43
62
|
)
|
|
44
63
|
}
|
|
45
64
|
|
package/src/client/i18n.js
CHANGED
|
@@ -91,6 +91,8 @@ export const zh = {
|
|
|
91
91
|
restartFeatures: '重启 DSH 以启用启动容器 / Attach',
|
|
92
92
|
restartForArchitecture: '重启 DSH 以显示镜像架构',
|
|
93
93
|
attach: 'Attach',
|
|
94
|
+
exec: 'Exec',
|
|
95
|
+
execTitle: 'Exec 终端',
|
|
94
96
|
askAi: '问 AI',
|
|
95
97
|
diagnose: '诊断',
|
|
96
98
|
askAiSent: '已发送到 AI 会话',
|
|
@@ -105,6 +107,7 @@ export const zh = {
|
|
|
105
107
|
attachHint: '已连接。点击终端后输入;Ctrl+C 中断,关闭窗口结束会话。',
|
|
106
108
|
attachInputHint: '点击终端输入 · Ctrl+C 中断',
|
|
107
109
|
attachClose: '结束 Attach',
|
|
110
|
+
execClose: '结束 Exec',
|
|
108
111
|
attachNotRunning: '容器未运行,无法 attach',
|
|
109
112
|
followLogs: '持续日志',
|
|
110
113
|
inspect: '检查',
|
|
@@ -334,6 +337,8 @@ export const en = {
|
|
|
334
337
|
restartFeatures: 'Restart DSH to enable Start container / Attach',
|
|
335
338
|
restartForArchitecture: 'Restart DSH to show image architecture',
|
|
336
339
|
attach: 'Attach',
|
|
340
|
+
exec: 'Exec',
|
|
341
|
+
execTitle: 'Exec terminal',
|
|
337
342
|
askAi: 'Ask AI',
|
|
338
343
|
diagnose: 'Diagnose',
|
|
339
344
|
askAiSent: 'Sent to the AI conversation',
|
|
@@ -348,6 +353,7 @@ export const en = {
|
|
|
348
353
|
attachHint: 'Connected. Click the terminal and type; Ctrl+C interrupts. Closing the dialog ends the session.',
|
|
349
354
|
attachInputHint: 'Click terminal to type · Ctrl+C interrupts',
|
|
350
355
|
attachClose: 'End attach',
|
|
356
|
+
execClose: 'End exec',
|
|
351
357
|
attachNotRunning: 'Container is not running; attach is unavailable',
|
|
352
358
|
followLogs: 'Follow logs',
|
|
353
359
|
inspect: 'Inspect',
|
package/src/client/icons.jsx
CHANGED
|
@@ -206,6 +206,14 @@ export const IconSparkles = (props) => (
|
|
|
206
206
|
</Svg>
|
|
207
207
|
)
|
|
208
208
|
|
|
209
|
+
export const IconExec = (props) => (
|
|
210
|
+
<Svg {...props}>
|
|
211
|
+
<rect x="3.5" y="4.5" width="17" height="15" rx="2" />
|
|
212
|
+
<path d="m7.2 9.2 3 2.8-3 2.8M12.5 15h4" />
|
|
213
|
+
<path d="M17 9.2h.01" />
|
|
214
|
+
</Svg>
|
|
215
|
+
)
|
|
216
|
+
|
|
209
217
|
export const IconAttach = (props) => (
|
|
210
218
|
<Svg {...props}>
|
|
211
219
|
<path d="M9.5 14.5 14.5 9.5" />
|
package/src/client/page.jsx
CHANGED
|
@@ -97,7 +97,7 @@ export default function DockerPage(props) {
|
|
|
97
97
|
const [runtimeBusy, setRuntimeBusy] = useState(null)
|
|
98
98
|
const [inspect, setInspect] = useState({ open: false, loading: false, kind: null, id: null, value: null, raw: '', error: null })
|
|
99
99
|
const [confirm, setConfirm] = useState(null)
|
|
100
|
-
const [attach, setAttach] = useState({ open: false, loading: false, session: null, output: '', error: null })
|
|
100
|
+
const [attach, setAttach] = useState({ open: false, loading: false, mode: 'attach', session: null, output: '', error: null })
|
|
101
101
|
const [runDialog, setRunDialog] = useState({ open: false, image: '' })
|
|
102
102
|
const attachRef = useRef(null)
|
|
103
103
|
const attachRequest = useRef(0)
|
|
@@ -186,6 +186,22 @@ export default function DockerPage(props) {
|
|
|
186
186
|
statusState.refresh()
|
|
187
187
|
}, [statusState.refresh])
|
|
188
188
|
|
|
189
|
+
// Some very short actions finish before the controller's callback is observed;
|
|
190
|
+
// refresh on every terminal run transition so the list/count cannot stay stale.
|
|
191
|
+
const lastRun = useRef(null)
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
const run = runState.run
|
|
194
|
+
if (run === null || run === undefined) {
|
|
195
|
+
lastRun.current = null
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
if (run.status === 'running' || run.status === 'cancelling') return
|
|
199
|
+
const signature = `${run.id ?? ''}:${run.status}`
|
|
200
|
+
if (lastRun.current === signature) return
|
|
201
|
+
lastRun.current = signature
|
|
202
|
+
refreshAll()
|
|
203
|
+
}, [refreshAll, runState.run?.id, runState.run?.status])
|
|
204
|
+
|
|
189
205
|
const confirmThen = useCallback((config, execute) => {
|
|
190
206
|
setConfirm({
|
|
191
207
|
...config,
|
|
@@ -288,11 +304,11 @@ export default function DockerPage(props) {
|
|
|
288
304
|
attachRequest.current += 1
|
|
289
305
|
const session = attachRef.current
|
|
290
306
|
attachRef.current = null
|
|
291
|
-
setAttach({ open: false, loading: false, session: null, output: '', error: null })
|
|
307
|
+
setAttach({ open: false, loading: false, mode: 'attach', session: null, output: '', error: null })
|
|
292
308
|
if (session !== null && session?.id !== undefined) api.call('attachClose', { attachId: session.id }).catch(() => {})
|
|
293
309
|
}, [api])
|
|
294
310
|
|
|
295
|
-
const openAttach = useCallback(async (container) => {
|
|
311
|
+
const openAttach = useCallback(async (container, mode = 'attach') => {
|
|
296
312
|
if (container?.state !== 'running') {
|
|
297
313
|
notify('info', t('attachNotRunning'))
|
|
298
314
|
return
|
|
@@ -303,13 +319,14 @@ export default function DockerPage(props) {
|
|
|
303
319
|
attachRef.current = null
|
|
304
320
|
api.call('attachClose', { attachId: previous.id }).catch(() => {})
|
|
305
321
|
}
|
|
306
|
-
setAttach({ open: true, loading: true, session: null, output: '', error: null })
|
|
322
|
+
setAttach({ open: true, loading: true, mode, session: null, output: '', error: null })
|
|
307
323
|
try {
|
|
308
324
|
const value = await api.call('attach', {
|
|
309
325
|
provider,
|
|
310
326
|
context: selectedContext ?? undefined,
|
|
311
327
|
id: container.id,
|
|
312
328
|
name: container.name || container.id,
|
|
329
|
+
mode,
|
|
313
330
|
cols: 120,
|
|
314
331
|
rows: 30,
|
|
315
332
|
})
|
|
@@ -318,10 +335,10 @@ export default function DockerPage(props) {
|
|
|
318
335
|
return
|
|
319
336
|
}
|
|
320
337
|
attachRef.current = value.session
|
|
321
|
-
setAttach({ open: true, loading: false, session: value.session, output: '', error: null })
|
|
338
|
+
setAttach({ open: true, loading: false, mode, session: value.session, output: '', error: null })
|
|
322
339
|
} catch (error) {
|
|
323
340
|
if (request !== attachRequest.current) return
|
|
324
|
-
setAttach({ open: true, loading: false, session: null, output: '', error })
|
|
341
|
+
setAttach({ open: true, loading: false, mode, session: null, output: '', error })
|
|
325
342
|
}
|
|
326
343
|
}, [api, notify, provider, selectedContext, t])
|
|
327
344
|
|
|
@@ -406,6 +423,13 @@ export default function DockerPage(props) {
|
|
|
406
423
|
const openRun = useCallback((image = '') => setRunDialog({ open: true, image }), [])
|
|
407
424
|
const closeRun = useCallback(() => setRunDialog({ open: false, image: '' }), [])
|
|
408
425
|
|
|
426
|
+
const scheduleListRefresh = useCallback(() => {
|
|
427
|
+
// The job may finish before the run poll is observed by this page; a short
|
|
428
|
+
// second timer makes stop/delete reliably drop out of the list.
|
|
429
|
+
window.setTimeout(() => refreshAll(), 900)
|
|
430
|
+
window.setTimeout(() => refreshAll(), 2600)
|
|
431
|
+
}, [refreshAll])
|
|
432
|
+
|
|
409
433
|
const runAction = useCallback(async (input, { focusConsole = false } = {}) => {
|
|
410
434
|
if (runs.active) {
|
|
411
435
|
notify('info', t('commandRunning'))
|
|
@@ -416,6 +440,7 @@ export default function DockerPage(props) {
|
|
|
416
440
|
try {
|
|
417
441
|
const run = await runs.startAction(payload)
|
|
418
442
|
if (run !== null && input.kind === 'container' && input.action === 'run') notify('success', t('runStarted'))
|
|
443
|
+
if (run !== null) scheduleListRefresh()
|
|
419
444
|
return run
|
|
420
445
|
} catch (error) {
|
|
421
446
|
if (error instanceof DockerApiError && error.code === 'docker/confirmation-required') {
|
|
@@ -429,6 +454,7 @@ export default function DockerPage(props) {
|
|
|
429
454
|
},
|
|
430
455
|
async () => {
|
|
431
456
|
await runs.startAction({ ...payload, confirm: true })
|
|
457
|
+
scheduleListRefresh()
|
|
432
458
|
},
|
|
433
459
|
)
|
|
434
460
|
return null
|
|
@@ -436,7 +462,7 @@ export default function DockerPage(props) {
|
|
|
436
462
|
notify('error', errorMessage(error))
|
|
437
463
|
return null
|
|
438
464
|
}
|
|
439
|
-
}, [actions, confirmThen, notify, provider, runs, t])
|
|
465
|
+
}, [actions, confirmThen, notify, provider, runs, scheduleListRefresh, t])
|
|
440
466
|
|
|
441
467
|
const openTab = useCallback((next) => actions.setTab(next), [actions])
|
|
442
468
|
|
|
@@ -631,6 +657,7 @@ export default function DockerPage(props) {
|
|
|
631
657
|
|
|
632
658
|
<AttachDialog
|
|
633
659
|
open={attach.open}
|
|
660
|
+
mode={attach.mode}
|
|
634
661
|
t={t}
|
|
635
662
|
session={attach.session}
|
|
636
663
|
output={attach.output}
|
package/src/client/styles.js
CHANGED
|
@@ -89,6 +89,7 @@ const CSS = `
|
|
|
89
89
|
.dsh-docker-btn.dsh-docker-sm { height: 26px; padding: 0 9px; font-size: 12px; border-radius: 6px; }
|
|
90
90
|
.dsh-docker-btn.dsh-docker-lg { height: 34px; padding: 0 14px; font-size: 13px; }
|
|
91
91
|
.dsh-docker-icon-btn {
|
|
92
|
+
position: relative;
|
|
92
93
|
appearance: none; border: .5px solid transparent; background: transparent; cursor: pointer;
|
|
93
94
|
width: 30px; height: 30px; border-radius: 7px; padding: 0;
|
|
94
95
|
display: inline-grid; place-items: center;
|
|
@@ -99,6 +100,13 @@ const CSS = `
|
|
|
99
100
|
.dsh-docker-icon-btn[disabled] { opacity: .4; cursor: not-allowed; }
|
|
100
101
|
.dsh-docker-icon-btn.dsh-docker-sm { width: 26px; height: 26px; }
|
|
101
102
|
|
|
103
|
+
.dsh-docker-tooltip {
|
|
104
|
+
position: fixed; z-index: 10000; transform: translate(-50%, -100%);
|
|
105
|
+
padding: 5px 8px; border-radius: 6px; pointer-events: none; white-space: nowrap;
|
|
106
|
+
color: var(--dsw-alias-label-primary, #fff); background: var(--dsw-alias-bg-inverse, #23252b);
|
|
107
|
+
font-size: 11px; line-height: 1.2; box-shadow: 0 4px 14px rgba(0,0,0,.18);
|
|
108
|
+
}
|
|
109
|
+
|
|
102
110
|
.dsh-docker-select, .dsh-docker-input {
|
|
103
111
|
appearance: none; -webkit-appearance: none;
|
|
104
112
|
height: 30px; min-width: 0; padding: 0 9px; border-radius: 7px;
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
StateLabel,
|
|
13
13
|
} from '../components.jsx'
|
|
14
14
|
import { useDockerList } from '../hooks.js'
|
|
15
|
-
import { IconAlert, IconAttach, IconBox, IconInfo, IconPlay, IconRestart, IconSparkles, IconStop, IconTerminal, IconTrash } from '../icons.jsx'
|
|
15
|
+
import { IconAlert, IconAttach, IconBox, IconExec, IconInfo, IconPlay, IconRestart, IconSparkles, IconStop, IconTerminal, IconTrash } from '../icons.jsx'
|
|
16
16
|
|
|
17
17
|
export default function ContainersTab({ t, api, provider, context, status, refreshToken, features, onAction, onAttach, onRunContainer, onAskAI, canAskAI, onInspect }) {
|
|
18
18
|
const [all, setAll] = useState(true)
|
|
@@ -112,6 +112,11 @@ export default function ContainersTab({ t, api, provider, context, status, refre
|
|
|
112
112
|
<IconSparkles size={14} />
|
|
113
113
|
</IconButton>
|
|
114
114
|
)}
|
|
115
|
+
{features.includes('exec') && item.state === 'running' && (
|
|
116
|
+
<IconButton size="sm" label={t('exec')} onClick={() => onAttach(item, 'exec')}>
|
|
117
|
+
<IconExec size={14} />
|
|
118
|
+
</IconButton>
|
|
119
|
+
)}
|
|
115
120
|
{features.includes('attach') && item.state === 'running' && (
|
|
116
121
|
<IconButton size="sm" label={t('attach')} onClick={() => onAttach(item)}>
|
|
117
122
|
<IconAttach size={14} />
|
package/src/host/attach.js
CHANGED
|
@@ -53,6 +53,7 @@ export class AttachManager {
|
|
|
53
53
|
* containerId: string,
|
|
54
54
|
* containerName?: string,
|
|
55
55
|
* command: string,
|
|
56
|
+
* mode?: 'attach' | 'exec',
|
|
56
57
|
* cols?: number,
|
|
57
58
|
* rows?: number,
|
|
58
59
|
* signal?: AbortSignal,
|
|
@@ -95,6 +96,7 @@ export class AttachManager {
|
|
|
95
96
|
containerId: input.containerId,
|
|
96
97
|
containerName: input.containerName ?? input.containerId,
|
|
97
98
|
command: input.command,
|
|
99
|
+
mode: input.mode === 'exec' ? 'exec' : 'attach',
|
|
98
100
|
cols,
|
|
99
101
|
rows,
|
|
100
102
|
status: 'running',
|
|
@@ -146,6 +148,7 @@ export class AttachManager {
|
|
|
146
148
|
containerId: session.containerId,
|
|
147
149
|
containerName: session.containerName,
|
|
148
150
|
command: session.command,
|
|
151
|
+
mode: session.mode,
|
|
149
152
|
cols: session.cols,
|
|
150
153
|
rows: session.rows,
|
|
151
154
|
status: session.status,
|
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', 'run', 'image-architecture']
|
|
259
|
+
const features = ['attach', 'exec', 'run', 'image-architecture']
|
|
260
260
|
if (this.toolsEnabled) features.push('ai-tools')
|
|
261
261
|
return { providers, defaultProvider, features }
|
|
262
262
|
}
|
|
@@ -645,6 +645,7 @@ export class DockerService {
|
|
|
645
645
|
async attachStart(provider, payload, signal) {
|
|
646
646
|
const id = safeIdentifier(payload.id)
|
|
647
647
|
const name = optionalString(payload.name, 'name') ?? id
|
|
648
|
+
const mode = payload.mode === 'exec' ? 'exec' : 'attach'
|
|
648
649
|
const cols = optionalInteger(payload.cols, 'cols', { min: 20, max: 300 }) ?? 120
|
|
649
650
|
const rows = optionalInteger(payload.rows, 'rows', { min: 5, max: 120 }) ?? 30
|
|
650
651
|
if (provider === 'apple-container') {
|
|
@@ -657,7 +658,7 @@ export class DockerService {
|
|
|
657
658
|
} catch (error) {
|
|
658
659
|
throw new CommandError(`无法读取 Apple container 状态: ${messageOf(error)}`, 'container/exec-failed')
|
|
659
660
|
}
|
|
660
|
-
if (!running) throw new CommandError(
|
|
661
|
+
if (!running) throw new CommandError(`容器未运行,无法 ${mode}`, 'container/not-running')
|
|
661
662
|
const argv = [path, 'exec', '-i', '-t', id, '/bin/sh']
|
|
662
663
|
return {
|
|
663
664
|
session: await this.attach.start({
|
|
@@ -668,6 +669,7 @@ export class DockerService {
|
|
|
668
669
|
containerId: id,
|
|
669
670
|
containerName: name,
|
|
670
671
|
command: `container exec -i -t ${id} /bin/sh`,
|
|
672
|
+
mode,
|
|
671
673
|
cols,
|
|
672
674
|
rows,
|
|
673
675
|
signal,
|
|
@@ -682,16 +684,18 @@ export class DockerService {
|
|
|
682
684
|
signal,
|
|
683
685
|
timeoutMs: 15_000,
|
|
684
686
|
})
|
|
685
|
-
if (running.stdout.trim() !== 'true') throw new CommandError(
|
|
687
|
+
if (running.stdout.trim() !== 'true') throw new CommandError(`容器未运行,无法 ${mode}`, 'docker/not-running')
|
|
688
|
+
const subargv = mode === 'exec' ? ['exec', '-i', '-t', id, '/bin/sh'] : ['attach', id]
|
|
686
689
|
return {
|
|
687
690
|
session: await this.attach.start({
|
|
688
|
-
argv: this.cli.argvFor(
|
|
691
|
+
argv: this.cli.argvFor(subargv, { context }),
|
|
689
692
|
cwd: this.home,
|
|
690
693
|
env: this.cli.childEnv(),
|
|
691
694
|
provider: 'docker',
|
|
692
695
|
containerId: id,
|
|
693
696
|
containerName: name,
|
|
694
|
-
command: describeCommand(
|
|
697
|
+
command: describeCommand(subargv, context),
|
|
698
|
+
mode,
|
|
695
699
|
cols,
|
|
696
700
|
rows,
|
|
697
701
|
signal,
|