@lemoncat7/dsh-ssh 1.1.7 → 1.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/ARCHITECTURE.md +62 -0
- package/README.md +25 -17
- package/lib/activity-panel.d.ts +17 -0
- package/lib/activity-panel.d.ts.map +1 -0
- package/lib/activity-panel.js +160 -0
- package/lib/activity-panel.js.map +1 -0
- package/lib/adaptive-workspace.d.ts +22 -0
- package/lib/adaptive-workspace.d.ts.map +1 -0
- package/lib/adaptive-workspace.js +37 -0
- package/lib/adaptive-workspace.js.map +1 -0
- package/lib/api.js +93 -6
- package/lib/api.js.map +1 -1
- package/lib/client-api.d.ts +20 -0
- package/lib/client-api.d.ts.map +1 -1
- package/lib/client-api.js +13 -0
- package/lib/client-api.js.map +1 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +3709 -21040
- package/lib/client.js.map +4 -4
- package/lib/domain.d.ts +17 -1
- package/lib/domain.d.ts.map +1 -1
- package/lib/domain.js +15 -0
- package/lib/domain.js.map +1 -1
- package/lib/profile-editor.d.ts +16 -0
- package/lib/profile-editor.d.ts.map +1 -0
- package/lib/profile-editor.js +143 -0
- package/lib/profile-editor.js.map +1 -0
- package/lib/project-session-dialog.d.ts +15 -0
- package/lib/project-session-dialog.d.ts.map +1 -0
- package/lib/project-session-dialog.js +60 -0
- package/lib/project-session-dialog.js.map +1 -0
- package/lib/remote-workspace-tree.d.ts +28 -0
- package/lib/remote-workspace-tree.d.ts.map +1 -0
- package/lib/remote-workspace-tree.js +127 -0
- package/lib/remote-workspace-tree.js.map +1 -0
- package/lib/resizable-split.d.ts +10 -0
- package/lib/resizable-split.d.ts.map +1 -0
- package/lib/resizable-split.js +115 -0
- package/lib/resizable-split.js.map +1 -0
- package/lib/session-access-channel.d.ts +6 -0
- package/lib/session-access-channel.d.ts.map +1 -0
- package/lib/session-access-channel.js +10 -0
- package/lib/session-access-channel.js.map +1 -0
- package/lib/session-access.d.ts +15 -0
- package/lib/session-access.d.ts.map +1 -0
- package/lib/session-access.js +99 -0
- package/lib/session-access.js.map +1 -0
- package/lib/sftp-client.d.ts +5 -1
- package/lib/sftp-client.d.ts.map +1 -1
- package/lib/sftp-client.js +85 -31
- package/lib/sftp-client.js.map +1 -1
- package/lib/store.d.ts +3 -1
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +35 -3
- package/lib/store.js.map +1 -1
- package/lib/terminal.d.ts +2 -0
- package/lib/terminal.d.ts.map +1 -1
- package/lib/terminal.js +10 -2
- package/lib/terminal.js.map +1 -1
- package/lib/ui-components.d.ts +25 -0
- package/lib/ui-components.d.ts.map +1 -0
- package/lib/ui-components.js +27 -0
- package/lib/ui-components.js.map +1 -0
- package/lib/workspace-ownership.d.ts +5 -0
- package/lib/workspace-ownership.d.ts.map +1 -0
- package/lib/workspace-ownership.js +18 -0
- package/lib/workspace-ownership.js.map +1 -0
- package/package.json +2 -2
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# DSH SSH architecture
|
|
2
|
+
|
|
3
|
+
This plugin is split into four boundaries. UI code never reaches the SSH transport directly, and server code never depends on the DSH client runtime.
|
|
4
|
+
|
|
5
|
+
## Runtime boundaries
|
|
6
|
+
|
|
7
|
+
### DSH integration
|
|
8
|
+
|
|
9
|
+
- `src/index.ts` registers the plugin and its server capabilities.
|
|
10
|
+
- `src/client.tsx` is the browser composition root. It only owns DSH slot registration, top-level workspace selection, and cross-feature navigation.
|
|
11
|
+
- `src/workspace-ownership.ts` keeps the conversation and details slots mutually exclusive without modifying DSH itself.
|
|
12
|
+
|
|
13
|
+
### Server application
|
|
14
|
+
|
|
15
|
+
- `src/api.ts` translates HTTP routes into explicit store, terminal, SFTP, forwarding, and credential operations.
|
|
16
|
+
- `src/store.ts` owns persistent SSH configuration.
|
|
17
|
+
- `src/session-access.ts` and `src/tools.ts` define the session authorization boundary used by AI tools.
|
|
18
|
+
- `src/connector.ts`, `src/terminal.ts`, `src/sftp.ts`, and `src/forwards.ts` own long-lived resources and their cleanup.
|
|
19
|
+
|
|
20
|
+
### Browser features
|
|
21
|
+
|
|
22
|
+
- `src/activity-panel.tsx` owns the session-scoped details panel and terminal observation lifecycle.
|
|
23
|
+
- `src/profile-editor.tsx` owns connection editing, validation, jump chains, and deletion safeguards.
|
|
24
|
+
- `src/remote-workspace-tree.tsx` owns host mounting, fixed directories, and remote-session creation.
|
|
25
|
+
- `src/sftp-client.tsx` owns directory browsing, upload, preview, and download.
|
|
26
|
+
- `src/resizable-split.tsx` owns the terminal/SFTP split and persisted sizing.
|
|
27
|
+
- `src/ui-components.tsx` provides shared dialog, field, segment, and empty-state behavior.
|
|
28
|
+
|
|
29
|
+
### Transport
|
|
30
|
+
|
|
31
|
+
- `src/terminal-transport.ts` is the browser stream client.
|
|
32
|
+
- `src/terminal-stream.ts` and `src/terminal-io.ts` provide ordered server-side output and input.
|
|
33
|
+
- `src/activity-events.ts` announces session terminal lifecycle changes.
|
|
34
|
+
|
|
35
|
+
## State ownership
|
|
36
|
+
|
|
37
|
+
| State | Owner | Persistence |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| SSH profiles, proxies, credentials, forwards | server store | profile data + DSH credential service |
|
|
40
|
+
| Mounted hosts, permission, fixed directories | session access store | per DSH session |
|
|
41
|
+
| Browser terminal process | terminal manager | process lifetime |
|
|
42
|
+
| Active panel, selected host, open dialog | React feature component | browser lifetime |
|
|
43
|
+
| Terminal/SFTP split width | `ResizableSplit` | browser local storage |
|
|
44
|
+
|
|
45
|
+
## Invariants
|
|
46
|
+
|
|
47
|
+
- The plugin uses only public DSH slots and injected client services; it does not patch DSH source code.
|
|
48
|
+
- A tool call must pass the session access boundary before reaching SSH resources.
|
|
49
|
+
- Secrets are write-only from the browser and are never returned by profile APIs.
|
|
50
|
+
- Terminal input is sequenced and terminal resources are explicitly disposed on close or unmount.
|
|
51
|
+
- Visual motion uses `transform` and `opacity`, remains interruptible, and is disabled by `prefers-reduced-motion`.
|
|
52
|
+
- Filesystem paths are treated as remote paths unless a value is explicitly named as a DSH local workspace.
|
|
53
|
+
|
|
54
|
+
## Review findings addressed in 1.2
|
|
55
|
+
|
|
56
|
+
- Split the former browser monolith into activity, profile editing, shared UI, remote tree, SFTP, and layout modules.
|
|
57
|
+
- Replaced overlapping polling intervals with completion-based, visibility-aware scheduling.
|
|
58
|
+
- Centralized modal focus trapping, Escape handling, unique accessible labels, and focus restoration.
|
|
59
|
+
- Grouped the long SSH connection form into connection, authentication, and route sections.
|
|
60
|
+
- Established shared motion tokens and reduced-motion fallbacks without adding an animation runtime dependency.
|
|
61
|
+
|
|
62
|
+
Future feature work should extend the closest feature module instead of adding unrelated state or styles to `client.tsx`.
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
面向 DeepSeek Harness 的 SSH 会话管理插件。它把连接管理、浏览器终端、代理、端口转发和 AI 会话授权放在同一个 DSH 工作区里。
|
|
4
4
|
|
|
5
|
+
浏览器 UI、服务端运行时、会话授权边界和终端生命周期的职责说明见 [ARCHITECTURE.md](./ARCHITECTURE.md)。
|
|
6
|
+
|
|
5
7
|
## 安装
|
|
6
8
|
|
|
7
9
|
```bash
|
|
@@ -10,13 +12,17 @@ dsh plugin --profile web add @lemoncat7/dsh-ssh@latest
|
|
|
10
12
|
|
|
11
13
|
安装或升级后重启对应的 DSH Profile。桌面端与 Docker 部署使用相同的插件包。
|
|
12
14
|
|
|
13
|
-
## 1.
|
|
15
|
+
## 1.2.0 更新
|
|
14
16
|
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
17
|
+
- 浏览器端按活动侧栏、连接编辑、主机树、SFTP、终端和共享 UI 分层;弹窗统一支持焦点锁定、Escape 关闭和焦点归还。
|
|
18
|
+
- SSH 活动与端口转发状态改为完成后调度,后台页面自动降频,避免慢请求造成轮询重叠。
|
|
19
|
+
- 新建与编辑连接按「连接信息 / 身份认证 / 连接路径」分组,并统一按钮、焦点、按压反馈和减少动态效果。
|
|
20
|
+
- SSH 面板改为「主机 / 固定项目目录」树,主工作区同时展示左侧终端和右侧 SFTP。
|
|
21
|
+
- 主机前的授权选择与底部「当前会话权限」共同控制 AI 可见范围,不再使用独立的会话注入检查器。
|
|
22
|
+
- 每台主机可以保存多个固定远端目录;当前会话最多显式绑定其中一个,终端、SFTP 和 AI 命令共享该工作目录。选中后目录项会明确显示当前会话已固定到该路径。
|
|
23
|
+
- 从固定远端目录新建会话时,会先选择会话归属的 DSH 本地项目;默认使用当前项目,其次使用最近项目,并明确区分本地项目路径与 SSH 远端工作目录。
|
|
24
|
+
- 固定目录后的新增按钮通过 DSH 官方 Workspace 接口创建并直接打开新会话,同时写入对应主机、目录与权限绑定;成功后自动关闭 SSH 管理面板,目录下不展示历史会话清单。
|
|
25
|
+
- 撤销主机访问会关闭该会话在对应主机上的 AI 终端;降级为仅命令会关闭全部交互终端。
|
|
20
26
|
|
|
21
27
|
## 能力
|
|
22
28
|
|
|
@@ -26,11 +32,12 @@ dsh plugin --profile web add @lemoncat7/dsh-ssh@latest
|
|
|
26
32
|
- 浏览器交互终端,支持输入、增量输出、窗口尺寸同步和主动断开
|
|
27
33
|
- 当前会话右侧栏支持多个 AI 终端切换与键盘输入
|
|
28
34
|
- AI 调用 `ssh_terminal_open` 创建终端时,当前会话会自动展开 SSH 右栏并切换到新终端
|
|
29
|
-
- SFTP 文件浏览、文本/图片/PDF
|
|
35
|
+
- SFTP 文件浏览、文本/图片/PDF 预览和流式下载;支持拖放或选择多个文件顺序上传,并可从内嵌预览打开独立放大窗口
|
|
30
36
|
- 有序多主机跳板链,兼容已有单跳板配置
|
|
31
37
|
- 独立密钥库集中保存常用用户名、密码和私钥,SSH 连接仅保存引用
|
|
32
38
|
- 独立代理库集中保存 HTTP CONNECT 与 SOCKS5 代理,多台主机可复用同一连接路径
|
|
33
|
-
- 按 DSH
|
|
39
|
+
- 按 DSH 会话授权连接;未勾选的主机对 AI 完全不可见
|
|
40
|
+
- 保存固定远端项目目录,并从目录直接创建和打开新 DSH 会话
|
|
34
41
|
- AI 可执行一次性命令,或打开、读取、操作和关闭独立的交互终端
|
|
35
42
|
- 左侧远端区域提供右侧栏开关;目录页使用真实 SFTP 浏览远端文件,终端页使用单一终端画面观察并操作 AI 终端
|
|
36
43
|
- 插件服务与 Web 客户端兼容 Windows、macOS 和 Linux,不依赖本机 `ssh` 或 `sftp` 命令
|
|
@@ -40,15 +47,14 @@ dsh plugin --profile web add @lemoncat7/dsh-ssh@latest
|
|
|
40
47
|
|
|
41
48
|
侧栏入口名为「远端」,通过官方 `sidebar.footer.action` 注册。浏览器兼容层只把这个入口锚定到官方 Workspace 区域上方;它不会替换 `sidebar.workspaces`,锚点失效时会自动留在官方 Footer。远端标题右侧按钮与折叠栏图标负责开关 SSH 右侧栏;展开后的第一项「SSH 面板」进入完整管理工作区,它下面只显示当前 DSH 会话已授权的 SSH 连接。点击其中一台主机会打开右侧栏并直接切换到该远端。切换到其他会话会自动退出管理工作区,也可以使用工作区左上角的返回按钮。
|
|
42
49
|
|
|
43
|
-
|
|
50
|
+
管理工作区分成两部分:
|
|
44
51
|
|
|
45
|
-
1.
|
|
46
|
-
2.
|
|
47
|
-
3. 右侧会话注入检查器:选择当前对话可用的主机、权限级别,以及是否自动允许权限范围内的 SSH 操作。
|
|
52
|
+
1. 左侧远端树:点击主机行只决定当前会话能否访问,展开箭头独立控制固定目录的展开与收起。点击目录整行即可固定,再次点击已固定目录即可取消;每台主机最多固定一个目录。目录后的新增按钮直接创建并打开新会话。底部统一设置「仅命令 / 终端控制」与执行前确认。
|
|
53
|
+
2. 右侧工作区:默认把终端与当前主机或项目目录的 SFTP 左右并排展示,不需要在两个页面之间切换;SFTP 会等终端首次连接成功后再读取远端目录。拖动两栏之间的分隔线可以调整 SFTP 宽度,双击恢复默认,比例会保存在当前浏览器。另可进入端口转发、密钥库、代理库和设置。SFTP 支持浏览、预览、下载,以及拖放或文件选择器触发的多文件顺序上传;同名文件逐项确认跳过或覆盖,单文件上限为 512 MB。内嵌文件预览可通过下载旁的放大按钮打开独立预览窗口。
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
工作区使用插件内职责单一的自适应壳布局。宽容器使用双栏;窄于 820px 时远端树切换为左侧抽屉,窄于 520px 时进一步压缩 SFTP 次要信息和操作密度。响应式判断使用容器查询,因此手机、桌面分屏和 DSH 窄面板采用同一套行为。
|
|
50
56
|
|
|
51
|
-
SSH
|
|
57
|
+
SSH 插件不会向聊天标题栏添加按钮。右侧栏统一从左侧「远端」区域打开;当前会话尚未授权主机时会显示引导。仅执行命令权限显示 SFTP 目录;终端控制权限同时显示 SFTP 目录和 AI 终端。多个终端通过紧凑标签切换,运行中的终端支持键盘输入。进入 SFTP 子目录时,该目录会同步成为后续 `ssh_exec` 与新终端的工作目录。
|
|
52
58
|
|
|
53
59
|
新建和编辑连接的表单内提供「测试连接」。测试直接使用尚未保存的表单内容,可验证密钥库凭据、HTTP/SOCKS5 代理和有序跳板链;首次连接的主机指纹也在表单内确认。测试过程不会临时创建 Profile,也不会把凭据写入配置文件。
|
|
54
60
|
|
|
@@ -56,7 +62,7 @@ SSH 插件不会向聊天标题栏添加按钮。右侧栏统一从左侧「远
|
|
|
56
62
|
|
|
57
63
|
| 工具 | 用途 |
|
|
58
64
|
| --- | --- |
|
|
59
|
-
| `ssh_list` | 只列出当前 DSH
|
|
65
|
+
| `ssh_list` | 只列出当前 DSH 会话已授权的连接 |
|
|
60
66
|
| `ssh_set_cwd` | 设置并验证当前会话在指定主机上的工作目录 |
|
|
61
67
|
| `ssh_exec` | 执行一次性远端命令 |
|
|
62
68
|
| `ssh_terminal_open` | 打开当前 Agent 独占的 SSH 终端 |
|
|
@@ -64,11 +70,13 @@ SSH 插件不会向聊天标题栏添加按钮。右侧栏统一从左侧「远
|
|
|
64
70
|
| `ssh_terminal_read` | 分页读取终端回滚缓冲区 |
|
|
65
71
|
| `ssh_terminal_signal` | 发送允许的 POSIX 信号 |
|
|
66
72
|
| `ssh_terminal_close` | 关闭终端 |
|
|
67
|
-
| `ssh_forward_list` |
|
|
73
|
+
| `ssh_forward_list` | 列出已授权连接的转发规则 |
|
|
68
74
|
| `ssh_forward_start` | 启动已有转发规则 |
|
|
69
75
|
| `ssh_forward_stop` | 停止已有转发规则 |
|
|
70
76
|
|
|
71
|
-
工具始终从 `exec.agent.session.id`
|
|
77
|
+
工具始终从 `exec.agent.session.id` 解析会话授权关系。模型不能通过参数绕过授权,也不能枚举其他 DSH 会话的主机或终端。
|
|
78
|
+
|
|
79
|
+
「执行前确认」依赖 DSH 当前会话的审批策略:Workspace Write 的 Ask 策略会显示确认;Full Access 使用 Never 策略,不显示确认,并会直接拒绝被 SSH 插件标记为需要审批的操作。若希望在 Full Access 下直接执行 SSH,请关闭该开关。
|
|
72
80
|
|
|
73
81
|
## 凭据与安全
|
|
74
82
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
+
type DetailsProps = PropsRuntime<'details'>;
|
|
3
|
+
export type ActivityViewMode = 'local-directory' | 'remote-directory' | 'terminals';
|
|
4
|
+
export interface ActivityController {
|
|
5
|
+
open(sessionId: string, profileId?: string, view?: ActivityViewMode): void;
|
|
6
|
+
toggle(sessionId: string): void;
|
|
7
|
+
close(sessionId?: string): void;
|
|
8
|
+
isOpen(sessionId: string): boolean;
|
|
9
|
+
selected(sessionId: string): string | undefined;
|
|
10
|
+
requestedView(sessionId: string): ActivityViewMode | undefined;
|
|
11
|
+
subscribe(listener: () => void): () => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function SshActivityPanel(props: DetailsProps & {
|
|
14
|
+
controller: ActivityController;
|
|
15
|
+
}): JSX.Element;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=activity-panel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activity-panel.d.ts","sourceRoot":"","sources":["../src/activity-panel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAwBpE,KAAK,YAAY,GAAG,YAAY,CAAC,SAAS,CAAC,CAAA;AAE3C,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,GAAG,kBAAkB,GAAG,WAAW,CAAA;AAEnF,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAC1E,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAA;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IAC/C,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAAA;IAC9D,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;CAC5C;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,GAAG;IAAE,UAAU,EAAE,kBAAkB,CAAA;CAAE,GAAG,GAAG,CAAC,OAAO,CAyEtG"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { IconCloseOutline16, IconCodeOutline16, IconFolderOpenOutline16, } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
|
+
import { FitAddon } from '@xterm/addon-fit';
|
|
5
|
+
import { activityTerminalStreamUrl, closeActivityTerminal, loadActivity, readActivityTerminalOutput, resizeActivityTerminal, sendActivityTerminalInput, } from './client-api.js';
|
|
6
|
+
import { ActivitySftpBrowser, LocalWorkspaceBrowser } from './sftp-client.js';
|
|
7
|
+
import { TerminalTransport } from './terminal-transport.js';
|
|
8
|
+
import { attachTerminalViewport, createSshTerminal } from './terminal-view.js';
|
|
9
|
+
import { ServerGlyph, errorMessage, shortId } from './ui-components.js';
|
|
10
|
+
export function SshActivityPanel(props) {
|
|
11
|
+
const sessionId = String(props.sessionId);
|
|
12
|
+
const [activity, setActivity] = useState();
|
|
13
|
+
const [view, setView] = useState(() => props.controller.requestedView(sessionId) ?? 'local-directory');
|
|
14
|
+
const [error, setError] = useState();
|
|
15
|
+
useEffect(() => props.controller.subscribe(() => {
|
|
16
|
+
const requested = props.controller.requestedView(sessionId);
|
|
17
|
+
if (requested !== undefined)
|
|
18
|
+
setView(requested);
|
|
19
|
+
}), [props.controller, sessionId]);
|
|
20
|
+
const refresh = useCallback(async () => {
|
|
21
|
+
try {
|
|
22
|
+
const next = await loadActivity(sessionId);
|
|
23
|
+
setActivity(next);
|
|
24
|
+
if (next.injection?.permission !== 'terminal') {
|
|
25
|
+
setView(current => current === 'terminals' ? 'local-directory' : current);
|
|
26
|
+
}
|
|
27
|
+
setError(undefined);
|
|
28
|
+
}
|
|
29
|
+
catch (reason) {
|
|
30
|
+
setError(errorMessage(reason));
|
|
31
|
+
}
|
|
32
|
+
}, [sessionId]);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
let disposed = false;
|
|
35
|
+
let timer;
|
|
36
|
+
let polling = false;
|
|
37
|
+
const poll = async () => {
|
|
38
|
+
if (polling)
|
|
39
|
+
return;
|
|
40
|
+
polling = true;
|
|
41
|
+
await refresh();
|
|
42
|
+
polling = false;
|
|
43
|
+
if (disposed)
|
|
44
|
+
return;
|
|
45
|
+
const foregroundDelay = view === 'terminals' ? 2200 : 3200;
|
|
46
|
+
timer = window.setTimeout(() => { void poll(); }, document.visibilityState === 'hidden' ? 12000 : foregroundDelay);
|
|
47
|
+
};
|
|
48
|
+
const resume = () => {
|
|
49
|
+
if (document.visibilityState !== 'visible' || disposed)
|
|
50
|
+
return;
|
|
51
|
+
if (timer !== undefined)
|
|
52
|
+
window.clearTimeout(timer);
|
|
53
|
+
timer = undefined;
|
|
54
|
+
void poll();
|
|
55
|
+
};
|
|
56
|
+
document.addEventListener('visibilitychange', resume);
|
|
57
|
+
void poll();
|
|
58
|
+
return () => {
|
|
59
|
+
disposed = true;
|
|
60
|
+
if (timer !== undefined)
|
|
61
|
+
window.clearTimeout(timer);
|
|
62
|
+
document.removeEventListener('visibilitychange', resume);
|
|
63
|
+
};
|
|
64
|
+
}, [refresh, view]);
|
|
65
|
+
return _jsxs("section", { className: "dsh-ssh-activity-panel", "aria-label": "SSH \u6D3B\u52A8", children: [_jsxs("header", { className: "dsh-ssh-activity-header", children: [_jsxs("div", { className: "dsh-ssh-activity-title", children: [_jsx("span", { className: "dsh-ssh-activity-mark", children: _jsx(ServerGlyph, {}) }), _jsxs("span", { children: [_jsx("strong", { children: "SSH \u6D3B\u52A8" }), _jsxs("small", { children: ["\u5F53\u524D\u4F1A\u8BDD \u00B7 ", shortId(sessionId)] })] })] }), _jsx("button", { type: "button", className: "dsh-ssh-icon-button", "aria-label": "\u5173\u95ED SSH \u6D3B\u52A8", onClick: () => props.controller.close(sessionId), children: _jsx(IconCloseOutline16, { size: 16 }) })] }), activity && _jsxs("nav", { className: "dsh-ssh-activity-tabs", "aria-label": "SSH \u6D3B\u52A8\u89C6\u56FE", children: [_jsxs("button", { type: "button", className: view === 'local-directory' ? 'is-active' : '', "aria-pressed": view === 'local-directory', onClick: () => setView('local-directory'), children: [_jsx(IconFolderOpenOutline16, { size: 16 }), "\u4F1A\u8BDD\u76EE\u5F55"] }), activity.profiles.length > 0 && _jsxs("button", { type: "button", className: view === 'remote-directory' ? 'is-active' : '', "aria-pressed": view === 'remote-directory', onClick: () => setView('remote-directory'), children: [_jsx(ServerGlyph, {}), "\u8FDC\u7AEF\u76EE\u5F55"] }), activity.injection?.permission === 'terminal' && _jsxs("button", { type: "button", className: view === 'terminals' ? 'is-active' : '', "aria-pressed": view === 'terminals', onClick: () => setView('terminals'), children: [_jsx(IconCodeOutline16, { size: 16 }), "\u7EC8\u7AEF", activity.terminals.length > 0 && _jsx("em", { children: activity.terminals.length })] })] }), _jsxs("div", { className: "dsh-ssh-activity-body", children: [activity === undefined ? _jsx("p", { className: "dsh-ssh-activity-state", role: "status", children: "\u6B63\u5728\u8BFB\u53D6 SSH \u4F1A\u8BDD\u2026" })
|
|
66
|
+
: view === 'local-directory' ? _jsx(LocalWorkspaceBrowser, { sessionId: sessionId })
|
|
67
|
+
: view === 'remote-directory' ? _jsx(RemoteDirectoryActivity, { sessionId: sessionId, profiles: activity.profiles, selectedProfileId: props.controller.selected(sessionId), onProfile: profileId => props.controller.open(sessionId, profileId, 'remote-directory'), onSaved: refresh })
|
|
68
|
+
: _jsx(TerminalActivity, { sessionId: sessionId, terminals: activity.terminals, onClosed: refresh }), error && _jsx("p", { className: "dsh-ssh-inline-error", role: "alert", children: error })] })] });
|
|
69
|
+
}
|
|
70
|
+
function RemoteDirectoryActivity({ sessionId, profiles, selectedProfileId, onProfile, onSaved }) {
|
|
71
|
+
const profile = profiles.find(item => item.id === selectedProfileId) ?? profiles[0];
|
|
72
|
+
if (profile === undefined)
|
|
73
|
+
return _jsxs("div", { className: "dsh-ssh-activity-empty", children: [_jsx(IconFolderOpenOutline16, { size: 22 }), _jsx("strong", { children: "\u6CA1\u6709\u53EF\u6D4F\u89C8\u7684\u8FDC\u7AEF" }), _jsx("p", { children: "\u8BF7\u5148\u5728 SSH \u9762\u677F\u4E2D\u5141\u8BB8\u5F53\u524D\u4F1A\u8BDD\u8BBF\u95EE\u4E00\u53F0\u4E3B\u673A\u3002" })] });
|
|
74
|
+
return _jsx(ActivitySftpBrowser, { sessionId: sessionId, profile: profile, profiles: profiles, onProfile: onProfile, onSaved: onSaved }, profile.id);
|
|
75
|
+
}
|
|
76
|
+
function TerminalActivity({ sessionId, terminals, onClosed }) {
|
|
77
|
+
const preferred = [...terminals].reverse().find(item => item.status.kind === 'running') ?? terminals.at(-1);
|
|
78
|
+
const [selectedId, setSelectedId] = useState(preferred?.terminalId);
|
|
79
|
+
const [closingId, setClosingId] = useState();
|
|
80
|
+
const [error, setError] = useState();
|
|
81
|
+
const terminal = terminals.find(item => item.terminalId === selectedId) ?? preferred;
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (selectedId === undefined || !terminals.some(item => item.terminalId === selectedId)) {
|
|
84
|
+
setSelectedId(preferred?.terminalId);
|
|
85
|
+
}
|
|
86
|
+
}, [preferred?.terminalId, selectedId, terminals]);
|
|
87
|
+
if (terminal === undefined)
|
|
88
|
+
return _jsxs("div", { className: "dsh-ssh-activity-empty", children: [_jsx(IconCodeOutline16, { size: 22 }), _jsx("strong", { children: "\u8FD8\u6CA1\u6709\u6253\u5F00\u7684\u7EC8\u7AEF" }), _jsx("p", { children: "AI \u6253\u5F00\u4EA4\u4E92\u7EC8\u7AEF\u540E\uFF0C\u4F1A\u76F4\u63A5\u663E\u793A\u5728\u8FD9\u91CC\u3002" })] });
|
|
89
|
+
const closeSelectedTerminal = async () => {
|
|
90
|
+
if (closingId !== undefined)
|
|
91
|
+
return;
|
|
92
|
+
setClosingId(terminal.terminalId);
|
|
93
|
+
setError(undefined);
|
|
94
|
+
try {
|
|
95
|
+
await closeActivityTerminal(sessionId, terminal.terminalId);
|
|
96
|
+
await onClosed();
|
|
97
|
+
}
|
|
98
|
+
catch (reason) {
|
|
99
|
+
setError(errorMessage(reason));
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
setClosingId(undefined);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const closeLabel = terminal.status.kind === 'running' ? '结束' : '移除';
|
|
106
|
+
return _jsxs("div", { className: "dsh-ssh-terminal-workbench", children: [terminals.length > 1 && _jsx("nav", { className: "dsh-ssh-terminal-switcher dsh-ssh-scroll-surface", "aria-label": "SSH \u7EC8\u7AEF", children: terminals.map((item, index) => {
|
|
107
|
+
const label = item.name || `终端 ${index + 1}`;
|
|
108
|
+
return _jsxs("button", { type: "button", title: label, className: item.terminalId === terminal.terminalId ? 'is-active' : '', "aria-pressed": item.terminalId === terminal.terminalId, onClick: () => setSelectedId(item.terminalId), children: [_jsx("span", { className: `dsh-ssh-terminal-state-dot is-${item.status.kind}` }), _jsx("span", { className: "dsh-ssh-terminal-switcher-label", children: label })] }, item.terminalId);
|
|
109
|
+
}) }), _jsxs("div", { className: "dsh-ssh-terminal-observer", children: [_jsxs("div", { className: "dsh-ssh-terminal-observer-heading", children: [_jsxs("span", { children: [_jsx("strong", { children: terminal.name }), _jsx("small", { children: terminal.cwd })] }), _jsxs("div", { className: "dsh-ssh-terminal-observer-actions", children: [_jsx("span", { className: `dsh-ssh-terminal-state is-${terminal.status.kind}`, children: terminal.status.kind === 'running' ? '运行中' : '已退出' }), _jsxs("button", { type: "button", className: "dsh-ssh-terminal-close", disabled: closingId !== undefined, "aria-label": `${closeLabel}终端 ${terminal.name}`, title: terminal.status.kind === 'running' ? '结束并关闭这个终端' : '从活动面板移除这个终端', onClick: () => { void closeSelectedTerminal(); }, children: [_jsx(IconCloseOutline16, { size: 14 }), _jsx("span", { children: closingId === terminal.terminalId ? '处理中' : closeLabel })] })] })] }), _jsx(InteractiveTerminal, { sessionId: sessionId, terminal: terminal, onError: setError }, terminal.terminalId)] }), error && _jsx("p", { className: "dsh-ssh-terminal-input-error", role: "alert", children: error })] });
|
|
110
|
+
}
|
|
111
|
+
function InteractiveTerminal({ sessionId, terminal: activity, onError }) {
|
|
112
|
+
const hostRef = useRef(null);
|
|
113
|
+
const terminalRef = useRef();
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
const host = hostRef.current;
|
|
116
|
+
if (host === null)
|
|
117
|
+
return;
|
|
118
|
+
const terminal = createSshTerminal({ compact: true, readOnly: activity.status.kind !== 'running', scrollback: 4000 });
|
|
119
|
+
const fit = new FitAddon();
|
|
120
|
+
terminal.loadAddon(fit);
|
|
121
|
+
terminal.open(host);
|
|
122
|
+
terminalRef.current = terminal;
|
|
123
|
+
terminal.focus();
|
|
124
|
+
const transport = new TerminalTransport({
|
|
125
|
+
streamUrl: activityTerminalStreamUrl(sessionId, activity.terminalId),
|
|
126
|
+
read: cursor => readActivityTerminalOutput(sessionId, activity.terminalId, cursor),
|
|
127
|
+
send: (text, sequence) => sendActivityTerminalInput(sessionId, activity.terminalId, text, sequence),
|
|
128
|
+
});
|
|
129
|
+
const input = terminal.onData(data => transport.sendInput(data, reason => onError(errorMessage(reason))));
|
|
130
|
+
const stopOutput = transport.observe({
|
|
131
|
+
output: value => {
|
|
132
|
+
if (value.truncated)
|
|
133
|
+
terminal.reset();
|
|
134
|
+
if (value.data)
|
|
135
|
+
terminal.write(value.data);
|
|
136
|
+
terminal.scrollToBottom();
|
|
137
|
+
onError(undefined);
|
|
138
|
+
},
|
|
139
|
+
error: reason => onError(errorMessage(reason)),
|
|
140
|
+
});
|
|
141
|
+
const viewport = attachTerminalViewport(host, terminal, fit, (cols, rows) => {
|
|
142
|
+
void resizeActivityTerminal(sessionId, activity.terminalId, cols, rows).catch(reason => onError(errorMessage(reason)));
|
|
143
|
+
});
|
|
144
|
+
return () => {
|
|
145
|
+
stopOutput();
|
|
146
|
+
transport.dispose();
|
|
147
|
+
input.dispose();
|
|
148
|
+
viewport.dispose();
|
|
149
|
+
terminal.dispose();
|
|
150
|
+
terminalRef.current = undefined;
|
|
151
|
+
};
|
|
152
|
+
}, [activity.terminalId, onError, sessionId]);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
const terminal = terminalRef.current;
|
|
155
|
+
if (terminal !== undefined)
|
|
156
|
+
terminal.options.disableStdin = activity.status.kind !== 'running';
|
|
157
|
+
}, [activity.status.kind]);
|
|
158
|
+
return _jsx("div", { className: "dsh-ssh-terminal-screen", "aria-label": "\u4EA4\u4E92\u5F0F SSH \u7EC8\u7AEF", children: _jsx("div", { ref: hostRef, className: "dsh-ssh-terminal-viewport" }) });
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=activity-panel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activity-panel.js","sourceRoot":"","sources":["../src/activity-panel.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface AdaptiveWorkspaceControls {
|
|
3
|
+
closePanel(): void;
|
|
4
|
+
openPanel(panel: AdaptivePanel): void;
|
|
5
|
+
}
|
|
6
|
+
type AdaptivePanel = 'navigation' | 'inspector';
|
|
7
|
+
type AdaptiveWorkspaceSlot = ReactNode | ((controls: AdaptiveWorkspaceControls) => ReactNode);
|
|
8
|
+
interface AdaptiveWorkspaceProps {
|
|
9
|
+
className?: string;
|
|
10
|
+
toolbar?: ReactNode;
|
|
11
|
+
notice?: ReactNode;
|
|
12
|
+
navigation: AdaptiveWorkspaceSlot;
|
|
13
|
+
navigationLabel: string;
|
|
14
|
+
navigationIcon?: ReactNode;
|
|
15
|
+
inspector?: AdaptiveWorkspaceSlot;
|
|
16
|
+
inspectorLabel?: string;
|
|
17
|
+
inspectorIcon?: ReactNode;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
export declare function AdaptiveWorkspace({ className, toolbar, notice, navigation, navigationLabel, navigationIcon, inspector, inspectorLabel, inspectorIcon, children }: AdaptiveWorkspaceProps): JSX.Element;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=adaptive-workspace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adaptive-workspace.d.ts","sourceRoot":"","sources":["../src/adaptive-workspace.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAEnE,MAAM,WAAW,yBAAyB;IACxC,UAAU,IAAI,IAAI,CAAA;IAClB,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAA;CACtC;AAED,KAAK,aAAa,GAAG,YAAY,GAAG,WAAW,CAAA;AAC/C,KAAK,qBAAqB,GAAG,SAAS,GAAG,CAAC,CAAC,QAAQ,EAAE,yBAAyB,KAAK,SAAS,CAAC,CAAA;AAE7F,UAAU,sBAAsB;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,UAAU,EAAE,qBAAqB,CAAA;IACjC,eAAe,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,SAAS,CAAA;IAC1B,SAAS,CAAC,EAAE,qBAAqB,CAAA;IACjC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,SAAS,CAAA;IACzB,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,iBAAiB,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,cAAc,EAAE,SAAS,EAAE,cAAqB,EAAE,aAAa,EAAE,QAAQ,EAAE,EAAE,sBAAsB,GAAG,GAAG,CAAC,OAAO,CAwC7M"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
export function AdaptiveWorkspace({ className, toolbar, notice, navigation, navigationLabel, navigationIcon, inspector, inspectorLabel = '详情', inspectorIcon, children }) {
|
|
4
|
+
const [panel, setPanel] = useState();
|
|
5
|
+
const navigationRef = useRef(null);
|
|
6
|
+
const inspectorRef = useRef(null);
|
|
7
|
+
const returnFocusRef = useRef(null);
|
|
8
|
+
const closePanel = () => {
|
|
9
|
+
setPanel(undefined);
|
|
10
|
+
window.requestAnimationFrame(() => {
|
|
11
|
+
const target = returnFocusRef.current;
|
|
12
|
+
if (target?.isConnected)
|
|
13
|
+
target.focus();
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
const controls = {
|
|
17
|
+
closePanel,
|
|
18
|
+
openPanel: next => setPanel(next),
|
|
19
|
+
};
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (panel === undefined)
|
|
22
|
+
return;
|
|
23
|
+
returnFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
24
|
+
const target = panel === 'navigation' ? navigationRef.current : inspectorRef.current;
|
|
25
|
+
const initial = target?.querySelector('input:not(:disabled), button:not(:disabled), select:not(:disabled), textarea:not(:disabled), a[href], [tabindex]:not([tabindex="-1"])');
|
|
26
|
+
window.requestAnimationFrame(() => initial?.focus());
|
|
27
|
+
const close = (event) => { if (event.key === 'Escape')
|
|
28
|
+
closePanel(); };
|
|
29
|
+
window.addEventListener('keydown', close);
|
|
30
|
+
return () => window.removeEventListener('keydown', close);
|
|
31
|
+
}, [panel]);
|
|
32
|
+
return _jsxs("main", { className: `dsh-ssh-adaptive-workspace${className === undefined ? '' : ` ${className}`}`, "data-panel": panel ?? 'none', "data-has-inspector": inspector === undefined ? 'false' : 'true', children: [toolbar, notice, _jsxs("nav", { className: "dsh-ssh-mobile-actions", "aria-label": "\u5DE5\u4F5C\u533A\u9762\u677F", children: [_jsxs("button", { type: "button", "aria-controls": "dsh-ssh-adaptive-navigation", "aria-expanded": panel === 'navigation', onClick: () => panel === 'navigation' ? closePanel() : setPanel('navigation'), children: [navigationIcon, _jsx("span", { children: navigationLabel })] }), inspector !== undefined && _jsxs("button", { type: "button", "aria-controls": "dsh-ssh-adaptive-inspector", "aria-expanded": panel === 'inspector', onClick: () => panel === 'inspector' ? closePanel() : setPanel('inspector'), children: [inspectorIcon, _jsx("span", { children: inspectorLabel })] })] }), _jsxs("div", { className: "dsh-ssh-adaptive-shell", children: [_jsx("aside", { ref: navigationRef, id: "dsh-ssh-adaptive-navigation", className: "dsh-ssh-adaptive-navigation", "aria-label": navigationLabel, children: renderSlot(navigation, controls) }), _jsx("section", { className: "dsh-ssh-adaptive-content", children: children }), inspector !== undefined && _jsx("aside", { ref: inspectorRef, id: "dsh-ssh-adaptive-inspector", className: "dsh-ssh-adaptive-inspector", "aria-label": inspectorLabel, children: renderSlot(inspector, controls) }), _jsx("button", { type: "button", className: "dsh-ssh-adaptive-backdrop", "aria-label": "\u5173\u95ED\u9762\u677F", tabIndex: panel === undefined ? -1 : 0, onClick: closePanel })] })] });
|
|
33
|
+
}
|
|
34
|
+
function renderSlot(slot, controls) {
|
|
35
|
+
return typeof slot === 'function' ? slot(controls) : slot;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=adaptive-workspace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adaptive-workspace.js","sourceRoot":"","sources":["../src/adaptive-workspace.tsx"],"names":[],"mappings":"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"}
|
package/lib/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { pipeline } from 'node:stream/promises';
|
|
3
3
|
import { HostKeyRequiredError } from './connector.js';
|
|
4
|
-
import { normalizeCredentialEntryDraft, normalizeForwardDraft, normalizeProfileDraft, normalizeProxyEntryDraft, normalizeSecrets, } from './domain.js';
|
|
4
|
+
import { normalizeCredentialEntryDraft, normalizeForwardDraft, normalizeProfileDraft, normalizeProxyEntryDraft, normalizeRemoteProjectDraft, normalizeSecrets, } from './domain.js';
|
|
5
5
|
import { setSessionDirectory } from './directory.js';
|
|
6
6
|
import { streamTerminalOutput } from './terminal-stream.js';
|
|
7
7
|
import { listSftpDirectory, openSftpFile, readSftpFilePreview, uploadSftpFile } from './sftp.js';
|
|
@@ -30,7 +30,7 @@ async function dispatch(req, res, prefix, runtime) {
|
|
|
30
30
|
const segments = relative ? relative.split('/').map(decodeURIComponent) : [];
|
|
31
31
|
const method = req.method ?? 'GET';
|
|
32
32
|
if (method === 'GET' && segments[0] === 'health')
|
|
33
|
-
return sendJson(res, 200, { ok: true, service: 'dsh-ssh', schemaVersion:
|
|
33
|
+
return sendJson(res, 200, { ok: true, service: 'dsh-ssh', schemaVersion: 4 });
|
|
34
34
|
if (segments[0] === 'vault') {
|
|
35
35
|
if (method === 'GET' && segments.length === 1)
|
|
36
36
|
return sendJson(res, 200, await credentialEntryViews(runtime));
|
|
@@ -178,6 +178,49 @@ async function dispatch(req, res, prefix, runtime) {
|
|
|
178
178
|
return sendJson(res, 201, await profileView(runtime, profile));
|
|
179
179
|
}
|
|
180
180
|
const id = segments[1];
|
|
181
|
+
if (id !== undefined && segments[2] === 'projects') {
|
|
182
|
+
requiredProfile(runtime.store, id);
|
|
183
|
+
if (method === 'GET' && segments.length === 3)
|
|
184
|
+
return sendJson(res, 200, runtime.store.remoteProjects(id));
|
|
185
|
+
if (method === 'POST' && segments.length === 3) {
|
|
186
|
+
requireMutationHeader(req);
|
|
187
|
+
const draft = normalizeRemoteProjectDraft((await readObject(req)).project);
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
const project = { ...draft, id: createId('project'), profileId: id, createdAt: now, updatedAt: now };
|
|
190
|
+
await runtime.store.update(state => { state.remoteProjects.push(project); });
|
|
191
|
+
return sendJson(res, 201, project);
|
|
192
|
+
}
|
|
193
|
+
const projectId = segments[3];
|
|
194
|
+
if (projectId !== undefined && method === 'PUT' && segments.length === 4) {
|
|
195
|
+
requireMutationHeader(req);
|
|
196
|
+
const previous = requiredRemoteProject(runtime.store, projectId, id);
|
|
197
|
+
const draft = normalizeRemoteProjectDraft((await readObject(req)).project);
|
|
198
|
+
const next = { ...previous, ...draft, updatedAt: Date.now() };
|
|
199
|
+
await runtime.store.update(state => {
|
|
200
|
+
state.remoteProjects = state.remoteProjects.map(project => project.id === projectId ? next : project);
|
|
201
|
+
state.injections = state.injections.map(injection => {
|
|
202
|
+
if (injection.workingProjectIds[previous.profileId] !== projectId || injection.workingDirectories[previous.profileId] !== previous.path)
|
|
203
|
+
return injection;
|
|
204
|
+
return { ...injection, workingDirectories: { ...injection.workingDirectories, [previous.profileId]: next.path } };
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
return sendJson(res, 200, next);
|
|
208
|
+
}
|
|
209
|
+
if (projectId !== undefined && method === 'DELETE' && segments.length === 4) {
|
|
210
|
+
requireMutationHeader(req);
|
|
211
|
+
requiredRemoteProject(runtime.store, projectId, id);
|
|
212
|
+
await runtime.store.update(state => {
|
|
213
|
+
state.remoteProjects = state.remoteProjects.filter(project => project.id !== projectId);
|
|
214
|
+
state.injections = state.injections.map(injection => {
|
|
215
|
+
if (injection.workingProjectIds[id] !== projectId)
|
|
216
|
+
return injection;
|
|
217
|
+
const { [id]: _removed, ...workingProjectIds } = injection.workingProjectIds;
|
|
218
|
+
return { ...injection, workingProjectIds };
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
return sendJson(res, 204, undefined);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
181
224
|
if (id !== undefined && segments[2] === 'sftp') {
|
|
182
225
|
requiredProfile(runtime.store, id);
|
|
183
226
|
const operation = segments[3];
|
|
@@ -236,10 +279,12 @@ async function dispatch(req, res, prefix, runtime) {
|
|
|
236
279
|
await Promise.all(related.map(rule => runtime.forwards.stop(rule.id).catch(() => { })));
|
|
237
280
|
await runtime.store.update(state => {
|
|
238
281
|
state.profiles = state.profiles.filter(profile => profile.id !== id);
|
|
282
|
+
state.remoteProjects = (state.remoteProjects ?? []).filter(project => project.profileId !== id);
|
|
239
283
|
state.forwardRules = state.forwardRules.filter(rule => rule.profileId !== id);
|
|
240
284
|
state.injections = state.injections.map(item => {
|
|
241
285
|
const { [id]: _removed, ...workingDirectories } = item.workingDirectories;
|
|
242
|
-
|
|
286
|
+
const { [id]: _removedProject, ...workingProjectIds } = item.workingProjectIds;
|
|
287
|
+
return { ...item, profileIds: item.profileIds.filter(profileId => profileId !== id), workingDirectories, workingProjectIds };
|
|
243
288
|
});
|
|
244
289
|
});
|
|
245
290
|
await runtime.credentials.delete(id);
|
|
@@ -333,15 +378,22 @@ async function dispatch(req, res, prefix, runtime) {
|
|
|
333
378
|
const permission = body.permission === 'exec' ? 'exec' : body.permission === 'terminal' ? 'terminal' : undefined;
|
|
334
379
|
if (permission === undefined)
|
|
335
380
|
throw httpError(400, 'permission must be exec or terminal');
|
|
336
|
-
const
|
|
337
|
-
const workingDirectories =
|
|
338
|
-
const
|
|
381
|
+
const previous = runtime.store.injection(sessionId);
|
|
382
|
+
const workingDirectories = parseWorkingDirectories(body.workingDirectories, profileIds);
|
|
383
|
+
const workingProjectIds = parseWorkingProjectIds(body.workingProjectIds, profileIds, runtime.store);
|
|
384
|
+
const injection = { sessionId, profileIds, permission, requireCommandApproval: body.requireCommandApproval !== false, workingDirectories, workingProjectIds, updatedAt: Date.now() };
|
|
339
385
|
await runtime.store.update(state => { state.injections = [...state.injections.filter(item => item.sessionId !== sessionId), injection]; });
|
|
386
|
+
const revoked = previous?.profileIds.filter(profileId => !profileIds.includes(profileId)) ?? [];
|
|
387
|
+
if (permission !== 'terminal')
|
|
388
|
+
await runtime.aiTerminals.closeOwner(sessionId);
|
|
389
|
+
else
|
|
390
|
+
await Promise.all(revoked.map(profileId => runtime.aiTerminals.closeProfile(sessionId, profileId)));
|
|
340
391
|
return sendJson(res, 200, injection);
|
|
341
392
|
}
|
|
342
393
|
if (sessionId !== undefined && method === 'DELETE' && segments.length === 2) {
|
|
343
394
|
requireMutationHeader(req);
|
|
344
395
|
await runtime.store.update(state => { state.injections = state.injections.filter(item => item.sessionId !== sessionId); });
|
|
396
|
+
await runtime.aiTerminals.closeOwner(sessionId);
|
|
345
397
|
return sendJson(res, 204, undefined);
|
|
346
398
|
}
|
|
347
399
|
}
|
|
@@ -569,6 +621,12 @@ function requiredProfile(store, id) {
|
|
|
569
621
|
throw httpError(404, 'SSH profile was not found');
|
|
570
622
|
return profile;
|
|
571
623
|
}
|
|
624
|
+
function requiredRemoteProject(store, id, profileId) {
|
|
625
|
+
const project = store.remoteProject(id);
|
|
626
|
+
if (project === undefined || project.profileId !== profileId)
|
|
627
|
+
throw httpError(404, 'remote project was not found');
|
|
628
|
+
return project;
|
|
629
|
+
}
|
|
572
630
|
function requiredForward(store, id) {
|
|
573
631
|
const rule = store.forward(id);
|
|
574
632
|
if (rule === undefined)
|
|
@@ -616,6 +674,35 @@ function parseProfileIds(value, store) {
|
|
|
616
674
|
throw httpError(400, 'profileIds must be an array');
|
|
617
675
|
return [...new Set(value.map(item => requireText(item, 'profileId', 100)))].map(id => requiredProfile(store, id).id);
|
|
618
676
|
}
|
|
677
|
+
function parseWorkingDirectories(value, profileIds) {
|
|
678
|
+
if (value === undefined)
|
|
679
|
+
return {};
|
|
680
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
681
|
+
throw httpError(400, 'workingDirectories must be an object');
|
|
682
|
+
const result = {};
|
|
683
|
+
for (const [profileId, rawPath] of Object.entries(value)) {
|
|
684
|
+
if (!profileIds.includes(profileId))
|
|
685
|
+
continue;
|
|
686
|
+
const path = requireRawText(rawPath, 'working directory', 4096).trim();
|
|
687
|
+
if (path.length > 0)
|
|
688
|
+
result[profileId] = path;
|
|
689
|
+
}
|
|
690
|
+
return result;
|
|
691
|
+
}
|
|
692
|
+
function parseWorkingProjectIds(value, profileIds, store) {
|
|
693
|
+
if (value === undefined)
|
|
694
|
+
return {};
|
|
695
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
696
|
+
throw httpError(400, 'workingProjectIds must be an object');
|
|
697
|
+
const result = {};
|
|
698
|
+
for (const [profileId, rawProjectId] of Object.entries(value)) {
|
|
699
|
+
if (!profileIds.includes(profileId))
|
|
700
|
+
continue;
|
|
701
|
+
const projectId = requireText(rawProjectId, 'working project id', 100);
|
|
702
|
+
result[profileId] = requiredRemoteProject(store, projectId, profileId).id;
|
|
703
|
+
}
|
|
704
|
+
return result;
|
|
705
|
+
}
|
|
619
706
|
function createId(prefix) { return `${prefix}-${randomBytes(10).toString('hex')}`; }
|
|
620
707
|
function assertSameOrigin(req) {
|
|
621
708
|
const origin = req.headers.origin;
|