@toddzheng024/dscode-bundle 0.3.0 → 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/THIRD_PARTY_NOTICES.md +15 -0
- package/cordis.patch.yml +5 -3
- package/package.json +14 -1
- package/plugins/clipboard-image/clipboard-image.swift +56 -0
- package/plugins/clipboard-image/index.mjs +70 -0
- package/plugins/code-review/git.mjs +95 -0
- package/plugins/code-review/index.mjs +88 -0
- package/plugins/dscode/index.mjs +17 -3
- package/plugins/email/cli.mjs +32 -0
- package/plugins/email/contacts.mjs +36 -0
- package/plugins/email/gmail-oauth.mjs +69 -0
- package/plugins/email/gmail-store.mjs +2 -0
- package/plugins/email/gmail.mjs +194 -0
- package/plugins/email/imap.mjs +136 -0
- package/plugins/email/inbox.d.mts +25 -0
- package/plugins/email/inbox.mjs +76 -0
- package/plugins/email/smtp.mjs +69 -0
- package/plugins/email/store.mjs +32 -0
- package/plugins/email-tools/index.mjs +44 -0
- package/plugins/session-metrics/index.mjs +6 -2
- package/plugins/session-metrics/rate.mjs +62 -0
- package/plugins/session-metrics/view.mjs +15 -9
- package/plugins/tui-tools/doctor-cli.mjs +22 -0
- package/plugins/tui-tools/doctor.mjs +141 -0
- package/plugins/tui-tools/index.mjs +14 -6
- package/plugins/ultra/policy.mjs +12 -1
- package/plugins/worktree-subagent/worktree.mjs +50 -0
- package/presets/dscode/agent.cordis.yml +7 -2
- package/vendor/deepseek/index.js +12 -2
- package/vendor/subagent/index.js +29 -9
- package/vendor/subagent-core/LICENSE +21 -0
- package/vendor/subagent-core/index.js +3232 -0
- package/vendor/subagent-core/invariant.js +76 -0
- package/vendor/subagent-core/typert.host.d.ts +3 -0
- package/vendor/subagent-core/typert.host.js +964 -0
- package/vendor/subagent-core/typert.remote-client.d.ts +27 -0
- package/vendor/subagent-core/typert.remote-client.js +160 -0
- package/vendor/subagent-core/types/assistant-output.d.ts +48 -0
- package/vendor/subagent-core/types/assistant-output.js +73 -0
- package/vendor/subagent-core/types/catalog.d.ts +77 -0
- package/vendor/subagent-core/types/catalog.js +103 -0
- package/vendor/subagent-core/types/child-agent.d.ts +150 -0
- package/vendor/subagent-core/types/child-agent.js +207 -0
- package/vendor/subagent-core/types/client.d.ts +8 -0
- package/vendor/subagent-core/types/client.js +7 -0
- package/vendor/subagent-core/types/continuation-activation.d.ts +251 -0
- package/vendor/subagent-core/types/continuation-activation.js +663 -0
- package/vendor/subagent-core/types/continuation-messages.d.ts +62 -0
- package/vendor/subagent-core/types/continuation-messages.js +102 -0
- package/vendor/subagent-core/types/continuation.d.ts +124 -0
- package/vendor/subagent-core/types/continuation.js +455 -0
- package/vendor/subagent-core/types/control-types.d.ts +146 -0
- package/vendor/subagent-core/types/control-types.js +9 -0
- package/vendor/subagent-core/types/control.d.ts +71 -0
- package/vendor/subagent-core/types/control.js +116 -0
- package/vendor/subagent-core/types/depth.d.ts +31 -0
- package/vendor/subagent-core/types/depth.js +39 -0
- package/vendor/subagent-core/types/descriptor.d.ts +144 -0
- package/vendor/subagent-core/types/descriptor.js +193 -0
- package/vendor/subagent-core/types/error.d.ts +11 -0
- package/vendor/subagent-core/types/error.js +14 -0
- package/vendor/subagent-core/types/inbox.d.ts +43 -0
- package/vendor/subagent-core/types/inbox.js +61 -0
- package/vendor/subagent-core/types/index.d.ts +316 -0
- package/vendor/subagent-core/types/index.js +511 -0
- package/vendor/subagent-core/types/internal.d.ts +58 -0
- package/vendor/subagent-core/types/internal.js +58 -0
- package/vendor/subagent-core/types/invariant.d.ts +13 -0
- package/vendor/subagent-core/types/invariant.js +91 -0
- package/vendor/subagent-core/types/lifecycle.d.ts +113 -0
- package/vendor/subagent-core/types/lifecycle.js +178 -0
- package/vendor/subagent-core/types/list-children.d.ts +65 -0
- package/vendor/subagent-core/types/list-children.js +381 -0
- package/vendor/subagent-core/types/out-of-process.d.ts +118 -0
- package/vendor/subagent-core/types/out-of-process.js +219 -0
- package/vendor/subagent-core/types/projection-types.d.ts +74 -0
- package/vendor/subagent-core/types/projection-types.js +7 -0
- package/vendor/subagent-core/types/projection.d.ts +95 -0
- package/vendor/subagent-core/types/projection.js +151 -0
- package/vendor/subagent-core/types/run-settlement.d.ts +17 -0
- package/vendor/subagent-core/types/run-settlement.js +70 -0
- package/vendor/subagent-core/types/types.d.ts +377 -0
- package/vendor/subagent-core/types/types.js +19 -0
- package/vendor/subagent-driver/LICENSE +21 -0
- package/vendor/subagent-driver/index.js +255 -0
- package/vendor/subagent-driver/types/index.d.ts +33 -0
- package/vendor/subagent-driver/types/structured.d.ts +42 -0
- package/vendor/subagent-fork/LICENSE +21 -0
- package/vendor/subagent-fork/index.js +61 -0
- package/vendor/subagent-fork/types/index.d.ts +20 -0
- package/vendor/subagent-spawn/LICENSE +21 -0
- package/vendor/subagent-spawn/index.js +45 -0
- package/vendor/subagent-spawn/types/index.d.ts +19 -0
- package/vendor/terminal/LICENSE +21 -0
- package/vendor/terminal/index.js +1013 -0
- package/vendor/terminal/types/config.d.ts +71 -0
- package/vendor/terminal/types/index.d.ts +36 -0
- package/vendor/terminal/types/sanitize.d.ts +47 -0
- package/vendor/terminal/types/session.d.ts +87 -0
- package/vendor/tui/dscode-clipboard-image/clipboard-image.swift +56 -0
- package/vendor/tui/dscode-clipboard-image/index.mjs +70 -0
- package/vendor/tui/dscode-email/cli.mjs +32 -0
- package/vendor/tui/dscode-email/contacts.mjs +36 -0
- package/vendor/tui/dscode-email/gmail-oauth.mjs +69 -0
- package/vendor/tui/dscode-email/gmail-store.mjs +2 -0
- package/vendor/tui/dscode-email/gmail.mjs +194 -0
- package/vendor/tui/dscode-email/imap.mjs +136 -0
- package/vendor/tui/dscode-email/inbox.d.mts +25 -0
- package/vendor/tui/dscode-email/inbox.mjs +76 -0
- package/vendor/tui/dscode-email/smtp.mjs +69 -0
- package/vendor/tui/dscode-email/store.mjs +32 -0
- package/vendor/tui/dscode-email.mjs +69 -0
- package/vendor/tui/index.mjs +473 -82
|
@@ -0,0 +1,1013 @@
|
|
|
1
|
+
// dscode-no-history-expansion-v1
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { TerminalBackendCleanupError, TerminalError } from "@deepseek-ai/dsh-terminal";
|
|
4
|
+
import { ENCODING_PREAMBLE, resolvePwshPath } from "@deepseek-ai/dsh-pwsh-local";
|
|
5
|
+
import z from "@deepseek-ai/schemastery";
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
//#region lib/types/config.js
|
|
8
|
+
/** Validated configuration for the local PTY backend. */
|
|
9
|
+
/** Bash dialect default executable. */
|
|
10
|
+
const DEFAULT_BASH_SHELL = "/bin/bash";
|
|
11
|
+
/** Bash dialect default arguments (interactive, profile-free). */
|
|
12
|
+
const DEFAULT_BASH_ARGS = [
|
|
13
|
+
"--noprofile",
|
|
14
|
+
"--norc",
|
|
15
|
+
"+H",
|
|
16
|
+
"-i"
|
|
17
|
+
];
|
|
18
|
+
/** Pwsh dialect default arguments (interactive host, profile-free). */
|
|
19
|
+
const DEFAULT_PWSH_ARGS = ["-NoLogo", "-NoProfile"];
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the effective per-dialect shell specification. Defaulting is this
|
|
22
|
+
* explicit step: an unset or empty `shellPath`/`shellArgs` selects the
|
|
23
|
+
* dialect's defaults, while a non-empty explicit value always wins.
|
|
24
|
+
* (Schemastery materializes an absent optional array as `[]`, so emptiness —
|
|
25
|
+
* not just `undefined` — means "dialect default".)
|
|
26
|
+
* @param config - Schemastery-resolved plugin configuration.
|
|
27
|
+
* @returns the fully resolved configuration.
|
|
28
|
+
*/
|
|
29
|
+
function resolveConfig(config) {
|
|
30
|
+
const shellDialect = config.shellDialect ?? "bash";
|
|
31
|
+
return {
|
|
32
|
+
...config,
|
|
33
|
+
shellDialect,
|
|
34
|
+
shellPath: config.shellPath !== void 0 && config.shellPath.length > 0 ? config.shellPath : shellDialect === "pwsh" ? resolvePwshPath() : DEFAULT_BASH_SHELL,
|
|
35
|
+
shellArgs: config.shellArgs !== void 0 && config.shellArgs.length > 0 ? config.shellArgs : shellDialect === "pwsh" ? DEFAULT_PWSH_ARGS : DEFAULT_BASH_ARGS
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Schemastery config exposed by the plugin. */
|
|
39
|
+
const Config = z.object({
|
|
40
|
+
backendType: z.string().default("shell"),
|
|
41
|
+
shellDialect: z.union(["bash", "pwsh"]).default("bash"),
|
|
42
|
+
shellPath: z.string().required(false),
|
|
43
|
+
shellArgs: z.array(z.string()).required(false),
|
|
44
|
+
rows: z.number().default(40),
|
|
45
|
+
cols: z.number().default(160),
|
|
46
|
+
scrollbackLines: z.number().default(1e4),
|
|
47
|
+
scrollbackMaxBytes: z.number().default(4 * 1024 * 1024),
|
|
48
|
+
maxReadBytes: z.number().default(256 * 1024),
|
|
49
|
+
pollIntervalMs: z.number().default(50),
|
|
50
|
+
exactProbeAfterMs: z.number().default(150),
|
|
51
|
+
idleSilenceMs: z.number().default(3e3),
|
|
52
|
+
handoffGraceMs: z.number().default(500),
|
|
53
|
+
timeoutMs: z.number().default(3e4),
|
|
54
|
+
disposeGraceMs: z.number().default(3e3)
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* Assert every effective numeric config field is a positive safe integer and bounds compose.
|
|
58
|
+
* @param config - Schemastery-resolved plugin configuration.
|
|
59
|
+
* @returns Narrows the input to the fully resolved configuration.
|
|
60
|
+
*/
|
|
61
|
+
function validateConfig(config) {
|
|
62
|
+
const resolved = config;
|
|
63
|
+
if (resolved.backendType.length === 0) throw new Error("terminal-bash: backendType must be non-empty");
|
|
64
|
+
if (resolved.shellPath.length === 0) throw new Error("terminal-bash: shellPath must be non-empty");
|
|
65
|
+
for (const [name, value] of Object.entries(resolved)) if (typeof value === "number" && (!Number.isSafeInteger(value) || value <= 0)) throw new Error(`terminal-bash: ${name} must be a positive safe integer`);
|
|
66
|
+
if (resolved.maxReadBytes > resolved.scrollbackMaxBytes) throw new Error("terminal-bash: maxReadBytes must not exceed scrollbackMaxBytes");
|
|
67
|
+
if (resolved.handoffGraceMs < resolved.pollIntervalMs) throw new Error("terminal-bash: handoffGraceMs must be at least pollIntervalMs so one readiness poll runs inside the grace window");
|
|
68
|
+
}
|
|
69
|
+
/** Exact printable prompt emitted after the private marker. */
|
|
70
|
+
const CONTROLLED_PROMPT = "dsh> ";
|
|
71
|
+
/**
|
|
72
|
+
* Remove CSI/OSC/short escape sequences while preserving split-sequence carry.
|
|
73
|
+
* Full terminal emulation is deliberately deferred; ordinary line output and
|
|
74
|
+
* the private prompt marker are the supported contract.
|
|
75
|
+
*/
|
|
76
|
+
var TerminalSanitizer = class {
|
|
77
|
+
maxPendingBytes;
|
|
78
|
+
pending = "";
|
|
79
|
+
discardMode;
|
|
80
|
+
discardOscEscape = false;
|
|
81
|
+
trailingCarriageReturn = false;
|
|
82
|
+
trackingPromptTail = false;
|
|
83
|
+
constructor(maxPendingBytes) {
|
|
84
|
+
this.maxPendingBytes = maxPendingBytes;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Consume one decoded `node-pty` data chunk.
|
|
88
|
+
* @param chunk - decoded terminal data.
|
|
89
|
+
* @returns Printable text and whether the private prompt marker completed.
|
|
90
|
+
*/
|
|
91
|
+
push(chunk) {
|
|
92
|
+
this.pending += this.discardPrefix(chunk);
|
|
93
|
+
let text = "";
|
|
94
|
+
let prompt = false;
|
|
95
|
+
let includePromptTail = this.trackingPromptTail;
|
|
96
|
+
let promptTail = "";
|
|
97
|
+
let index = 0;
|
|
98
|
+
const appendText = (value) => {
|
|
99
|
+
text += value;
|
|
100
|
+
if (this.trackingPromptTail) promptTail += value;
|
|
101
|
+
};
|
|
102
|
+
while (index < this.pending.length) {
|
|
103
|
+
const escape = this.pending.indexOf("\x1B", index);
|
|
104
|
+
if (escape < 0) {
|
|
105
|
+
appendText(this.pending.slice(index));
|
|
106
|
+
index = this.pending.length;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
appendText(this.pending.slice(index, escape));
|
|
110
|
+
if (escape + 1 >= this.pending.length) {
|
|
111
|
+
index = escape;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
const kind = this.pending[escape + 1];
|
|
115
|
+
if (kind === "]") {
|
|
116
|
+
const bel = this.pending.indexOf("\x07", escape + 2);
|
|
117
|
+
const stringTerminator = this.pending.indexOf("\x1B\\", escape + 2);
|
|
118
|
+
let end = -1;
|
|
119
|
+
if (bel >= 0 && stringTerminator >= 0) end = Math.min(bel + 1, stringTerminator + 2);
|
|
120
|
+
else if (bel >= 0) end = bel + 1;
|
|
121
|
+
else if (stringTerminator >= 0) end = stringTerminator + 2;
|
|
122
|
+
if (end < 0) {
|
|
123
|
+
index = escape;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
const terminatorBytes = this.pending[end - 1] === "\x07" ? 1 : 2;
|
|
127
|
+
if (this.pending.slice(escape + 2, end - terminatorBytes).startsWith("133;D;")) {
|
|
128
|
+
prompt = true;
|
|
129
|
+
this.trackingPromptTail = true;
|
|
130
|
+
includePromptTail = true;
|
|
131
|
+
promptTail = "";
|
|
132
|
+
}
|
|
133
|
+
index = end;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (kind === "[") {
|
|
137
|
+
let end = escape + 2;
|
|
138
|
+
while (end < this.pending.length) {
|
|
139
|
+
const code = this.pending.charCodeAt(end);
|
|
140
|
+
if (code >= 64 && code <= 126) break;
|
|
141
|
+
end += 1;
|
|
142
|
+
}
|
|
143
|
+
if (end >= this.pending.length) {
|
|
144
|
+
index = escape;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
index = end + 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
index = escape + 2;
|
|
151
|
+
}
|
|
152
|
+
this.pending = this.pending.slice(index);
|
|
153
|
+
this.enforcePendingBound();
|
|
154
|
+
return {
|
|
155
|
+
text: this.normalizeText(text),
|
|
156
|
+
prompt,
|
|
157
|
+
...includePromptTail ? { promptTail } : {}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Flush a trailing printable fragment when the PTY exits.
|
|
162
|
+
* @returns Remaining printable text; incomplete escapes are discarded.
|
|
163
|
+
*/
|
|
164
|
+
flush() {
|
|
165
|
+
const text = this.pending.startsWith("\x1B") ? "" : this.pending;
|
|
166
|
+
this.pending = "";
|
|
167
|
+
this.discardMode = void 0;
|
|
168
|
+
this.discardOscEscape = false;
|
|
169
|
+
this.trackingPromptTail = false;
|
|
170
|
+
const normalized = this.normalizeText(text);
|
|
171
|
+
if (!this.trailingCarriageReturn) return normalized;
|
|
172
|
+
this.trailingCarriageReturn = false;
|
|
173
|
+
return `${normalized}\n`;
|
|
174
|
+
}
|
|
175
|
+
normalizeText(text) {
|
|
176
|
+
let complete = this.trailingCarriageReturn ? `\r${text}` : text;
|
|
177
|
+
this.trailingCarriageReturn = false;
|
|
178
|
+
if (complete.endsWith("\r")) {
|
|
179
|
+
complete = complete.slice(0, -1);
|
|
180
|
+
this.trailingCarriageReturn = true;
|
|
181
|
+
}
|
|
182
|
+
return normalizeTerminalText(complete);
|
|
183
|
+
}
|
|
184
|
+
enforcePendingBound() {
|
|
185
|
+
if (Buffer.byteLength(this.pending) <= this.maxPendingBytes) return;
|
|
186
|
+
this.discardMode = this.pending[1] === "]" ? "osc" : "csi";
|
|
187
|
+
this.pending = "";
|
|
188
|
+
}
|
|
189
|
+
discardPrefix(chunk) {
|
|
190
|
+
if (this.discardMode === void 0) return chunk;
|
|
191
|
+
if (this.discardMode === "csi") {
|
|
192
|
+
for (let index = 0; index < chunk.length; index += 1) {
|
|
193
|
+
const code = chunk.charCodeAt(index);
|
|
194
|
+
if (code >= 64 && code <= 126) {
|
|
195
|
+
this.discardMode = void 0;
|
|
196
|
+
return chunk.slice(index + 1);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return "";
|
|
200
|
+
}
|
|
201
|
+
let index = 0;
|
|
202
|
+
if (this.discardOscEscape) {
|
|
203
|
+
this.discardOscEscape = false;
|
|
204
|
+
if (chunk.startsWith("\\")) {
|
|
205
|
+
this.discardMode = void 0;
|
|
206
|
+
return chunk.slice(1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
while (index < chunk.length) {
|
|
210
|
+
if (chunk[index] === "\x07") {
|
|
211
|
+
this.discardMode = void 0;
|
|
212
|
+
return chunk.slice(index + 1);
|
|
213
|
+
}
|
|
214
|
+
if (chunk[index] === "\x1B") {
|
|
215
|
+
if (chunk[index + 1] === "\\") {
|
|
216
|
+
this.discardMode = void 0;
|
|
217
|
+
return chunk.slice(index + 2);
|
|
218
|
+
}
|
|
219
|
+
if (index + 1 === chunk.length) this.discardOscEscape = true;
|
|
220
|
+
}
|
|
221
|
+
index += 1;
|
|
222
|
+
}
|
|
223
|
+
return "";
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Normalize CRLF and standalone carriage returns for line-oriented rendering.
|
|
228
|
+
* @param text - sanitized terminal text.
|
|
229
|
+
* @returns Line-normalized text with BEL removed.
|
|
230
|
+
*/
|
|
231
|
+
function normalizeTerminalText(text) {
|
|
232
|
+
return text.replaceAll("\r\n", "\n").replaceAll("\r", "\n").replaceAll("\x07", "");
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region lib/types/session.js
|
|
236
|
+
/** Persistent PTY session with bounded output, readiness, and terminal-protocol replies. */
|
|
237
|
+
const { Terminal: HeadlessTerminal } = createRequire(import.meta.url)("@xterm/headless");
|
|
238
|
+
function utf8Tail(text, maxBytes) {
|
|
239
|
+
if (Buffer.byteLength(text) <= maxBytes) return {
|
|
240
|
+
text,
|
|
241
|
+
truncated: false
|
|
242
|
+
};
|
|
243
|
+
const chars = Array.from(text);
|
|
244
|
+
let bytes = 0;
|
|
245
|
+
let start = chars.length;
|
|
246
|
+
while (start > 0) {
|
|
247
|
+
const next = Buffer.byteLength(chars[start - 1]);
|
|
248
|
+
if (bytes + next > maxBytes) break;
|
|
249
|
+
bytes += next;
|
|
250
|
+
start -= 1;
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
text: chars.slice(start).join(""),
|
|
254
|
+
truncated: true
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
var BoundedTextBuffer = class {
|
|
258
|
+
maxBytes;
|
|
259
|
+
maxLines;
|
|
260
|
+
value = "";
|
|
261
|
+
dropped = false;
|
|
262
|
+
constructor(maxBytes, maxLines) {
|
|
263
|
+
this.maxBytes = maxBytes;
|
|
264
|
+
this.maxLines = maxLines;
|
|
265
|
+
}
|
|
266
|
+
append(text) {
|
|
267
|
+
if (text.length === 0) return;
|
|
268
|
+
this.value += text;
|
|
269
|
+
if (this.maxLines !== void 0) {
|
|
270
|
+
const lines = this.value.split("\n");
|
|
271
|
+
if (lines.length > this.maxLines) {
|
|
272
|
+
this.value = lines.slice(lines.length - this.maxLines).join("\n");
|
|
273
|
+
this.dropped = true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const tail = utf8Tail(this.value, this.maxBytes);
|
|
277
|
+
this.value = tail.text;
|
|
278
|
+
this.dropped ||= tail.truncated;
|
|
279
|
+
}
|
|
280
|
+
consume() {
|
|
281
|
+
const delta = this.value;
|
|
282
|
+
const truncated = this.dropped;
|
|
283
|
+
this.value = "";
|
|
284
|
+
this.dropped = false;
|
|
285
|
+
return {
|
|
286
|
+
delta,
|
|
287
|
+
truncated
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
snapshot() {
|
|
291
|
+
return {
|
|
292
|
+
text: this.value,
|
|
293
|
+
truncated: this.dropped
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
var LocalSendOperation = class {
|
|
298
|
+
startedAt;
|
|
299
|
+
onCancel;
|
|
300
|
+
output;
|
|
301
|
+
promise;
|
|
302
|
+
finished = false;
|
|
303
|
+
cancellationRequested = false;
|
|
304
|
+
initialForegroundLeftWait;
|
|
305
|
+
initialForegroundPgid;
|
|
306
|
+
constructor(maxBytes, startedAt, onCancel) {
|
|
307
|
+
this.startedAt = startedAt;
|
|
308
|
+
this.onCancel = onCancel;
|
|
309
|
+
this.output = new BoundedTextBuffer(maxBytes);
|
|
310
|
+
this.promise = Promise.withResolvers();
|
|
311
|
+
this.initialForegroundLeftWait = true;
|
|
312
|
+
}
|
|
313
|
+
get done() {
|
|
314
|
+
return this.promise.promise;
|
|
315
|
+
}
|
|
316
|
+
get settled() {
|
|
317
|
+
return this.finished;
|
|
318
|
+
}
|
|
319
|
+
get cancelRequested() {
|
|
320
|
+
return this.cancellationRequested;
|
|
321
|
+
}
|
|
322
|
+
append(text) {
|
|
323
|
+
if (!this.finished) this.output.append(text);
|
|
324
|
+
}
|
|
325
|
+
settle(waitReason, sessionStatus, inheritedTruncation) {
|
|
326
|
+
if (this.finished) return;
|
|
327
|
+
this.finished = true;
|
|
328
|
+
const read = this.output.snapshot();
|
|
329
|
+
this.promise.resolve({
|
|
330
|
+
viewport: read.text,
|
|
331
|
+
waitReason,
|
|
332
|
+
sessionStatus,
|
|
333
|
+
truncated: read.truncated || inheritedTruncation
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
fail(error) {
|
|
337
|
+
if (this.finished) return;
|
|
338
|
+
this.finished = true;
|
|
339
|
+
this.promise.reject(error);
|
|
340
|
+
}
|
|
341
|
+
readOutput() {
|
|
342
|
+
return this.output.consume();
|
|
343
|
+
}
|
|
344
|
+
setInitialForeground(foreground) {
|
|
345
|
+
this.initialForegroundPgid = foreground?.processGroupId;
|
|
346
|
+
this.initialForegroundLeftWait = foreground?.inputWaiting !== true;
|
|
347
|
+
}
|
|
348
|
+
acceptsStdinWait(pgid, waiting) {
|
|
349
|
+
if (pgid !== this.initialForegroundPgid) return waiting;
|
|
350
|
+
if (!waiting) this.initialForegroundLeftWait = true;
|
|
351
|
+
return waiting && this.initialForegroundLeftWait;
|
|
352
|
+
}
|
|
353
|
+
cancel() {
|
|
354
|
+
if (this.finished) return false;
|
|
355
|
+
this.cancellationRequested = true;
|
|
356
|
+
this.onCancel();
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
/** Backend session wrapping one provider-owned terminal process. */
|
|
361
|
+
var LocalPtySession = class {
|
|
362
|
+
terminal;
|
|
363
|
+
config;
|
|
364
|
+
motd = "";
|
|
365
|
+
pid;
|
|
366
|
+
decoder = new TextDecoder();
|
|
367
|
+
/** Protocol state only; the sanitizer and bounded buffers own returned text. */
|
|
368
|
+
emulator;
|
|
369
|
+
emulatorData;
|
|
370
|
+
sanitizer;
|
|
371
|
+
scrollback;
|
|
372
|
+
outputEnded = Promise.withResolvers();
|
|
373
|
+
completion;
|
|
374
|
+
statusValue = { kind: "running" };
|
|
375
|
+
active;
|
|
376
|
+
activeTimer;
|
|
377
|
+
activeDeadlineTimer;
|
|
378
|
+
activeAbort;
|
|
379
|
+
interrupting;
|
|
380
|
+
activeWrite;
|
|
381
|
+
pollingReady;
|
|
382
|
+
polling = false;
|
|
383
|
+
promptSeen = false;
|
|
384
|
+
promptTextSeen = false;
|
|
385
|
+
promptTail = "";
|
|
386
|
+
shellPgid;
|
|
387
|
+
initializing = false;
|
|
388
|
+
lastOutputAt = Date.now();
|
|
389
|
+
closing = false;
|
|
390
|
+
closePromise;
|
|
391
|
+
transportFailure;
|
|
392
|
+
emulatorWrites = Promise.resolve();
|
|
393
|
+
emulatorWriteDone;
|
|
394
|
+
emulatorBuffer = "";
|
|
395
|
+
emulatorWriting = false;
|
|
396
|
+
responseWrites = Promise.resolve();
|
|
397
|
+
pendingResponseWrites = 0;
|
|
398
|
+
emulatorClosed = false;
|
|
399
|
+
constructor(terminal, config) {
|
|
400
|
+
this.terminal = terminal;
|
|
401
|
+
this.config = config;
|
|
402
|
+
this.pid = terminal.pid;
|
|
403
|
+
this.emulator = new HeadlessTerminal({
|
|
404
|
+
cols: config.cols,
|
|
405
|
+
rows: config.rows,
|
|
406
|
+
scrollback: 0
|
|
407
|
+
});
|
|
408
|
+
this.emulatorData = this.emulator.onData((data) => {
|
|
409
|
+
this.pendingResponseWrites += 1;
|
|
410
|
+
const response = this.responseWrites.then(async () => {
|
|
411
|
+
await this.terminal.write(data);
|
|
412
|
+
});
|
|
413
|
+
this.responseWrites = response.then(() => {
|
|
414
|
+
this.finishResponseWrite();
|
|
415
|
+
}, (error) => {
|
|
416
|
+
this.finishResponseWrite();
|
|
417
|
+
if (!this.emulatorClosed && !this.closing) this.onTransportFailure(error);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
this.sanitizer = new TerminalSanitizer(config.maxReadBytes);
|
|
421
|
+
this.scrollback = new BoundedTextBuffer(config.scrollbackMaxBytes, config.scrollbackLines);
|
|
422
|
+
terminal.output.on("data", this.onTerminalData);
|
|
423
|
+
terminal.output.once("end", this.onTerminalEnd);
|
|
424
|
+
terminal.output.once("error", this.onTerminalError);
|
|
425
|
+
this.completion = terminal.done.then((outcome) => this.onExit(outcome), (error) => {
|
|
426
|
+
this.onTransportFailure(error);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Capture startup output through the same readiness contract as later sends.
|
|
431
|
+
* @param signal - optional cancellation while the shell reaches its first prompt.
|
|
432
|
+
* @returns Resolves after startup readiness; rejects on exit or readiness timeout.
|
|
433
|
+
*/
|
|
434
|
+
async initialize(signal) {
|
|
435
|
+
this.initializing = true;
|
|
436
|
+
try {
|
|
437
|
+
const result = await this.startSend({
|
|
438
|
+
text: "",
|
|
439
|
+
submit: false,
|
|
440
|
+
...signal !== void 0 ? { signal } : {}
|
|
441
|
+
}).done;
|
|
442
|
+
if (result.waitReason === "session_exit") throw new Error("PTY shell exited during startup");
|
|
443
|
+
if (result.waitReason === "timeout") throw new Error("PTY shell did not reach readiness before startup timeout");
|
|
444
|
+
this.motd = result.viewport;
|
|
445
|
+
} catch (error) {
|
|
446
|
+
signal?.throwIfAborted();
|
|
447
|
+
throw error;
|
|
448
|
+
} finally {
|
|
449
|
+
this.initializing = false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
startSend(request) {
|
|
453
|
+
if (this.closing) throw new Error("PTY session is closing");
|
|
454
|
+
if (this.statusValue.kind === "exited") throw new Error("PTY session has exited");
|
|
455
|
+
if (this.active !== void 0) throw new TerminalError(`PTY session already has an active send${this.activeWrite !== void 0 ? " or draining provider write" : this.interrupting !== void 0 ? " or draining foreground interrupt" : ""}`, "SEND_ACTIVE");
|
|
456
|
+
if (request.signal?.aborted === true) throw new Error("PTY send aborted before write");
|
|
457
|
+
const operation = new LocalSendOperation(this.config.maxReadBytes, Date.now(), () => {
|
|
458
|
+
this.interrupt(operation);
|
|
459
|
+
});
|
|
460
|
+
this.active = operation;
|
|
461
|
+
this.resetReadinessEvidence();
|
|
462
|
+
if (request.signal !== void 0) {
|
|
463
|
+
const onAbort = () => {
|
|
464
|
+
operation.cancel();
|
|
465
|
+
};
|
|
466
|
+
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
467
|
+
this.activeAbort = () => request.signal?.removeEventListener("abort", onAbort);
|
|
468
|
+
}
|
|
469
|
+
this.activeDeadlineTimer = setTimeout(() => {
|
|
470
|
+
if (this.active === operation) this.settleActive("timeout", this.activeWrite !== void 0 || this.interrupting === operation || this.protocolWorkPending());
|
|
471
|
+
}, this.config.timeoutMs);
|
|
472
|
+
this.beginSend(operation, request);
|
|
473
|
+
return operation;
|
|
474
|
+
}
|
|
475
|
+
async beginSend(operation, request) {
|
|
476
|
+
let foreground;
|
|
477
|
+
try {
|
|
478
|
+
if (this.protocolWorkPending()) await this.drainTerminalProtocol();
|
|
479
|
+
const emulatorWrites = this.emulatorWrites;
|
|
480
|
+
const responseWrites = this.responseWrites;
|
|
481
|
+
foreground = await this.terminal.inspectForeground();
|
|
482
|
+
if (this.protocolStateChanged(emulatorWrites, responseWrites)) foreground = await this.inspectForegroundAfterProtocol();
|
|
483
|
+
} catch (error) {
|
|
484
|
+
if (this.protocolWorkPending()) await this.drainTerminalProtocol();
|
|
485
|
+
if (this.active === operation && !this.closing && this.interrupting !== operation) this.failActive(error);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
if (this.active !== operation || this.closing || this.interrupting === operation) return;
|
|
490
|
+
operation.setInitialForeground(foreground);
|
|
491
|
+
const input = `${request.text}${request.submit ? "\r" : ""}`;
|
|
492
|
+
if (input.length > 0 && !operation.cancelRequested) {
|
|
493
|
+
this.resetReadinessEvidence();
|
|
494
|
+
const write = this.terminal.write(input);
|
|
495
|
+
this.activeWrite = write.then(() => true, () => false);
|
|
496
|
+
try {
|
|
497
|
+
await write;
|
|
498
|
+
} finally {
|
|
499
|
+
this.activeWrite = void 0;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (operation.cancelRequested) return;
|
|
503
|
+
if (this.active === operation && operation.settled) {
|
|
504
|
+
this.releaseSettledActive();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (this.active === operation && !this.closing) {
|
|
508
|
+
this.pollingReady = operation;
|
|
509
|
+
this.schedulePoll(operation);
|
|
510
|
+
}
|
|
511
|
+
} catch (error) {
|
|
512
|
+
if (this.active === operation && !this.closing) if (operation.settled) this.releaseSettledActive();
|
|
513
|
+
else this.failActive(error);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
resetReadinessEvidence() {
|
|
517
|
+
this.lastOutputAt = Date.now();
|
|
518
|
+
this.promptSeen = false;
|
|
519
|
+
this.promptTextSeen = false;
|
|
520
|
+
this.promptTail = "";
|
|
521
|
+
}
|
|
522
|
+
read(request) {
|
|
523
|
+
const snapshot = this.scrollback.snapshot();
|
|
524
|
+
const lines = snapshot.text.split("\n");
|
|
525
|
+
const totalLines = snapshot.text.length === 0 ? 0 : lines.length;
|
|
526
|
+
const offset = request.offset ?? 0;
|
|
527
|
+
const count = request.count ?? 500;
|
|
528
|
+
if (!Number.isSafeInteger(offset) || offset < 0) throw new Error("PTY read offset must be a non-negative safe integer");
|
|
529
|
+
if (!Number.isSafeInteger(count) || count <= 0) throw new Error("PTY read count must be a positive safe integer");
|
|
530
|
+
if (offset >= totalLines) return {
|
|
531
|
+
text: "",
|
|
532
|
+
totalLines,
|
|
533
|
+
lineBegin: offset,
|
|
534
|
+
lineEnd: offset,
|
|
535
|
+
truncated: snapshot.truncated
|
|
536
|
+
};
|
|
537
|
+
const end = totalLines - offset;
|
|
538
|
+
const start = Math.max(0, end - count);
|
|
539
|
+
const bounded = utf8Tail(lines.slice(start, end).join("\n"), this.config.maxReadBytes);
|
|
540
|
+
const returnedLines = bounded.text.length === 0 ? 0 : bounded.text.split("\n").length;
|
|
541
|
+
return {
|
|
542
|
+
text: bounded.text,
|
|
543
|
+
totalLines,
|
|
544
|
+
lineBegin: offset,
|
|
545
|
+
lineEnd: offset + returnedLines,
|
|
546
|
+
truncated: snapshot.truncated || bounded.truncated
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
async signal(signal) {
|
|
550
|
+
if (this.closing) throw new Error("PTY session is closing");
|
|
551
|
+
return {
|
|
552
|
+
delivered: true,
|
|
553
|
+
targetPgid: await this.terminal.signalForeground(signal)
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
status() {
|
|
557
|
+
return this.statusValue;
|
|
558
|
+
}
|
|
559
|
+
close(reason) {
|
|
560
|
+
this.closing = true;
|
|
561
|
+
if (this.closePromise !== void 0) return this.closePromise;
|
|
562
|
+
const closing = this.closeOnce(reason).catch((error) => {
|
|
563
|
+
this.closePromise = void 0;
|
|
564
|
+
this.failActive(error);
|
|
565
|
+
throw error;
|
|
566
|
+
});
|
|
567
|
+
this.closePromise = closing;
|
|
568
|
+
return closing;
|
|
569
|
+
}
|
|
570
|
+
onTerminalData = (chunk) => {
|
|
571
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
572
|
+
const data = this.decoder.decode(bytes, { stream: true });
|
|
573
|
+
this.queueEmulatorData(data);
|
|
574
|
+
this.onData(data);
|
|
575
|
+
};
|
|
576
|
+
onTerminalEnd = () => {
|
|
577
|
+
this.onData(this.decoder.decode());
|
|
578
|
+
this.appendOutput(this.sanitizer.flush());
|
|
579
|
+
this.closeEmulator();
|
|
580
|
+
this.outputEnded.resolve();
|
|
581
|
+
};
|
|
582
|
+
onTerminalError = (error) => {
|
|
583
|
+
this.closeEmulator();
|
|
584
|
+
this.onTransportFailure(error);
|
|
585
|
+
this.outputEnded.resolve();
|
|
586
|
+
};
|
|
587
|
+
onData(data) {
|
|
588
|
+
const sanitized = this.sanitizer.push(data);
|
|
589
|
+
this.appendOutput(sanitized.text);
|
|
590
|
+
if (sanitized.prompt) {
|
|
591
|
+
this.promptSeen = true;
|
|
592
|
+
this.promptTail = "";
|
|
593
|
+
this.lastOutputAt = Date.now();
|
|
594
|
+
}
|
|
595
|
+
if (this.promptSeen && sanitized.promptTail !== void 0) {
|
|
596
|
+
const remaining = Math.max(0, 6 - this.promptTail.length);
|
|
597
|
+
this.promptTail += sanitized.promptTail.slice(0, remaining);
|
|
598
|
+
if (sanitized.promptTail.length > remaining) this.promptTail = `${CONTROLLED_PROMPT}\0`;
|
|
599
|
+
this.promptTextSeen = this.promptTail === CONTROLLED_PROMPT;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async onExit(outcome) {
|
|
603
|
+
await this.outputEnded.promise;
|
|
604
|
+
if (this.transportFailure !== void 0) return;
|
|
605
|
+
this.statusValue = {
|
|
606
|
+
kind: "exited",
|
|
607
|
+
exitCode: outcome.exitCode,
|
|
608
|
+
signal: outcome.signal
|
|
609
|
+
};
|
|
610
|
+
this.settleActive("session_exit");
|
|
611
|
+
}
|
|
612
|
+
onTransportFailure(error) {
|
|
613
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
614
|
+
this.transportFailure ??= failure;
|
|
615
|
+
this.statusValue = {
|
|
616
|
+
kind: "exited",
|
|
617
|
+
exitCode: null,
|
|
618
|
+
signal: null
|
|
619
|
+
};
|
|
620
|
+
this.closeEmulator();
|
|
621
|
+
this.failActive(failure);
|
|
622
|
+
this.terminal.terminate().catch(() => {});
|
|
623
|
+
}
|
|
624
|
+
appendOutput(text) {
|
|
625
|
+
if (text.length === 0) return;
|
|
626
|
+
this.lastOutputAt = Date.now();
|
|
627
|
+
this.scrollback.append(text);
|
|
628
|
+
this.active?.append(text);
|
|
629
|
+
}
|
|
630
|
+
schedulePoll(operation, delayMs = this.config.pollIntervalMs) {
|
|
631
|
+
if (this.active !== operation || this.interrupting === operation || this.polling) return;
|
|
632
|
+
if (this.activeTimer !== void 0) clearTimeout(this.activeTimer);
|
|
633
|
+
this.activeTimer = setTimeout(() => {
|
|
634
|
+
this.activeTimer = void 0;
|
|
635
|
+
this.pollReadiness(operation);
|
|
636
|
+
}, delayMs);
|
|
637
|
+
}
|
|
638
|
+
async pollReadiness(operation) {
|
|
639
|
+
if (this.active !== operation || this.polling) return;
|
|
640
|
+
this.polling = true;
|
|
641
|
+
try {
|
|
642
|
+
if (this.statusValue.kind === "exited") {
|
|
643
|
+
this.settleActive("session_exit");
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (this.protocolWorkPending()) await this.drainTerminalProtocol();
|
|
647
|
+
const emulatorWrites = this.emulatorWrites;
|
|
648
|
+
const responseWrites = this.responseWrites;
|
|
649
|
+
let foreground = await this.terminal.inspectForeground();
|
|
650
|
+
if (this.protocolStateChanged(emulatorWrites, responseWrites)) foreground = await this.inspectForegroundAfterProtocol();
|
|
651
|
+
if (this.active !== operation || this.closing || this.interrupting === operation) return;
|
|
652
|
+
const idleFor = Date.now() - this.lastOutputAt;
|
|
653
|
+
if (this.promptSeen && foreground !== void 0 && this.shellPgid === void 0) this.shellPgid = foreground.processGroupId;
|
|
654
|
+
if (this.promptSeen && this.promptTextSeen && idleFor >= this.config.pollIntervalMs && foreground?.processGroupId === this.shellPgid) {
|
|
655
|
+
this.settleActive("stdin_read");
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const elapsed = Date.now() - operation.startedAt;
|
|
659
|
+
const startupHasOutput = !this.initializing || this.scrollback.snapshot().text.length > 0;
|
|
660
|
+
const acceptsStdinWait = startupHasOutput && foreground !== void 0 && operation.acceptsStdinWait(foreground.processGroupId, foreground.inputWaiting);
|
|
661
|
+
if (elapsed >= this.config.exactProbeAfterMs && acceptsStdinWait) {
|
|
662
|
+
this.settleActive("stdin_read");
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const handoffGrace = this.promptSeen ? this.config.handoffGraceMs : 0;
|
|
666
|
+
if (startupHasOutput && idleFor >= this.config.idleSilenceMs + handoffGrace) this.settleActive("inferred_idle");
|
|
667
|
+
} catch (error) {
|
|
668
|
+
if (this.protocolWorkPending()) await this.drainTerminalProtocol();
|
|
669
|
+
if (this.active === operation && !this.closing && this.interrupting !== operation) this.failActive(error);
|
|
670
|
+
} finally {
|
|
671
|
+
this.polling = false;
|
|
672
|
+
const active = this.active;
|
|
673
|
+
if (active !== void 0 && this.pollingReady === active) this.schedulePoll(active);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/** Wait until generated replies reach the provider before another send can publish. */
|
|
677
|
+
async drainTerminalProtocol() {
|
|
678
|
+
for (;;) {
|
|
679
|
+
const emulatorWrites = this.emulatorWrites;
|
|
680
|
+
await emulatorWrites;
|
|
681
|
+
const responseWrites = this.responseWrites;
|
|
682
|
+
await responseWrites;
|
|
683
|
+
if (emulatorWrites === this.emulatorWrites && responseWrites === this.responseWrites && !this.protocolWorkPending()) return;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
/** Sample foreground state only after protocol replies are quiet for the entire inspection. */
|
|
687
|
+
async inspectForegroundAfterProtocol() {
|
|
688
|
+
for (;;) {
|
|
689
|
+
if (this.protocolWorkPending()) await this.drainTerminalProtocol();
|
|
690
|
+
const emulatorWrites = this.emulatorWrites;
|
|
691
|
+
const responseWrites = this.responseWrites;
|
|
692
|
+
const foreground = await this.terminal.inspectForeground();
|
|
693
|
+
if (!this.protocolStateChanged(emulatorWrites, responseWrites)) return foreground;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
protocolStateChanged(emulatorWrites, responseWrites) {
|
|
697
|
+
return emulatorWrites !== this.emulatorWrites || responseWrites !== this.responseWrites || this.protocolWorkPending();
|
|
698
|
+
}
|
|
699
|
+
protocolWorkPending() {
|
|
700
|
+
return this.emulatorWriteDone !== void 0 || this.pendingResponseWrites > 0;
|
|
701
|
+
}
|
|
702
|
+
queueEmulatorData(data) {
|
|
703
|
+
if (this.emulatorClosed) return;
|
|
704
|
+
this.emulatorBuffer += data;
|
|
705
|
+
if (this.emulatorWriteDone === void 0) {
|
|
706
|
+
const idle = Promise.withResolvers();
|
|
707
|
+
this.emulatorWrites = idle.promise;
|
|
708
|
+
this.emulatorWriteDone = () => {
|
|
709
|
+
idle.resolve(void 0);
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
this.pumpEmulator();
|
|
713
|
+
}
|
|
714
|
+
pumpEmulator() {
|
|
715
|
+
if (this.emulatorWriting || this.emulatorClosed) return;
|
|
716
|
+
if (this.emulatorBuffer.length === 0) {
|
|
717
|
+
const done = this.emulatorWriteDone;
|
|
718
|
+
this.emulatorWriteDone = void 0;
|
|
719
|
+
done?.();
|
|
720
|
+
this.releaseSettledActive();
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const data = this.emulatorBuffer;
|
|
724
|
+
this.emulatorBuffer = "";
|
|
725
|
+
this.emulatorWriting = true;
|
|
726
|
+
try {
|
|
727
|
+
this.emulator.write(data, () => {
|
|
728
|
+
this.emulatorWriting = false;
|
|
729
|
+
this.pumpEmulator();
|
|
730
|
+
});
|
|
731
|
+
} catch (error) {
|
|
732
|
+
this.emulatorWriting = false;
|
|
733
|
+
this.emulatorBuffer = "";
|
|
734
|
+
const done = this.emulatorWriteDone;
|
|
735
|
+
this.emulatorWriteDone = void 0;
|
|
736
|
+
done?.();
|
|
737
|
+
this.releaseSettledActive();
|
|
738
|
+
if (!this.closing) this.onTransportFailure(error);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
finishResponseWrite() {
|
|
742
|
+
this.pendingResponseWrites -= 1;
|
|
743
|
+
this.releaseSettledActive();
|
|
744
|
+
}
|
|
745
|
+
releaseSettledActive() {
|
|
746
|
+
const operation = this.active;
|
|
747
|
+
if (operation === void 0 || !operation.settled || this.activeWrite !== void 0 || this.interrupting === operation || this.protocolWorkPending()) return;
|
|
748
|
+
this.clearActive();
|
|
749
|
+
}
|
|
750
|
+
closeEmulator() {
|
|
751
|
+
if (this.emulatorClosed) return;
|
|
752
|
+
this.emulatorClosed = true;
|
|
753
|
+
this.emulatorBuffer = "";
|
|
754
|
+
this.emulatorWriting = false;
|
|
755
|
+
const done = this.emulatorWriteDone;
|
|
756
|
+
this.emulatorWriteDone = void 0;
|
|
757
|
+
done?.();
|
|
758
|
+
this.emulatorData.dispose();
|
|
759
|
+
this.emulator.dispose();
|
|
760
|
+
}
|
|
761
|
+
settleActive(waitReason, retainOwnership = false) {
|
|
762
|
+
const operation = this.active;
|
|
763
|
+
if (operation === void 0) return;
|
|
764
|
+
const scrollbackTruncated = this.scrollback.snapshot().truncated;
|
|
765
|
+
if (retainOwnership) {
|
|
766
|
+
this.stopPolling();
|
|
767
|
+
this.activeAbort?.();
|
|
768
|
+
this.activeAbort = void 0;
|
|
769
|
+
} else this.clearActive();
|
|
770
|
+
operation.settle(waitReason, this.statusValue, scrollbackTruncated);
|
|
771
|
+
}
|
|
772
|
+
stopPolling() {
|
|
773
|
+
this.stopReadinessPolling();
|
|
774
|
+
if (this.activeDeadlineTimer !== void 0) clearTimeout(this.activeDeadlineTimer);
|
|
775
|
+
this.activeDeadlineTimer = void 0;
|
|
776
|
+
}
|
|
777
|
+
stopReadinessPolling() {
|
|
778
|
+
if (this.activeTimer !== void 0) clearTimeout(this.activeTimer);
|
|
779
|
+
this.activeTimer = void 0;
|
|
780
|
+
this.pollingReady = void 0;
|
|
781
|
+
}
|
|
782
|
+
clearActive() {
|
|
783
|
+
const operation = this.active;
|
|
784
|
+
this.stopPolling();
|
|
785
|
+
this.activeAbort?.();
|
|
786
|
+
this.activeAbort = void 0;
|
|
787
|
+
if (this.interrupting === operation) this.interrupting = void 0;
|
|
788
|
+
this.pollingReady = void 0;
|
|
789
|
+
this.active = void 0;
|
|
790
|
+
}
|
|
791
|
+
failActive(error) {
|
|
792
|
+
const operation = this.active;
|
|
793
|
+
if (operation === void 0) return;
|
|
794
|
+
this.clearActive();
|
|
795
|
+
operation.fail(error);
|
|
796
|
+
}
|
|
797
|
+
interrupt(operation) {
|
|
798
|
+
if (this.active !== operation) return;
|
|
799
|
+
this.interrupting = operation;
|
|
800
|
+
this.stopReadinessPolling();
|
|
801
|
+
this.interruptOnce(operation);
|
|
802
|
+
}
|
|
803
|
+
async interruptOnce(operation) {
|
|
804
|
+
try {
|
|
805
|
+
const activeWrite = this.activeWrite;
|
|
806
|
+
if (activeWrite !== void 0 && !await activeWrite) return;
|
|
807
|
+
await this.terminal.signalForeground("SIGINT");
|
|
808
|
+
} catch (error) {
|
|
809
|
+
if (this.active === operation && !this.closing) this.onTransportFailure(error);
|
|
810
|
+
return;
|
|
811
|
+
} finally {
|
|
812
|
+
if (this.interrupting === operation) this.interrupting = void 0;
|
|
813
|
+
}
|
|
814
|
+
if (this.active === operation && operation.settled) this.releaseSettledActive();
|
|
815
|
+
else if (this.active === operation && !this.closing) {
|
|
816
|
+
this.pollingReady = operation;
|
|
817
|
+
this.schedulePoll(operation, 0);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async closeOnce(reason) {
|
|
821
|
+
this.stopPolling();
|
|
822
|
+
this.closeEmulator();
|
|
823
|
+
try {
|
|
824
|
+
await this.terminal.terminate();
|
|
825
|
+
} catch (error) {
|
|
826
|
+
throw new Error(`PTY cleanup failed (${reason})`, { cause: error });
|
|
827
|
+
}
|
|
828
|
+
this.settleActive("session_exit");
|
|
829
|
+
await this.completion;
|
|
830
|
+
this.terminal.output.off("data", this.onTerminalData);
|
|
831
|
+
this.terminal.output.off("end", this.onTerminalEnd);
|
|
832
|
+
this.terminal.output.off("error", this.onTerminalError);
|
|
833
|
+
if (this.transportFailure !== void 0) throw this.transportFailure;
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
//#endregion
|
|
837
|
+
//#region lib/types/index.js
|
|
838
|
+
/**
|
|
839
|
+
* Persistent shell PTY backend over the subprocess terminal primitive, shared
|
|
840
|
+
* sandbox policy, bounded output, and provider-owned session cleanup.
|
|
841
|
+
* @module @deepseek-ai/dsh-terminal-bash
|
|
842
|
+
*/
|
|
843
|
+
/** Cordis plugin name. */
|
|
844
|
+
const name = "terminal-bash";
|
|
845
|
+
/** Required services: terminal registry, shared confinement policy, projection registry, and process substrate. */
|
|
846
|
+
const inject = [
|
|
847
|
+
"terminals",
|
|
848
|
+
"sandboxPolicy",
|
|
849
|
+
"sessionProjections",
|
|
850
|
+
"subprocess"
|
|
851
|
+
];
|
|
852
|
+
const sandboxModeFences = /* @__PURE__ */ new WeakMap();
|
|
853
|
+
function ensureSandboxModeFence(ctx, owner) {
|
|
854
|
+
const existing = sandboxModeFences.get(owner);
|
|
855
|
+
if (existing !== void 0) {
|
|
856
|
+
existing.pty = ctx.terminals;
|
|
857
|
+
existing.sandboxPolicy = ctx.sandboxPolicy;
|
|
858
|
+
existing.sessionProjections = ctx.sessionProjections;
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const state = {
|
|
862
|
+
pty: ctx.terminals,
|
|
863
|
+
sandboxPolicy: ctx.sandboxPolicy,
|
|
864
|
+
sessionProjections: ctx.sessionProjections
|
|
865
|
+
};
|
|
866
|
+
sandboxModeFences.set(owner, state);
|
|
867
|
+
owner.ctx.on("internal/dispatch", (_mode, eventName, args) => {
|
|
868
|
+
if (eventName !== "session/event") return;
|
|
869
|
+
const [session, event] = args;
|
|
870
|
+
if (session !== owner.session || event.type !== "sandbox/mode") return;
|
|
871
|
+
const currentMode = state.sessionProjections.stateOf(session, "sandboxMode") ?? null ?? state.sandboxPolicy.defaultMode;
|
|
872
|
+
if (event.data.mode === currentMode || !state.pty.hasOwnerActivity(owner)) return;
|
|
873
|
+
throw new Error(`cannot change sandbox mode from "${currentMode}" to "${event.data.mode}" while persistent terminal sessions are open or being created; wait for creation to settle and close them first`);
|
|
874
|
+
}, { global: true });
|
|
875
|
+
}
|
|
876
|
+
function childEnvironment(spec, dialect) {
|
|
877
|
+
const common = {
|
|
878
|
+
TERM: "dumb",
|
|
879
|
+
PAGER: "cat",
|
|
880
|
+
GIT_PAGER: "cat",
|
|
881
|
+
DSH_SHELL: "1",
|
|
882
|
+
DSH_SESSION_ID: spec.owner.id,
|
|
883
|
+
DSH_PTY_SESSION_ID: spec.sessionId
|
|
884
|
+
};
|
|
885
|
+
if (dialect === "pwsh") return {
|
|
886
|
+
...common,
|
|
887
|
+
NO_COLOR: "1"
|
|
888
|
+
};
|
|
889
|
+
return {
|
|
890
|
+
...common,
|
|
891
|
+
PS1: CONTROLLED_PROMPT,
|
|
892
|
+
PROMPT_COMMAND: `printf "\\033]133;D;%s\\007" "$?"; PS1='${CONTROLLED_PROMPT}'`,
|
|
893
|
+
BASH_SILENCE_DEPRECATION_WARNING: "1"
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* The pwsh prompt function that emits the shared OSC `133;D;` + BEL marker
|
|
898
|
+
* before every prompt, mirroring bash's PROMPT_COMMAND. `[char]27`/`[char]7`
|
|
899
|
+
* build the control bytes at runtime because raw ESC characters in submitted
|
|
900
|
+
* input are unreliable under PSReadLine.
|
|
901
|
+
*/
|
|
902
|
+
const PWSH_PROMPT_SETUP = "function prompt { [Console]::Write([char]27 + ']133;D;' + [int]$LASTEXITCODE + [char]7); 'dsh> ' }";
|
|
903
|
+
function spawnArgv(ctx, config, policy) {
|
|
904
|
+
const argv = [config.shellPath, ...config.shellArgs];
|
|
905
|
+
if (policy.mode === "danger-full-access") return argv;
|
|
906
|
+
const sandbox = ctx.get("sandbox");
|
|
907
|
+
if (sandbox === void 0) throw new Error(`terminal-bash: sandbox mode "${policy.mode}" requires a ctx.sandbox provider in the execution world`);
|
|
908
|
+
return sandbox.confine(argv, {
|
|
909
|
+
...policy,
|
|
910
|
+
mode: policy.mode
|
|
911
|
+
}).argv;
|
|
912
|
+
}
|
|
913
|
+
async function startupSession(session, dialect, timeoutMs, signal) {
|
|
914
|
+
let startupOperation;
|
|
915
|
+
const start = async () => {
|
|
916
|
+
if (dialect === "bash") {
|
|
917
|
+
await session.initialize(signal);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
let viewport = "";
|
|
921
|
+
for (;;) {
|
|
922
|
+
const first = viewport.length === 0;
|
|
923
|
+
startupOperation = session.startSend({
|
|
924
|
+
text: first ? ENCODING_PREAMBLE + PWSH_PROMPT_SETUP : "",
|
|
925
|
+
submit: first,
|
|
926
|
+
...signal !== void 0 ? { signal } : {}
|
|
927
|
+
});
|
|
928
|
+
const result = await startupOperation.done;
|
|
929
|
+
if (result.waitReason === "session_exit") throw new Error("PTY shell exited during startup");
|
|
930
|
+
if (result.waitReason === "timeout") throw new Error("PTY shell did not reach readiness before startup timeout");
|
|
931
|
+
viewport = result.viewport;
|
|
932
|
+
if (result.waitReason === "stdin_read") break;
|
|
933
|
+
}
|
|
934
|
+
session.motd = viewport;
|
|
935
|
+
};
|
|
936
|
+
const races = [];
|
|
937
|
+
let onAbort;
|
|
938
|
+
if (signal !== void 0) {
|
|
939
|
+
const aborted = Promise.withResolvers();
|
|
940
|
+
onAbort = () => {
|
|
941
|
+
aborted.reject(signal.reason);
|
|
942
|
+
};
|
|
943
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
944
|
+
races.push(aborted.promise);
|
|
945
|
+
}
|
|
946
|
+
let deadlineTimer;
|
|
947
|
+
if (dialect === "pwsh") {
|
|
948
|
+
const deadline = Promise.withResolvers();
|
|
949
|
+
deadlineTimer = setTimeout(() => {
|
|
950
|
+
startupOperation?.cancel();
|
|
951
|
+
deadline.reject(/* @__PURE__ */ new Error("PTY shell did not reach readiness before startup timeout"));
|
|
952
|
+
}, timeoutMs);
|
|
953
|
+
races.push(deadline.promise);
|
|
954
|
+
}
|
|
955
|
+
try {
|
|
956
|
+
signal?.throwIfAborted();
|
|
957
|
+
await Promise.race([start(), ...races]);
|
|
958
|
+
} finally {
|
|
959
|
+
if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
|
|
960
|
+
if (signal !== void 0 && onAbort !== void 0) signal.removeEventListener("abort", onAbort);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
/** Local shell backend registered under the configured type. */
|
|
964
|
+
var BashTerminalBackend = class {
|
|
965
|
+
ctx;
|
|
966
|
+
config;
|
|
967
|
+
spawnTerminal;
|
|
968
|
+
createSession;
|
|
969
|
+
type;
|
|
970
|
+
constructor(ctx, config, spawnTerminal = (spec) => ctx.subprocess.spawnTerminal(spec), createSession = (terminal, config) => new LocalPtySession(terminal, config)) {
|
|
971
|
+
this.ctx = ctx;
|
|
972
|
+
this.config = config;
|
|
973
|
+
this.spawnTerminal = spawnTerminal;
|
|
974
|
+
this.createSession = createSession;
|
|
975
|
+
this.type = config.backendType;
|
|
976
|
+
}
|
|
977
|
+
async spawn(spec) {
|
|
978
|
+
spec.signal?.throwIfAborted();
|
|
979
|
+
ensureSandboxModeFence(this.ctx, spec.owner);
|
|
980
|
+
const policy = this.ctx.sandboxPolicy.resolve({ session: spec.owner.session });
|
|
981
|
+
const argv = spawnArgv(this.ctx, this.config, policy);
|
|
982
|
+
if (argv[0] === void 0) throw new Error("terminal-bash: sandbox returned empty argv");
|
|
983
|
+
const terminal = await this.spawnTerminal({
|
|
984
|
+
argv,
|
|
985
|
+
cwd: spec.cwd ?? policy.workspaceRoot,
|
|
986
|
+
env: childEnvironment(spec, this.config.shellDialect),
|
|
987
|
+
rows: this.config.rows,
|
|
988
|
+
cols: this.config.cols,
|
|
989
|
+
graceMs: this.config.disposeGraceMs,
|
|
990
|
+
signal: spec.signal
|
|
991
|
+
});
|
|
992
|
+
const session = this.createSession(terminal, this.config);
|
|
993
|
+
try {
|
|
994
|
+
await startupSession(session, this.config.shellDialect, this.config.timeoutMs, spec.signal);
|
|
995
|
+
return session;
|
|
996
|
+
} catch (error) {
|
|
997
|
+
try {
|
|
998
|
+
await session.close("PTY startup failed");
|
|
999
|
+
} catch (closeError) {
|
|
1000
|
+
throw new TerminalBackendCleanupError(error, closeError);
|
|
1001
|
+
}
|
|
1002
|
+
throw error;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
/** Register the local PTY backend. */
|
|
1007
|
+
function apply(ctx, config) {
|
|
1008
|
+
const resolved = resolveConfig(config);
|
|
1009
|
+
validateConfig(resolved);
|
|
1010
|
+
ctx.terminals.registerBackend(new BashTerminalBackend(ctx, resolved));
|
|
1011
|
+
}
|
|
1012
|
+
//#endregion
|
|
1013
|
+
export { BashTerminalBackend, Config, PWSH_PROMPT_SETUP, apply, inject, name };
|