@linxin666/dsh-ssh 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +15638 -0
- package/lib/index.js +3297 -0
- package/lib/invariant.js +6 -0
- package/lib/types/client/api.d.ts +72 -0
- package/lib/types/client/index.d.ts +37 -0
- package/lib/types/client/locales.d.ts +137 -0
- package/lib/types/client/mount.d.ts +12 -0
- package/lib/types/client/panel/ClusterTab.d.ts +7 -0
- package/lib/types/client/panel/HostFormDialog.d.ts +12 -0
- package/lib/types/client/panel/HostsTab.d.ts +9 -0
- package/lib/types/client/panel/SshPanel.d.ts +13 -0
- package/lib/types/client/panel/TerminalTab.d.ts +11 -0
- package/lib/types/client/panel/TransferTab.d.ts +7 -0
- package/lib/types/client/panel/TunnelsTab.d.ts +7 -0
- package/lib/types/client/panel/controller.d.ts +22 -0
- package/lib/types/client/panel/helpers.d.ts +14 -0
- package/lib/types/client/panel/xterm.css.d.ts +2 -0
- package/lib/types/client/sidebar-entry.d.ts +24 -0
- package/lib/types/engine.d.ts +135 -0
- package/lib/types/index.d.ts +40 -0
- package/lib/types/invariant.d.ts +3 -0
- package/lib/types/protocol.d.ts +189 -0
- package/lib/types/routes.d.ts +29 -0
- package/lib/types/store.d.ts +54 -0
- package/lib/types/tools.d.ts +18 -0
- package/package.json +106 -0
- package/src/client/api.ts +363 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/index.ts +68 -0
- package/src/client/locales.ts +284 -0
- package/src/client/mount.tsx +83 -0
- package/src/client/panel/ClusterTab.tsx +123 -0
- package/src/client/panel/HostFormDialog.tsx +221 -0
- package/src/client/panel/HostsTab.tsx +166 -0
- package/src/client/panel/SshPanel.tsx +77 -0
- package/src/client/panel/TerminalTab.tsx +176 -0
- package/src/client/panel/TransferTab.tsx +232 -0
- package/src/client/panel/TunnelsTab.tsx +177 -0
- package/src/client/panel/controller.ts +48 -0
- package/src/client/panel/helpers.ts +26 -0
- package/src/client/panel/panel.module.css +833 -0
- package/src/client/panel/xterm.css.ts +2 -0
- package/src/client/sidebar-entry.ts +123 -0
- package/src/engine.ts +780 -0
- package/src/index.ts +155 -0
- package/src/invariant.ts +4 -0
- package/src/protocol.ts +188 -0
- package/src/routes.ts +540 -0
- package/src/store.ts +344 -0
- package/src/tools.ts +371 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-ssh — host half. Mounts the SSH engine (persistent ssh2 connection
|
|
3
|
+
* pool, exec / PTY shell / SFTP / tunnels / cluster), the /api/dsh-ssh route
|
|
4
|
+
* family plus the terminal WebSocket upgrade, the agent tools (ssh_list,
|
|
5
|
+
* ssh_exec, ssh_upload, ssh_download, ssh_tunnel, ssh_cluster), and a
|
|
6
|
+
* system-prompt announcement. The browser half (./client) renders the host
|
|
7
|
+
* manager and web terminal. Everything rides official NPM SDK packages —
|
|
8
|
+
* no dsh source changes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
12
|
+
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
13
|
+
import z from 'schemastery'
|
|
14
|
+
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
15
|
+
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
16
|
+
import type {} from '@deepseek-ai/dsh-tools'
|
|
17
|
+
import { SshEngine } from './engine.ts'
|
|
18
|
+
import { makeRoutes } from './routes.ts'
|
|
19
|
+
import { HostStore } from './store.ts'
|
|
20
|
+
import { sshClusterTool, sshDownloadTool, sshExecTool, sshListTool, sshTunnelTool, sshUploadTool } from './tools.ts'
|
|
21
|
+
|
|
22
|
+
/** Stable cordis plugin name. */
|
|
23
|
+
export const name = 'ssh'
|
|
24
|
+
|
|
25
|
+
/** Services required before the SSH surfaces can mount. */
|
|
26
|
+
export const inject = ['webServer', 'tools', 'systemPrompt']
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Settings namespace of the SSH capability — the section the web settings
|
|
30
|
+
* surface edits. Spelled here rather than imported: the browser half spells
|
|
31
|
+
* the same value and must not depend on a Host package.
|
|
32
|
+
*/
|
|
33
|
+
export const SSH_SETTINGS_NAMESPACE = settingsNamespace('dsh-ssh')
|
|
34
|
+
|
|
35
|
+
/** Plugin config, validated by the same-named schemastery schema. */
|
|
36
|
+
export interface Config {
|
|
37
|
+
/**
|
|
38
|
+
* When true (default), a system-prompt section announces the SSH plugin to
|
|
39
|
+
* every agent (tools + host store). Set false to keep it silent.
|
|
40
|
+
*/
|
|
41
|
+
announceToAgent?: boolean
|
|
42
|
+
/** Master switch for the plugin (routes, tools, prompt section). */
|
|
43
|
+
enabled?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Config: z<Config> = z.object({
|
|
47
|
+
announceToAgent: z.boolean().default(true),
|
|
48
|
+
enabled: z.boolean().default(true),
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
/** Schema default, re-read for hand-built test contexts (the loader applies them normally). */
|
|
52
|
+
const DEFAULT_ANNOUNCE = true
|
|
53
|
+
|
|
54
|
+
/** Order of the announcement section within the tool-guidance band. */
|
|
55
|
+
const SECTION_ORDER = 150
|
|
56
|
+
|
|
57
|
+
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
58
|
+
export const SSH_GUIDANCE = '本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存 ~/.dsh/dsh-ssh.json(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster 集群并发执行;支持密钥/密码认证、passphrase 密钥与 ProxyJump 跳板机;Web 终端走 WebSocket。限制:主机操作由用户在 GUI 中配置后 agent 方可使用;密码以明文存在用户主目录私有文件(权限 0600);命令输出原样返回、可能含敏感信息;断线重连可能重放非幂等命令;传输/执行消耗真实远程资源,先确认再操作。用户提到「SSH / 远程服务器 / 服务器操作 / 跳板机 / 隧道 / 部署 / 上传下载」时即指本插件,请据此协作。'
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Mount the SSH engine, routes, tools, and announcement.
|
|
62
|
+
* @param ctx - host plugin context carrying webServer/tools/systemPrompt.
|
|
63
|
+
* @param config - resolved plugin config (schema defaults applied by the loader).
|
|
64
|
+
*/
|
|
65
|
+
export function apply(ctx: Context, config?: Config): void {
|
|
66
|
+
// The live source the surfaces read: the settings section once the web
|
|
67
|
+
// settings surface is served, the composition entry otherwise.
|
|
68
|
+
let current: () => Config = () => config ?? {}
|
|
69
|
+
const resolve = (): Config => {
|
|
70
|
+
const value = current()
|
|
71
|
+
return {
|
|
72
|
+
announceToAgent: value.announceToAgent ?? DEFAULT_ANNOUNCE,
|
|
73
|
+
enabled: value.enabled ?? true,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const store = new HostStore()
|
|
78
|
+
const engine = new SshEngine(store)
|
|
79
|
+
ctx.effect(() => () => { engine.dispose() }, 'dsh-ssh: engine')
|
|
80
|
+
|
|
81
|
+
// The /api/dsh-ssh route family + terminal upgrade.
|
|
82
|
+
const { routes, upgrade } = makeRoutes({ store, engine })
|
|
83
|
+
let disposeRoutes: (() => void) | undefined
|
|
84
|
+
|
|
85
|
+
// Agent tools + their prompt sections.
|
|
86
|
+
const tools = [
|
|
87
|
+
sshListTool(engine),
|
|
88
|
+
sshExecTool(engine),
|
|
89
|
+
sshUploadTool(engine),
|
|
90
|
+
sshDownloadTool(engine),
|
|
91
|
+
sshTunnelTool(engine),
|
|
92
|
+
sshClusterTool(engine),
|
|
93
|
+
]
|
|
94
|
+
let disposeTools: (() => void) | undefined
|
|
95
|
+
|
|
96
|
+
// System-prompt announcement.
|
|
97
|
+
let disposeSection: (() => void) | undefined
|
|
98
|
+
|
|
99
|
+
// Register (or drop) every surface to match the current source. Each group
|
|
100
|
+
// is kept under one disposer: re-registering first tears the old one down
|
|
101
|
+
// so duplicate-name registrations never throw.
|
|
102
|
+
const sync = (): void => {
|
|
103
|
+
const value = resolve()
|
|
104
|
+
if (disposeSection !== undefined) {
|
|
105
|
+
disposeSection()
|
|
106
|
+
disposeSection = undefined
|
|
107
|
+
}
|
|
108
|
+
if (disposeRoutes !== undefined) {
|
|
109
|
+
disposeRoutes()
|
|
110
|
+
disposeRoutes = undefined
|
|
111
|
+
}
|
|
112
|
+
if (disposeTools !== undefined) {
|
|
113
|
+
disposeTools()
|
|
114
|
+
disposeTools = undefined
|
|
115
|
+
}
|
|
116
|
+
if (!value.enabled) return
|
|
117
|
+
if (value.announceToAgent) {
|
|
118
|
+
disposeSection = ctx.systemPrompt.section({
|
|
119
|
+
name: 'plugin:dsh-ssh',
|
|
120
|
+
order: SECTION_ORDER,
|
|
121
|
+
text: SSH_GUIDANCE,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
disposeRoutes = ctx.effect(
|
|
125
|
+
() => {
|
|
126
|
+
const disposers = routes.map(route => ctx.webServer.register(route))
|
|
127
|
+
const upgradeDisposer = ctx.webServer.registerUpgrade(upgrade)
|
|
128
|
+
return () => {
|
|
129
|
+
for (const dispose of disposers) dispose()
|
|
130
|
+
upgradeDisposer()
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
'dsh-ssh: routes',
|
|
134
|
+
)
|
|
135
|
+
disposeTools = ctx.effect(
|
|
136
|
+
() => {
|
|
137
|
+
const disposers = tools.map(tool => ctx.tools.register(tool))
|
|
138
|
+
return () => { for (const dispose of disposers) dispose() }
|
|
139
|
+
},
|
|
140
|
+
'dsh-ssh: tools',
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
installSettingsSection(ctx, SSH_SETTINGS_NAMESPACE, Config, config ?? {}, {
|
|
145
|
+
setSource: (source) => {
|
|
146
|
+
current = source
|
|
147
|
+
sync()
|
|
148
|
+
},
|
|
149
|
+
onChange: sync,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// Initial registration from the composition entry (covers deployments with
|
|
153
|
+
// no settings service, whose installSettingsSection never fires its hooks).
|
|
154
|
+
sync()
|
|
155
|
+
}
|
package/src/invariant.ts
ADDED
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire contract between the host half (routes.ts) and the browser half
|
|
3
|
+
* (client/api.ts). Pure types only — imported by both halves, bundled into
|
|
4
|
+
* each, no runtime identity to share.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Authentication flavors a host entry may carry. */
|
|
8
|
+
export type SshAuthKind = 'key' | 'password'
|
|
9
|
+
|
|
10
|
+
/** One stored host entry (the ~/.dsh/dsh-ssh.json store shape). */
|
|
11
|
+
export interface SshHostEntry {
|
|
12
|
+
/** Stable, user-chosen identifier used by every operation. */
|
|
13
|
+
alias: string
|
|
14
|
+
/** Hostname or IP of the target. */
|
|
15
|
+
host: string
|
|
16
|
+
/** SSH port (default 22). */
|
|
17
|
+
port: number
|
|
18
|
+
/** Login user. */
|
|
19
|
+
user: string
|
|
20
|
+
/** Authentication. */
|
|
21
|
+
auth: {
|
|
22
|
+
kind: SshAuthKind
|
|
23
|
+
/** Absolute path to the private key for 'key' auth. */
|
|
24
|
+
keyPath?: string
|
|
25
|
+
/** Passphrase for an encrypted key. */
|
|
26
|
+
passphrase?: string
|
|
27
|
+
/** Password for 'password' auth. */
|
|
28
|
+
password?: string
|
|
29
|
+
}
|
|
30
|
+
/** Jump chain: local aliases connected through in order (ProxyJump). */
|
|
31
|
+
proxyJump: string[]
|
|
32
|
+
/** Free-form note. */
|
|
33
|
+
description?: string
|
|
34
|
+
/** Deployment environment label (development / production / ...). */
|
|
35
|
+
environment?: string
|
|
36
|
+
/** Free-form tags. */
|
|
37
|
+
tags: string[]
|
|
38
|
+
/** Physical location note. */
|
|
39
|
+
location?: string
|
|
40
|
+
createdAt: number
|
|
41
|
+
updatedAt: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Public (secret-free) projection of an entry, safe for the browser/agent. */
|
|
45
|
+
export interface SshHostSummary {
|
|
46
|
+
alias: string
|
|
47
|
+
host: string
|
|
48
|
+
port: number
|
|
49
|
+
user: string
|
|
50
|
+
auth: SshAuthKind
|
|
51
|
+
/** Whether the key path exists on the host machine (key auth only). */
|
|
52
|
+
keyReady: boolean
|
|
53
|
+
proxyJump: string[]
|
|
54
|
+
description?: string
|
|
55
|
+
environment?: string
|
|
56
|
+
tags: string[]
|
|
57
|
+
location?: string
|
|
58
|
+
createdAt: number
|
|
59
|
+
updatedAt: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Result of one non-interactive command execution. */
|
|
63
|
+
export interface ExecResult {
|
|
64
|
+
success: boolean
|
|
65
|
+
/** Remote exit code, or null when the channel died without one. */
|
|
66
|
+
exitCode: number | null
|
|
67
|
+
timedOut: boolean
|
|
68
|
+
stdout: string
|
|
69
|
+
stderr: string
|
|
70
|
+
/** Wall-clock duration of the round trip in ms. */
|
|
71
|
+
durationMs: number
|
|
72
|
+
/** Connection error message when the command never ran. */
|
|
73
|
+
error?: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One server entry in a cluster run. */
|
|
77
|
+
export interface ClusterResult {
|
|
78
|
+
alias: string
|
|
79
|
+
ok: boolean
|
|
80
|
+
exitCode?: number | null
|
|
81
|
+
timedOut?: boolean
|
|
82
|
+
stdout?: string
|
|
83
|
+
stderr?: string
|
|
84
|
+
durationMs?: number
|
|
85
|
+
error?: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** SFTP transfer progress frame (upload stream). */
|
|
89
|
+
export interface TransferProgress {
|
|
90
|
+
phase: 'connecting' | 'transferring' | 'done' | 'error'
|
|
91
|
+
file: string
|
|
92
|
+
transferred: number
|
|
93
|
+
total: number
|
|
94
|
+
percent: number
|
|
95
|
+
speedBps?: number
|
|
96
|
+
error?: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** One active local port-forward tunnel. */
|
|
100
|
+
export interface TunnelInfo {
|
|
101
|
+
id: string
|
|
102
|
+
alias: string
|
|
103
|
+
localPort: number
|
|
104
|
+
remoteHost: string
|
|
105
|
+
remotePort: number
|
|
106
|
+
state: 'forwarding' | 'connecting' | 'failed'
|
|
107
|
+
error?: string
|
|
108
|
+
startedAt: number
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** One directory listing entry (remote file browser). */
|
|
112
|
+
export interface RemoteDirEntry {
|
|
113
|
+
name: string
|
|
114
|
+
type: 'dir' | 'file' | 'other'
|
|
115
|
+
size: number
|
|
116
|
+
mtimeMs: number
|
|
117
|
+
mode?: number
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Test-connection outcome. */
|
|
121
|
+
export interface TestResult {
|
|
122
|
+
ok: boolean
|
|
123
|
+
latencyMs?: number
|
|
124
|
+
error?: string
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Host edit payload (create/update); 'alias' comes from the URL for updates. */
|
|
128
|
+
export interface HostPayload {
|
|
129
|
+
alias?: string
|
|
130
|
+
host: string
|
|
131
|
+
port?: number
|
|
132
|
+
user: string
|
|
133
|
+
/**
|
|
134
|
+
* Authentication. Required on create; on update an omitted auth keeps the
|
|
135
|
+
* stored secrets (the browser never receives them back).
|
|
136
|
+
*/
|
|
137
|
+
auth?: SshHostEntry['auth']
|
|
138
|
+
proxyJump?: string[]
|
|
139
|
+
description?: string
|
|
140
|
+
environment?: string
|
|
141
|
+
tags?: string[]
|
|
142
|
+
location?: string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Import outcome from ~/.ssh/config. */
|
|
146
|
+
export interface ImportResult {
|
|
147
|
+
parsed: number
|
|
148
|
+
added: number
|
|
149
|
+
skipped: number
|
|
150
|
+
/** Aliases that failed to map (wildcard patterns, missing HostName, ...). */
|
|
151
|
+
skippedNames: string[]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** JSON error body used by every route. */
|
|
155
|
+
export interface ApiErrorBody {
|
|
156
|
+
error: string
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** WebSocket terminal protocol frames (host -> client and client -> host). */
|
|
160
|
+
export type TerminalServerFrame =
|
|
161
|
+
| { type: 'ready'; alias: string }
|
|
162
|
+
| { type: 'output'; data: string }
|
|
163
|
+
| { type: 'exit'; code: number | null; error?: string }
|
|
164
|
+
|
|
165
|
+
export type TerminalClientFrame =
|
|
166
|
+
| { type: 'input'; data: string }
|
|
167
|
+
| { type: 'resize'; cols: number; rows: number }
|
|
168
|
+
|
|
169
|
+
/** Route paths the client calls (shared literals). */
|
|
170
|
+
export const SSH_API_BASE = '/api/dsh-ssh' as const
|
|
171
|
+
|
|
172
|
+
export const SSH_API = {
|
|
173
|
+
hosts: SSH_API_BASE + '/hosts',
|
|
174
|
+
importSshConfig: SSH_API_BASE + '/hosts/import-ssh-config',
|
|
175
|
+
test: SSH_API_BASE + '/test',
|
|
176
|
+
exec: SSH_API_BASE + '/exec',
|
|
177
|
+
cluster: SSH_API_BASE + '/cluster',
|
|
178
|
+
upload: SSH_API_BASE + '/upload',
|
|
179
|
+
download: SSH_API_BASE + '/download',
|
|
180
|
+
ls: SSH_API_BASE + '/ls',
|
|
181
|
+
tunnel: SSH_API_BASE + '/tunnel',
|
|
182
|
+
terminal: SSH_API_BASE + '/terminal',
|
|
183
|
+
} as const
|
|
184
|
+
|
|
185
|
+
/** NDJSON transfer stream line shapes (upload). */
|
|
186
|
+
export type TransferStreamLine =
|
|
187
|
+
| { type: 'progress'; progress: TransferProgress }
|
|
188
|
+
| { type: 'result'; ok: boolean; transferredBytes?: number; error?: string }
|