dsh-single-terminal 0.1.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.
Files changed (72) hide show
  1. package/README.md +144 -0
  2. package/README.zh.md +129 -0
  3. package/cordis.patch.yml +11 -0
  4. package/lib/client.js +11438 -0
  5. package/lib/client.js.map +1 -0
  6. package/lib/index.js +588 -0
  7. package/lib/types/client/controller.d.ts +86 -0
  8. package/lib/types/client/controller.d.ts.map +1 -0
  9. package/lib/types/client/controller.js +235 -0
  10. package/lib/types/client/drawer.d.ts +10 -0
  11. package/lib/types/client/drawer.d.ts.map +1 -0
  12. package/lib/types/client/drawer.js +84 -0
  13. package/lib/types/client/i18n.d.ts +8 -0
  14. package/lib/types/client/i18n.d.ts.map +1 -0
  15. package/lib/types/client/i18n.js +51 -0
  16. package/lib/types/client/index.d.ts +13 -0
  17. package/lib/types/client/index.d.ts.map +1 -0
  18. package/lib/types/client/index.js +13 -0
  19. package/lib/types/client/plugin.d.ts +23 -0
  20. package/lib/types/client/plugin.d.ts.map +1 -0
  21. package/lib/types/client/plugin.js +60 -0
  22. package/lib/types/client/protocol.d.ts +81 -0
  23. package/lib/types/client/protocol.d.ts.map +1 -0
  24. package/lib/types/client/protocol.js +5 -0
  25. package/lib/types/client/storage.d.ts +6 -0
  26. package/lib/types/client/storage.d.ts.map +1 -0
  27. package/lib/types/client/storage.js +21 -0
  28. package/lib/types/client/styles.d.ts +5 -0
  29. package/lib/types/client/styles.d.ts.map +1 -0
  30. package/lib/types/client/styles.js +103 -0
  31. package/lib/types/client/term.d.ts +15 -0
  32. package/lib/types/client/term.d.ts.map +1 -0
  33. package/lib/types/client/term.js +86 -0
  34. package/lib/types/client/toggle.d.ts +9 -0
  35. package/lib/types/client/toggle.d.ts.map +1 -0
  36. package/lib/types/client/toggle.js +19 -0
  37. package/lib/types/client/ws.d.ts +25 -0
  38. package/lib/types/client/ws.d.ts.map +1 -0
  39. package/lib/types/client/ws.js +79 -0
  40. package/lib/types/client/xterm-css.d.ts +7 -0
  41. package/lib/types/client/xterm-css.d.ts.map +1 -0
  42. package/lib/types/client/xterm-css.js +224 -0
  43. package/lib/types/host/hub.d.ts +39 -0
  44. package/lib/types/host/hub.d.ts.map +1 -0
  45. package/lib/types/host/hub.js +291 -0
  46. package/lib/types/host/index.d.ts +17 -0
  47. package/lib/types/host/index.d.ts.map +1 -0
  48. package/lib/types/host/index.js +66 -0
  49. package/lib/types/host/shells.d.ts +30 -0
  50. package/lib/types/host/shells.d.ts.map +1 -0
  51. package/lib/types/host/shells.js +202 -0
  52. package/lib/types/host/types.d.ts +109 -0
  53. package/lib/types/host/types.d.ts.map +1 -0
  54. package/lib/types/host/types.js +4 -0
  55. package/package.json +102 -0
  56. package/src/client/controller.ts +293 -0
  57. package/src/client/drawer.tsx +182 -0
  58. package/src/client/i18n.ts +58 -0
  59. package/src/client/index.ts +17 -0
  60. package/src/client/plugin.tsx +92 -0
  61. package/src/client/protocol.ts +40 -0
  62. package/src/client/storage.ts +21 -0
  63. package/src/client/styles.ts +106 -0
  64. package/src/client/term.tsx +95 -0
  65. package/src/client/toggle.tsx +43 -0
  66. package/src/client/ws.ts +81 -0
  67. package/src/client/xterm-css.ts +224 -0
  68. package/src/host/cordis-augment.d.ts +22 -0
  69. package/src/host/hub.ts +302 -0
  70. package/src/host/index.ts +78 -0
  71. package/src/host/shells.ts +216 -0
  72. package/src/host/types.ts +79 -0
@@ -0,0 +1,216 @@
1
+ /**
2
+ * dsh-single-terminal —— shell 注册表与探测。
3
+ *
4
+ * 探测范式对齐宿主 packages/shell/pwsh-local/src/resolve.ts:
5
+ * 已知路径 + PATH 逐项探测 + lstatSync(isFile||isSymbolicLink),
6
+ * 不用注册表 / where.exe。检测不到的 shell available=false(客户端隐藏)。
7
+ */
8
+
9
+ import { lstatSync } from 'node:fs'
10
+ import { delimiter, join } from 'node:path'
11
+ import type { CustomShellConfig } from './types.ts'
12
+
13
+ export interface ResolvedShell {
14
+ id: string
15
+ name: string
16
+ file: string
17
+ args: string[]
18
+ }
19
+
20
+ function isFileLike(path: string): boolean {
21
+ try {
22
+ const stat = lstatSync(path)
23
+ return stat.isFile() || stat.isSymbolicLink()
24
+ } catch {
25
+ return false
26
+ }
27
+ }
28
+
29
+ function firstExisting(paths: readonly string[]): string | null {
30
+ for (const path of paths) {
31
+ if (path.length > 0 && isFileLike(path)) return path
32
+ }
33
+ return null
34
+ }
35
+
36
+ function pathEntries(env: NodeJS.ProcessEnv): string[] {
37
+ return (env.PATH ?? '')
38
+ .split(delimiter)
39
+ .map((entry) => entry.trim().replace(/^"|"$/g, ''))
40
+ .filter((entry) => entry.length > 0)
41
+ }
42
+
43
+ /** PATH 逐项探测一个裸可执行名(Windows 附加 PATHEXT 扩展名)。 */
44
+ function probeOnPath(name: string, env: NodeJS.ProcessEnv, platform: NodeJS.Platform): string | null {
45
+ const extensions = platform === 'win32'
46
+ ? (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter((ext) => ext.length > 0)
47
+ : ['']
48
+ const candidates: string[] = []
49
+ for (const entry of pathEntries(env)) {
50
+ for (const ext of extensions) candidates.push(join(entry, name + ext))
51
+ }
52
+ return firstExisting(candidates)
53
+ }
54
+
55
+ interface ShellDef {
56
+ id: string
57
+ name: string
58
+ platforms: readonly NodeJS.Platform[]
59
+ resolve(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): { file: string; args: string[] } | null
60
+ }
61
+
62
+ const WINDOWS_BUILTINS: ShellDef[] = [
63
+ {
64
+ id: 'powershell',
65
+ name: 'PowerShell',
66
+ platforms: ['win32'],
67
+ resolve: (env) => {
68
+ const systemRoot = env.SystemRoot ?? 'C:\\Windows'
69
+ const file = firstExisting([join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')])
70
+ return file === null ? null : { file, args: [] }
71
+ },
72
+ },
73
+ {
74
+ id: 'pwsh',
75
+ name: 'PowerShell 7',
76
+ platforms: ['win32', 'darwin', 'linux'],
77
+ resolve: (env, platform) => {
78
+ const candidates: string[] = []
79
+ if (platform === 'win32') {
80
+ const programFiles = env.ProgramFiles ?? 'C:\\Program Files'
81
+ candidates.push(join(programFiles, 'PowerShell', '7', 'pwsh.exe'))
82
+ for (const entry of pathEntries(env)) candidates.push(join(entry, 'pwsh.exe'))
83
+ } else {
84
+ candidates.push(...pathEntries(env).map((entry) => join(entry, 'pwsh')))
85
+ }
86
+ const file = firstExisting(candidates)
87
+ return file === null ? null : { file, args: [] }
88
+ },
89
+ },
90
+ {
91
+ id: 'cmd',
92
+ name: 'CMD',
93
+ platforms: ['win32'],
94
+ resolve: (env) => {
95
+ const systemRoot = env.SystemRoot ?? 'C:\\Windows'
96
+ const file = firstExisting([join(systemRoot, 'System32', 'cmd.exe')])
97
+ return file === null ? null : { file, args: [] }
98
+ },
99
+ },
100
+ {
101
+ id: 'gitbash',
102
+ name: 'Git Bash',
103
+ platforms: ['win32'],
104
+ resolve: (env) => {
105
+ const programFiles = env.ProgramFiles ?? 'C:\\Program Files'
106
+ const programFilesX86 = env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)'
107
+ const localAppData = env.LocalAppData ?? ''
108
+ const file = firstExisting([
109
+ join(programFiles, 'Git', 'bin', 'bash.exe'),
110
+ join(programFiles, 'Git', 'usr', 'bin', 'bash.exe'),
111
+ join(programFilesX86, 'Git', 'bin', 'bash.exe'),
112
+ localAppData.length > 0 ? join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe') : '',
113
+ ])
114
+ return file === null ? null : { file, args: ['-i'] }
115
+ },
116
+ },
117
+ {
118
+ id: 'wsl',
119
+ name: 'WSL',
120
+ platforms: ['win32'],
121
+ resolve: (env) => {
122
+ const systemRoot = env.SystemRoot ?? 'C:\\Windows'
123
+ const file = firstExisting([join(systemRoot, 'System32', 'wsl.exe')])
124
+ return file === null ? null : { file, args: [] }
125
+ },
126
+ },
127
+ ]
128
+
129
+ const POSIX_BUILTINS: ShellDef[] = [
130
+ {
131
+ id: 'bash',
132
+ name: 'Bash',
133
+ platforms: ['darwin', 'linux'],
134
+ resolve: (env) => {
135
+ const file = firstExisting([...pathEntries(env).map((entry) => join(entry, 'bash')), '/bin/bash', '/usr/bin/bash'])
136
+ return file === null ? null : { file, args: [] }
137
+ },
138
+ },
139
+ {
140
+ id: 'zsh',
141
+ name: 'Zsh',
142
+ platforms: ['darwin', 'linux'],
143
+ resolve: (env) => {
144
+ const file = firstExisting(['/bin/zsh', '/usr/bin/zsh', ...pathEntries(env).map((entry) => join(entry, 'zsh'))])
145
+ return file === null ? null : { file, args: [] }
146
+ },
147
+ },
148
+ {
149
+ id: 'fish',
150
+ name: 'Fish',
151
+ platforms: ['darwin', 'linux'],
152
+ resolve: (env) => {
153
+ const file = firstExisting([
154
+ '/usr/local/bin/fish',
155
+ '/opt/homebrew/bin/fish',
156
+ ...pathEntries(env).map((entry) => join(entry, 'fish')),
157
+ ])
158
+ return file === null ? null : { file, args: [] }
159
+ },
160
+ },
161
+ ]
162
+
163
+ const BUILTINS: readonly ShellDef[] = [...WINDOWS_BUILTINS, ...POSIX_BUILTINS]
164
+
165
+ export class ShellRegistry {
166
+ constructor(private readonly customShells: readonly CustomShellConfig[]) {}
167
+
168
+ /** 枚举当前平台可配置的 shell(检测不到的 available=false,由客户端隐藏)。 */
169
+ list(env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): Array<{ id: string; name: string; available: boolean }> {
170
+ const result: Array<{ id: string; name: string; available: boolean }> = []
171
+ for (const def of BUILTINS) {
172
+ if (!def.platforms.includes(platform)) continue
173
+ result.push({ id: def.id, name: def.name, available: def.resolve(env, platform) !== null })
174
+ }
175
+ for (const custom of this.customShells) {
176
+ if (platform !== 'win32' && !/[\\/]/.test(custom.command) && custom.command.toLowerCase().endsWith('.exe')) continue
177
+ result.push({ id: custom.id, name: custom.name, available: this.resolveCustom(custom, env, platform) !== null })
178
+ }
179
+ return result
180
+ }
181
+
182
+ /** 解析为可执行规格;内置 id 优先,其次自定义 shell。 */
183
+ resolve(id: string, env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): ResolvedShell | null {
184
+ const builtin = BUILTINS.find((def) => def.id === id && def.platforms.includes(platform))
185
+ if (builtin !== undefined) {
186
+ const resolved = builtin.resolve(env, platform)
187
+ return resolved === null ? null : { id: builtin.id, name: builtin.name, ...resolved }
188
+ }
189
+ const custom = this.customShells.find((entry) => entry.id === id)
190
+ if (custom !== undefined) {
191
+ const resolved = this.resolveCustom(custom, env, platform)
192
+ return resolved === null ? null : { id: custom.id, name: custom.name, ...resolved }
193
+ }
194
+ return null
195
+ }
196
+
197
+ /** 默认 shell id:配置值可用则用之;否则 win32 用 powershell,POSIX 用 $SHELL 名或 bash。 */
198
+ defaultShellId(preferred: string | undefined, env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): string {
199
+ if (preferred !== undefined && preferred.length > 0 && this.resolve(preferred, env, platform) !== null) return preferred
200
+ if (platform === 'win32') return 'powershell'
201
+ const shellName = (env.SHELL ?? '').split('/').pop() ?? ''
202
+ if (shellName.length > 0 && this.resolve(shellName, env, platform) !== null) return shellName
203
+ return 'bash'
204
+ }
205
+
206
+ private resolveCustom(custom: CustomShellConfig, env: NodeJS.ProcessEnv, platform: NodeJS.Platform): { file: string; args: string[] } | null {
207
+ const raw = custom.command.trim()
208
+ if (raw.length === 0) return null
209
+ const args = Array.isArray(custom.args) ? [...custom.args] : []
210
+ if (/[\\/]/.test(raw) || isFileLike(raw)) {
211
+ return isFileLike(raw) ? { file: raw, args } : null
212
+ }
213
+ const file = probeOnPath(raw, env, platform)
214
+ return file === null ? null : { file, args }
215
+ }
216
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * dsh-single-terminal —— 共享类型:插件配置、WS 帧协议、宿主服务最小视图。
3
+ */
4
+
5
+ /* ── 插件配置(cordis.yml config / Plugins 设置页)────────────────── */
6
+
7
+ export interface CustomShellConfig {
8
+ id: string
9
+ name: string
10
+ command: string
11
+ args: string[]
12
+ }
13
+
14
+ export interface TerminalPluginConfig {
15
+ defaultShell: string
16
+ defaultCwd: string
17
+ scrollbackLimit: number
18
+ fontSize: number
19
+ fontFamily: string
20
+ customShells: CustomShellConfig[]
21
+ }
22
+
23
+ /* ── WS 帧协议(JSON 文本帧,/api/dsh-single-terminal.ws)─────────── */
24
+
25
+ export interface ShellInfo {
26
+ id: string
27
+ name: string
28
+ available: boolean
29
+ }
30
+
31
+ export interface SessionInfo {
32
+ id: string
33
+ shellId: string
34
+ label: string
35
+ cwd: string
36
+ alive: boolean
37
+ pid: number
38
+ exitCode: number | null
39
+ signal: number | null
40
+ }
41
+
42
+ /** client → host */
43
+ export type ClientFrame =
44
+ | { type: 'ping' }
45
+ | { type: 'list' }
46
+ | { type: 'open'; shellId: string; cols: number; rows: number; cwd?: string }
47
+ | { type: 'input'; id: string; data: string }
48
+ | { type: 'resize'; id: string; cols: number; rows: number }
49
+ | { type: 'attach'; id: string }
50
+ | { type: 'close'; id: string }
51
+
52
+ /** host → client */
53
+ export type HostFrame =
54
+ | { type: 'pong' }
55
+ | { type: 'hello'; platform: string; defaultShell: string; fontSize: number; fontFamily: string }
56
+ | { type: 'shells'; defaultShell: string; shells: ShellInfo[]; sessions: SessionInfo[] }
57
+ | { type: 'opened'; session: SessionInfo }
58
+ | { type: 'data'; id: string; data: string }
59
+ | { type: 'replay'; id: string; data: string }
60
+ | { type: 'exit'; id: string; exitCode: number | null; signal: number | null; closed?: boolean }
61
+ | { type: 'error'; message: string; id?: string }
62
+
63
+ /* ── 宿主服务最小视图(避免对 @deepseek-ai/* 的值依赖)────────────── */
64
+
65
+ import type { IncomingMessage } from 'node:http'
66
+ import type { Duplex } from 'node:stream'
67
+
68
+ /** 宿主 connection 服务最小视图(@deepseek-ai/dsh-client-connection)。 */
69
+ export interface ConnectionService {
70
+ requestRejection(request: IncomingMessage): number | undefined
71
+ }
72
+
73
+ /** 宿主 webServer 服务最小视图(@deepseek-ai/dsh-host-webserver)。 */
74
+ export interface WebServerService {
75
+ registerUpgrade(route: {
76
+ path: string
77
+ handler: (req: IncomingMessage, socket: Duplex, head: Buffer) => void | Promise<void>
78
+ }): () => void
79
+ }