dsh-bash-terminal-ts 0.2.6 → 0.5.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.en.md +82 -197
- package/README.md +197 -194
- package/cordis.patch.yml +5 -5
- package/dist/client.core.js +46 -39
- package/dist/client.js +47 -40
- package/dsh.plugin.json +22 -22
- package/lib/client.js +148 -0
- package/lib/dsh-types.d.ts +1 -1
- package/lib/index.d.ts +17 -16
- package/lib/index.js +47 -47
- package/lib/terminal.d.ts +24 -1
- package/lib/terminal.js +214 -49
- package/package.json +38 -57
- package/screenshots.json +3 -0
- package/scripts/build-client.mjs +14 -13
- package/src/client.tsx +70 -46
- package/src/dsh-types.ts +1 -1
- package/src/index.ts +47 -47
- package/src/terminal.ts +254 -54
- package/README.ja.md +0 -196
- package/README.ko.md +0 -194
package/lib/terminal.d.ts
CHANGED
|
@@ -4,14 +4,23 @@ export type TerminalSignal = (typeof TERMINAL_SIGNALS)[number];
|
|
|
4
4
|
interface TerminalBufferRead {
|
|
5
5
|
delta: string;
|
|
6
6
|
nextOffset: number;
|
|
7
|
+
truncated: boolean;
|
|
7
8
|
}
|
|
8
9
|
interface TerminalBuffer {
|
|
9
10
|
append(chunk: string): void;
|
|
10
11
|
readFrom(offset: number): TerminalBufferRead;
|
|
11
12
|
snapshot(): string;
|
|
13
|
+
wasTruncated(): boolean;
|
|
14
|
+
lastAppendAt(): number;
|
|
12
15
|
}
|
|
13
16
|
/** Interactive-shell argv (no -c: the terminal itself is the session). */
|
|
14
17
|
export declare function terminalArgv(shell: string, paths: ResolvedPaths, distro?: string): Array<string | undefined>;
|
|
18
|
+
export interface TerminalSessionInfo {
|
|
19
|
+
sessionId: string;
|
|
20
|
+
shell: string;
|
|
21
|
+
pid: number;
|
|
22
|
+
closed: boolean;
|
|
23
|
+
}
|
|
15
24
|
interface TerminalSession {
|
|
16
25
|
id: string;
|
|
17
26
|
handle: TerminalHandle;
|
|
@@ -19,6 +28,9 @@ interface TerminalSession {
|
|
|
19
28
|
shell: string;
|
|
20
29
|
distro?: string;
|
|
21
30
|
closed: boolean;
|
|
31
|
+
idleMs: number;
|
|
32
|
+
idleTimer?: NodeJS.Timeout;
|
|
33
|
+
touch: () => void;
|
|
22
34
|
}
|
|
23
35
|
export interface TerminalOpenOptions {
|
|
24
36
|
argv: string[];
|
|
@@ -29,6 +41,7 @@ export interface TerminalOpenOptions {
|
|
|
29
41
|
cols: number;
|
|
30
42
|
distro?: string;
|
|
31
43
|
initial?: string;
|
|
44
|
+
idleMs?: number;
|
|
32
45
|
}
|
|
33
46
|
export interface TerminalRegistry {
|
|
34
47
|
open(options: TerminalOpenOptions): Promise<TerminalSession>;
|
|
@@ -37,6 +50,7 @@ export interface TerminalRegistry {
|
|
|
37
50
|
read(id: string): TerminalBufferRead;
|
|
38
51
|
signal(id: string, sig: string): Promise<TerminalBufferRead>;
|
|
39
52
|
close(id: string): Promise<boolean>;
|
|
53
|
+
list(): TerminalSessionInfo[];
|
|
40
54
|
}
|
|
41
55
|
/**
|
|
42
56
|
* Terminal-session registry owned by the plugin fiber: opens PTYs through
|
|
@@ -51,6 +65,7 @@ export type ValidatedTerminalArgs = {
|
|
|
51
65
|
command?: string;
|
|
52
66
|
distro?: string;
|
|
53
67
|
workdir?: string;
|
|
68
|
+
idleMs?: number;
|
|
54
69
|
} | {
|
|
55
70
|
action: "send";
|
|
56
71
|
sessionId: string;
|
|
@@ -65,11 +80,14 @@ export type ValidatedTerminalArgs = {
|
|
|
65
80
|
} | {
|
|
66
81
|
action: "close";
|
|
67
82
|
sessionId: string;
|
|
83
|
+
} | {
|
|
84
|
+
action: "list";
|
|
68
85
|
};
|
|
69
86
|
export declare function validateArgs(args: Record<string, unknown>): asserts args is ValidatedTerminalArgs;
|
|
70
87
|
export interface TerminalOpenResult {
|
|
71
88
|
kind: "open";
|
|
72
89
|
sessionId: string;
|
|
90
|
+
jobId?: string;
|
|
73
91
|
pid: number;
|
|
74
92
|
shell: string;
|
|
75
93
|
output: string;
|
|
@@ -77,11 +95,16 @@ export interface TerminalOpenResult {
|
|
|
77
95
|
export interface TerminalSessionResult {
|
|
78
96
|
kind: "session";
|
|
79
97
|
output: string;
|
|
98
|
+
truncated?: boolean;
|
|
80
99
|
}
|
|
81
100
|
export interface TerminalClosedResult {
|
|
82
101
|
kind: "closed";
|
|
83
102
|
sessionId: string;
|
|
84
103
|
}
|
|
85
|
-
export
|
|
104
|
+
export interface TerminalListResult {
|
|
105
|
+
kind: "list";
|
|
106
|
+
sessions: TerminalSessionInfo[];
|
|
107
|
+
}
|
|
108
|
+
export type TerminalToolResult = TerminalOpenResult | TerminalSessionResult | TerminalClosedResult | TerminalListResult;
|
|
86
109
|
export declare function terminalTool(ctx: BashTerminalContext, registry: TerminalRegistry, paths: ResolvedPaths, defaultShell: () => string): import("./dsh-types.js").ToolDefinition;
|
|
87
110
|
export {};
|
package/lib/terminal.js
CHANGED
|
@@ -1,29 +1,69 @@
|
|
|
1
|
-
// dsh-bash-terminal
|
|
1
|
+
// dsh-bash-terminal: interactive terminal tool over the official PTY seam.
|
|
2
2
|
// ctx.subprocess.spawnTerminal (node-pty under the hood) allocates a real
|
|
3
3
|
// terminal; this module owns model-facing sessions: open / send / read /
|
|
4
4
|
// signal (Ctrl+C etc.) / close. The backend follows the user's default
|
|
5
5
|
// terminal setting, exactly like the shell tool.
|
|
6
|
+
//
|
|
7
|
+
// Windows note: the official seam's process inspector is POSIX-only
|
|
8
|
+
// (createProcessInspector throws on win32), so on Windows the PTY is
|
|
9
|
+
// allocated directly through node-pty. Session readiness here is
|
|
10
|
+
// output-quiet based (waitSettled), which needs no process inspection.
|
|
6
11
|
import { randomUUID } from "node:crypto";
|
|
7
12
|
import { isAbsolute, resolve } from "node:path";
|
|
13
|
+
import { PassThrough } from "node:stream";
|
|
8
14
|
import { TOOL_ABORTED, defineTool, HarnessError } from "./dsh.js";
|
|
15
|
+
// Single source of truth for backend env (msys2 MSYSTEM injection, WSL WSLENV).
|
|
16
|
+
// The cycle with ./index.js is safe: buildEnv is a hoisted function declaration
|
|
17
|
+
// and is only called at tool-execution time, never during module evaluation.
|
|
9
18
|
import { buildEnv } from "./index.js";
|
|
10
19
|
const MAX_BUFFER_BYTES = 1024 * 1024;
|
|
11
|
-
const READ_SETTLE_MS = 800;
|
|
12
20
|
const DEFAULT_ROWS = 30;
|
|
13
21
|
const DEFAULT_COLS = 110;
|
|
22
|
+
const DEFAULT_IDLE_MS = 10 * 60 * 1000;
|
|
23
|
+
const MAX_SESSIONS = 8;
|
|
14
24
|
const TERMINAL_SIGNALS = ["SIGINT", "SIGTERM", "SIGKILL", "SIGTSTP", "SIGHUP"];
|
|
15
25
|
function delay(ms) {
|
|
16
26
|
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
17
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Wait until the terminal output goes quiet (no new bytes for quietMs) or the
|
|
30
|
+
* cap elapses. Reads after interactive input then return the complete reply
|
|
31
|
+
* instead of a fixed-delay slice.
|
|
32
|
+
*/
|
|
33
|
+
async function waitSettled(buffer, quietMs = 300, timeoutMs = 5000) {
|
|
34
|
+
const start = Date.now();
|
|
35
|
+
let last = buffer.lastAppendAt();
|
|
36
|
+
while (Date.now() - start < timeoutMs) {
|
|
37
|
+
await delay(80);
|
|
38
|
+
const now = buffer.lastAppendAt();
|
|
39
|
+
if (now > last) {
|
|
40
|
+
last = now;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (Date.now() - last >= quietMs)
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
18
47
|
/** Interactive-shell argv (no -c: the terminal itself is the session). */
|
|
19
48
|
export function terminalArgv(shell, paths, distro) {
|
|
20
49
|
switch (shell) {
|
|
21
50
|
case "powershell": return [paths.pwsh, "-NoLogo", "-NoProfile"];
|
|
22
51
|
case "gitbash": return [paths.gitbash, "-i"];
|
|
23
|
-
case "msys2":
|
|
52
|
+
case "msys2":
|
|
53
|
+
// -l, NOT -lc: this argv carries no command (the PTY itself is the
|
|
54
|
+
// session), and `bash -lc` with no operand dies immediately with
|
|
55
|
+
// "-c: option requires an argument" (measured: exit 2). A login shell is
|
|
56
|
+
// also what sources /etc/profile, putting /usr/bin and /mingw64/bin on
|
|
57
|
+
// PATH for the interactive session.
|
|
58
|
+
return [paths.msys2, "-l"];
|
|
24
59
|
case "wsl": {
|
|
25
|
-
|
|
26
|
-
|
|
60
|
+
// Verified under ConPTY: 'wsl -e bash -i' on the DEFAULT distro fails with
|
|
61
|
+
// a WSL service RPC error (0x8007072c), while 'wsl -- bash -i' works;
|
|
62
|
+
// with an explicit -d distro, '-e bash -i' also works.
|
|
63
|
+
if (distro !== undefined && distro.trim().length > 0) {
|
|
64
|
+
return [paths.wsl, "-d", distro.trim(), "-e", "bash", "-i"];
|
|
65
|
+
}
|
|
66
|
+
return [paths.wsl, "--", "bash", "-i"];
|
|
27
67
|
}
|
|
28
68
|
default: throw new Error("invalid shell: " + JSON.stringify(shell));
|
|
29
69
|
}
|
|
@@ -31,17 +71,66 @@ export function terminalArgv(shell, paths, distro) {
|
|
|
31
71
|
/** In-memory output ring for one session (drop-oldest at the cap). */
|
|
32
72
|
function createBuffer() {
|
|
33
73
|
let text = "";
|
|
74
|
+
let truncated = false;
|
|
75
|
+
let lastAppendAt = Date.now();
|
|
34
76
|
return {
|
|
35
77
|
append(chunk) {
|
|
36
78
|
text += chunk;
|
|
37
|
-
|
|
79
|
+
lastAppendAt = Date.now();
|
|
80
|
+
if (text.length > MAX_BUFFER_BYTES) {
|
|
38
81
|
text = text.slice(text.length - MAX_BUFFER_BYTES);
|
|
82
|
+
truncated = true;
|
|
83
|
+
}
|
|
39
84
|
},
|
|
40
85
|
readFrom(offset) {
|
|
41
86
|
const delta = offset >= text.length ? "" : text.slice(offset);
|
|
42
|
-
return { delta, nextOffset: text.length };
|
|
87
|
+
return { delta, nextOffset: text.length, truncated };
|
|
88
|
+
},
|
|
89
|
+
snapshot: () => text,
|
|
90
|
+
wasTruncated: () => truncated,
|
|
91
|
+
lastAppendAt: () => lastAppendAt
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Direct node-pty handle with the same surface the official seam returns:
|
|
96
|
+
* output (stream), pid, done, write / signalForeground / terminate. Used on
|
|
97
|
+
* win32 where the official spawnTerminal's process inspector is unsupported.
|
|
98
|
+
*/
|
|
99
|
+
async function spawnPtyHandle(spec) {
|
|
100
|
+
const { argv, cwd, env, rows, cols } = spec;
|
|
101
|
+
const mod = (await import("node-pty"));
|
|
102
|
+
const nodePty = mod.default ?? mod;
|
|
103
|
+
const term = nodePty.spawn(argv[0], argv.slice(1), {
|
|
104
|
+
name: "dumb",
|
|
105
|
+
cols,
|
|
106
|
+
rows,
|
|
107
|
+
cwd,
|
|
108
|
+
env: { ...env, TERM: "dumb", NO_COLOR: "1" }
|
|
109
|
+
});
|
|
110
|
+
const output = new PassThrough();
|
|
111
|
+
term.onData((chunk) => output.write(Buffer.from(chunk, "utf8")));
|
|
112
|
+
const done = new Promise((resolveDone) => {
|
|
113
|
+
term.onExit(({ exitCode, signal }) => resolveDone({ exitCode, signal: typeof signal === "string" ? signal : null }));
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
pid: term.pid,
|
|
117
|
+
output,
|
|
118
|
+
done,
|
|
119
|
+
write: (data) => term.write(data),
|
|
120
|
+
// node-pty accepts no named signals on Windows: Ctrl+C is the \x03 byte
|
|
121
|
+
// (ConPTY delivers it to the foreground process), other signals degrade
|
|
122
|
+
// to terminating the session.
|
|
123
|
+
signalForeground: (sig) => {
|
|
124
|
+
if (sig === "SIGINT") {
|
|
125
|
+
term.write("\x03");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
term.kill();
|
|
43
129
|
},
|
|
44
|
-
|
|
130
|
+
terminate: async () => { try {
|
|
131
|
+
term.kill();
|
|
132
|
+
}
|
|
133
|
+
catch { /* already dead */ } }
|
|
45
134
|
};
|
|
46
135
|
}
|
|
47
136
|
/**
|
|
@@ -58,19 +147,21 @@ export function createTerminalRegistry(ctx) {
|
|
|
58
147
|
sessions.clear();
|
|
59
148
|
}, "bash-terminal: terminal sessions teardown");
|
|
60
149
|
async function open(options) {
|
|
61
|
-
const { argv, shell, cwd, env, rows, cols, distro, initial } = options;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
150
|
+
const { argv, shell, cwd, env, rows, cols, distro, initial, idleMs } = options;
|
|
151
|
+
if (sessions.size >= MAX_SESSIONS) {
|
|
152
|
+
throw new Error("terminal: too many open sessions (" + MAX_SESSIONS + "); close one before opening another");
|
|
153
|
+
}
|
|
154
|
+
let handle;
|
|
155
|
+
if (process.platform === "win32") {
|
|
156
|
+
handle = await spawnPtyHandle({ argv, cwd, env, rows, cols });
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const spawnTerminal = ctx.subprocess?.spawnTerminal;
|
|
160
|
+
if (spawnTerminal === undefined) {
|
|
161
|
+
throw new Error("terminal: ctx.subprocess.spawnTerminal seam unavailable");
|
|
162
|
+
}
|
|
163
|
+
handle = await spawnTerminal({ argv, cwd, env, rows, cols, graceMs: 3000 });
|
|
65
164
|
}
|
|
66
|
-
const handle = await spawnTerminal({
|
|
67
|
-
argv,
|
|
68
|
-
cwd,
|
|
69
|
-
env,
|
|
70
|
-
rows,
|
|
71
|
-
cols,
|
|
72
|
-
graceMs: 3000
|
|
73
|
-
});
|
|
74
165
|
const buffer = createBuffer();
|
|
75
166
|
handle.output.on("data", (chunk) => buffer.append(typeof chunk === "string" ? chunk : String(chunk)));
|
|
76
167
|
const session = {
|
|
@@ -79,12 +170,30 @@ export function createTerminalRegistry(ctx) {
|
|
|
79
170
|
buffer,
|
|
80
171
|
shell,
|
|
81
172
|
distro,
|
|
82
|
-
closed: false
|
|
173
|
+
closed: false,
|
|
174
|
+
idleMs: idleMs ?? DEFAULT_IDLE_MS,
|
|
175
|
+
touch: () => { }
|
|
176
|
+
};
|
|
177
|
+
// Idle guard: any read/send/signal touches the timer; expiry terminates the
|
|
178
|
+
// session so an abandoned PTY never leaks a process tree.
|
|
179
|
+
const touch = () => {
|
|
180
|
+
if (session.closed)
|
|
181
|
+
return;
|
|
182
|
+
clearTimeout(session.idleTimer);
|
|
183
|
+
session.idleTimer = setTimeout(() => {
|
|
184
|
+
if (session.closed)
|
|
185
|
+
return;
|
|
186
|
+
session.closed = true;
|
|
187
|
+
sessions.delete(session.id);
|
|
188
|
+
void session.handle.terminate().catch(() => { });
|
|
189
|
+
}, session.idleMs);
|
|
83
190
|
};
|
|
191
|
+
session.touch = touch;
|
|
192
|
+
touch();
|
|
84
193
|
sessions.set(session.id, session);
|
|
85
194
|
if (initial !== undefined && initial.length > 0) {
|
|
86
195
|
await handle.write(initial);
|
|
87
|
-
await
|
|
196
|
+
await waitSettled(buffer);
|
|
88
197
|
}
|
|
89
198
|
return session;
|
|
90
199
|
}
|
|
@@ -94,41 +203,51 @@ export function createTerminalRegistry(ctx) {
|
|
|
94
203
|
throw new Error("terminal session not found: " + JSON.stringify(id));
|
|
95
204
|
if (session.closed)
|
|
96
205
|
throw new Error("terminal session is closed: " + JSON.stringify(id));
|
|
206
|
+
session.touch();
|
|
97
207
|
return session;
|
|
98
208
|
}
|
|
99
209
|
async function send(id, input) {
|
|
100
210
|
const session = get(id);
|
|
101
211
|
await session.handle.write(input);
|
|
102
|
-
await
|
|
212
|
+
await waitSettled(session.buffer);
|
|
103
213
|
return read(id);
|
|
104
214
|
}
|
|
105
|
-
function read(id) {
|
|
106
|
-
const session = get(id);
|
|
107
|
-
const { delta, nextOffset } = session.buffer.readFrom(0);
|
|
108
|
-
return { delta, nextOffset };
|
|
109
|
-
}
|
|
110
215
|
async function signal(id, sig) {
|
|
111
216
|
const session = get(id);
|
|
112
217
|
if (!TERMINAL_SIGNALS.includes(sig)) {
|
|
113
218
|
throw new Error("invalid terminal signal: " + JSON.stringify(sig));
|
|
114
219
|
}
|
|
115
220
|
await session.handle.signalForeground(sig);
|
|
116
|
-
await
|
|
221
|
+
await waitSettled(session.buffer);
|
|
117
222
|
return read(id);
|
|
118
223
|
}
|
|
224
|
+
function read(id) {
|
|
225
|
+
const session = get(id);
|
|
226
|
+
const { delta, nextOffset, truncated } = session.buffer.readFrom(0);
|
|
227
|
+
return { delta, nextOffset, truncated };
|
|
228
|
+
}
|
|
119
229
|
async function close(id) {
|
|
120
230
|
const session = get(id);
|
|
121
231
|
session.closed = true;
|
|
232
|
+
clearTimeout(session.idleTimer);
|
|
122
233
|
sessions.delete(id);
|
|
123
234
|
await session.handle.terminate();
|
|
124
235
|
return true;
|
|
125
236
|
}
|
|
126
|
-
|
|
237
|
+
function list() {
|
|
238
|
+
return [...sessions.values()].map((session) => ({
|
|
239
|
+
sessionId: session.id,
|
|
240
|
+
shell: session.shell,
|
|
241
|
+
pid: session.handle.pid,
|
|
242
|
+
closed: session.closed
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
return { open, get, send, read, signal, close, list };
|
|
127
246
|
}
|
|
128
247
|
export function validateArgs(args) {
|
|
129
248
|
const action = args.action;
|
|
130
|
-
if (typeof action !== "string" || ["open", "send", "read", "signal", "close"].indexOf(action) === -1) {
|
|
131
|
-
throw new Error("invalid action: expected open, send, read, signal, or
|
|
249
|
+
if (typeof action !== "string" || ["open", "send", "read", "signal", "close", "list"].indexOf(action) === -1) {
|
|
250
|
+
throw new Error("invalid action: expected open, send, read, signal, close, or list");
|
|
132
251
|
}
|
|
133
252
|
if ((action === "send" || action === "read" || action === "signal" || action === "close") && (typeof args.sessionId !== "string" || args.sessionId.length === 0)) {
|
|
134
253
|
throw new Error("invalid sessionId: required for " + action);
|
|
@@ -143,13 +262,13 @@ export function validateArgs(args) {
|
|
|
143
262
|
export function terminalTool(ctx, registry, paths, defaultShell) {
|
|
144
263
|
return defineTool({
|
|
145
264
|
name: "terminal",
|
|
146
|
-
description: "Interactive terminal session over the user's default terminal (Settings -> General -> Default terminal: powershell / gitbash / msys2 / wsl). A real PTY hosts a persistent shell: open a session, send input and read output across turns, deliver signals (Ctrl+C = SIGINT) to the foreground process, and close when done. Backend and env follow the shell tool exactly; the session survives between calls until closed. Use this for interactive programs (REPLs, ssh, databases, TUI tools) or when you need shell state (cwd, variables, aliases) to persist across calls.",
|
|
265
|
+
description: "Interactive terminal session over the user's default terminal (Settings -> General -> Default terminal: powershell / gitbash / msys2 / wsl). A real PTY hosts a persistent shell: open a session, send input and read output across turns, deliver signals (Ctrl+C = SIGINT) to the foreground process, and close when done. Backend and env follow the shell tool exactly; the session survives between calls until closed and is managed as a background job (job_kill / job_output work on it); idle sessions close automatically. Use this for interactive programs (REPLs, ssh, databases, TUI tools) or when you need shell state (cwd, variables, aliases) to persist across calls.",
|
|
147
266
|
parameters: {
|
|
148
267
|
action: {
|
|
149
268
|
type: "string",
|
|
150
|
-
enum: ["open", "send", "read", "signal", "close"],
|
|
269
|
+
enum: ["open", "send", "read", "signal", "close", "list"],
|
|
151
270
|
required: true,
|
|
152
|
-
description: "open: create a session and return its id. send: write input and read new output. read: read new output without writing. signal: send a signal to the foreground process (SIGINT for Ctrl+C). close: terminate the session."
|
|
271
|
+
description: "open: create a session and return its id. send: write input and read new output. read: read new output without writing. signal: send a signal to the foreground process (SIGINT for Ctrl+C). close: terminate the session. list: list live sessions."
|
|
153
272
|
},
|
|
154
273
|
sessionId: {
|
|
155
274
|
type: "string",
|
|
@@ -175,6 +294,10 @@ export function terminalTool(ctx, registry, paths, defaultShell) {
|
|
|
175
294
|
workdir: {
|
|
176
295
|
type: "string",
|
|
177
296
|
description: "Working directory for the session (open only). Defaults to the session workspace."
|
|
297
|
+
},
|
|
298
|
+
idleMs: {
|
|
299
|
+
type: "number",
|
|
300
|
+
description: "Idle timeout in ms (open only): the session closes automatically after this long without send/read/signal. Default 600000 (10 min)."
|
|
178
301
|
}
|
|
179
302
|
},
|
|
180
303
|
output: {
|
|
@@ -186,6 +309,7 @@ export function terminalTool(ctx, registry, paths, defaultShell) {
|
|
|
186
309
|
properties: {
|
|
187
310
|
kind: { type: "string", required: true, const: "open" },
|
|
188
311
|
sessionId: { type: "string", required: true },
|
|
312
|
+
jobId: { type: "string" },
|
|
189
313
|
pid: { type: "integer", required: true },
|
|
190
314
|
shell: { type: "string", required: true },
|
|
191
315
|
output: { type: "string" }
|
|
@@ -196,7 +320,8 @@ export function terminalTool(ctx, registry, paths, defaultShell) {
|
|
|
196
320
|
additionalProperties: false,
|
|
197
321
|
properties: {
|
|
198
322
|
kind: { type: "string", required: true, const: "session" },
|
|
199
|
-
output: { type: "string", required: true }
|
|
323
|
+
output: { type: "string", required: true },
|
|
324
|
+
truncated: { type: "boolean" }
|
|
200
325
|
}
|
|
201
326
|
},
|
|
202
327
|
{
|
|
@@ -206,6 +331,26 @@ export function terminalTool(ctx, registry, paths, defaultShell) {
|
|
|
206
331
|
kind: { type: "string", required: true, const: "closed" },
|
|
207
332
|
sessionId: { type: "string", required: true }
|
|
208
333
|
}
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
type: "object",
|
|
337
|
+
additionalProperties: false,
|
|
338
|
+
properties: {
|
|
339
|
+
kind: { type: "string", required: true, const: "list" },
|
|
340
|
+
sessions: {
|
|
341
|
+
type: "array",
|
|
342
|
+
items: {
|
|
343
|
+
type: "object",
|
|
344
|
+
additionalProperties: false,
|
|
345
|
+
properties: {
|
|
346
|
+
sessionId: { type: "string", required: true },
|
|
347
|
+
shell: { type: "string", required: true },
|
|
348
|
+
pid: { type: "integer", required: true },
|
|
349
|
+
closed: { type: "boolean" }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
209
354
|
}
|
|
210
355
|
]
|
|
211
356
|
},
|
|
@@ -215,7 +360,11 @@ export function terminalTool(ctx, registry, paths, defaultShell) {
|
|
|
215
360
|
? "terminal session " + value.sessionId + " (pid " + value.pid + ", " + value.shell + ")" + (value.output ? "\n" + value.output : "")
|
|
216
361
|
: value.kind === "closed"
|
|
217
362
|
? "terminal session " + value.sessionId + " closed"
|
|
218
|
-
: value.
|
|
363
|
+
: value.kind === "list"
|
|
364
|
+
? (value.sessions.length === 0 ? "no open terminal sessions" : value.sessions.map((s) => s.sessionId + " (" + s.shell + ", pid " + s.pid + ")").join("\n"))
|
|
365
|
+
: value.truncated === true
|
|
366
|
+
? (value.output.length === 0 ? "[terminal buffer overflowed; oldest output dropped]" : value.output + "\n[terminal buffer overflowed; oldest output dropped]")
|
|
367
|
+
: value.output
|
|
219
368
|
}]
|
|
220
369
|
},
|
|
221
370
|
async execute(args, exec) {
|
|
@@ -238,30 +387,46 @@ export function terminalTool(ctx, registry, paths, defaultShell) {
|
|
|
238
387
|
// buildEnv, not an inline duplicate: the msys2 backend needs its
|
|
239
388
|
// MSYSTEM=MINGW64 injection here too, or the login shell sources
|
|
240
389
|
// /etc/profile with the default MSYS environment and /mingw64/bin
|
|
241
|
-
// (gcc, make) never joins PATH.
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
390
|
+
// (gcc, make) never joins PATH. It also owns the WSL WSLENV layering,
|
|
391
|
+
// so a terminal session's env follows the shell tool exactly.
|
|
392
|
+
// process.env.WSLENV is passed explicitly: spawnTerminal replaces the
|
|
393
|
+
// child environment wholesale (childEnv(spec.env)), so the ambient
|
|
394
|
+
// allow-list is not otherwise visible and inherited entries such as
|
|
395
|
+
// WT_SESSION / WT_PROFILE_ID would be dropped.
|
|
245
396
|
const env = buildEnv(shell, ctx.shellEnv.collect(exec), process.env.WSLENV);
|
|
246
|
-
const session = await registry.open({ argv, shell, cwd, env, rows: DEFAULT_ROWS, cols: DEFAULT_COLS, distro: args.distro, initial: args.command !== undefined ? args.command + "\r" : undefined });
|
|
247
|
-
|
|
397
|
+
const session = await registry.open({ argv, shell, cwd, env, rows: DEFAULT_ROWS, cols: DEFAULT_COLS, distro: args.distro, initial: args.command !== undefined ? args.command + "\r" : undefined, idleMs: args.idleMs });
|
|
398
|
+
const jobs = ctx.get("jobs");
|
|
399
|
+
const jobId = jobs === undefined ? undefined : jobs.start({
|
|
400
|
+
kind: "terminal/session",
|
|
401
|
+
label: "terminal " + session.id.slice(0, 8) + " (" + shell + ")",
|
|
402
|
+
...(exec.agent ? { owner: exec.agent } : {}),
|
|
403
|
+
run: () => ({
|
|
404
|
+
cancel: () => { void session.handle.terminate().catch(() => { }); },
|
|
405
|
+
done: session.handle.done,
|
|
406
|
+
readOutput: () => registry.read(session.id).delta
|
|
407
|
+
})
|
|
408
|
+
});
|
|
409
|
+
return { kind: "open", sessionId: session.id, ...(jobId !== undefined ? { jobId } : {}), pid: session.handle.pid, shell, output: session.buffer.snapshot() };
|
|
248
410
|
}
|
|
249
411
|
case "send": {
|
|
250
|
-
const { delta } = await registry.send(args.sessionId, args.input);
|
|
251
|
-
return { kind: "session", output: delta };
|
|
412
|
+
const { delta, truncated } = await registry.send(args.sessionId, args.input);
|
|
413
|
+
return { kind: "session", output: delta, ...(truncated ? { truncated } : {}) };
|
|
252
414
|
}
|
|
253
415
|
case "read": {
|
|
254
|
-
const { delta } = registry.read(args.sessionId);
|
|
255
|
-
return { kind: "session", output: delta };
|
|
416
|
+
const { delta, truncated } = registry.read(args.sessionId);
|
|
417
|
+
return { kind: "session", output: delta, ...(truncated ? { truncated } : {}) };
|
|
256
418
|
}
|
|
257
419
|
case "signal": {
|
|
258
|
-
const { delta } = await registry.signal(args.sessionId, args.signal);
|
|
259
|
-
return { kind: "session", output: delta };
|
|
420
|
+
const { delta, truncated } = await registry.signal(args.sessionId, args.signal);
|
|
421
|
+
return { kind: "session", output: delta, ...(truncated ? { truncated } : {}) };
|
|
260
422
|
}
|
|
261
423
|
case "close": {
|
|
262
424
|
await registry.close(args.sessionId);
|
|
263
425
|
return { kind: "closed", sessionId: args.sessionId };
|
|
264
426
|
}
|
|
427
|
+
case "list": {
|
|
428
|
+
return { kind: "list", sessions: registry.list() };
|
|
429
|
+
}
|
|
265
430
|
}
|
|
266
431
|
},
|
|
267
432
|
presentCall: (args) => ({
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-bash-terminal-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"registry": "https://registry.npmjs.org",
|
|
6
|
+
"access": "public",
|
|
7
|
+
"tag": "dsh-0.1.5"
|
|
8
|
+
},
|
|
4
9
|
"description": "DSH plugin: one shell tool that runs commands through PowerShell, Git Bash, MSYS2, or WSL on Windows, with a user-chosen default terminal in the Web UI settings. TypeScript rewrite of MAXeaglet/dsh-bash-terminal with working MSYS2/MINGW64 support.",
|
|
5
10
|
"author": "drscrewdriver",
|
|
6
|
-
"contributors": [
|
|
7
|
-
"MAXeaglet (original dsh-bash-terminal)"
|
|
8
|
-
],
|
|
9
11
|
"license": "MIT",
|
|
10
12
|
"repository": {
|
|
11
13
|
"type": "git",
|
|
@@ -23,24 +25,19 @@
|
|
|
23
25
|
"shell",
|
|
24
26
|
"powershell",
|
|
25
27
|
"git-bash",
|
|
26
|
-
"msys2",
|
|
27
|
-
"mingw64",
|
|
28
28
|
"wsl",
|
|
29
29
|
"windows",
|
|
30
30
|
"typescript"
|
|
31
31
|
],
|
|
32
32
|
"engines": {
|
|
33
33
|
"node": ">=22",
|
|
34
|
-
"dsh": ">=0.1.
|
|
35
|
-
},
|
|
36
|
-
"publishConfig": {
|
|
37
|
-
"access": "public"
|
|
34
|
+
"dsh": ">=0.1.5-alpha.1 <0.2.0-0"
|
|
38
35
|
},
|
|
39
36
|
"type": "module",
|
|
40
37
|
"main": "lib/index.js",
|
|
41
38
|
"exports": {
|
|
42
39
|
".": "./lib/index.js",
|
|
43
|
-
"./client": "./
|
|
40
|
+
"./client": "./lib/client.js",
|
|
44
41
|
"./package.json": "./package.json"
|
|
45
42
|
},
|
|
46
43
|
"files": [
|
|
@@ -52,17 +49,15 @@
|
|
|
52
49
|
"tsconfig.client.json",
|
|
53
50
|
"tsconfig.test.json",
|
|
54
51
|
"README.md",
|
|
55
|
-
"README.en.md",
|
|
56
|
-
"README.ja.md",
|
|
57
|
-
"README.ko.md",
|
|
58
52
|
"LICENSE",
|
|
59
53
|
"cordis.patch.yml",
|
|
60
|
-
"dsh.plugin.json"
|
|
54
|
+
"dsh.plugin.json",
|
|
55
|
+
"screenshots.json"
|
|
61
56
|
],
|
|
62
57
|
"scripts": {
|
|
63
58
|
"build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json && node scripts/build-client.mjs && tsc -p tsconfig.test.json",
|
|
64
59
|
"build:client": "node scripts/build-client.mjs",
|
|
65
|
-
"test": "node test-dist/unit.js && node test-dist/apply.js && node test-dist/client.js"
|
|
60
|
+
"test": "node test-dist/unit.js && node test-dist/apply.js && node test-dist/client.js && node test-dist/terminal.js"
|
|
66
61
|
},
|
|
67
62
|
"dsh": {
|
|
68
63
|
"bundle": {
|
|
@@ -72,64 +67,50 @@
|
|
|
72
67
|
"platform": "web",
|
|
73
68
|
"immediately": true,
|
|
74
69
|
"inject": [
|
|
75
|
-
"@deepseek-ai/dsh-client-store",
|
|
76
70
|
"@deepseek-ai/dsh-client-locale",
|
|
77
71
|
"@deepseek-ai/dsh-client-ui-settings",
|
|
78
|
-
"@deepseek-ai/dsh-
|
|
72
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
79
73
|
]
|
|
80
74
|
}
|
|
81
75
|
},
|
|
82
76
|
"peerDependencies": {
|
|
83
77
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
84
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
85
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
86
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
87
|
-
"@deepseek-ai/dsh-shell": "^0.1.
|
|
88
|
-
"@deepseek-ai/dsh-
|
|
89
|
-
"@deepseek-ai/dsh-
|
|
90
|
-
"@deepseek-ai/dsh-client-
|
|
91
|
-
"@deepseek-ai/dsh-client-
|
|
92
|
-
"@deepseek-ai/dsh-client-ui-
|
|
93
|
-
"@deepseek-ai/dsh-client-ui-
|
|
94
|
-
"@deepseek-ai/dsh-
|
|
78
|
+
"@deepseek-ai/dsh-tools": "^0.1.5-rc.1",
|
|
79
|
+
"@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
|
|
80
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
|
|
81
|
+
"@deepseek-ai/dsh-shell": "^0.1.5-rc.1",
|
|
82
|
+
"@deepseek-ai/dsh-sandbox": "^0.1.5-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-timeout": "^0.1.5-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-client-store": "^0.1.5-rc.1",
|
|
85
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
|
|
86
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.1",
|
|
87
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
|
|
88
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.1",
|
|
95
89
|
"react": "^18.2.0"
|
|
96
90
|
},
|
|
97
|
-
"peerDependenciesMeta": {
|
|
98
|
-
"@deepseek-ai/cordis": { "optional": true },
|
|
99
|
-
"@deepseek-ai/dsh-tools": { "optional": true },
|
|
100
|
-
"@deepseek-ai/dsh-settings": { "optional": true },
|
|
101
|
-
"@deepseek-ai/dsh-llm": { "optional": true },
|
|
102
|
-
"@deepseek-ai/dsh-shell": { "optional": true },
|
|
103
|
-
"@deepseek-ai/dsh-timeout": { "optional": true },
|
|
104
|
-
"@deepseek-ai/dsh-client-store": { "optional": true },
|
|
105
|
-
"@deepseek-ai/dsh-client-locale": { "optional": true },
|
|
106
|
-
"@deepseek-ai/dsh-client-ui-settings": { "optional": true },
|
|
107
|
-
"@deepseek-ai/dsh-client-ui-slots": { "optional": true },
|
|
108
|
-
"@deepseek-ai/dsh-client-ui-primitives": { "optional": true },
|
|
109
|
-
"@deepseek-ai/dsh-sandbox": { "optional": true },
|
|
110
|
-
"react": { "optional": true }
|
|
111
|
-
},
|
|
112
91
|
"dependencies": {
|
|
113
|
-
"@deepseek-ai/schemastery": "^3.18.1"
|
|
92
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
93
|
+
"node-pty": "^1.1.0"
|
|
114
94
|
},
|
|
115
95
|
"devDependencies": {
|
|
116
96
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
117
|
-
"@deepseek-ai/dsh-client-locale": "
|
|
118
|
-
"@deepseek-ai/dsh-client-store": "
|
|
119
|
-
"@deepseek-ai/dsh-client-ui-primitives": "
|
|
120
|
-
"@deepseek-ai/dsh-client-ui-settings": "
|
|
121
|
-
"@deepseek-ai/dsh-client-ui-slots": "
|
|
122
|
-
"@deepseek-ai/dsh-llm": "
|
|
123
|
-
"@deepseek-ai/dsh-sandbox": "
|
|
124
|
-
"@deepseek-ai/dsh-settings": "
|
|
125
|
-
"@deepseek-ai/dsh-shell": "
|
|
126
|
-
"@deepseek-ai/dsh-timeout": "
|
|
127
|
-
"@deepseek-ai/dsh-tools": "
|
|
128
|
-
"@types/node": "^
|
|
97
|
+
"@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
|
|
98
|
+
"@deepseek-ai/dsh-client-store": "0.1.5-rc.2",
|
|
99
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2",
|
|
100
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2",
|
|
101
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2",
|
|
102
|
+
"@deepseek-ai/dsh-llm": "0.1.5-rc.2",
|
|
103
|
+
"@deepseek-ai/dsh-sandbox": "0.1.5-rc.2",
|
|
104
|
+
"@deepseek-ai/dsh-settings": "0.1.5-rc.2",
|
|
105
|
+
"@deepseek-ai/dsh-shell": "0.1.5-rc.2",
|
|
106
|
+
"@deepseek-ai/dsh-timeout": "0.1.5-rc.2",
|
|
107
|
+
"@deepseek-ai/dsh-tools": "0.1.5-rc.2",
|
|
108
|
+
"@types/node": "^22.10.0",
|
|
129
109
|
"@types/react": "^18.2.0",
|
|
130
110
|
"@types/react-dom": "^18.2.0",
|
|
131
111
|
"esbuild": "^0.28.2",
|
|
132
112
|
"react": "^18.2.0",
|
|
113
|
+
"react-dom": "^18.2.0",
|
|
133
114
|
"typescript": "^5.9.0"
|
|
134
115
|
}
|
|
135
116
|
}
|