@kkutysllb/dsh-terminal 1.0.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/LICENSE +21 -0
- package/README.md +25 -0
- package/client.js +778 -0
- package/cordis.patch.yml +14 -0
- package/entry.js +418 -0
- package/package.json +46 -0
- package/vendor/addon-fit.js +2 -0
- package/vendor/xterm.css +218 -0
- package/vendor/xterm.js +2 -0
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @kkutysllb/dsh-terminal bundle layer(KCoder 桌面端内置嵌入式终端)。
|
|
2
|
+
#
|
|
3
|
+
# 声明于 package.json 的 dsh.bundle.patch,由 KCoder 主进程物化到
|
|
4
|
+
# web profile 的 node_modules 并注册进 dsh.profile.bundles(紧跟
|
|
5
|
+
# dsh-git-panel 之后)。本层挂一个胶水插件(entry.js):激活时
|
|
6
|
+
# 注册终端 RPC(/dsh-terminal/api/*:pty 操作 + SSE 输出流 + xterm
|
|
7
|
+
# vendor 静态托管);client 交付物(exports["./client"] → client.js)
|
|
8
|
+
# 由 dsh client-modules 自动集成,在 shell 页面渲染底部终端面板 +
|
|
9
|
+
# 标题栏开关按钮。后续层(profile 自身 cordis.patch.yml、--patch
|
|
10
|
+
# 覆盖)可按行 id 覆写或禁用本行。
|
|
11
|
+
|
|
12
|
+
- insert:
|
|
13
|
+
- id: '@kkutysllb/dsh-terminal'
|
|
14
|
+
name: '@kkutysllb/dsh-terminal'
|
package/entry.js
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @kkutysllb/dsh-terminal — server 半(dsh web 插件,cordis patch 层挂载)。
|
|
3
|
+
*
|
|
4
|
+
* 嵌入式终端 RPC(逻辑平移自退役宿主 desktop/main/pty-host.ts +
|
|
5
|
+
* terminal-panel.ts,语义保持一致):
|
|
6
|
+
* - POST /dsh-terminal/api/rpc pty 操作(tabs/new/write/resize/
|
|
7
|
+
* restart/close,cwd 为工作区桶键)
|
|
8
|
+
* - GET /dsh-terminal/api/stream SSE 输出流(data/exit 事件带 bucket,
|
|
9
|
+
* 全局一条广播,client 按当前桶路由;15s 心跳防代理断连)
|
|
10
|
+
* - GET /dsh-terminal/api/vendor/<name> xterm vendor 静态托管
|
|
11
|
+
* (client.js 自包含无 import,运行时懒拉 + eval;白名单三件)
|
|
12
|
+
*
|
|
13
|
+
* 安全边界与 dsh-git-panel 同款:isTrusted(loopback 放行 +
|
|
14
|
+
* webRuntime.trustedHosts);写操作 POST-only;SSE 为只读推送。
|
|
15
|
+
*
|
|
16
|
+
* pty 引擎:node-pty(VS Code 同款),经 createRequire 从运行时
|
|
17
|
+
* node_modules 解析(bundle 物化在 profiles/web/node_modules/@kcoder/
|
|
18
|
+
* terminal/,向上可达 profiles/node_modules/node-pty——dsh-tool-bash
|
|
19
|
+
* 已带)。延迟加载:模块导入不触发 native 绑定加载,首次 create 才
|
|
20
|
+
* require,环境异常时报错仅影响终端功能不拖垮宿主。
|
|
21
|
+
*
|
|
22
|
+
* 纯逻辑(桶管理/参数校验)导出供 tests/run-tests.mjs node 直跑;
|
|
23
|
+
* spawn 集成用例走真 pty。
|
|
24
|
+
*
|
|
25
|
+
* @module @kkutysllb/dsh-terminal/entry
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { createRequire } from 'node:module'
|
|
29
|
+
import { EventEmitter } from 'node:events'
|
|
30
|
+
import { statSync } from 'node:fs'
|
|
31
|
+
import { homedir } from 'node:os'
|
|
32
|
+
import { basename } from 'node:path'
|
|
33
|
+
import { readFile } from 'node:fs/promises'
|
|
34
|
+
|
|
35
|
+
/** client-modules 集成对应的 cordis 依赖:RPC 注册必需 webServer。 */
|
|
36
|
+
export const inject = ['webServer']
|
|
37
|
+
|
|
38
|
+
/** RPC 前缀(client.js 的 API 常量与之对齐)。 */
|
|
39
|
+
export const RPC_PREFIX = '/dsh-terminal/api'
|
|
40
|
+
|
|
41
|
+
/** vendor 白名单(文件名 → content-type;client 懒拉的三件)。 */
|
|
42
|
+
const VENDOR_FILES = {
|
|
43
|
+
'xterm.js': 'text/javascript; charset=utf-8',
|
|
44
|
+
'addon-fit.js': 'text/javascript; charset=utf-8',
|
|
45
|
+
'xterm.css': 'text/css; charset=utf-8',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** SSE 心跳间隔(毫秒;注释行保活,防中间层空闲断连)。 */
|
|
49
|
+
const SSE_HEARTBEAT_MS = 15000
|
|
50
|
+
|
|
51
|
+
/** 面板高度界限(与 client clamp 一致;拖拽在 client 端持久化)。 */
|
|
52
|
+
export const PANEL_MIN_H = 140
|
|
53
|
+
export const PANEL_MAX_H = 620
|
|
54
|
+
export const PANEL_DEFAULT_H = 280
|
|
55
|
+
|
|
56
|
+
/** 延迟解析 node-pty(native 模块,导入期不加载)。 */
|
|
57
|
+
let ptyModule = null
|
|
58
|
+
function getPty() {
|
|
59
|
+
if (ptyModule === null) {
|
|
60
|
+
ptyModule = createRequire(import.meta.url)('node-pty')
|
|
61
|
+
}
|
|
62
|
+
return ptyModule
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 标签信息(tabs/rpc 响应;与宿主 TerminalTab 契约一致)。 */
|
|
66
|
+
function tabOf(s) {
|
|
67
|
+
const shell = process.platform === 'win32' ? 'powershell.exe' : (process.env.SHELL || '/bin/zsh')
|
|
68
|
+
return { id: s.id, alive: !s.exited, cwd: s.cwd, title: basename(shell) }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function killPty(s) {
|
|
72
|
+
if (s.pty !== null) {
|
|
73
|
+
try { s.pty.kill() } catch { /* 已退出 */ }
|
|
74
|
+
s.pty = null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 目录可用性校验:存在且是目录才采用,否则交给回退。 */
|
|
79
|
+
export function usableDir(path) {
|
|
80
|
+
if (path === null || path === undefined || path === '') return null
|
|
81
|
+
try {
|
|
82
|
+
if (!statSync(path).isDirectory()) return null
|
|
83
|
+
return path
|
|
84
|
+
} catch {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 供面板 header 显示的目录短名。 */
|
|
90
|
+
export function dirLabel(cwd) {
|
|
91
|
+
if (cwd === '') return '~'
|
|
92
|
+
return basename(cwd) || cwd
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 高度 clamp(插件版本地持久化,服务端不再管高度;导出仅为测试对齐)。 */
|
|
96
|
+
export function clampH(h) {
|
|
97
|
+
return Math.min(PANEL_MAX_H, Math.max(PANEL_MIN_H, Math.round(h)))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* pty 会话宿主(平移自宿主 PtyHost):每工作区一份私有 sessions 池
|
|
102
|
+
* (buckets = Map<cwd, Map<id, session>>),不同工作区互不干扰;
|
|
103
|
+
* id 全局唯一跨桶可寻址;面板关闭仅隐藏不杀进程,restart 销毁重建
|
|
104
|
+
* 同 id,close 关单标签。事件:data(chunk, id, bucket) / exit(id, bucket)。
|
|
105
|
+
*/
|
|
106
|
+
export class PtyHost {
|
|
107
|
+
constructor() {
|
|
108
|
+
this.events = new EventEmitter()
|
|
109
|
+
this.buckets = new Map()
|
|
110
|
+
this.nextId = 1
|
|
111
|
+
this.cols = 80
|
|
112
|
+
this.rows = 24
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
bucketOf(cwd) {
|
|
116
|
+
return cwd !== null && cwd !== undefined ? cwd : ''
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
bucket(cwd) {
|
|
120
|
+
const key = this.bucketOf(cwd)
|
|
121
|
+
let m = this.buckets.get(key)
|
|
122
|
+
if (m === undefined) { m = new Map(); this.buckets.set(key, m) }
|
|
123
|
+
return m
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 当前工作区全部标签快照(Map 迭代序即创建序)。 */
|
|
127
|
+
list(cwd) {
|
|
128
|
+
const m = this.buckets.get(this.bucketOf(cwd))
|
|
129
|
+
return m === undefined ? [] : [...m.values()].map(tabOf)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 全部桶标签快照(调试/清账用)。 */
|
|
133
|
+
listAll() {
|
|
134
|
+
const out = []
|
|
135
|
+
for (const m of this.buckets.values()) for (const s of m.values()) out.push(tabOf(s))
|
|
136
|
+
return out
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 单个标签信息(按全局 id 查,跨桶一次)。 */
|
|
140
|
+
info(id) {
|
|
141
|
+
const s = this.find(id)
|
|
142
|
+
return s === null ? null : tabOf(s)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 全局 id 定位 session(id 全局唯一,跨桶查找一次即可)。 */
|
|
146
|
+
find(id) {
|
|
147
|
+
for (const m of this.buckets.values()) {
|
|
148
|
+
const s = m.get(id)
|
|
149
|
+
if (s !== undefined) return s
|
|
150
|
+
}
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 新建标签到指定工作区桶(shell 进程立即启动)。 */
|
|
155
|
+
create(cwd) {
|
|
156
|
+
const bucket = this.bucketOf(cwd)
|
|
157
|
+
const s = { id: this.nextId++, pty: null, cwd: usableDir(cwd) ?? homedir(), bucket, exited: false }
|
|
158
|
+
this.bucket(bucket).set(s.id, s)
|
|
159
|
+
this.spawn(s)
|
|
160
|
+
return tabOf(s)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 面板打开时确保指定工作区桶至少有一个标签(无则新建)。 */
|
|
164
|
+
ensureFirst(cwd) {
|
|
165
|
+
const first = this.bucket(cwd).values().next().value
|
|
166
|
+
if (first !== undefined) return tabOf(first)
|
|
167
|
+
return this.create(cwd)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 销毁对应标签并以(可能已变化的)工作区目录重建(同 id,跨桶迁移)。 */
|
|
171
|
+
restart(id, preferredCwd) {
|
|
172
|
+
const bucket = this.bucketOf(preferredCwd)
|
|
173
|
+
const s = this.buckets.get(bucket)?.get(id)
|
|
174
|
+
if (s === undefined) return null
|
|
175
|
+
const cwd = usableDir(preferredCwd) ?? s.cwd ?? homedir()
|
|
176
|
+
killPty(s)
|
|
177
|
+
s.cwd = cwd
|
|
178
|
+
s.bucket = bucket
|
|
179
|
+
s.exited = false
|
|
180
|
+
for (const m of this.buckets.values()) m.delete(id)
|
|
181
|
+
this.bucket(bucket).set(id, s)
|
|
182
|
+
this.spawn(s)
|
|
183
|
+
return tabOf(s)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
write(id, data) {
|
|
187
|
+
this.find(id)?.pty?.write(data)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
resize(id, cols, rows) {
|
|
191
|
+
const s = this.find(id)
|
|
192
|
+
if (s === undefined || s === null) return
|
|
193
|
+
this.cols = cols
|
|
194
|
+
this.rows = rows
|
|
195
|
+
try { s.pty?.resize(cols, rows) } catch {
|
|
196
|
+
// 进程退出瞬间 resize 会抛错,忽略(exit 事件会接手)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 关闭单个标签(仅限 cwd 桶内:跨桶 id 一律不动),返回剩余标签。 */
|
|
201
|
+
close(id, cwd) {
|
|
202
|
+
const bucket = this.bucketOf(cwd)
|
|
203
|
+
const s = this.buckets.get(bucket)?.get(id)
|
|
204
|
+
if (s === undefined) return this.list(cwd)
|
|
205
|
+
killPty(s)
|
|
206
|
+
this.buckets.get(bucket)?.delete(id)
|
|
207
|
+
return this.list(cwd)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** 彻底销毁全部会话(插件 dispose 时调用)。 */
|
|
211
|
+
dispose() {
|
|
212
|
+
for (const m of this.buckets.values())
|
|
213
|
+
for (const s of m.values()) killPty(s)
|
|
214
|
+
this.buckets.clear()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
spawn(s) {
|
|
218
|
+
const pty = getPty()
|
|
219
|
+
const shell = process.platform === 'win32' ? 'powershell.exe' : (process.env.SHELL || '/bin/zsh')
|
|
220
|
+
const args = process.platform === 'win32' ? [] : ['--login']
|
|
221
|
+
s.pty = pty.spawn(shell, args, {
|
|
222
|
+
name: 'xterm-256color',
|
|
223
|
+
cwd: s.cwd,
|
|
224
|
+
env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor' },
|
|
225
|
+
cols: this.cols,
|
|
226
|
+
rows: this.rows,
|
|
227
|
+
})
|
|
228
|
+
s.pty.onData(chunk => { this.events.emit('data', chunk, s.id, s.bucket) })
|
|
229
|
+
s.pty.onExit(() => {
|
|
230
|
+
s.exited = true
|
|
231
|
+
s.pty = null
|
|
232
|
+
this.events.emit('exit', s.id, s.bucket)
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* ---------------------------------------------------------------- *
|
|
238
|
+
* RPC(/dsh-terminal/api/{rpc,stream,vendor/<name>})
|
|
239
|
+
* ---------------------------------------------------------------- */
|
|
240
|
+
|
|
241
|
+
function isLoopbackHostname(hostname) {
|
|
242
|
+
const h = String(hostname || '').replace(/^\[|\]$/g, '').toLowerCase()
|
|
243
|
+
return h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '0.0.0.0'
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** 请求来源校验(dsh-git-panel 同款):loopback 放行,其余对 trustedHosts。 */
|
|
247
|
+
export function isTrusted(req, trustedHosts) {
|
|
248
|
+
try {
|
|
249
|
+
const hostHeader = req.headers?.host || req.headers?.Host
|
|
250
|
+
if (!hostHeader) return false
|
|
251
|
+
const url = new URL('http://' + hostHeader)
|
|
252
|
+
if (isLoopbackHostname(url.hostname)) return true
|
|
253
|
+
const list = Array.isArray(trustedHosts) ? trustedHosts : []
|
|
254
|
+
return list.some((entry) => {
|
|
255
|
+
const e = String(entry)
|
|
256
|
+
return e === hostHeader || e === url.hostname || e === url.host
|
|
257
|
+
})
|
|
258
|
+
} catch {
|
|
259
|
+
return false
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function writeJson(res, status, body) {
|
|
264
|
+
res.statusCode = status
|
|
265
|
+
res.setHeader('content-type', 'application/json; charset=utf-8')
|
|
266
|
+
res.setHeader('cache-control', 'no-store')
|
|
267
|
+
res.end(JSON.stringify(body))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function readJsonBody(req) {
|
|
271
|
+
const chunks = []
|
|
272
|
+
for await (const chunk of req) chunks.push(chunk)
|
|
273
|
+
if (!chunks.length) return {}
|
|
274
|
+
const raw = Buffer.concat(chunks).toString('utf8')
|
|
275
|
+
if (!raw.trim()) return {}
|
|
276
|
+
return JSON.parse(raw)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** RPC 操作分派(纯逻辑导出供测试;pty 实例操作走 host)。 */
|
|
280
|
+
export function dispatchRpc(host, body) {
|
|
281
|
+
const op = typeof body?.op === 'string' ? body.op : ''
|
|
282
|
+
const cwd = typeof body?.cwd === 'string' && body.cwd !== '' ? body.cwd : null
|
|
283
|
+
switch (op) {
|
|
284
|
+
case 'tabs': return { ok: true, tabs: host.list(cwd) }
|
|
285
|
+
case 'new': return { ok: true, tab: host.create(cwd) }
|
|
286
|
+
case 'write':
|
|
287
|
+
if (typeof body.id !== 'number' || typeof body.data !== 'string') return { ok: false, error: 'bad id/data' }
|
|
288
|
+
host.write(body.id, body.data)
|
|
289
|
+
return { ok: true }
|
|
290
|
+
case 'resize': {
|
|
291
|
+
if (typeof body.id !== 'number' || typeof body.cols !== 'number' || typeof body.rows !== 'number') {
|
|
292
|
+
return { ok: false, error: 'bad id/cols/rows' }
|
|
293
|
+
}
|
|
294
|
+
host.resize(body.id, body.cols, body.rows)
|
|
295
|
+
return { ok: true }
|
|
296
|
+
}
|
|
297
|
+
case 'restart': {
|
|
298
|
+
if (typeof body.id !== 'number') return { ok: false, error: 'bad id' }
|
|
299
|
+
return { ok: true, tab: host.restart(body.id, cwd) }
|
|
300
|
+
}
|
|
301
|
+
case 'close': {
|
|
302
|
+
if (typeof body.id !== 'number') return { ok: false, error: 'bad id' }
|
|
303
|
+
return { ok: true, tabs: host.close(body.id, cwd) }
|
|
304
|
+
}
|
|
305
|
+
default: return { ok: false, error: 'unknown op' }
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** dsh web 插件入口:注册 RPC + SSE + vendor 托管(effect 包裹随 dispose 摘除)。 */
|
|
310
|
+
export function apply(ctx) {
|
|
311
|
+
const host = new PtyHost()
|
|
312
|
+
/** 活跃 SSE 客户端(res 集;断开即摘除)。 */
|
|
313
|
+
const sseClients = new Set()
|
|
314
|
+
// 帧不带 event: 行(保持默认 message 事件,client 端 onmessage 直收)
|
|
315
|
+
const onData = (chunk, id, bucket) => {
|
|
316
|
+
const frame = `data: ${JSON.stringify({ type: 'data', bucket, id, chunk })}\n\n`
|
|
317
|
+
for (const res of sseClients) res.write(frame)
|
|
318
|
+
}
|
|
319
|
+
const onExit = (id, bucket) => {
|
|
320
|
+
const frame = `data: ${JSON.stringify({ type: 'exit', bucket, id })}\n\n`
|
|
321
|
+
for (const res of sseClients) res.write(frame)
|
|
322
|
+
}
|
|
323
|
+
host.events.on('data', onData)
|
|
324
|
+
host.events.on('exit', onExit)
|
|
325
|
+
const heartbeat = setInterval(() => {
|
|
326
|
+
for (const res of sseClients) res.write(`: ping\n\n`)
|
|
327
|
+
}, SSE_HEARTBEAT_MS)
|
|
328
|
+
if (typeof heartbeat.unref === 'function') heartbeat.unref()
|
|
329
|
+
|
|
330
|
+
const trustedHosts = () => {
|
|
331
|
+
try { return ctx.get('webRuntime')?.trustedHosts || [] } catch { return [] }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
ctx.effect(
|
|
335
|
+
() =>
|
|
336
|
+
ctx.webServer.register({
|
|
337
|
+
kind: 'prefix',
|
|
338
|
+
path: RPC_PREFIX,
|
|
339
|
+
handler: async (req, res) => {
|
|
340
|
+
if (!isTrusted(req, trustedHosts())) {
|
|
341
|
+
writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
let pathname = '/'
|
|
345
|
+
try { pathname = new URL(req.url ?? '/', 'http://x').pathname } catch { /* 兜底 '/' */ }
|
|
346
|
+
const tail = pathname.slice(RPC_PREFIX.length).replace(/^\/+/, '')
|
|
347
|
+
|
|
348
|
+
// SSE 输出流(只读 GET;持有响应即订阅,断开即退订)
|
|
349
|
+
if (tail === 'stream') {
|
|
350
|
+
if (req.method !== 'GET') {
|
|
351
|
+
writeJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
res.statusCode = 200
|
|
355
|
+
res.setHeader('content-type', 'text/event-stream; charset=utf-8')
|
|
356
|
+
res.setHeader('cache-control', 'no-store')
|
|
357
|
+
res.setHeader('connection', 'keep-alive')
|
|
358
|
+
res.write(': open\n\n')
|
|
359
|
+
sseClients.add(res)
|
|
360
|
+
req.on('close', () => { sseClients.delete(res) })
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// xterm vendor 静态托管(白名单三件;client 懒拉 + eval)
|
|
365
|
+
if (tail.startsWith('vendor/')) {
|
|
366
|
+
const name = tail.slice('vendor/'.length)
|
|
367
|
+
const type = VENDOR_FILES[name]
|
|
368
|
+
if (type === undefined) {
|
|
369
|
+
writeJson(res, 404, { ok: false, error: 'unknown vendor file' })
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const text = await readFile(new URL(`./vendor/${name}`, import.meta.url))
|
|
374
|
+
res.statusCode = 200
|
|
375
|
+
res.setHeader('content-type', type)
|
|
376
|
+
res.setHeader('cache-control', 'no-store')
|
|
377
|
+
res.end(text)
|
|
378
|
+
} catch (error) {
|
|
379
|
+
writeJson(res, 500, { ok: false, error: String(error?.message ?? error) })
|
|
380
|
+
}
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// pty 操作 RPC(写操作 POST-only)
|
|
385
|
+
if (tail === 'rpc') {
|
|
386
|
+
if (req.method !== 'POST') {
|
|
387
|
+
writeJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
let body = {}
|
|
391
|
+
try { body = await readJsonBody(req) } catch {
|
|
392
|
+
writeJson(res, 400, { ok: false, error: 'bad json body' })
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
writeJson(res, 200, dispatchRpc(host, body))
|
|
397
|
+
} catch (error) {
|
|
398
|
+
writeJson(res, 500, { ok: false, error: String(error?.message ?? error) })
|
|
399
|
+
}
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
writeJson(res, 404, { ok: false, error: 'unknown method' })
|
|
404
|
+
},
|
|
405
|
+
}),
|
|
406
|
+
'dsh-terminal: api',
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
// 插件停用时杀全部 shell + 摘事件(cordis effect 反向函数)
|
|
410
|
+
return () => {
|
|
411
|
+
clearInterval(heartbeat)
|
|
412
|
+
host.events.off('data', onData)
|
|
413
|
+
host.events.off('exit', onExit)
|
|
414
|
+
for (const res of sseClients) res.end()
|
|
415
|
+
sseClients.clear()
|
|
416
|
+
host.dispose()
|
|
417
|
+
}
|
|
418
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kkutysllb/dsh-terminal",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "dsh-external in-box dsh bundle: embedded terminal panel (node-pty backed multi-tab shells, per-workspace buckets, xterm.js UI) as a bottom dock inside the dsh web page, replacing the retired Electron host terminal-panel.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "entry.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./entry.js",
|
|
10
|
+
"./client": "./client.js",
|
|
11
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"entry.js",
|
|
16
|
+
"client.js",
|
|
17
|
+
"vendor",
|
|
18
|
+
"cordis.patch.yml",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"dsh": {
|
|
25
|
+
"bundle": {
|
|
26
|
+
"patch": "./cordis.patch.yml"
|
|
27
|
+
},
|
|
28
|
+
"client": {
|
|
29
|
+
"inject": [],
|
|
30
|
+
"platform": "web"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"smoke": "node scripts/smoke-plugin.mjs",
|
|
35
|
+
"sync:mirror": "node scripts/sync-to-dsh-plugins.mjs",
|
|
36
|
+
"sync:check": "node scripts/sync-to-dsh-plugins.mjs --check",
|
|
37
|
+
"prepack": "pnpm smoke"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/kkutysllb/dsh-terminal.git"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));
|
|
2
|
+
//# sourceMappingURL=addon-fit.js.map
|
package/vendor/xterm.css
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
|
3
|
+
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
4
|
+
* https://github.com/chjj/term.js
|
|
5
|
+
* @license MIT
|
|
6
|
+
*
|
|
7
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
* in the Software without restriction, including without limitation the rights
|
|
10
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
* furnished to do so, subject to the following conditions:
|
|
13
|
+
*
|
|
14
|
+
* The above copyright notice and this permission notice shall be included in
|
|
15
|
+
* all copies or substantial portions of the Software.
|
|
16
|
+
*
|
|
17
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
* THE SOFTWARE.
|
|
24
|
+
*
|
|
25
|
+
* Originally forked from (with the author's permission):
|
|
26
|
+
* Fabrice Bellard's javascript vt100 for jslinux:
|
|
27
|
+
* http://bellard.org/jslinux/
|
|
28
|
+
* Copyright (c) 2011 Fabrice Bellard
|
|
29
|
+
* The original design remains. The terminal itself
|
|
30
|
+
* has been extended to include xterm CSI codes, among
|
|
31
|
+
* other features.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Default styles for xterm.js
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
.xterm {
|
|
39
|
+
cursor: text;
|
|
40
|
+
position: relative;
|
|
41
|
+
user-select: none;
|
|
42
|
+
-ms-user-select: none;
|
|
43
|
+
-webkit-user-select: none;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.xterm.focus,
|
|
47
|
+
.xterm:focus {
|
|
48
|
+
outline: none;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.xterm .xterm-helpers {
|
|
52
|
+
position: absolute;
|
|
53
|
+
top: 0;
|
|
54
|
+
/**
|
|
55
|
+
* The z-index of the helpers must be higher than the canvases in order for
|
|
56
|
+
* IMEs to appear on top.
|
|
57
|
+
*/
|
|
58
|
+
z-index: 5;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.xterm .xterm-helper-textarea {
|
|
62
|
+
padding: 0;
|
|
63
|
+
border: 0;
|
|
64
|
+
margin: 0;
|
|
65
|
+
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
|
|
66
|
+
position: absolute;
|
|
67
|
+
opacity: 0;
|
|
68
|
+
left: -9999em;
|
|
69
|
+
top: 0;
|
|
70
|
+
width: 0;
|
|
71
|
+
height: 0;
|
|
72
|
+
z-index: -5;
|
|
73
|
+
/** Prevent wrapping so the IME appears against the textarea at the correct position */
|
|
74
|
+
white-space: nowrap;
|
|
75
|
+
overflow: hidden;
|
|
76
|
+
resize: none;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.xterm .composition-view {
|
|
80
|
+
/* TODO: Composition position got messed up somewhere */
|
|
81
|
+
background: #000;
|
|
82
|
+
color: #FFF;
|
|
83
|
+
display: none;
|
|
84
|
+
position: absolute;
|
|
85
|
+
white-space: nowrap;
|
|
86
|
+
z-index: 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.xterm .composition-view.active {
|
|
90
|
+
display: block;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.xterm .xterm-viewport {
|
|
94
|
+
/* On OS X this is required in order for the scroll bar to appear fully opaque */
|
|
95
|
+
background-color: #000;
|
|
96
|
+
overflow-y: scroll;
|
|
97
|
+
cursor: default;
|
|
98
|
+
position: absolute;
|
|
99
|
+
right: 0;
|
|
100
|
+
left: 0;
|
|
101
|
+
top: 0;
|
|
102
|
+
bottom: 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.xterm .xterm-screen {
|
|
106
|
+
position: relative;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.xterm .xterm-screen canvas {
|
|
110
|
+
position: absolute;
|
|
111
|
+
left: 0;
|
|
112
|
+
top: 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.xterm .xterm-scroll-area {
|
|
116
|
+
visibility: hidden;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.xterm-char-measure-element {
|
|
120
|
+
display: inline-block;
|
|
121
|
+
visibility: hidden;
|
|
122
|
+
position: absolute;
|
|
123
|
+
top: 0;
|
|
124
|
+
left: -9999em;
|
|
125
|
+
line-height: normal;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.xterm.enable-mouse-events {
|
|
129
|
+
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
|
130
|
+
cursor: default;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.xterm.xterm-cursor-pointer,
|
|
134
|
+
.xterm .xterm-cursor-pointer {
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.xterm.column-select.focus {
|
|
139
|
+
/* Column selection mode */
|
|
140
|
+
cursor: crosshair;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.xterm .xterm-accessibility:not(.debug),
|
|
144
|
+
.xterm .xterm-message {
|
|
145
|
+
position: absolute;
|
|
146
|
+
left: 0;
|
|
147
|
+
top: 0;
|
|
148
|
+
bottom: 0;
|
|
149
|
+
right: 0;
|
|
150
|
+
z-index: 10;
|
|
151
|
+
color: transparent;
|
|
152
|
+
pointer-events: none;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
|
|
156
|
+
color: transparent;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.xterm .xterm-accessibility-tree {
|
|
160
|
+
user-select: text;
|
|
161
|
+
white-space: pre;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.xterm .live-region {
|
|
165
|
+
position: absolute;
|
|
166
|
+
left: -9999px;
|
|
167
|
+
width: 1px;
|
|
168
|
+
height: 1px;
|
|
169
|
+
overflow: hidden;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.xterm-dim {
|
|
173
|
+
/* Dim should not apply to background, so the opacity of the foreground color is applied
|
|
174
|
+
* explicitly in the generated class and reset to 1 here */
|
|
175
|
+
opacity: 1 !important;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.xterm-underline-1 { text-decoration: underline; }
|
|
179
|
+
.xterm-underline-2 { text-decoration: double underline; }
|
|
180
|
+
.xterm-underline-3 { text-decoration: wavy underline; }
|
|
181
|
+
.xterm-underline-4 { text-decoration: dotted underline; }
|
|
182
|
+
.xterm-underline-5 { text-decoration: dashed underline; }
|
|
183
|
+
|
|
184
|
+
.xterm-overline {
|
|
185
|
+
text-decoration: overline;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
|
|
189
|
+
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
|
|
190
|
+
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
|
|
191
|
+
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
|
|
192
|
+
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
|
|
193
|
+
|
|
194
|
+
.xterm-strikethrough {
|
|
195
|
+
text-decoration: line-through;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.xterm-screen .xterm-decoration-container .xterm-decoration {
|
|
199
|
+
z-index: 6;
|
|
200
|
+
position: absolute;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
|
|
204
|
+
z-index: 7;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.xterm-decoration-overview-ruler {
|
|
208
|
+
z-index: 8;
|
|
209
|
+
position: absolute;
|
|
210
|
+
top: 0;
|
|
211
|
+
right: 0;
|
|
212
|
+
pointer-events: none;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.xterm-decoration-top {
|
|
216
|
+
z-index: 2;
|
|
217
|
+
position: relative;
|
|
218
|
+
}
|