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.
- package/README.md +144 -0
- package/README.zh.md +129 -0
- package/cordis.patch.yml +11 -0
- package/lib/client.js +11438 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +588 -0
- package/lib/types/client/controller.d.ts +86 -0
- package/lib/types/client/controller.d.ts.map +1 -0
- package/lib/types/client/controller.js +235 -0
- package/lib/types/client/drawer.d.ts +10 -0
- package/lib/types/client/drawer.d.ts.map +1 -0
- package/lib/types/client/drawer.js +84 -0
- package/lib/types/client/i18n.d.ts +8 -0
- package/lib/types/client/i18n.d.ts.map +1 -0
- package/lib/types/client/i18n.js +51 -0
- package/lib/types/client/index.d.ts +13 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +13 -0
- package/lib/types/client/plugin.d.ts +23 -0
- package/lib/types/client/plugin.d.ts.map +1 -0
- package/lib/types/client/plugin.js +60 -0
- package/lib/types/client/protocol.d.ts +81 -0
- package/lib/types/client/protocol.d.ts.map +1 -0
- package/lib/types/client/protocol.js +5 -0
- package/lib/types/client/storage.d.ts +6 -0
- package/lib/types/client/storage.d.ts.map +1 -0
- package/lib/types/client/storage.js +21 -0
- package/lib/types/client/styles.d.ts +5 -0
- package/lib/types/client/styles.d.ts.map +1 -0
- package/lib/types/client/styles.js +103 -0
- package/lib/types/client/term.d.ts +15 -0
- package/lib/types/client/term.d.ts.map +1 -0
- package/lib/types/client/term.js +86 -0
- package/lib/types/client/toggle.d.ts +9 -0
- package/lib/types/client/toggle.d.ts.map +1 -0
- package/lib/types/client/toggle.js +19 -0
- package/lib/types/client/ws.d.ts +25 -0
- package/lib/types/client/ws.d.ts.map +1 -0
- package/lib/types/client/ws.js +79 -0
- package/lib/types/client/xterm-css.d.ts +7 -0
- package/lib/types/client/xterm-css.d.ts.map +1 -0
- package/lib/types/client/xterm-css.js +224 -0
- package/lib/types/host/hub.d.ts +39 -0
- package/lib/types/host/hub.d.ts.map +1 -0
- package/lib/types/host/hub.js +291 -0
- package/lib/types/host/index.d.ts +17 -0
- package/lib/types/host/index.d.ts.map +1 -0
- package/lib/types/host/index.js +66 -0
- package/lib/types/host/shells.d.ts +30 -0
- package/lib/types/host/shells.d.ts.map +1 -0
- package/lib/types/host/shells.js +202 -0
- package/lib/types/host/types.d.ts +109 -0
- package/lib/types/host/types.d.ts.map +1 -0
- package/lib/types/host/types.js +4 -0
- package/package.json +102 -0
- package/src/client/controller.ts +293 -0
- package/src/client/drawer.tsx +182 -0
- package/src/client/i18n.ts +58 -0
- package/src/client/index.ts +17 -0
- package/src/client/plugin.tsx +92 -0
- package/src/client/protocol.ts +40 -0
- package/src/client/storage.ts +21 -0
- package/src/client/styles.ts +106 -0
- package/src/client/term.tsx +95 -0
- package/src/client/toggle.tsx +43 -0
- package/src/client/ws.ts +81 -0
- package/src/client/xterm-css.ts +224 -0
- package/src/host/cordis-augment.d.ts +22 -0
- package/src/host/hub.ts +302 -0
- package/src/host/index.ts +78 -0
- package/src/host/shells.ts +216 -0
- 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
|
+
}
|