@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.
Files changed (52) hide show
  1. package/README.md +75 -0
  2. package/cordis.patch.yml +12 -0
  3. package/lib/client.js +15638 -0
  4. package/lib/index.js +3297 -0
  5. package/lib/invariant.js +6 -0
  6. package/lib/types/client/api.d.ts +72 -0
  7. package/lib/types/client/index.d.ts +37 -0
  8. package/lib/types/client/locales.d.ts +137 -0
  9. package/lib/types/client/mount.d.ts +12 -0
  10. package/lib/types/client/panel/ClusterTab.d.ts +7 -0
  11. package/lib/types/client/panel/HostFormDialog.d.ts +12 -0
  12. package/lib/types/client/panel/HostsTab.d.ts +9 -0
  13. package/lib/types/client/panel/SshPanel.d.ts +13 -0
  14. package/lib/types/client/panel/TerminalTab.d.ts +11 -0
  15. package/lib/types/client/panel/TransferTab.d.ts +7 -0
  16. package/lib/types/client/panel/TunnelsTab.d.ts +7 -0
  17. package/lib/types/client/panel/controller.d.ts +22 -0
  18. package/lib/types/client/panel/helpers.d.ts +14 -0
  19. package/lib/types/client/panel/xterm.css.d.ts +2 -0
  20. package/lib/types/client/sidebar-entry.d.ts +24 -0
  21. package/lib/types/engine.d.ts +135 -0
  22. package/lib/types/index.d.ts +40 -0
  23. package/lib/types/invariant.d.ts +3 -0
  24. package/lib/types/protocol.d.ts +189 -0
  25. package/lib/types/routes.d.ts +29 -0
  26. package/lib/types/store.d.ts +54 -0
  27. package/lib/types/tools.d.ts +18 -0
  28. package/package.json +106 -0
  29. package/src/client/api.ts +363 -0
  30. package/src/client/css-modules.d.ts +5 -0
  31. package/src/client/index.ts +68 -0
  32. package/src/client/locales.ts +284 -0
  33. package/src/client/mount.tsx +83 -0
  34. package/src/client/panel/ClusterTab.tsx +123 -0
  35. package/src/client/panel/HostFormDialog.tsx +221 -0
  36. package/src/client/panel/HostsTab.tsx +166 -0
  37. package/src/client/panel/SshPanel.tsx +77 -0
  38. package/src/client/panel/TerminalTab.tsx +176 -0
  39. package/src/client/panel/TransferTab.tsx +232 -0
  40. package/src/client/panel/TunnelsTab.tsx +177 -0
  41. package/src/client/panel/controller.ts +48 -0
  42. package/src/client/panel/helpers.ts +26 -0
  43. package/src/client/panel/panel.module.css +833 -0
  44. package/src/client/panel/xterm.css.ts +2 -0
  45. package/src/client/sidebar-entry.ts +123 -0
  46. package/src/engine.ts +780 -0
  47. package/src/index.ts +155 -0
  48. package/src/invariant.ts +4 -0
  49. package/src/protocol.ts +188 -0
  50. package/src/routes.ts +540 -0
  51. package/src/store.ts +344 -0
  52. 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
+ }
@@ -0,0 +1,4 @@
1
+ /** Invariant companion plugin (no assertions — nothing to check at runtime). */
2
+
3
+ /** Provides no assertions: the ssh plugin owns no cross-package runtime invariants. */
4
+ export function apply(): void {}
@@ -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 }